From 0e979b37b3d961e1e9405c018e65d21cd5127add Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 28 Jan 2013 09:25:50 -0800 Subject: [PATCH 001/330] Add milliseconds to Message Decoder Output Messages arrive very very fast. Adding milliseconds to the print log helps debug what is going on. --- lib/Insteon_PLM.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index ac24168e5..267721fc6 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -309,7 +309,7 @@ sub _send_cmd { } &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $main::Debug{insteon} >= 3; - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4: " . substr(&main::get_tickcount-1,-6) . "\n" .Insteon::MessageDecoder::plm_decode($command)) if $main::Debug{insteon} >= 4; my $data = pack("H*",$command); $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; @@ -343,7 +343,7 @@ sub _parse_data { } &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $main::Debug{insteon} >= 3; - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($data)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4: ". substr(&main::get_tickcount-1,-6) . "\n" .Insteon::MessageDecoder::plm_decode($data)) if $main::Debug{insteon} >= 4; # begin by pulling out any PLM ack/nacks my $prev_cmd = ''; @@ -707,4 +707,4 @@ sub firmware { } -1; \ No newline at end of file +1; From f92fc5fc2c1fae669823d715d799e4bf350daba0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 6 Feb 2013 18:55:35 -0800 Subject: [PATCH 002/330] Fix decimal places in milliseconds Trailing zeros were previously omitted. Printing the milliseconds helps a lot in debugging corrupt messages --- lib/Insteon_PLM.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 267721fc6..2e96cd5e0 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -309,7 +309,7 @@ sub _send_cmd { } &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $main::Debug{insteon} >= 3; - &::print_log( "[Insteon_PLM] DEBUG4: " . substr(&main::get_tickcount-1,-6) . "\n" .Insteon::MessageDecoder::plm_decode($command)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4: Milliseconds " . substr(sprintf('%.2f', &main::get_tickcount),-6) . "\n" .Insteon::MessageDecoder::plm_decode($command)) if $main::Debug{insteon} >= 4; my $data = pack("H*",$command); $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; @@ -343,7 +343,7 @@ sub _parse_data { } &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $main::Debug{insteon} >= 3; - &::print_log( "[Insteon_PLM] DEBUG4: ". substr(&main::get_tickcount-1,-6) . "\n" .Insteon::MessageDecoder::plm_decode($data)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4: Milliseconds ". substr(sprintf('%.2f', &main::get_tickcount),-6) . "\n" .Insteon::MessageDecoder::plm_decode($data)) if $main::Debug{insteon} >= 4; # begin by pulling out any PLM ack/nacks my $prev_cmd = ''; From d1a3c3c01d79b56a3779a1a539906bb77b14e6dd Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 12:22:58 -0800 Subject: [PATCH 003/330] Move Old Insteon Thermostat Code to Insteon lib --- lib/{Insteon_Thermostat.pm => Insteon/Thermostat.pm} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/{Insteon_Thermostat.pm => Insteon/Thermostat.pm} (100%) diff --git a/lib/Insteon_Thermostat.pm b/lib/Insteon/Thermostat.pm similarity index 100% rename from lib/Insteon_Thermostat.pm rename to lib/Insteon/Thermostat.pm From abf31522940968835434db38749cbe246afa778f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 12:40:05 -0800 Subject: [PATCH 004/330] Recode Initialization of Inteon Thermostat Object --- lib/Insteon/Thermostat.pm | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index bd4885e49..d5eb0ce1e 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -10,7 +10,7 @@ In user code: In items.mht: -IPLT, 12.34.56, thermostat, HVAC, plm +INSTEON_THERMOSTAT, 12.34.56, thermostat, HVAC BUGS @@ -94,20 +94,21 @@ All of the functions available: - Manage aldb - should be able to adjust setpoints based on plm scene. <- may be overkill =cut -use strict; +package Insteon::Thermostat; -package Insteon_Thermostat; +use strict; +use Insteon::BaseInsteon; -@Insteon_Thermostat::ISA = ('Insteon_Device'); +@Insteon::Thermostat::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); # -------------------- START OF SUBROUTINES -------------------- # -------------------------------------------------------------- sub new { - my ($class, $p_interface, $p_deviceid, $p_devcat) = @_; + my ($class, $p_deviceid, $p_interface) = @_; - my $self = $class->SUPER::new($p_interface, $p_deviceid, $p_devcat); + my $self = new Insteon::BaseDevice($p_deviceid,$p_interface) bless $self, $class; $$self{temp} = undef; $$self{mode} = undef; From 364c9324c5fac1b431f01942102628a27bbaf201 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 13:17:20 -0800 Subject: [PATCH 005/330] Convert all calls to _send_cmd to use Message Class --- lib/Insteon/Thermostat.pm | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index d5eb0ce1e..fe31267ad 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -122,8 +122,8 @@ sub new { sub poll_mode { my ($self) = @_; - my $subcmd = '02'; - $self->_send_cmd(command => 'thermostat_get_mode', type => 'standard', extra => $subcmd, 'is_synchronous' => 1); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_get_mode', '02'); + $self->_send_cmd($message); return; } @@ -150,7 +150,8 @@ sub mode{ print "Insteon_Thermostat: Invalid Mode state: $state\n"; return(); } - $self->_send_cmd(command => 'thermostat_control', type => 'standard', extra => $mode); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_control', $mode); + $self->_send_cmd($message); } sub fan{ @@ -160,15 +161,16 @@ sub fan{ my $fan; if (($state eq 'on') or ($state eq 'fan_on')) { $fan = '07'; - $state = 'fan_on'; + $state = 'fan_on'; } elsif ($state eq 'auto' or $state eq 'off' or $state eq 'fan_auto') { $fan = '08'; - $state = 'fan_auto'; + $state = 'fan_auto'; } else { print "Insteon_Thermostat: Invalid Fan state: $state\n"; return(); } - $self->_send_cmd(command => 'thermostat_control', type => 'standard', extra => $fan); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_control', $fan); + $self->_send_cmd($message); } sub cool_setpoint{ @@ -178,8 +180,8 @@ sub cool_setpoint{ print "$::Time_Date: [Insteon_Thermostat] ERROR: cool_setpoint $temp not numeric\n"; return; } - - $self->_send_cmd(command => 'thermostat_setpoint_cool', type => 'standard', extra => sprintf('%02X',($temp*2))); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_setpoint_cool', sprintf('%02X',($temp*2))); + $self->_send_cmd($message); } sub heat_setpoint{ @@ -189,14 +191,14 @@ sub heat_setpoint{ print "$::Time_Date: [Insteon_Thermostat] ERROR: heat_setpoint $temp not numeric\n"; return; } - - $self->_send_cmd(command => 'thermostat_setpoint_heat', type => 'standard', extra => sprintf('%02X',($temp*2))); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_setpoint_heat', sprintf('%02X',($temp*2))); + $self->_send_cmd($message); } sub poll_temp { my ($self) = @_; - my $subcmd = '00'; - $self->_send_cmd(command => 'thermostat_get_zone_temp', type => 'standard', extra => $subcmd, 'is_synchronous' => 1); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_get_zone_temp', '00'); + $self->_send_cmd($message); return; } @@ -212,8 +214,8 @@ sub get_temp() { sub poll_setpoint { my ($self) = @_; $self->poll_mode(); - my $subcmd = '20'; - $self->_send_cmd(command => 'thermostat_get_zone_setpoint', type => 'standard', extra => $subcmd, 'is_synchronous' => 1); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_get_zone_setpoint', '20'); + $self->_send_cmd($message); return; } From 95780d9f508781782896beed41b0697c256d795a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 13:36:58 -0800 Subject: [PATCH 006/330] Convert Print Log Messages to the Proper Format --- lib/Insteon/Thermostat.pm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index fe31267ad..ea70b098e 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -130,7 +130,7 @@ sub poll_mode { sub mode{ my ($self, $state) = @_; $state = lc($state); - print "$::Time_Date: Insteon_Thermostat -> Mode $state\n" unless $main::config_parms{no_log} =~/Insteon_Thermostat/; + main::print_log("[Insteon_Thermostat] Mode $state") if $main::Debug{insteon}; my $mode; if ($state eq 'off') { $mode = "09"; @@ -147,7 +147,7 @@ sub mode{ } elsif ($state eq 'program_auto') { $mode = "0c"; } else { - print "Insteon_Thermostat: Invalid Mode state: $state\n"; + main::print_log("[Insteon_Thermostat] ERROR: Invalid Mode state: $state"); return(); } my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_control', $mode); @@ -157,7 +157,7 @@ sub mode{ sub fan{ my ($self, $state) = @_; $state = lc($state); - print "$::Time_Date: Insteon_Thermostat -> Fan $state\n" unless $main::config_parms{no_log} =~/Insteon_Thermostat/; + main::print_log("[Insteon_Thermostat] Fan $state") if $main::Debug{insteon}; my $fan; if (($state eq 'on') or ($state eq 'fan_on')) { $fan = '07'; @@ -166,7 +166,7 @@ sub fan{ $fan = '08'; $state = 'fan_auto'; } else { - print "Insteon_Thermostat: Invalid Fan state: $state\n"; + main::print_log("[Insteon_Thermostat] ERROR: Invalid Fan state: $state"); return(); } my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_control', $fan); @@ -175,9 +175,9 @@ sub fan{ sub cool_setpoint{ my ($self, $temp) = @_; - print "$::Time_Date: [Insteon_Thermostat] Cool setpoint -> $temp\n" unless $main::config_parms{no_log} =~/Insteon_Thermostat/; + main::print_log("[Insteon_Thermostat] Cool setpoint -> $temp") if $main::Debug{insteon}; if($temp !~ /^\d+$/){ - print "$::Time_Date: [Insteon_Thermostat] ERROR: cool_setpoint $temp not numeric\n"; + main::print_log("[Insteon_Thermostat] ERROR: cool_setpoint $temp not numeric"); return; } my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_setpoint_cool', sprintf('%02X',($temp*2))); @@ -186,9 +186,9 @@ sub cool_setpoint{ sub heat_setpoint{ my ($self, $temp) = @_; - print "$::Time_Date: [Insteon_Thermostat] Heat setpoint -> $temp\n" unless $main::config_parms{no_log} =~/Insteon_Thermostat/; + main::print_log("[Insteon_Thermostat] Heat setpoint -> $temp") if $main::Debug{insteon}; if($temp !~ /^\d+$/){ - print "$::Time_Date: [Insteon_Thermostat] ERROR: heat_setpoint $temp not numeric\n"; + main::print_log("[Insteon_Thermostat] ERROR: heat_setpoint $temp not numeric"); return; } my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_setpoint_heat', sprintf('%02X',($temp*2))); @@ -282,7 +282,7 @@ sub _is_info_request { or $cmd eq 'thermostat_get_mode' or $cmd eq 'thermostat_get_temp') ? 1 : 0; if ($is_info_request) { my $val = $msg{extra}; - &::print_log("[Insteon_Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon_Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; if ($cmd eq 'thermostat_get_temp' or $cmd eq 'thermostat_get_zone_temp') { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { From ac0e056b76385ea1d4536106f7ec73cfadf3dbea Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 21:54:53 -0800 Subject: [PATCH 007/330] Add Thermostat Message Types --- lib/Insteon/Thermostat.pm | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index ea70b098e..0e9c90d1c 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -105,6 +105,20 @@ use Insteon::BaseInsteon; # -------------------- START OF SUBROUTINES -------------------- # -------------------------------------------------------------- +my %message_types = ( + %Insteon::BaseDevice::message_types, + thermostat_temp_up => 0x68, + thermostat_temp_down => 0x69, + thermostat_get_zone_temp => 0x6a, + thermostat_get_zone_setpoint => 0x6a, + thermostat_get_zone_humidity => 0x6a, + thermostat_control => 0x6b, + thermostat_get_mode => 0x6b, + thermostat_get_temp => 0x6b, + thermostat_setpoint_cool => 0x6c, + thermostat_setpoint_heat => 0x6d +); + sub new { my ($class, $p_deviceid, $p_interface) = @_; From 96baeabbc4d0a74619ca35cc045093a654e6e462 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 22:10:31 -0800 Subject: [PATCH 008/330] Revise Read_Table_A to Point to Thermostat.pm --- lib/read_table_A.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index f86834765..e4c96d264 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -150,10 +150,10 @@ sub read_table_A { } } elsif($type eq 'IPLT' or $type eq 'INSTEON_THERMOSTAT') { - require 'Insteon_Thermostat.pm'; + require Insteon::Thermostat; ($address, $name, $grouplist, $object, @other) = @item_info; $other = join ', ', (map {"'$_'"} @other); # Quote data - $object = "Insteon_Thermostat(\$$object, \'$address\', $other)"; + $object = "Insteon::Thermostat(\'$address\', $other)"; } elsif($type eq "INSTEON_IRRIGATION") { require 'Insteon_Irrigation.pm'; From 06f3fa8e923979756236afaaf8e764e3c12d3ef7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 2 Mar 2013 22:14:07 -0800 Subject: [PATCH 009/330] Fix Missing Semicolon --- lib/Insteon/Thermostat.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 0e9c90d1c..5b2f20f07 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -122,7 +122,7 @@ my %message_types = ( sub new { my ($class, $p_deviceid, $p_interface) = @_; - my $self = new Insteon::BaseDevice($p_deviceid,$p_interface) + my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); bless $self, $class; $$self{temp} = undef; $$self{mode} = undef; From cc9cc32b54eac64ada9cf75e449f10c62ebc76d5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 4 Mar 2013 19:25:33 -0800 Subject: [PATCH 010/330] Fix Message Creation, Add Awaiting Ack for Thermostat --- lib/Insteon/BaseInsteon.pm | 2 ++ lib/Insteon/Thermostat.pm | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c57e7abe5..7e8c42678 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -516,6 +516,7 @@ sub _process_message } else { +main::print_log("[krk2] " . $self->get_object_name); # allow non-synchronous messages to also use the _is_info_request hook $self->_is_info_request($pending_cmd,$p_setby,%msg); $self->is_acknowledged(1); @@ -597,6 +598,7 @@ sub _process_command_stack or $message->command eq 'status_request' or $message->command eq 'do_read_ee' or $message->command eq 'set_address_msb' + or $message->command eq 'thermostat_get_mode' ) { $$self{awaiting_ack} = 1; diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 5b2f20f07..556e67194 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -130,13 +130,14 @@ sub new { $$self{heat_sp} = undef; $$self{cool_sp} = undef; $self->restore_data('temp','mode','fan_mode','heat_sp','cool_sp'); - $$self{m_pending_setpoint} = undef; + $$self{m_pending_setpoint} = undef; + $$self{message_types} = \%message_types; return $self; } sub poll_mode { my ($self) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_get_mode', '02'); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_mode', '02'); $self->_send_cmd($message); return; } @@ -164,7 +165,7 @@ sub mode{ main::print_log("[Insteon_Thermostat] ERROR: Invalid Mode state: $state"); return(); } - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_control', $mode); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $mode); $self->_send_cmd($message); } @@ -183,7 +184,7 @@ sub fan{ main::print_log("[Insteon_Thermostat] ERROR: Invalid Fan state: $state"); return(); } - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_control', $fan); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $fan); $self->_send_cmd($message); } @@ -194,7 +195,7 @@ sub cool_setpoint{ main::print_log("[Insteon_Thermostat] ERROR: cool_setpoint $temp not numeric"); return; } - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_setpoint_cool', sprintf('%02X',($temp*2))); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_setpoint_cool', sprintf('%02X',($temp*2))); $self->_send_cmd($message); } @@ -205,13 +206,13 @@ sub heat_setpoint{ main::print_log("[Insteon_Thermostat] ERROR: heat_setpoint $temp not numeric"); return; } - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_setpoint_heat', sprintf('%02X',($temp*2))); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_setpoint_heat', sprintf('%02X',($temp*2))); $self->_send_cmd($message); } sub poll_temp { my ($self) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_get_zone_temp', '00'); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_temp', '00'); $self->_send_cmd($message); return; } @@ -228,7 +229,7 @@ sub get_temp() { sub poll_setpoint { my ($self) = @_; $self->poll_mode(); - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'thermostat_get_zone_setpoint', '20'); + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_setpoint', '20'); $self->_send_cmd($message); return; } From ad0d3b798ba53e63226c50cae755b82c4c68aef3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 4 Mar 2013 20:09:55 -0800 Subject: [PATCH 011/330] Condense Duplicate Thermostat Message Types --- lib/Insteon/BaseInsteon.pm | 3 ++- lib/Insteon/Thermostat.pm | 31 +++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7e8c42678..e21132cf1 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -598,7 +598,8 @@ sub _process_command_stack or $message->command eq 'status_request' or $message->command eq 'do_read_ee' or $message->command eq 'set_address_msb' - or $message->command eq 'thermostat_get_mode' + or $message->command eq 'thermostat_control' + or $message->command eq 'thermostat_get_zone_info' ) { $$self{awaiting_ack} = 1; diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 556e67194..080644869 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -109,12 +109,8 @@ my %message_types = ( %Insteon::BaseDevice::message_types, thermostat_temp_up => 0x68, thermostat_temp_down => 0x69, - thermostat_get_zone_temp => 0x6a, - thermostat_get_zone_setpoint => 0x6a, - thermostat_get_zone_humidity => 0x6a, + thermostat_get_zone_info => 0x6a, thermostat_control => 0x6b, - thermostat_get_mode => 0x6b, - thermostat_get_temp => 0x6b, thermostat_setpoint_cool => 0x6c, thermostat_setpoint_heat => 0x6d ); @@ -137,7 +133,8 @@ sub new { sub poll_mode { my ($self) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_mode', '02'); + $$self{_control_action} = "mode"; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', '02'); $self->_send_cmd($message); return; } @@ -212,7 +209,8 @@ sub heat_setpoint{ sub poll_temp { my ($self) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_temp', '00'); + $$self{_zone_action} = "temp"; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_info', '00'); $self->_send_cmd($message); return; } @@ -229,7 +227,8 @@ sub get_temp() { sub poll_setpoint { my ($self) = @_; $self->poll_mode(); - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_setpoint', '20'); + $$self{_zone_info} = "setpoint"; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_info', '20'); $self->_send_cmd($message); return; } @@ -292,19 +291,18 @@ sub get_fan_mode() { sub _is_info_request { my ($self, $cmd, $ack_setby, %msg) = @_; - my $is_info_request = ($cmd eq 'thermostat_get_zone_temp' - or $cmd eq 'thermostat_get_zone_setpoint' or $cmd eq 'thermostat_get_zone_humidity' - or $cmd eq 'thermostat_get_mode' or $cmd eq 'thermostat_get_temp') ? 1 : 0; + my $is_info_request = ($cmd eq 'thermostat_get_zone_info' + or $cmd eq 'thermostat_control') ? 1 : 0; if ($is_info_request) { my $val = $msg{extra}; main::print_log("[Insteon_Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; - if ($cmd eq 'thermostat_get_temp' or $cmd eq 'thermostat_get_zone_temp') { + if ($$self{_zone_info} eq "temp") { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { $self->set_receive('temp_change'); } $$self{'temp'} = $val; - } elsif ($cmd eq 'thermostat_get_mode') { + } elsif ($$self{_control_action} eq "mode") { if ($val eq '00') { $self->_mode('off'); } elsif ($val eq '01') { @@ -324,7 +322,7 @@ sub _is_info_request { } elsif ($val eq '08') { $self->_fan_mode('fan_auto'); } - } elsif ($cmd eq 'thermostat_get_zone_setpoint') { + } elsif ($$self{_zone_info} eq 'setpoint') { $val = (hex $val) / 2; # returned value is twice the real value # in auto modes, expect direct message with cool_setpoint to follow if ($self->get_mode() eq 'auto' or 'program_auto') { @@ -338,7 +336,8 @@ sub _is_info_request { } } - + $$self{_control_action} = undef; + $$self{_zone_action} = undef; return $is_info_request; } @@ -398,7 +397,7 @@ sub _process_message &::print_log("[Insteon_Thermostat] device category: $msg{devcat} received for " . $self->{object_name}); #stop ping timer now that we have a devcat; possibly may want to change this behavior to allow recurring pings $$self{ping_timer}->stop(); - } elsif ($msg{command} eq 'thermostat_get_zone_setpoint' && $$self{m_pending_setpoint}) { + } elsif ($$self{_zone_info} eq 'setpoint' && $$self{m_pending_setpoint}) { # we got our cool setpoint in auto mode my $val = (hex $msg{extra})/2; $self->_cool_sp($val); From 7a59f691237d195b407c1d7fc7d6a5ffb6dd4041 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 5 Mar 2013 17:45:25 -0800 Subject: [PATCH 012/330] Fix Library Name in Print_Log Messages --- lib/Insteon/Thermostat.pm | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 080644869..8dac67758 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -142,7 +142,7 @@ sub poll_mode { sub mode{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon_Thermostat] Mode $state") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Mode $state") if $main::Debug{insteon}; my $mode; if ($state eq 'off') { $mode = "09"; @@ -159,7 +159,7 @@ sub mode{ } elsif ($state eq 'program_auto') { $mode = "0c"; } else { - main::print_log("[Insteon_Thermostat] ERROR: Invalid Mode state: $state"); + main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); return(); } my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $mode); @@ -169,7 +169,7 @@ sub mode{ sub fan{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon_Thermostat] Fan $state") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Fan $state") if $main::Debug{insteon}; my $fan; if (($state eq 'on') or ($state eq 'fan_on')) { $fan = '07'; @@ -178,7 +178,7 @@ sub fan{ $fan = '08'; $state = 'fan_auto'; } else { - main::print_log("[Insteon_Thermostat] ERROR: Invalid Fan state: $state"); + main::print_log("[Insteon::Thermostat] ERROR: Invalid Fan state: $state"); return(); } my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $fan); @@ -187,9 +187,9 @@ sub fan{ sub cool_setpoint{ my ($self, $temp) = @_; - main::print_log("[Insteon_Thermostat] Cool setpoint -> $temp") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Cool setpoint -> $temp") if $main::Debug{insteon}; if($temp !~ /^\d+$/){ - main::print_log("[Insteon_Thermostat] ERROR: cool_setpoint $temp not numeric"); + main::print_log("[Insteon::Thermostat] ERROR: cool_setpoint $temp not numeric"); return; } my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_setpoint_cool', sprintf('%02X',($temp*2))); @@ -198,9 +198,9 @@ sub cool_setpoint{ sub heat_setpoint{ my ($self, $temp) = @_; - main::print_log("[Insteon_Thermostat] Heat setpoint -> $temp") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Heat setpoint -> $temp") if $main::Debug{insteon}; if($temp !~ /^\d+$/){ - main::print_log("[Insteon_Thermostat] ERROR: heat_setpoint $temp not numeric"); + main::print_log("[Insteon::Thermostat] ERROR: heat_setpoint $temp not numeric"); return; } my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_setpoint_heat', sprintf('%02X',($temp*2))); @@ -295,7 +295,7 @@ sub _is_info_request { or $cmd eq 'thermostat_control') ? 1 : 0; if ($is_info_request) { my $val = $msg{extra}; - main::print_log("[Insteon_Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; if ($$self{_zone_info} eq "temp") { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { @@ -349,7 +349,7 @@ sub _process_message { my ($self,$p_setby,%msg) = @_; my $p_state = undef; -# &::print_log("[Insteon_Thermostat] _process_message Type: ".$msg{type}. +# &::print_log("[Insteon::Thermostat] _process_message Type: ".$msg{type}. # " Command: (" . $msg{command} . " CMD2: " .$msg{extra}) if $main::Debug{insteon}; #XXX # the current approach assumes that links from other controllers to some responder @@ -371,30 +371,30 @@ sub _process_message $self->is_acknowledged(1); # signal receipt of message to the command stack in case commands are queued $self->_process_command_stack(%msg); - &::print_log("[Insteon_Thermostat] received command/state (awaiting) acknowledge from " . $self->{object_name} + &::print_log("[Insteon::Thermostat] received command/state (awaiting) acknowledge from " . $self->{object_name} . ": $pending_cmd and data: $msg{extra}") if $main::Debug{insteon}; } } else { $self->is_acknowledged(1); # signal receipt of message to the command stack in case commands are queued $self->_process_command_stack(%msg); - &::print_log("[Insteon_Thermostat] received command/state acknowledge from " . $self->{object_name} + &::print_log("[Insteon::Thermostat] received command/state acknowledge from " . $self->{object_name} . ": " . (($msg{command}) ? $msg{command} : "(unknown)") . " and data: $msg{extra}") if $main::Debug{insteon}; } } elsif ($msg{is_nack}) { if ($$self{awaiting_ack}) { - &::print_log("[Insteon_Thermostat] WARN!! encountered a nack message for " . $self->{object_name} + &::print_log("[Insteon::Thermostat] WARN!! encountered a nack message for " . $self->{object_name} . " ... waiting for retry"); } else { - &::print_log("[Insteon_Thermostat] WARN!! encountered a nack message for " . $self->{object_name} + &::print_log("[Insteon::Thermostat] WARN!! encountered a nack message for " . $self->{object_name} . " ... skipping"); $self->is_acknowledged(0); $self->_process_command_stack(%msg); } } elsif ($msg{type} eq 'broadcast') { $self->devcat($msg{devcat}); - &::print_log("[Insteon_Thermostat] device category: $msg{devcat} received for " . $self->{object_name}); + &::print_log("[Insteon::Thermostat] device category: $msg{devcat} received for " . $self->{object_name}); #stop ping timer now that we have a devcat; possibly may want to change this behavior to allow recurring pings $$self{ping_timer}->stop(); } elsif ($$self{_zone_info} eq 'setpoint' && $$self{m_pending_setpoint}) { From 0914b7ac21b0dab1f23a44f8909a9f1338d9fd48 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 5 Mar 2013 17:55:25 -0800 Subject: [PATCH 013/330] Remove Duplicate Entries in _is_info_request and _process_message Test for unique items first, then call the routine in SUPER to handle the remaining --- lib/Insteon/BaseInsteon.pm | 1 - lib/Insteon/Thermostat.pm | 86 +++++++++----------------------------- 2 files changed, 19 insertions(+), 68 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index e21132cf1..b7ff7c8e4 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -516,7 +516,6 @@ sub _process_message } else { -main::print_log("[krk2] " . $self->get_object_name); # allow non-synchronous messages to also use the _is_info_request hook $self->_is_info_request($pending_cmd,$p_setby,%msg); $self->is_acknowledged(1); diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 8dac67758..afa24cc27 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -334,86 +334,38 @@ sub _is_info_request { $self->_cool_sp($val); } } - + $$self{_control_action} = undef; + $$self{_zone_action} = undef; + } + else #This was not a thermostat info_request + { + #Check if this was a generic info_request + $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); } - $$self{_control_action} = undef; - $$self{_zone_action} = undef; return $is_info_request; } -# Need to handle some of these messages differently than Insteon_Device -# Trimming what I know we don't need, leaving what I'm unsure of. Still an excess -# of duplicated code. +## Unique messages handled first, non-unique sent to SUPER sub _process_message { my ($self,$p_setby,%msg) = @_; - my $p_state = undef; -# &::print_log("[Insteon::Thermostat] _process_message Type: ".$msg{type}. -# " Command: (" . $msg{command} . " CMD2: " .$msg{extra}) if $main::Debug{insteon}; #XXX - - # the current approach assumes that links from other controllers to some responder - # would be seen by the plm by also direct linking the controller as a responder - # and not putting the plm into monitor mode. This means that updating the state - # of the responder based upon the link controller's request is handled - # by Insteon_Link. - $$self{m_is_locally_set} = 1 if $msg{source} eq lc $self->device_id; - if ($msg{is_ack}) { - if ($$self{awaiting_ack}) { - my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}{command} : $msg{command}; - my $ack_setby = (ref $$self{m_status_request_pending}) - ? $$self{m_status_request_pending} : $p_setby; - if ($self->_is_info_request($pending_cmd,$ack_setby,%msg)) { - $self->is_acknowledged(1); - $$self{m_status_request_pending} = 0; - $self->_process_command_stack(%msg); - } else { - $self->is_acknowledged(1); - # signal receipt of message to the command stack in case commands are queued - $self->_process_command_stack(%msg); - &::print_log("[Insteon::Thermostat] received command/state (awaiting) acknowledge from " . $self->{object_name} - . ": $pending_cmd and data: $msg{extra}") if $main::Debug{insteon}; - } - } else { - $self->is_acknowledged(1); - # signal receipt of message to the command stack in case commands are queued - $self->_process_command_stack(%msg); - &::print_log("[Insteon::Thermostat] received command/state acknowledge from " . $self->{object_name} - . ": " . (($msg{command}) ? $msg{command} : "(unknown)") - . " and data: $msg{extra}") if $main::Debug{insteon}; - } - } elsif ($msg{is_nack}) { - if ($$self{awaiting_ack}) { - &::print_log("[Insteon::Thermostat] WARN!! encountered a nack message for " . $self->{object_name} - . " ... waiting for retry"); - } else { - &::print_log("[Insteon::Thermostat] WARN!! encountered a nack message for " . $self->{object_name} - . " ... skipping"); - $self->is_acknowledged(0); - $self->_process_command_stack(%msg); - } - } elsif ($msg{type} eq 'broadcast') { - $self->devcat($msg{devcat}); - &::print_log("[Insteon::Thermostat] device category: $msg{devcat} received for " . $self->{object_name}); - #stop ping timer now that we have a devcat; possibly may want to change this behavior to allow recurring pings - $$self{ping_timer}->stop(); - } elsif ($$self{_zone_info} eq 'setpoint' && $$self{m_pending_setpoint}) { - # we got our cool setpoint in auto mode - my $val = (hex $msg{extra})/2; - $self->_cool_sp($val); - $$self{m_setpoint_pending} = 0; + my $clear_message = 0; + if ($$self{_zone_info} eq 'setpoint' && $$self{m_pending_setpoint}) { + # we got our cool setpoint in auto mode + my $val = (hex $msg{extra})/2; + $self->_cool_sp($val); + $$self{m_setpoint_pending} = 0; + $clear_message = 1; } else { - ## TO-DO: make sure that the state passed by command is something that is reasonable to set - $p_state = $msg{command}; - $$self{_pending_cleanup} = 1 if $msg{type} eq 'alllink'; -# $self->set($p_state, $p_setby) unless (lc($self->state) eq lc($p_state)) and - $self->set($p_state, $self) unless (lc($self->state) eq lc($p_state)) and - ($msg{type} eq 'cleanup' and $$self{_pending_cleanup}); - $$self{_pending_cleanup} = 0 if $msg{type} eq 'cleanup'; + $clear_message = $self->SUPER::_process_message($p_setby,%msg); } + return $clear_message; } # Overload methods we don't use, but would otherwise cause Insteon traffic. sub request_status { return 0 } +sub level { return 0 } + 1; From 181fc280faada372aa75a5c598f3abe33c9ca835 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 5 Mar 2013 16:05:25 -0800 Subject: [PATCH 014/330] Fix Typo in _zone_action tracking, allow for subsequent setpoint messages --- lib/Insteon/Thermostat.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index afa24cc27..ff2867516 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -296,7 +296,7 @@ sub _is_info_request { if ($is_info_request) { my $val = $msg{extra}; main::print_log("[Insteon::Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; - if ($$self{_zone_info} eq "temp") { + if ($$self{_zone_action} eq "temp") { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { $self->set_receive('temp_change'); @@ -333,6 +333,11 @@ sub _is_info_request { } elsif ($self->get_mode() eq 'cool' or 'program_cool') { $self->_cool_sp($val); } + } elsif ($$self{'m_pending_setpoint'} == 1) { + #This is the second message with the cool_setpoint + $val = (hex $val) / 2; + $self->_cool_sp($val); + $$self{'m_pending_setpoint'} = undef; } $$self{_control_action} = undef; $$self{_zone_action} = undef; From 53cf854165d893442b8071216b93e6da50944a2b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 6 Mar 2013 17:45:25 -0800 Subject: [PATCH 015/330] Add Documentation to Existing Code --- lib/Insteon/Thermostat.pm | 149 +++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 49 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index ff2867516..9884c43e8 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -1,27 +1,18 @@ -=begin comment +=head1 NAME -AUTHORS -Gregg Liming -Brian Warren - -INITIAL CONFIGURATION -In user code: - $thermostat = new Insteon_Thermostat($myPLM, '12.34.56'); - -In items.mht: - -INSTEON_THERMOSTAT, 12.34.56, thermostat, HVAC +B - Insteon Thermostat -BUGS +=head1 DESCRIPTION +Enables support for an Insteon Thermostat. -EXAMPLE USAGE -see code/examples/Insteon_thermostat.pl for more. - -Creating the object: +=head1 SYNOPSIS - $thermostat = new Insteon_Thermostat($myPLM, '12.34.56'); +In user code: + $thermostat = new Insteon_Thermostat($myPLM, '12.34.56'); +In items.mht: + INSTEON_THERMOSTAT, 12.34.56, thermostat, HVAC Poll for temperature changes. @@ -29,7 +20,6 @@ Poll for temperature changes. $thermostat->poll_temp(); } - Watch for temperature changes. if (state_now $thermostat eq 'temp_change') { @@ -57,41 +47,42 @@ All of the states that may be set: fan_mode_change: Fan mode changed (call get_fan_mode() to get value). -All of the functions available: - mode(): - Sets system mode to argument: 'off', 'heat', 'cool', 'auto', - 'program_heat', 'program_cool', 'program_auto' - poll_mode(): - Causes thermostat to return mode; detected as state change if mode changes - get_mode(): - Returns the last mode returned by poll_mode(). - fan(): - Sets fan to 'on' or 'auto' - get_fan_mode(): - Returns the current fan mode (fan_on or fan_auto) - poll_setpoint(): - Causes thermostat to return setpoint(s); detected as state change if setpoint changes - Returns setpoint based on mode, auto modes return both heat and cool. - cool_setpoint(): - Sets a new cool setpoint. - get_cool_sp(): - Returns the current cool setpoint. - heat_setpoint(): - Sets a new heat setpoint. - get_heat_sp(): - Returns the current heat setpoint. - poll_temp(): - Causes thermostat to return temp; detected as state change - get_temp(): - Returns the current temperature at the thermostat. - - -#TODO +see code/examples/Insteon_thermostat.pl for more. + +=head1 BUGS + +Initial code for Venstar thermostats, which use Insteon engine version i1, only +provided basic features. The new Insteon 2441TH thermostats use the i2cs engine +and only allow the polling, but not setting, of the thermostat attributes using +i2 code. As such, I am unable to test or provide enhancements to certain i1 +only aspects. + +=head1 AUTHOR + +Initial Code by: +Gregg Liming +Brian Warren + +Enhanced to i2 by: +Kevin Rober Keegan + +=head1 TODO + - Look at possible bugs when starting from factory defaults There seemed to be an issue with the setpoints changing when changing modes until they were set programatically. - Test fan modes and associated state_changes - Manage aldb - should be able to adjust setpoints based on plm scene. <- may be overkill + +=head1 INHERITS + +B + +B + +=head1 Methods + +=over =cut package Insteon::Thermostat; @@ -131,6 +122,10 @@ sub new { return $self; } +=item C + +Causes thermostat to return mode; detected as state change if mode changes +=cut sub poll_mode { my ($self) = @_; $$self{_control_action} = "mode"; @@ -139,6 +134,11 @@ sub poll_mode { return; } +=item C + +Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', +'program_cool', 'program_auto' +=cut sub mode{ my ($self, $state) = @_; $state = lc($state); @@ -166,6 +166,10 @@ sub mode{ $self->_send_cmd($message); } +=item C + +Sets fan to 'on' or 'auto' +=cut sub fan{ my ($self, $state) = @_; $state = lc($state); @@ -185,6 +189,10 @@ sub fan{ $self->_send_cmd($message); } +=item C + +Sets a new cool setpoint. +=cut sub cool_setpoint{ my ($self, $temp) = @_; main::print_log("[Insteon::Thermostat] Cool setpoint -> $temp") if $main::Debug{insteon}; @@ -196,6 +204,10 @@ sub cool_setpoint{ $self->_send_cmd($message); } +=item C + +Sets a new heat setpoint. +=cut sub heat_setpoint{ my ($self, $temp) = @_; main::print_log("[Insteon::Thermostat] Heat setpoint -> $temp") if $main::Debug{insteon}; @@ -207,6 +219,10 @@ sub heat_setpoint{ $self->_send_cmd($message); } +=item C + +Causes thermostat to return temp; detected as state change. +=cut sub poll_temp { my ($self) = @_; $$self{_zone_action} = "temp"; @@ -215,11 +231,20 @@ sub poll_temp { return; } +=item C + +Returns the current temperature at the thermostat. +=cut sub get_temp() { my ($self) = @_; return $$self{'temp'}; } +=item C + +Causes thermostat to return setpoint(s); detected as state change if setpoint changes. +Returns setpoint based on mode, auto modes return both heat and cool. +=cut # The setpoint is returned in 2 messages while in the auto modes. # The heat setpoint is returned in the ACK, which is followed by # a direct message containing the cool setpoint. Because of this, @@ -233,11 +258,19 @@ sub poll_setpoint { return; } +=item C + +Returns the current heat setpoint. +=cut sub get_heat_sp() { my ($self) = @_; return $$self{'heat_sp'}; } +=item C + +Returns the current cool setpoint. +=cut sub get_cool_sp() { my ($self) = @_; return $$self{'cool_sp'}; @@ -279,11 +312,19 @@ sub _mode() { return $$self{'mode'}; } +=item C + +Returns the last mode returned by C. +=cut sub get_mode() { my ($self) = @_; return $$self{'mode'}; } +=item C + +Returns the current fan mode (fan_on or fan_auto) +=cut sub get_fan_mode() { my ($self) = @_; return $$self{'fan_mode'}; @@ -374,3 +415,13 @@ sub request_status { return 0 } sub level { return 0 } 1; +=back + +=head1 LICENSE + +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. +=cut From d3544c0bb6d0cef78ddfca43822be3dd8ebee2e4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 7 Mar 2013 17:45:25 -0800 Subject: [PATCH 016/330] Test out Extended Message for Mode --- lib/Insteon/Thermostat.pm | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 9884c43e8..a6ad76886 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -162,8 +162,13 @@ sub mode{ main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); return(); } - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $mode); - $self->_send_cmd($message); + if ($self->_aldb->isa('Insteon::ALDB_i2'){ + my $extra = $mode . "00000000000000000000000000"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'thermostat_control', $extra); + } else { + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $mode); + } + $self->_send_cmd($message); } =item C From 1aee18dfc310f84bac849069574334452eaad48d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 7 Mar 2013 17:55:25 -0800 Subject: [PATCH 017/330] Create simple_message(), Use for all subs that set data simple_message() is a stop-gap to bring support of i1, i2, & i2cs thermostats up to the level provided before Insteon Redux. This should allow for quick merging into the stable codebase before release 3.0. Next steps will be to split the thermostat class into an i1 and i2 class. --- lib/Insteon/Thermostat.pm | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index a6ad76886..972c88228 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -162,13 +162,7 @@ sub mode{ main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); return(); } - if ($self->_aldb->isa('Insteon::ALDB_i2'){ - my $extra = $mode . "00000000000000000000000000"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'thermostat_control', $extra); - } else { - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $mode); - } - $self->_send_cmd($message); + $self->_send_cmd($self->simple_message('thermostat_control', $mode)); } =item C @@ -190,8 +184,7 @@ sub fan{ main::print_log("[Insteon::Thermostat] ERROR: Invalid Fan state: $state"); return(); } - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_control', $fan); - $self->_send_cmd($message); + $self->_send_cmd($self->simple_message('thermostat_control', $fan)); } =item C @@ -205,8 +198,7 @@ sub cool_setpoint{ main::print_log("[Insteon::Thermostat] ERROR: cool_setpoint $temp not numeric"); return; } - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_setpoint_cool', sprintf('%02X',($temp*2))); - $self->_send_cmd($message); + $self->_send_cmd($self->simple_message('thermostat_setpoint_cool', sprintf('%02X',($temp*2)))); } =item C @@ -220,8 +212,7 @@ sub heat_setpoint{ main::print_log("[Insteon::Thermostat] ERROR: heat_setpoint $temp not numeric"); return; } - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_setpoint_heat', sprintf('%02X',($temp*2))); - $self->_send_cmd($message); + $self->_send_cmd($self->simple_message('thermostat_setpoint_heat', sprintf('%02X',($temp*2)))); } =item C @@ -414,6 +405,20 @@ sub _process_message return $clear_message; } +## Creates either a Standard or Extended Message depending on the device type +## Can be used to create different classes later +sub simple_message { + my ($self,$type,$extra) = @_; + my $message; + if ($self->_aldb->isa('Insteon::ALDB_i2')){ + $extra = $extra . "0000000000000000000000000000"; + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, $type, $extra); + } else { + $message = new Insteon::InsteonMessage('insteon_send', $self, $type, $extra); + } + return $message; +} + # Overload methods we don't use, but would otherwise cause Insteon traffic. sub request_status { return 0 } From 1079eeda01a113f9e5d2acff2fbe01109ff39d2e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 7 Mar 2013 20:02:24 -0800 Subject: [PATCH 018/330] Final cleanup of Insteon Thermostat --- lib/Insteon/Thermostat.pm | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 972c88228..24d0be61b 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -137,7 +137,8 @@ sub poll_mode { =item C Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', -'program_cool', 'program_auto' +'program_cool', 'program_auto'. The 2441TH thermostat does not have program_heat + or program_cool. =cut sub mode{ my ($self, $state) = @_; @@ -158,6 +159,7 @@ sub mode{ $mode = "0b"; } elsif ($state eq 'program_auto') { $mode = "0c"; + $mode = "0a" if $self->_aldb->isa('Insteon::ALDB_i2'); } else { main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); return(); @@ -248,7 +250,7 @@ Returns setpoint based on mode, auto modes return both heat and cool. sub poll_setpoint { my ($self) = @_; $self->poll_mode(); - $$self{_zone_info} = "setpoint"; + $$self{_zone_action} = "setpoint"; my $message = new Insteon::InsteonMessage('insteon_send', $self, 'thermostat_get_zone_info', '20'); $self->_send_cmd($message); return; @@ -310,7 +312,7 @@ sub _mode() { =item C -Returns the last mode returned by C. +Returns the last mode returned by C I2 devices will report auto for both auto and program_auto. =cut sub get_mode() { my ($self) = @_; @@ -359,7 +361,8 @@ sub _is_info_request { } elsif ($val eq '08') { $self->_fan_mode('fan_auto'); } - } elsif ($$self{_zone_info} eq 'setpoint') { + $$self{_control_action} = undef; + } elsif ($$self{_zone_action} eq 'setpoint') { $val = (hex $val) / 2; # returned value is twice the real value # in auto modes, expect direct message with cool_setpoint to follow if ($self->get_mode() eq 'auto' or 'program_auto') { @@ -370,14 +373,13 @@ sub _is_info_request { } elsif ($self->get_mode() eq 'cool' or 'program_cool') { $self->_cool_sp($val); } + $$self{_zone_action} = undef; } elsif ($$self{'m_pending_setpoint'} == 1) { #This is the second message with the cool_setpoint $val = (hex $val) / 2; $self->_cool_sp($val); $$self{'m_pending_setpoint'} = undef; } - $$self{_control_action} = undef; - $$self{_zone_action} = undef; } else #This was not a thermostat info_request { @@ -393,7 +395,7 @@ sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; - if ($$self{_zone_info} eq 'setpoint' && $$self{m_pending_setpoint}) { + if ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { # we got our cool setpoint in auto mode my $val = (hex $msg{extra})/2; $self->_cool_sp($val); From 012b54f1d411982d25188d98808958c1babdc526 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Mar 2013 23:36:20 -0800 Subject: [PATCH 019/330] Add Initial Thermo i1 & i2 Subclasses Objects are reblessed when aldb version is checked --- lib/Insteon/BaseInsteon.pm | 7 +++++++ lib/Insteon/Thermostat.pm | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index dfdcbe5f7..475891fbd 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1434,6 +1434,13 @@ sub check_aldb_version if $@ and $main::Debug{insteon}; package Insteon::BaseDevice; } + + if ($self->isa('Insteon::Thermostat')&& $self->_aldb->aldb_version() eq "I2"){ + bless $self, 'Insteon::Thermo_i2'; + } + elsif ($self->isa('Insteon::Thermostat')&& $self->_aldb->aldb_version() eq "I1"){ + bless $self, 'Insteon::Thermo_i1'; + } } diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 24d0be61b..6d8805363 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -108,7 +108,7 @@ my %message_types = ( sub new { my ($class, $p_deviceid, $p_interface) = @_; - +print "[KRK] $class, $p_deviceid, $p_interface"; my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); bless $self, $class; $$self{temp} = undef; @@ -426,6 +426,19 @@ sub request_status { return 0 } sub level { return 0 } + +package Insteon::Thermo_i1; +use strict; + +@Insteon::Thermo_i1::ISA = ('Insteon::Thermostat'); + + +package Insteon::Thermo_i2; +use strict; + +@Insteon::Thermo_i2::ISA = ('Insteon::Thermostat'); + + 1; =back From 54f08ff7c318973e3824a7f3166ac79501bb009a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 11 Mar 2013 22:43:46 -0700 Subject: [PATCH 020/330] Create Thermostat Broadcast Dummy Object The 2441th Insteon thermostat will broadcast changes to all devices registered as a responder to group EF. Created a dummy object under thermostat that is registered as a subdevice on group EF of the thermostat. Sync links properly handles adding the link to the device, but has an error adding it to the PLM. PLM responder link is not necessary, but will solve in future commit. Moved reclass of thermostat to i1 and i2 to Insteon.pm. Will add subchildren to same sub in future. --- lib/Insteon.pm | 25 +++++++++++++++++++++++++ lib/Insteon/BaseInsteon.pm | 7 ------- lib/Insteon/Thermostat.pm | 32 +++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 14ccd1946..c173e9cb3 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -556,6 +556,30 @@ sub check_all_aldb_versions main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); } +sub check_thermo_versions +{ + main::print_log("[Insteon] DEBUG4 Checking thermostat versions") if ($main::Debug{insteon} >= 4); + + my @thermo_devices = (); + push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); + foreach my $thermo_device (@thermo_devices) + { + main::print_log("[Insteon] DEBUG4 Checking thermostat version for " + . $thermo_device->get_object_name()) + if ($main::Debug{insteon} >= 4); + if ($thermo_device->isa('Insteon::Thermostat') && + $thermo_device->_aldb->aldb_version() eq "I2"){ + bless $thermo_device, 'Insteon::Thermo_i2'; + $thermo_device->init(); + } + elsif ($thermo_device->isa('Insteon::Thermostat') + && $thermo_device->_aldb->aldb_version() eq "I1"){ + bless $thermo_device, 'Insteon::Thermo_i1'; + } + } + main::print_log("[Insteon] DEBUG4 Checking thermostat version of all devices completed") if ($main::Debug{insteon} >= 4); +} + package InsteonManager; @@ -582,6 +606,7 @@ sub _active_interface $init_complete = 0; &main::MainLoop_pre_add_hook(\&Insteon::init, 1); &main::Reload_post_add_hook(\&Insteon::generate_voice_commands, 1); + &main::Reload_post_add_hook(\&Insteon::check_thermo_versions, 1); } $$self{active_interface} = $interface if $interface; return $$self{active_interface}; diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 475891fbd..dfdcbe5f7 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1434,13 +1434,6 @@ sub check_aldb_version if $@ and $main::Debug{insteon}; package Insteon::BaseDevice; } - - if ($self->isa('Insteon::Thermostat')&& $self->_aldb->aldb_version() eq "I2"){ - bless $self, 'Insteon::Thermo_i2'; - } - elsif ($self->isa('Insteon::Thermostat')&& $self->_aldb->aldb_version() eq "I1"){ - bless $self, 'Insteon::Thermo_i1'; - } } diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 6d8805363..b846c1c9e 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -108,7 +108,6 @@ my %message_types = ( sub new { my ($class, $p_deviceid, $p_interface) = @_; -print "[KRK] $class, $p_deviceid, $p_interface"; my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); bless $self, $class; $$self{temp} = undef; @@ -438,6 +437,37 @@ use strict; @Insteon::Thermo_i2::ISA = ('Insteon::Thermostat'); +sub init { + my ($self) = @_; + my $dev_id = $self->device_id(); + $dev_id =~ /(\w\w)(\w\w)(\w\w)/; + $dev_id = "$1.$2.$3"; + $$self{bcast} = new Insteon::Thermo_bcast("$dev_id".':EF'); + Insteon::add($$self{bcast}); + + ## Child objects + #my $obj_group = ::get_object_by_name('HVAC'); + #$obj_group->add($$self{bcast}); + #&main::register_object_by_name($self->get_object_name ."{bcast}",$$self{bcast}); + #$$self{bcast}->{category} = "sample"; + #$$self{bcast}->{filename} = "sample"; + #$$self{bcast}->{object_name} = $self->get_object_name ."{bcast}"; +} + +package Insteon::Thermo_bcast; +use strict; + +@Insteon::Thermo_bcast::ISA = ('Insteon::Thermostat'); + +###This is basically a dummy object, it is designed to allow a link from group +###EF to be added as part of sync links. + +sub new { + my ($class, $p_deviceid) = @_; + my $self = new Insteon::Thermostat($p_deviceid); + bless $self, $class; + return $self; +} 1; =back From e4d977a65c0a79d49b0767cbb70d07bbcd46ae97 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 12 Mar 2013 21:14:52 -0700 Subject: [PATCH 021/330] Continue to work on adding Broadcast Device --- lib/Insteon/Thermostat.pm | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index b846c1c9e..d2d4f9569 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -90,7 +90,7 @@ package Insteon::Thermostat; use strict; use Insteon::BaseInsteon; -@Insteon::Thermostat::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); +@Insteon::Thermostat::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); # -------------------- START OF SUBROUTINES -------------------- @@ -439,32 +439,40 @@ use strict; sub init { my ($self) = @_; + + ## Create the broadcast dummy item my $dev_id = $self->device_id(); $dev_id =~ /(\w\w)(\w\w)(\w\w)/; $dev_id = "$1.$2.$3"; - $$self{bcast} = new Insteon::Thermo_bcast("$dev_id".':EF'); + $$self{bcast} = new Insteon::Thermo_i2_bcast("$dev_id".':EF'); + # Add bcast object to list of Insteon objects Insteon::add($$self{bcast}); + # Register bcast object with MH + &main::register_object_by_name('$' . $self->get_object_name ."{bcast}",$$self{bcast}); + $$self{bcast}->{object_name} = '$' . $self->get_object_name ."{bcast}"; - ## Child objects + ## Create the child objects + my @child_objs = ("mode", "fan", "temp", "humidity", "setpoint_h", "setpoint_c"); #my $obj_group = ::get_object_by_name('HVAC'); #$obj_group->add($$self{bcast}); #&main::register_object_by_name($self->get_object_name ."{bcast}",$$self{bcast}); #$$self{bcast}->{category} = "sample"; #$$self{bcast}->{filename} = "sample"; - #$$self{bcast}->{object_name} = $self->get_object_name ."{bcast}"; + #$$self{bcast}->{object_name} = '$' . $self->get_object_name ."{bcast}"; } -package Insteon::Thermo_bcast; +package Insteon::Thermo_i2_bcast; use strict; -@Insteon::Thermo_bcast::ISA = ('Insteon::Thermostat'); +@Insteon::Thermo_i2_bcast::ISA = ('Insteon::BaseDevice', 'Insteon::DeviceController'); ###This is basically a dummy object, it is designed to allow a link from group -###EF to be added as part of sync links. +###EF to be added as part of sync links. Group EF is the broadcast group used +###by the 2441th thermostat to announce changes. sub new { my ($class, $p_deviceid) = @_; - my $self = new Insteon::Thermostat($p_deviceid); + my $self = new Insteon::BaseDevice($p_deviceid); bless $self, $class; return $self; } From cb07a83bbf2fd9c8a72f9f3dea8f80b8d5798e39 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 17:55:25 -0800 Subject: [PATCH 022/330] Change Insteon Thermostat i2 Broadcast Item Name --- lib/Insteon/Thermostat.pm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index d2d4f9569..28038b7f8 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -444,12 +444,14 @@ sub init { my $dev_id = $self->device_id(); $dev_id =~ /(\w\w)(\w\w)(\w\w)/; $dev_id = "$1.$2.$3"; - $$self{bcast} = new Insteon::Thermo_i2_bcast("$dev_id".':EF'); + $$self{bcast_item} = new Insteon::Thermo_i2_bcast("$dev_id".':EF'); + # Add bcast object to list of Insteon objects - Insteon::add($$self{bcast}); + Insteon::add($$self{bcast_item}); + # Register bcast object with MH - &main::register_object_by_name('$' . $self->get_object_name ."{bcast}",$$self{bcast}); - $$self{bcast}->{object_name} = '$' . $self->get_object_name ."{bcast}"; + &main::register_object_by_name('$' . $self->get_object_name ."{bcast_item}",$$self{bcast_item}); + $$self{bcast_item}->{object_name} = '$' . $self->get_object_name ."{bcast_item}"; ## Create the child objects my @child_objs = ("mode", "fan", "temp", "humidity", "setpoint_h", "setpoint_c"); From 55d578afd327cde0009a3422f5f2e4d073ab5ddb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:05:25 -0800 Subject: [PATCH 023/330] Create Insteon Thermostat i2 Child Device Objects --- lib/Insteon/Thermostat.pm | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 28038b7f8..482923902 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -479,6 +479,78 @@ sub new { return $self; } +package Insteon::Thermo_i2_mode; +use strict; + +@Insteon::Thermo_i2_mode::ISA = ('Generic_Item'); + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + +package Insteon::Thermo_i2_fan; +use strict; + +@Insteon::Thermo_i2_fan::ISA = ('Generic_Item'); + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + +package Insteon::Thermo_i2_temp; +use strict; + +@Insteon::Thermo_i2_temp::ISA = ('Generic_Item'); + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + +package Insteon::Thermo_i2_humidity; +use strict; + +@Insteon::Thermo_i2_humidity::ISA = ('Generic_Item'); + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + +package Insteon::Thermo_i2_setpoint_h; +use strict; + +@Insteon::Thermo_i2_setpoint_h::ISA = ('Generic_Item'); + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + +package Insteon::Thermo_i2_setpoint_c; +use strict; + +@Insteon::Thermo_i2_setpoint_c::ISA = ('Generic_Item'); + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + 1; =back From f052ccc1be69ffcc59dbe21c1f9097edbc501058 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:15:25 -0800 Subject: [PATCH 024/330] Create Insteon Thermostat i2 Child Items on Init --- lib/Insteon/Thermostat.pm | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 482923902..5b63705fe 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -454,13 +454,27 @@ sub init { $$self{bcast_item}->{object_name} = '$' . $self->get_object_name ."{bcast_item}"; ## Create the child objects - my @child_objs = ("mode", "fan", "temp", "humidity", "setpoint_h", "setpoint_c"); - #my $obj_group = ::get_object_by_name('HVAC'); - #$obj_group->add($$self{bcast}); - #&main::register_object_by_name($self->get_object_name ."{bcast}",$$self{bcast}); - #$$self{bcast}->{category} = "sample"; - #$$self{bcast}->{filename} = "sample"; - #$$self{bcast}->{object_name} = '$' . $self->get_object_name ."{bcast}"; + my @child_objs = ('mode_item', 'fan_item', 'temp_item', 'humidity_item', + 'setpoint_h_item', 'setpoint_c_item'); + foreach my $obj (@child_objs) { + $$self{$obj} = new Insteon::Thermo_i2_mode() if ($obj eq 'mode_item'); + $$self{$obj} = new Insteon::Thermo_i2_fan() if ($obj eq 'fan_item'); + $$self{$obj} = new Insteon::Thermo_i2_temp() if ($obj eq 'temp_item'); + $$self{$obj} = new Insteon::Thermo_i2_humidity() if ($obj eq 'humidity_item'); + $$self{$obj} = new Insteon::Thermo_i2_setpoint_h() if ($obj eq 'setpoint_h_item'); + $$self{$obj} = new Insteon::Thermo_i2_setpoint_c() if ($obj eq 'setpoint_c_item'); + + # Register child object with MH + &main::register_object_by_name('$' . $self->get_object_name ."{$obj}",$$self{$obj}); + $$self{$obj}->{object_name} = '$' . $self->get_object_name ."{$obj}"; + $$self{$obj}{parent} = $self; + + #Add child to the same groups as parent + foreach my $parent_group (::list_groups_by_object($self)){ + $parent_group->add($$self{$obj}); + } + } +} } package Insteon::Thermo_i2_bcast; From 5873c95a88fde211bd395c4238f47d9cff979db3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:25:25 -0800 Subject: [PATCH 025/330] Insteon::Thermo_i2 Inject Broadcast Setting Message into Sync_links The device requires that a specific flag be enabled for it to provide broadcast updates. Now, when sync_links is called, this flag will be automatically set. This is done by injecting code into the sync_links sub before calling the main sync_links in BaseDevice --- lib/Insteon/Thermostat.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 5b63705fe..d13944d12 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -475,6 +475,14 @@ sub init { } } } + +sub sync_links{ + my ($self, $audit_mode, $callback, $failure_callback) = @_; + #Make sure thermostat is set to broadcast changes + ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; + ## send command unless $audit_mode; + # Call the main sync_links code + return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); } package Insteon::Thermo_i2_bcast; From 60af877ab9036116e1a20fd01d3ff309e9d33779 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:35:25 -0800 Subject: [PATCH 026/330] Insteon::Thermo_i2 Add Message Types --- lib/Insteon/Thermostat.pm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index d13944d12..952b3de76 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -437,8 +437,14 @@ use strict; @Insteon::Thermo_i2::ISA = ('Insteon::Thermostat'); +my %message_types = ( + %Insteon::Thermostat::message_types, + extended_set_get => 0x2e +); + sub init { my ($self) = @_; + $$self{message_types} = \%message_types ## Create the broadcast dummy item my $dev_id = $self->device_id(); From aea4e57b7b68749d472ad3b918e8bbce5026f1c9 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:45:25 -0800 Subject: [PATCH 027/330] Insteon::Thermo_i2 Set Broadcast Flag with Sync Links --- lib/Insteon/BaseInsteon.pm | 1 + lib/Insteon/Thermostat.pm | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index dfdcbe5f7..91a58398e 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -740,6 +740,7 @@ sub _process_command_stack or $message->command eq 'read_write_aldb' or $message->command eq 'thermostat_control' or $message->command eq 'thermostat_get_zone_info' + or $message->command eq 'extended_set_get' ) { $$self{awaiting_ack} = 1; diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 952b3de76..03e3b55c7 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -444,7 +444,7 @@ my %message_types = ( sub init { my ($self) = @_; - $$self{message_types} = \%message_types + $$self{message_types} = \%message_types; ## Create the broadcast dummy item my $dev_id = $self->device_id(); @@ -486,7 +486,9 @@ sub sync_links{ my ($self, $audit_mode, $callback, $failure_callback) = @_; #Make sure thermostat is set to broadcast changes ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; - ## send command unless $audit_mode; + my $extra = "000008000000000000000000000000"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $self->_send_cmd($message); # Call the main sync_links code return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); } From 65ed4bf87ad2ef246e4fd2f9cc118d264a917cd2 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:50:25 -0800 Subject: [PATCH 028/330] Insteon::Thermo_i2 Fix Debug Logging --- lib/Insteon.pm | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index c173e9cb3..166d5e61a 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -558,26 +558,29 @@ sub check_all_aldb_versions sub check_thermo_versions { - main::print_log("[Insteon] DEBUG4 Checking thermostat versions") if ($main::Debug{insteon} >= 4); + #main::print_log("[Insteon] DEBUG4 Checking thermostat versions") if ($main::Debug{insteon} >= 4); my @thermo_devices = (); push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); foreach my $thermo_device (@thermo_devices) { - main::print_log("[Insteon] DEBUG4 Checking thermostat version for " - . $thermo_device->get_object_name()) - if ($main::Debug{insteon} >= 4); if ($thermo_device->isa('Insteon::Thermostat') && $thermo_device->_aldb->aldb_version() eq "I2"){ + main::print_log("[Insteon] DEBUG4 Setting thermostat " + . $thermo_device->get_object_name() . " to i2") + if ($main::Debug{insteon} >= 4); bless $thermo_device, 'Insteon::Thermo_i2'; $thermo_device->init(); } elsif ($thermo_device->isa('Insteon::Thermostat') && $thermo_device->_aldb->aldb_version() eq "I1"){ + main::print_log("[Insteon] DEBUG4 Setting thermostat " + . $thermo_device->get_object_name() . " to i2") + if ($main::Debug{insteon} >= 4); bless $thermo_device, 'Insteon::Thermo_i1'; } } - main::print_log("[Insteon] DEBUG4 Checking thermostat version of all devices completed") if ($main::Debug{insteon} >= 4); + #main::print_log("[Insteon] DEBUG4 Checking thermostat version of all devices completed") if ($main::Debug{insteon} >= 4); } From 4f543821f14bf18b597cb9a5e0264b8ba68edcf4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 18:55:25 -0800 Subject: [PATCH 029/330] Insteon::Thermo_i2 Fix inheritance problem in message_types --- lib/Insteon/Thermostat.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 03e3b55c7..2bcd542b0 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -96,7 +96,7 @@ use Insteon::BaseInsteon; # -------------------- START OF SUBROUTINES -------------------- # -------------------------------------------------------------- -my %message_types = ( +our %message_types = ( %Insteon::BaseDevice::message_types, thermostat_temp_up => 0x68, thermostat_temp_down => 0x69, @@ -437,7 +437,7 @@ use strict; @Insteon::Thermo_i2::ISA = ('Insteon::Thermostat'); -my %message_types = ( +our %message_types = ( %Insteon::Thermostat::message_types, extended_set_get => 0x2e ); From feb2c895c80d495f5521507bca55ddfef0b5982a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 13 Mar 2013 20:21:14 -0700 Subject: [PATCH 030/330] Insteon::Thermo_i2 Child default states and tie parent state to child Add default states to child items. Tie changes in the parent item so that they cause state updates in the children Fix error with get_setpoint when used in auto mode. --- lib/Insteon/Thermostat.pm | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 2bcd542b0..a29c376f9 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -369,15 +369,11 @@ sub _is_info_request { $$self{'m_pending_setpoint'} = 1; } elsif ($self->get_mode() eq 'heat' or 'program_heat') { $self->_heat_sp($val); + $$self{_zone_action} = undef; } elsif ($self->get_mode() eq 'cool' or 'program_cool') { $self->_cool_sp($val); + $$self{_zone_action} = undef; } - $$self{_zone_action} = undef; - } elsif ($$self{'m_pending_setpoint'} == 1) { - #This is the second message with the cool_setpoint - $val = (hex $val) / 2; - $self->_cool_sp($val); - $$self{'m_pending_setpoint'} = undef; } } else #This was not a thermostat info_request @@ -396,9 +392,11 @@ sub _process_message my $clear_message = 0; if ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { # we got our cool setpoint in auto mode + main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $main::Debug{insteon}; my $val = (hex $msg{extra})/2; $self->_cool_sp($val); $$self{m_setpoint_pending} = 0; + $$self{_zone_action} = undef; $clear_message = 1; } else { $clear_message = $self->SUPER::_process_message($p_setby,%msg); @@ -480,6 +478,15 @@ sub init { $parent_group->add($$self{$obj}); } } + #Set child saved states + $$self{temp_item}->set($self->get_temp()); + $$self{setpoint_h_item}->set($self->get_heat_sp()); + $$self{setpoint_c_item}->set($self->get_cool_sp()); + $$self{fan_item}->set($self->get_fan_mode()); + $$self{mode_item}->set($self->get_mode()); + + #Tie changes in parent item to children + $self -> tie_event ('Insteon::Thermo_i2::parent_event(\''.$$self{object_name} . '\', "$state")'); } sub sync_links{ @@ -493,6 +500,26 @@ sub sync_links{ return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); } +sub parent_event { + my ($self, $p_state) = @_; + $self = ::get_object_by_name($self); + if ($p_state eq 'temp_change'){ + $$self{temp_item}->set($self->get_temp()); + } + elsif ($p_state eq 'heat_setpoint_change'){ + $$self{setpoint_h_item}->set($self->get_heat_sp()); + } + elsif ($p_state eq 'cool_setpoint_change'){ + $$self{setpoint_c_item}->set($self->get_cool_sp()); + } + elsif ($p_state eq 'fan_mode_change'){ + $$self{fan_item}->set($self->get_fan_mode()); + } + elsif ($p_state eq 'mode_change'){ + $$self{mode_item}->set($self->get_mode()); + } +} + package Insteon::Thermo_i2_bcast; use strict; @@ -518,6 +545,7 @@ sub new { my ($class) = @_; my $self = new Generic_Item(); bless $self, $class; + @{$$self{states}} = ('Off', 'Heat', 'Cool', 'Auto'); return $self; } @@ -530,6 +558,7 @@ sub new { my ($class) = @_; my $self = new Generic_Item(); bless $self, $class; + @{$$self{states}} = ('Auto', 'On'); return $self; } @@ -566,6 +595,7 @@ sub new { my ($class) = @_; my $self = new Generic_Item(); bless $self, $class; + @{$$self{states}} = ('Cooler' , 'Warmer'); return $self; } @@ -578,6 +608,7 @@ sub new { my ($class) = @_; my $self = new Generic_Item(); bless $self, $class; + @{$$self{states}} = ('Cooler', 'Warmer'); return $self; } From 9a1a05b80374f8292ac392718412fa9aa96ae248 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 15:45:25 -0800 Subject: [PATCH 031/330] Insteon::Thermo_i2 Only put child objects into non-recursive groups Child objects should only be put into the groups which the parent is directly a member of, not the parent groups of the member. --- lib/Insteon/Thermostat.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index a29c376f9..bc9559fb3 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -474,7 +474,7 @@ sub init { $$self{$obj}{parent} = $self; #Add child to the same groups as parent - foreach my $parent_group (::list_groups_by_object($self)){ + foreach my $parent_group (::list_groups_by_object($self,1)){ $parent_group->add($$self{$obj}); } } From 59d0113bfef89f6f81136eb39c76fd9da2a1ef28 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 20:56:30 -0700 Subject: [PATCH 032/330] Insteon:Thermo_i2 Add Poll All Request, and Processing One of the 0x2E commands for the i2 devices provides a single response that contains almost all of the data we would need to get started. Not sure how often this message would need to be called, as we will be getting status updates. --- lib/Insteon/Thermostat.pm | 102 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index bc9559fb3..7a82a9e25 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -518,6 +518,108 @@ sub parent_event { elsif ($p_state eq 'mode_change'){ $$self{mode_item}->set($self->get_mode()); } + elsif ($p_state eq 'humid_change'){ + $$self{humidity_item}->set($$self{humid}); + } +} + +sub poll_simple{ + my ($self) = @_; + my $extra = "020000000000000000000000000000"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$message{add_crc16} = 1; + $self->_send_cmd($message); +} + +sub _process_message { + my ($self,$p_setby,%msg) = @_; + my $clear_message = 0; + if ($msg{command} eq "extended_set_get" && $msg{is_ack}){ + ##Don't clear until data packet received + main::print_log("[Insteon::Thermo_i2] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + } + elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { + if (substr($msg{extra},0,4) eq "0201") { + main::print_log("[Insteon::Thermo_i2] Extended Set/Get Data ". + "Received for ". $self->get_object_name) if $main::Debug{insteon}; + #0 = 2 #14 = Cool SP + #2 = 1 #16 = humidity + #3 = day #18 = temp in Celsius High byte + #6 = hour #20 = temp low byte + #8 = minute #22 = status flag + #10 = second #24 = Heat SP + #12 = Sys_mode * 16 + Fan_mode + my $mode = hex(substr($msg{extra}, 12, 2)); + my $fan_mode = ($mode % 16); + $self->dec_mode(($mode - $fan_mode) / 16); + $self->dec_fan($fan_mode); + $self->hex_cool(substr($msg{extra}, 14, 2)); + $self->hex_humid(substr($msg{extra}, 16, 2)); + $self->hex_long_temp(substr($msg{extra}, 18, 4)); + $self->hex_status(substr($msg{extra}, 22, 2)); + $self->hex_heat(substr($msg{extra}, 24, 2)); + $clear_message = 1; + $self->_process_command_stack(%msg); + } else { + main::print_log("[Insteon::Thermo_i2] WARN: Corrupt Extended " + ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; + } + } + else { + $clear_message = $self->SUPER::_process_message($p_setby,%msg); + } + return $clear_message; +} + +sub dec_mode{ + my ($self, $dec_mode) = @_; + my $mode; + $mode = 'Off' if ($dec_mode == 0); + $mode = 'Auto' if ($dec_mode == 1); + $mode = 'Heat' if ($dec_mode == 2); + $mode = 'Cool' if ($dec_mode == 3); + $mode = 'Program' if ($dec_mode == 4); + $self->_mode($mode); +} +sub dec_fan{ + my ($self, $dec_fan) = @_; + my $fan; + $fan = 'Auto' if ($dec_fan == 0); + $fan = 'Always On' if ($dec_fan == 1); + $self->_fan_mode($fan); +} + +sub hex_cool{ + my ($self, $hex_cool) = @_; + $self->_cool_sp(hex($hex_cool)); +} +sub hex_humid{ + my ($self, $hex_humid) = @_; + $self->_humid(hex($hex_humid)); +} +sub hex_long_temp{ + my ($self, $hex_temp) = @_; + my $temp_cel = (hex($hex_temp)/10); + ## ATM I am going to assume farenheit b/c that is what I have + # in future, can pull setting bit from thermometer + $$self{temp} = (($temp_cel*9)/5 +32); + $self->set_receive('temp_change'); +} +sub hex_status{ + ### Not sure about this one yet, was 80 when set to auto but no activity +} +sub hex_heat{ + my ($self, $hex_heat) = @_; + $self->_heat_sp(hex($hex_heat)); +} + +sub _humid { + my ($self,$p_state) = @_; + if ($p_state ne $$self{humid}) { + $$self{humid} = $p_state; + $self->set_receive('humid_change'); + } + return $$self{humid}; } package Insteon::Thermo_i2_bcast; From 1c7c0e8fe0dc6735b5b0a9bda24662039ff9eb6a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 15:45:25 -0800 Subject: [PATCH 033/330] Modify list_groups_by_object to allow for non-recursive listing list_groups_by_object identifies all of the groups that an object is a member of. Currently, if an object is a member of a child group, the child and parent group will be returned in the list. This change allows for only the groups which the object is a direct member of to be returned. --- bin/mh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/mh b/bin/mh index 5e23a889a..1f7d5ee3f 100755 --- a/bin/mh +++ b/bin/mh @@ -3186,12 +3186,12 @@ sub list_files_by_webname { } sub list_groups_by_object { - my ($object) = @_; + my ($object, $no_child_members) = @_; my @groups; for my $group_name (&list_objects_by_type('Group')) { my $group = &get_object_by_name($group_name); # print "testing group=$group_name -> $group\n"; - for my $object2 (list $group) { + for my $object2 ($group->list(undef, undef,$no_child_members)) { push @groups, $group if $object eq $object2; } } From ac56b79f95972fb48e460aaf2dca4f32975fe9a8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 22:36:36 -0700 Subject: [PATCH 034/330] Insteon::Thermo_i2 Save and Restore Humidity State --- lib/Insteon/Thermostat.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 7a82a9e25..64b929a2c 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -478,12 +478,16 @@ sub init { $parent_group->add($$self{$obj}); } } + #Set saved state unique to i2 + $self->restore_data('humid'); + #Set child saved states $$self{temp_item}->set($self->get_temp()); $$self{setpoint_h_item}->set($self->get_heat_sp()); $$self{setpoint_c_item}->set($self->get_cool_sp()); $$self{fan_item}->set($self->get_fan_mode()); $$self{mode_item}->set($self->get_mode()); + $$self{humidity_item}->set($$self{humid}); #Tie changes in parent item to children $self -> tie_event ('Insteon::Thermo_i2::parent_event(\''.$$self{object_name} . '\', "$state")'); From 88d779144a2ff0074d5619a094efc3a41a6bf1bb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 22:37:41 -0700 Subject: [PATCH 035/330] Insteon::Thermo_i2 Add Status Message Types --- lib/Insteon/Thermostat.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 64b929a2c..9715f0acb 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -437,7 +437,12 @@ use strict; our %message_types = ( %Insteon::Thermostat::message_types, - extended_set_get => 0x2e + extended_set_get => 0x2e, + status_temp => 0x6e, + status_humid => 0x6f, + status_mode => 0x70, + status_cool => 0x71, + status_heat => 0x72 ); sub init { From 85135d832efac34de9277c719a70926f3c135384 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 22:38:28 -0700 Subject: [PATCH 036/330] Insteon::Thermo_i2 Add subs to interpret status mode and temp Insteon is so annoying, why can't it pick a single format for the same data? Anyways, temp in status message is tempx2, instead of long temp form. In addition, for reasons beyond comprehension, the mode in a status message conveys the same data, but uses a different flag than a extended get response. So stupid. --- lib/Insteon/Thermostat.pm | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 9715f0acb..e6fb23491 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -590,6 +590,22 @@ sub dec_mode{ $mode = 'Program' if ($dec_mode == 4); $self->_mode($mode); } + +sub status_mode{ + my ($self, $status_mode) = @_; + my $mode; + my $conv_mode = (hex($status_mode)%16); + $mode = 'Off' if ($conv_mode == 0); + $mode = 'Heat' if ($conv_mode == 1); + $mode = 'Cool' if ($conv_mode == 2); + $mode = 'Auto' if ($conv_mode == 3); + $mode = 'Program' if ($conv_mode == 4); + $self->_mode($mode); + my $fan_mode; + $fan_mode = (hex($status_mode) >= 16) ? 'Always On' : 'Auto'; + $self->_fan_mode($fan_mode); +} + sub dec_fan{ my ($self, $dec_fan) = @_; my $fan; @@ -614,6 +630,13 @@ sub hex_long_temp{ $$self{temp} = (($temp_cel*9)/5 +32); $self->set_receive('temp_change'); } + +sub hex_short_temp{ + my ($self, $hex_temp) = @_; + $$self{temp} = (hex($hex_temp)/2); + $self->set_receive('temp_change'); +} + sub hex_status{ ### Not sure about this one yet, was 80 when set to auto but no activity } From fb2e38dd79b12f259ca93d71cbf3cf7acf0eda25 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Mar 2013 22:40:40 -0700 Subject: [PATCH 037/330] Insteon::Thermo_i2 add steps in _process_message to catch and process status messages --- lib/Insteon/Thermostat.pm | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index e6fb23491..1c1b89ba1 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -574,6 +574,31 @@ sub _process_message { ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; } } + elsif ($msg{command} eq "status_temp" && !$msg{is_ack}){ + main::print_log("[Insteon::Thermo_i2] Received Status Temp Message ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->hex_short_temp($msg{extra}); + } + elsif ($msg{command} eq "status_mode" && !$msg{is_ack}){ + main::print_log("[Insteon::Thermo_i2] Received Status Mode Message ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->status_mode($msg{extra}); + } + elsif ($msg{command} eq "status_cool" && !$msg{is_ack}){ + main::print_log("[Insteon::Thermo_i2] Received Status Cool Message ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->hex_cool($msg{extra}); + } + elsif ($msg{command} eq "status_humid" && !$msg{is_ack}){ + main::print_log("[Insteon::Thermo_i2] Received Status Humid Message ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->hex_humid($msg{extra}); + } + elsif ($msg{command} eq "status_heat" && !$msg{is_ack}){ + main::print_log("[Insteon::Thermo_i2] Received Status Heat Message ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->hex_heat($msg{extra}); + } else { $clear_message = $self->SUPER::_process_message($p_setby,%msg); } From 5f47ec1b1256b534d26019a9a75aa6b3f8160d28 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 18 Mar 2013 21:59:07 -0700 Subject: [PATCH 038/330] Insteon:Thermostat Distinguish Between Extended Set/Get Messages that Need to be Cleared --- lib/Insteon/Thermostat.pm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 1c1b89ba1..77c7c6ee6 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -504,6 +504,7 @@ sub sync_links{ ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; my $extra = "000008000000000000000000000000"; my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$self{_ext_set_get_action} = 'set'; $self->_send_cmd($message); # Call the main sync_links code return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); @@ -544,8 +545,14 @@ sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; if ($msg{command} eq "extended_set_get" && $msg{is_ack}){ - ##Don't clear until data packet received + #If this was a get request don't clear until data packet received main::print_log("[Insteon::Thermo_i2] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + if ($$self{_ext_set_get_action} eq 'set'){ + main::print_log("[Insteon::Thermo_i2] Clearing active message") if $main::Debug{insteon}; + $clear_message = 1; + $$self{_ext_set_get_action} = undef; + $self->_process_command_stack(%msg); + } } elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { if (substr($msg{extra},0,4) eq "0201") { From 2908e6cd251836ab90d96b00c25c1677714bdc9f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 18 Mar 2013 22:00:12 -0700 Subject: [PATCH 039/330] Insteon:Thermo_i2 Add Set Routine to Children --- lib/Insteon/Thermostat.pm | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 77c7c6ee6..6cc9aff43 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -715,6 +715,21 @@ sub new { return $self; } +sub set { + my ($self, $p_state, $p_setby, $p_response) = @_; + my $found_state = 0; + foreach my $test_state (@{$$self{states}}){ + if (lc($test_state) eq lc($p_state)){ + $found_state = 1; + } + } + if ($found_state){ + ::print_log("[Insteon::Thermo_i2] Received set mode request to " + . $p_state . " for device " . $self->get_object_name); + } +} + + package Insteon::Thermo_i2_fan; use strict; @@ -728,6 +743,20 @@ sub new { return $self; } +sub set { + my ($self, $p_state, $p_setby, $p_response) = @_; + my $found_state = 0; + foreach my $test_state (@{$$self{states}}){ + if (lc($test_state) eq lc($p_state)){ + $found_state = 1; + } + } + if ($found_state){ + ::print_log("[Insteon::Thermo_i2] Received set mode request to " + . $p_state . " for device " . $self->get_object_name); + } +} + package Insteon::Thermo_i2_temp; use strict; @@ -765,6 +794,20 @@ sub new { return $self; } +sub set { + my ($self, $p_state, $p_setby, $p_response) = @_; + my $found_state = 0; + foreach my $test_state (@{$$self{states}}){ + if (lc($test_state) eq lc($p_state)){ + $found_state = 1; + } + } + if ($found_state){ + ::print_log("[Insteon::Thermo_i2] Received set mode request to " + . $p_state . " for device " . $self->get_object_name); + } +} + package Insteon::Thermo_i2_setpoint_c; use strict; @@ -778,6 +821,20 @@ sub new { return $self; } +sub set { + my ($self, $p_state, $p_setby, $p_response) = @_; + my $found_state = 0; + foreach my $test_state (@{$$self{states}}){ + if (lc($test_state) eq lc($p_state)){ + $found_state = 1; + } + } + if ($found_state){ + ::print_log("[Insteon::Thermo_i2] Received set mode request to " + . $p_state . " for device " . $self->get_object_name); + } +} + 1; =back From 3f94bc043940799a4ff724262229959ac0419c91 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 21:28:54 -0700 Subject: [PATCH 040/330] Insteon::Thermostat Split Mode Routine into i1 and i2 Specific Routines For whatever reason the commands are different between the two --- lib/Insteon/Thermostat.pm | 98 +++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 6cc9aff43..b6f8929ad 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -133,39 +133,6 @@ sub poll_mode { return; } -=item C - -Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', -'program_cool', 'program_auto'. The 2441TH thermostat does not have program_heat - or program_cool. -=cut -sub mode{ - my ($self, $state) = @_; - $state = lc($state); - main::print_log("[Insteon::Thermostat] Mode $state") if $main::Debug{insteon}; - my $mode; - if ($state eq 'off') { - $mode = "09"; - } elsif ($state eq 'heat') { - $mode = "04"; - } elsif ($state eq 'cool') { - $mode = "05"; - } elsif ($state eq 'auto') { - $mode = "06"; - } elsif ($state eq 'program_heat') { - $mode = "0a"; - } elsif ($state eq 'program_cool') { - $mode = "0b"; - } elsif ($state eq 'program_auto') { - $mode = "0c"; - $mode = "0a" if $self->_aldb->isa('Insteon::ALDB_i2'); - } else { - main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); - return(); - } - $self->_send_cmd($self->simple_message('thermostat_control', $mode)); -} - =item C Sets fan to 'on' or 'auto' @@ -429,6 +396,39 @@ use strict; @Insteon::Thermo_i1::ISA = ('Insteon::Thermostat'); +=item C + +Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', +'program_cool', 'program_auto'. The 2441TH thermostat does not have program_heat + or program_cool. +=cut +sub mode{ + my ($self, $state) = @_; + $state = lc($state); + main::print_log("[Insteon::Thermostat] Mode $state") if $main::Debug{insteon}; + my $mode; + if ($state eq 'off') { + $mode = "09"; + } elsif ($state eq 'heat') { + $mode = "04"; + } elsif ($state eq 'cool') { + $mode = "05"; + } elsif ($state eq 'auto') { + $mode = "06"; + } elsif ($state eq 'program_heat') { + $mode = "0a"; + } elsif ($state eq 'program_cool') { + $mode = "0b"; + } elsif ($state eq 'program_auto') { + $mode = "0c"; + } else { + main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); + return(); + } + $$self{_control_action} = "mode"; + $self->_send_cmd($self->simple_message('thermostat_control', $mode)); +} + package Insteon::Thermo_i2; use strict; @@ -686,6 +686,36 @@ sub _humid { return $$self{humid}; } +=item C + +Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', +'program_cool', 'program_auto'. The 2441TH thermostat does not have program_heat + or program_cool. +=cut +sub mode{ + my ($self, $state) = @_; + $state = lc($state); + main::print_log("[Insteon::Thermostat] Mode $state") if $main::Debug{insteon}; + my $mode; + if ($state eq 'off') { + $mode = "09"; + } elsif ($state eq 'heat') { + $mode = "04"; + } elsif ($state eq 'cool') { + $mode = "05"; + } elsif ($state eq 'auto') { + $mode = "06"; + } elsif ($state eq 'program') { + $mode = "0a" if $self->_aldb->isa('Insteon::ALDB_i2'); + } else { + main::print_log("[Insteon::Thermostat] ERROR: Invalid Mode state: $state"); + return(); + } + $$self{_control_action} = "mode"; + $self->_send_cmd($self->simple_message('thermostat_control', $mode)); +} + + package Insteon::Thermo_i2_bcast; use strict; @@ -711,7 +741,7 @@ sub new { my ($class) = @_; my $self = new Generic_Item(); bless $self, $class; - @{$$self{states}} = ('Off', 'Heat', 'Cool', 'Auto'); + @{$$self{states}} = ('Off', 'Heat', 'Cool', 'Auto', 'Program'); return $self; } From 7b7e3883102732575f647118b0f6a9e6ff045312 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 21:31:43 -0700 Subject: [PATCH 041/330] Insteon::Thermostat Split _is_info_request for Mode Change into i1 and i2 The response from the devices use different bytes to signify their modes --- lib/Insteon/Thermostat.pm | 89 ++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index b6f8929ad..f8c7349e8 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -296,38 +296,16 @@ sub get_fan_mode() { sub _is_info_request { my ($self, $cmd, $ack_setby, %msg) = @_; - my $is_info_request = ($cmd eq 'thermostat_get_zone_info' - or $cmd eq 'thermostat_control') ? 1 : 0; + my $is_info_request = ($cmd eq 'thermostat_get_zone_info') ? 1 : 0; if ($is_info_request) { my $val = $msg{extra}; - main::print_log("[Insteon::Thermostat] Processing data for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; if ($$self{_zone_action} eq "temp") { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { $self->set_receive('temp_change'); } $$self{'temp'} = $val; - } elsif ($$self{_control_action} eq "mode") { - if ($val eq '00') { - $self->_mode('off'); - } elsif ($val eq '01') { - $self->_mode('heat'); - } elsif ($val eq '02') { - $self->_mode('cool'); - } elsif ($val eq '03') { - $self->_mode('auto'); - } elsif ($val eq '04') { - $self->_fan_mode('fan_on'); - } elsif ($val eq '05') { - $self->_mode('program_auto'); - } elsif ($val eq '06') { - $self->_mode('program_heat'); - } elsif ($val eq '07') { - $self->_mode('program_cool'); - } elsif ($val eq '08') { - $self->_fan_mode('fan_auto'); - } - $$self{_control_action} = undef; } elsif ($$self{_zone_action} eq 'setpoint') { $val = (hex $val) / 2; # returned value is twice the real value # in auto modes, expect direct message with cool_setpoint to follow @@ -429,6 +407,41 @@ sub mode{ $self->_send_cmd($self->simple_message('thermostat_control', $mode)); } +sub _is_info_request { + my ($self, $cmd, $ack_setby, %msg) = @_; + my $is_info_request; + if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { + my $val = $msg{extra}; + main::print_log("[Insteon::Thermo_i1] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; + if ($val eq '00') { + $self->_mode('off'); + } elsif ($val eq '01') { + $self->_mode('heat'); + } elsif ($val eq '02') { + $self->_mode('cool'); + } elsif ($val eq '03') { + $self->_mode('auto'); + } elsif ($val eq '04') { + $self->_fan_mode('fan_on'); + } elsif ($val eq '05') { + $self->_mode('program_auto'); + } elsif ($val eq '06') { + $self->_mode('program_heat'); + } elsif ($val eq '07') { + $self->_mode('program_cool'); + } elsif ($val eq '08') { + $self->_fan_mode('fan_auto'); + } + $$self{_control_action} = undef; + $is_info_request = 1; + } + else #This was not a thermo_1 info_request + { + #Check if this was a generic info_request + $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); + } + return $is_info_request; +} package Insteon::Thermo_i2; use strict; @@ -612,6 +625,34 @@ sub _process_message { return $clear_message; } +sub _is_info_request { + my ($self, $cmd, $ack_setby, %msg) = @_; + my $is_info_request; + if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { + my $val = $msg{extra}; + main::print_log("[Insteon::Thermo_i2] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; + if ($val eq '09') { + $self->_mode('Off'); + } elsif ($val eq '04') { + $self->_mode('Heat'); + } elsif ($val eq '05') { + $self->_mode('Cool'); + } elsif ($val eq '06') { + $self->_mode('Auto'); + } elsif ($val eq '0a') { + $self->_mode('Program'); + } + $$self{_control_action} = undef; + $is_info_request = 1; + } + else #This was not a thermo_i2 info_request + { + #Check if this was a generic info_request + $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); + } + return $is_info_request; +} + sub dec_mode{ my ($self, $dec_mode) = @_; my $mode; From 1ea9faa70384d000d50c24f715e59b84757f6ed5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 21:34:20 -0700 Subject: [PATCH 042/330] Insteon::Thermo_i2 Create set_receive Routines in Child Objects Need to distinguish between a command that we send to set the device and commands received in which the device notifies MH of a new state --- lib/Insteon/Thermostat.pm | 50 +++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index f8c7349e8..dd82aad48 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -500,12 +500,12 @@ sub init { $self->restore_data('humid'); #Set child saved states - $$self{temp_item}->set($self->get_temp()); - $$self{setpoint_h_item}->set($self->get_heat_sp()); - $$self{setpoint_c_item}->set($self->get_cool_sp()); - $$self{fan_item}->set($self->get_fan_mode()); - $$self{mode_item}->set($self->get_mode()); - $$self{humidity_item}->set($$self{humid}); + $$self{temp_item}->set_receive($self->get_temp()); + $$self{setpoint_h_item}->set_receive($self->get_heat_sp()); + $$self{setpoint_c_item}->set_receive($self->get_cool_sp()); + $$self{fan_item}->set_receive($self->get_fan_mode()); + $$self{mode_item}->set_receive($self->get_mode()); + $$self{humidity_item}->set_receive($$self{humid}); #Tie changes in parent item to children $self -> tie_event ('Insteon::Thermo_i2::parent_event(\''.$$self{object_name} . '\', "$state")'); @@ -527,22 +527,22 @@ sub parent_event { my ($self, $p_state) = @_; $self = ::get_object_by_name($self); if ($p_state eq 'temp_change'){ - $$self{temp_item}->set($self->get_temp()); + $$self{temp_item}->set_receive($self->get_temp()); } elsif ($p_state eq 'heat_setpoint_change'){ - $$self{setpoint_h_item}->set($self->get_heat_sp()); + $$self{setpoint_h_item}->set_receive($self->get_heat_sp()); } elsif ($p_state eq 'cool_setpoint_change'){ - $$self{setpoint_c_item}->set($self->get_cool_sp()); + $$self{setpoint_c_item}->set_receive($self->get_cool_sp()); } elsif ($p_state eq 'fan_mode_change'){ - $$self{fan_item}->set($self->get_fan_mode()); + $$self{fan_item}->set_receive($self->get_fan_mode()); } elsif ($p_state eq 'mode_change'){ - $$self{mode_item}->set($self->get_mode()); + $$self{mode_item}->set_receive($self->get_mode()); } elsif ($p_state eq 'humid_change'){ - $$self{humidity_item}->set($$self{humid}); + $$self{humidity_item}->set_receive($$self{humid}); } } @@ -800,6 +800,10 @@ sub set { } } +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} package Insteon::Thermo_i2_fan; use strict; @@ -828,6 +832,10 @@ sub set { } } +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} package Insteon::Thermo_i2_temp; use strict; @@ -840,6 +848,10 @@ sub new { return $self; } +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} package Insteon::Thermo_i2_humidity; use strict; @@ -852,6 +864,11 @@ sub new { return $self; } +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} + package Insteon::Thermo_i2_setpoint_h; use strict; @@ -879,6 +896,11 @@ sub set { } } +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} + package Insteon::Thermo_i2_setpoint_c; use strict; @@ -906,6 +928,10 @@ sub set { } } +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} 1; =back From f861c331a58412741d5becb0313d48b3df977af0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 21:36:42 -0700 Subject: [PATCH 043/330] Insteon::Thermo_i2 Allow Changes to Child Objects to be Propogated to Parent Item --- lib/Insteon/Thermostat.pm | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index dd82aad48..bcda5c40a 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -797,6 +797,7 @@ sub set { if ($found_state){ ::print_log("[Insteon::Thermo_i2] Received set mode request to " . $p_state . " for device " . $self->get_object_name); + $$self{parent}->mode($p_state); } } @@ -827,8 +828,9 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received set mode request to " + ::print_log("[Insteon::Thermo_i2] Received set fan to " . $p_state . " for device " . $self->get_object_name); + $$self{parent}->fan($p_state); } } @@ -891,8 +893,14 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received set mode request to " + ::print_log("[Insteon::Thermo_i2] Received request to set heat setpoint " . $p_state . " for device " . $self->get_object_name); + if (lc($p_state) eq 'cooler'){ + $$self{parent}->heat_setpoint($$self{parent}->get_heat_sp - 1); + } + elsif (lc($p_state) eq 'warmer'){ + $$self{parent}->heat_setpoint($$self{parent}->get_heat_sp + 1); + } } } @@ -923,8 +931,14 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received set mode request to " + ::print_log("[Insteon::Thermo_i2] Received request to set cool setpoint " . $p_state . " for device " . $self->get_object_name); + if (lc($p_state) eq 'cooler'){ + $$self{parent}->cool_setpoint($$self{parent}->get_cool_sp - 1); + } + elsif (lc($p_state) eq 'warmer'){ + $$self{parent}->cool_setpoint($$self{parent}->get_cool_sp + 1); + } } } From c44c80e42205e4df32853bfaedabbb3c4a03605b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 21:37:46 -0700 Subject: [PATCH 044/330] Insteon::Thermo_i2 Catch ACK of Setpoint Changes and Update State --- lib/Insteon/Thermostat.pm | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index bcda5c40a..bc09a61dc 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -335,7 +335,19 @@ sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; - if ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { + if ($msg{command} eq "thermostat_setpoint_cool" && $msg{is_ack}){ + main::print_log("[Insteon::Thermostat] Received ACK of cool setpoint ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->_cool_sp((hex($msg{extra})/2)); + $clear_message = 1; + } + elsif ($msg{command} eq "thermostat_setpoint_heat" && $msg{is_ack}){ + main::print_log("[Insteon::Thermostat] Received ACK of heat setpoint ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->_heat_sp((hex($msg{extra})/2)); + $clear_message = 1; + } + elsif ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { # we got our cool setpoint in auto mode main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $main::Debug{insteon}; my $val = (hex $msg{extra})/2; From f8c55ec51dc80c1fbecc80e5d48de46b53797dd2 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 21:46:17 -0700 Subject: [PATCH 045/330] Insteon::Thermostat Split simple_message into i1 and i2 --- lib/Insteon/Thermostat.pm | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index bc09a61dc..9cce2f867 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -361,20 +361,6 @@ sub _process_message return $clear_message; } -## Creates either a Standard or Extended Message depending on the device type -## Can be used to create different classes later -sub simple_message { - my ($self,$type,$extra) = @_; - my $message; - if ($self->_aldb->isa('Insteon::ALDB_i2')){ - $extra = $extra . "0000000000000000000000000000"; - $message = new Insteon::InsteonMessage('insteon_ext_send', $self, $type, $extra); - } else { - $message = new Insteon::InsteonMessage('insteon_send', $self, $type, $extra); - } - return $message; -} - # Overload methods we don't use, but would otherwise cause Insteon traffic. sub request_status { return 0 } @@ -455,6 +441,14 @@ sub _is_info_request { return $is_info_request; } +## Creates a simple Standard Message +sub simple_message { + my ($self,$type,$extra) = @_; + my $message; + $message = new Insteon::InsteonMessage('insteon_send', $self, $type, $extra); + return $message; +} + package Insteon::Thermo_i2; use strict; @@ -768,6 +762,14 @@ sub mode{ $self->_send_cmd($self->simple_message('thermostat_control', $mode)); } +## Creates an Extended Message +sub simple_message { + my ($self,$type,$extra) = @_; + my $message; + $extra = $extra . "0000000000000000000000000000"; + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, $type, $extra); + return $message; +} package Insteon::Thermo_i2_bcast; use strict; From 779bbb34ce0f491ec78e0943358ec121d16500f6 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 19 Mar 2013 22:33:10 -0700 Subject: [PATCH 046/330] Insteon::Thermo_i2 Added sync_time Function Syncs the thermostat time to your computer time, as simple as that --- lib/Insteon/Thermostat.pm | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 9cce2f867..224f660ab 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -595,6 +595,26 @@ sub _process_message { $self->hex_heat(substr($msg{extra}, 24, 2)); $clear_message = 1; $self->_process_command_stack(%msg); + if ($$self{set_time}){ + #This poll was requested as part of sync_time + my $message; + my $extra; + my @time_array = localtime(time); + my @req_items = ($time_array[6], $time_array[2], + $time_array[1], $time_array[0]); + my $time_str = ''; + foreach (@req_items){ + $time_str .= sprintf("%02d", $_); + } + $extra = $extra . "0202". $time_str . substr($msg{extra}, 12, 18); + #This will include the prior CRC16 message, but it will + #get overwritten with the correct value in Message.pm + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$message{add_crc16} = 1; + $$self{_ext_set_get_action} = 'set'; + $$self{set_time} = undef; + $self->_send_cmd($message); + } } else { main::print_log("[Insteon::Thermo_i2] WARN: Corrupt Extended " ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; @@ -771,6 +791,15 @@ sub simple_message { return $message; } +sub sync_time { + my ($self) = @_; + #In order to set the time, we need to know the current value of other data + #points such as mode and what not becuase we can't just set the time without + #setting these variables too. + $$self{set_time} = 1; + $self->poll_simple(); +} + package Insteon::Thermo_i2_bcast; use strict; From 304b7026445f300e759090f688a5b381c21a903e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 20 Mar 2013 22:12:16 -0700 Subject: [PATCH 047/330] Insteon::Thermostat Add Hop Tracking --- lib/Insteon/Thermostat.pm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 224f660ab..1de662ee3 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -336,18 +336,21 @@ sub _process_message my ($self,$p_setby,%msg) = @_; my $clear_message = 0; if ($msg{command} eq "thermostat_setpoint_cool" && $msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of cool setpoint ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->_cool_sp((hex($msg{extra})/2)); $clear_message = 1; } elsif ($msg{command} eq "thermostat_setpoint_heat" && $msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of heat setpoint ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->_heat_sp((hex($msg{extra})/2)); $clear_message = 1; } elsif ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); # we got our cool setpoint in auto mode main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $main::Debug{insteon}; my $val = (hex $msg{extra})/2; @@ -564,6 +567,7 @@ sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; if ($msg{command} eq "extended_set_get" && $msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received main::print_log("[Insteon::Thermo_i2] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; if ($$self{_ext_set_get_action} eq 'set'){ @@ -575,6 +579,7 @@ sub _process_message { } elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { if (substr($msg{extra},0,4) eq "0201") { + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2] Extended Set/Get Data ". "Received for ". $self->get_object_name) if $main::Debug{insteon}; #0 = 2 #14 = Cool SP @@ -621,26 +626,31 @@ sub _process_message { } } elsif ($msg{command} eq "status_temp" && !$msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2] Received Status Temp Message ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_short_temp($msg{extra}); } elsif ($msg{command} eq "status_mode" && !$msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2] Received Status Mode Message ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->status_mode($msg{extra}); } elsif ($msg{command} eq "status_cool" && !$msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2] Received Status Cool Message ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_cool($msg{extra}); } elsif ($msg{command} eq "status_humid" && !$msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2] Received Status Humid Message ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_humid($msg{extra}); } elsif ($msg{command} eq "status_heat" && !$msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2] Received Status Heat Message ". "for ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_heat($msg{extra}); From cd2310897b74720753705656759720f747391cf4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 21 Mar 2013 20:02:39 -0700 Subject: [PATCH 048/330] Insteon::Thermo_i2 Change to sync_time flag and convert time to hex Set_time is a flay already used in MH, likely for idle time. Changed flag to sync_time instead. Also time values needed to be hex encoded. --- lib/Insteon/Thermostat.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 1de662ee3..eeb0e5ff0 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -600,7 +600,7 @@ sub _process_message { $self->hex_heat(substr($msg{extra}, 24, 2)); $clear_message = 1; $self->_process_command_stack(%msg); - if ($$self{set_time}){ + if ($$self{sync_time}){ #This poll was requested as part of sync_time my $message; my $extra; @@ -609,7 +609,7 @@ sub _process_message { $time_array[1], $time_array[0]); my $time_str = ''; foreach (@req_items){ - $time_str .= sprintf("%02d", $_); + $time_str .= sprintf("%02x", $_); } $extra = $extra . "0202". $time_str . substr($msg{extra}, 12, 18); #This will include the prior CRC16 message, but it will @@ -617,7 +617,7 @@ sub _process_message { $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); $$message{add_crc16} = 1; $$self{_ext_set_get_action} = 'set'; - $$self{set_time} = undef; + $$self{sync_time} = undef; $self->_send_cmd($message); } } else { @@ -806,7 +806,7 @@ sub sync_time { #In order to set the time, we need to know the current value of other data #points such as mode and what not becuase we can't just set the time without #setting these variables too. - $$self{set_time} = 1; + $$self{sync_time} = 1; $self->poll_simple(); } From 1b55b88c7f2ddc2c80c35ddeee3c5d199dd101e1 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 26 Mar 2013 18:15:20 -0700 Subject: [PATCH 049/330] Insteon::Thermo_i2 Reorganize i2 init routine and clean up spacing in i2 package --- lib/Insteon/Thermostat.pm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index eeb0e5ff0..70bdcead6 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -470,8 +470,14 @@ our %message_types = ( sub init { my ($self) = @_; $$self{message_types} = \%message_types; + #Set saved state unique to i2 devices + $self->restore_data('humid'); - ## Create the broadcast dummy item + # Create the broadcast dummy item + # This may not belong here. Maybe this should go into read table A? + # Otherwise, users cannot define insteon scenes containing this device. + # While rare a thermostat may be set to provide broadcast updates to + # another device my $dev_id = $self->device_id(); $dev_id =~ /(\w\w)(\w\w)(\w\w)/; $dev_id = "$1.$2.$3"; @@ -505,8 +511,6 @@ sub init { $parent_group->add($$self{$obj}); } } - #Set saved state unique to i2 - $self->restore_data('humid'); #Set child saved states $$self{temp_item}->set_receive($self->get_temp()); @@ -727,10 +731,12 @@ sub hex_cool{ my ($self, $hex_cool) = @_; $self->_cool_sp(hex($hex_cool)); } + sub hex_humid{ my ($self, $hex_humid) = @_; $self->_humid(hex($hex_humid)); } + sub hex_long_temp{ my ($self, $hex_temp) = @_; my $temp_cel = (hex($hex_temp)/10); @@ -749,6 +755,7 @@ sub hex_short_temp{ sub hex_status{ ### Not sure about this one yet, was 80 when set to auto but no activity } + sub hex_heat{ my ($self, $hex_heat) = @_; $self->_heat_sp(hex($hex_heat)); @@ -769,6 +776,7 @@ Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', 'program_cool', 'program_auto'. The 2441TH thermostat does not have program_heat or program_cool. =cut + sub mode{ my ($self, $state) = @_; $state = lc($state); From 6c555d09a259d0173a9fd3ebefc61b6068fbe77e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 26 Mar 2013 18:16:43 -0700 Subject: [PATCH 050/330] Insteon::Thermo_i2 Move Child Objects to First-Level MH Objects Use eval to create true Top-Level objects with user friendly variable names. This may be frowned upon in perl programming, but is is already used in a number of places within MH, so it can't be that bad. :-p As noted in the comments, objects which will be linked to insteon scenes should not be created in this manner. A big thanks to Gregg in 2007 who posted a comment that led me to this solution. http://misterhouse.10964.n7.nabble.com/Coding-question-Dynamic-variables-with-strict-refs-td6300.html --- lib/Insteon/Thermostat.pm | 59 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 70bdcead6..2070bb510 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -490,37 +490,48 @@ sub init { &main::register_object_by_name('$' . $self->get_object_name ."{bcast_item}",$$self{bcast_item}); $$self{bcast_item}->{object_name} = '$' . $self->get_object_name ."{bcast_item}"; - ## Create the child objects - my @child_objs = ('mode_item', 'fan_item', 'temp_item', 'humidity_item', - 'setpoint_h_item', 'setpoint_c_item'); - foreach my $obj (@child_objs) { - $$self{$obj} = new Insteon::Thermo_i2_mode() if ($obj eq 'mode_item'); - $$self{$obj} = new Insteon::Thermo_i2_fan() if ($obj eq 'fan_item'); - $$self{$obj} = new Insteon::Thermo_i2_temp() if ($obj eq 'temp_item'); - $$self{$obj} = new Insteon::Thermo_i2_humidity() if ($obj eq 'humidity_item'); - $$self{$obj} = new Insteon::Thermo_i2_setpoint_h() if ($obj eq 'setpoint_h_item'); - $$self{$obj} = new Insteon::Thermo_i2_setpoint_c() if ($obj eq 'setpoint_c_item'); - + # Define the child objects + # These we can create ourselves as they cannot be used in an insteon scene + my %child_objs = ( + mode_item => "new Insteon::Thermo_i2_mode(".$self->get_object_name.")", + fan_item => "new Insteon::Thermo_i2_fan(".$self->get_object_name.")", + temp_item => "new Insteon::Thermo_i2_temp(".$self->get_object_name.")", + humidity_item => "new Insteon::Thermo_i2_humidity(".$self->get_object_name.")", + setpoint_h_item => "new Insteon::Thermo_i2_setpoint_h(".$self->get_object_name.")", + setpoint_c_item => "new Insteon::Thermo_i2_setpoint_c(".$self->get_object_name.")" + ); + + #Now create them + foreach my $child_type (keys %child_objs) { + #Name the Child Object + my $child_name = $self->get_object_name . "_" . $child_type; + + #Define the code to run in eval + my $eval_cmd; + $eval_cmd = "use vars '$child_name';\n"; + $eval_cmd .= "$child_name = $child_objs{$child_type};\n"; # Register child object with MH - &main::register_object_by_name('$' . $self->get_object_name ."{$obj}",$$self{$obj}); - $$self{$obj}->{object_name} = '$' . $self->get_object_name ."{$obj}"; - $$self{$obj}{parent} = $self; + $eval_cmd .= "&main::register_object_by_name('$child_name',$child_name);\n"; + + #Run the eval command + package main; + eval($eval_cmd); + if ($@) { + ::print_log( "[Insteon::Thermo_i2] Error in init eval command:\n $@ \n---\n $eval_cmd"); + } + package Insteon::Thermo_i2; + + #Get the newly created child object + $$self{$child_type} = ::get_object_by_name($child_name); + $$self{$child_type}->{object_name} = "$child_name"; #Add child to the same groups as parent foreach my $parent_group (::list_groups_by_object($self,1)){ - $parent_group->add($$self{$obj}); + $parent_group->add($$self{$child_type}); } } - #Set child saved states - $$self{temp_item}->set_receive($self->get_temp()); - $$self{setpoint_h_item}->set_receive($self->get_heat_sp()); - $$self{setpoint_c_item}->set_receive($self->get_cool_sp()); - $$self{fan_item}->set_receive($self->get_fan_mode()); - $$self{mode_item}->set_receive($self->get_mode()); - $$self{humidity_item}->set_receive($$self{humid}); - - #Tie changes in parent item to children + #Create tie so that changes in parent update the child $self -> tie_event ('Insteon::Thermo_i2::parent_event(\''.$$self{object_name} . '\', "$state")'); } From 43ddcdfea5ad2478eeffec67d050be943f88b172 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 26 Mar 2013 18:21:56 -0700 Subject: [PATCH 051/330] Insteon::Thermo_i2 Define Parent and set Initial Value in Child Object new Routines --- lib/Insteon/Thermostat.pm | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 2070bb510..443fdfed5 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -851,10 +851,12 @@ use strict; @Insteon::Thermo_i2_mode::ISA = ('Generic_Item'); sub new { - my ($class) = @_; + my ($class,$parent) = @_; my $self = new Generic_Item(); bless $self, $class; + $$self{parent} = $parent; @{$$self{states}} = ('Off', 'Heat', 'Cool', 'Auto', 'Program'); + $self->set_receive($$self{parent}->get_mode()); return $self; } @@ -884,10 +886,12 @@ use strict; @Insteon::Thermo_i2_fan::ISA = ('Generic_Item'); sub new { - my ($class) = @_; + my ($class, $parent) = @_; my $self = new Generic_Item(); bless $self, $class; + $$self{parent} = $parent; @{$$self{states}} = ('Auto', 'On'); + $self->set_receive($$self{parent}->get_fan_mode()); return $self; } @@ -910,15 +914,18 @@ sub set_receive { my ($self, $p_state) = @_; $self->SUPER::set($p_state); } + package Insteon::Thermo_i2_temp; use strict; @Insteon::Thermo_i2_temp::ISA = ('Generic_Item'); sub new { - my ($class) = @_; + my ($class, $parent) = @_; my $self = new Generic_Item(); bless $self, $class; + $$self{parent} = $parent; + $self->set_receive($$self{parent}->get_temp()); return $self; } @@ -932,9 +939,11 @@ use strict; @Insteon::Thermo_i2_humidity::ISA = ('Generic_Item'); sub new { - my ($class) = @_; + my ($class, $parent) = @_; my $self = new Generic_Item(); bless $self, $class; + $$self{parent} = $parent; + $self->set_receive($$self{parent}{humid}); return $self; } @@ -949,10 +958,12 @@ use strict; @Insteon::Thermo_i2_setpoint_h::ISA = ('Generic_Item'); sub new { - my ($class) = @_; + my ($class, $parent) = @_; my $self = new Generic_Item(); bless $self, $class; + $$self{parent} = $parent; @{$$self{states}} = ('Cooler' , 'Warmer'); + $self->set_receive($$self{parent}->get_heat_sp()); return $self; } @@ -987,10 +998,12 @@ use strict; @Insteon::Thermo_i2_setpoint_c::ISA = ('Generic_Item'); sub new { - my ($class) = @_; + my ($class, $parent) = @_; my $self = new Generic_Item(); bless $self, $class; + $$self{parent} = $parent; @{$$self{states}} = ('Cooler', 'Warmer'); + $self->set_receive($$self{parent}->get_cool_sp()); return $self; } From dd9501ee4957202913cc77db45c8591597bee112 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 17:18:26 -0700 Subject: [PATCH 052/330] Insteon_Thermo_i2: Remove Complex Code Creating Child Objects The code is just too difficult to try and make work. Either the child objects have to be stored in a hash, which results in awful looking veriable names that are not user friendly to the basic user. The removed code made user friendly variable names, but these variables could not be used in other user code because the init code was not eval'd until runtime. --- lib/Insteon/Thermostat.pm | 44 --------------------------------------- 1 file changed, 44 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 443fdfed5..d5f1b5992 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -489,50 +489,6 @@ sub init { # Register bcast object with MH &main::register_object_by_name('$' . $self->get_object_name ."{bcast_item}",$$self{bcast_item}); $$self{bcast_item}->{object_name} = '$' . $self->get_object_name ."{bcast_item}"; - - # Define the child objects - # These we can create ourselves as they cannot be used in an insteon scene - my %child_objs = ( - mode_item => "new Insteon::Thermo_i2_mode(".$self->get_object_name.")", - fan_item => "new Insteon::Thermo_i2_fan(".$self->get_object_name.")", - temp_item => "new Insteon::Thermo_i2_temp(".$self->get_object_name.")", - humidity_item => "new Insteon::Thermo_i2_humidity(".$self->get_object_name.")", - setpoint_h_item => "new Insteon::Thermo_i2_setpoint_h(".$self->get_object_name.")", - setpoint_c_item => "new Insteon::Thermo_i2_setpoint_c(".$self->get_object_name.")" - ); - - #Now create them - foreach my $child_type (keys %child_objs) { - #Name the Child Object - my $child_name = $self->get_object_name . "_" . $child_type; - - #Define the code to run in eval - my $eval_cmd; - $eval_cmd = "use vars '$child_name';\n"; - $eval_cmd .= "$child_name = $child_objs{$child_type};\n"; - # Register child object with MH - $eval_cmd .= "&main::register_object_by_name('$child_name',$child_name);\n"; - - #Run the eval command - package main; - eval($eval_cmd); - if ($@) { - ::print_log( "[Insteon::Thermo_i2] Error in init eval command:\n $@ \n---\n $eval_cmd"); - } - package Insteon::Thermo_i2; - - #Get the newly created child object - $$self{$child_type} = ::get_object_by_name($child_name); - $$self{$child_type}->{object_name} = "$child_name"; - - #Add child to the same groups as parent - foreach my $parent_group (::list_groups_by_object($self,1)){ - $parent_group->add($$self{$child_type}); - } - } - - #Create tie so that changes in parent update the child - $self -> tie_event ('Insteon::Thermo_i2::parent_event(\''.$$self{object_name} . '\', "$state")'); } sub sync_links{ From ae540383ee61f8c5dd06cefdf06521cecf4075aa Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 17:21:17 -0700 Subject: [PATCH 053/330] Insteon_thermo_i2: Remove Parent_Event Routine Not needed without tied children. --- lib/Insteon/Thermostat.pm | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index d5f1b5992..5d6533e75 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -503,29 +503,6 @@ sub sync_links{ return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); } -sub parent_event { - my ($self, $p_state) = @_; - $self = ::get_object_by_name($self); - if ($p_state eq 'temp_change'){ - $$self{temp_item}->set_receive($self->get_temp()); - } - elsif ($p_state eq 'heat_setpoint_change'){ - $$self{setpoint_h_item}->set_receive($self->get_heat_sp()); - } - elsif ($p_state eq 'cool_setpoint_change'){ - $$self{setpoint_c_item}->set_receive($self->get_cool_sp()); - } - elsif ($p_state eq 'fan_mode_change'){ - $$self{fan_item}->set_receive($self->get_fan_mode()); - } - elsif ($p_state eq 'mode_change'){ - $$self{mode_item}->set_receive($self->get_mode()); - } - elsif ($p_state eq 'humid_change'){ - $$self{humidity_item}->set_receive($$self{humid}); - } -} - sub poll_simple{ my ($self) = @_; my $extra = "020000000000000000000000000000"; From 6558d86e3a3db751369f0744549d3cb25f74decd Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 17:42:38 -0700 Subject: [PATCH 054/330] Insteon_thermo_i2: Change Name of Child Objects While designed for i2 devices, the child objects should work with i1 devices as well, except the humidity object which I don't believe exits for i1 devices. --- lib/Insteon/Thermostat.pm | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 5d6533e75..587adbd5f 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -778,10 +778,10 @@ sub new { return $self; } -package Insteon::Thermo_i2_mode; +package Insteon::Thermo_mode; use strict; -@Insteon::Thermo_i2_mode::ISA = ('Generic_Item'); +@Insteon::Thermo_mode::ISA = ('Generic_Item'); sub new { my ($class,$parent) = @_; @@ -813,10 +813,10 @@ sub set_receive { $self->SUPER::set($p_state); } -package Insteon::Thermo_i2_fan; +package Insteon::Thermo_fan; use strict; -@Insteon::Thermo_i2_fan::ISA = ('Generic_Item'); +@Insteon::Thermo_fan::ISA = ('Generic_Item'); sub new { my ($class, $parent) = @_; @@ -848,10 +848,10 @@ sub set_receive { $self->SUPER::set($p_state); } -package Insteon::Thermo_i2_temp; +package Insteon::Thermo_temp; use strict; -@Insteon::Thermo_i2_temp::ISA = ('Generic_Item'); +@Insteon::Thermo_temp::ISA = ('Generic_Item'); sub new { my ($class, $parent) = @_; @@ -866,10 +866,10 @@ sub set_receive { my ($self, $p_state) = @_; $self->SUPER::set($p_state); } -package Insteon::Thermo_i2_humidity; +package Insteon::Thermo_humidity; use strict; -@Insteon::Thermo_i2_humidity::ISA = ('Generic_Item'); +@Insteon::Thermo_humidity::ISA = ('Generic_Item'); sub new { my ($class, $parent) = @_; @@ -885,10 +885,10 @@ sub set_receive { $self->SUPER::set($p_state); } -package Insteon::Thermo_i2_setpoint_h; +package Insteon::Thermo_setpoint_h; use strict; -@Insteon::Thermo_i2_setpoint_h::ISA = ('Generic_Item'); +@Insteon::Thermo_setpoint_h::ISA = ('Generic_Item'); sub new { my ($class, $parent) = @_; @@ -925,10 +925,10 @@ sub set_receive { $self->SUPER::set($p_state); } -package Insteon::Thermo_i2_setpoint_c; +package Insteon::Thermo_setpoint_c; use strict; -@Insteon::Thermo_i2_setpoint_c::ISA = ('Generic_Item'); +@Insteon::Thermo_setpoint_c::ISA = ('Generic_Item'); sub new { my ($class, $parent) = @_; From ed339a7662a4a57ab96396cfb56282ec837c0977 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 17:45:03 -0700 Subject: [PATCH 055/330] Insteon_thermo_it: Cleanup Example Thermostat Code File Fix to match current code base --- code/examples/Insteon_thermostat.pl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/code/examples/Insteon_thermostat.pl b/code/examples/Insteon_thermostat.pl index 4f9ad29bf..bd6cf6ce5 100755 --- a/code/examples/Insteon_thermostat.pl +++ b/code/examples/Insteon_thermostat.pl @@ -1,13 +1,13 @@ # Category=HVAC -$v_test_thermostat = new Voice_Cmd("Send Thermostat cmd [ping,poll_mode,mode_off,mode_heat,mode_cool,mode_auto,mode_pgm_heat,mode_pgm_cool,mode_pgm_auto,fan_on,fan_auto,poll_temp,poll_setpoint]"); +$v_test_thermostat = new Voice_Cmd("Send Thermostat cmd [poll_mode,mode_off,mode_heat,mode_cool,mode_auto,mode_pgm_heat,mode_pgm_cool,mode_pgm_auto,fan_on,fan_auto,poll_temp,poll_setpoint]"); # Create the Object in user code: #use Insteon_Thermostat; -#$thermostat = new Insteon_Thermostat($plm,'12.34.56'); +#$thermostat = new Insteon_Thermostat('12.34.56', $plm); # or in items.mht (read_table_A) -#IPLT, 12.34.56,, thermostat, HVAC, plm +#INSTEON_THERMOSTAT, 12.34.56,, thermostat, HVAC # poll_setpoint also runs poll_mode if ($Startup || $Reload) { @@ -29,9 +29,7 @@ if (my $state = said $v_test_thermostat) { - if ($state eq 'ping') { - $thermostat->ping(); - }elsif ($state eq 'poll_mode') { + if ($state eq 'poll_mode') { $thermostat->poll_mode(); }elsif ($state eq 'poll_temp') { $thermostat->poll_temp(); From 1842025592a4a594b89cf55bff64496c00b00d8d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 17:45:45 -0700 Subject: [PATCH 056/330] Insteon_thermo_i2: Add Example to Thermostat Code File for Child Objects Add example entry into code file to demonstrate how to create child objects that are tied to parent item --- code/examples/Insteon_thermostat.pl | 56 ++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/code/examples/Insteon_thermostat.pl b/code/examples/Insteon_thermostat.pl index bd6cf6ce5..71400fe67 100755 --- a/code/examples/Insteon_thermostat.pl +++ b/code/examples/Insteon_thermostat.pl @@ -87,4 +87,58 @@ $thermostat->heat_setpoint(66); $thermostat->cool_setpoint(82); $thermostat->poll_setpoint(); -} +} + +## The examples show how a Generic Item can be Used to Track and Display +## individual data points in the thermostat. + +#Define the Children +$thermo_temp = new Insteon::Thermo_temp($thermostat); +$thermo_fan = new Insteon::Thermo_fan($thermostat); +$thermo_mode = new Insteon::Thermo_mode($thermostat); +$thermo_humidity = new Insteon::Thermo_humidity($thermostat); +$thermo_setpoint_h = new Insteon::Thermo_setpoint_h($thermostat); +$thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); + +#Add the Children to the HVAC Group +$HVAC->add($thermo_temp); +$HVAC->add($thermo_fan); +$HVAC->add($thermo_mode); +$HVAC->add($thermo_humidity); +$HVAC->add($thermo_setpoint_h); +$HVAC->add($thermo_setpoint_c); + +if ($Reload){ + #Create tie so that changes in parent update the child + $thermostat -> tie_event ('::thermo_parent_event($object->get_object_name, "$state")'); + + #set Children to initial states + $thermo_temp->set_receive($thermostat->get_temp()); + $thermo_setpoint_h->set_receive($thermostat->get_heat_sp()); + $thermo_setpoint_c->set_receive($thermostat->get_cool_sp()); + $thermo_fan->set_receive($thermostat->get_fan_mode()); + $thermo_mode->set_receive($thermostat->get_mode()); +} + +sub thermo_parent_event { + my ($self, $p_state) = @_; + $self = ::get_object_by_name($self); + if ($p_state eq 'temp_change'){ + $thermo_temp->set_receive($self->get_temp(), $self); + } + elsif ($p_state eq 'heat_setpoint_change'){ + $thermo_setpoint_h->set_receive($self->get_heat_sp(), $self); + } + elsif ($p_state eq 'cool_setpoint_change'){ + $thermo_setpoint_c->set_receive($self->get_cool_sp(), $self); + } + elsif ($p_state eq 'fan_mode_change'){ + $thermo_fan->set_receive($self->get_fan_mode(), $self); + } + elsif ($p_state eq 'mode_change'){ + $thermo_mode->set_receive($self->get_mode(), $self); + } + elsif ($p_state eq 'humid_change'){ + $thermo_humidity->set_receive($$self{humid}, $self); + } +} From 80f67ebe548c4b9856d1a5cfb7fdadb573037aaf Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 19:40:25 -0700 Subject: [PATCH 057/330] Insteon_thermo_i2: Fix Comments in Example Code --- code/examples/Insteon_thermostat.pl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/examples/Insteon_thermostat.pl b/code/examples/Insteon_thermostat.pl index 71400fe67..88408e944 100755 --- a/code/examples/Insteon_thermostat.pl +++ b/code/examples/Insteon_thermostat.pl @@ -89,8 +89,10 @@ $thermostat->poll_setpoint(); } -## The examples show how a Generic Item can be Used to Track and Display -## individual data points in the thermostat. +## The examples show how the defined child objects can be Used to Track and Display +## individual data points in the thermostat. Each of the child objects will +## display and permit the adjusting (if applicable) of one data point such as +## fan mode or cool setpoint. #Define the Children $thermo_temp = new Insteon::Thermo_temp($thermostat); From 220f503a04b215d802b23d90cf6f2ee961dd2b9e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 19:45:25 -0700 Subject: [PATCH 058/330] Insteon_thermo_i2: Remove Complex Tie Example from Example User Code Will add to individual child objects instead --- code/examples/Insteon_thermostat.pl | 35 ----------------------------- 1 file changed, 35 deletions(-) diff --git a/code/examples/Insteon_thermostat.pl b/code/examples/Insteon_thermostat.pl index 88408e944..5b7e83650 100755 --- a/code/examples/Insteon_thermostat.pl +++ b/code/examples/Insteon_thermostat.pl @@ -109,38 +109,3 @@ $HVAC->add($thermo_humidity); $HVAC->add($thermo_setpoint_h); $HVAC->add($thermo_setpoint_c); - -if ($Reload){ - #Create tie so that changes in parent update the child - $thermostat -> tie_event ('::thermo_parent_event($object->get_object_name, "$state")'); - - #set Children to initial states - $thermo_temp->set_receive($thermostat->get_temp()); - $thermo_setpoint_h->set_receive($thermostat->get_heat_sp()); - $thermo_setpoint_c->set_receive($thermostat->get_cool_sp()); - $thermo_fan->set_receive($thermostat->get_fan_mode()); - $thermo_mode->set_receive($thermostat->get_mode()); -} - -sub thermo_parent_event { - my ($self, $p_state) = @_; - $self = ::get_object_by_name($self); - if ($p_state eq 'temp_change'){ - $thermo_temp->set_receive($self->get_temp(), $self); - } - elsif ($p_state eq 'heat_setpoint_change'){ - $thermo_setpoint_h->set_receive($self->get_heat_sp(), $self); - } - elsif ($p_state eq 'cool_setpoint_change'){ - $thermo_setpoint_c->set_receive($self->get_cool_sp(), $self); - } - elsif ($p_state eq 'fan_mode_change'){ - $thermo_fan->set_receive($self->get_fan_mode(), $self); - } - elsif ($p_state eq 'mode_change'){ - $thermo_mode->set_receive($self->get_mode(), $self); - } - elsif ($p_state eq 'humid_change'){ - $thermo_humidity->set_receive($$self{humid}, $self); - } -} From a5d8ef3d2a9eaa61d298f7e54340598056398b9a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 19:55:25 -0700 Subject: [PATCH 059/330] Insteon_thermo_i2: Add Parent Event Routine to Base Thermostat Code parent_event is called by tie_event on state changes in the parent. The routine updates the child object with the correct state --- lib/Insteon/Thermostat.pm | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 587adbd5f..f3bd5af1e 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -364,6 +364,29 @@ sub _process_message return $clear_message; } +#Used to update the state of child objects +sub parent_event { + my ($self, $p_state) = @_; + if ($p_state eq 'mode_change'){ + $$self{child_mode}->set_receive($self->get_mode()); + } + elsif ($p_state eq 'temp_change'){ + $$self{child_temp}->set_receive($self->get_temp(), $self); + } + elsif ($p_state eq 'heat_setpoint_change'){ + $$self{child_setpoint_h}->set_receive($self->get_heat_sp(), $self); + } + elsif ($p_state eq 'cool_setpoint_change'){ + $$self{child_setpoint_c}->set_receive($self->get_cool_sp(), $self); + } + elsif ($p_state eq 'fan_mode_change'){ + $$self{child_fan}->set_receive($self->get_fan_mode(), $self); + } + elsif ($p_state eq 'humid_change'){ + $$self{child_humidity}->set_receive($$self{humid}, $self); + } +} + # Overload methods we don't use, but would otherwise cause Insteon traffic. sub request_status { return 0 } From 1e934f1b7c9ca3dcfe1922a8ba22d3ade4c01f63 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 20:05:25 -0700 Subject: [PATCH 060/330] Insteon_thermo_i2: Add Child Object to Parent Hash and Call Tie_Event The child objects are stored in the parent hash so that the parent can locate the correct child to update it. tie_events is set on child object creation. --- lib/Insteon/Thermostat.pm | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index f3bd5af1e..1a5609749 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -812,7 +812,8 @@ sub new { bless $self, $class; $$self{parent} = $parent; @{$$self{states}} = ('Off', 'Heat', 'Cool', 'Auto', 'Program'); - $self->set_receive($$self{parent}->get_mode()); + $$self{parent}{child_mode} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "mode_change"); return $self; } @@ -847,7 +848,8 @@ sub new { bless $self, $class; $$self{parent} = $parent; @{$$self{states}} = ('Auto', 'On'); - $self->set_receive($$self{parent}->get_fan_mode()); + $$self{parent}{child_fan} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "fan_mode_change"); return $self; } @@ -881,7 +883,8 @@ sub new { my $self = new Generic_Item(); bless $self, $class; $$self{parent} = $parent; - $self->set_receive($$self{parent}->get_temp()); + $$self{parent}{child_temp} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "temp_change"); return $self; } @@ -889,6 +892,7 @@ sub set_receive { my ($self, $p_state) = @_; $self->SUPER::set($p_state); } + package Insteon::Thermo_humidity; use strict; @@ -899,7 +903,8 @@ sub new { my $self = new Generic_Item(); bless $self, $class; $$self{parent} = $parent; - $self->set_receive($$self{parent}{humid}); + $$self{parent}{child_humidity} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "humid_change"); return $self; } @@ -919,7 +924,8 @@ sub new { bless $self, $class; $$self{parent} = $parent; @{$$self{states}} = ('Cooler' , 'Warmer'); - $self->set_receive($$self{parent}->get_heat_sp()); + $$self{parent}{child_setpoint_h} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "heat_setpoint_change"); return $self; } @@ -959,7 +965,8 @@ sub new { bless $self, $class; $$self{parent} = $parent; @{$$self{states}} = ('Cooler', 'Warmer'); - $self->set_receive($$self{parent}->get_cool_sp()); + $$self{parent}{child_setpoint_c} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "cool_setpoint_change"); return $self; } From b07628aca845de9bbb5332b97a9e4e096a406ad1 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 20:15:25 -0700 Subject: [PATCH 061/330] Insteon: Add Parameter to Allow Per Device Min/Max Hop Setting --- lib/Insteon/BaseInsteon.pm | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4ec80281f..51a3d5107 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -24,6 +24,7 @@ Special Thanks to: Bruce Winter - MH @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=over =cut package Insteon::BaseObject; @@ -155,6 +156,28 @@ sub group return $$self{m_group}; } +=item C + +Sets the maximum number of hops that may be used in a message sent to the device. +The default and maximum number is 3. $int is an integer between 0-3. +=cut +sub max_hops { + my ($self, $hops) = @_; + $$self{max_hops} = $hops if $hops; + return $$self{max_hops}; +} + +=item C + +Sets the minimum number of hops that may be used in a message sent to the device. +The default and minimum number is 0. $int is an integer between 0-3. +=cut +sub min_hops { + my ($self, $hops) = @_; + $$self{min_hops} = $hops if $hops; + return $$self{min_hops}; +} + sub default_hop_count { my ($self, $hop_count) = @_; @@ -170,6 +193,10 @@ sub default_hop_count $high = $_ if ($high < $_);; } $$self{default_hop_count} = $high; + $$self{default_hop_count} = $$self{max_hops} if ($$self{max_hops} && + $$self{default_hop_count} > $$self{max_hops}); + $$self{default_hop_count} = $$self{min_hops} if ($$self{min_hops} && + $$self{default_hop_count} < $$self{min_hops}); return $$self{default_hop_count}; } @@ -1872,3 +1899,5 @@ sub is_root } 1; +=back +=cut From b93ddb0a4faf167e07a9d1d93f6877f80b7109fd Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 20:15:25 -0700 Subject: [PATCH 062/330] Insteon: Fix Error in Logic Which Caused an Unnecessary Increase in Hop Count --- lib/Insteon/Message.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 841553322..f2b87ca18 100755 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -410,7 +410,7 @@ sub _derive_interface_data } else { - my $hop_count = $self->send_attempts + $self->setby->default_hop_count - 1; + my $hop_count = $self->setby->default_hop_count; $cmd.=$self->setby->device_id(); if ($self->command_type =~ /insteon_ext_send/i) { From 18bf2108595da0cda8f0f672f5d3a8af279652f3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 18 Apr 2013 20:18:25 -0700 Subject: [PATCH 063/330] Insteon: Add Parameter to Alter Send Timeout on a Per Device Basis --- lib/Insteon/BaseInsteon.pm | 17 +++++++++++++++++ lib/Insteon/Message.pm | 27 ++++++++++++++++----------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4ec80281f..64d43f013 100755 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -24,6 +24,7 @@ Special Thanks to: Bruce Winter - MH @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=over =cut package Insteon::BaseObject; @@ -155,6 +156,20 @@ sub group return $$self{m_group}; } +=item C + +Changes the amount of time MH will wait to receive a response from a device before +resending the message. The value set will be multiplied by the predefined value +in MH. $float can be set to any positive decimal number. For example using 1.0 +will not change the preset values; 1.1 will increase the time MH waits by 10%; and +0.9 will force MH to wait for only 90% of the predefined time. +=cut +sub timeout_factor { + my ($self, $factor) = @_; + $$self{timeout_factor} = $factor if $factor; + return $$self{timeout_factor}; +} + sub default_hop_count { my ($self, $hop_count) = @_; @@ -1872,3 +1887,5 @@ sub is_root } 1; +=back +=cut diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 841553322..78b8f36a9 100755 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -303,53 +303,58 @@ sub send_timeout my ($self, $ignore) = @_; my $hop_count = (ref $self->setby and $self->setby->isa('Insteon::BaseObject')) ? $self->setby->default_hop_count : $self->send_attempts; + my $timeout = 1400; if($self->command eq 'peek' || $self->command eq 'set_address_msb') { - return 4000; + $timeout = 4000; } - if ($self->command_type eq 'all_link_send') + elsif ($self->command_type eq 'all_link_send') { # note, the following was set to 2000 and that was insufficient - return 3000; + $timeout = 3000; } elsif ($self->command_type eq 'insteon_ext_send') { if ($hop_count == 0) { - return 2220; + $timeout = 2220; } elsif ($hop_count == 1) { - return 2690; + $timeout = 2690; } elsif ($hop_count == 2) { - return 3000; + $timeout = 3000; } elsif ($hop_count >= 3) { - return 3170; + $timeout = 3170; } } else { if ($hop_count == 0) { - return 1400; + $timeout = 1400; } elsif ($hop_count == 1) { - return 1700; + $timeout = 1700; } elsif ($hop_count == 2) { - return 1900; + $timeout = 1900; } elsif ($hop_count >= 3) { - return 2000; + $timeout = 2000; } } + if (ref $self->setby and $self->setby->isa('Insteon::BaseObject')){ + $timeout = int($timeout * $self->setby->timeout_factor); + } + return $timeout; } sub to_string From 6d30f8bd33048f7246962fce87e19a543b3d0144 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 22 Apr 2013 20:45:49 -0700 Subject: [PATCH 064/330] Insteon_Thermo_i2: Final touches on Thermostat Code --- lib/Insteon/Thermostat.pm | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 1a5609749..7b05d2e2d 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -47,6 +47,14 @@ All of the states that may be set: fan_mode_change: Fan mode changed (call get_fan_mode() to get value). +Child objects which track the states of the thermostat can be created: +$thermo_temp = new Insteon::Thermo_temp($thermostat); +$thermo_fan = new Insteon::Thermo_fan($thermostat); +$thermo_mode = new Insteon::Thermo_mode($thermostat); +$thermo_humidity = new Insteon::Thermo_humidity($thermostat); +$thermo_setpoint_h = new Insteon::Thermo_setpoint_h($thermostat); +$thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); + see code/examples/Insteon_thermostat.pl for more. =head1 BUGS @@ -68,11 +76,9 @@ Kevin Rober Keegan =head1 TODO - - Look at possible bugs when starting from factory defaults - There seemed to be an issue with the setpoints changing when changing modes until - they were set programatically. - - Test fan modes and associated state_changes - Manage aldb - should be able to adjust setpoints based on plm scene. <- may be overkill + - Add ability to link devices to the I2 Thermostat groups 1-4 + - Add ability to link other devices to the broadcast group. =head1 INHERITS @@ -516,16 +522,23 @@ sub init { sub sync_links{ my ($self, $audit_mode, $callback, $failure_callback) = @_; - #Make sure thermostat is set to broadcast changes - ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; - my $extra = "000008000000000000000000000000"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); - $$self{_ext_set_get_action} = 'set'; - $self->_send_cmd($message); + if !($audit_mode){ + #Make sure thermostat is set to broadcast changes + ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; + my $extra = "000008000000000000000000000000"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$self{_ext_set_get_action} = 'set'; + $self->_send_cmd($message); + } # Call the main sync_links code return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); } +=item C + +Requests the status of all Thermostat data points (temp, fan, mode ...) in a single +request. Only available for I2 devices. +=cut sub poll_simple{ my ($self) = @_; my $extra = "020000000000000000000000000000"; @@ -775,7 +788,10 @@ sub simple_message { $message = new Insteon::InsteonMessage('insteon_ext_send', $self, $type, $extra); return $message; } +=item C +Sets the data and time of the thermostat based on the time of the MH server. +=cut sub sync_time { my ($self) = @_; #In order to set the time, we need to know the current value of other data From 1005109bd1afb9bda622354bf078890205054674 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 22 Apr 2013 21:03:07 -0700 Subject: [PATCH 065/330] Insteon_Thermo_i2: Fix Typo --- lib/Insteon/Thermostat.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 7b05d2e2d..3fc90c8d5 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -55,6 +55,8 @@ $thermo_humidity = new Insteon::Thermo_humidity($thermostat); $thermo_setpoint_h = new Insteon::Thermo_setpoint_h($thermostat); $thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); +where $thermostat is the parent object to track. + see code/examples/Insteon_thermostat.pl for more. =head1 BUGS @@ -522,7 +524,7 @@ sub init { sub sync_links{ my ($self, $audit_mode, $callback, $failure_callback) = @_; - if !($audit_mode){ + if (!$audit_mode){ #Make sure thermostat is set to broadcast changes ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; my $extra = "000008000000000000000000000000"; From 30547d6be6845cf01dee6a6e8bc62756c520d04f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 27 May 2013 18:56:03 -0700 Subject: [PATCH 066/330] Insteon_Thermostat: Cleanup Instructions, Make Print Log More Clear, Force Temperature to a Whole Number --- lib/Insteon/Thermostat.pm | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 3fc90c8d5..9ada5af49 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -35,7 +35,8 @@ And, you can set the temperature and mode at will... $thermostat->cool_setpoint(89); } -All of the states that may be set: +All of the states of the parent object that may be set by MH, you can use tie_event +to link specific actions to these states: temp_change: Inside temperature changed (call get_temp() to get value) heat_sp_change: Heat setpoint was changed @@ -61,11 +62,8 @@ see code/examples/Insteon_thermostat.pl for more. =head1 BUGS -Initial code for Venstar thermostats, which use Insteon engine version i1, only -provided basic features. The new Insteon 2441TH thermostats use the i2cs engine -and only allow the polling, but not setting, of the thermostat attributes using -i2 code. As such, I am unable to test or provide enhancements to certain i1 -only aspects. +This code has not been tested on older Venstar thermsotats, however it is believed +that the basic functionality should work as it did in the old code. =head1 AUTHOR @@ -74,13 +72,14 @@ Gregg Liming Brian Warren Enhanced to i2 by: -Kevin Rober Keegan +Kevin Robert Keegan =head1 TODO - - Manage aldb - should be able to adjust setpoints based on plm scene. <- may be overkill - - Add ability to link devices to the I2 Thermostat groups 1-4 - - Add ability to link other devices to the broadcast group. + - Enable Linking of the Thermostat as a Responder - The current design of MH + will not create valid links when the thermostat is the responder. To enable + this function, a reorganization of the add_link and update_link code at the + BaseObject level needs to be performed. =head1 INHERITS @@ -613,32 +612,32 @@ sub _process_message { } elsif ($msg{command} eq "status_temp" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Status Temp Message ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2] Received Temp Change Message ". + "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_short_temp($msg{extra}); } elsif ($msg{command} eq "status_mode" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Status Mode Message ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2] Received Mode Change Message ". + "from ". $self->get_object_name) if $main::Debug{insteon}; $self->status_mode($msg{extra}); } elsif ($msg{command} eq "status_cool" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Status Cool Message ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2] Received Cool Setpoint Change Message ". + "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_cool($msg{extra}); } elsif ($msg{command} eq "status_humid" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Status Humid Message ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2] Received Humidity Change Message ". + "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_humid($msg{extra}); } elsif ($msg{command} eq "status_heat" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Status Heat Message ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2] Received Heat Setpoint Change Message ". + "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_heat($msg{extra}); } else { @@ -724,7 +723,8 @@ sub hex_long_temp{ my $temp_cel = (hex($hex_temp)/10); ## ATM I am going to assume farenheit b/c that is what I have # in future, can pull setting bit from thermometer - $$self{temp} = (($temp_cel*9)/5 +32); + # Extra .5 since sprintf doesn't round + $$self{temp} = sprintf("%d", (($temp_cel*9)/5 +32 +.5)); $self->set_receive('temp_change'); } From 1f42c65a70ea6a305a877fbc491c1e09b90eab51 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 27 May 2013 18:57:55 -0700 Subject: [PATCH 067/330] Insteon_Thermo: Add support for Groups 2-4, Add Status Child Groups 1-4 of the i2 devices report on/off events for cooling, heating, high humidity, and low humidity. Insteon objects can be linked to these events. e.g. Turn on a fan when the humidity is high. In addition, a new status child has been added. It provides a simple text of the current status of the HVAC system. --- lib/Insteon/Thermostat.pm | 186 +++++++++++++++++++++++++++++++++++--- 1 file changed, 175 insertions(+), 11 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 9ada5af49..553a05cca 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -9,10 +9,30 @@ Enables support for an Insteon Thermostat. =head1 SYNOPSIS In user code: + $thermostat = new Insteon_Thermostat($myPLM, '12.34.56'); + +Additional i2 specific objects: + + $thermostat_heating = new Insteon_Thermostat($myPLM, '12.34.56:02'); + $thermostat_high_humid = new Insteon_Thermostat($myPLM, '12.34.56:03'); + $thermostat_low_humid = new Insteon_Thermostat($myPLM, '12.34.56:04'); + $thermostat_broadcast = new Insteon_Thermostat($myPLM, '12.34.56:EF'); + +These devices will not have any states, but are only used for linking purposes. In items.mht: + INSTEON_THERMOSTAT, 12.34.56, thermostat, HVAC + +Additional i2 specific objects: + + INSTEON_THERMOSTAT, 12.34.56:02, thermostat_heating, HVAC + INSTEON_THERMOSTAT, 12.34.56:03, thermostat_high_humid, HVAC + INSTEON_THERMOSTAT, 12.34.56:04, thermostat_low_humid, HVAC + INSTEON_THERMOSTAT, 12.34.56:EF, thermostat_broadcast, HVAC + +These devices will not have any states, but are only used for linking purposes. Poll for temperature changes. @@ -47,16 +67,60 @@ to link specific actions to these states: (call get_mode() to get value). fan_mode_change: Fan mode changed (call get_fan_mode() to get value). - -Child objects which track the states of the thermostat can be created: -$thermo_temp = new Insteon::Thermo_temp($thermostat); -$thermo_fan = new Insteon::Thermo_fan($thermostat); -$thermo_mode = new Insteon::Thermo_mode($thermostat); -$thermo_humidity = new Insteon::Thermo_humidity($thermostat); -$thermo_setpoint_h = new Insteon::Thermo_setpoint_h($thermostat); -$thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); - -where $thermostat is the parent object to track. + status_change: Heating, Cooling, Dehumidifying, or Humidifying change (i2 only) + (call get_status() to get status). + +I2 Broadcast messages: + +If a group EF device is defined, MH will receive broadcast changes from the +thermostat. When enabled, broadcast messages for changes in setpoint, mode, +temp, and humidity will be sent to MH. When enabled, there is no reason to +poll the thermostat, except for possibly at reboot. To enable simply define +the EF group as described above and run sync links. + +Broadcast messages are NOT sent when the heater turns on/off. Broadcast +message are also NOT sent when the humidity setpoints are exceeded. Instead, +you must define the heating, high_humdid, and _low_humid groups and link them +to MH. (The base group 01 is the cooling group and should always be linked to +MH). When linked, these groups will send on/off commands to MH when these events +occur. Alternatively, you can periodically call the poll_simple method to check +the status of these attributes. + +Linking: + +I am not sure how or if the i1 device can be linked to other devices. + +I2 devices have 5 controllers, groups 01-04 plus the broadcast group EF. At the +moment, MH only supports using the thermostat as a controller of another device. +To control another device, simply define it as a scene member of the desired +thermostat group. The groups are: + + 01 - Cooling - Will send an ON/OFF command when the A/C is turned on/off. + 02 - Heating - Will send an ON/OFF command when the heater is turned on/off. + 03 - Humid High - Will send an ON/OFF command when the humidity exceeds the + humid high setpoint. + 04 - Humid Low - Will send an ON/OFF command when the humidity falls below the + humid low setpoint. + EF - Broadcast - Other than MH, I do not know if any other device can + respond to these commands. + +Tracking Child Objects: + +For both, i1 and i2 devices, optional child objects which track the states of the +thermostat can be created in user code: + + $thermo_temp = new Insteon::Thermo_temp($thermostat); + $thermo_fan = new Insteon::Thermo_fan($thermostat); + $thermo_mode = new Insteon::Thermo_mode($thermostat); + $thermo_setpoint_h = new Insteon::Thermo_setpoint_h($thermostat); + $thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); + $thermo_humidity = new Insteon::Thermo_humidity($thermostat); #Only available on i2 devices + $thermo_status = new Insteon::Thermo_status($thermostat); #Only available on i2 devices + +where $thermostat is the parent object to track. The state of these child objects +will be the state of the various objects. This makes the display of the various +states easier within MH. The child objects also make it easier to change the +various states on the thermostat. see code/examples/Insteon_thermostat.pl for more. @@ -392,6 +456,9 @@ sub parent_event { elsif ($p_state eq 'humid_change'){ $$self{child_humidity}->set_receive($$self{humid}, $self); } + elsif ($p_state eq 'status_change'){ + $$self{child_status}->set_receive($self->get_status(), $self); + } } # Overload methods we don't use, but would otherwise cause Insteon traffic. @@ -501,7 +568,7 @@ sub init { my ($self) = @_; $$self{message_types} = \%message_types; #Set saved state unique to i2 devices - $self->restore_data('humid'); + $self->restore_data('humid', 'cooling', 'heating', 'high_humid', 'low_humid'); # Create the broadcast dummy item # This may not belong here. Maybe this should go into read table A? @@ -521,6 +588,33 @@ sub init { $$self{bcast_item}->{object_name} = '$' . $self->get_object_name ."{bcast_item}"; } +sub set { + my ($self, $p_state, $p_setby, $p_respond) = @_; + my $root = $self->get_root(); + if (!(ref $p_setby) || !($p_setby->equals($self))) { + ::print_log("[Insteon::Thermo_i2] Sorry, you cannot control the ". + "thermostat in this manner. Please read the documentation ". + "for Insteon::Thermostat for help."); + return; + } + #Update the root object state + my $link_state = &Insteon::BaseObject::derive_link_state($p_state); + if ($self->group eq '01'){ + $root->_cooling($link_state); + } + elsif ($self->group eq '02') { + $root->_heating($link_state); + } + elsif ($self->group eq '03') { + $root->_high_humid($link_state); + } + elsif ($self->group eq '04') { + $root->_low_humid($link_state); + } + #Update the status of linked devices + $self->set_linked_devices($link_state); +} + sub sync_links{ my ($self, $audit_mode, $callback, $failure_callback) = @_; if (!$audit_mode){ @@ -548,6 +642,25 @@ sub poll_simple{ $self->_send_cmd($message); } +=item C + +Returns a text string describing the current status of the thermostat. May include +a combination of "Heating; Cooling; Dehumidifying; Humidifying; or Off." Only +available for I2 devices. + +=cut +sub get_status() { + my ($self) = @_; + my $root = $self->get_root(); + my $output = ""; + $output .= "Heating, " if ($$root{heating} eq 'on'); + $output .= "Cooling, " if ($$root{cooling} eq 'on'); + $output .= "Dehumidifying, " if ($$root{high_humid} eq 'on'); + $output .= "Humidifying" if ($$root{low_humid} eq 'on'); + $output = 'Off' if ($output eq ''); + return $output; +} + sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; @@ -736,6 +849,8 @@ sub hex_short_temp{ sub hex_status{ ### Not sure about this one yet, was 80 when set to auto but no activity + ## need to call _cooling, _heating, _high_humid, and _low_humid when + ## figured out } sub hex_heat{ @@ -752,6 +867,34 @@ sub _humid { return $$self{humid}; } +sub _cooling { + my ($self,$p_state) = @_; + $$self{cooling} = $p_state; + $self->set_receive('status_change'); + return $$self{cooling}; +} + +sub _heating { + my ($self,$p_state) = @_; + $$self{heating} = $p_state; + $self->set_receive('status_change'); + return $$self{heating}; +} + +sub _high_humid { + my ($self,$p_state) = @_; + $$self{high_humid} = $p_state; + $self->set_receive('status_change'); + return $$self{high_humid}; +} + +sub _low_humid { + my ($self,$p_state) = @_; + $$self{low_humid} = $p_state; + $self->set_receive('status_change'); + return $$self{low_humid}; +} + =item C Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', @@ -1012,6 +1155,27 @@ sub set_receive { my ($self, $p_state) = @_; $self->SUPER::set($p_state); } + +package Insteon::Thermo_status; +use strict; + +@Insteon::Thermo_status::ISA = ('Generic_Item'); + +sub new { + my ($class, $parent) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{parent} = $parent; + $$self{parent}{child_status} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "status_change"); + return $self; +} + +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} + 1; =back From e42ae6c90e1f60c08f898738850b6caa1004316f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 27 May 2013 19:00:02 -0700 Subject: [PATCH 068/330] Insteon_Thermostat: Add support for setting the high_humidity setpoint --- lib/Insteon/Thermostat.pm | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 553a05cca..8bee556e0 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -946,6 +946,25 @@ sub sync_time { $self->poll_simple(); } +=item C + +Sets the high humidity setpoint. + +=cut +sub high_humid_setpoint { + my ($self, $value) = @_; + main::print_log("[Insteon::Thermo_i2] Setting high humid setpoint -> $value") if $main::Debug{insteon}; + if($value !~ /^\d+$/){ + main::print_log("[Insteon::Thermostat] ERROR: Setpoint $value not numeric"); + return; + } + my $extra = "00000B" . sprintf("%02x", $value); + $extra .= '0' x (30 - length $extra); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$self{_ext_set_get_action} = 'set'; + $self->_send_cmd($message); +} + package Insteon::Thermo_i2_bcast; use strict; From 849fd9de036fcb3b0b2a67d5a1a8d944195622dd Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 1 Jun 2013 15:34:38 -0700 Subject: [PATCH 069/330] Insteon_Thermoi2: Add Support for Humidity Setpoints --- lib/Insteon/Thermostat.pm | 66 +++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 8bee556e0..489ef9a96 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -718,8 +718,27 @@ sub _process_message { $$self{sync_time} = undef; $self->_send_cmd($message); } - } else { - main::print_log("[Insteon::Thermo_i2] WARN: Corrupt Extended " + } + elsif (substr($msg{extra},0,8) eq "00000101") { + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); + #0 = 00 #14 = Cool SP + #2 = 00 #16 = Heat SP + #4 = 01 Response #18 = RF Offset + #6 = 01 Data Set 2 #20 = Energy Saving Setback + #8 = humid low #22 = External TempOffset + #10 = humid high #24 = 1 = Status Report Enabled + #12 = firmware #26 = 1 = External Power On + #28 = 1 = Int, 2=Ext Temp + main::print_log("[Insteon::Thermo_i2] Humidity setpoints for ". + $self->get_object_name . " are High: " . + $self->_high_humid(hex(substr($msg{extra}, 8, 2))) . + " Low: " . $self->_low_humid(hex(substr($msg{extra}, 10, 2))) + ) if $main::Debug{insteon}; + $clear_message = 1; + $self->_process_command_stack(%msg); + } + else { + main::print_log("[Insteon::Thermo_i2] WARN: Unknown Extended " ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; } } @@ -881,17 +900,20 @@ sub _heating { return $$self{heating}; } + sub _high_humid { my ($self,$p_state) = @_; - $$self{high_humid} = $p_state; - $self->set_receive('status_change'); + if ($p_state ne $$self{high_humid}) { + $$self{high_humid} = $p_state; + } return $$self{high_humid}; } sub _low_humid { my ($self,$p_state) = @_; - $$self{low_humid} = $p_state; - $self->set_receive('status_change'); + if ($p_state ne $$self{low_humid}) { + $$self{low_humid} = $p_state; + } return $$self{low_humid}; } @@ -965,6 +987,38 @@ sub high_humid_setpoint { $self->_send_cmd($message); } +=item C + +Sets the low humidity setpoint. + +=cut +sub low_humid_setpoint { + my ($self, $value) = @_; + main::print_log("[Insteon::Thermo_i2] Setting low humid setpoint -> $value") if $main::Debug{insteon}; + if($value !~ /^\d+$/){ + main::print_log("[Insteon::Thermostat] ERROR: Setpoint $value not numeric"); + return; + } + my $extra = "00000C" . sprintf("%02x", $value); + $extra .= '0' x (30 - length $extra); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$self{_ext_set_get_action} = 'set'; + $self->_send_cmd($message); +} + +=item C + +Retreives and prints the current humidity high and low setpoints. Only available for I2 devices. +=cut +sub get_humid_setpoints{ + my ($self) = @_; + my $extra = "00000001"; + $extra .= '0' x (30 - length $extra); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $$self{_ext_set_get_action} = 'get'; + $self->_send_cmd($message); +} + package Insteon::Thermo_i2_bcast; use strict; From 54b0beb6a69aed48c34c65af34d9f63551edbd7e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 1 Jun 2013 15:45:43 -0700 Subject: [PATCH 070/330] Insteon_Thermoi2: Elevate Broadcast Item to a Full Fledged Object There is a possibility that a user may define a link to the EF group. If a broadcast object is defined, sync_links will automatically enable the broadcast setting. --- lib/Insteon/Thermostat.pm | 37 +++---------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 489ef9a96..647fb1f35 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -569,23 +569,6 @@ sub init { $$self{message_types} = \%message_types; #Set saved state unique to i2 devices $self->restore_data('humid', 'cooling', 'heating', 'high_humid', 'low_humid'); - - # Create the broadcast dummy item - # This may not belong here. Maybe this should go into read table A? - # Otherwise, users cannot define insteon scenes containing this device. - # While rare a thermostat may be set to provide broadcast updates to - # another device - my $dev_id = $self->device_id(); - $dev_id =~ /(\w\w)(\w\w)(\w\w)/; - $dev_id = "$1.$2.$3"; - $$self{bcast_item} = new Insteon::Thermo_i2_bcast("$dev_id".':EF'); - - # Add bcast object to list of Insteon objects - Insteon::add($$self{bcast_item}); - - # Register bcast object with MH - &main::register_object_by_name('$' . $self->get_object_name ."{bcast_item}",$$self{bcast_item}); - $$self{bcast_item}->{object_name} = '$' . $self->get_object_name ."{bcast_item}"; } sub set { @@ -617,7 +600,9 @@ sub set { sub sync_links{ my ($self, $audit_mode, $callback, $failure_callback) = @_; - if (!$audit_mode){ + my $dev_id = $self->device_id(); + my $bcast_obj = Insteon::get_object($self->device_id(), 'EF'); + if (!$audit_mode && ref $bcast_obj){ #Make sure thermostat is set to broadcast changes ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; my $extra = "000008000000000000000000000000"; @@ -1019,22 +1004,6 @@ sub get_humid_setpoints{ $self->_send_cmd($message); } -package Insteon::Thermo_i2_bcast; -use strict; - -@Insteon::Thermo_i2_bcast::ISA = ('Insteon::BaseDevice', 'Insteon::DeviceController'); - -###This is basically a dummy object, it is designed to allow a link from group -###EF to be added as part of sync links. Group EF is the broadcast group used -###by the 2441th thermostat to announce changes. - -sub new { - my ($class, $p_deviceid) = @_; - my $self = new Insteon::BaseDevice($p_deviceid); - bless $self, $class; - return $self; -} - package Insteon::Thermo_mode; use strict; From 9aced81c21d2c30803e8c5da920c1b99af9ff2dc Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 1 Jun 2013 18:07:46 -0700 Subject: [PATCH 071/330] Insteon_Thermoi2: Only send Broadcast Request Once --- lib/Insteon/Thermostat.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 647fb1f35..4992f0954 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -602,7 +602,7 @@ sub sync_links{ my ($self, $audit_mode, $callback, $failure_callback) = @_; my $dev_id = $self->device_id(); my $bcast_obj = Insteon::get_object($self->device_id(), 'EF'); - if (!$audit_mode && ref $bcast_obj){ + if (!$audit_mode && ref $bcast_obj && $self->is_root){ #Make sure thermostat is set to broadcast changes ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; my $extra = "000008000000000000000000000000"; From 61102b2e8536f1f3c04517368a902c07765a1923 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Mon, 3 Jun 2013 12:26:53 -0400 Subject: [PATCH 072/330] Fix issues with INSTEON lights in web interface INSTEON lights are treated differently in the web interface versus X10 lights, for example: they get different icons, and cannot be dimmed or brightened. This patch does the following: - Use the same light icons for INSTEON lights that are used for X10 lights. - Allow dimming/brightening of INSTEON lights by clicking on the left or right side of the icon in the web interface (only dimmable light, of course). - Fix a logic error that prevents disabling the button image cache. Ran into this while creating new icons for my web interface. - As an experimental "feature", and a small delay to try to give MH a chance to receive the new state of an INSTEON device changed through the web interface. This will allow the correct state to be displayed when the web page renders again. Disabled by default since it's just a hack (though it works good enough for me). Comments welcome. --- web/bin/button.pl | 43 ++++++++++++++++++++---------- web/bin/button_action.pl | 56 +++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/web/bin/button.pl b/web/bin/button.pl index 2d779d090..9654b07b7 100644 --- a/web/bin/button.pl +++ b/web/bin/button.pl @@ -1,10 +1,15 @@ +# +# Create buttons with JPEG images generated on-the-fly using the GD module. +# +# For text buttons: +# Example: +# +# For item buttons: +# Example: +# $^W = 0; # Avoid redefined sub msgs -# Create jpeg buttons on-the-fly with GD module -# For text buttons: -# For item buttons: - # Authority: anyone my ($text, $type, $state, $bg_color, $file_name_only) = @ARGV; @@ -29,10 +34,14 @@ $image_file =~ s/ /_/g; # Blanks in file names are nasty $image_file = "/cache/$image_file.jpg"; - +# Set to 1 if you'd like to disable the image cache. Normally you should +# not need to do this because it affects performance (MisterHouse needs +# to re-generate the button image every time). This is only useful if you +# are tweaking your button images and need new images re-generated every +# time the button generation script (this script) is called. my $nocache = 0; -#$nocache = 1; -if (-e "$config_parms{data_dir}$image_file" or $nocache) { + +if (-e "$config_parms{data_dir}$image_file" && !$nocache) { return $image_file if $file_name_only; # print "Returning data from: $image_file\n"; my $data = file_read "$config_parms{data_dir}$image_file"; @@ -41,21 +50,27 @@ # Look for an icon my ($icon, $light); + if ($type eq 'item') { my $object = &get_object_by_name($text); ($icon) = &http_get_local_file(&html_find_icon_image($object, 'voice')); -# $light = 1 if $text =~ /light/i or $text =~ /lite/i; - $light = 1 if $object->isa('X10_Item') and !$object->isa('X10_Appliance'); - $light = 1 if $object->isa('EIB2_Item'); -} -else { -# Uncomment this to put in images into group, category icons. Seem too small to be useful. -# ($icon) = &http_get_local_file(&html_find_icon_image($text, 'text')); + + if ( ($object->isa('X10_Item') and !$object->isa('X10_Appliance') ) + || $object->isa('Insteon::BaseLight') + || $object->isa('EIB2_Item') + || $text =~ /light|lite/i) { + $light = 1; + } +} else { + # Uncomment this to put in images into group, category icons. Seem too small to be useful. + #($icon) = &http_get_local_file(&html_find_icon_image($text, 'text')); } + undef $icon if $icon and $icon !~ /.jpg$/i; # GD does not do gifs :( my $image_icon = GD::Image->newFromJpeg($icon) if $icon; my $image; + if ($image_icon or $type eq 'item') { # Template = blank_on/off/unk or blank_light_on/off/dim diff --git a/web/bin/button_action.pl b/web/bin/button_action.pl index 4c6fddf78..3d635755c 100644 --- a/web/bin/button_action.pl +++ b/web/bin/button_action.pl @@ -10,30 +10,62 @@ my ($state, $x, $y) = $state_xy =~ /(\S+)\?(\d+),(\d+)/; #print "db ln=$list_name, i=$item, s=$state_xy xy=$x,$y\n"; - # Do not dim the dishwasher :) -unless (eval qq|$item->isa('X10_Appliance') or $item->isa('Fan_Motor') or $item->isa('Insteon_Device')|) { - $state = 'dim' if $x < 40; # Left side of image - $state = 'brighten' if $x > 110; # Right side of image -} +my $object = &get_object_by_name($item); + +if ($object->isa('X10_Item') && !$object->isa('X10_Appliance') ) { + # Do not dim the dishwasher :) -if (eval qq|$item->isa('EIB7_Item')|) { # Motor/drive states are stop/up/down + # Dim if clicked on left side of image, brighten if clicked on right + # side of image, or use state passed through the button URL if clicked + # in the center of the image. + if ($x < 40) { + $state = 'dim'; + } elsif ($x > 110) { + $state = 'brighten'; + } +} elsif ($object->isa('EIB7_Item') ) { # Motor/drive states are stop/up/down $state = 'stop'; $state = 'down' if $x < 40; # Left side of image $state = 'up' if $x > 110; # Right side of image -} +} elsif ($object->isa('Insteon::DimmableLight') ) { + my @states = $object->get_states(); + my $curr_state = $object->state(); -#if (eval qq|$item->isa('Insteon_Device'|) { -# $state = "toggle"; -#} + # Find the index into @states for the element that corresponds to the + # current state. + my ($index) = grep { $states[$_] eq $curr_state } 0..$#states; -eval qq|$item->set("$state", 'web')|; -print "button_action.pl eval error: $@\n" if $@; + # Dim if clicked on left side of image, brighten if clicked on right + # side of image, or use state passed through the button URL if clicked + # in the center of the image. + if ($x < 40) { + $index-- if ($index); # Can't dim if light is off + $state = $states[$index]; + } elsif ($x > 110) { + $index++ if ($index != $#states); # Can't brighten if light is fully on + $state = $states[$index]; + } +} + +$object->set("$state", 'web'); # print "dbx4a i=$item s=$state\n"; # my $object = &get_object_by_name($item); # $state = $$object{state}; # print "dbx4b i=$item s=$state\n"; +# Internal state of INSTEON devices does not change immediately after +# clicking on the button. That is because, unlike X10 devices (for example), +# an acknowledgement from the INSTEON device needs to be received so MH +# can change the internal state. If we finish the HTTP transaction before +# the acknowledge comes back then the resulting HTML page will display the +# object that was just clicked on in the old state. This delay here prevents +# this problem at the expense of, well, an extra delay. As this is +# experimental, and this delay causes MH to pause for the duration of the +# delay, this is currently disabled by default. But feel free to enable +# to see if things improve. +#sleep(1); my $h = &referer("/bin/list_buttons.pl?$list_name"); + return &http_redirect($h); From b462b10b1de81b66ec38bf34615def709b33d1e5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 6 Jun 2013 21:12:22 -0700 Subject: [PATCH 073/330] Insteon: Change "initiate linking as controller" Group to 00 I was having issues where some sort of corruption within the PLM was causing it to send messages to the 01 group, very annoying. HouseLinc uses 00, plus the insteon documents seem to envision that 01 is a valid group that could be used for other purposes. Group 01 links will not be deleted by delete_orphans to prevent causing issues for other users. --- lib/Insteon/AllLinkDatabase.pm | 12 ++++++------ lib/Insteon_PLM.pm | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 52cdea2c0..de0283a64 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -464,10 +464,10 @@ sub delete_orphan_links } else { # no corresponding PLM link found in items.mht - if ($group eq '01') { - #ignore manual responder link to PLM group 01 required for I2CS devices + if ($group eq '01' || $group eq '00') { + #ignore manual responder link to PLM group 01 or 00 required for I2CS devices main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01") if $main::Debug{insteon} >= 2; + . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; } elsif ($audit_mode) { @@ -2398,9 +2398,9 @@ sub delete_orphan_links if (!($link)) { # a reference in the PLM's linktable does not match a scene member target - if ($group eq '01') { - #ignore manual controller link from PLM group 01 to device required for I2CS devices - main::print_log("[Insteon::ALDB_PLM] DEBUG2 Ignoring orphan PLM controller(01) link to " + if ($group eq '01' || $group eq '00') { + #ignore manual controller link from PLM group 01 or 00 to device required for I2CS devices + main::print_log("[Insteon::ALDB_PLM] DEBUG2 Ignoring orphan PLM controller(01 or 00) link to " . $device->get_object_name() ) if $main::Debug{insteon} >= 2; } elsif ($audit_mode) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index db50f4a0e..07f674476 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -240,7 +240,7 @@ sub initiate_linking_as_controller { my ($self, $group) = @_; - $group = '01' unless $group; + $group = '00' unless $group; # set up the PLM as the responder my $cmd = '01'; # controller code $cmd .= $group; # WARN - must be 2 digits and in hex!! From e654e93c7c167d5f4efe0fc626683452ca3a4151 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 7 Jun 2013 17:45:25 -0700 Subject: [PATCH 074/330] Insteon_Thermo: Catch Status Request Responses Catch and process status request responses before processing unique _process_message commands. --- lib/Insteon/Thermostat.pm | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 4992f0954..251ceb2ff 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -406,7 +406,14 @@ sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; - if ($msg{command} eq "thermostat_setpoint_cool" && $msg{is_ack}){ + my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; + my $ack_setby = (ref $$self{m_status_request_pending}) ? $$self{m_status_request_pending} : $p_setby; + if ($msg{is_ack} && $self->_is_info_request($pending_cmd,$ack_setby,%msg)) { + $clear_message = 1; + $$self{m_status_request_pending} = 0; + $self->_process_command_stack(%msg); + } + elsif ($msg{command} eq "thermostat_setpoint_cool" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of cool setpoint ". "for ". $self->get_object_name) if $main::Debug{insteon}; @@ -649,7 +656,14 @@ sub get_status() { sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; - if ($msg{command} eq "extended_set_get" && $msg{is_ack}){ + my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; + my $ack_setby = (ref $$self{m_status_request_pending}) ? $$self{m_status_request_pending} : $p_setby; + if ($msg{is_ack} && $self->_is_info_request($pending_cmd,$ack_setby,%msg)) { + $clear_message = 1; + $$self{m_status_request_pending} = 0; + $self->_process_command_stack(%msg); + } + elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received main::print_log("[Insteon::Thermo_i2] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; From 9a9b53626f3e70df726f0305282a1d0e86add8d0 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Sat, 15 Jun 2013 09:46:42 -0500 Subject: [PATCH 075/330] Modified hop count tracking to use running average vs. max In addition to the running average implementation, this update also provides a few slight performance optimizations. One of the primary advantages to this algorythm is that it does not need to loop over then entire array for each hop count update. Also enhanced log_aldb_status a bit to include the device name. --- lib/Insteon/BaseInsteon.pm | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index a0d50044c..924502cbe 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -156,18 +156,24 @@ sub group sub default_hop_count { my ($self, $hop_count) = @_; - unshift(@{$$self{hop_array}}, $$self{default_hop_count}) if (!defined(@{$$self{hop_array}})); if (defined($hop_count)){ ::print_log("[Insteon::BaseObject] DEBUG3: Adding hop count of " . $hop_count . " to hop_array of " . $self->get_object_name) if $main::Debug{insteon} >= 3; - unshift(@{$$self{hop_array}}, $hop_count) - } - pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >20); - my $high = 0; - foreach (@{$$self{hop_array}}){ - $high = $_ if ($high < $_);; + if (!defined(@{$$self{hop_array}})) { + unshift(@{$$self{hop_array}}, $$self{default_hop_count}); + $$self{hop_sum} = $$self{default_hop_count}; + } + #Calculate a simple moving average + unshift(@{$$self{hop_array}}, $hop_count); + $$self{hop_sum} += ${$$self{hop_array}}[0]; + $$self{hop_sum} -= pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >20); + $$self{default_hop_count} = int(($$self{hop_sum} / scalar(@{$$self{hop_array}})) + 0.5); + + ::print_log("[Insteon::BaseObject] DEBUG4: ".$self->get_object_name + ."->default_hop_count()=".$$self{default_hop_count} + ." :: hop_array[]=". join("",@{$$self{hop_array}})) + if $main::Debug{insteon} >= 4; } - $$self{default_hop_count} = $high; return $$self{default_hop_count}; } @@ -1239,7 +1245,8 @@ sub scan_link_table sub log_aldb_status { my ($self) = @_; - main::print_log( " Hop Count: ".$self->default_hop_count()); + main::print_log( " Device ID: ".$self->device_id()); + main::print_log( " Hop Count: ".$self->default_hop_count()." :: [". join("",@{$$self{hop_array}})."]"); main::print_log( "Engine Version: ".$self->engine_version()); my $aldb = $self->get_root()->_aldb; if ($aldb) From 10dd42701633e96f463cddaf3a6490cf9273079c Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Wed, 26 Jun 2013 12:11:32 -0500 Subject: [PATCH 076/330] Fix issue #224. WeatherBug - Mixed case city search corrections Removed debug flag override; was unable to effectively set debug from .ini or web interface Eliminated lowercase conversion of city name Modified forecast city search API call to use lowercase Modified forecast city match to ignore case Added additional debug logging for forecase city match --- code/common/weather_weatherbug.pl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code/common/weather_weatherbug.pl b/code/common/weather_weatherbug.pl index 6ec5be5bd..e2013d8f9 100755 --- a/code/common/weather_weatherbug.pl +++ b/code/common/weather_weatherbug.pl @@ -45,10 +45,9 @@ # ----------------------------------------------------------------------- # noloop=start use Weather_Common; - $Debug{weatherbug} =0; # Define config parameters # add variables for the weatherbug feed - my $weatherbug_city=lc($config_parms{weather_weatherbug_city}); + my $weatherbug_city=$config_parms{weather_weatherbug_city}; my $weatherbug_state=lc($config_parms{weather_weatherbug_state}); my $weatherbug_country=lc($config_parms{weather_weatherbug_country}); my $weatherbug_units= $config_parms{weather_weatherbug_units}; @@ -60,7 +59,7 @@ # build the url from existing init variables if the values have been left blank $weatherbug_state=uc($config_parms{state}) unless $weatherbug_state; $weatherbug_country=uc($config_parms{country}) unless $weatherbug_country; - $weatherbug_city=lc($config_parms{city}) unless $weatherbug_city; + $weatherbug_city=$config_parms{city} unless $weatherbug_city; $weatherbug_zipcode= $config_parms{zipcode} unless $weatherbug_zipcode; $weatherbug_latitude= $config_parms{latitude} unless $weatherbug_latitude; $weatherbug_longitude= $config_parms{longitude} unless $weatherbug_longitude; @@ -84,7 +83,7 @@ # Define the new process item that fetchs the RSS feed for the location $p_weather_weatherbug_forecast = new Process_Item(qq{get_url -quiet "$weatherbug_url" "$weatherbug_file"}); # Need to add a process to search for the citycode - $weatherbug_url="http://api.wxbug.net/getLocationsXML.aspx?ACode=".$key."&searchString=".$weatherbug_city; + $weatherbug_url="http://api.wxbug.net/getLocationsXML.aspx?ACode=".$key."&searchString=".lc($weatherbug_city); logit("$config_parms{data_dir}/web/weatherbug_debug",$weatherbug_url,13,0) if($Debug{weatherbug}); # Define the new process item that fetchs the RSS feed for the location $p_weather_weatherbug_citycode = new Process_Item(qq{get_url -quiet "$weatherbug_url" "$weatherbug_file"}); @@ -425,10 +424,11 @@ # There should be a title for forecast in the text for the # city if the fetch was successful $Weather{weatherbug_fcst_valid} = 0; #Set to not valid unless proven - my $search_for = "Forecast for ".ucfirst($weatherbug_city); + my $search_for = "Forecast for $weatherbug_city"; my $title = $channel->first_child_text("title"); - logit("$config_parms{data_dir}/web/weatherbug_debug",$search_for,13,0) if($Debug{weatherbug}); - if ($title =~ /$search_for/ ) { + logit("$config_parms{data_dir}/web/weatherbug_debug","Search for: ".$search_for,13,0) if($Debug{weatherbug}); + logit("$config_parms{data_dir}/web/weatherbug_debug","WeatherBug returned: $title",13,0) if($Debug{weatherbug}); + if ($title =~ /$search_for/i ) { $Weather{weatherbug_fcst_valid} = 1; } else { From 39788ba96b3d63a51b2d8aa219995270169b7e18 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Wed, 26 Jun 2013 20:22:25 -0500 Subject: [PATCH 077/330] Fix issue #227. Remove weather_aws.pl - serive is not longer available Use weather_weatherbug.pl or internet_weather.pl as an alternative. --- code/common/weather_aws.pl | 317 ------------------------------------- 1 file changed, 317 deletions(-) delete mode 100644 code/common/weather_aws.pl diff --git a/code/common/weather_aws.pl b/code/common/weather_aws.pl deleted file mode 100644 index 606a3b46a..000000000 --- a/code/common/weather_aws.pl +++ /dev/null @@ -1,317 +0,0 @@ -# Category=Weather - -# $Date$ -# $Revision$ - -#@ This code will retrieve and parse data from an AWS weather station via -#@ their website. - -#@ Updated: 2008-01-16 -#@ The method for finding the station ID has changed: -#@ Go to http://www.aws.com/aws_2001/broadcasters/asp/Online.asp and enter your zip code: ie: 30005 -#@ It then brings up the following page: -#@ http://weather.weatherbug.com/GA/Alpharetta-weather.html?zcode=z6169 -#@ View Source and search for stat= (DNWDY is the station ID for my location) -#@ The full URL that this code uses looks like: http://www.aws.com/full.asp?id=dnwdy -#@ then add the id to the aws_id config parameter in your mh.ini or -#@ mh.private.ini:

-#@ -#@ aws_id = TMISC -#@ aws_id = STATION1,STATION2,STATION3 -#@ -#@ As you can see, you can specify multiple stations. The first station will -#@ always be tried first. If data is not available, the remaining stations -#@ will be tried in order until we find good data. -#@

-#@ The URL containing the ID of the TMISC AWS station is shown for example: -#@ -#@ http://www.aws.com/aws_2001/asp/obsForecast.asp?id=TMISC&obs=full - -=begin comment - - weather_aws.pl - Created by Brian Rudy (brudyNO@SPAMpraecogito.com) - - This code will retrieve and parse data from an AWS weather station via - AWS's website. - - Most AWS sites are k-12 schools and non profit organizations like museums. - Most sites upload their data in real time to AWS whenever a request is - made via a remote site. Be warned some sites only update their sensor data - when their internet connection is up (usually during the school day). - - If you need real-time data, you should check the 'staleness' of the - timestamp on the returned data from your selected site to make sure it's - current and is being updated when you need the data. - - - Revision History - - Version 1.3 January 23, 2003 - Bug fixes from Martin Dolphin (with help from Bruce) to support negative - temperature, windchill and dewpoint values. Updated weather summary for - status line. - - Version 1.2 October 26, 2002 - General code cleanup and compatability enhacements. Support for status_line.pl, - seamless support for Bruce's weather_monitor.pl and weather_log.pl. - - Version 1.1b October 1, 2002 - Added convert_direction_br as temporary workaround for weather_monitor.pl - wind direction problem. - - Version 1.1 April 13, 2002 - Updated table depth per Ron Wright's suggestion. - - Version 1.0 December 13, 2000 - Complete re-write supporting MH 2.36's Weather_Item, and - Bruce's weather_log.pl, and weather_monitor.pl. - Added hooks to use as library. - - Version 0.2 September 20, 2000 - Updated $AWSWeatherURL to reflect change on AWS's web site. - - Version 0.1 September 14, 2000 - Yea, it works! - -=cut - -use HTML::TableExtract; -use Weather_Common; - -# noloop=start -my $aws_ids=$config_parms{aws_id}; -$aws_ids='TMISC' unless $aws_ids; -$aws_ids=~s/\s//g; -my @AWS_IDs = split(/,/,$aws_ids); -$p_awsweather_page = new Process_Item; -my $AWS_ID_index=0; -&set_aws_index($AWS_ID_index); - -my $prev_timestamp; -my $AWSWeatherURL="http://www.aws.com/full.asp?id="; -$v_get_aws_weather = new Voice_Cmd('Get AWS weather data'); -my $f_awsweather_html; - -# noloop=stop - -# These values aren't provided by this code, but leaving them undefined causes -# problems with logging. Leave them commented out if you have external code to -# fill them with usefull data. -#$Weather{TempIndoor} = 0.00; -#$Weather{HumidIndoor} = 0.00; - -# *** Conditions?, IsRaining?, etc. - -# Create trigger - -if ($Reload) { - &trigger_set("new_minute 5", "run_voice_cmd('Get AWS weather data')", 'NoExpire', 'get aws weather') - unless &trigger_get('get aws weather'); -} - -# Events - -sub set_aws_index { - ($AWS_ID_index)=@_; - - if ($AWS_ID_index > $#AWS_IDs) { - $AWS_ID_index = 0; - } - my $aws_id=$AWS_IDs[$AWS_ID_index]; - $f_awsweather_html = "$config_parms{data_dir}/web/${aws_id}.html"; - $p_awsweather_page->set(qq!get_url -quiet "${AWSWeatherURL}${aws_id}" "$f_awsweather_html"!); - return $AWS_ID_index; -} - -if (said $v_get_aws_weather) { - if (&net_connect_check) { - $v_get_aws_weather->respond("app=weather Retrieving AWS weather..."); - # always start at the 1st station - &set_aws_index(0); - # Use start instead of run so we can detect when it is done - start $p_awsweather_page; - } else { - $v_get_aws_weather->respond("I must be connected to the Internet to get weather data."); - } -} - -# This would be far more efficient if AWS allowed access to -# their SQL db. Might be time for a little reverse engineering... -# *** More like a little XML... (if they have it, otherwise forget them.) -if (done_now $p_awsweather_page) { - - my $html = file_read $f_awsweather_html; - return unless $html; - - if ($html =~ 'Temporarily Unavailable') { - &print_log("weather_aws: info not available for station ".$AWS_IDs[$AWS_ID_index]); - if (&set_aws_index($AWS_ID_index+1)) { - &print_log("weather_aws: moving forward to next station: ".$AWS_IDs[$AWS_ID_index]); - $p_awsweather_page->start; - } else { - &print_log("weather_aws: no more stations to use"); - } - return; - } - - # hash used to temporarily store weather info before selective load into %Weather - my %w=(); - - my $te = new HTML::TableExtract( depth => 2, count => 1, subtables => 1); - - $te->parse($html); - my @cell = $te->rows; - - # Timestamp of last sucessful data retrieval from internet node - $cell[0][0] =~ m/\s+(\d+)\/(\d+)\/(\d+) - (\d+):(\d+):(\d+) (\w+)/; - my $timestamp = "$1$2$3$4$5$6$7"; - return unless $timestamp and $timestamp ne $prev_timestamp; - $prev_timestamp = $timestamp; - #print "Timestamp of last sucessfull data retrieval: $1\/$2\/$3 $4:$5:$6 $7\n"; - - $cell[1][1] =~ m/\s+(-?\d+).(\d+)/; - #print "Current temperature: ", join(".", $1, $2), "°F\n"; - $w{TempOutdoor} = join(".", $1, $2); - - #$cell[1][2] =~ m/\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n/; - #print "Min temp: ", join(".", $1, $2), "°F at ", "$3:$4 $5\n"; - #print "Max temp: ", join(".", $6, $7), "°F at ", "$8:$9 $10\n"; - - # Currently no way to determine if increasing or decreasing. - # Need to parse image name at the end of this cell to get it. - #$cell[1][3] =~ m/\s+(\d+).(\d+)/; - #print "Temperature hourly increase/decrease: ", join(".", $1, $2), "\n"; - - $cell[2][1] =~ m/\s+(\d+).(\d+)/; - #print "Current humidity: ", join(".", $1, $2), "\n"; - $w{HumidOutdoor} = join(".", $1, $2); - $w{HumidOutdoorMeasured}=1; # tell Weather_Common that we directly measured humidity - - #$cell[2][2] =~ m/\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n/; - #print "Min humidity: ", join(".", $1, $2), "\% at ", "$3:$4 $5\n"; - #print "Max humidity: ", join(".", $6, $7), "\% at ", "$8:$9 $10\n"; - - # Currently no way to determine if increasing or decreasing. - # Need to parse image name at the end of this cell to get it. - #$cell[2][3] =~ m/\s+(\d+).(\d+)/; - #print "Humidity hourly increase/decrease: ", join(".", $1, $2), "\n"; - - $cell[3][1] =~ m/(\w+) at\s+(\d+).(\d+)/; - #print "Current wind direction: $1, Speed: ", join(".", $2, $3), "\n"; - #$w{WindAvgDir} = $1; - my $newdirection = $1; - $w{WindAvgSpeed} = join(".", $2, $3); - $w{WindAvgDir} = convert_wind_dir_abbr_to_num($newdirection); - - $cell[3][2] =~ m/\s+(\w+)\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n/; - #print "Max wind direction: $1, Speed: ", join(".", $2, $3), " at $4:$5 $6\n"; - my $newgustdir = $1; - my $newgusttime = "$4:$5 $6"; - if ($Weather{WindGustTime} ne $newgusttime) { - $w{WindGustSpeed} = join(".", $2, $3); - $w{WindGustDir} = convert_wind_dir_abbr_to_num($newgustdir); - $w{WindGustTime} = $newgusttime; - } - else { - $w{WindGustSpeed} = $w{WindAvgSpeed}; - $w{WindGustDir} = $w{WindAvgDir}; - } - - $cell[4][1] =~ m/\s+(\d+).(\d+)/; - #print "Current rain: ", join(".", $1, $2), "\n"; - $w{RainTotal} = join(".", $1, $2) unless $config_parms{aws_ignore_rain}; - - # *** Set IsRaining here - - - # Don't know how to parse this until it rains and we get some data. - #$cell[4][2] =~ m/\s+(\d+).(\d+)\s+\"\/h\s+at\s+(\d+):(\d+)(\D+)/ - #print "Rain max of ", join("." $1, $2), " at ", join(":", $2, $3), $4, "\n" unless $config_parms{aws_ignore_rain}; - - - $cell[4][3] =~ m/\s+(\d+).(\d+)/; - #print "Rain hourly increase/decrease: ", join(".", $1, $2), "\n"; - $w{RainRate} = join(".", $1, $2) unless $config_parms{aws_ignore_rain}; - - $cell[5][1] =~ m/\s+(\d+).(\d+)/; - #print "Current pressure: ", join(".", $1, $2), "in Hg\n"; - $w{BaromSea} = join(".", $1, $2); - $w{Barom}=convert_sea_barom_to_local_in($w{BaromSea}); - - #$cell[5][2] =~ m/\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n/; - #print "Max pressure: ", join(".", $1, $2), "in Hg at ", "$3:$4 $5\n"; - #print "Min pressure: ", join(".", $6, $7), "in Hg at ", "$8:$9 $10\n"; - - #$cell[5][3] =~ m/\s+(\d+).(\d+)/; - #print "Pressure hourly increase/decrease: ", join(".", $1, $2), "\n"; - - # *** Set rising/falling - - $cell[6][1] =~ m/\s+(\d+).(\d+)/; - #print "Current light: ", join(".", $1, $2), "\%\n"; - $w{sun_sensor} = join(".", $1, $2); - - # *** Set conditions - - #$cell[6][2] =~ m/\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n\s+(\d+).(\d+) at\s+(\d+):(\d+)(\D+)\n/; - #print "Max light: ", join(".", $1, $2), "\% at ", "$3:$4 $5\n"; - #print "Min light: ", join(".", $6, $7), "\% at ", "$8:$9 $10\n"; - - #$cell[6][3] =~ m/\s+(\d+).(\d+)/; - #print "Light hourly increase/decrease: ", join(".", $1, $2), "\n"; - - #$cell[7][0] =~ m/\n\s+\w+ \w+\n\n\s+(\d+).(\d+) /; - $cell[7][0] =~ m/(-?\d+).(\d+) /; - # heat index/wind chill - #print "Heat index: ", join(".", $1, $2), "°F\n"; - - # wind chill is now calculated by Weather_Common using a modern formula - #$w{WindChill} = join(".", $1, $2); - - #$cell[7][1] =~ m/\s\w+ \w+\s+(\d+).(\d+) /; - #print "Monthly rain: ", join(".", $1, $2), "in\n"; - - $cell[7][2] =~ m/\s\w+ \w+\s+(-?\d+).(\d+) /; - #print "Dew point: ", join(".", $1, $2), "°F\n"; - $w{DewOutdoor} = join(".", $1, $2); - - #$cell[7][3] =~ m/\s\w+ \w+\s+(\d+).(\d+) /; - #print "Wet bulb: ", join(".", $1, $2), "°F\n"; - - if ($config_parms{weather_uom_temp} eq 'C') { - grep {$w{$_}=convert_f2c($w{$_});} qw( - TempOutdoor - DewOutdoor - ); - } - if ($config_parms{weather_uom_baro} eq 'mb') { - grep {$w{$_}=convert_in2mb($w{$_});} qw( - Barom - BaromSea - ); - } - if ($config_parms{weather_uom_wind} eq 'kph') { - grep {$w{$_}=convert_mile2km($w{$_});} qw( - WindGustSpeed - WindAvgSpeed - ); - } - if ($config_parms{weather_uom_wind} eq 'm/s') { - grep {$w{$_}=convert_mph2mps($w{$_});} qw( - WindGustSpeed - WindAvgSpeed - ); - } - - &populate_internet_weather(\%w, $config_parms{weather_internet_elements_aws}); - &weather_updated; - if ($Debug{weather}) { - foreach my $key (sort(keys(%w))) { - &print_log("weather_aws: $key is ".$w{$key}); - } - } - &print_log("weather_aws: finished retrieving weather for station $AWS_IDs[$AWS_ID_index]"); - $v_get_aws_weather->respond('app=weather connected=0 Weather data retrieved.'); - -} \ No newline at end of file From 235bc5c01e8835e8c5fef9979472e91e1c19668a Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Sun, 30 Jun 2013 11:44:28 -0500 Subject: [PATCH 078/330] Updated Insteon state definition for new Insteon object model Moved responsibility for Insteon states out of http_server.pl and into the Insteon objects themselves. (i.e. Insteon::DimmableLight) The existing code was not working with the new object model. This is an additional fix for Issue #212. Open Issue: http_server.pl->button_action() probably needs to be modified so Insteon lights dim/brighten correctly. It doesn't look like is_dimable() was ever implemented for any MH object. --- lib/Insteon.pm | 3 --- lib/Insteon/Lighting.pm | 5 +++++ lib/http_server.pl | 4 ---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index eb3b59c47..799f26c3f 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -549,7 +549,6 @@ so that each class can have its own unique set of voice commands. sub generate_voice_commands { - my $insteon_menu_states = $main::config_parms{insteon_menu_states} if $main::config_parms{insteon_menu_states}; &main::print_log("Generating Voice commands for all Insteon objects"); my $object_string; for my $object (&main::list_all_objects) { @@ -594,8 +593,6 @@ sub generate_voice_commands $object_string .= &main::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_link_commands'); push @_insteon_link, $object_name; } elsif ($object->isa('Insteon::BaseDevice')) { - $states = $insteon_menu_states if $insteon_menu_states - && ($object->can('is_dimmable') && $object->is_dimmable); my $cmd_states = "$states,status,get engine version,scan link table,log links,update onlevel/ramprate"; #,on level,ramp rate"; $cmd_states .= ",link to interface,unlink with interface" if $object->isa("Insteon::BaseController") || $object->is_controller; $object_string .= "$object_name_v = new Voice_Cmd '$command [$cmd_states]';\n"; diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index c7660e965..be9146285 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -216,6 +216,11 @@ sub new my $self = new Insteon::BaseLight($p_deviceid,$p_interface); bless $self,$class; + + if( $main::config_parms{insteon_menu_states}) { + push(@{$$self{states}}, split( ',', $main::config_parms{insteon_menu_states}) ); + } + return $self; } diff --git a/lib/http_server.pl b/lib/http_server.pl index 9f44eb88a..4e3ff7e69 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -2367,9 +2367,7 @@ sub html_item_state { my $object_name = $object->{object_name}; my $object_name2 = &pretty_object_name($object_name); my $isa_X10 = UNIVERSAL::isa($object, 'X10_Item'); -# my $isa_X10 = $object->isa('X10_Item'); # This will abend if object is not an object my $isa_EIB2 = UNIVERSAL::isa($object, 'EIB2_Item'); - my $isa_insteon = UNIVERSAL::isa($object, 'Insteon_Device'); # If not a state item, just list it unless ($isa_X10 or UNIVERSAL::isa($object, 'Group') or exists $object->{state} or $object->{states}) { @@ -2384,9 +2382,7 @@ sub html_item_state { # If >2 possible states, add a Select pull down form my @states; @states = @{$object->{states}} if $object->{states}; -# print "db on=$object_name ix10=$isa_X10 s=@states\n"; @states = split ',', $config_parms{x10_menu_states} if $isa_X10; - @states = split ',', $config_parms{insteon_menu_states} if $isa_insteon; @states = qw(on off) if UNIVERSAL::isa($object, 'X10_Appliance'); From 283432d0320c63f2ea7dc04f9215a8c624352e9c Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Sun, 30 Jun 2013 17:26:50 -0500 Subject: [PATCH 079/330] Corrections for setting Insteon device states for web Flip order of on/off states to match new web object model Used the set_states() function rather than directly modifying data from the Generic_Item class. Eliminated the save/restore of the "states" array. I'm not sure what the original pupose was but saving/restoring prevents users from defining additional states in their ini file. Note that the current device "state" is still stored. --- lib/Insteon/BaseInsteon.pm | 20 +++++--------------- lib/Insteon/Lighting.pm | 6 +++--- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 3ea998f5d..a976953d7 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1756,33 +1756,23 @@ sub restore_string { $restore_string .= $self->_aldb->restore_string(); } - if ($$self{states}) - { - my $states = ''; - foreach my $state (@{$$self{states}}) - { - $states .= '|' if $states; - $states .= $state; - } - $restore_string .= $self->{object_name} . "->restore_states(q~$states~);\n"; - } return $restore_string; } =item C -Used to reload the persistent states of variables on restart. +Obsolete / do not use. + +Function should remain so that upgrading users will not have issues starting +MH from previous versions that referenced this function in the +mh_temp.saved_states file. =cut sub restore_states { my ($self, $states) = @_; - if ($states) - { - @{$$self{states}} = split(/\|/,$states); - } } =item C diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index be9146285..5d53fea1e 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -33,8 +33,8 @@ sub new my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); bless $self,$class; - # include very basic states - @{$$self{states}} = ('on','off'); + # include very basic states; off first so web interface up/down works + $self->set_states('off','on'); return $self; } @@ -218,7 +218,7 @@ sub new bless $self,$class; if( $main::config_parms{insteon_menu_states}) { - push(@{$$self{states}}, split( ',', $main::config_parms{insteon_menu_states}) ); + $self->set_states(split( ',', $main::config_parms{insteon_menu_states})); } return $self; From eadf6e0d3f69ef2c7d297d7ffd252b3b89c9af35 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Mon, 1 Jul 2013 07:57:43 -0500 Subject: [PATCH 080/330] Make sure web icons show 'on' if light state is 100% --- web/bin/list_buttons.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/web/bin/list_buttons.pl b/web/bin/list_buttons.pl index 7bbc1c533..4f8fe62c9 100644 --- a/web/bin/list_buttons.pl +++ b/web/bin/list_buttons.pl @@ -65,6 +65,7 @@ my $icon; if ($Info{module_GD}) { # Use custom icons if they exist + $state = 'on' if $state eq '100%'; $icon = $state; $icon = 'dim' if $state =~ /d+/; my $image = "/graphics/light-" . lc $item . "_" . $icon . ".gif"; From e1db05a0a830b6855dc8528d761312b507e759e9 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Mon, 1 Jul 2013 08:40:34 -0500 Subject: [PATCH 081/330] Better set of states for insteon_menu_states given new behavior --- bin/mh.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/mh.ini b/bin/mh.ini index 69d51a153..aa65a3df8 100644 --- a/bin/mh.ini +++ b/bin/mh.ini @@ -2058,7 +2058,7 @@ eib_errata=2 @ These are the states displayed on the tk and web menus @ French: insteon_menu_states=on,off,normal,eco,plus,moins,plus2,moins2,plus3,moins3,+40,-40,5%,30%,60%,100% -insteon_menu_states=on,off,+40,-40,5%,30%,60%,100% +insteon_menu_states=off,20%,40%,50%,60%,80%,on ****************************************************************************** # Category = Misc @@ -2451,4 +2451,4 @@ owfs_uom_temp = F # - add net parms. add cm11_serial parm. # -# \ No newline at end of file +# From 871d5e7e332eb27567a5d6a15127ebf3780619f2 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Thu, 4 Jul 2013 22:45:35 -0500 Subject: [PATCH 082/330] Fix issue where setting level 100% sets to 0xfe Changed each instance of this calcualtion to use 5/4 rounding prior to calling sprintf(). There is some issue between perl data types and the C sprintf() implementation. In this case perl will print 255 but sprintf will return fe. Some instances of this were previously fixed by adding check logic to force fe or 254 to 100%. I modified all the instances to be consistent. --- lib/Insteon/BaseInsteon.pm | 4 ++-- lib/Insteon/Lighting.pm | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index a976953d7..4aaad4330 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -528,7 +528,7 @@ sub derive_message } else { if ($command eq 'on') { - $message->extra(sprintf("%02X",$level)); + $message->extra(sprintf("%02X",int($level+.5))); } else { $message->extra('00'); } @@ -594,7 +594,7 @@ sub _is_info_request my $is_info_request = 0; if ($cmd eq 'status_request') { $is_info_request++; - my $ack_on_level = (hex($msg{extra}) >= 254) ? 100 : sprintf("%d", hex($msg{extra}) * 100 / 255); + my $ack_on_level = sprintf("%d", int((hex($msg{extra}) * 100 / 255)+.5)); &::print_log("[Insteon::BaseObject] received status for " . $self->{object_name} . " with on-level: $ack_on_level%, " . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 5d53fea1e..92d69cf18 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -193,13 +193,7 @@ sub convert_level my $level = 'ff'; if (defined ($on_level)) { $on_level =~ s/(\d+)%?/$1/; - if ($on_level eq '100') { - $level = 'ff'; - } elsif ($on_level eq '0') { - $level = '00'; - } else { - $level = sprintf('%02X',$on_level * 2.55); - } + $level = sprintf('%02X',int(($on_level * 2.55) + .5)); } return $level; } From 313d46edec465d47d692af219d6250bb2a16a080 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Fri, 5 Jul 2013 08:12:59 -0500 Subject: [PATCH 083/330] Make sure web icons show 'off' if light state is 0% --- web/bin/list_buttons.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/web/bin/list_buttons.pl b/web/bin/list_buttons.pl index 4f8fe62c9..3dedf71f5 100644 --- a/web/bin/list_buttons.pl +++ b/web/bin/list_buttons.pl @@ -66,6 +66,7 @@ if ($Info{module_GD}) { # Use custom icons if they exist $state = 'on' if $state eq '100%'; + $state = 'off' if $state eq '0%'; $icon = $state; $icon = 'dim' if $state =~ /d+/; my $image = "/graphics/light-" . lc $item . "_" . $icon . ".gif"; From d96571b25aa0249b5a3d6d10b5b3e37b497101f8 Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Sat, 13 Jul 2013 23:36:06 -0500 Subject: [PATCH 084/330] Fix issue #232: fix automated version check Modified to match recent changes to docs/download.pod Modified to make revision optional. (i.e. 3.0 R1). If revision is not present then don't print "revision unknown". Updated syntax to eliminate perl warnings --- code/common/mh_release.pl | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/code/common/mh_release.pl b/code/common/mh_release.pl index 1188c38c8..7e8392446 100644 --- a/code/common/mh_release.pl +++ b/code/common/mh_release.pl @@ -33,7 +33,11 @@ sub parse_version { my ($maj,$min) = $Version =~ /(\d)\.(\d*)/; my ($rev) = $Version =~ /R(\d*)/; - return ($maj, $min, $rev); + $maj = $Version unless($maj); + my $version_str = $maj; + $version_str .= ".$min" unless($min eq ''); + $version_str .= " (revision $rev)" if($rev); + return ($maj, $min, $version_str); } sub calc_age { @@ -57,14 +61,13 @@ sub calc_age { if (said $v_version) { - my ($maj,$min,$revision) = &parse_version(); - $revision = "unknown" unless $revision; + my ($maj,$min,$version_str) = &parse_version(); if (($Save{mhdl_maj} > $maj) or (($Save{mhdl_maj} == $maj) and ($Save{mhdl_min} > $min))) { - respond "app=control I am version $maj.$min (revision $revision) and $Save{mhdl_maj}.$Save{mhdl_min} was released " . &calc_age($Save{mhdl_date}) . '.'; + respond("app=control I am version $version_str and $Save{mhdl_maj}.$Save{mhdl_min} was released " . &calc_age($Save{mhdl_date}) . '.'); } else { - respond "app=control I am version $maj.$min (revision $revision), released " . &calc_age($Save{mhdl_date}) . '.'; + respond("app=control I am version $version_str."); } } @@ -74,7 +77,7 @@ sub calc_age { if (&net_connect_check) { $msg = 'Checking version...'; - print_log "Retrieving download page"; + print_log("Retrieving download page"); start $p_mhdl_page; } else { @@ -86,10 +89,10 @@ sub calc_age { if (done_now $p_mhdl_page) { - my @html = file_head($mhdl_file,16); - print_log "Download page retrieved"; + my @html = file_read($mhdl_file); + print_log("Download page retrieved"); foreach(@html) { - next unless /^

Version (\d+)\.(\d+) released on (.*):/i; + next unless /Version (\d+)\.(\d+).*released on (\d+\/\d+\/\d+)/; $Save{mhdl_maj} = $1; $Save{mhdl_min} = $2; $Save{mhdl_date} = $3; @@ -98,14 +101,13 @@ sub calc_age { if (defined $Save{mhdl_maj} and defined $Save{mhdl_min}) { - my ($maj,$min,$revision) = &parse_version(); - $revision = "unknown" unless $revision; + my ($maj,$min,$version_str) = &parse_version(); if (($Save{mhdl_maj} > $maj) or (($Save{mhdl_maj} == $maj) and ($Save{mhdl_min} > $min))) { - $v_mhdl_page->respond("important=1 connected=0 app=control I am version $maj.$min (revision $revision) and version $Save{mhdl_maj}.$Save{mhdl_min} was released " . &calc_age($Save{mhdl_date} . '.')); + $v_mhdl_page->respond("important=1 connected=0 app=control I am version $version_str and version $Save{mhdl_maj}.$Save{mhdl_min} was released " . &calc_age($Save{mhdl_date} . '.')); } else { # Voice command is only code to start this process, so check its set_by - $v_mhdl_page->respond("connected=0 app=control Version $Save{mhdl_maj}.$Save{mhdl_min} is current."); + $v_mhdl_page->respond("connected=0 app=control Version $version_str is current."); } } From 5251f28235485a7d35e10b4c24ea1dc74b4af380 Mon Sep 17 00:00:00 2001 From: Ryan Davies Date: Thu, 18 Jul 2013 01:11:40 +1200 Subject: [PATCH 085/330] Added Basic Base Google TTS Support --- bin/mh.ini | 6 +++--- lib/Voice_Text.pm | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/bin/mh.ini b/bin/mh.ini index aa65a3df8..2e999aaf0 100644 --- a/bin/mh.ini +++ b/bin/mh.ini @@ -1085,9 +1085,9 @@ ipaddress_xpl_broadcast = @ See the mh/docs/install.html on how to install various speech engines. -@ voice_text can be MS, MSV4, MSV5, festival, flite, theta or swift (for the Cepstral engine on Linux), -@ viavoice, vv_tts (for IBM's viavoice Outloud), NaturalVoice, NaturalVoiceWine, or -@ program xyz (for a stand alone xyz program), +@ voice_text can be MS, MSV4, MSV5, festival, flite, google (for the Google TTS API, requires ffmpeg), +@ theta or swift (for the Cepstral engine on Linux), viavoice, vv_tts (for IBM's viavoice Outloud), +@ NaturalVoice, NaturalVoiceWine, or program xyz (for a stand alone xyz program), @ Set to blank to disable. @ Use voice_text = MS on windows for all Microsoft SAPI compatible TTS engines, including AT&T NaturalVoice. @ Other parms specify paths to various TTS engines on unix systems. diff --git a/lib/Voice_Text.pm b/lib/Voice_Text.pm index 3c91ab04b..bf515a4b1 100644 --- a/lib/Voice_Text.pm +++ b/lib/Voice_Text.pm @@ -4,6 +4,8 @@ package Voice_Text; # $Revision$ use strict; +use LWP::UserAgent; +use LWP::ConnCache; use vars '$VTxt_version'; my (@VTxt, $VTxt_stream1, $VTxt_stream2, %VTxt_cards, $VTxt_festival, $VTxt_mac); @@ -429,6 +431,42 @@ sub speak_text { # exit; # nothing left for the child to do } } + }elsif ($speak_engine =~ /google/) { + # Speak to tile using the Google TTS Engine + + # Define a basic LWP agent for retrieving the Google MP3 + my $ua; + $ua = LWP::UserAgent->new; + $ua->agent("Mozilla/5.0 (X11; Linux; rv:8.0) Gecko/20100101"); + $ua->env_proxy; + $ua->conn_cache(LWP::ConnCache->new()); + $ua->timeout(10); + + my $random = int rand 1000; # Use random file name so we can talk 2+ at once. + + # If being forced to file, use the filename being forced, otherwise, use a random temp file. + my $out_file = ($parms{to_file}) ? $parms{to_file} : "$main::config_parms{data_dir}/mh_temp.google-$random.wav"; + + # The temp file to store google's MP3 as + my $google_file = "$main::config_parms{data_dir}/mh_temp.google-$random.mp3"; + + # Make the request, store the result in the google temp file + my $ua_request = HTTP::Request->new('GET' => "http://translate.google.com/translate_tts?tl=en&q=".qq[ $parms{text} ]); + my $ua_response = $ua->request($ua_request, $google_file); + + # Log the failure + if (!$ua_response->is_success) { + print "Failed to contact the Google TTS API.\n"; + return; + } + + # Convert the returned mp3 file to a wav, and clean up the temp file + system("ffmpeg", "-loglevel", "panic", "-i", "$google_file", "$out_file"); + unlink($google_file); + + # Play the wav file, clean up only if we are not being forced to file + system($main::config_parms{sound_program}, $out_file) unless $parms{to_file}; + unlink($out_file) unless $parms{to_file}; } elsif ($speak_pgm) { my $fork = 1 unless $parms{to_file} and !$parms{async}; # Must wait for to_file requests, so http requests work From 46579ca6ab271868719477ae5f7015147282fa9a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 19 Jul 2013 17:11:14 -0700 Subject: [PATCH 086/330] Insteon: Reorganize Structure for Generating Voice Commands Generate_voice_commands in Insteon.pm now calls get_voice_cmds for each object. get_voice_cmds is written to be inherited and modified by higher level objects This allows for unique voice commands to be added to objects in their respective class files without messing with the base Insteon.pm file. This commit merely copied over the voice commands as they existed before with little to no changes in the voice command design. Fixes hollie/misterhouse#181 --- lib/Insteon.pm | 96 +++++++++--------------------------- lib/Insteon/BaseInsteon.pm | 88 +++++++++++++++++++++++++++++++++ lib/Insteon/BaseInterface.pm | 35 +++++++++++++ lib/Insteon/Lighting.pm | 55 +++++++++++++++++++++ 4 files changed, 201 insertions(+), 73 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 799f26c3f..5147cc965 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -554,87 +554,37 @@ sub generate_voice_commands for my $object (&main::list_all_objects) { next unless ref $object; next unless $object->isa('Insteon::BaseInterface') or $object->isa('Insteon::BaseObject'); + + #get object name to use as part of variable in voice command my $object_name = $object->get_object_name; - # ignore the thermostat - next if $object->isa('Insteon_Thermostat'); + my $object_name_v = $object_name . '_v'; + $object_string .= "use vars '${object_name}_v';\n"; + + #Convert object name into readable voice command words my $command = $object_name; $command =~ s/^\$//; $command =~ tr/_/ /; - my $object_name_v = $object_name . '_v'; - $object_string .= "use vars '${object_name}_v';\n"; - my $states = 'on,off'; + my $group = ($object->isa('Insteon_PLM')) ? '' : $object->group; - if ($object->isa('Insteon::BaseController')) { - $states = 'on,off,sync links'; #,resume,enroll,unenroll,manual'; - my $cmd_states = $states; - if ($object->isa('Insteon::InterfaceController')) { - $cmd_states .= ',initiate linking as controller,cancel linking'; - } else { - $cmd_states .= ",link to interface,unlink with interface"; - } - if ($object->is_root and !($object->isa('Insteon::InterfaceController'))) { - $cmd_states .= ",status,get engine version,scan link table,log links"; - push @_scannable_link, $object_name; - } - $object_string .= "$object_name_v = new Voice_Cmd '$command [$cmd_states]';\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->initiate_linking_as_controller(\"$group\")', 'initiate linking as controller');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->interface()->cancel_linking','cancel linking');\n\n"; - if ($object->is_root and !($object->isa('Insteon::InterfaceController'))) { - $object_string .= "$object_name_v -> tie_event('$object_name->link_to_interface','link to interface');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->unlink_to_interface','unlink with interface');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->request_status','status');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->get_engine_version','get engine version');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")','scan link table');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->log_alllink_table()','log links');\n\n"; - } - $object_string .= "$object_name_v -> tie_event('$object_name->sync_links(0)','sync links');\n\n"; - $object_string .= "$object_name_v -> tie_items($object_name, 'on');\n\n"; - $object_string .= "$object_name_v -> tie_items($object_name, 'off');\n\n"; - $object_string .= &main::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_link_commands'); - push @_insteon_link, $object_name; - } elsif ($object->isa('Insteon::BaseDevice')) { - my $cmd_states = "$states,status,get engine version,scan link table,log links,update onlevel/ramprate"; #,on level,ramp rate"; - $cmd_states .= ",link to interface,unlink with interface" if $object->isa("Insteon::BaseController") || $object->is_controller; - $object_string .= "$object_name_v = new Voice_Cmd '$command [$cmd_states]';\n"; - foreach my $state (split(/,/,$states)) { - $object_string .= "$object_name_v -> tie_items($object_name, '$state');\n\n"; - } - $object_string .= "$object_name_v -> tie_event('$object_name->log_alllink_table()','log links');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->request_status','status');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->get_engine_version','get engine version');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->update_local_properties','update onlevel/ramprate');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")','scan link table');\n\n"; - if ($object->isa("Insteon::BaseController") || $object->is_controller) { - $object_string .= "$object_name_v -> tie_event('$object_name->link_to_interface','link to interface');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->unlink_to_interface','unlink with interface');\n\n"; - } -# the remote_set_button_taps provide incorrect/inconsistent results -# $object_string .= "$object_name_v -> tie_event('$object_name->remote_set_button_tap(1)','on level');\n\n"; -# $object_string .= "$object_name_v -> tie_event('$object_name->remote_set_button_tap(2)','ramp rate');\n\n"; - $object_string .= &main::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_item_commands'); - push @_insteon_device, $object_name if $group eq '01'; # don't allow non-base items to participate - } elsif ($object->isa('Insteon_PLM')) { - my $cmd_states = "complete linking as responder,initiate linking as controller,cancel linking,delete link with PLM,scan link table,log links,delete orphan links,AUDIT - delete orphan links,scan all device link tables,scan changed device link tables,sync all links,AUDIT - sync all links"; - $cmd_states .= ",log all device ALDB status"; - $object_string .= "$object_name_v = new Voice_Cmd '$command [$cmd_states]';\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->complete_linking_as_responder','complete linking as responder');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->initiate_linking_as_controller','initiate linking as controller');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->initiate_unlinking_as_controller','initiate unlinking');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->cancel_linking','cancel linking');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->log_alllink_table','log links');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")','scan link table');\n\n"; - $object_string .= "$object_name_v -> tie_event('&Insteon::scan_all_linktables(1)','scan changed device link tables');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->delete_orphan_links','delete orphan links');\n\n"; - $object_string .= "$object_name_v -> tie_event('$object_name->delete_orphan_links(1)','AUDIT - delete orphan links');\n\n"; - $object_string .= "$object_name_v -> tie_event('&Insteon::scan_all_linktables','scan all device link tables');\n\n"; - $object_string .= "$object_name_v -> tie_event('&Insteon::sync_all_links(0)','sync all links');\n\n"; - $object_string .= "$object_name_v -> tie_event('&Insteon::sync_all_links(1)','AUDIT - sync all links');\n\n"; - $object_string .= "$object_name_v -> tie_event('&Insteon::log_all_ADLB_status','log all device ALDB status');\n\n"; - $object_string .= &main::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_PLM_commands'); - push @_insteon_plm, $object_name; + + #Get list of all voice commands from the object + my $voice_cmds = $object->get_voice_cmds(); + + #Initialize the voice command with all of the possible device commands + $object_string .= "$object_name_v = new Voice_Cmd '$command [" + . join(",", sort keys %$voice_cmds) . "]';\n"; + + #Tie the proper routine to each voice command + foreach (keys %$voice_cmds) { + $object_string .= "$object_name_v -> tie_event('" . $voice_cmds->{$_} + . "', '$_');\n\n"; } + + #Add this object to the list of Insteon Voice Commands on the Web Interface + $object_string .= ::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_PLM_commands'); } + #Evaluate the resulting object generating string package main; eval $object_string; print "Error in insteon_item_commands: $@\n" if $@; diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4aaad4330..e8714c40e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1012,6 +1012,34 @@ sub failure_reason return $$self{failure_reason}; } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my %voice_cmds = ( + #The Sync Links routine really resides in DeviceController, but that + #class seems a little redundant as in practice all devices are controllers + #in some sense. As a result, that class will likely be folded into + #BaseObject/Device at some future date. In order to avoid a bizarre + #inheritance of this routine by higher classes, this command was placed + #here + 'sync links' => $self->get_object_name . '->sync_links(0)' + ); + return \%voice_cmds; +} + =back =head2 INI PARAMETERS @@ -2032,6 +2060,40 @@ sub check_aldb_version } } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'link to interface' => "$object_name->link_to_interface", + 'unlink with interface' => "$object_name->unlink_to_interface" + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'status' => "$object_name->request_status", + 'get engine version' => "$object_name->get_engine_version", + 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", + 'log links' => "$object_name->log_alllink_table()" + ) + } + return \%voice_cmds; +} + =back =head2 INI PARAMETERS @@ -2953,6 +3015,32 @@ sub is_root return 0; } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my $group = $self->group; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'initiate linking as controller' => "$object_name->initiate_linking_as_controller(\"$group\")", + 'cancel linking' => "$object_name->interface()->cancel_linking" + ); + return \%voice_cmds; +} + =back =head2 AUTHOR diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index b2493956a..c12e9bbd6 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -852,6 +852,41 @@ sub _is_duplicate_received { return $is_duplicate; } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + 'complete linking as responder' => "$object_name->complete_linking_as_responder", + 'initiate linking as controller' => "$object_name->initiate_linking_as_controller", + 'initiate unlinking' => "$object_name->initiate_unlinking_as_controller", + 'cancel linking' => "$object_name->cancel_linking", + 'log links' => "$object_name->log_alllink_table", + 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", + 'scan changed device link tables' => "Insteon::scan_all_linktables(1)", + 'delete orphan links' => "$object_name->delete_orphan_links", + 'AUDIT - delete orphan links' => "$object_name->delete_orphan_links(1)", + 'scan all device link tables' => "Insteon::scan_all_linktables", + 'sync all links' => "Insteon::sync_all_links(0)", + 'AUDIT - sync all links' => "Insteon::sync_all_links(1)", + 'log all device ALDB status' => "Insteon::log_all_ADLB_status" + ); + return \%voice_cmds; +} + =back =head2 INI PARAMETERS diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 92d69cf18..7ce94da5b 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -60,6 +60,31 @@ sub level } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'on' => "$object_name->set(\"on\")", + 'off' => "$object_name->set(\"off\")" + ); + return \%voice_cmds; +} + =back =head2 AUTHOR @@ -254,6 +279,36 @@ sub level } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my $insteon_menu_states = $main::config_parms{insteon_menu_states} if $main::config_parms{insteon_menu_states}; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'update onlevel/ramprate' => "$object_name->update_local_properties" + ); + if ($insteon_menu_states){ + foreach my $state (split(/,/,$insteon_menu_states)) { + $voice_cmds{$state} = "$object_name->set(\"$state\")"; + } + } + return \%voice_cmds; +} + =back =head2 AUTHOR From 34ac765af43d1c51487e0d11ad8cfc44a46bfe2f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 20 Jul 2013 12:04:01 -0700 Subject: [PATCH 087/330] Insteon: Add Voice Commands to IOLinc --- lib/Insteon/IOLinc.pm | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index d5186fee7..084a6c5ba 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -455,6 +455,47 @@ sub set_relay_mode return; } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + #Rename status command to note that it will request status of the + #relay + 'on' => "$object_name->set(\"on\")", + 'off' => "$object_name->set(\"off\")", + 'status - relay' => "$object_name->request_status", + 'status - sensor' => "$object_name->request_sensor_status", + 'print momentary time' => "$object_name->get_momentary_time", + 'link relay to sensor' => "$object_name->set_relay_linked(1)", + 'unlink relay from sensor' => "$object_name->set_relay_linked(0)", + 'reverse sensor output' => "$object_name->set_trigger_reverse(1)", + 'unreverse sensor output' => "$object_name->set_trigger_reverse(0)", + 'set relay to latching' => "$object_name->set_relay_mode(\"Latching\")", + 'set relay to momentary a' => "$object_name->set_relay_mode(\"Momentary_A\")", + 'set relay to momentary b' => "$object_name->set_relay_mode(\"Momentary_B\")", + 'set relay to momentary c' => "$object_name->set_relay_mode(\"Momentary_C\")", + 'print settings to log' => "$object_name->get_operating_flag" + ); + #Remove generic status command + delete $voice_cmds{status}; + return \%voice_cmds; +} + =back =head2 AUTHOR From 5e57ac9f6a2d7df6d9314555d2c3a3162cec9c04 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 20 Jul 2013 12:44:43 -0700 Subject: [PATCH 088/330] Insteon: Add KeyPadLinc Unique Voice Commands Moved update_flags routine to KeyPadLinc classes Added unique voice commands --- lib/Insteon/AllLinkDatabase.pm | 2 +- lib/Insteon/BaseInsteon.pm | 30 --------- lib/Insteon/Lighting.pm | 112 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 31 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 5c7918641..eed61071a 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1935,7 +1935,7 @@ sub update_local_properties =item C -Used to update the flags of a device. Called by L. +Used to update the flags of a device. Called by L. =cut diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index e8714c40e..4b0b7918a 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1953,36 +1953,6 @@ sub update_local_properties } } -=item C - -Can be used to set the button layout and light level on a keypadlinc. Flag -options include: - - '0a' - 8 button; backlighting dim - '06' - 8 button; backlighting off - '02' - 8 button; backlighting normal - - '08' - 6 button; backlighting dim - '04' - 6 button; backlighting off - '00' - 6 button; backlighting normal - -Note: This routine will likely be moved to L at some point. - -=cut - -sub update_flags -{ - my ($self, $flags) = @_; - if (!($self->isa('Insteon::KeyPadLinc') or $self->isa('Insteon::KeyPadLincRelay'))) - { - &::print_log("[Insteon::BaseDevice] Operating flags may only be revised on keypadlincs!"); - return; - } - return unless defined $flags; - - $self->_aldb->update_flags($flags) if $self->_aldb; -} - =item C Sets or gets the device object engine version. If setting the engine version, diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 7ce94da5b..304631bbd 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -738,6 +738,62 @@ sub set } +=item C + +Can be used to set the button layout and light level on a keypadlinc. Flag +options include: + + '0a' - 8 button; backlighting dim + '06' - 8 button; backlighting off + '02' - 8 button; backlighting normal + + '08' - 6 button; backlighting dim + '04' - 6 button; backlighting off + '00' - 6 button; backlighting normal + +=cut + +sub update_flags +{ + my ($self, $flags) = @_; + return unless defined $flags; + $self->_aldb->update_flags($flags) if $self->_aldb; +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'set 8 button - backlight dim' => "$object_name->update_flags(\"0a\")", + 'set 8 button - backlight off' => "$object_name->update_flags(\"06\")", + 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", + 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", + 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", + 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")" + ); + } + return \%voice_cmds; +} + =back =head2 AUTHOR @@ -853,6 +909,62 @@ sub set } +=item C + +Can be used to set the button layout and light level on a keypadlinc. Flag +options include: + + '0a' - 8 button; backlighting dim + '06' - 8 button; backlighting off + '02' - 8 button; backlighting normal + + '08' - 6 button; backlighting dim + '04' - 6 button; backlighting off + '00' - 6 button; backlighting normal + +=cut + +sub update_flags +{ + my ($self, $flags) = @_; + return unless defined $flags; + $self->_aldb->update_flags($flags) if $self->_aldb; +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'set 8 button - backlight dim' => "$object_name->update_flags(\"0a\")", + 'set 8 button - backlight off' => "$object_name->update_flags(\"06\")", + 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", + 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", + 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", + 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")" + ); + } + return \%voice_cmds; +} + =back =head2 AUTHOR From ecbcaddcc61aac415282691ef6214c8aac0fa440 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 20 Jul 2013 13:01:35 -0700 Subject: [PATCH 089/330] Insteon: Add Unique Voice Commands to Motion Sensor --- lib/Insteon/Security.pm | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index 27f7d04f0..0af6cf4b9 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -494,6 +494,36 @@ sub is_responder return 0; } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'enable night only' => "$object_name->enable_night_only(1)", + 'disable night only' => "$object_name->enable_night_only(0)", + 'enable on only mode' => "$object_name->enable_on_only(1)", + 'disable on only mode' => "$object_name->enable_on_only(0)", + 'enable all motion mode' => "$object_name->enable_all_motion(1)", + 'disable all motion mode' => "$object_name->enable_all_motion(0)" + ); + return \%voice_cmds; +} + + =back =head2 AUTHOR From c378df3074698ad91e4272915a75ceadf09195c3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 22 Jul 2013 21:04:10 -0700 Subject: [PATCH 090/330] Insteon: Add Message Tracking Statistics Features Added a number of functions to track message details. Added functions to print message statistics to the log Added functions to reset the message statistics Added 2 voice commands: - print message statistics - prints details on all devices to the log - reset message statistics - resets the message statistics to 0 for all devices --- lib/Insteon.pm | 52 ++++++++++++ lib/Insteon/AllLinkDatabase.pm | 4 +- lib/Insteon/BaseInsteon.pm | 149 ++++++++++++++++++++++++++++++++- lib/Insteon/BaseInterface.pm | 3 + lib/Insteon/Message.pm | 4 + 5 files changed, 209 insertions(+), 3 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 799f26c3f..2d0d5f5a5 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -488,6 +488,58 @@ sub log_all_ADLB_status } } +=item C + +Walks through every Insteon device and prints statistical information about +its message handling. + +=cut + +sub print_message_logs +{ + my @_log_devices = (); + push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); + + if (@_log_devices) + { + foreach my $current_log_device (@_log_devices) + { + $current_log_device->print_message_log + if $current_log_device->can('print_message_log'); + } + main::print_log("[Insteon::Print_Message_Logs] All devices have completed logging"); + } else + { + main::print_log("[Insteon::Print_Message_Logs] WARN: No insteon devices could be found"); + } +} + +=item C + +Walks through every Insteon device and resets the statistical information about +its message handling. + +=cut + +sub reset_message_logs +{ + my @_log_devices = (); + push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); + + if (@_log_devices) + { + foreach my $current_log_device (@_log_devices) + { + $current_log_device->reset_message_log + if $current_log_device->can('reset_message_log'); + } + main::print_log("[Insteon::Reset_Message_Logs] All devices have been reset"); + } else + { + main::print_log("[Insteon::Reset_Message_Logs] WARN: No insteon devices could be found"); + } +} + =item C Initiates the insteon stack, mostly just sets the trigger. diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 5c7918641..98e65338d 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2146,7 +2146,8 @@ sub on_read_write_aldb . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; - #retry previous address again + $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); + #retry previous address again $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); } elsif ($$self{_mem_msb} . $$self{_mem_lsb} ne '0000' and $$self{_mem_msb} . $$self{_mem_lsb} ne substr($msg{extra},6,4)){ @@ -2155,6 +2156,7 @@ sub on_read_write_aldb . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #retry previous address again $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4aaad4330..c1f587e15 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -778,6 +778,7 @@ sub _process_message . $self->get_object_name . " in response to a " . $pending_cmd . " command, but the command code " . $msg{cmd_code} . " is incorrect. Ignorring received message."); + $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); $p_setby->active_message->no_hop_increase(1); } } @@ -1147,7 +1148,8 @@ sub new $$self{aldb} = new Insteon::ALDB_i1($self); } - $self->restore_data('level'); + $self->restore_data('level', 'retry_count_log', 'fail_count_log', + 'outgoing_count_log', 'incoming_count_log', 'corrupt_count_log'); $self->initialize(); $self->rate(undef); @@ -1161,6 +1163,11 @@ sub new @{$$self{command_stack}} = (); $$self{_onlevel} = undef; $$self{is_responder} = 1; + $$self{retry_count_log} = 0; + $$self{fail_count_log} = 0; + $$self{outgoing_count_log} = 0; + $$self{incoming_count_log} = 0; + $$self{corrupt_count_log} = 0; return $self; } @@ -1975,7 +1982,145 @@ sub engine_version return $engine_version; } -=item C +=item C + +Sets or gets the number of message retries that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment retry log by one. + +Returns: current retry count. + +=cut + +sub retry_count_log +{ + my ($self, $retry_count_log) = @_; + $$self{retry_count_log} = $retry_count_log if $retry_count_log; + return $$self{retry_count_log}; +} + +=item C + +Sets or gets the number of message failures that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment fail log by one. + +Returns: current fail count. + +=cut + +sub fail_count_log +{ + my ($self, $fail_count_log) = @_; + $$self{fail_count_log} = $fail_count_log if $fail_count_log; + return $$self{fail_count_log}; +} + +=item C + +Sets or gets the number of outgoing message that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment output count by one. + +Returns: current output count. + +=cut + +sub outgoing_count_log +{ + my ($self, $outgoing_count_log) = @_; + $$self{outgoing_count_log} = $outgoing_count_log if $outgoing_count_log; + return $$self{outgoing_count_log}; +} + +=item C + +Sets or gets the number of incoming message that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment incoming count by one. + +Returns: current incoming count. + +=cut + +sub incoming_count_log +{ + my ($self, $incoming_count_log) = @_; + $$self{incoming_count_log} = $incoming_count_log if $incoming_count_log; + return $$self{incoming_count_log}; +} + +=item C + +Sets or gets the number of currupt message that have arrived from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current corrupt count. + +=cut + +sub corrupt_count_log +{ + my ($self, $corrupt_count_log) = @_; + $$self{corrupt_count_log} = $corrupt_count_log if $corrupt_count_log; + return $$self{corrupt_count_log}; +} + + +=item C + +Resets the retry, fail, outgoing, incoming, and corrupt message counters. + +=cut + +sub reset_message_log +{ + my ($self) = @_; + $$self{retry_count_log} = 0; + $$self{fail_count_log} = 0; + $$self{outgoing_count_log} = 0; + $$self{incoming_count_log} = 0; + $$self{corrupt_count_log} = 0; +} + +=item C + +Prints message statistics for this device to the print log. + +=cut + +sub print_message_log +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my $retry_percentage = 0; + $retry_percentage = sprintf("%.2f", ($$self{retry_count_log} / + $$self{outgoing_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); + my $fail_percentage = 0; + $fail_percentage = sprintf("%.2f", ($$self{fail_count_log} / + $$self{outgoing_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); + my $corrupt_percentage = 0; + $corrupt_percentage = sprintf("%.2f", ($$self{corrupt_count_log} / + $$self{incoming_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); + ::print_log("[Insteon::BaseDevice] Message statistics for $object_name:\n" + . "Outgoing Count = " . $$self{outgoing_count_log} ."\n" + . " Retry Count = " . $$self{retry_count_log} ."\n" + . " Retry % = " . $retry_percentage . "%\n" + . " Fail Count = " . $$self{fail_count_log} ."\n" + . " Fail % = " . $fail_percentage . "%\n" + . "Incoming Count = " . $$self{incoming_count_log} ."\n" + . " Corrupt Count = " . $$self{corrupt_count_log} ."\n" + . " Corrupt % = " . $corrupt_percentage. "%\n"); +} + + +=item C Because of the way MH saves / restores states "after" object creation the aldb must be initially created before the engine_version is restored. diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index b2493956a..c779c6656 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -359,6 +359,7 @@ sub process_queue . $self->active_message->send_attempts . ") for " . $self->active_message->to_string() . " exceeds limit. Now moving on...") if $main::Debug{insteon}; + $self->fail_count_log(1) if $self->can('fail_count_log'); # !!!!!!!!! TO-DO - handle failure timeout ??? my $failed_message = $self->active_message; # make sure to let the sending object know!!! @@ -530,6 +531,7 @@ sub on_standard_insteon_received my $object = &Insteon::get_object($msg{source}, $msg{group}); if (defined $object) { + $object->incoming_count_log(1) if $object->can('incoming_count_log'); if ($msg{type} ne 'broadcast') { $msg{command} = $object->message_type($msg{cmd_code}); @@ -690,6 +692,7 @@ sub on_extended_insteon_received my $object = &Insteon::get_object($msg{source}, $msg{group}); if (defined $object) { + $object->incoming_count_log(1) if $object->can('incoming_count_log'); if ($msg{type} ne 'broadcast') { $msg{command} = $object->message_type($msg{cmd_code}); diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index a9c8966b2..12cea5827 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -218,6 +218,7 @@ sub send &::print_log("[Insteon::BaseMessage] WARN: now resending " . $self->to_string() . " after " . $self->send_attempts . " attempts.") if $main::Debug{insteon}; + $self->setby->retry_count_log(1) if $self->setby->can('retry_count_log'); # revise default hop count to reflect retries if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') && !defined($$self{no_hop_increase})) @@ -237,6 +238,9 @@ sub send } # need to set timeout as a function of retries; also need to alter hop count + if ($self->setby->can('outgoing_count_log') && $self->send_attempts <= 0) { + $self->setby->outgoing_count_log(1); + } $self->send_attempts($self->send_attempts + 1); $interface->_send_cmd($self, $self->send_timeout); if ($self->callback) From 0d368c3398f593fd65d0404059fedfe12005f551 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 22 Jul 2013 21:25:37 -0700 Subject: [PATCH 091/330] Insteon: Fix Path Comparison in Windows for Update_Docs.pl If inode==0 then we are probably on windows, so use abs_path to compare the two paths and see if they are the same. I don't have windows so hopefully this works. Fixes hollie/misterhouse#234 --- bin/update_docs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/update_docs b/bin/update_docs index 2ca9526ce..802edd75c 100755 --- a/bin/update_docs +++ b/bin/update_docs @@ -60,6 +60,7 @@ BEGIN { use strict; use warnings; use Pod::Html; +use Cwd qw(abs_path); # Get parms from mh.ini require 'handy_utilities.pl'; @@ -242,7 +243,8 @@ closedir POD; # delete html files from docs dir if out dir is diff and pod exists my $docsi = ( stat($docdir) )[1]; my $outi = ( stat($outdir) )[1]; -if ( $docsi eq $outi ) { +if ((($docsi == $outi) && ($outi == 0) && (abs_path($docdir) eq abs_path($outdir))) + || (( $docsi eq $outi ) && ($outi != 0))) { print "you should set the html_alias2_docs directory to a place outside" . " the mh distribution\n directory. Otherwise, everytime this script is" . " run, you will alter\n your distribution files."; From 192ac8f1e66e2c228763be79ad17cd3716b85b8f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 23 Jul 2013 15:41:42 -0700 Subject: [PATCH 092/330] Fix additional typos form manual merge --- lib/Insteon/BaseInsteon.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index da4c1eb9b..dd8473a50 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2985,5 +2985,3 @@ You should have received a copy of the GNU General Public License along with thi =cut 1; -=back -=cut From 09c5456d6f5705c9cc7f18f539f8c83f484e05cb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 23 Jul 2013 15:44:52 -0700 Subject: [PATCH 093/330] Fix additional typos from manual merge --- lib/Insteon/BaseInsteon.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index fbcd9fb6f..da30b099c 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2998,5 +2998,3 @@ You should have received a copy of the GNU General Public License along with thi =cut 1; -=back -=cut From 6bbdcf35edff51bd59c3b48b5119d1054c45c5bb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 23 Jul 2013 16:35:40 -0700 Subject: [PATCH 094/330] Insteon: Make Sure Timeout Factor Set to 1 on Reboot --- lib/Insteon/BaseInsteon.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index dd8473a50..94deed443 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -112,6 +112,7 @@ sub new $$self{_onlevel} = undef; $$self{is_responder} = 1; $$self{default_hop_count} = 0; + $$self{timeout_factor} = 1.0; &Insteon::add($self); return $self; @@ -203,6 +204,9 @@ resending the message. The value set will be multiplied by the predefined value in MH. $float can be set to any positive decimal number. For example using 1.0 will not change the preset values; 1.1 will increase the time MH waits by 10%; and 0.9 will force MH to wait for only 90% of the predefined time. + +This value is NOT saved on reboot, as such likely should be called in a $Reload loop. + =cut sub timeout_factor { From 4835bd9ef572a8abcf04c9a50a9935d6a6ed9053 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 23 Jul 2013 16:37:39 -0700 Subject: [PATCH 095/330] Insteon: Max/Min Hops Setting Not Saved on Reboot --- lib/Insteon/BaseInsteon.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index da30b099c..4b998e4f7 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -101,6 +101,8 @@ sub new $self->restore_data('default_hop_count', 'engine_version'); $self->initialize(); + $$self{max_hops} = 3; + $$self{min_hops} = 0; $$self{level} = undef; $$self{flag} = "0F"; $$self{ackMode} = "1"; @@ -200,6 +202,9 @@ sub group Sets the maximum number of hops that may be used in a message sent to the device. The default and maximum number is 3. $int is an integer between 0-3. + +This value is NOT saved on reboot, as such likely should be called in a $Reload loop. + =cut sub max_hops { @@ -212,6 +217,9 @@ sub max_hops { Sets the minimum number of hops that may be used in a message sent to the device. The default and minimum number is 0. $int is an integer between 0-3. + +This value is NOT saved on reboot, as such likely should be called in a $Reload loop. + =cut sub min_hops { From 4ff51a17545ccb5fa1f94dabbde13b32a3d7a400 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 18:41:37 -0700 Subject: [PATCH 096/330] Better Formatting in Web Interface Print Log Changed the web interface print log to a monospaced or fix width font and formatting. This allows the nice formatting that relies on consistent spacing that has been visible in the text based logs, to be viewable online. I realize that fixed width fonts are not always the prettiest, but I did the best I could. If anyone has any suggestions for how to make it prettier, I am open to additional changes. I had to work around one oddity. The web code converts all \n characters to \n\r when formatting the html. This is a blind conversion without any regard as to whether the \n character is within a

 tag.
---
 lib/http_server.pl | 8 ++++++--
 web/list.css       | 1 +
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/lib/http_server.pl b/lib/http_server.pl
index 4e3ff7e69..ace04ad86 100644
--- a/lib/http_server.pl
+++ b/lib/http_server.pl
@@ -1247,8 +1247,12 @@ sub html_print_log {
         $h_response .= "Refresh Print Log\n";
         my @last_printed = &main::print_log_last($main::config_parms{max_log_entries});
         for my $text (@last_printed) {
-            $text =~ s/\n/\n
/g; - $h_response .= "
  • $text\n"; + #This formatting is a little bizarre, but sub html_page blindly + #converts all \n to \n\r apparently to be standards compliant. It + #would be easier to set the white space of list-item to pre, however + #the additional newline characters added by html_page look ugly. + $text =~ s/\n/<\/pre><\/br>\n
    /g;
    +            $h_response .= "
  • $text
  • \n"; } } else { diff --git a/web/list.css b/web/list.css index 5ec3a2f7e..82a49ddca 100644 --- a/web/list.css +++ b/web/list.css @@ -7,3 +7,4 @@ A:link { color: blue; } A:visited { color: lightblue; } A:active { color: brown; } A:hover { color: green; } +PRE { white-space:pre-wrap; font-family:courier, monospace; display:inline; font-size:10pt;} From e18e5ad50b34b52d4102b9add7fc94a4d9007f24 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 18:59:37 -0700 Subject: [PATCH 097/330] Insteon_Stats: Fix Print Message Voice Command Documentation --- lib/Insteon.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 2d0d5f5a5..f88d3e578 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -491,7 +491,9 @@ sub log_all_ADLB_status =item C Walks through every Insteon device and prints statistical information about -its message handling. +its message handling, as well as a summary average of the entire network. See +L +for more detailed information. =cut From d28a660a17c943ff9d0b69238fa21d830aa0b46e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:00:35 -0700 Subject: [PATCH 098/330] Insteon_Stats: Add a More Extensive Print Message Voice Command Now collects and prints a total average for the entire network. --- lib/Insteon.pm | 76 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index f88d3e578..67fcc48d2 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -504,11 +504,83 @@ sub print_message_logs if (@_log_devices) { + #Initialize all of the tracking variables + my $retry_average = 0; + my $fail_percentage = 0; + my $corrupt_percentage = 0; + my $dupe_percentage = 0; + my $avg_hops_left = 0; + my $avg_max_hops = 0; + my $avg_out_hops = 0; + my $curr_hops_avg = 0; + + my $incoming_count_log = 0; + my $corrupt_count_log = 0; + my $dupe_count_log = 0; + my $retry_count_log = 0; + my $outgoing_count_log = 0; + my $fail_count_log = 0; + my $default_hop_count = 0; + my $hops_left_count = 0; + my $max_hops_count = 0; + my $outgoing_hop_count =0; + foreach my $current_log_device (@_log_devices) { - $current_log_device->print_message_log - if $current_log_device->can('print_message_log'); + #Prints the Individual Message for the Device + $current_log_device->print_message_log; + + #Add values for each device to the master count + $incoming_count_log += $current_log_device->incoming_count_log; + $corrupt_count_log += $current_log_device->corrupt_count_log; + $dupe_count_log += $current_log_device->dupe_count_log; + $retry_count_log += $current_log_device->retry_count_log; + $outgoing_count_log += $current_log_device->outgoing_count_log; + $fail_count_log += $current_log_device->fail_count_log; + $default_hop_count += $current_log_device->default_hop_count; + $hops_left_count += $current_log_device->hops_left_count; + $max_hops_count += $current_log_device->max_hops_count; + $outgoing_hop_count += $current_log_device->outgoing_hop_count; } + + #Calculate the averages + $retry_average = sprintf("%.1f", ($retry_count_log / + $outgoing_count_log) + 1) if ($outgoing_count_log > 0); + $fail_percentage = sprintf("%.1f", ($fail_count_log / + $outgoing_count_log) * 100 ) if ($outgoing_count_log > 0); + $corrupt_percentage = sprintf("%.1f", ($corrupt_count_log / + $incoming_count_log) * 100 ) if ($incoming_count_log > 0); + $dupe_percentage = sprintf("%.1f", ($dupe_count_log / + $incoming_count_log) * 100 ) if ($incoming_count_log > 0); + $avg_hops_left = sprintf("%.1f", ($hops_left_count / + $incoming_count_log)) if ($incoming_count_log > 0); + $avg_max_hops = sprintf("%.1f", ($max_hops_count / + $incoming_count_log)) if ($incoming_count_log > 0); + $avg_out_hops = sprintf("%.1f", ($outgoing_hop_count / + $outgoing_count_log)) if ($outgoing_count_log > 0); + $curr_hops_avg = sprintf("%.1f", ($default_hop_count / + scalar @_log_devices)) if (scalar @_log_devices > 0); + ::print_log( + "[Insteon] Average Network Statistics:\n" + . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops\n" + . sprintf("%6s", $incoming_count_log) + . sprintf("%8s", $corrupt_count_log) + . sprintf("%8s", $corrupt_percentage . '%') + . sprintf("%6s", $dupe_count_log) + . sprintf("%8s", $dupe_percentage . '%') + . sprintf("%9s", $avg_hops_left) + . sprintf("%9s", $avg_max_hops) + . sprintf("%9s", $avg_max_hops - $avg_hops_left) + . "\n" + . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" + . sprintf("%6s", $outgoing_count_log) + . sprintf("%8s", $fail_count_log) + . sprintf("%8s", $fail_percentage . '%') + . sprintf("%6s", $retry_count_log) + . sprintf("%8s", $retry_average) + . sprintf("%9s", $avg_out_hops) + . sprintf("%9s", $curr_hops_avg) + ); main::print_log("[Insteon::Print_Message_Logs] All devices have completed logging"); } else { From 9e5f68941c45b21e12f4f56e53a976dfc98e95fa Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:01:55 -0700 Subject: [PATCH 099/330] Insteon_Stats: Add new Saved Message Stats Variables --- lib/Insteon/BaseInsteon.pm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c1f587e15..2b9eb45b1 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1149,7 +1149,9 @@ sub new } $self->restore_data('level', 'retry_count_log', 'fail_count_log', - 'outgoing_count_log', 'incoming_count_log', 'corrupt_count_log'); + 'outgoing_count_log', 'incoming_count_log', 'corrupt_count_log', + 'dupe_count_log', 'hops_left_count', 'max_hops_count', + 'outgoing_hop_count'); $self->initialize(); $self->rate(undef); @@ -1168,6 +1170,10 @@ sub new $$self{outgoing_count_log} = 0; $$self{incoming_count_log} = 0; $$self{corrupt_count_log} = 0; + $$self{dupe_count_log} = 0; + $$self{hops_left_count} = 0; + $$self{max_hops_count} = 0; + $$self{outgoing_hop_count} = 0; return $self; } From e60e3097d2d7ba7c8024bc2b4a17453489222ed0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:02:57 -0700 Subject: [PATCH 100/330] Insteon_Stats: Fix Bug Message Stats Should Increment --- lib/Insteon/BaseInsteon.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 2b9eb45b1..11ca3e36f 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2002,7 +2002,7 @@ Returns: current retry count. sub retry_count_log { my ($self, $retry_count_log) = @_; - $$self{retry_count_log} = $retry_count_log if $retry_count_log; + $$self{retry_count_log}++ if $retry_count_log; return $$self{retry_count_log}; } @@ -2020,7 +2020,7 @@ Returns: current fail count. sub fail_count_log { my ($self, $fail_count_log) = @_; - $$self{fail_count_log} = $fail_count_log if $fail_count_log; + $$self{fail_count_log}++ if $fail_count_log; return $$self{fail_count_log}; } @@ -2056,7 +2056,7 @@ Returns: current incoming count. sub incoming_count_log { my ($self, $incoming_count_log) = @_; - $$self{incoming_count_log} = $incoming_count_log if $incoming_count_log; + $$self{incoming_count_log}++ if $incoming_count_log; return $$self{incoming_count_log}; } From 47c7e8f9c1edd7b9583547ee70b4ec7001da28a0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:03:56 -0700 Subject: [PATCH 101/330] Insteon_Stats: Add Routine to Track Outgoing Hop Counts --- lib/Insteon/BaseInsteon.pm | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 11ca3e36f..0cffd0226 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2038,10 +2038,28 @@ Returns: current output count. sub outgoing_count_log { my ($self, $outgoing_count_log) = @_; - $$self{outgoing_count_log} = $outgoing_count_log if $outgoing_count_log; + $$self{outgoing_count_log}++ if $outgoing_count_log; return $$self{outgoing_count_log}; } +=item C + +Sets or gets the number of hops that have been used in all outgoing messages +since the last time C was called. + +If type is set, to any value, will increment output count by that value. + +Returns: current hop count. + +=cut + +sub outgoing_hop_count +{ + my ($self, $outgoing_hop_count) = @_; + $$self{outgoing_hop_count} += $outgoing_hop_count if $outgoing_hop_count; + return $$self{outgoing_hop_count}; +} + =item C Sets or gets the number of incoming message that have occured for this device From a42eca82629f0aa33d369e1b86b6c98d50222b92 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:04:55 -0700 Subject: [PATCH 102/330] Insteon_Stats: Add Routines to Track Duplicate Messages, Max Hops, and Hops Left --- lib/Insteon/BaseInsteon.pm | 56 +++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 0cffd0226..4c435ce75 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2092,10 +2092,64 @@ Returns: current corrupt count. sub corrupt_count_log { my ($self, $corrupt_count_log) = @_; - $$self{corrupt_count_log} = $corrupt_count_log if $corrupt_count_log; + $$self{corrupt_count_log}++ if $corrupt_count_log; return $$self{corrupt_count_log}; } +=item C + +Sets or gets the number of duplicate message that have arrived from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current duplicate count. + +=cut + +sub dupe_count_log +{ + my ($self, $dupe_count_log) = @_; + $$self{dupe_count_log}++ if $dupe_count_log; + return $$self{dupe_count_log}; +} + +=item C + +Sets or gets the number of hops_left for messages that arrive from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current hops_left count. + +=cut + +sub hops_left_count +{ + my ($self, $hops_left_count) = @_; + $$self{hops_left_count} += $hops_left_count if $hops_left_count; + return $$self{hops_left_count}; +} + +=item C + +Sets or gets the number of max_hops for messages that arrive from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current duplicate count. + +=cut + +sub max_hops_count +{ + my ($self, $max_hops_count) = @_; + $$self{max_hops_count} += $max_hops_count if $max_hops_count; + return $$self{max_hops_count}; +} + =item C From aa6ea1a4f8177814359bf87de73c7e5d50c7a97e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:05:41 -0700 Subject: [PATCH 103/330] Insteon_Stats: Add Better Documentation of Print Message Log --- lib/Insteon/BaseInsteon.pm | 87 +++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4c435ce75..47db239cf 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2165,11 +2165,96 @@ sub reset_message_log $$self{outgoing_count_log} = 0; $$self{incoming_count_log} = 0; $$self{corrupt_count_log} = 0; + $$self{dupe_count_log} = 0; + $$self{hops_left_count} = 0; + $$self{max_hops_count} = 0; + $$self{outgoing_hop_count} = 0; } =item C -Prints message statistics for this device to the print log. +Prints message statistics for this device to the print log. The output contains: + +=back + +=over8 + +=item * + +In - The number of incoming messages received + +=item * + +Corrupt - The number of incoming corrupt messages received + +=item * + +%Corrpt - Of the incoming messages received, the percentage that were +corrupt + +=item * + +Dupe - The number of duplicate messages that have been received from this +device. + +=item * + +%Dupe - The percentage of duplilicate incoming messages received. + +=item * + +Hops_Left - The average hops left in the messages received from this device. + +=item * + +Max_Hops - The average maximum hops in the messages received from this device. + +=item * + +Act_Hops - Max_Hops - Hops_Left, this is the average number of hops that have +been required for a message sent from the device to reach MisterHouse. + +=item * + +Out - The number of unique outgoing messages, without retries, sent. + +=item * + +Fail - The number times that all retries were exhausted without a successful +delivery of a message. + +=item * + +%Fail - Of the outgoing messages sent, the percentage that failed. + +=item * + +Retry - The number of retry attempts that have been made to deliver a message. +Ideally this is 0, but Sends/Msg is a better indication of this parameter. + +=item * + +AvgSend - The average number of send attempts that must be made in order to +successfully deliver a message. Ideally this would be 1.0. + +NOTE: If the number of retries exceeds the value set in the configuration file +for Insteon_retry_count, MisterHouse will abandon sending the message. As a +result, as this number approaches Insteon_retry_count it becomes a less accurate +representation of the number of retries needed to reach a device. + +=item * + +Avg_Hops - The average number of hops that have been used by MisterHouse when +sending messages to this device. + +=item * + +Hop_Count - The current hop count being used by MH. This count is dynamically +controlled by MH and is not reset by calling C + +=back + +=over =cut From 85d0f6b9f4af054e8cbc5dfd1af6ca83a405e08f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:06:26 -0700 Subject: [PATCH 104/330] Insteon_Stats: Beautify the Print Message Log Output --- lib/Insteon/BaseInsteon.pm | 54 +++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 47db239cf..e7adee13e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2262,24 +2262,48 @@ sub print_message_log { my ($self) = @_; my $object_name = $self->get_object_name; - my $retry_percentage = 0; - $retry_percentage = sprintf("%.2f", ($$self{retry_count_log} / - $$self{outgoing_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); + my $retry_average = 0; + $retry_average = sprintf("%.1f", ($$self{retry_count_log} / + $$self{outgoing_count_log}) + 1) if ($$self{outgoing_count_log} > 0); my $fail_percentage = 0; - $fail_percentage = sprintf("%.2f", ($$self{fail_count_log} / + $fail_percentage = sprintf("%.1f", ($$self{fail_count_log} / $$self{outgoing_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); my $corrupt_percentage = 0; - $corrupt_percentage = sprintf("%.2f", ($$self{corrupt_count_log} / - $$self{incoming_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); - ::print_log("[Insteon::BaseDevice] Message statistics for $object_name:\n" - . "Outgoing Count = " . $$self{outgoing_count_log} ."\n" - . " Retry Count = " . $$self{retry_count_log} ."\n" - . " Retry % = " . $retry_percentage . "%\n" - . " Fail Count = " . $$self{fail_count_log} ."\n" - . " Fail % = " . $fail_percentage . "%\n" - . "Incoming Count = " . $$self{incoming_count_log} ."\n" - . " Corrupt Count = " . $$self{corrupt_count_log} ."\n" - . " Corrupt % = " . $corrupt_percentage. "%\n"); + $corrupt_percentage = sprintf("%.1f", ($$self{corrupt_count_log} / + $$self{incoming_count_log}) * 100 ) if ($$self{incoming_count_log} > 0); + my $dupe_percentage = 0; + $dupe_percentage = sprintf("%.1f", ($$self{dupe_count_log} / + $$self{incoming_count_log}) * 100 ) if ($$self{incoming_count_log} > 0); + my $avg_hops_left = 0; + $avg_hops_left = sprintf("%.1f", ($$self{hops_left_count} / + $$self{incoming_count_log})) if ($$self{incoming_count_log} > 0); + my $avg_max_hops = 0; + $avg_max_hops = sprintf("%.1f", ($$self{max_hops_count} / + $$self{incoming_count_log})) if ($$self{incoming_count_log} > 0); + my $avg_out_hops = 0; + $avg_out_hops = sprintf("%.1f", ($$self{outgoing_hop_count} / + $$self{outgoing_count_log})) if ($$self{outgoing_count_log} > 0); + ::print_log( + "[Insteon::BaseDevice] Message statistics for $object_name:\n" + . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops\n" + . sprintf("%6s", $$self{incoming_count_log}) + . sprintf("%8s", $$self{corrupt_count_log}) + . sprintf("%8s", $corrupt_percentage . '%') + . sprintf("%6s", $$self{dupe_count_log}) + . sprintf("%8s", $dupe_percentage . '%') + . sprintf("%9s", $avg_hops_left) + . sprintf("%9s", $avg_max_hops) + . sprintf("%9s", $avg_max_hops - $avg_hops_left) + . "\n" + . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" + . sprintf("%6s", $$self{outgoing_count_log}) + . sprintf("%8s", $$self{fail_count_log}) + . sprintf("%8s", $fail_percentage . '%') + . sprintf("%6s", $$self{retry_count_log}) + . sprintf("%8s", $retry_average) + . sprintf("%9s", $avg_out_hops) + . sprintf("%9s", $self->default_hop_count) + ); } From 9a964f85a097e90851e2f1c32813c545b21e1826 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:07:19 -0700 Subject: [PATCH 105/330] Insteon_Stats: Fix Bug in Call to Fail_Count --- lib/Insteon/BaseInterface.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index c779c6656..a377c103f 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -359,13 +359,14 @@ sub process_queue . $self->active_message->send_attempts . ") for " . $self->active_message->to_string() . " exceeds limit. Now moving on...") if $main::Debug{insteon}; - $self->fail_count_log(1) if $self->can('fail_count_log'); # !!!!!!!!! TO-DO - handle failure timeout ??? my $failed_message = $self->active_message; # make sure to let the sending object know!!! if (defined($failed_message->setby) and $failed_message->setby->can('is_acknowledged')) { $failed_message->setby->is_acknowledged(0); + $failed_message->setby->fail_count_log(1) + if $failed_message->setby->can('fail_count_log'); } else { From 172ab16274debe9d8cf40b9a64443b180c44ba22 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:08:45 -0700 Subject: [PATCH 106/330] Insteon_Stats: Add Calls to Max_Hops and Hops_Left --- lib/Insteon/BaseInterface.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index a377c103f..9d344aa91 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -532,6 +532,8 @@ sub on_standard_insteon_received my $object = &Insteon::get_object($msg{source}, $msg{group}); if (defined $object) { + $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); + $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); $object->incoming_count_log(1) if $object->can('incoming_count_log'); if ($msg{type} ne 'broadcast') { @@ -693,6 +695,8 @@ sub on_extended_insteon_received my $object = &Insteon::get_object($msg{source}, $msg{group}); if (defined $object) { + $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); + $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); $object->incoming_count_log(1) if $object->can('incoming_count_log'); if ($msg{type} ne 'broadcast') { From 1753671eac599572cad689b2f8e1305da3fd46ca Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:09:51 -0700 Subject: [PATCH 107/330] Insteon: Save Setby Object in On_Standard_Received Save the object so we can use it in print_log messages --- lib/Insteon/BaseInterface.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 9d344aa91..20e47f57f 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -578,6 +578,7 @@ sub on_standard_insteon_received } elsif ($msg{type} eq 'cleanup') { + my $setby_object = $object; $object = &Insteon::get_object('000000', $msg{extra}); if ($object) { @@ -586,7 +587,7 @@ sub on_standard_insteon_received # Don't clear active message as ACK is only one of many if (($msg{extra} == $self->active_message->setby->group)){ &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received for scene " - . $object->get_object_name . " from source " . uc($msg{source})) + . $object->get_object_name . " from " . $setby_object->get_object_name) if $main::Debug{insteon} >= 3; } elsif ($self->active_message->command_type eq 'all_link_direct_cleanup' && lc($self->active_message->setby->device_id) eq $msg{source}) @@ -596,7 +597,7 @@ sub on_standard_insteon_received } else { &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received from " - . $msg{source} . " for scene " + . $setby_object->get_object_name . " for scene " . $object->get_object_name . ", but group in recent message " . $msg{extra}. " did not match group in " . "prior sent message group " . $self->active_message->setby->group) @@ -609,7 +610,7 @@ sub on_standard_insteon_received else { &main::print_log("[Insteon::BaseInterface] ERROR: received cleanup message from " - . $msg{source} . "that does not correspond to a valid PLM group. Corrupted message is assumed " + . $setby_object->get_object_name . "that does not correspond to a valid PLM group. Corrupted message is assumed " . "and will be skipped! Was group " . $msg{extra}); } } From 396cf2a4fac8cc3a63a839dc1d3ae8e2caec1cd0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:11:07 -0700 Subject: [PATCH 108/330] Insteon_Stats: Add Call to Corrupt Log Count --- lib/Insteon/BaseInterface.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 20e47f57f..16bf32588 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -612,6 +612,7 @@ sub on_standard_insteon_received &main::print_log("[Insteon::BaseInterface] ERROR: received cleanup message from " . $setby_object->get_object_name . "that does not correspond to a valid PLM group. Corrupted message is assumed " . "and will be skipped! Was group " . $msg{extra}); + $setby_object->corrupt_count_log(1) if $setby_object->can('corrupt_count_log'); } } else #not direct or cleanup @@ -620,6 +621,7 @@ sub on_standard_insteon_received . $object->get_object_name . " but unable to process $msg{type} message type." . " IGNORING received message!!"); $self->active_message->no_hop_increase(1); + $object->corrupt_count_log(1) if $object->can('corrupt_count_log'); } } else #does not correspond to current active message From 069252a418089b280e1094ec42e2f3a227875a46 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:11:45 -0700 Subject: [PATCH 109/330] Insteon_Stats: Track Hops in Duplicate Messages They still provide some detail on how long messages take to arrive. There may be one issue in that this may catch subsequent deliveries of the same message with a lower hops_left count. Such as when the message is delivered on the 2nd and 3rd hop. But at the same time, sometimes the messages that should take longer to arrive, as they have used more hops, somehow arrive first, so this may be a wash. In the end, the best way to avoid this problem is to use proper hop counts. --- lib/Insteon/BaseInterface.pm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 16bf32588..e37ebc0ca 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -853,7 +853,16 @@ sub _is_duplicate_received { #Make a nicer name my $source = $msg{source}; my $object = &Insteon::get_object($msg{source}, $msg{group}); - $source = $object->get_object_name() if (defined $object); + if (defined $object) { + $source = $object->get_object_name(); + $object->dupe_count_log(1) if $object->can('dupe_count_log'); + $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); + $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); + $object->incoming_count_log(1) if $object->can('incoming_count_log'); + #This message still provides a data point on how many hops it is + #taking for messages to arrive. + $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); + }; ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " . $message_data . ", from $source.") if $main::Debug{insteon}; } else { From 9c275d2653a79197439332b2b4a3b9bda9c505e8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 24 Jul 2013 19:15:45 -0700 Subject: [PATCH 110/330] Insteon_Stats: Add Calls to Outgoing_Count and Hop_Count --- lib/Insteon/Message.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 12cea5827..089334efa 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -238,8 +238,10 @@ sub send } # need to set timeout as a function of retries; also need to alter hop count - if ($self->setby->can('outgoing_count_log') && $self->send_attempts <= 0) { - $self->setby->outgoing_count_log(1); + if ($self->send_attempts <= 0) { + $self->setby->outgoing_count_log(1) if $self->setby->can('outgoing_count_log'); + $self->setby->outgoing_hop_count($self->setby->default_hop_count) + if $self->setby->can('outgoing_hop_count'); } $self->send_attempts($self->send_attempts + 1); $interface->_send_cmd($self, $self->send_timeout); From d21f1f5fe515a731d2cd3f29e2c443865d07c9e3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:29:25 -0700 Subject: [PATCH 111/330] Insteon_Stats: Cleanup Print All Messages to Ignore Non-Root and Non-Responders --- lib/Insteon.pm | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 67fcc48d2..0d8a6b7c8 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -495,6 +495,21 @@ its message handling, as well as a summary average of the entire network. See L for more detailed information. +This command adds the following extra data points: + +=back + +=over8 + +=item * + +PLM_Error - The number of messages which have arrived at the PLM which cannot +be associated with any know device. + +=back + +=over + =cut sub print_message_logs @@ -524,9 +539,16 @@ sub print_message_logs my $hops_left_count = 0; my $max_hops_count = 0; my $outgoing_hop_count =0; + + my $device_count = 0; foreach my $current_log_device (@_log_devices) { + #Skip non-root items + next unless $current_log_device->is_root; + + $device_count++; + #Prints the Individual Message for the Device $current_log_device->print_message_log; @@ -559,7 +581,7 @@ sub print_message_logs $avg_out_hops = sprintf("%.1f", ($outgoing_hop_count / $outgoing_count_log)) if ($outgoing_count_log > 0); $curr_hops_avg = sprintf("%.1f", ($default_hop_count / - scalar @_log_devices)) if (scalar @_log_devices > 0); + $device_count)) if ($device_count > 0); ::print_log( "[Insteon] Average Network Statistics:\n" . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops\n" From 1c82694a761db44f1aab2444369b62dfbcfb7604 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:32:32 -0700 Subject: [PATCH 112/330] Insteon_Stats: Add Routine to Track Errors From Undefined Devices --- lib/Insteon.pm | 4 +++- lib/Insteon/BaseInterface.pm | 1 + lib/Insteon_PLM.pm | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 0d8a6b7c8..c240addae 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -584,7 +584,7 @@ sub print_message_logs $device_count)) if ($device_count > 0); ::print_log( "[Insteon] Average Network Statistics:\n" - . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops\n" + . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops PLM_Error\n" . sprintf("%6s", $incoming_count_log) . sprintf("%8s", $corrupt_count_log) . sprintf("%8s", $corrupt_percentage . '%') @@ -593,6 +593,7 @@ sub print_message_logs . sprintf("%9s", $avg_hops_left) . sprintf("%9s", $avg_max_hops) . sprintf("%9s", $avg_max_hops - $avg_hops_left) + . sprintf("%10s", &Insteon::active_interface->corrupt_count_log) . "\n" . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" . sprintf("%6s", $outgoing_count_log) @@ -620,6 +621,7 @@ its message handling. sub reset_message_logs { my @_log_devices = (); + &Insteon::active_interface->reset_message_log; push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); if (@_log_devices) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index e37ebc0ca..8b9a9d7e3 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -657,6 +657,7 @@ sub on_standard_insteon_received else { &::print_log("[Insteon::BaseInterface] Warn! Unable to locate object for source: $msg{source} and group: $msg{group}"); + $self->corrupt_count_log(1); } # treat the message as legitimate even if an object match did not occur } diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 90e670463..563e04c44 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -99,7 +99,8 @@ sub new { $$self{last_command} = ''; $$self{_prior_data_fragment} = ''; bless $self, $class; - $self->restore_data('debug'); + $self->restore_data('debug', 'corrupt_count_log'); + $$self{corrupt_count_log} = 0; $$self{aldb} = new Insteon::ALDB_PLM($self); &Insteon::add($self); @@ -118,6 +119,37 @@ sub new { return $self; } +=item C + +Sets or gets the number of corrupt message that have arrived that could not be +associated with any device since the last time C was called. +These are generally instances in which the from device ID is corrupt. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current corrupt count. + +=cut + +sub corrupt_count_log +{ + my ($self, $corrupt_count_log) = @_; + $$self{corrupt_count_log}++ if $corrupt_count_log; + return $$self{corrupt_count_log}; +} + +=item C + +Resets the retry, fail, outgoing, incoming, and corrupt message counters. + +=cut + +sub reset_message_log +{ + my ($self) = @_; + $$self{corrupt_count_log} = 0; +} + =item C This is called by mh on exit to save the cached ALDB of a device to persistant data. From 67e55924464d6693b497b11c1535d9a708871261 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:34:45 -0700 Subject: [PATCH 113/330] Insteon Stats: Only Track Stats for Root Devices Add More Calls to Stat Trackers --- lib/Insteon/BaseInsteon.pm | 12 ++++++++++++ lib/Insteon/BaseInterface.pm | 2 ++ 2 files changed, 14 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index e7adee13e..8a85eaaab 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -880,6 +880,7 @@ sub _process_message } else { main::print_log("[Insteon::BaseObject] Ignoring unsupported command from " . $self->{object_name}) if $main::Debug{insteon}; + $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); } } return $clear_message; @@ -2002,6 +2003,7 @@ Returns: current retry count. sub retry_count_log { my ($self, $retry_count_log) = @_; + $self = $self->get_root; $$self{retry_count_log}++ if $retry_count_log; return $$self{retry_count_log}; } @@ -2020,6 +2022,7 @@ Returns: current fail count. sub fail_count_log { my ($self, $fail_count_log) = @_; + $self = $self->get_root; $$self{fail_count_log}++ if $fail_count_log; return $$self{fail_count_log}; } @@ -2038,6 +2041,7 @@ Returns: current output count. sub outgoing_count_log { my ($self, $outgoing_count_log) = @_; + $self = $self->get_root; $$self{outgoing_count_log}++ if $outgoing_count_log; return $$self{outgoing_count_log}; } @@ -2056,6 +2060,7 @@ Returns: current hop count. sub outgoing_hop_count { my ($self, $outgoing_hop_count) = @_; + $self = $self->get_root; $$self{outgoing_hop_count} += $outgoing_hop_count if $outgoing_hop_count; return $$self{outgoing_hop_count}; } @@ -2074,6 +2079,7 @@ Returns: current incoming count. sub incoming_count_log { my ($self, $incoming_count_log) = @_; + $self = $self->get_root; $$self{incoming_count_log}++ if $incoming_count_log; return $$self{incoming_count_log}; } @@ -2092,6 +2098,7 @@ Returns: current corrupt count. sub corrupt_count_log { my ($self, $corrupt_count_log) = @_; + $self = $self->get_root; $$self{corrupt_count_log}++ if $corrupt_count_log; return $$self{corrupt_count_log}; } @@ -2110,6 +2117,7 @@ Returns: current duplicate count. sub dupe_count_log { my ($self, $dupe_count_log) = @_; + $self = $self->get_root; $$self{dupe_count_log}++ if $dupe_count_log; return $$self{dupe_count_log}; } @@ -2128,6 +2136,7 @@ Returns: current hops_left count. sub hops_left_count { my ($self, $hops_left_count) = @_; + $self = $self->get_root; $$self{hops_left_count} += $hops_left_count if $hops_left_count; return $$self{hops_left_count}; } @@ -2146,6 +2155,7 @@ Returns: current duplicate count. sub max_hops_count { my ($self, $max_hops_count) = @_; + $self = $self->get_root; $$self{max_hops_count} += $max_hops_count if $max_hops_count; return $$self{max_hops_count}; } @@ -2160,6 +2170,7 @@ Resets the retry, fail, outgoing, incoming, and corrupt message counters. sub reset_message_log { my ($self) = @_; + $self = $self->get_root; $$self{retry_count_log} = 0; $$self{fail_count_log} = 0; $$self{outgoing_count_log} = 0; @@ -2261,6 +2272,7 @@ controlled by MH and is not reset by calling C sub print_message_log { my ($self) = @_; + $self = $self->get_root; my $object_name = $self->get_object_name; my $retry_average = 0; $retry_average = sprintf("%.1f", ($$self{retry_count_log} / diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 8b9a9d7e3..7beb0fc3e 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -574,6 +574,8 @@ sub on_standard_insteon_received &main::print_log("[Insteon::BaseInterface] WARN: deviceid of " . "active message != received message source (" . $object->get_object_name() . "). IGNORING received message!!"); + #These generally seem to be duplicate messages + $object->dupe_count_log(1) if $object->can('dupe_count_log'); } } elsif ($msg{type} eq 'cleanup') From a20c3a97483ef66d88d5af79e6b6ba2985fda65e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:40:40 -0700 Subject: [PATCH 114/330] Insteon_Diags: Add Function to Call Test Read for All Devices --- lib/Insteon.pm | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 799f26c3f..efc5104fb 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -179,6 +179,44 @@ my (@_insteon_plm,@_insteon_device,@_insteon_link,@_scannable_link,$_scan_cnt,$_ my $init_complete; my (@_scan_devices,@_scan_device_failures,$current_scan_device); my (@_sync_devices,@_sync_device_failures,$current_sync_device); +my ($_test_count, @_test_devices); + +=item C + +Walks through every Insteon device and performs a test_read on it as many times as defined by +count. See L +for a more detailed description of test_read. + +=cut + +sub test_read_all +{ + my ($p_count) = @_; + if (defined $p_count){ + $_test_count = $p_count; + push @_test_devices, Insteon::find_members("Insteon::BaseDevice"); + main::print_log("[Insteon::Test Read All Devices] Test Read All Devices $p_count times"); + }; + + if (@_test_devices) + { + my $current_test_device; + my $not_found = 1; + my $complete_callback = '&Insteon::test_read_all()'; + while ($not_found){ + $current_test_device = pop @_test_devices; + next unless $current_test_device->is_root(); + next unless $current_test_device->is_responder(); + $not_found = 0; + } + if (ref $current_test_device && $current_test_device->can('test_read')){ + $current_test_device->test_read($_test_count, $complete_callback); + } + } else + { + main::print_log("[Insteon::Test Read All Devices] Complete"); + } +} =item C From 2b638739ed2a4ff6f93d8ba658e1a6eaaf8f6de2 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:41:33 -0700 Subject: [PATCH 115/330] Insteon_Diags: Add Function to Ping All Devices --- lib/Insteon.pm | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index efc5104fb..4c98dbbba 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -453,7 +453,36 @@ sub _get_next_linksync_failure } +=item C +Walks through every Insteon device and pings it as many times as defined by +count. See L +for a more detailed description of ping. + +=cut + +sub ping_all +{ + my ($p_count) = @_; + my @_ping_devices = (); + push @_ping_devices, Insteon::find_members("Insteon::BaseDevice"); + + if (@_ping_devices) + { + main::print_log("[Insteon::Ping All Devices] Ping All Devices $p_count times"); + foreach my $current_ping_device (@_ping_devices) + { + next unless $current_ping_device->is_root(); + next unless $current_ping_device->is_responder(); + $current_ping_device->ping($p_count) + if $current_ping_device->can('ping'); + } + main::print_log("[Insteon::Ping All Devices] Ping All Complete"); + } else + { + main::print_log("[Insteon::Ping All Devices] WARN: No insteon devices could be found"); + } +} =item C From 487dd802bb29e1119062524c360add35865b6c93 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:43:47 -0700 Subject: [PATCH 116/330] Insteon_Diags: Add Ability to Read One ALDB Entry Only --- lib/Insteon/AllLinkDatabase.pm | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 5c7918641..3eaae2294 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1590,7 +1590,8 @@ sub _on_peek $$self{_mem_action} = 'aldb_flag'; # if the device is responding to the peek, then init the link table # if at the very start of a scan - if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8') + if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8' + && !$$self{_mem_only_one}) { # reinit the aldb hash as there will be a new one $$self{aldb} = undef; @@ -1635,7 +1636,12 @@ sub _on_peek $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; - if (!($$self{pending_aldb}{highwater})) + if ($$self{_mem_only_one} && !($$self{pending_aldb}{highwater})){ + ::print_log("[Insteon::ALDB_i1] You need to create a link on this device before running test_read"); + $$self{_mem_only_one} = 0; + $$self{device}->test_read(); + } + elsif (!($$self{pending_aldb}{highwater})) { # since this is the last unused memory location, then add it to the empty list $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); @@ -1836,8 +1842,12 @@ sub _on_peek . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; $$self{pending_aldb}{data3} = $msg{extra}; - # check the previous record if highwater is set - if ($$self{pending_aldb}{highwater}) + + if ($$self{_mem_only_one}){ + $$self{_mem_only_one} = 0; + $$self{device}->test_read(); + } + elsif ($$self{pending_aldb}{highwater}) { if ($$self{pending_aldb}{inuse}) { @@ -2158,6 +2168,10 @@ sub on_read_write_aldb #retry previous address again $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); } + elsif ($$self{_mem_only_one}){ + $$self{_mem_only_one} = 0; + $$self{device}->test_read(); + } else { # init the link table if at the very start of a scan From dfc54aa080cbc122c19e24dfaac031cb37d50d87 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:44:42 -0700 Subject: [PATCH 117/330] Insteon_Diags: Add Function to Repetitively Read One ALDB Entry --- lib/Insteon/BaseInsteon.pm | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4aaad4330..418fc546f 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1955,6 +1955,69 @@ sub update_flags $self->_aldb->update_flags($flags) if $self->_aldb; } +=item C + +Simulates a read of a link address from the device. Repeats this process as many +times as defined by count. This routine is meant to be used as a diagnostic tool. +It is similar to scan_link_table, however, if a failure occurs, this process will +continue with the next iteration. Scan link table will stop as soon as a single +failure occurs. + +This is also similar to the C test, however, rather than simply requesting +an ACK, this requests a full set of data equivalent to a link entry. Similar to +C this should be used with C to diagnose issues and try +different settings. + +Note: This routine can create a lot of traffic if count is set very high. Try +setting it to 5 first and working your way up. + +=cut + +sub test_read +{ + my ($self, $p_count, $complete_callback) = @_; + $$self{test_read_count} = $p_count if (defined ($p_count)); + $$self{test_callback} = $complete_callback if (defined ($complete_callback)); + if ($$self{test_read_count}){ + &::print_log("[Insteon::BaseDevice] " . $self->get_object_name + . " - Test Read " . $$self{test_read_count} . " iterations left"); + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + $$aldb{_mem_activity} = 'scan'; + $$aldb{_mem_only_one} = 1; + $$aldb{_failure_callback} = $self->get_object_name + . '->test_read()'; + if($aldb->isa('Insteon::ALDB_i1')) { + $aldb->_peek('0FF8'); + } else { + #Prevents duplicate commands in queue error + #Also allows for better identification of + #sequential dupe incoming messages + my $odd = $$self{test_read_count} % 2; + if ($odd){ + $aldb->send_read_aldb('0fff'); + } else { + $aldb->send_read_aldb('0ff7'); + } + } + } + $$self{test_read_count}--; + } else { + &::print_log("[Insteon::BaseDevice] Test Read Complete for " + . $self->get_object_name); + if (defined $$self{test_callback}){ + $complete_callback = $$self{test_callback}; + package main; + eval ($complete_callback); + &::print_log("[Insteon::BaseDevice] error in test_read callback: " . $@) + if $@ and $main::Debug{insteon}; + package Insteon::BaseDevice; + delete $$self{test_callback}; + } + } +} + =item C Sets or gets the device object engine version. If setting the engine version, From 625f0e67c7beb4540221e1cf1b0dca5ad45e6d2a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 25 Jul 2013 20:45:12 -0700 Subject: [PATCH 118/330] Insteon_Diags: Fix Ping Function to Send Repeated Ping Messages --- lib/Insteon/BaseInsteon.pm | 64 +++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 418fc546f..5860e083e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -747,6 +747,17 @@ sub _process_message $clear_message = 0; } } + elsif ($pending_cmd eq 'ping'){ + $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); + $corrupt_cmd = 1 if ($msg{extra} ne sprintf ("%02d", $$self{ping_count})); + if (!$corrupt_cmd){ + $self->_process_command_stack(%msg); + &::print_log("[Insteon::BaseObject] received ping acknowledgement from " . $self->{object_name}) + if $main::Debug{insteon}; + $self->ping(); + $clear_message = 1; + } + } else { if (($pending_cmd eq 'do_read_ee') && @@ -911,6 +922,7 @@ sub _process_command_stack or $message->command eq 'set_operating_flags' or $message->command eq 'get_operating_flags' or $message->command eq 'read_write_aldb' + or $message->command eq 'ping' ) { $$self{awaiting_ack} = 1; @@ -1084,7 +1096,7 @@ our %message_types = ( linking_mode => 0x09, unlinking_mode => 0x0A, get_engine_version => 0x0D, - ping => 0x10, + ping => 0x0F, on_fast => 0x12, off_fast => 0x14, start_manual_change => 0x17, @@ -1713,18 +1725,54 @@ sub _get_engine_version_failure } } -=item C +=item C -Sends a ping command to the device. +Sends the number of ping messages defined by count. A ping message is a basic +message that simply asks the device to respond with an ACKnowledgement. For +i1 devices this will send a standard length command, for i2 and i2cs devices +this will send an extended ping command. In both cases, the device responds +back with a standard length ACKnowledgement only. -=cut +Much like the ping command in IP networks, this command is useful for testing the +connectivity of a device on your network. You likely want to use this in +conjunction with the C routine. For example, you can use +this to compare the message stats for a device when changing settings in +MisterHouse. + +Parameters: + count = the number of pings to send. + +Returns: Nothing. + +=cut sub ping { - my ($self) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'ping'); - $self->_send_cmd($message); -# $self->_send_cmd('command' => 'ping'); + my ($self, $p_count) = @_; + $$self{ping_count} = $p_count if defined($p_count); + if ($$self{ping_count}) { + $$self{ping_count}--; + my $message; + my $extra = sprintf("%02d", $$self{ping_count}); + if (uc($self->engine_version) eq 'I1'){ + $message = new Insteon::InsteonMessage('insteon_send', $self, + 'ping', $extra); + } + else { + my $extra = $extra . '0' x 28; + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, + 'ping', $extra); + } + ::print_log("[Insteon::BaseDevice] Sending ping request to " + . $self->get_object_name . " " . $$self{ping_count} + . " more ping requests queued."); + $message->failure_callback($self->get_object_name . '->ping()'); + $self->_send_cmd($message); + } + else { + ::print_log("[Insteon::BaseDevice] Completed ping queue for " + . $self->get_object_name); + } } =item C From 433181a58a96df2140a11947f7309570f950ab8a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 26 Jul 2013 17:33:00 -0700 Subject: [PATCH 119/330] Insteon_Diagnostics: Change Name of Routine to Stress_Test add One Pass Option --- lib/Insteon.pm | 65 ++++++++++++++++++++-------------- lib/Insteon/AllLinkDatabase.pm | 22 ++++++------ lib/Insteon/BaseInsteon.pm | 30 ++++++++-------- 3 files changed, 65 insertions(+), 52 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 4c98dbbba..b88ee0212 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -179,42 +179,55 @@ my (@_insteon_plm,@_insteon_device,@_insteon_link,@_scannable_link,$_scan_cnt,$_ my $init_complete; my (@_scan_devices,@_scan_device_failures,$current_scan_device); my (@_sync_devices,@_sync_device_failures,$current_sync_device); -my ($_test_count, @_test_devices); +my ($_stress_test_count, $_stress_test_one_pass, @_stress_test_devices); -=item C +=item C -Walks through every Insteon device and performs a test_read on it as many times as defined by -count. See L -for a more detailed description of test_read. +Sequentially goes through every Insteon device and performs a stress_test on it. +See L +for a more detailed description of stress_test. + +Parameters: + Count: defines the number of stress_tests to perform on each device. + is_one_pass: if true, all stress_tests will be performed on a device + before proceeding to the next device. if false, the routine + loops through all devices performing one stress_test on each + device before moving on to the next device. =cut -sub test_read_all +sub stress_test_all { - my ($p_count) = @_; + my ($p_count, $is_one_pass) = @_; if (defined $p_count){ - $_test_count = $p_count; - push @_test_devices, Insteon::find_members("Insteon::BaseDevice"); - main::print_log("[Insteon::Test Read All Devices] Test Read All Devices $p_count times"); + $_stress_test_count = $p_count; + $_stress_test_one_pass = $is_one_pass; + push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); + main::print_log("[Insteon::Stress Test All Devices] Stress Testing All Devices $p_count times"); }; - - if (@_test_devices) - { - my $current_test_device; - my $not_found = 1; - my $complete_callback = '&Insteon::test_read_all()'; - while ($not_found){ - $current_test_device = pop @_test_devices; - next unless $current_test_device->is_root(); - next unless $current_test_device->is_responder(); - $not_found = 0; + if (!@_stress_test_devices) { + #Iteration may be complete, start over from the beginning + $_stress_test_count = ($_stress_test_one_pass) ? 0 : $_stress_test_count--; + push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); + } + if ($_stress_test_count > 0){ + my $current_stress_test_device; + my $complete_callback = '&Insteon::stress_test_all()'; + while (@_stress_test_devices){ + $current_stress_test_device = pop @_stress_test_devices; + next unless $current_stress_test_device->is_root(); + next unless $current_stress_test_device->is_responder(); + last; } - if (ref $current_test_device && $current_test_device->can('test_read')){ - $current_test_device->test_read($_test_count, $complete_callback); + my $run_count = ($_stress_test_one_pass) ? $_stress_test_count : 1; + if (ref $current_stress_test_device && $current_stress_test_device->can('stress_test')){ + $current_stress_test_device->stress_test($run_count, $complete_callback); } - } else - { - main::print_log("[Insteon::Test Read All Devices] Complete"); + } + else { + $_stress_test_one_pass = 0; + @_stress_test_devices = undef; + main::print_log("[Insteon::Stress Test All Devices] Complete"); } } diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 3eaae2294..3f6bb5769 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1591,7 +1591,7 @@ sub _on_peek # if the device is responding to the peek, then init the link table # if at the very start of a scan if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8' - && !$$self{_mem_only_one}) + && !$$self{_stress_test_act}) { # reinit the aldb hash as there will be a new one $$self{aldb} = undef; @@ -1636,10 +1636,10 @@ sub _on_peek $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; - if ($$self{_mem_only_one} && !($$self{pending_aldb}{highwater})){ - ::print_log("[Insteon::ALDB_i1] You need to create a link on this device before running test_read"); - $$self{_mem_only_one} = 0; - $$self{device}->test_read(); + if ($$self{_stress_test_act} && !($$self{pending_aldb}{highwater})){ + ::print_log("[Insteon::ALDB_i1] You need to create a link on this device before running stress_test"); + $$self{_stress_test_act} = 0; + $$self{device}->stress_test(); } elsif (!($$self{pending_aldb}{highwater})) { @@ -1843,9 +1843,9 @@ sub _on_peek . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; $$self{pending_aldb}{data3} = $msg{extra}; - if ($$self{_mem_only_one}){ - $$self{_mem_only_one} = 0; - $$self{device}->test_read(); + if ($$self{_stress_test_act}){ + $$self{_stress_test_act} = 0; + $$self{device}->stress_test(); } elsif ($$self{pending_aldb}{highwater}) { @@ -2168,9 +2168,9 @@ sub on_read_write_aldb #retry previous address again $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); } - elsif ($$self{_mem_only_one}){ - $$self{_mem_only_one} = 0; - $$self{device}->test_read(); + elsif ($$self{_stress_test_act}){ + $$self{_stress_test_act} = 0; + $$self{device}->stress_test(); } else { diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 5860e083e..b0fd51b79 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2003,7 +2003,7 @@ sub update_flags $self->_aldb->update_flags($flags) if $self->_aldb; } -=item C +=item C Simulates a read of a link address from the device. Repeats this process as many times as defined by count. This routine is meant to be used as a diagnostic tool. @@ -2021,28 +2021,28 @@ setting it to 5 first and working your way up. =cut -sub test_read +sub stress_test { my ($self, $p_count, $complete_callback) = @_; - $$self{test_read_count} = $p_count if (defined ($p_count)); - $$self{test_callback} = $complete_callback if (defined ($complete_callback)); - if ($$self{test_read_count}){ + $$self{stress_test_count} = $p_count if (defined ($p_count)); + $$self{stress_test_callback} = $complete_callback if (defined ($complete_callback)); + if ($$self{stress_test_count}){ &::print_log("[Insteon::BaseDevice] " . $self->get_object_name - . " - Test Read " . $$self{test_read_count} . " iterations left"); + . " - Stress Test " . $$self{stress_test_count} . " iterations left"); my $aldb = $self->get_root()->_aldb; if ($aldb) { $$aldb{_mem_activity} = 'scan'; - $$aldb{_mem_only_one} = 1; + $$aldb{_stress_test_act} = 1; $$aldb{_failure_callback} = $self->get_object_name - . '->test_read()'; + . '->stress_test()'; if($aldb->isa('Insteon::ALDB_i1')) { $aldb->_peek('0FF8'); } else { #Prevents duplicate commands in queue error #Also allows for better identification of #sequential dupe incoming messages - my $odd = $$self{test_read_count} % 2; + my $odd = $$self{stress_test_count} % 2; if ($odd){ $aldb->send_read_aldb('0fff'); } else { @@ -2050,18 +2050,18 @@ sub test_read } } } - $$self{test_read_count}--; + $$self{stress_test_count}--; } else { - &::print_log("[Insteon::BaseDevice] Test Read Complete for " + &::print_log("[Insteon::BaseDevice] Stress Test Complete for " . $self->get_object_name); - if (defined $$self{test_callback}){ - $complete_callback = $$self{test_callback}; + if (defined $$self{stress_test_callback}){ + $complete_callback = $$self{stress_test_callback}; package main; eval ($complete_callback); - &::print_log("[Insteon::BaseDevice] error in test_read callback: " . $@) + &::print_log("[Insteon::BaseDevice] error in stress_test callback: " . $@) if $@ and $main::Debug{insteon}; package Insteon::BaseDevice; - delete $$self{test_callback}; + delete $$self{stress_test_callback}; } } } From 6ffcfbcb4f2752164412f3ee02c55fdcc1ace9ce Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 27 Jul 2013 09:47:12 -0700 Subject: [PATCH 120/330] Insteon_Diags: Clear ALBD Memory Pointer --- lib/Insteon/AllLinkDatabase.pm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 3f6bb5769..0f939cbeb 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1638,6 +1638,8 @@ sub _on_peek $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; if ($$self{_stress_test_act} && !($$self{pending_aldb}{highwater})){ ::print_log("[Insteon::ALDB_i1] You need to create a link on this device before running stress_test"); + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; $$self{_stress_test_act} = 0; $$self{device}->stress_test(); } @@ -1845,6 +1847,8 @@ sub _on_peek if ($$self{_stress_test_act}){ $$self{_stress_test_act} = 0; + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; $$self{device}->stress_test(); } elsif ($$self{pending_aldb}{highwater}) @@ -1917,9 +1921,11 @@ sub _on_peek $message->failure_callback($$self{_failure_callback}); $self->_send_cmd($message); } -# -# &::print_log("AllLinkDataBase: peek for " . $self->{object_name} -# . " is " . $msg{extra}) if $main::Debug{insteon}; + else { + ::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name + . ": unhandled _mem_action=".$$self{_mem_action}) + if $main::Debug{insteon}; + } } } @@ -2169,6 +2175,8 @@ sub on_read_write_aldb $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); } elsif ($$self{_stress_test_act}){ + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; $$self{_stress_test_act} = 0; $$self{device}->stress_test(); } From e8d4c33c0229afb5d8cb58cad7f6a62466848db7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 29 Jul 2013 19:48:47 -0700 Subject: [PATCH 121/330] Insteon_Diags: Process Ping_All Command on Devices Sequentially Finish processing each device before proceeding onto the next device. --- lib/Insteon.pm | 26 ++++++++++++++++---------- lib/Insteon/BaseInsteon.pm | 12 +++++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index b88ee0212..719e66d08 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -180,6 +180,7 @@ my $init_complete; my (@_scan_devices,@_scan_device_failures,$current_scan_device); my (@_sync_devices,@_sync_device_failures,$current_sync_device); my ($_stress_test_count, $_stress_test_one_pass, @_stress_test_devices); +my ($_ping_count, @_ping_devices); =item C @@ -202,6 +203,7 @@ sub stress_test_all if (defined $p_count){ $_stress_test_count = $p_count; $_stress_test_one_pass = $is_one_pass; + @_stress_test_devices = undef; push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); main::print_log("[Insteon::Stress Test All Devices] Stress Testing All Devices $p_count times"); }; @@ -226,7 +228,6 @@ sub stress_test_all } else { $_stress_test_one_pass = 0; - @_stress_test_devices = undef; main::print_log("[Insteon::Stress Test All Devices] Complete"); } } @@ -477,23 +478,28 @@ for a more detailed description of ping. sub ping_all { my ($p_count) = @_; - my @_ping_devices = (); - push @_ping_devices, Insteon::find_members("Insteon::BaseDevice"); - + if (defined $p_count){ + $_ping_count = $p_count; + @_ping_devices = (); + push @_ping_devices, Insteon::find_members("Insteon::BaseDevice"); + main::print_log("[Insteon::Ping All Devices] Ping All Devices $p_count times"); + } if (@_ping_devices) { - main::print_log("[Insteon::Ping All Devices] Ping All Devices $p_count times"); - foreach my $current_ping_device (@_ping_devices) + my $current_ping_device; + while(@_ping_devices) { + $current_ping_device = pop @_ping_devices; next unless $current_ping_device->is_root(); next unless $current_ping_device->is_responder(); - $current_ping_device->ping($p_count) - if $current_ping_device->can('ping'); + last; } - main::print_log("[Insteon::Ping All Devices] Ping All Complete"); + $current_ping_device->ping($_ping_count, '&Insteon::ping_all()') + if $current_ping_device->can('ping'); } else { - main::print_log("[Insteon::Ping All Devices] WARN: No insteon devices could be found"); + $_ping_count = 0; + main::print_log("[Insteon::Ping All Devices] Ping All Complete"); } } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index b0fd51b79..fe28d7ffb 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1748,8 +1748,9 @@ Returns: Nothing. sub ping { - my ($self, $p_count) = @_; + my ($self, $p_count, $p_callback) = @_; $$self{ping_count} = $p_count if defined($p_count); + $$self{ping_callback} = $p_callback if defined($p_callback); if ($$self{ping_count}) { $$self{ping_count}--; my $message; @@ -1772,6 +1773,15 @@ sub ping else { ::print_log("[Insteon::BaseDevice] Completed ping queue for " . $self->get_object_name); + if (defined $$self{ping_callback}){ + my $complete_callback = $$self{ping_callback}; + package main; + eval ($complete_callback); + &::print_log("[Insteon::BaseDevice] error in ping callback: " . $@) + if $@ and $main::Debug{insteon}; + package Insteon::BaseDevice; + delete $$self{ping_callback}; + } } } From aaaf542f6abc933e8f60778b341e9f143b515df4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 29 Jul 2013 19:54:43 -0700 Subject: [PATCH 122/330] Insteon_Stats: Change Name of Routine to Message_Stats Much more user friendly and descriptice name. --- lib/Insteon.pm | 30 +++++++++++++++--------------- lib/Insteon/BaseInsteon.pm | 28 ++++++++++++++-------------- lib/Insteon_PLM.pm | 6 +++--- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index c240addae..bf2ca0931 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -488,11 +488,11 @@ sub log_all_ADLB_status } } -=item C +=item C Walks through every Insteon device and prints statistical information about its message handling, as well as a summary average of the entire network. See -L +L for more detailed information. This command adds the following extra data points: @@ -503,7 +503,7 @@ This command adds the following extra data points: =item * -PLM_Error - The number of messages which have arrived at the PLM which cannot +Unk_Error - The number of messages which have arrived at the PLM which cannot be associated with any know device. =back @@ -512,7 +512,7 @@ be associated with any know device. =cut -sub print_message_logs +sub print_all_message_stats { my @_log_devices = (); push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); @@ -550,7 +550,7 @@ sub print_message_logs $device_count++; #Prints the Individual Message for the Device - $current_log_device->print_message_log; + $current_log_device->print_message_stats; #Add values for each device to the master count $incoming_count_log += $current_log_device->incoming_count_log; @@ -584,7 +584,7 @@ sub print_message_logs $device_count)) if ($device_count > 0); ::print_log( "[Insteon] Average Network Statistics:\n" - . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops PLM_Error\n" + . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops Unk_Error\n" . sprintf("%6s", $incoming_count_log) . sprintf("%8s", $corrupt_count_log) . sprintf("%8s", $corrupt_percentage . '%') @@ -604,37 +604,37 @@ sub print_message_logs . sprintf("%9s", $avg_out_hops) . sprintf("%9s", $curr_hops_avg) ); - main::print_log("[Insteon::Print_Message_Logs] All devices have completed logging"); + main::print_log("[Insteon::Print_All_Message_Stats] All devices have completed logging"); } else { - main::print_log("[Insteon::Print_Message_Logs] WARN: No insteon devices could be found"); + main::print_log("[Insteon::Print_All_Message_Stats] WARN: No insteon devices could be found"); } } -=item C +=item C Walks through every Insteon device and resets the statistical information about its message handling. =cut -sub reset_message_logs +sub reset_all_message_stats { my @_log_devices = (); - &Insteon::active_interface->reset_message_log; + &Insteon::active_interface->reset_message_stats; push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); if (@_log_devices) { foreach my $current_log_device (@_log_devices) { - $current_log_device->reset_message_log - if $current_log_device->can('reset_message_log'); + $current_log_device->reset_message_stats + if $current_log_device->can('reset_message_stats'); } - main::print_log("[Insteon::Reset_Message_Logs] All devices have been reset"); + main::print_log("[Insteon::Reset_All_Message_Stats] All devices have been reset"); } else { - main::print_log("[Insteon::Reset_Message_Logs] WARN: No insteon devices could be found"); + main::print_log("[Insteon::Reset_All_Message_Stats] WARN: No insteon devices could be found"); } } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 8a85eaaab..d431e9e54 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1992,7 +1992,7 @@ sub engine_version =item C Sets or gets the number of message retries that have occured for this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment retry log by one. @@ -2011,7 +2011,7 @@ sub retry_count_log =item C Sets or gets the number of message failures that have occured for this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment fail log by one. @@ -2030,7 +2030,7 @@ sub fail_count_log =item C Sets or gets the number of outgoing message that have occured for this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment output count by one. @@ -2049,7 +2049,7 @@ sub outgoing_count_log =item C Sets or gets the number of hops that have been used in all outgoing messages -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment output count by that value. @@ -2068,7 +2068,7 @@ sub outgoing_hop_count =item C Sets or gets the number of incoming message that have occured for this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment incoming count by one. @@ -2087,7 +2087,7 @@ sub incoming_count_log =item C Sets or gets the number of currupt message that have arrived from this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment corrupt count by one. @@ -2106,7 +2106,7 @@ sub corrupt_count_log =item C Sets or gets the number of duplicate message that have arrived from this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment corrupt count by one. @@ -2125,7 +2125,7 @@ sub dupe_count_log =item C Sets or gets the number of hops_left for messages that arrive from this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment corrupt count by one. @@ -2144,7 +2144,7 @@ sub hops_left_count =item C Sets or gets the number of max_hops for messages that arrive from this device -since the last time C was called. +since the last time C was called. If type is set, to any value, will increment corrupt count by one. @@ -2161,13 +2161,13 @@ sub max_hops_count } -=item C +=item C Resets the retry, fail, outgoing, incoming, and corrupt message counters. =cut -sub reset_message_log +sub reset_message_stats { my ($self) = @_; $self = $self->get_root; @@ -2182,7 +2182,7 @@ sub reset_message_log $$self{outgoing_hop_count} = 0; } -=item C +=item C Prints message statistics for this device to the print log. The output contains: @@ -2261,7 +2261,7 @@ sending messages to this device. =item * Hop_Count - The current hop count being used by MH. This count is dynamically -controlled by MH and is not reset by calling C +controlled by MH and is not reset by calling C =back @@ -2269,7 +2269,7 @@ controlled by MH and is not reset by calling C =cut -sub print_message_log +sub print_message_stats { my ($self) = @_; $self = $self->get_root; diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 563e04c44..ad7bd8e3b 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -122,7 +122,7 @@ sub new { =item C Sets or gets the number of corrupt message that have arrived that could not be -associated with any device since the last time C was called. +associated with any device since the last time C was called. These are generally instances in which the from device ID is corrupt. If type is set, to any value, will increment corrupt count by one. @@ -138,13 +138,13 @@ sub corrupt_count_log return $$self{corrupt_count_log}; } -=item C +=item C Resets the retry, fail, outgoing, incoming, and corrupt message counters. =cut -sub reset_message_log +sub reset_message_stats { my ($self) = @_; $$self{corrupt_count_log} = 0; From 1a6b8bacb6604652a08ef8820eef54e4bc2b6b6a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 29 Jul 2013 20:58:37 -0700 Subject: [PATCH 123/330] Insteon_Diags: Remove all Old References to Ping Timer Get rid of old devcat type entries and related ping timer that is no longer used --- lib/Insteon/BaseInsteon.pm | 26 +------------------------- lib/Insteon/BaseInterface.pm | 4 ---- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index fe28d7ffb..14c44a53f 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -130,12 +130,6 @@ sub initialize $$self{m_write} = 1; $$self{m_is_locally_set} = 0; # persist local, simple attribs - - # do we really need to ping the devices anymore for a devcat? - $$self{ping_timer} = new Timer(); - $$self{ping_timerTime} = 300; -# $$self{ping_timer}->set($$self{ping_timerTime} + (rand() * $$self{ping_timerTime}), $self) -# unless $self->group eq '01' and defined $self->devcat; } =item C @@ -282,14 +276,7 @@ sub set delete $$self{set_timer}; } - # did the queue timer go off? - if (ref $p_setby and $p_setby eq $$self{ping_timer}) { - if (! (defined($$self{devcat}))) { - $self->ping(); - # set the timer again in case nothing occurs - $$self{ping_timer}->set($$self{ping_timerTime} + (rand() * $$self{ping_timerTime}), $self); - } - } elsif ($self->_is_valid_state($p_state)) { + if ($self->_is_valid_state($p_state)) { # always reset the is_locally_set property unless set_by is the device $$self{m_is_locally_set} = 0 unless ref $p_setby and $p_setby eq $self; @@ -847,8 +834,6 @@ sub _process_message } elsif ($msg{type} eq 'broadcast') { $self->devcat($msg{devcat}); &::print_log("[Insteon::BaseObject] device category: $msg{devcat} received for " . $self->{object_name}); - # stop ping timer now that we have a devcat; possibly may want to change this behavior to allow recurring pings - $$self{ping_timer}->stop(); } else { ## TO-DO: make sure that the state passed by command is something that is reasonable to set $p_state = $msg{command}; @@ -1190,10 +1175,6 @@ sub initialize $$self{m_write} = 1; $$self{m_is_locally_set} = 0; # persist local, simple attribs - - # do we really need to ping the devices anymore for a devcat? - $$self{ping_timer} = new Timer(); - $$self{ping_timerTime} = 300; } =item C @@ -2224,8 +2205,6 @@ sub new # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller my $self = {}; bless $self,$class; -# don't apply ping timer to this class -# $$self{ping_timer}->stop(); return $self; } @@ -2563,9 +2542,6 @@ sub set $p_setby->{set_by} eq $self); return -1 if &main::check_for_tied_filters($self, $p_state); - # prevent setby internal Insteon_Device timers - return -1 if $p_setby eq $$self{ping_timer}; - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); $self->set_linked_devices($link_state); diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index b2493956a..6b0d64761 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -71,10 +71,6 @@ sub poll_all $insteon_device->get_engine_version(); $insteon_device->request_status(); } - if ($insteon_device->devcat) { - # reset devcat so as to trigger any device specific properties - $insteon_device->devcat($insteon_device->devcat); - } } } } From 6585c92ad904e05adbf0e40897677f82bb08b624 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 29 Jul 2013 21:02:35 -0700 Subject: [PATCH 124/330] Insteon_Diags: Restore Devcat Support, Add Support for Firmware Reintroduced method to request devcat that was previously called ping and overwritten by a real ping command. New method to request devcat is called get_devcat(). - Once devcat is obtained, it is saved through reboots and can be retreived with devcat() routine - Now also saves firmware which can be retrieved with firmware() routine We do not currently use the devcat values, but I could certainly see using it in the future. --- lib/Insteon/BaseInsteon.pm | 46 ++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 14c44a53f..c600b752a 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -833,7 +833,9 @@ sub _process_message } } elsif ($msg{type} eq 'broadcast') { $self->devcat($msg{devcat}); - &::print_log("[Insteon::BaseObject] device category: $msg{devcat} received for " . $self->{object_name}); + $self->firmware($msg{firmware}); + &::print_log("[Insteon::BaseObject] device category: $msg{devcat}" + . " firmware: $msg{firmware} received for " . $self->{object_name}); } else { ## TO-DO: make sure that the state passed by command is something that is reasonable to set $p_state = $msg{command}; @@ -901,6 +903,7 @@ sub _process_command_stack or $message->command eq 'poke' or $message->command eq 'status_request' or $message->command eq 'get_engine_version' + or $message->command eq 'id_request' or $message->command eq 'do_read_ee' or $message->command eq 'set_address_msb' or $message->command eq 'sensor_status' @@ -1082,6 +1085,7 @@ our %message_types = ( unlinking_mode => 0x0A, get_engine_version => 0x0D, ping => 0x0F, + id_request => 0x10, on_fast => 0x12, off_fast => 0x14, start_manual_change => 0x17, @@ -1144,7 +1148,10 @@ sub new $$self{aldb} = new Insteon::ALDB_i1($self); } - $self->restore_data('level'); + $self->restore_data('devcat', 'firmware', 'level', 'retry_count_log', 'fail_count_log', + 'outgoing_count_log', 'incoming_count_log', 'corrupt_count_log', + 'dupe_count_log', 'hops_left_count', 'max_hops_count', + 'outgoing_hop_count'); $self->initialize(); $self->rate(undef); @@ -1831,7 +1838,8 @@ sub restore_aldb =item C -NOT USED - Sets and returns the device category of a device. No longer used. +Sets and returns the device category of a device. Devcat can be requested by +calling C. =cut @@ -1841,14 +1849,38 @@ sub devcat if ($devcat) { $$self{devcat} = $devcat; - if (($$self{devcat} =~ /^01\w\w/) or ($$self{devcat} =~ /^02\w\w/) && !($self->states)) - { - $self->states( 'on,off' ); - } } return $$self{devcat}; } +=item C + +Requests the device category for the device. The returned value can be obtained +by calling C. + +=cut + +sub get_devcat +{ + my ($self) = @_; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'id_request'); + $self->_send_cmd($message); +} + +=item C + +Sets and returns the device's firmware version. Value can be obtained from the +device by calling C. + +=cut + +sub firmware +{ + my ($self, $firmware) = @_; + $$self{firmware} = $firmware if (defined $firmware); + return $$self{firmware}; +} + =item C Sets and returns the available states for a device. From dee488bbdd79329f9e43eeee5caaf817d056ed40 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 29 Jul 2013 21:11:13 -0700 Subject: [PATCH 125/330] Insteon_Diags: Fix Dumb Typo --- lib/Insteon/BaseInsteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c600b752a..76f381b08 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1867,7 +1867,7 @@ sub get_devcat $self->_send_cmd($message); } -=item C +=item C Sets and returns the device's firmware version. Value can be obtained from the device by calling C. From cc64f873fd79cc0aac2dbe81116ea2a20a1ce695 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 30 Jul 2013 19:06:18 -0700 Subject: [PATCH 126/330] Insteon: Relocate local_onlevel, local_ramprate, and update_local_properties Moved to the more appropriate DimmableLight class --- lib/Insteon/BaseInsteon.pm | 57 -------------------------------------- lib/Insteon/Lighting.pm | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 545c0fc87..ff8e00599 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1887,42 +1887,6 @@ sub states } -=item C - -Sets and returns the local onlevel for the device in MH only. Level is a -percentage from 0%-100% - -=cut - -sub local_onlevel -{ - my ($self, $p_onlevel) = @_; - if (defined $p_onlevel) - { - my ($onlevel) = $p_onlevel =~ /(\d+)%?/; - $$self{_onlevel} = $onlevel; - } - return $$self{_onlevel}; -} - -=item C - -Sets and returns the local ramp rate for the device in MH only. Rate is a time -between .1 and 540 seconds. Only 32 rate steps exist, to MH will pick a time -equal to of the closest below this time. - -=cut - -sub local_ramprate -{ - my ($self, $p_ramprate) = @_; - if (defined $p_ramprate) { - $$self{_ramprate} = &Insteon::DimmableLight::convert_ramp($p_ramprate); - } - return $$self{_ramprate}; - -} - =item C Reviews the cached version of all of the ALDBs and based on this review removes @@ -1959,27 +1923,6 @@ sub log_alllink_table $self->_aldb->log_alllink_table if $self->_aldb; } -=item C - -Pushes the values set in C and C to the device. -The device will only reread these values when it is power-cycled. This can be -done by pulling the air-gap for 4 seconds or unplugging the device. - -=cut - -sub update_local_properties -{ - my ($self) = @_; - if ($self->isa('Insteon::DimmableLight')) - { - $self->_aldb->update_local_properties() if $self->_aldb; - } - else - { - &::print_log("[Insteon::BaseDevice] update_local_properties may only be applied to dimmable devices!"); - } -} - =item C Can be used to set the button layout and light level on a keypadlinc. Flag diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 92d69cf18..ca20c0322 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -218,6 +218,60 @@ sub new return $self; } +=item C + +Sets and returns the local onlevel for the device in MH only. Level is a +percentage from 0%-100%. + +This setting can be pushed to the device using C. + +=cut + +sub local_onlevel +{ + my ($self, $p_onlevel) = @_; + if (defined $p_onlevel) + { + my ($onlevel) = $p_onlevel =~ /(\d+)%?/; + $$self{_onlevel} = $onlevel; + } + return $$self{_onlevel}; +} + +=item C + +Sets and returns the local ramp rate for the device in MH only. Rate is a time +between .1 and 540 seconds. Only 32 rate steps exist, to MH will pick a time +equal to of the closest below this time. + +This setting can be pushed to the device using C. + +=cut + +sub local_ramprate +{ + my ($self, $p_ramprate) = @_; + if (defined $p_ramprate) { + $$self{_ramprate} = &Insteon::DimmableLight::convert_ramp($p_ramprate); + } + return $$self{_ramprate}; + +} + +=item C + +Pushes the values set in C and C to the device. +The device will only reread these values when it is power-cycled. This can be +done by pulling the air-gap for 4 seconds or unplugging the device. + +=cut + +sub update_local_properties +{ + my ($self) = @_; + $self->_aldb->update_local_properties() if $self->_aldb; +} + =item C Takes the p_level, and stores it as a numeric level in memory. If the p_level From d5b2e4c83152bb80f76822a070bf0274b4c9657f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 30 Jul 2013 19:13:27 -0700 Subject: [PATCH 127/330] Insteon: Supplement update_local_properties to handle I2 devices - Added necessary messages. - Relocated definition of extended_get_set to BaseObject as multiple objects use this - Added some better descriptions of the functions --- lib/Insteon/BaseInsteon.pm | 1 + lib/Insteon/Controller.pm | 3 +-- lib/Insteon/IOLinc.pm | 6 ------ lib/Insteon/Lighting.pm | 36 ++++++++++++++++++++++++++++++++++-- lib/Insteon/Security.pm | 6 ------ 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ff8e00599..5f5959a89 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1156,6 +1156,7 @@ our %message_types = ( peek => 0x2b, peek_internal => 0x2c, poke_internal => 0x2d, + extended_set_get => 0x2e, read_write_aldb => 0x2f, ); diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 573439dc9..20b081bcb 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -59,8 +59,7 @@ use Insteon::BaseInsteon; my %message_types = ( %Insteon::BaseDevice::message_types, bright => 0x15, - dim => 0x16, - extended_set_get => 0x2e + dim => 0x16 ); =item C diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index d5186fee7..3e0b44bff 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -122,11 +122,6 @@ my %operating_flags = ( 'momentary_c_off' => '15', ); -my %message_types = ( - %Insteon::BaseDevice::message_types, - extended_set_get => 0x2e -); - =item C Instantiates a new object. @@ -138,7 +133,6 @@ sub new my ($class, $p_deviceid, $p_interface) = @_; my $self = new Insteon::BaseDevice($p_deviceid, $p_interface); $$self{operating_flags} = \%operating_flags; - $$self{message_types} = \%message_types; bless $self, $class; $self->restore_data('momentary_time'); $$self{momentary_timer} = new Timer; diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index ca20c0322..a273ae971 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -225,6 +225,10 @@ percentage from 0%-100%. This setting can be pushed to the device using C. +Parameters: level [0-100] + +Returns: [0-100] + =cut sub local_onlevel @@ -246,6 +250,10 @@ equal to of the closest below this time. This setting can be pushed to the device using C. +Parameters: rate = ramp rate [.1s - 540s] see C for valid values + +Returns: hexadecimal representation of the ramprate. + =cut sub local_ramprate @@ -261,23 +269,47 @@ sub local_ramprate =item C Pushes the values set in C and C to the device. + +I1 Devices: + The device will only reread these values when it is power-cycled. This can be done by pulling the air-gap for 4 seconds or unplugging the device. +I2 & I2CS Devices + +The device will immediately read and update the values. + =cut sub update_local_properties { my ($self) = @_; - $self->_aldb->update_local_properties() if $self->_aldb; + if ($self->engine_version eq 'I1'){ + $self->_aldb->update_local_properties() if $self->_aldb; + } + else { + #Queue Ramp Rate First + my $extra = '000005' . $self->local_ramprate(); + $extra .= '0' x (30 - length $extra); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $self->_send_cmd($message); + + #Now queue on level + $extra = '000006' . ::Insteon::DimmableLight::convert_level($self->local_onlevel()); + $extra .= '0' x (30 - length $extra); + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $self->_send_cmd($message); + } } =item C -Takes the p_level, and stores it as a numeric level in memory. If the p_level +Stores and returns the objects current on_level as a percentage. If p_level is ON and the device has a defined local_onlevel, the local_onlevel is stored as the numeric level in memory. +Returns [0-100] + =cut sub level diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index 27f7d04f0..6f69695ff 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -110,11 +110,6 @@ use Insteon::BaseInsteon; @Insteon::MotionSensor::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); -my %message_types = ( - %Insteon::BaseDevice::message_types, - extended_set_get => 0x2e -); - =item C Instantiates a new object. @@ -126,7 +121,6 @@ sub new my ($class,$p_deviceid,$p_interface) = @_; my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); - $$self{message_types} = \%message_types; if ($self->is_root){ $self->restore_data('query_timer', 'last_query_time'); $$self{queue_timer} = new Timer; From 02ee1f22a2e30b26581a2a2e9f3df7b5126eead5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 30 Jul 2013 20:42:55 -0700 Subject: [PATCH 128/330] Insteon: Move update_flags, Condense KeypadLinc Classes, Add I2 Support to update_flags - Update flags belongs in the Keypadlinc class as it only works on these devices - Condensed the set routine from Keypadlinc and KeypadlincRelay into Keypadlinc - Added I2 support to update_flags - - In order to support the old version of update_flags, the manner in which the routine works is a little odd as users have to pass a hexadecimal flag to the routine. This could obviously be changed or supplemented in the future. --- lib/Insteon/BaseInsteon.pm | 30 ----------- lib/Insteon/Lighting.pm | 108 +++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 77 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 5f5959a89..33bf97d77 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1924,36 +1924,6 @@ sub log_alllink_table $self->_aldb->log_alllink_table if $self->_aldb; } -=item C - -Can be used to set the button layout and light level on a keypadlinc. Flag -options include: - - '0a' - 8 button; backlighting dim - '06' - 8 button; backlighting off - '02' - 8 button; backlighting normal - - '08' - 6 button; backlighting dim - '04' - 6 button; backlighting off - '00' - 6 button; backlighting normal - -Note: This routine will likely be moved to L at some point. - -=cut - -sub update_flags -{ - my ($self, $flags) = @_; - if (!($self->isa('Insteon::KeyPadLinc') or $self->isa('Insteon::KeyPadLincRelay'))) - { - &::print_log("[Insteon::BaseDevice] Operating flags may only be revised on keypadlincs!"); - return; - } - return unless defined $flags; - - $self->_aldb->update_flags($flags) if $self->_aldb; -} - =item C Sets or gets the device object engine version. If setting the engine version, diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index a273ae971..0a91dea56 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -712,6 +712,21 @@ use Insteon::BaseInsteon; @Insteon::KeyPadLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); +my %operating_flags = ( + 'program_lock_on' => '00', + 'program_lock_off' => '01', + 'led_on_during_tx' => '02', + 'led_off_during_tx' => '03', + 'resume_dim_on' => '04', + 'resume_dim_off' => '05', + '8_key_mode' => '06', + '6_key_mode' => '07', + 'led_off' => '08', + 'led_enabled' => '09', + 'key_beep_enabled' => '0a', + 'key_beep_off' => '0b' +); + =item C Instantiates a new object. @@ -762,6 +777,7 @@ sub set } else { + $link_state = $p_state if $self->can('level'); return $self->Insteon::DeviceController::set($link_state, $p_setby, $p_respond); } @@ -769,6 +785,50 @@ sub set } +=item C + +Can be used to set the button layout and light level on a keypadlinc. Flag +options include: + + '0a' - 8 button; backlighting dim + '06' - 8 button; backlighting off + '02' - 8 button; backlighting normal + + '08' - 6 button; backlighting dim + '04' - 6 button; backlighting off + '00' - 6 button; backlighting normal + +=cut + +sub update_flags +{ + my ($self, $flags) = @_; + return unless defined $flags; + if ($self->engine_version ne 'I1') { + $self->_aldb->update_flags($flags) if $self->_aldb; + } + else { + if ($flags & 0x02) { + $self->set_operating_flags('8_key_mode'); + } + else { + $self->set_operating_flags('6_key_mode'); + } + if ($flags & 0x04) { + $self->set_operating_flags('led_off'); + } + else { + $self->set_operating_flags('led_enabled'); + } + if ($flags & 0x08) { + $self->set_operating_flags('resume_dim_on'); + } + else { + $self->set_operating_flags('resume_dim_off'); + } + } +} + =back =head2 AUTHOR @@ -822,7 +882,7 @@ package Insteon::KeyPadLinc; use strict; use Insteon::BaseInsteon; -@Insteon::KeyPadLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController'); +@Insteon::KeyPadLinc::ISA = ('Insteon::KeyPadLincRelay', 'Insteon::DimmableLight','Insteon::DeviceController'); =item C @@ -833,57 +893,11 @@ Instantiates a new object. sub new { my ($class,$p_deviceid,$p_interface) = @_; - my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); bless $self,$class; return $self; } -=item C - -Handles setting and receiving states from the device and specifically its -subordinate buttons. - -NOTE: This could be merged somehow with the set() function in -C - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - if (!($self->is_root)) - { - my $rslt_code = $self->Insteon::BaseController::set($p_state, $p_setby, $p_respond); - return $rslt_code if $rslt_code; - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - if (ref $p_setby and $p_setby->isa('Insteon::BaseDevice')) - { - $self->Insteon::BaseObject::set($p_state, $p_setby, $p_respond); - } - elsif (ref $$self{surrogate} && ($$self{surrogate}->isa('Insteon::InterfaceController'))) - { - $$self{surrogate}->set($link_state, $p_setby, $p_respond) - unless ref $p_setby and $p_setby eq $self; - } - else - { - &::print_log("[Insteon::KeyPadLinc] You may not directly attempt to set a keypadlinc's button " - . "unless you have defined a reverse link with the \"surrogate\" keyword"); - } - } - else - { - return $self->Insteon::DeviceController::set($p_state, $p_setby, $p_respond); - } - - return 0; - -} - =back =head2 AUTHOR From a853372623d0b4f0bf2b58944001a0fcec2f4672 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 30 Jul 2013 21:23:21 -0700 Subject: [PATCH 129/330] Insteon: Fix Typos in Update_Flags Routine --- lib/Insteon/BaseInsteon.pm | 2 +- lib/Insteon/Lighting.pm | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 33bf97d77..5fc64a860 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -545,7 +545,7 @@ sub derive_message if (!(defined $p_extra)) { if ($command eq 'on') { - if ($self->isa('Insteon::BaseDevice') && defined $self->local_onlevel) { + if ($self->can('local_onlevel') && defined $self->local_onlevel) { $level = 2.55 * $self->local_onlevel; $command = 'on_fast'; } else { diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 0a91dea56..7945ea105 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -712,7 +712,7 @@ use Insteon::BaseInsteon; @Insteon::KeyPadLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); -my %operating_flags = ( +our %operating_flags = ( 'program_lock_on' => '00', 'program_lock_off' => '01', 'led_on_during_tx' => '02', @@ -736,8 +736,8 @@ Instantiates a new object. sub new { my ($class,$p_deviceid,$p_interface) = @_; - my $self = new Insteon::BaseLight($p_deviceid,$p_interface); + $$self{operating_flags} = \%operating_flags; bless $self,$class; return $self; } @@ -809,22 +809,22 @@ sub update_flags } else { if ($flags & 0x02) { - $self->set_operating_flags('8_key_mode'); + $self->set_operating_flag('8_key_mode'); } else { - $self->set_operating_flags('6_key_mode'); + $self->set_operating_flag('6_key_mode'); } if ($flags & 0x04) { - $self->set_operating_flags('led_off'); + $self->set_operating_flag('led_off'); } else { - $self->set_operating_flags('led_enabled'); + $self->set_operating_flag('led_enabled'); } if ($flags & 0x08) { - $self->set_operating_flags('resume_dim_on'); + $self->set_operating_flag('resume_dim_on'); } else { - $self->set_operating_flags('resume_dim_off'); + $self->set_operating_flag('resume_dim_off'); } } } @@ -894,6 +894,7 @@ sub new { my ($class,$p_deviceid,$p_interface) = @_; my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); + $$self{operating_flags} = \%Insteon::KeyPadLincRelay::operating_flags; bless $self,$class; return $self; } From 3abcc1474ce7ce4ed924362e0397ad6e243c736d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 30 Jul 2013 22:58:30 -0700 Subject: [PATCH 130/330] Insteon: Fix Typo in Update_Flags Logic --- lib/Insteon/Lighting.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 7945ea105..bcf81794e 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -804,7 +804,7 @@ sub update_flags { my ($self, $flags) = @_; return unless defined $flags; - if ($self->engine_version ne 'I1') { + if ($self->engine_version eq 'I1') { $self->_aldb->update_flags($flags) if $self->_aldb; } else { From 7173c3ae963aa9aab3e436ef3f0062a4cb8b9be8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 31 Jul 2013 16:48:15 -0700 Subject: [PATCH 131/330] Insteon_Data3: Change Default Data3 value for I2CS devices This may fix issue hollie/misterhouse#176 --- lib/Insteon/AllLinkDatabase.pm | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index eed61071a..8321d9c22 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1025,13 +1025,15 @@ sub add_link } my $is_controller = ($link_parms{is_controller}) ? 1 : 0; # check whether the link already exists - my $subaddress = ($link_parms{data3}) ? $link_parms{data3} : '00'; + # for I2CS devices the default data3 should be 01 no 00 + my $data3_default = ($$insteon_object->engine_version eq 'I2CS') ? '01' : '00'; + my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; # get the address via lookup into the hash my $key = lc $device_id . $group . $is_controller; # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - if (!($subaddress eq '00' or $subaddress eq '01')) + if (!($data3 eq '00' or $data3 eq '01')) { - $key .= $subaddress; + $key .= $data3; } if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ ## Check whether ALDB is in sync @@ -1094,7 +1096,7 @@ sub add_link $data1 = &Insteon::DimmableLight::convert_level($on_level); $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; } - my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; + #data3 is defined above $$self{_mem_activity} = 'add'; $self->_write_link($address, $device_id, $group, $is_controller, $data1, $data2, $data3); # TO-DO: ensure that pop'd address is restored back to queue if the transaction fails @@ -1147,7 +1149,9 @@ sub update_link . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $main::Debug{insteon}; my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; - my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; + # for I2CS devices the default data3 should be 01 no 00 + my $data3_default = ($$insteon_object->engine_version eq 'I2CS') ? '01' : '00'; + my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; my $deviceid = $insteon_object->device_id; my $subaddress = $data3; # get the address via lookup into the hash From a8c060e4e7b9ddd4d0e0f824f47ef4c6eb540ccb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 5 Aug 2013 18:45:40 -0700 Subject: [PATCH 132/330] Insteon_Voice_Cmds: Add On/Off Command to PLM Scene Controller Fix Bug found by Bill Dripps --- lib/Insteon/BaseInsteon.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 4b0b7918a..c0e5d0ef9 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3005,6 +3005,8 @@ sub get_voice_cmds my $group = $self->group; my %voice_cmds = ( %{$self->SUPER::get_voice_cmds}, + 'on' => "$object_name->set(\"on\")", + 'off' => "$object_name->set(\"off\")", 'initiate linking as controller' => "$object_name->initiate_linking_as_controller(\"$group\")", 'cancel linking' => "$object_name->interface()->cancel_linking" ); From 298002344f7a798cdc6ef8b74cc5353ba40d5622 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 7 Aug 2013 17:33:00 -0700 Subject: [PATCH 133/330] Do Not Move Tabbed 'Add' Entries in User Code to Global Declaration Misterhouse merges all of the User Code files into a single file at runtime. When merging the files, MisterHouse moves certain commands and declarations to the top of the merged file. The purpose is to allow these "global records" to be defined early on, to avoid errors. They include variable instantiaions and other routines. In order to avoid moving lines which are meant to be part of a subroutine or logical statement, MisterHouse will only move theabove "global records" if they occur at the start of a line. The thought being that coders practicing even the most basic level of standard formatting will indent statements inside of logical routines or subroutines. The one exception to this rule is the add statement. This is generally used to add an item to a group, but a few other items have add functions as well, notably the Process_Item. For some reason, MisterHouse will allow anywhere from 0-3 whitespaces to proceed the add statement while still considering it to be a "global record" This is somewhat logical in that tabs are usually at least 4 spaces. But it is unclear why this exception only applies to add functions, and not to the other functions as well. Eitherway, a white space in perl is defined as [\ \t\r\n\f]. [\r\n\f] are unnecessary, since they would be interpreted as a newline. The problem arose by including [\t]. In so doing, MisterHouse was moving add functions which were tabbed in 3 times. By changing this to simply [\ ] the code at least does what the author originally intended it to do. I think the odds that this change would cause an error for any currently existing user code files is small, but there is some remote possibility that it might. Personally, I think the spacing should be removed entirely, but I am hesitant to change a function that is so fundamental to MisterHouse. --- bin/mh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/mh b/bin/mh index 897c75c0d..8ad7f76b6 100755 --- a/bin/mh +++ b/bin/mh @@ -4963,8 +4963,8 @@ sub read_user_code { $record =~ /->\s*hidden/ or $record =~ /^\s*set_(authority|icon|info|order|fp_location)\s/ or $record =~ /->\s*set_(authority|icon|info|order|fp_location)/ or - $record =~ /^\s{0,3}\S+\s*->\s*add[\s\(]/ or - $record =~ /^\s{0,3}add\s/) { + $record =~ /^\ {0,3}\S+\s*->\s*add[\s\(]/ or + $record =~ /^\ {0,3}add\s/) { $noloop_statement_flag = 1 unless $pod_flag; # Allow for multi-record statements } From b96eeee084cedde14c9ec38ef91ac236872c60b9 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 9 Aug 2013 16:07:13 -0700 Subject: [PATCH 134/330] Insteon_Stats: Add Voice Commands --- lib/Insteon/BaseInsteon.pm | 2 ++ lib/Insteon/BaseInterface.pm | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 5593de262..ea17ae646 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2457,6 +2457,8 @@ sub get_voice_cmds 'status' => "$object_name->request_status", 'get engine version' => "$object_name->get_engine_version", 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", + 'print message stats' => "$object_name->print_message_stats", + 'reset message stats' => "$object_name->reset_message_stats", 'log links' => "$object_name->log_alllink_table()" ) } diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 10f3ad6ef..79519a1f2 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -905,6 +905,8 @@ sub get_voice_cmds 'scan all device link tables' => "Insteon::scan_all_linktables", 'sync all links' => "Insteon::sync_all_links(0)", 'AUDIT - sync all links' => "Insteon::sync_all_links(1)", + 'print all message stats' => "Insteon::print_all_message_stats", + 'reset all message stats' => "Insteon::reset_all_message_stats", 'log all device ALDB status' => "Insteon::log_all_ADLB_status" ); return \%voice_cmds; From a8821e8df0a501f848ec1d43eeac347fbe998770 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 9 Aug 2013 16:48:10 -0700 Subject: [PATCH 135/330] Insteon_Diags: Add voice commands and POD notes --- lib/Insteon.pm | 65 ++++++++++++++++++++++++++++++------ lib/Insteon/BaseInsteon.pm | 2 ++ lib/Insteon/BaseInterface.pm | 2 ++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index e8598ebb5..fab4d6363 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -107,6 +107,25 @@ See the workflow described in C. Same as C but prints what it would do to the log, without doing anything else. +=item C + +Prints the message stats for all devices plus a summary of the entire network +stats. + +=item C + +Resets all the message stats for all devices including the Unk_Error stat. + +=item C + +Performs a 5 count stress test on all devices on the network. See the description +of what a stress test is under the device voice commands. + +=item C + +Performs a 5 count ping test on all devices on the network. See the description +of what a ping test is under the device voice commands. + =item C Logs some details about each device to the log. See C @@ -117,55 +136,79 @@ Logs some details about each device to the log. See C =over -=item on +=item C Turns the device on. -=item off +=item C Turns the device off. -=item Sync Links +=item C Similar to C above, but this will only add links that are related to this device. Useful when adding a new device. -=item Link to Interface +=item C Will create the controller/responder links between the device and the PLM. -=item Unlink with Interface +=item C Will delete the controller/responder links between the device and the PLM. Useful if you are removing a device from your network. -=item Status +=item C Requests the status of the device. -=item Get Engine Version +=item C Requests the engine version of the device. Generally you would not need to call this, but every now and then it is needed when a new device is installed. -=item Scan Link Table +=item C This will scan and output to the log only the link table of this device. -=item Log Links +=item C Will output to the log only the link table of this device. -=item Initiate Linking as Controller +=item C Generally only available for PLM Scenes. This places the PLM in linking mode and adds any device which the set button is pressed for 4 seconds as a responder to this scene. Generally not needed. -=item Cancel Linking +=item C Cancels the above linking session without creating a link. +=item C + +Simulates a read of a 5 link addresses from the device. This routine is meant to +be used as a diagnostic tool. + +This is also similar to the C test, however, rather than simply requesting +an ACK, this requests a full set of data equivalent to a link entry. Similar to +C this should be used with C to diagnose issues and try +different settings. + +=item C + +Sends 5 ping messages to the device. A ping message is a basic +message that simply asks the device to respond with an ACKnowledgement. For +i1 devices this will send a standard length command, for i2 and i2cs devices +this will send an extended ping command. In both cases, the device responds +back with a standard length ACKnowledgement only. + +Much like the ping command in IP networks, this command is useful for testing the +connectivity of a device on your network. You likely want to use this in +conjunction with the C routine. For example, you can use +this to compare the message stats for a device when changing settings in +MisterHouse. + =back =head2 METHODS diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ecf4ad278..b92df149e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2534,6 +2534,8 @@ sub get_voice_cmds 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", 'print message stats' => "$object_name->print_message_stats", 'reset message stats' => "$object_name->reset_message_stats", + 'run stress test' => "$object_name->stress_test(5)", + 'run ping test' => "$object_name->ping(5)", 'log links' => "$object_name->log_alllink_table()" ) } diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 30b00de14..e562aa763 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -903,6 +903,8 @@ sub get_voice_cmds 'AUDIT - sync all links' => "Insteon::sync_all_links(1)", 'print all message stats' => "Insteon::print_all_message_stats", 'reset all message stats' => "Insteon::reset_all_message_stats", + 'stress test ALL devices' => "Insteon::stress_test_all(5,1)", + 'ping test ALL devices' => "Insteon::ping_all(5)", 'log all device ALDB status' => "Insteon::log_all_ADLB_status" ); return \%voice_cmds; From b94b81b977e84341d88f80f5af9fba79a8f63e94 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 9 Aug 2013 16:53:52 -0700 Subject: [PATCH 136/330] Insteon_Diags: Add a few more POD notes --- lib/Insteon.pm | 93 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index fab4d6363..fa01d60d6 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -110,7 +110,9 @@ anything else. =item C Prints the message stats for all devices plus a summary of the entire network -stats. +stats. For full details on what is contained in this printout please see +the description for the C voice command under the device +heading below. =item C @@ -209,6 +211,95 @@ conjunction with the C routine. For example, you can use this to compare the message stats for a device when changing settings in MisterHouse. +=item C + +Prints message statistics for this device to the print log. + +=back + +=over8 + +=item * + +In - The number of incoming messages received + +=item * + +Corrupt - The number of incoming corrupt messages received + +=item * + +%Corrpt - Of the incoming messages received, the percentage that were +corrupt + +=item * + +Dupe - The number of duplicate messages that have been received from this +device. + +=item * + +%Dupe - The percentage of duplilicate incoming messages received. + +=item * + +Hops_Left - The average hops left in the messages received from this device. + +=item * + +Max_Hops - The average maximum hops in the messages received from this device. + +=item * + +Act_Hops - Max_Hops - Hops_Left, this is the average number of hops that have +been required for a message sent from the device to reach MisterHouse. + +=item * + +Out - The number of unique outgoing messages, without retries, sent. + +=item * + +Fail - The number times that all retries were exhausted without a successful +delivery of a message. + +=item * + +%Fail - Of the outgoing messages sent, the percentage that failed. + +=item * + +Retry - The number of retry attempts that have been made to deliver a message. +Ideally this is 0, but Sends/Msg is a better indication of this parameter. + +=item * + +AvgSend - The average number of send attempts that must be made in order to +successfully deliver a message. Ideally this would be 1.0. + +NOTE: If the number of retries exceeds the value set in the configuration file +for Insteon_retry_count, MisterHouse will abandon sending the message. As a +result, as this number approaches Insteon_retry_count it becomes a less accurate +representation of the number of retries needed to reach a device. + +=item * + +Avg_Hops - The average number of hops that have been used by MisterHouse when +sending messages to this device. + +=item * + +Hop_Count - The current hop count being used by MH. This count is dynamically +controlled by MH and is not reset by calling C + +=back + +=over + +=item C + +Resets the message stats back to 0 for this device. + =back =head2 METHODS From 2d998fdac221a0fef954627ba164eef54ec0a602 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 12 Aug 2013 19:20:04 -0700 Subject: [PATCH 137/330] Insteon_Diags: Make Sure Setby is an Object Fix bug identified by @JaredF --- lib/Insteon/Message.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 31d48510a..70ea818f3 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -218,11 +218,11 @@ sub send &::print_log("[Insteon::BaseMessage] WARN: now resending " . $self->to_string() . " after " . $self->send_attempts . " attempts.") if $main::Debug{insteon}; - $self->setby->retry_count_log(1) if $self->setby->can('retry_count_log'); # revise default hop count to reflect retries if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') && !defined($$self{no_hop_increase})) { + $self->setby->retry_count_log(1) if $self->setby->can('retry_count_log'); if ($self->setby->default_hop_count < 3) { $self->setby->default_hop_count($self->setby->default_hop_count + 1); @@ -238,7 +238,7 @@ sub send } # need to set timeout as a function of retries; also need to alter hop count - if ($self->send_attempts <= 0) { + if ($self->send_attempts <= 0 && ref $self->setby) { $self->setby->outgoing_count_log(1) if $self->setby->can('outgoing_count_log'); $self->setby->outgoing_hop_count($self->setby->default_hop_count) if $self->setby->can('outgoing_hop_count'); From ce0b2a85f44c12d9c10438e4247ef47be11e591e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 27 Aug 2013 17:33:00 -0700 Subject: [PATCH 138/330] Insteon_Thermo: Convert i2 Entries to i2CS Some of the older thermostat devices including the 2441v venstar device were i2 devices but do not support the newer thermostat code. As such the distinction between the old and new devices is not i1 vs i2 but rather i2CS or not i2CS. --- lib/Insteon.pm | 6 ++-- lib/Insteon/Thermostat.pm | 72 +++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 166d5e61a..4cd68c02f 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -565,11 +565,11 @@ sub check_thermo_versions foreach my $thermo_device (@thermo_devices) { if ($thermo_device->isa('Insteon::Thermostat') && - $thermo_device->_aldb->aldb_version() eq "I2"){ + $thermo_device->_aldb->aldb_version() eq "I2CS"){ main::print_log("[Insteon] DEBUG4 Setting thermostat " - . $thermo_device->get_object_name() . " to i2") + . $thermo_device->get_object_name() . " to i2CS") if ($main::Debug{insteon} >= 4); - bless $thermo_device, 'Insteon::Thermo_i2'; + bless $thermo_device, 'Insteon::Thermo_i2CS'; $thermo_device->init(); } elsif ($thermo_device->isa('Insteon::Thermostat') diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 251ceb2ff..5ed8c940c 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -12,7 +12,7 @@ In user code: $thermostat = new Insteon_Thermostat($myPLM, '12.34.56'); -Additional i2 specific objects: +Additional i2CS specific objects: $thermostat_heating = new Insteon_Thermostat($myPLM, '12.34.56:02'); $thermostat_high_humid = new Insteon_Thermostat($myPLM, '12.34.56:03'); @@ -25,7 +25,7 @@ In items.mht: INSTEON_THERMOSTAT, 12.34.56, thermostat, HVAC -Additional i2 specific objects: +Additional i2CS specific objects: INSTEON_THERMOSTAT, 12.34.56:02, thermostat_heating, HVAC INSTEON_THERMOSTAT, 12.34.56:03, thermostat_high_humid, HVAC @@ -67,10 +67,10 @@ to link specific actions to these states: (call get_mode() to get value). fan_mode_change: Fan mode changed (call get_fan_mode() to get value). - status_change: Heating, Cooling, Dehumidifying, or Humidifying change (i2 only) + status_change: Heating, Cooling, Dehumidifying, or Humidifying change (i2CS only) (call get_status() to get status). -I2 Broadcast messages: +I2CS Broadcast messages: If a group EF device is defined, MH will receive broadcast changes from the thermostat. When enabled, broadcast messages for changes in setpoint, mode, @@ -90,7 +90,7 @@ Linking: I am not sure how or if the i1 device can be linked to other devices. -I2 devices have 5 controllers, groups 01-04 plus the broadcast group EF. At the +I2CS devices have 5 controllers, groups 01-04 plus the broadcast group EF. At the moment, MH only supports using the thermostat as a controller of another device. To control another device, simply define it as a scene member of the desired thermostat group. The groups are: @@ -106,7 +106,7 @@ thermostat group. The groups are: Tracking Child Objects: -For both, i1 and i2 devices, optional child objects which track the states of the +For both, i1 and i2CS devices, optional child objects which track the states of the thermostat can be created in user code: $thermo_temp = new Insteon::Thermo_temp($thermostat); @@ -114,8 +114,8 @@ thermostat can be created in user code: $thermo_mode = new Insteon::Thermo_mode($thermostat); $thermo_setpoint_h = new Insteon::Thermo_setpoint_h($thermostat); $thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); - $thermo_humidity = new Insteon::Thermo_humidity($thermostat); #Only available on i2 devices - $thermo_status = new Insteon::Thermo_status($thermostat); #Only available on i2 devices + $thermo_humidity = new Insteon::Thermo_humidity($thermostat); #Only available on i2CS devices + $thermo_status = new Insteon::Thermo_status($thermostat); #Only available on i2CS devices where $thermostat is the parent object to track. The state of these child objects will be the state of the various objects. This makes the display of the various @@ -135,7 +135,7 @@ Initial Code by: Gregg Liming Brian Warren -Enhanced to i2 by: +Enhanced to i2CS by: Kevin Robert Keegan =head1 TODO @@ -349,7 +349,7 @@ sub _mode() { =item C -Returns the last mode returned by C I2 devices will report auto for both auto and program_auto. +Returns the last mode returned by C I2CS devices will report auto for both auto and program_auto. =cut sub get_mode() { my ($self) = @_; @@ -556,10 +556,10 @@ sub simple_message { return $message; } -package Insteon::Thermo_i2; +package Insteon::Thermo_i2CS; use strict; -@Insteon::Thermo_i2::ISA = ('Insteon::Thermostat'); +@Insteon::Thermo_i2CS::ISA = ('Insteon::Thermostat'); our %message_types = ( %Insteon::Thermostat::message_types, @@ -574,7 +574,7 @@ our %message_types = ( sub init { my ($self) = @_; $$self{message_types} = \%message_types; - #Set saved state unique to i2 devices + #Set saved state unique to i2CS devices $self->restore_data('humid', 'cooling', 'heating', 'high_humid', 'low_humid'); } @@ -582,7 +582,7 @@ sub set { my ($self, $p_state, $p_setby, $p_respond) = @_; my $root = $self->get_root(); if (!(ref $p_setby) || !($p_setby->equals($self))) { - ::print_log("[Insteon::Thermo_i2] Sorry, you cannot control the ". + ::print_log("[Insteon::Thermo_i2CS] Sorry, you cannot control the ". "thermostat in this manner. Please read the documentation ". "for Insteon::Thermostat for help."); return; @@ -611,7 +611,7 @@ sub sync_links{ my $bcast_obj = Insteon::get_object($self->device_id(), 'EF'); if (!$audit_mode && ref $bcast_obj && $self->is_root){ #Make sure thermostat is set to broadcast changes - ::print_log("[Insteon::Thermo_i2] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; + ::print_log("[Insteon::Thermo_i2CS] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; my $extra = "000008000000000000000000000000"; my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); $$self{_ext_set_get_action} = 'set'; @@ -624,7 +624,7 @@ sub sync_links{ =item C Requests the status of all Thermostat data points (temp, fan, mode ...) in a single -request. Only available for I2 devices. +request. Only available for I2CS devices. =cut sub poll_simple{ my ($self) = @_; @@ -638,7 +638,7 @@ sub poll_simple{ Returns a text string describing the current status of the thermostat. May include a combination of "Heating; Cooling; Dehumidifying; Humidifying; or Off." Only -available for I2 devices. +available for I2CS devices. =cut sub get_status() { @@ -666,9 +666,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::Thermo_i2] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::Thermo_i2] Clearing active message") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Clearing active message") if $main::Debug{insteon}; $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -677,7 +677,7 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { if (substr($msg{extra},0,4) eq "0201") { $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Extended Set/Get Data ". + main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get Data ". "Received for ". $self->get_object_name) if $main::Debug{insteon}; #0 = 2 #14 = Cool SP #2 = 1 #16 = humidity @@ -728,7 +728,7 @@ sub _process_message { #10 = humid high #24 = 1 = Status Report Enabled #12 = firmware #26 = 1 = External Power On #28 = 1 = Int, 2=Ext Temp - main::print_log("[Insteon::Thermo_i2] Humidity setpoints for ". + main::print_log("[Insteon::Thermo_i2CS] Humidity setpoints for ". $self->get_object_name . " are High: " . $self->_high_humid(hex(substr($msg{extra}, 8, 2))) . " Low: " . $self->_low_humid(hex(substr($msg{extra}, 10, 2))) @@ -737,37 +737,37 @@ sub _process_message { $self->_process_command_stack(%msg); } else { - main::print_log("[Insteon::Thermo_i2] WARN: Unknown Extended " + main::print_log("[Insteon::Thermo_i2CS] WARN: Unknown Extended " ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; } } elsif ($msg{command} eq "status_temp" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Temp Change Message ". + main::print_log("[Insteon::Thermo_i2CS] Received Temp Change Message ". "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_short_temp($msg{extra}); } elsif ($msg{command} eq "status_mode" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Mode Change Message ". + main::print_log("[Insteon::Thermo_i2CS] Received Mode Change Message ". "from ". $self->get_object_name) if $main::Debug{insteon}; $self->status_mode($msg{extra}); } elsif ($msg{command} eq "status_cool" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Cool Setpoint Change Message ". + main::print_log("[Insteon::Thermo_i2CS] Received Cool Setpoint Change Message ". "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_cool($msg{extra}); } elsif ($msg{command} eq "status_humid" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Humidity Change Message ". + main::print_log("[Insteon::Thermo_i2CS] Received Humidity Change Message ". "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_humid($msg{extra}); } elsif ($msg{command} eq "status_heat" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::Thermo_i2] Received Heat Setpoint Change Message ". + main::print_log("[Insteon::Thermo_i2CS] Received Heat Setpoint Change Message ". "from ". $self->get_object_name) if $main::Debug{insteon}; $self->hex_heat($msg{extra}); } @@ -782,7 +782,7 @@ sub _is_info_request { my $is_info_request; if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { my $val = $msg{extra}; - main::print_log("[Insteon::Thermo_i2] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; if ($val eq '09') { $self->_mode('Off'); } elsif ($val eq '04') { @@ -797,7 +797,7 @@ sub _is_info_request { $$self{_control_action} = undef; $is_info_request = 1; } - else #This was not a thermo_i2 info_request + else #This was not a thermo_i2CS info_request { #Check if this was a generic info_request $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); @@ -974,7 +974,7 @@ Sets the high humidity setpoint. =cut sub high_humid_setpoint { my ($self, $value) = @_; - main::print_log("[Insteon::Thermo_i2] Setting high humid setpoint -> $value") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Setting high humid setpoint -> $value") if $main::Debug{insteon}; if($value !~ /^\d+$/){ main::print_log("[Insteon::Thermostat] ERROR: Setpoint $value not numeric"); return; @@ -993,7 +993,7 @@ Sets the low humidity setpoint. =cut sub low_humid_setpoint { my ($self, $value) = @_; - main::print_log("[Insteon::Thermo_i2] Setting low humid setpoint -> $value") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Setting low humid setpoint -> $value") if $main::Debug{insteon}; if($value !~ /^\d+$/){ main::print_log("[Insteon::Thermostat] ERROR: Setpoint $value not numeric"); return; @@ -1007,7 +1007,7 @@ sub low_humid_setpoint { =item C -Retreives and prints the current humidity high and low setpoints. Only available for I2 devices. +Retreives and prints the current humidity high and low setpoints. Only available for I2CS devices. =cut sub get_humid_setpoints{ my ($self) = @_; @@ -1043,7 +1043,7 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received set mode request to " + ::print_log("[Insteon::Thermo_i2CS] Received set mode request to " . $p_state . " for device " . $self->get_object_name); $$self{parent}->mode($p_state); } @@ -1079,7 +1079,7 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received set fan to " + ::print_log("[Insteon::Thermo_i2CS] Received set fan to " . $p_state . " for device " . $self->get_object_name); $$self{parent}->fan($p_state); } @@ -1155,7 +1155,7 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received request to set heat setpoint " + ::print_log("[Insteon::Thermo_i2CS] Received request to set heat setpoint " . $p_state . " for device " . $self->get_object_name); if (lc($p_state) eq 'cooler'){ $$self{parent}->heat_setpoint($$self{parent}->get_heat_sp - 1); @@ -1196,7 +1196,7 @@ sub set { } } if ($found_state){ - ::print_log("[Insteon::Thermo_i2] Received request to set cool setpoint " + ::print_log("[Insteon::Thermo_i2CS] Received request to set cool setpoint " . $p_state . " for device " . $self->get_object_name); if (lc($p_state) eq 'cooler'){ $$self{parent}->cool_setpoint($$self{parent}->get_cool_sp - 1); From cd001a4e2c6849ba1445b1f3b902601413bb2b17 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 27 Aug 2013 17:38:00 -0700 Subject: [PATCH 139/330] Insteon_Thermo: Check Engine Version of Root Device not ALDB Version The ALDB can only be i1 or i2, need to be checking the engine version instead. --- lib/Insteon.pm | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 4cd68c02f..c97b2cdee 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -558,24 +558,23 @@ sub check_all_aldb_versions sub check_thermo_versions { - #main::print_log("[Insteon] DEBUG4 Checking thermostat versions") if ($main::Debug{insteon} >= 4); + main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if ($main::Debug{insteon} >= 4); my @thermo_devices = (); push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); foreach my $thermo_device (@thermo_devices) { if ($thermo_device->isa('Insteon::Thermostat') && - $thermo_device->_aldb->aldb_version() eq "I2CS"){ + $thermo_device->get_root()->engine_version eq "I2CS"){ main::print_log("[Insteon] DEBUG4 Setting thermostat " . $thermo_device->get_object_name() . " to i2CS") if ($main::Debug{insteon} >= 4); bless $thermo_device, 'Insteon::Thermo_i2CS'; $thermo_device->init(); } - elsif ($thermo_device->isa('Insteon::Thermostat') - && $thermo_device->_aldb->aldb_version() eq "I1"){ + else { main::print_log("[Insteon] DEBUG4 Setting thermostat " - . $thermo_device->get_object_name() . " to i2") + . $thermo_device->get_object_name() . " to i1") if ($main::Debug{insteon} >= 4); bless $thermo_device, 'Insteon::Thermo_i1'; } From e1d45644d76d721b67fc1196d7f430722fc65e7b Mon Sep 17 00:00:00 2001 From: surge919 Date: Fri, 30 Aug 2013 09:53:46 -0400 Subject: [PATCH 140/330] Sends Notification messages to NMA - Notify My Android --- code/common/nma_notification.pl | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 code/common/nma_notification.pl diff --git a/code/common/nma_notification.pl b/code/common/nma_notification.pl new file mode 100644 index 000000000..144161f31 --- /dev/null +++ b/code/common/nma_notification.pl @@ -0,0 +1,53 @@ +# Category=Home_Network +# +#@ Sends Notification messages to NMA - Notify My Android + +# The following config parameters should be in mh.private.ini or mh.ini +# +# nma_api_key= (API Key from NotifyMyAndroid) + +use LWP::UserAgent; + +# noloop=start +my $nma_api_key = $config_parms{nma_api_key}; +# noloop=stop + + +#Tell MH to call our routine each time something is spoken +&Speak_pre_add_hook(\&nma_notify,1) if $Reload; + +# Notify the on startup / restart +if ($Startup) { + print_log("System Restarted, Notifying NMA -- using api key: ", $nma_api_key ); + nma_notify_b("$nma_api_key", "Misterhouse has been restarted"); +} + +sub nma_notify() { + my %parms = @_; + print "NMA message sent"; + print "----------------"; + nma_notify_b("$nma_api_key", "$parms{text}"); + return; +} + +sub nma_notify_b{ + my ($nma_api_key, $text) = @_; + + print "NMA message sent"; + + # syntax to send a message from a browser or curl -- ie: for testing + # https://www.notifymyandroid.com/publicapi/notify?apikey=&application=&event=&description=&priority=0 + + # NMA allows you to print to 3 lines + # I prefer printing to the main line which is listed as application in the below url. This works great fot GTV since the event pops up over whatever you are watching + my $url = 'https://www.notifymyandroid.com/publicapi/notify?apikey='.$nma_api_key.'&application='.$text.'&event=&description=&priority=0'; + + # If you prefer the main line to list MisterHouse and have your event appear on the second line, uncomment the following line and comment out the line above this + # my $url = 'https://www.notifymyandroid.com/publicapi/notify?apikey='.$nma_api_key.'&application=MisterHouse&event='.$text.'&description=&priority=0'; + + # Change spaces to HTML space codes + $url =~ s/ /%20/g; + my $ua = new LWP::UserAgent; + my $req = new HTTP::Request GET => $url; + my $res = $ua->request($req); +} From 7ea6d0aa0c6a59f9ca590fadbce683467abbffb1 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 22 Sep 2013 23:38:25 -0400 Subject: [PATCH 141/330] First creation of SCENE_SIMPLE, tested and working --- bin/mh | 4 ++++ lib/read_table_A.pl | 55 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/bin/mh b/bin/mh index 897c75c0d..b4b078c70 100755 --- a/bin/mh +++ b/bin/mh @@ -4775,6 +4775,10 @@ sub read_table_files { print "Error in &read_table_$format: $@\n" if $@; } } + if (defined(&{"read_table_finish_$format"})) { + print TABLE_OUT eval "&read_table_finish_$format()"; + print "Error in &read_table_finish_$format: $@\n" if $@; + } close TABLE_IN; close TABLE_OUT; if ($format =~/xml/) { $/="\n"; } diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 93ca9a499..29688fe2b 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -14,7 +14,7 @@ #print_log "Using read_table_A.pl"; -my (%groups, %objects, %packages, %addresses); +my (%groups, %objects, %packages, %addresses, %scenes); sub read_table_init_A { # reset known groups @@ -23,6 +23,7 @@ sub read_table_init_A { %objects=(); %packages=(); %addresses=(); + %scenes=(); } sub read_table_A { @@ -992,6 +993,17 @@ sub read_table_A { } $object = ''; } + elsif($type eq "SCENE_SIMPLE") { + #SCENE_SIMPLE, scene_name, scene_member, controller?, responder?, onlevel, ramprate + my ($scene_member, $scene_controller, $scene_responder, $on_level, $ramp_rate); + ($name, $scene_member, $scene_controller, $scene_responder, $on_level, $ramp_rate) = @item_info; + if( ! $packages{Scene}++ ) { # first time for this object type? + $code .= "use Scene;\n"; + } + $scenes{$name}{$scene_member}="$scene_controller,$scene_responder,$on_level,$ramp_rate"; + $code .= sprintf "#SCENE_SIMPLE: \$%-35s -> add(\$%s);\n", $name, $scene_member; + $object = ''; + } elsif ($type eq "PHILIPS_HUE"){ ($address, $name, $grouplist, @other) = @item_info; $other = join ', ', (map {"'$_'"} @other); # Quote data @@ -1047,6 +1059,47 @@ sub read_table_A { return $code; } +sub read_table_finish_A { + my ($code, $scene, $scene_member, %scene_data, $member_data); + foreach $scene (sort keys %scenes) { + $code .= "\n#SCENE DEFINITION: $scene\n"; + #my $object = &get_object_by_name($scene); + #if(defined($object) and $object->isa("Insteon::InterfaceController")) { #Doesn't work because object technically doesn't exist yet. Is it even necessary to limit to ICONTROLLER's? +# print "\n\nOBJECT: $objects{$scene}\n\n\n"; + if($objects{$scene}) { + $scenes{$scene}{$scene}="1,0"; #Make the INSTEON_ICONTROLLER a controller too + } + #Loop through the hash and find all controller->responder combinations that make sense. + my %scenememberlist=%{$scenes{$scene}}; #Make a hash copy so we can iterate through the hash inside another iteration of the same hash. Is there a better way? + while (($scene_member, $member_data) = each($scenes{$scene})) { + my ($scene_controller, $scene_responder) = split(',', $member_data); + + if (($objects{$scene_member}) and ($scene_controller)) { + while (my($scene2_member, $member2_data) = each(%scenememberlist)) { + my ($scene2_controller, $scene2_responder, $on_level, $ramp_rate) = split(',', $member2_data); + + if (($objects{$scene2_member}) and ($scene2_responder) and ($scene_member ne $scene2_member)) { + if ($on_level) { + if ($ramp_rate) { + $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", + $scene_member, $scene2_member, $on_level, $ramp_rate; + } else { + $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", $scene_member, $scene2_member, $on_level; + } + } else { + $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_member, $scene2_member; + } + } + } + + } else { + print "\nThere is no object called $scene_member defined. Ignoring SCENE_SIMPLE entry.\n" unless $objects{$scene_member}; + } + } + } + return $code; +} + 1; # From 9d8050fd62e05113bd704b13cad910bbcc8c25e4 Mon Sep 17 00:00:00 2001 From: Steve Switzer Date: Mon, 23 Sep 2013 00:05:46 -0400 Subject: [PATCH 142/330] Add some clarity to the I2CS device is not linked error message, teling us WHICH device. --- lib/Insteon/BaseInsteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index b92df149e..7e857a6d3 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1802,7 +1802,7 @@ sub _get_engine_version_failure if($failure_reason eq 'NAK') { #assume I2CS because no other device will NAK this command - main::print_log("[Insteon::BaseDevice] WARN: I2CS device is not " + main::print_log("[Insteon::BaseDevice] WARN: I2CS device (" . $self->get_object_name . ") is not " ."linked; Please use 'link to interface' voice command"); $self->engine_version('I2CS'); } From 9992f80e42b41e87f315f8c780735bcee46d562f Mon Sep 17 00:00:00 2001 From: Steve Switzer Date: Mon, 23 Sep 2013 00:34:01 -0400 Subject: [PATCH 143/330] Allow MH to have a master volume that controls the relative volume of each speak or play event. --- code/common/mh_sound.pl | 105 +++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/code/common/mh_sound.pl b/code/common/mh_sound.pl index 8304c0f75..c6a33e735 100644 --- a/code/common/mh_sound.pl +++ b/code/common/mh_sound.pl @@ -19,12 +19,17 @@ $mh_volume = new Generic_Item; $mh_speakers = new Generic_Item; $mh_speakers_timer = new Timer; -$Info{Volume_Control} = 'Command Line' if $Reload and $config_parms{volume_get_cmd} and $config_parms{volume_set_cmd}; +$Info{Volume_Control} = 'Command Line' if $Reload and $config_parms{volume_master_get_cmd} and $config_parms{volume_master_set_cmd}; # Allow for default volume control. Reset on startup. -&set_volume3($mh_volume->{state}) if $Startup and defined $mh_volume->{state}; #noloop +&set_volume_master_wrapper($mh_volume->{state}) if $Startup and defined $mh_volume->{state}; #noloop +&set_volume_wav($config_parms{volume_wav_default_volume}) if $Startup and defined $config_parms{volume_wav_default_volume}; #noloop + +if (defined($state = state_now $mh_volume) and $state ne '') { + &set_volume_master_wrapper($state); +} $Tk_objects{sliders}{volume} = &tk_scalebar(\$mh_volume, 0, 'Volume') if $MW and $Reload and $Run_Members{mh_sound}; @@ -66,9 +71,15 @@ sub put_volume_back { # MSv5 has nothing to do with the mixer - if (defined $volume_previous and ($Voice_Text::VTxt_version ne 'msv5' or $wav_did_it)) { - print_log("Putting volume back to $volume_previous"); - set_volume2($volume_previous); + if (defined $config_parms{volume_wav_default_volume} and ($Voice_Text::VTxt_version ne 'msv5' or $wav_did_it)) { + print_log("Putting wav volume back to $config_parms{volume_wav_default_volume}"); + &set_volume_wav($config_parms{volume_wav_default_volume}); + + if ($volume_master_changed) { + $volume_master_changed=0; + &set_volume_master_wrapper($mh_volume->{state}); + } + } } @@ -77,6 +88,7 @@ sub put_volume_back { if (!$is_speaking_flag and $is_speaking) { # print_log 'Speakers on'; $is_speaking_flag = 1; + print_log "Setting speakers ON"; set $mh_speakers ON; # The following has no effect :( # &Voice_Cmd::deactivate if $OS_win; # So mh does not listen to itself @@ -84,7 +96,7 @@ sub put_volume_back { if ($is_speaking_flag and !$is_speaking) { # *** v5 has nothing to do with the mixer - print_log "Speakers off, volume reset to $volume_previous" if defined $volume_previous and $Voice_Text::VTxt_version ne 'msv5'; + print_log "Speakers off, volume reset to $volume_wav_previous" if defined $volume_wav_previous and $Voice_Text::VTxt_version ne 'msv5'; &put_volume_back(); } @@ -105,7 +117,18 @@ sub put_volume_back { $Tk_objects{volume_status}->configure(-value => $mh_volume->{state}) if $Tk_objects{volume_status} and (state_now $mh_volume); -sub set_volume3 { + +sub set_volume_master { + my ($volume) = @_; + + if ($Info{Volume_Control} eq 'Command Line') { + my $volume_cmd=$config_parms{volume_master_set_cmd}; + print_log eval qq("$volume_cmd"); + my $r = system eval qq("$volume_cmd"); + } +} + +sub set_volume_master_wrapper { my $state = shift; if (!$Info{Volume_Control}) { print_log "Volume control not enabled"; @@ -116,32 +139,45 @@ sub set_volume3 { set $mh_volume 100; } else { - print_log "Setting mixer volume to $state"; - set_volume2($state); + print_log "Setting master volume to $state"; + &set_volume_master($state); } $Tk_objects{volume_status}->configure(-value => $state) if $Tk_objects{volume_status}; } - -if (defined($state = state_now $mh_volume) and $state ne '') { - &set_volume3($state); +sub set_volume_wav { + my ($volume) = @_; + my $volume_wav_previous; + if ($Info{Volume_Control} eq 'Command Line') { + print_log "$config_parms{volume_wav_get_cmd}"; + $volume_wav_previous = `$config_parms{volume_wav_get_cmd}`; + chomp $volume_wav_previous; + my $volume_cmd=$config_parms{volume_wav_set_cmd}; + print_log eval qq("$volume_cmd"); + my $r = system eval qq("$volume_cmd"); + } + print_log "Previous wav volume was $volume_wav_previous"; + #return $volume_wav_previous; } + # Set hooks so set_volume is called whenever speak or play is called -&Speak_pre_add_hook(\&set_volume) if $Reload; -&Play_pre_add_hook (\&set_volume) if $Reload; +&Speak_pre_add_hook(\&set_volume_pre_hook) if $Reload; +&Play_pre_add_hook (\&set_volume_pre_hook) if $Reload; #noloop=start -my $volume_previous; +my $volume_master_changed=0; +my $volume_wav_previous; #noloop=stop -sub set_volume { +sub set_volume_pre_hook { + print_log "FUNCTION: set_volume_pre_hook"; return if $is_speaking and $Voice_Text::VTxt_version ne 'msv5'; # Speaking volume wins over play volume (unless using MSv5!) return unless $Info{Volume_Control}; # Verify we have a volume control module installed my %parms = @_; # msv5 changes volume with xml tags in lib/Voice_Text.pm return if $parms{text} and $Voice_Text::VTxt_version eq 'msv5'; - undef $volume_previous; + undef $volume_wav_previous; my $volume = $parms{volume}; # *** Oops the following line is wrong--mh_volume is linked to mixer @@ -157,41 +193,20 @@ sub set_volume { if ($parms{time} or ($parms{text} and $Voice_Text::VTxt_version ne 'msv5')) { - print_log "Setting mixer volume to $volume"; + print_log "Setting wav volume to $volume"; $volume = 100 if $volume > 100; - $volume_previous = set_volume2($volume); + $volume_wav_previous = &set_volume_wav($volume); - } -} + if($parms{mhvolume}) { + $volume_master_changed=1; + &set_volume_master_wrapper($parms{mhvolume}); + } -sub set_volume2 { - my ($volume) = @_; - my $volume_previous; - if ($Info{Volume_Control} eq 'Command Line') { - $volume_previous = `$config_parms{volume_get_cmd}`; - chomp $volume_previous; - my $r = system eval qq("$config_parms{volume_set_cmd} $volume"); } - elsif ($Info{Volume_Control} eq 'Win32::Sound' and !$config_parms{sound_volume_skip} ) { - - # *** nothing here to pull volume level from Windows! Will leave at this volume until next sound - # hack here (should actually work pretty well since mh_sound is sync'ed to volume now.) +} - $volume_previous = $mh_volume->{state}; - $volume = int 255 * $volume / 100; # (0->100 => 0->255) - $volume = $volume + ($volume << 16); # Hack to fix a bug in Win32::Sound::Volume - &Win32::Sound::Volume($volume); - } - elsif ($Info{Volume_Control} eq 'Audio::Mixer') { - my @vol = Audio::Mixer::get_cval('vol'); - $volume_previous = ($vol[0] + $vol[1]) / 2; -# Audio::Mixer::set_cval('vol', $volume); - Audio::Mixer::set_cval('spkr', $volume); - } - return $volume_previous; -} # Allow for a pre-speak/play wav file &Speak_pre_add_hook(\&sound_pre_speak) if $Reload and $config_parms{sound_pre_speak}; From 428bf6905ea8c7bf6531a3f2f166f54460a6f023 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 26 Sep 2013 17:32:00 -0700 Subject: [PATCH 144/330] Allow for State of 0 Fixes two things: - Allows for a State of 0 to be recorded in the state log - Allows for a State of 0 to be displyed through the web interface rather than N/A Both issues arise from the use of conditional statements such as: ``` if($state){...} ``` rather than using ``` if(defined($state)){...} ``` As a result a state of 0 was always ignored. --- lib/Generic_Item.pm | 2 +- lib/http_server.pl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Generic_Item.pm b/lib/Generic_Item.pm index 4896a6966..acf58f291 100644 --- a/lib/Generic_Item.pm +++ b/lib/Generic_Item.pm @@ -1059,7 +1059,7 @@ sub set_state_log { $target = '' unless defined $target; unshift(@{$$self{state_log}}, "$main::Time_Date $state set_by=$set_by_name" . (($target)?"target=$target":'')) - if $state or (ref $self) eq 'Voice_Cmd'; + if defined($state) or (ref $self) eq 'Voice_Cmd'; pop @{$$self{state_log}} if $$self{state_log} and @{$$self{state_log}} > $main::config_parms{max_state_log_entries}; diff --git a/lib/http_server.pl b/lib/http_server.pl index ace04ad86..a81e5cd63 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -2381,7 +2381,7 @@ sub html_item_state { my $filename = $object->{filename}; my $state_now = $object->{state}; my $html; - $state_now = '' unless $state_now; # Avoid -w uninitialized value msg + $state_now = '' unless defined($state_now); # Avoid -w uninitialized value msg # If >2 possible states, add a Select pull down form my @states; @@ -2407,7 +2407,7 @@ sub html_item_state { if (my $h_icon = &html_find_icon_image($object, $object_type)) { $html .= qq[$object_name]; } - elsif ($state_now) { + elsif ($state_now ne '') { my $temp = $state_now; $temp = substr($temp, 0, 8) . '..' if length $temp > 8; $html .= $temp . ' '; From 1590563c743139b1df7deb56ec81d1f6c8ad0a07 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 26 Sep 2013 17:42:00 -0700 Subject: [PATCH 145/330] Insteon: Pass Message Clearing Decision to on_read_write_aldb on_read_write_aldb now returns a 1/0 corresponding to whether the current message should be cleared. When a bad message arrived, on_read_write_aldb attempted to requeue the message that was currently pending. However, _process_message did not clear the pending message until after this routine was run. As a result, a new message was not queued because it was duplicative, but then the current message was cleared. This resulted in stalling the message queue. Fixes bug #258 --- lib/Insteon/AllLinkDatabase.pm | 18 ++++++++++++------ lib/Insteon/BaseInsteon.pm | 21 ++++----------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index ec74ce24c..d76a44222 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2127,7 +2127,7 @@ Called as part of any process to read or write to a device's ALDB. sub on_read_write_aldb { my ($self, %msg) = @_; - + my $clear_message = 1; #Default Action is to Clear the Current Message &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for _mem_activity=".$$self{_mem_activity} @@ -2135,6 +2135,8 @@ sub on_read_write_aldb if ($$self{_mem_action} eq 'aldb_i2read') { + #This is an ACK. Will be followed by a Link Data message, so don't clear + $clear_message = 0; #Only move to the next state if the received message is a device ack #if the ack is dropped the retransmission logic will resend the request if($msg{is_ack}) { @@ -2156,6 +2158,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received duplicate ack. Ignoring.") if $main::Debug{insteon} >= 3; + $clear_message = 0; } elsif(length($msg{extra})<30) { &::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed: " @@ -2163,8 +2166,9 @@ sub on_read_write_aldb . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); - #retry previous address again - $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); + #can't clear message, if valid message doesn't arrive + #resend logic will kick in + $clear_message = 0; } elsif ($$self{_mem_msb} . $$self{_mem_lsb} ne '0000' and $$self{_mem_msb} . $$self{_mem_lsb} ne substr($msg{extra},6,4)){ ::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed, " @@ -2172,9 +2176,10 @@ sub on_read_write_aldb . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; - $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); - #retry previous address again - $self->send_read_aldb(sprintf("%04x", hex($$self{_mem_msb} . $$self{_mem_lsb}))); + $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); + #can't clear message, if valid message doesn't arrive + #resend logic will kick in + $clear_message = 0; } elsif ($$self{_stress_test_act}){ $$self{_mem_activity} = undef; @@ -2369,6 +2374,7 @@ sub on_read_write_aldb . ": unhandled _mem_action=".$$self{_mem_action}) if $main::Debug{insteon}; } + return $clear_message; } sub _write_link diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index b92df149e..ff9571682 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -776,14 +776,8 @@ sub _process_message } elsif ($pending_cmd eq 'read_write_aldb') { if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { - if ($self->_aldb && $self->_aldb->{_mem_action} ne 'aldb_i2writeack'){ - #This is an ACK. Will be followed by a Link Data message - $clear_message = 0; - $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; - } else { - $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; - $self->_process_command_stack(%msg); - } + $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; + $self->_process_command_stack(%msg) if ($clear_message); } else { $corrupt_cmd = 1; $clear_message = 0; @@ -877,15 +871,8 @@ sub _process_message $self->request_status($self); } elsif ($msg{command} eq 'read_write_aldb') { if ($self->_aldb){ - if ($self->_aldb->{_mem_action} eq 'aldb_i2readack'){ - #If aldb_i2readack is set then this is good - $clear_message = 1; - $self->_aldb->on_read_write_aldb(%msg); - $self->_process_command_stack(%msg); - } else { - #This is an out of sequence message - $self->_aldb->on_read_write_aldb(%msg); - } + $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; + $self->_process_command_stack(%msg) if($clear_message); } } elsif ($msg{type} eq 'broadcast') { $self->devcat($msg{devcat}); From 211592bdb8127cdaebb84c2e3fb41967b57855e0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 27 Sep 2013 17:34:00 -0700 Subject: [PATCH 146/330] Insteon: Don't Clear Queue on Unhandled Mem_Action Missed one instance in which the queued message should not be cleared. Should not be cleared on an unhandled mem action either. Further Fix to #258 --- lib/Insteon/AllLinkDatabase.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index d76a44222..d67e0944b 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2373,6 +2373,7 @@ sub on_read_write_aldb main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) if $main::Debug{insteon}; + $clear_message = 0; } return $clear_message; } From 59def92cb020f78ae7000e27b1f881131da4cc9b Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 29 Sep 2013 00:29:14 -0400 Subject: [PATCH 147/330] rename to SCENE_BUILD and performed some code cleanup --- lib/read_table_A.pl | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 29688fe2b..165dd708c 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -993,15 +993,14 @@ sub read_table_A { } $object = ''; } - elsif($type eq "SCENE_SIMPLE") { - #SCENE_SIMPLE, scene_name, scene_member, controller?, responder?, onlevel, ramprate + elsif($type eq "SCENE_BUILD") { + #SCENE_BUILD, scene_name, scene_member, controller?, responder?, onlevel, ramprate my ($scene_member, $scene_controller, $scene_responder, $on_level, $ramp_rate); ($name, $scene_member, $scene_controller, $scene_responder, $on_level, $ramp_rate) = @item_info; if( ! $packages{Scene}++ ) { # first time for this object type? $code .= "use Scene;\n"; } - $scenes{$name}{$scene_member}="$scene_controller,$scene_responder,$on_level,$ramp_rate"; - $code .= sprintf "#SCENE_SIMPLE: \$%-35s -> add(\$%s);\n", $name, $scene_member; + $scenes{$name}{$scene_member} = "$scene_controller,$scene_responder,$on_level,$ramp_rate"; $object = ''; } elsif ($type eq "PHILIPS_HUE"){ @@ -1062,15 +1061,20 @@ sub read_table_A { sub read_table_finish_A { my ($code, $scene, $scene_member, %scene_data, $member_data); foreach $scene (sort keys %scenes) { - $code .= "\n#SCENE DEFINITION: $scene\n"; + $code .= "\n#SCENE_BUILD Definition for scene: $scene\n"; + + #Doesn't work because object technically doesn't exist yet. Is it even necessary to limit to ICONTROLLER's? #my $object = &get_object_by_name($scene); - #if(defined($object) and $object->isa("Insteon::InterfaceController")) { #Doesn't work because object technically doesn't exist yet. Is it even necessary to limit to ICONTROLLER's? -# print "\n\nOBJECT: $objects{$scene}\n\n\n"; + #if(defined($object) and $object->isa("Insteon::InterfaceController")) { + if($objects{$scene}) { - $scenes{$scene}{$scene}="1,0"; #Make the INSTEON_ICONTROLLER a controller too + #Since an INSTEON_ICONTROLLER exists with the same name as the scene, make it a controller of the scene, too. + $scenes{$scene}{$scene}="1,0"; } + #Make a hash copy so we can iterate through the hash inside another iteration of the same hash. Is there a better way? + my %scenememberlist=%{$scenes{$scene}}; + #Loop through the hash and find all controller->responder combinations that make sense. - my %scenememberlist=%{$scenes{$scene}}; #Make a hash copy so we can iterate through the hash inside another iteration of the same hash. Is there a better way? while (($scene_member, $member_data) = each($scenes{$scene})) { my ($scene_controller, $scene_responder) = split(',', $member_data); @@ -1093,7 +1097,7 @@ sub read_table_finish_A { } } else { - print "\nThere is no object called $scene_member defined. Ignoring SCENE_SIMPLE entry.\n" unless $objects{$scene_member}; + print "\nThere is no object called $scene_member defined. Ignoring SCENE_BUILD entry.\n" unless $objects{$scene_member}; } } } From f5a70fa8faa2a8c72172be83ce7024f3bba02d78 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 30 Sep 2013 01:27:22 -0400 Subject: [PATCH 148/330] Add PA zone type of 'object', clean up code, enhance to allow for mixed zone types, tested X10, xPL, xAP, object and wdio. xPL and xAP were only tested to send to the designated IP address - verified with netcat on target computer. --- code/common/pa_control.pl | 53 +++--- lib/PAobj.pm | 358 ++++++++++++++++++++++++-------------- lib/read_table_A.pl | 92 +++++----- 3 files changed, 305 insertions(+), 198 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index 514d2348b..fa35a6eb7 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -13,20 +13,20 @@ Centralized control of various PA zone types. Author: - Steve Switzer + Steve Switzer (Pmatis) steve@switzerny.org License: This free software is licensed under the terms of the GNU public license. Requires: - PAobj.pm from the lib directory - pa.mht, or other mht file listing all of your PA zones. See end of file for ezample + PAobj.pm from the lib directory + pa.mht, or other mht file listing all of your PA zones. See end of file for ezample Special Thanks to: Bruce Winter - MH Jason Sharpee - Example Perl Modules to "steal",learn from. :) - Ross Towbin - Providing me with code snippets for "setting weeder with more than 8 ports" + Ross Towbin - Providing me with code snippets for "setting weeder with more than 8 ports" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut @@ -34,15 +34,11 @@ use PAobj; #noloop=start -my $pa_port = $config_parms{pa_port}; my $pa_delay = $config_parms{pa_delay}; -my $pa_type = $config_parms{pa_type}; my $pa_timer = $config_parms{pa_timer}; -$pa_port = 'weeder' unless $pa_port; $pa_delay = 0.5 unless $pa_delay; -$pa_type = 'wdio' unless $pa_type; $pa_timer = 60 unless $pa_timer; -$pactrl = new PAobj($pa_type,$pa_port); +$pactrl = new PAobj(); $pactrl->set_delay($pa_delay); $v_pa_test = new Voice_Cmd('test pa'); $v_pa_speakers = new Voice_Cmd('speakers [on,off]'); @@ -58,8 +54,8 @@ if (said $v_pa_test) { my $state = $v_pa_test->{state}; $v_pa_test->respond('app=pa Testing PA...'); - #speak "nolog=1 rooms=all mode=unmuted volume=100 Hello. This is a PA system test."; - speak "nolog=1 rooms=downstairs mode=unmuted volume=100 Hi!"; + speak "nolog=1 rooms=all mode=unmuted volume=80 Hello. This is a PA system test."; + #speak "nolog=1 rooms=downstairs mode=unmuted volume=100 Hi!"; } # turn all speakers on/off @@ -84,15 +80,16 @@ sub pa_control_stub { return if $mode eq 'mute' or $mode eq 'offline'; my $rooms = $parms{rooms}; - print "pa_stub db: rooms=$rooms, mode=$mode\n" if $Debug{pa}; - my $results = $pactrl->set($rooms,ON,$mode); - print "PA set results: $results\n" if $Debug{pa}; + print "PA: control_stub: rooms=$rooms, mode=$mode\n" if $Debug{pa}; + my $results = $pactrl->set($rooms,ON,$mode,%parms); + print "PA: control_stub set results: $results\n" if $Debug{pa}; set $pa_speaker_timer $pa_timer if $results; } #Turn off speakers when MH says it's done speaking/playing if (state_now $mh_speakers eq OFF) { unset $pa_speaker_timer; + print "PA: Turning speakers off\n" if $Debug{pa}; $pactrl->set('allspeakers',OFF,'normal'); } @@ -100,9 +97,8 @@ sub pa_control_stub { $pa_speaker_timer = new Timer; set $pa_speaker_timer 60 if state_now $mh_speakers eq ON; if (expired $pa_speaker_timer) { -#print "Timer expired\n"; + print "PA: Timer expired.\n" if $Debug{pa}; set $mh_speakers OFF; - #$pactrl->set('allspeakers',OFF,'normal'); } =begin comment @@ -111,18 +107,25 @@ sub pa_control_stub { Example pa.mht file: # -# Type Address Name Groups Serial Name Other +#Type Address Name Groups Serial Other # - -PA, AA, kitchen, all|default|mainfloor, weeder, wdio -PA, AB, server, all|basement, weeder, wdio -PA, AG, master, all|default|upstairs, weeder, wdio +PA, AA, kitchen, all|default|mainfloor, weeder, wdio +PA, AB, server, all|basement, weeder, wdio +PA, AG, master, all|default|upstairs, weeder2, wdio_old +PA, B12, garage, all|outside, , X10 +PA, objname, living, all|mainfloor, , object +PA, 192.168.0.1,family, all|mainfloor, , xap +PA, 192.168.0.2,dining, all|mainfloor, , xpl Type: "PA", constant. This must be there. -Address: 2 characters. First character is the weeder address, the second is the pin - if the command to turn on the pin you want is: BHC, then the Address is: BC +Address: Address or Object name. + If Other is "object", then this should be an object name that can accept an ON or OFF + For Weeder, 2 characters. First character is the weeder address, the second is the pin + if the command to turn on the pin you want is: BHC, then the Address is: BC + For X10, the X10 address of the (likely) relay device. + For xAP and xPL, use the IP address or hostname of the target device. Name: Give a name to the pa zone, usually the room name. You use these in the speak and play commands with rooms=. @@ -135,11 +138,11 @@ sub pa_control_stub { rooms= parm. If no rooms are specified, all zones in the "default" group will be used. -Serial Name: The name of the serial port that you use for communcating to the IO device. +Serial: The name of the serial port that you use for communcating to the IO device. The default is "weeder". Note that this can be changed with an INI parm. Other: Optional. Sets the type of PA control. Defaults to 'wdio'. Available options are: - wdio,wdio_old,X10 + wdio,wdio_old,X10,xpl,xap,object @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 2c230ae2e..34299cc86 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -5,7 +5,6 @@ Example initialization: use PAobj; - $paobj = new PAobj('wdio','weeder'); Enable pa_control.pl using "Common code activation" in the IA5 interface to activate an instance of this PA code. @@ -43,18 +42,11 @@ sub last_char sub new { - my ($class,$pa_type,$pa_port) = @_; + my ($class) = @_; my $self={}; bless $self,$class; - $pa_type = 'wdio' unless $pa_type; - $pa_port = 'weeder' unless $pa_port; - - $$self{pa_type} = $pa_type; - $$self{pa_type_init} = 0; - - $$self{pa_port} = $pa_port; $$self{pa_delay} = 0.5; return $self; @@ -63,63 +55,84 @@ sub new sub init { my ($self) = @_; %pa_zone_types=(); - my $ref = &::get_object_by_name("pa_allspeakers"); - if (!$ref) { + my $ref2 = &::get_object_by_name("pa_allspeakers"); + if (!$ref2) { &::print_log("\n\nWARNING! PA Zones were not found! Your *.mht file probably doesn't list the PA zones correctly.\n\n"); return 0; } $self->check_group('default'); -# my @speakers = $self->get_speakers('allspeakers'); -# for my $room (@speakers) { -# my $paobjname = "pa_$room"; -# my $ref = &::get_object_by_name($paobjname); -# my $pa_zone_type = $pa_zone_type_by_zone{$paobjname}; -# print "db INIT room=$room, zonetype=$pa_zone_type\n"; -# $pa_zone_types{$pa_zone_type}++ unless $pa_zone_types{$$ref{pa_type}}; -# } - + my @speakers = $self->get_speakers('allspeakers'); + my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl); - if ($$self{pa_type} =~ /^wdio/i) { - $self->init_weeder(); + for my $room (@speakers) { + my $ref = &::get_object_by_name("pa_$room"); + my $type = $ref->get_type(); + print "PAObj: init: room=$room\n"; + print "PAObj: init: room=$room, zonetype=$type\n"; + $pa_zone_types{$type}++ unless $pa_zone_types{$type}; + + if($type eq 'wdio') { + push(@speakers_wdio,$room); + } + if($type eq 'x10') { + push(@speakers_x10,$room); + } + if($type eq 'xap') { + push(@speakers_xap,$room); + } + if($type eq 'xpl') { + push(@speakers_xpl,$room); + } + if($type eq 'object') { + push(@speakers_obj,$room); + } + } + + print "PAObj: speakers_wdio: $#speakers_wdio\n" if $main::Debug{pa}; + print "PAObj: speakers_x10: $#speakers_x10\n" if $main::Debug{pa}; + print "PAObj: speakers_xap: $#speakers_xap\n" if $main::Debug{pa}; + print "PAObj: speakers_xpl: $#speakers_xpl\n" if $main::Debug{pa}; + print "PAObj: speakers_obj: $#speakers_obj\n" if $main::Debug{pa}; + + if ($#speakers_wdio > -1) { + $self->init_weeder(@speakers_wdio); return 0 unless %pa_weeder_max_port; - } elsif (lc $$self{pa_type} eq 'x10') { - print "x10 PA type initialized...\n" if $main::Debug{pa}; - } elsif (lc $$self{pa_type} eq 'xap') { - print "xAP PA type initialized...\n" if $main::Debug{pa}; - } elsif (lc $$self{pa_type} eq 'xpl') { - print "xPL PA type initialized...\n" if $main::Debug{pa}; - } else { - &::print_log("\n\nWARNING! Unrecognized PA type of \"$$self{pa_type}\". PA code probably will not work.\n\n"); - return 0; + } + if ($pa_zone_types{'x10'}) { + print "PAObj: x10 PA type initialized...\n" if $main::Debug{pa}; + } + if ($pa_zone_types{'xap'}) { + print "PAObj: xAP PA type initialized...\n" if $main::Debug{pa}; + } + if ($pa_zone_types{'xpl'}) { + print "PAObj: xPL PA type initialized...\n" if $main::Debug{pa}; } return 1; } sub init_weeder { - my ($self) = @_; + my ($self,@speakers) = @_; my (%weeder_ref,%weeder_max); - my @speakers = $self->get_speakers('allspeakers'); undef %pa_weeder_max_port; for my $room (@speakers) { - print "db init PA Room loaded: $room\n" if $main::Debug{pa}; - my $ref = &::get_object_by_name("pa_$room"); + print "PAObj: init PA Room loaded: $room\n" if $main::Debug{pa}; + my $ref = &::get_object_by_name('pa_' . $room . '_obj'); $ref->{state} = 'off'; - print "db pa type: $$self{pa_type}\n" if $main::Debug{pa}; my ($card,$id); ($card,$id) = $ref->{id_by_state}{'on'} =~ /^D?(.)H(.)/s; $weeder_ref{$card} = '' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; - print "db init card: $card, id: $id, Room: $room, List: $weeder_ref{$card}\n" if $main::Debug{pa}; + print "PAObj: init card: $card, id: $id, Room: $room, List: $weeder_ref{$card}\n" if $main::Debug{pa}; } for my $card ('A' .. 'P','a' .. 'p') { if ($weeder_ref{$card}) { my $data = $weeder_ref{$card}; $weeder_max{$card}=$self->last_char($data); - print "\ndb init weeder board=$card, ports=$data, max port=" . $weeder_max{$card} . "\n" if $main::Debug{pa}; + print "\nPAObj: init weeder board=$card, ports=$data, max port=" . $weeder_max{$card} . "\n" if $main::Debug{pa}; } } %pa_weeder_max_port = %weeder_max; @@ -127,101 +140,147 @@ sub init_weeder sub set { - my ($self,$rooms,$state,$mode) = @_; + my ($self,$rooms,$state,$mode,%voiceparms) = @_; my $results = 0; - print "db: pa_type: $$self{pa_type}, delay: $$self{pa_delay}\n" if $main::Debug{pa}; - - print "pa db: set,mode: " . $mode . "\n" if $main::Debug{pa}; - print "pa db: set,rooms: " . $rooms . "\n" if $main::Debug{pa}; + print "PAObj: delay: $$self{pa_delay}\n" if $main::Debug{pa}; + print "PAObj: set,mode: " . $mode . "\n" if $main::Debug{pa}; + print "PAObj: set,rooms: " . $rooms . "\n" if $main::Debug{pa}; my @speakers = $self->get_speakers($rooms); @speakers = $self->get_speakers('') if $#speakers == -1; @speakers = $self->get_speakers_speakable($mode,@speakers); - $results = $self->set_weeder($state,@speakers) if substr(lc $$self{pa_type}, 0, 4) eq 'wdio'; - $results = $self->set_x10($state,@speakers) if lc $$self{pa_type} eq 'x10'; -# $results = $self->set_xap($state,@speakers) if lc $$self{pa_type} eq 'xap'; -# $results = $self->set_xpl($state,@speakers) if lc $$self{pa_type} eq 'xpl'; + + my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl); + + for my $room (@speakers) { + my $ref = &::get_object_by_name("pa_$room"); + my $type = lc $ref->get_type(); + if($type eq 'wdio' || $type eq 'wdio_old') { + print "PAObj: speakers_wdio: Adding $room\n" if $main::Debug{pa}; + push(@speakers_wdio,$room); + } + if($type eq 'x10') { + print "PAObj: speakers_x10: Adding $room\n" if $main::Debug{pa}; + push(@speakers_x10,$room); + } + if($type eq 'xap') { + print "PAObj: speakers_xap: Adding $room\n" if $main::Debug{pa}; + push(@speakers_xap,$room) if $state eq 'on'; #Only need to send if speech is starting + } + if($type eq 'xpl') { + print "PAObj: speakers_xpl: Adding $room\n" if $main::Debug{pa}; + push(@speakers_xpl,$room) if $state eq 'on'; #Only need to send if speech is starting + } + if($type eq 'object') { + print "PAObj: speakers_object: Adding $room\n" if $main::Debug{pa}; + push(@speakers_obj,$room); + } + } + + print "PAObj: speakers_wdio: $#speakers_wdio\n" if $main::Debug{pa}; + print "PAObj: speakers_x10: $#speakers_x10\n" if $main::Debug{pa}; + print "PAObj: speakers_xap: $#speakers_xap\n" if $main::Debug{pa}; + print "PAObj: speakers_xpl: $#speakers_xpl\n" if $main::Debug{pa}; + print "PAObj: speakers_obj: $#speakers_obj\n" if $main::Debug{pa}; + + #TODO: Properly handle $results across multiple types + #TODO: Break up the wdio zones based on serial port, in case there are more than one. + $results = $self->set_weeder($state,'weeder',@speakers_wdio) if $#speakers_wdio > -1; + $results = $self->set_x10($state,@speakers_x10) if $#speakers_x10 > -1; + $results = $self->set_xap($state,\@speakers_xap,\%voiceparms) if $#speakers_xap > -1; + $results = $self->set_xpl($state,\@speakers_xpl,\%voiceparms) if $#speakers_xpl > -1; + $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; + select undef, undef, undef, $$self{pa_delay} if $results; return $results; } -sub set_x10 +sub set_obj { my ($self,$state,@speakers) = @_; - my $x10_list; - my $pa_x10_hc; - for my $room (@speakers) { - my $ref = &::get_object_by_name("pa_$room"); + print "PAObj: set_obj: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + my $ref = &::get_object_by_name("pa_$room"); if ($ref) { - $ref->{state} = $state; - my ($id) = $ref->{x10_id}; - print "db pa set_x10 id: $id, Room: $room\n" if $main::Debug{pa}; - $pa_x10_hc = substr($id,1,1) unless $pa_x10_hc; - $x10_list .= substr($id,1,2); - } + $ref->set($state); + } } +} - $self->print_speaker_states() if $main::Debug{pa}; - $x10_list = 'X' . $x10_list . $pa_x10_hc; - $x10_list .= ($state eq 'on') ? 'J':'K'; - print "db pa x10 cmd: $x10_list\n" if $main::Debug{pa}; +sub set_x10 +{ + my ($self,$state,@speakers) = @_; + my ($x10_list,$pa_x10_hc,$ref,$refobj); + + for my $room (@speakers) { + print "PAObj: set_x10: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + $ref = &::get_object_by_name('pa_'.$room); + $refobj = &::get_object_by_name('pa_'.$room.'_obj'); + if ($refobj && $ref) { + my ($id) = $ref->get_address(); + print "PAObj: set_x10 ID: $id, State: $state, Room: $room\n" if $main::Debug{pa}; + $refobj->set($state); + } + } } sub set_xap { - my ($self,$rooms,$mode,%voiceparms) = @_; - my @speakers = $self->get_speakers($rooms); - @speakers = $self->get_speakers('') if $#speakers == -1; - @speakers = $self->get_speakers_speakable($mode, @speakers); + my ($self,$state,$param1,$param2) = @_; + my @speakers = @$param1; + my %voiceparms = %$param2; + return unless $#speakers > -1; for my $room (@speakers) { - my $ref = &::get_object_by_name("paxap_$room"); + print "PAObj: set_xap: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { - $ref->send_message($ref->target_address, $ref->class_name => {say => $voiceparms{text}, voice => $voiceparms{voice} }); - print "db pa xap cmd: $ref->{object_name} is sending voice text: $voiceparms{text}\n" if $main::Debug{pa}; + $ref->send_message($ref->target_address, $ref->class_name => {say => $voiceparms{text}, volume => $voiceparms{volume}, mode => $voiceparms{mode}, voice => $voiceparms{voice} }); + print "PAObj: xap cmd: $ref->{object_name} is sending voice text: $voiceparms{text}\n" if $main::Debug{pa}; } else { - print "unable to locate object: paxap_$room\n" if $main::Debug{pa}; + print "PAObj: Unable to locate object for: pa_$room\n" if $main::Debug{pa}; } } } sub set_xpl { - my ($self,$rooms,$mode,%voiceparms) = @_; - my @speakers = $self->get_speakers($rooms); - @speakers = $self->get_speakers('') if $#speakers == -1; - @speakers = $self->get_speakers_speakable($mode, @speakers); + my ($self,$state,$param1,$param2) = @_; + my @speakers = @$param1; + my %voiceparms = %$param2; + return unless $#speakers > -1; for my $room (@speakers) { - my $ref = &::get_object_by_name("paxpl_$room"); - if ($ref) { - my $max_length = $::config_parms{"paxpl_$room" . "_maxlength"}; + print "PAObj: set_xpl: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + my $ref = &::get_object_by_name('pa_'.$room.'_obj'); + if ($ref) { + my $max_length = $::config_parms{"pa_$room" . "_maxlength"}; $max_length = 0 unless $max_length; my $text = $voiceparms{text}; if ($max_length) { $text = substr($text, 0, $max_length) if $max_length < length($text); } - $ref->send_cmnd($ref->class_name => {speech => $text, voice => $voiceparms{voice} }); - print "db pa xpl cmd: $ref->{object_name} is sending voice text: $voiceparms{text}\n" if $main::Debug{pa}; - } else { - print "unable to locate object: paxpl_$room\n" if $main::Debug{pa}; - } + $ref->send_cmnd($ref->class_name => {speech => $text, voice => $voiceparms{voice}, volume => $voiceparms{volume}, mode => $voiceparms{mode} }); + print "PAObj: set_xpl: $ref->{object_name} is sending voice text: $voiceparms{text}\n" if $main::Debug{pa}; + } else { + print "PAObj: Unable to locate object for: pa_$room\n" if $main::Debug{pa}; + } } } sub set_weeder { - my ($self,$state,@speakers) = @_; + my ($self,$state,$weeder_port,@speakers) = @_; my %weeder_ref; my $weeder_command=''; my $command=''; for my $room (@speakers) { - my $ref = &::get_object_by_name("pa_$room"); - if ($ref) { + print "PAObj: set_weeder: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + my $ref = &::get_object_by_name('pa_'.$room.'_obj'); + if ($ref) { $ref->{state} = $state; my ($card,$id) = $ref->{id_by_state}{'on'} =~ /^D?(.)H(.)/s; $weeder_ref{$card}='' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; - print "card: $card, id: $id, Room: $room\n" if $main::Debug{pa}; - } + print "PAObj: card: $card, id: $id, Room: $room\n" if $main::Debug{pa}; + } } $self->print_speaker_states() if $main::Debug{pa}; @@ -234,10 +293,10 @@ sub set_weeder $weeder_command .= "$command\\r" if $command; } } - return 0 unless $command; - print "sending $weeder_command to the weeder card(s)\n" if $main::Debug{pa}; + return 0 unless $weeder_command; + print "PAObj: Sending $weeder_command to the weeder card(s)\n" if $main::Debug{pa}; $weeder_command =~ s/\\r/\r/g; - &Serial_Item::send_serial_data($$self{pa_port}, $weeder_command) if $main::Serial_Ports{$$self{pa_port}}{object}; + &Serial_Item::send_serial_data($weeder_port, $weeder_command) if $main::Serial_Ports{$weeder_port}{object}; return 1; } @@ -255,7 +314,7 @@ sub get_weeder_string for $bit ('A' .. $pa_weeder_max_port{$card}) { $id = $card . 'L' . $bit; - $id = "D$id" if $$self{pa_type} eq 'wdio_old'; + $id = "D$id" if $$self{pa_type} eq 'wdio_old'; #TODO: Find way to implement this with new code my $ref = &Device_Item::item_by_id($id); if ($ref) { $state = $ref->{state}; @@ -265,7 +324,7 @@ sub get_weeder_string } $bit_flag = ($state eq 'on') ? 1 : 0; # get 0 or 1 - print "db get_weeder_string card: $card, bit=$bit state=$bit_flag\n" if $main::Debug{pa}; + print "PAObj: get_weeder_string card: $card, bit=$bit state=$bit_flag\n" if $main::Debug{pa}; $byte_code += ($bit_flag << $bit_counter); # get bit in byte position if ($bit_counter++ >= 3) { @@ -275,13 +334,13 @@ sub get_weeder_string } } - # we have to do this again -- in case we don't have bits on a byte boundry + # we have to do this again -- in case we don't have bits on a byte boundary if ($bit_counter > 0) { # pre-pend our string with the new value $weeder_code = $decimal_to_hex{$byte_code} . $weeder_code; } - if ($$self{pa_type} eq 'wdio_old') { + if ($$self{pa_type} eq 'wdio_old') { #TODO: Find way to implement this with new code $card = "D$card"; $weeder_code = 'h' . $weeder_code; } @@ -293,7 +352,7 @@ sub get_speakers my ($self,$rooms) = @_; my @pazones; - print "pa db: get_speakers,rooms: " . $rooms . "\n" if $main::Debug{pa}; + print "PAObj: get_speakers,rooms: " . $rooms . "\n" if $main::Debug{pa}; if ($::mh_speakers->{rooms}) { $rooms = $::mh_speakers->{rooms}; $::mh_speakers->{rooms} = ''; @@ -305,28 +364,22 @@ sub get_speakers no strict 'refs'; my $ref = &::get_object_by_name("pa_$room"); if ($ref) { - print "pa db: name=$ref->{object_name}\n" if $main::Debug{pa}; + print "PAObj: name=$ref->{object_name}\n" if $main::Debug{pa}; if (UNIVERSAL::isa($ref,'Group')) { - print "pa db: It's a group!\n" if $main::Debug{pa}; + print "PAObj: It's a group!\n" if $main::Debug{pa}; for my $grouproom ($ref->list) { $grouproom = $grouproom->get_object_name; $grouproom =~ s/^\$pa_//; - $grouproom =~ s/^\$paxpl_//; - $grouproom =~ s/^\$paxap_//; - print "pa db: - member: $grouproom\n" if $main::Debug{pa}; + $grouproom =~ s/^\$paxpl_//; + $grouproom =~ s/^\$paxap_//; + print "PAObj: - member: $grouproom\n" if $main::Debug{pa}; push(@pazones, $grouproom); } } else { push(@pazones, $room); } - } elsif (lc $$self{pa_type} eq 'xpl') { - $ref = &::get_object_by_name("paxpl_$room"); - push(@pazones, $room) if $ref; - } elsif (lc $$self{pa_type} eq 'xap') { - $ref = &::get_object_by_name("paxap_$room"); - push(@pazones, $room) if $ref; } else { - &::print_log("WARNING: PA zone of '$room' not found!"); + &::print_log("PAObj: WARNING: PA zone of '$room' not found!"); } } return @pazones; @@ -335,13 +388,13 @@ sub get_speakers sub check_group { my ($self,$group) = @_; - print "db check group=$group\n" if $main::Debug{pa}; + print "PAObj: check group=$group\n" if $main::Debug{pa}; my $ref = &::get_object_by_name("pa_$group"); if (!$ref) {print "Error! Group does not exist: $group\n"; return;} my @list = $ref->list; - print "db check group=$group, list=$#list\n" if $main::Debug{pa}; + print "PAObj: check group=$group, list=$#list\n" if $main::Debug{pa}; if ($#list == -1) { - print "db check populating group: $group!\n" if $main::Debug{pa}; + print "PAObj: check populating group: $group!\n" if $main::Debug{pa}; for my $room ($self->get_speakers('allspeakers')) { my $ref2 = &::get_object_by_name("pa_$room"); $ref->add($ref2); @@ -358,17 +411,14 @@ sub get_speakers_speakable return @pazones if $mode eq 'mute' or $mode eq 'offline'; for my $room (@zones) { - my $ref = &::get_object_by_name("pa_$room"); - $ref = &::get_object_by_name("paxpl_$room") if lc $$self{pa_type} eq 'xpl'; - $ref = &::get_object_by_name("paxap_$room") if !$ref and $$self{pa_type} eq 'xap'; - print "pa db: ref=$ref\n" if $main::Debug{pa}; - print "pa db: name=$ref->{object_name}\n" if $main::Debug{pa}; + my $ref = &::get_object_by_name("pa_$room"); + print "PAObj: speakable: name=$ref->{object_name}\n" if $main::Debug{pa}; if ($ref->{sleeping} == 0) { $ref->{mode} = 'normal' unless $ref->{mode}; my $gss_mode = $ref->{mode}; if ($gss_mode ne 'sleeping' && ($gss_mode eq 'normal' || $mode eq 'unmuted')) { push(@pazones,$room); - print "pa db: pushing $room into pazones array:$#pazones\n" if $main::Debug{pa}; + print "PAObj: speakable: Pushing $room into pazones array:$#pazones\n" if $main::Debug{pa}; } } } @@ -388,20 +438,76 @@ sub print_speaker_states my ($ref,$room); for my $speaker (@speakers) { $ref = &::get_object_by_name("pa_$speaker"); - $ref = &::get_object_by_name("paxpl_$speaker") if !$ref and $$self{pa_type} eq 'xpl'; - $ref = &::get_object_by_name("paxap_$speaker") if !$ref and $$self{pa_type} eq 'xap'; $room = $ref->{object_name}; - if ($$self{pa_type} eq 'xpl') { - $room =~ s/^\$paxpl_//; - } elsif ($$self{pa_type} eq 'xap') { - $room =~ s/^\$paxap_//; - } else { - $room =~ s/^\$pa_//; - } - print "db name=$room, state=$ref->{state}\n" if $main::Debug{pa}; + $room =~ s/^\$pa_//; + print "PAObj: name=$room, state=$ref->{state}\n" if $main::Debug{pa}; } } +package PAobj_zone; + +@PAobj_zone::ISA = ('Generic_Item'); + +sub last_char +{ + my ($self,$string) = @_; + my @chars=split(//, $string); + return((sort @chars)[-1]); +} + +#Type Address Name Groups Serial Other +sub new +{ + my ($class,$paz_address,$paz_name,$paz_groups,$paz_serial,$paz_other) = @_; + my $self={}; + + bless $self,$class; + + $$self{name} = $paz_name; + $$self{address} = $paz_address; + $$self{groups} = $paz_groups; + $$self{serial} = $paz_serial; + $$self{other} = $paz_other; + + return $self; +} + +sub init +{ + my ($self) = @_; +} + +sub get_address +{ + my ($self) = @_; + return $$self{address}; +} + +sub get_name +{ + my ($self) = @_; + return $$self{name}; +} + +sub get_groups +{ + my ($self) = @_; + return $$self{groups}; +} + +sub get_serial +{ + my ($self) = @_; + return $$self{serial}; +} + +sub get_type +{ + my ($self) = @_; + return $$self{other}; +} + + 1; diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 93ca9a499..0d197677e 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -22,7 +22,7 @@ sub read_table_init_A { %groups=(); %objects=(); %packages=(); - %addresses=(); + %addresses=(); } sub read_table_A { @@ -517,65 +517,63 @@ sub read_table_A { } elsif ($type eq "PA") { require 'PAobj.pm'; - my $pa_type; - ($address, $name, $grouplist, $other, $pa_type, @other) = @item_info; - # $other is being used as the serial name + my ($pa_type, $serial); + ($address, $name, $grouplist, $serial, $pa_type, @other) = @item_info; $pa_type = 'wdio' unless $pa_type; if( ! $packages{PAobj}++ ) { # first time for this object type? $code .= "my (%pa_weeder_max_port,%pa_zone_types,%pa_zone_type_by_zone);\n"; } -# if ($config_parms{pa_type} ne $pa_type) { - if(1==0) { - print "ERROR! INI parm \"pa_type\"=$main::config_parms{pa_type}, but PA item $name is a type of $pa_type. Skipping PA zone.\n - r=$record\n"; - return; - } else { -# $name = "pa_$name"; - - $grouplist = "|$grouplist|allspeakers"; - $grouplist =~ s/\|\|/\|/g; - $grouplist =~ s/\|/\|pa_/g; - $grouplist =~ s/^\|//; - $grouplist .= '|hidden'; + $code .= sprintf "\n\$%-35s = new PAobj_zone('%s','%s','%s','%s','%s');\n","pa_$name", $address, $name, $grouplist, $serial, $pa_type; + $name = "pa_$name"; - if ($pa_type =~ /^wdio/i) { - $name = "pa_$name"; - # AHB / ALB or DBH / DBL - $address =~ s/^(\S)(\S)$/$1H$2/;# if $pa_type eq 'wdio'; - $address = "D$address" if $pa_type eq 'wdio_old'; -# $address =~ s/^(\S)(\S)$/DBH$2/ if $pa_type eq 'wdio_old'; - $code .= sprintf "\n\$%-35s = new Serial_Item('%s','on','%s');\n",$name,$address,$other; -# $code .= sprintf "\n\$\$%s{pa_type} = '%s';\n",$name,$pa_type; + $grouplist = "|$grouplist|allspeakers"; + $grouplist =~ s/\|\|/\|/g; + $grouplist =~ s/\|/\|pa_/g; + $grouplist =~ s/^\|//; + $grouplist .= '|hidden'; -# $code .= sprintf "\$pa_zone_types{%s}++ unless \$pa_zone_types{%s};\n",$pa_type,$pa_type; -# $code .= sprintf "\$pa_zone_type_by_zone{%s} = '%s';\n",$name,$pa_type; + if ($pa_type =~ /^wdio/i) { + # AHB / ALB or DBH / DBL + $address =~ s/^(\S)(\S)$/$1H$2/;# if $pa_type eq 'wdio'; + $address = "D$address" if $pa_type eq 'wdio_old'; + $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n",$name.'_obj',$address,$serial; - $address =~ s/^(\S{1,2})H(\S)$/$1L$2/; -# $address =~ s/^(\S)H(\S)$/$1L$2/ if $pa_type eq 'wdio'; -# $address =~ s/^D(\S)H(\S)$/D$1L$2/ if $pa_type eq 'wdio_old'; - $code .= sprintf "\$%-35s -> add ('%s','off');\n",$name,$address; + $address =~ s/^(\S{1,2})H(\S)$/$1L$2/; + $code .= sprintf "\$%-35s -> add ('%s','off');\n",$name.'_obj',$address; - $object = ''; - } elsif (lc $pa_type eq 'x10') { - $name = "pa_$name"; - $other = join ', ', (map {"'$_'"} @other); # Quote data - $object = "X10_Appliance('$address', $other)"; - } elsif (lc $pa_type eq 'xap') { - $name = "paxap_$name"; - $code .= sprintf "\n\$%-35s = new xAP_Item('%s');\n",$name,$address; - $code .= sprintf "\$%-35s -> target_address('%s');\n",$name,$address; - $code .= sprintf "\$%-35s -> class_name('%s');\n",$name,$other; - } elsif (lc $pa_type eq 'xpl') { - $name = "paxpl_$name"; - $code .= sprintf "\n\$%-35s = new xPL_Item('%s');\n",$name,$address; - $code .= sprintf "\$%-35s -> target_address('%s');\n",$name,$address; - $code .= sprintf "\$%-35s -> class_name('%s');\n",$name,$other; + $object = ''; + } elsif (lc $pa_type eq 'object') { + if($name =~ /^pa_pa_/i) { + print "\nObject name \"$name\" starts with \"pa_\". This will cause conflicts. Ignoring entry"; } else { - print "\nUnrecognized .mht entry for PA: $record\n"; - return; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n",$name,$address; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n",$name,$address; } + } elsif (lc $pa_type eq 'x10') { + $other = join ', ', (map {"'$_'"} @other); # Quote data + $code .= sprintf "\$%-35s = new X10_Appliance('%s','%s');\n",$name.'_obj',$address, $serial; + $code .= sprintf "\$%-35s -> hidden(1);\n", $name.'_obj'; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n",$name,$name.'_obj'; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n",$name,$name.'_obj'; + } elsif (lc $pa_type eq 'xap') { + $code .= sprintf "\$%-35s = new xAP_Item('%s');\n",$name.'_obj',$address; + $code .= sprintf "\$%-35s -> hidden(1);\n", $name.'_obj'; + $code .= sprintf "\$%-35s -> target_address('%s');\n",$name.'_obj',$address; + $code .= sprintf "\$%-35s -> class_name('%s');\n",$name.'_obj',$serial; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n",$name,$name.'_obj'; + } elsif (lc $pa_type eq 'xpl') { + $code .= sprintf "\$%-35s = new xPL_Item('%s');\n",$name.'_obj',$address; + $code .= sprintf "\$%-35s -> hidden(1);\n", $name.'_obj'; + $code .= sprintf "\$%-35s -> target_address('%s');\n",$name.'_obj',$address; + $code .= sprintf "\$%-35s -> class_name('%s');\n",$name.'_obj',$serial; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n",$name,$name.'_obj'; + } else { + print "\nUnrecognized .mht entry for PA: $record\n"; + return; } + } elsif($type =~ /^EIB/) { ($address, $name, $grouplist, @other) = @item_info; From 539e194b0823c725d3088389e4c53da6e1d9397d Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 30 Sep 2013 01:47:53 -0400 Subject: [PATCH 149/330] Clarify usage of zones of type object. --- code/common/pa_control.pl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index fa35a6eb7..3e9129813 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -126,6 +126,8 @@ sub pa_control_stub { if the command to turn on the pin you want is: BHC, then the Address is: BC For X10, the X10 address of the (likely) relay device. For xAP and xPL, use the IP address or hostname of the target device. + For "object", use the name of the object (without the $). You may use anything that + responds ON and OFF set commands. Tested with and Insteon device. Name: Give a name to the pa zone, usually the room name. You use these in the speak and play commands with rooms=. From 8243c11e7ddccf95743783a0078107e47b213299 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 30 Sep 2013 17:34:00 -0700 Subject: [PATCH 150/330] Insteon: Check if Code Can do Engine_Version Before Calling It Fix bad typos in coding. Not sure why I was attempting to dereferrence an object. Don't call engine_version on PLM object --- lib/Insteon/AllLinkDatabase.pm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 8321d9c22..8712366a9 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1026,7 +1026,10 @@ sub add_link my $is_controller = ($link_parms{is_controller}) ? 1 : 0; # check whether the link already exists # for I2CS devices the default data3 should be 01 no 00 - my $data3_default = ($$insteon_object->engine_version eq 'I2CS') ? '01' : '00'; + my $data3_default = '00'; + if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { + $data3_default = '01'; + } my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; # get the address via lookup into the hash my $key = lc $device_id . $group . $is_controller; @@ -1150,7 +1153,10 @@ sub update_link my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; # for I2CS devices the default data3 should be 01 no 00 - my $data3_default = ($$insteon_object->engine_version eq 'I2CS') ? '01' : '00'; + my $data3_default = '00'; + if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { + $data3_default = '01'; + } my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; my $deviceid = $insteon_object->device_id; my $subaddress = $data3; From c23ab978376337c01c7a59931af9c75ca0751ab5 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Tue, 1 Oct 2013 22:58:25 -0400 Subject: [PATCH 151/330] Imporove logging and add debug levels. --- code/common/pa_control.pl | 3 +- lib/PAobj.pm | 107 ++++++++++++++++++-------------------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index 3e9129813..bff9da14b 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -63,6 +63,7 @@ my $state = $v_pa_speakers->{state}; $v_pa_speakers->respond("app=pa Turning speakers $state..."); $state = ($state eq 'on') ? ON : OFF; + print "PA: Turning speakers $state\n" if $Debug{pa}; $pactrl->set('allspeakers',$state,'unmuted'); } @@ -82,7 +83,7 @@ sub pa_control_stub { my $rooms = $parms{rooms}; print "PA: control_stub: rooms=$rooms, mode=$mode\n" if $Debug{pa}; my $results = $pactrl->set($rooms,ON,$mode,%parms); - print "PA: control_stub set results: $results\n" if $Debug{pa}; + print "PA: control_stub set results: $results\n" if $Debug{pa} >=2; set $pa_speaker_timer $pa_timer if $results; } diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 34299cc86..e0d96cbd9 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -57,7 +57,7 @@ sub init { %pa_zone_types=(); my $ref2 = &::get_object_by_name("pa_allspeakers"); if (!$ref2) { - &::print_log("\n\nWARNING! PA Zones were not found! Your *.mht file probably doesn't list the PA zones correctly.\n\n"); + print("\n\nWARNING! PA Zones were not found! Your *.mht file probably doesn't list the PA zones correctly.\n\n"); return 0; } $self->check_group('default'); @@ -68,8 +68,7 @@ sub init { for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); my $type = $ref->get_type(); - print "PAObj: init: room=$room\n"; - print "PAObj: init: room=$room, zonetype=$type\n"; + &::print_log("PAObj: init: room=$room, zonetype=$type"); $pa_zone_types{$type}++ unless $pa_zone_types{$type}; if($type eq 'wdio') { @@ -89,24 +88,24 @@ sub init { } } - print "PAObj: speakers_wdio: $#speakers_wdio\n" if $main::Debug{pa}; - print "PAObj: speakers_x10: $#speakers_x10\n" if $main::Debug{pa}; - print "PAObj: speakers_xap: $#speakers_xap\n" if $main::Debug{pa}; - print "PAObj: speakers_xpl: $#speakers_xpl\n" if $main::Debug{pa}; - print "PAObj: speakers_obj: $#speakers_obj\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} || $#speakers_wdio gt -1; + &::print_log("PAObj: speakers_x10: $#speakers_x10") if $main::Debug{pa} || $#speakers_x10 gt -1; + &::print_log("PAObj: speakers_xap: $#speakers_xap") if $main::Debug{pa} || $#speakers_xap gt -1; + &::print_log("PAObj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} || $#speakers_xpl gt -1; + &::print_log("PAObj: speakers_obj: $#speakers_obj") if $main::Debug{pa} || $#speakers_obj gt -1; if ($#speakers_wdio > -1) { $self->init_weeder(@speakers_wdio); return 0 unless %pa_weeder_max_port; } if ($pa_zone_types{'x10'}) { - print "PAObj: x10 PA type initialized...\n" if $main::Debug{pa}; + &::print_log("PAObj: x10 PA type initialized...") if $main::Debug{pa}; } if ($pa_zone_types{'xap'}) { - print "PAObj: xAP PA type initialized...\n" if $main::Debug{pa}; + &::print_log("PAObj: xAP PA type initialized...") if $main::Debug{pa}; } if ($pa_zone_types{'xpl'}) { - print "PAObj: xPL PA type initialized...\n" if $main::Debug{pa}; + &::print_log("PAObj: xPL PA type initialized...") if $main::Debug{pa}; } return 1; } @@ -117,7 +116,7 @@ sub init_weeder my (%weeder_ref,%weeder_max); undef %pa_weeder_max_port; for my $room (@speakers) { - print "PAObj: init PA Room loaded: $room\n" if $main::Debug{pa}; + &::print_log("PAObj: init PA Room loaded: $room") if $main::Debug{pa}; my $ref = &::get_object_by_name('pa_' . $room . '_obj'); $ref->{state} = 'off'; my ($card,$id); @@ -125,14 +124,14 @@ sub init_weeder $weeder_ref{$card} = '' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; - print "PAObj: init card: $card, id: $id, Room: $room, List: $weeder_ref{$card}\n" if $main::Debug{pa}; + &::print_log("PAObj: init card: $card, id: $id, Room: $room, List: $weeder_ref{$card}") if $main::Debug{pa}; } for my $card ('A' .. 'P','a' .. 'p') { if ($weeder_ref{$card}) { my $data = $weeder_ref{$card}; $weeder_max{$card}=$self->last_char($data); - print "\nPAObj: init weeder board=$card, ports=$data, max port=" . $weeder_max{$card} . "\n" if $main::Debug{pa}; + &::print_log("PAObj: init weeder board=$card, ports=$data, max port=" . $weeder_max{$card}) if $main::Debug{pa}; } } %pa_weeder_max_port = %weeder_max; @@ -142,13 +141,14 @@ sub set { my ($self,$rooms,$state,$mode,%voiceparms) = @_; my $results = 0; - print "PAObj: delay: $$self{pa_delay}\n" if $main::Debug{pa}; - print "PAObj: set,mode: " . $mode . "\n" if $main::Debug{pa}; - print "PAObj: set,rooms: " . $rooms . "\n" if $main::Debug{pa}; + &::print_log("PAObj: delay: $$self{pa_delay}\n") if $main::Debug{pa} >=3; + &::print_log("PAObj: set,mode: " . $mode . ",rooms: " . $rooms) if $main::Debug{pa} >=3; my @speakers = $self->get_speakers($rooms); @speakers = $self->get_speakers('') if $#speakers == -1; + &::print_log("PAObj: Proposed rooms: ".join(', ', @speakers)) if $main::Debug{pa} >=2; @speakers = $self->get_speakers_speakable($mode,@speakers); + &::print_log("PAObj: Will speak in rooms: ".join(', ', @speakers)) if $main::Debug{pa}; my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl); @@ -156,32 +156,32 @@ sub set my $ref = &::get_object_by_name("pa_$room"); my $type = lc $ref->get_type(); if($type eq 'wdio' || $type eq 'wdio_old') { - print "PAObj: speakers_wdio: Adding $room\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_wdio: Adding $room") if $main::Debug{pa} >=3; push(@speakers_wdio,$room); } if($type eq 'x10') { - print "PAObj: speakers_x10: Adding $room\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_x10: Adding $room") if $main::Debug{pa} >=3; push(@speakers_x10,$room); } if($type eq 'xap') { - print "PAObj: speakers_xap: Adding $room\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_xap: Adding $room") if $main::Debug{pa} >=3; push(@speakers_xap,$room) if $state eq 'on'; #Only need to send if speech is starting } if($type eq 'xpl') { - print "PAObj: speakers_xpl: Adding $room\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_xpl: Adding $room") if $main::Debug{pa} >=3; push(@speakers_xpl,$room) if $state eq 'on'; #Only need to send if speech is starting } if($type eq 'object') { - print "PAObj: speakers_object: Adding $room\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_object: Adding $room") if $main::Debug{pa} >=3; push(@speakers_obj,$room); } } - print "PAObj: speakers_wdio: $#speakers_wdio\n" if $main::Debug{pa}; - print "PAObj: speakers_x10: $#speakers_x10\n" if $main::Debug{pa}; - print "PAObj: speakers_xap: $#speakers_xap\n" if $main::Debug{pa}; - print "PAObj: speakers_xpl: $#speakers_xpl\n" if $main::Debug{pa}; - print "PAObj: speakers_obj: $#speakers_obj\n" if $main::Debug{pa}; + &::print_log("PAObj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} >=2 || $#speakers_wdio gt -1; + &::print_log("PAObj: speakers_x10: $#speakers_x10") if $main::Debug{pa} >=2 || $#speakers_x10 gt -1; + &::print_log("PAObj: speakers_xap: $#speakers_xap") if $main::Debug{pa} >=2 || $#speakers_xap gt -1; + &::print_log("PAObj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} >=2 || $#speakers_xpl gt -1; + &::print_log("PAObj: speakers_obj: $#speakers_obj") if $main::Debug{pa} >=2 || $#speakers_obj gt -1; #TODO: Properly handle $results across multiple types #TODO: Break up the wdio zones based on serial port, in case there are more than one. @@ -200,7 +200,7 @@ sub set_obj { my ($self,$state,@speakers) = @_; for my $room (@speakers) { - print "PAObj: set_obj: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + &::print_log("PAObj: set_obj: " . $room . " / " . $state) if $main::Debug{pa} >=2; my $ref = &::get_object_by_name("pa_$room"); if ($ref) { $ref->set($state); @@ -214,12 +214,12 @@ sub set_x10 my ($x10_list,$pa_x10_hc,$ref,$refobj); for my $room (@speakers) { - print "PAObj: set_x10: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + &::print_log("PAObj: set_x10: " . $room . " / " . $state) if $main::Debug{pa} >=3; $ref = &::get_object_by_name('pa_'.$room); $refobj = &::get_object_by_name('pa_'.$room.'_obj'); if ($refobj && $ref) { my ($id) = $ref->get_address(); - print "PAObj: set_x10 ID: $id, State: $state, Room: $room\n" if $main::Debug{pa}; + &::print_log("PAObj: set_x10 ID: $id, State: $state, Room: $room") if $main::Debug{pa} >=2; $refobj->set($state); } } @@ -231,13 +231,13 @@ sub set_xap { my %voiceparms = %$param2; return unless $#speakers > -1; for my $room (@speakers) { - print "PAObj: set_xap: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + &::print_log("PAObj: set_xap: " . $room . " / " . $state) if $main::Debug{pa} >=3; my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { $ref->send_message($ref->target_address, $ref->class_name => {say => $voiceparms{text}, volume => $voiceparms{volume}, mode => $voiceparms{mode}, voice => $voiceparms{voice} }); - print "PAObj: xap cmd: $ref->{object_name} is sending voice text: $voiceparms{text}\n" if $main::Debug{pa}; + &::print_log("PAObj: xap cmd: $ref->{object_name} is sending voice text: $voiceparms{text}") if $main::Debug{pa}; } else { - print "PAObj: Unable to locate object for: pa_$room\n" if $main::Debug{pa}; + &::print_log("PAObj: Unable to locate object for: pa_$room"); } } } @@ -248,7 +248,7 @@ sub set_xpl { my %voiceparms = %$param2; return unless $#speakers > -1; for my $room (@speakers) { - print "PAObj: set_xpl: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + &::print_log("PAObj: set_xpl: " . $room . " / " . $state) if $main::Debug{pa} >=3; my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { my $max_length = $::config_parms{"pa_$room" . "_maxlength"}; @@ -258,9 +258,9 @@ sub set_xpl { $text = substr($text, 0, $max_length) if $max_length < length($text); } $ref->send_cmnd($ref->class_name => {speech => $text, voice => $voiceparms{voice}, volume => $voiceparms{volume}, mode => $voiceparms{mode} }); - print "PAObj: set_xpl: $ref->{object_name} is sending voice text: $voiceparms{text}\n" if $main::Debug{pa}; + &::print_log("PAObj: set_xpl: $ref->{object_name} is sending voice text: $voiceparms{text}") if $main::Debug{pa}; } else { - print "PAObj: Unable to locate object for: pa_$room\n" if $main::Debug{pa}; + &::print_log("PAObj: Unable to locate object for: pa_$room"); } } } @@ -272,18 +272,18 @@ sub set_weeder my $weeder_command=''; my $command=''; for my $room (@speakers) { - print "PAObj: set_weeder: " . $room . " / " . $state . "\n" if $main::Debug{pa}; + &::print_log("PAObj: set_weeder: " . $room . " / " . $state) if $main::Debug{pa} >=3; my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { $ref->{state} = $state; my ($card,$id) = $ref->{id_by_state}{'on'} =~ /^D?(.)H(.)/s; $weeder_ref{$card}='' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; - print "PAObj: card: $card, id: $id, Room: $room\n" if $main::Debug{pa}; + &::print_log("PAObj: card: $card, id: $id, Room: $room") if $main::Debug{pa} >=2; } } - $self->print_speaker_states() if $main::Debug{pa}; + $self->print_speaker_states() if $main::Debug{pa} >=3; for my $card ('A' .. 'P','a' .. 'p') { if ($weeder_ref{$card}) { @@ -294,7 +294,7 @@ sub set_weeder } } return 0 unless $weeder_command; - print "PAObj: Sending $weeder_command to the weeder card(s)\n" if $main::Debug{pa}; + &::print_log("PAObj: Sending $weeder_command to the weeder card(s)") if $main::Debug{pa}; $weeder_command =~ s/\\r/\r/g; &Serial_Item::send_serial_data($weeder_port, $weeder_command) if $main::Serial_Ports{$weeder_port}{object}; return 1; @@ -303,7 +303,6 @@ sub set_weeder sub get_weeder_string { my ($self,$card,$data) = @_; - my $bit_counter=0; my ($bit_flag,$state,$ref,$bit,$byte_code,$weeder_code,$id); @@ -324,7 +323,7 @@ sub get_weeder_string } $bit_flag = ($state eq 'on') ? 1 : 0; # get 0 or 1 - print "PAObj: get_weeder_string card: $card, bit=$bit state=$bit_flag\n" if $main::Debug{pa}; + &::print_log("PAObj: get_weeder_string card: $card, bit=$bit state=$bit_flag") if $main::Debug{pa} >=2; $byte_code += ($bit_flag << $bit_counter); # get bit in byte position if ($bit_counter++ >= 3) { @@ -352,7 +351,7 @@ sub get_speakers my ($self,$rooms) = @_; my @pazones; - print "PAObj: get_speakers,rooms: " . $rooms . "\n" if $main::Debug{pa}; + &::print_log("PAObj: get_speakers,rooms: " . $rooms) if $main::Debug{pa} >=2; if ($::mh_speakers->{rooms}) { $rooms = $::mh_speakers->{rooms}; $::mh_speakers->{rooms} = ''; @@ -364,15 +363,15 @@ sub get_speakers no strict 'refs'; my $ref = &::get_object_by_name("pa_$room"); if ($ref) { - print "PAObj: name=$ref->{object_name}\n" if $main::Debug{pa}; + &::print_log("PAObj: name=$ref->{object_name}") if $main::Debug{pa}; if (UNIVERSAL::isa($ref,'Group')) { - print "PAObj: It's a group!\n" if $main::Debug{pa}; + &::print_log("PAObj: It's a group!") if $main::Debug{pa} >=2; for my $grouproom ($ref->list) { $grouproom = $grouproom->get_object_name; $grouproom =~ s/^\$pa_//; $grouproom =~ s/^\$paxpl_//; $grouproom =~ s/^\$paxap_//; - print "PAObj: - member: $grouproom\n" if $main::Debug{pa}; + &::print_log("PAObj: - member: $grouproom\n") if $main::Debug{pa} >=2; push(@pazones, $grouproom); } } else { @@ -388,13 +387,13 @@ sub get_speakers sub check_group { my ($self,$group) = @_; - print "PAObj: check group=$group\n" if $main::Debug{pa}; + &::print_log("PAObj: check group=$group") if $main::Debug{pa} >=2; my $ref = &::get_object_by_name("pa_$group"); - if (!$ref) {print "Error! Group does not exist: $group\n"; return;} + if (!$ref) {&::print_log("PAObj: check group: Error! Group does not exist: $group"); return;} my @list = $ref->list; - print "PAObj: check group=$group, list=$#list\n" if $main::Debug{pa}; + &::print_log("PAObj: check group=$group, list=$#list") if $main::Debug{pa} >=2; if ($#list == -1) { - print "PAObj: check populating group: $group!\n" if $main::Debug{pa}; + &::print_log("PAObj: check populating group: $group!") if $main::Debug{pa}; for my $room ($self->get_speakers('allspeakers')) { my $ref2 = &::get_object_by_name("pa_$room"); $ref->add($ref2); @@ -412,13 +411,13 @@ sub get_speakers_speakable for my $room (@zones) { my $ref = &::get_object_by_name("pa_$room"); - print "PAObj: speakable: name=$ref->{object_name}\n" if $main::Debug{pa}; + &::print_log("PAObj: speakable: name=$ref->{object_name}") if $main::Debug{pa} >=3; if ($ref->{sleeping} == 0) { $ref->{mode} = 'normal' unless $ref->{mode}; my $gss_mode = $ref->{mode}; if ($gss_mode ne 'sleeping' && ($gss_mode eq 'normal' || $mode eq 'unmuted')) { push(@pazones,$room); - print "PAObj: speakable: Pushing $room into pazones array:$#pazones\n" if $main::Debug{pa}; + &::print_log("PAObj: speakable: Pushing $room into pazones array:$#pazones") if $main::Debug{pa} >=2; } } } @@ -440,7 +439,7 @@ sub print_speaker_states $ref = &::get_object_by_name("pa_$speaker"); $room = $ref->{object_name}; $room =~ s/^\$pa_//; - print "PAObj: name=$room, state=$ref->{state}\n" if $main::Debug{pa}; + &::print_log("PAObj: name=$room, state=$ref->{state}") if $main::Debug{pa}; } } @@ -507,10 +506,8 @@ sub get_type return $$self{other}; } - 1; - =back =head2 INI PARAMETERS From ce1c31dd1886e85e2e815a6d738ddd83260e3eca Mon Sep 17 00:00:00 2001 From: hplato Date: Wed, 2 Oct 2013 19:28:08 -0600 Subject: [PATCH 152/330] initial commit --- bin/get_areacodes.pl | 38 + bin/mythnotify | 33 + bin/mythnotify.pl | 33 + bin/skypecheck | 43 + bin/skypecheck.bat | 2 + lib/CID_Announce.pm | 137 +- lib/CID_Lookup.pm | 111 +- lib/Caller_ID.pm | 99 +- lib/Event_Logger.pm | 407 ++++++ lib/Telephony_Interface.pm | 8 +- lib/handy_net_utilities.pl | 60 +- lib/imap_utils.pl | 134 +- lib/site/DateTime/TimeZone.pm | 126 +- lib/vsDB.pm | 2228 +++++++++++++++++---------------- lib/vsLock.pm | 1223 +++++++++--------- web/bin/button_hp.pl | 209 ++++ web/bin/button_sensor.pl | 174 +++ web/bin/icon_panic.pl | 16 + web/bin/phone_in.pl | 7 +- web/bin/statuspanel.pl | 64 + web/graphics/icons/pcomm2.png | Bin 578 -> 579 bytes web/newclock/index.shtml | 257 ++++ web/newclock/maps/0.jpg | Bin 0 -> 26868 bytes web/newclock/maps/1.jpg | Bin 0 -> 26715 bytes web/newclock/maps/10.jpg | Bin 0 -> 25296 bytes web/newclock/maps/11.jpg | Bin 0 -> 25023 bytes web/newclock/maps/12.jpg | Bin 0 -> 24888 bytes web/newclock/maps/13.jpg | Bin 0 -> 25047 bytes web/newclock/maps/14.jpg | Bin 0 -> 25186 bytes web/newclock/maps/15.jpg | Bin 0 -> 25475 bytes web/newclock/maps/16.jpg | Bin 0 -> 25745 bytes web/newclock/maps/17.jpg | Bin 0 -> 25806 bytes web/newclock/maps/18.jpg | Bin 0 -> 27276 bytes web/newclock/maps/19.jpg | Bin 0 -> 25866 bytes web/newclock/maps/2.jpg | Bin 0 -> 26446 bytes web/newclock/maps/20.jpg | Bin 0 -> 25868 bytes web/newclock/maps/21.jpg | Bin 0 -> 26140 bytes web/newclock/maps/22.jpg | Bin 0 -> 26614 bytes web/newclock/maps/23.jpg | Bin 0 -> 26723 bytes web/newclock/maps/3.jpg | Bin 0 -> 26311 bytes web/newclock/maps/4.jpg | Bin 0 -> 26272 bytes web/newclock/maps/5.jpg | Bin 0 -> 26246 bytes web/newclock/maps/6.jpg | Bin 0 -> 26379 bytes web/newclock/maps/7.jpg | Bin 0 -> 26289 bytes web/newclock/maps/8.jpg | Bin 0 -> 26058 bytes web/newclock/maps/9.jpg | Bin 0 -> 25652 bytes web/newclock/numbers/text.gif | Bin 712 -> 1448 bytes 47 files changed, 3385 insertions(+), 2024 deletions(-) create mode 100755 bin/get_areacodes.pl create mode 100755 bin/mythnotify create mode 100644 bin/mythnotify.pl create mode 100755 bin/skypecheck create mode 100755 bin/skypecheck.bat create mode 100755 lib/Event_Logger.pm create mode 100644 web/bin/button_hp.pl create mode 100644 web/bin/button_sensor.pl create mode 100644 web/bin/icon_panic.pl create mode 100644 web/bin/statuspanel.pl create mode 100644 web/newclock/index.shtml create mode 100644 web/newclock/maps/0.jpg create mode 100644 web/newclock/maps/1.jpg create mode 100644 web/newclock/maps/10.jpg create mode 100644 web/newclock/maps/11.jpg create mode 100644 web/newclock/maps/12.jpg create mode 100644 web/newclock/maps/13.jpg create mode 100644 web/newclock/maps/14.jpg create mode 100644 web/newclock/maps/15.jpg create mode 100644 web/newclock/maps/16.jpg create mode 100644 web/newclock/maps/17.jpg create mode 100644 web/newclock/maps/18.jpg create mode 100644 web/newclock/maps/19.jpg create mode 100644 web/newclock/maps/2.jpg create mode 100644 web/newclock/maps/20.jpg create mode 100644 web/newclock/maps/21.jpg create mode 100644 web/newclock/maps/22.jpg create mode 100644 web/newclock/maps/23.jpg create mode 100644 web/newclock/maps/3.jpg create mode 100644 web/newclock/maps/4.jpg create mode 100644 web/newclock/maps/5.jpg create mode 100644 web/newclock/maps/6.jpg create mode 100644 web/newclock/maps/7.jpg create mode 100644 web/newclock/maps/8.jpg create mode 100644 web/newclock/maps/9.jpg diff --git a/bin/get_areacodes.pl b/bin/get_areacodes.pl new file mode 100755 index 000000000..df7ee44a8 --- /dev/null +++ b/bin/get_areacodes.pl @@ -0,0 +1,38 @@ +#!/usr/bin/perl +use strict; +use warnings; +use HTML::TableExtract; +use LWP::Simple; + +use Time::Local; + +my $url = "http://www-cse.ucsd.edu/users/bsy/area.html"; +my $html = get($url); +my $table = HTML::TableExtract->new; +my $row; +print "# Downloaded from $url\n"; +print "# On ". (localtime) . "\n"; +print "#\n"; +$table->parse($html); +# Table parsed, extract the data. + foreach $row ($table->rows) { + next unless @$row[0] =~ m/^[\-|\d]/; + my $ac = @$row[0]; + my $prov = @$row[1]; + my $tz = @$row[2]; + my $description = @$row[3]; + $description =~ s/^\s+//g; + if ($description =~ m/^canada\:/ig) { + $description =~ s/^canada\:\s+//i; + $description =~ s/\(.*\)$//; + $description =~ s/^.+\:\s+//; #remove additional province details + $description =~ s/[\-\-|\;].*//; + #my $place; + #($place) = $description =~ /\:(.*)\(.*\)$/i; #get rid of the end stuff + print "$ac $prov $tz $description\n"; + #print "\t\t$place\n"; + } else { + print "$ac $prov $tz $description\n"; + } +# print join(',', @$row), "\n"; + } diff --git a/bin/mythnotify b/bin/mythnotify new file mode 100755 index 000000000..3c1e8f6bf --- /dev/null +++ b/bin/mythnotify @@ -0,0 +1,33 @@ +#!/usr/bin/perl + +use IO::Socket; +use Getopt::Long; + +$MH_ADDRESS = '192.168.0.51'; +$MH_PORT = '5252'; +$username = 'mythtv_mh'; +$password = 'mythtv_mh'; + +GetOptions("h" => \$help, "c=s" => \$chanid, "s=s" => \$starttime, "e=s" => \$endtime, "t=s" => \$title, "st=s" => \$subtitle, "d=s" => \$description ); + + +# Try to create a TCP socket to Misterhouse to establish communications with MisterHouse. +$remote = IO::Socket::INET->new(Proto => "tcp", PeerAddr => "$MH_ADDRESS", PeerPort => "$MH_PORT",) + or die "cannot connect to misterhouse on port $MH_PORT at $MH_ADDRESS"; + +# Do a simple login to Misterhouse. Not really needed if you are behind a firewall but just in case you are not +# it is included. Keep in mind this is all plain text so if you are worried about that you should be aware. +print $remote "Login: $username\n"; +print $remote "Secret: $password\n"; + +print $remote "chanid: $chanid\n"; +print $remote "starttime: $starttime\n"; +print $remote "endtime: $endtime\n"; +print $remote "title: $title\n"; +print $remote "subtitle: $subtitle\n"; +print $remote "description: $description\n"; +print $remote ":done:\n"; + +close($remote); + +system qq [echo $chanid,$starttime,$endtime,$title,$subtitle,$description >> /tmp/mynotify.txt]; diff --git a/bin/mythnotify.pl b/bin/mythnotify.pl new file mode 100644 index 000000000..3c1e8f6bf --- /dev/null +++ b/bin/mythnotify.pl @@ -0,0 +1,33 @@ +#!/usr/bin/perl + +use IO::Socket; +use Getopt::Long; + +$MH_ADDRESS = '192.168.0.51'; +$MH_PORT = '5252'; +$username = 'mythtv_mh'; +$password = 'mythtv_mh'; + +GetOptions("h" => \$help, "c=s" => \$chanid, "s=s" => \$starttime, "e=s" => \$endtime, "t=s" => \$title, "st=s" => \$subtitle, "d=s" => \$description ); + + +# Try to create a TCP socket to Misterhouse to establish communications with MisterHouse. +$remote = IO::Socket::INET->new(Proto => "tcp", PeerAddr => "$MH_ADDRESS", PeerPort => "$MH_PORT",) + or die "cannot connect to misterhouse on port $MH_PORT at $MH_ADDRESS"; + +# Do a simple login to Misterhouse. Not really needed if you are behind a firewall but just in case you are not +# it is included. Keep in mind this is all plain text so if you are worried about that you should be aware. +print $remote "Login: $username\n"; +print $remote "Secret: $password\n"; + +print $remote "chanid: $chanid\n"; +print $remote "starttime: $starttime\n"; +print $remote "endtime: $endtime\n"; +print $remote "title: $title\n"; +print $remote "subtitle: $subtitle\n"; +print $remote "description: $description\n"; +print $remote ":done:\n"; + +close($remote); + +system qq [echo $chanid,$starttime,$endtime,$title,$subtitle,$description >> /tmp/mynotify.txt]; diff --git a/bin/skypecheck b/bin/skypecheck new file mode 100755 index 000000000..a6014bd38 --- /dev/null +++ b/bin/skypecheck @@ -0,0 +1,43 @@ +#!/usr/bin/perl -w + +use strict; + + +use XML::Simple; +use LWP::Simple; +use LWP::UserAgent; + +my $status_url="http://mystatus.skype.com/"; + +my $progname = "skype check"; +my $progver = "v1.0 2009-11-20"; +my $DB = 0; +my $help; + +unless ($ARGV[0]) { + &help; +} else { + + my @accounts = split /,/,$ARGV[0]; + + foreach my $account (@accounts) { + my $ua = new LWP::UserAgent; + my $req = new HTTP::Request GET => $status_url . "$account" . ".txt"; + my $results = $ua->request($req); + my $data; + if (!($results->is_success)) { + $data = "Error:" . $results->status_line; + } else { + $data = $results->content . ":"; + } + print "$account:" . $data ."\n"; + + } +} + +sub help { + print "$progname $progver \n"; + print "usage: $0 skypename[,skypename,skypename]\n\n"; + print "Reads skypename status from mystatus.skype.com\n"; + print "requires account to have \'allow my status to be viewed on the web\' preference\n"; +} \ No newline at end of file diff --git a/bin/skypecheck.bat b/bin/skypecheck.bat new file mode 100755 index 000000000..6d14b1aed --- /dev/null +++ b/bin/skypecheck.bat @@ -0,0 +1,2 @@ +@mh -run skypecheck %1 %2 %3 +@rem perl -S skypecheck %1 %2 %3 diff --git a/lib/CID_Announce.pm b/lib/CID_Announce.pm index 3d39cb555..fd4d42de7 100644 --- a/lib/CID_Announce.pm +++ b/lib/CID_Announce.pm @@ -1,51 +1,61 @@ -=head1 B +use strict; -=head2 SYNOPSIS +# $Revision: 1117 $ +# $Date: 2007-06-04 09:22:13 -0600 (Mon, 04 Jun 2007) $ -Example initialization: +=begin comment +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - use CID_Announce; - $cid = new CID_Announce($telephony_driver,'Call from $name $snumber.'); +File: + CID_Announce.pm -Constructor Parameters: +Description: + Announces a call. CID with category of 'reject' will not be announced. - ex. $x = new CID_Announce($y,$z); - $x - Reference to the class - $y - Telephony driver reference - $z - Format for speaking - Following variables are substitued in "" - $name,$first,$middle,$last,$number,$fnumber - (formated),$snumber(speakable),$type,$category,$city, - $state,$time,$areacode,$prefix,$suffix,$soundfile +Author: + Jason Sharpee + jason@sharpee.com -Input states: +License: + This free software is licensed under the terms of the GNU public license. - "cid" - Caller ID event - "ring" - Ring event 'to pass along to other consumers of this object' +Usage: -Output states: + Example initialization: - "cid" - Caller ID event - "ring" - Ring event 'to pass along to other consumers of this object' + use CID_Announce; + $cid = new CID_Announce($telephony_driver,'Call from $name $snumber.'); -=head2 DESCRIPTION + Constructor Parameters: + ex. $x = new CID_Announce($y,$z); + $x - Reference to the class + $y - Telephony driver reference + $z - Format for speaking + Following variables are substitued in "" + $name,$first,$middle,$last,$number,$fnumber + (formated),$snumber(speakable),$type,$category,$city, + $state,$time,$areacode,$prefix,$suffix,$soundfile -Announces a call. CID with category of 'reject' will not be announced. + Input states: + "cid" - Caller ID event + "ring" - Ring event 'to pass along to other consumers of this object' -=head2 INHERITS + Output states: + "cid" - Caller ID event + "ring" - Ring event 'to pass along to other consumers of this object' -B + For example see g_phone.pl -=head2 METHODS +Bugs: + There isnt a whole lot of error handling currently present in this version. Drop me + an email if you are seeing something odd. -=over +Special Thanks to: + Bruce Winter - MH -=item B +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut - -use strict; - package CID_Announce; @CID_Announce::ISA = ('Telephony_Item'); @@ -75,7 +85,7 @@ sub new sub add { my ($self, $p_telephony) = @_; -# print "CID ADD $p_telephony"; + print "CID ADD $p_telephony" if $main::Debug{callerid}; $p_telephony->tie_items($self,'cid') if defined $p_telephony; } @@ -128,7 +138,7 @@ sub announce my ($self,$p_telephony,$p_speak_format,$p_local_area_code) = @_; my $response=$self->parse_format($p_telephony,$p_speak_format,$p_local_area_code); -# print "CID Announce $response,$p_telephony,$p_speak_format"; + print "CID Announce $response,$p_telephony,$p_speak_format\n" if $main::Debug{callerid}; return if $response =~ /MESSAGE WAITING/; # Don't announce message waiting data return if $response =~ /\-MSG OFF\-/; if ($response=~ /\.wav$/) { @@ -181,16 +191,19 @@ sub parse_format my $areac = $areacodes[0]; $number =~ s/^$areac//; - print "CID_nnounce data: type=$type name=$name number=$number\n" if $main::Debug{phone}; + print "CID_Announce data: local_area=$areac type=$type name=$name number=$number\n" if $main::Debug{phone}; if ($p_telephony->isa('CID_Lookup')) { # print "CID ISA"; $areacode = $p_telephony->areacode(); $fnumber = $p_telephony->formated_number(); $snumber = $p_telephony->speakable_number(); - $fnumber =~ s/^$areac//g; +# really remove all area codes as we'll speak them later +# $fnumber =~ s/^$areac//g; $areac =~ s/([0-9])/$1 /g; - $snumber =~ s/^$areac//g; +# $snumber =~ s/^$areac//g; + $fnumber =~ s/^\d\d\d//g; + $snumber =~ s/^\d\s\d\s\d\s//g; # Make other vars available $address = $p_telephony->address(); @@ -208,15 +221,32 @@ sub parse_format $category = $p_telephony->category(); # $format1 = "$first $middle $last"; $format1 = $name; + $format1 = $snumber if $name eq 'unknown'; $format1 = $snumber if $name =~ 'UNKNOWN CALLER'; $format1 = "Unknown" if $type eq 'unknown' and !$snumber; $format1 = $snumber unless $format1 =~ /\S/; +print "CID_Announce: areacodes[0]=$areacodes[0] areacode=$areacode prefix=$prefix snumber=$snumber fnumber=$fnumber city=$city state=$state\n"; if ($areacodes[0] ne $areacode) { - $format1 .= " in $city" if $city =~ /\S/ and +print "CID_Announce: Area codes do not match. city=$city state=$state config_parms{city}=$::config_parms{city} config_parms{state}=$::config_parms{state}\n"; + my $outarea; + my $ac_speak = $areacode; + $ac_speak =~ s/(\d)/$1 /g; + $outarea = " from "; + $outarea .= "$city," if $city =~ /\S/ and (lc $city ne lc $::config_parms{city} or lc $state ne lc $::config_parms{state}); - $format1 =~ s/\s*$//; - $format1 .= ", $state" if $state and lc $state ne lc $::config_parms{state} and lc $state ne lc $city; + #$outarea =~ s/\s*$//; + $outarea .= "$state" if $state and lc $state ne lc $::config_parms{state} and lc $state ne lc $city and $state ne "--"; + if ($outarea eq " from ") { + if ($areacode) { + $outarea = " from area code $ac_speak"; + } else { + $outarea = " from unknown area code"; + } + } +print "CID_Announce: outarea=[$outarea]\n"; + $format1 .= $outarea; } + $format1 = "TollFree number" if $prefix eq "800" or $prefix eq "866" or $prefix eq "888"; } @@ -229,39 +259,10 @@ sub parse_format { $speak_string = $soundfile; } -# print "CID PARSE $speak_string"; + print "CID PARSE $speak_string\n" if $main::Debug{callerid}; return $speak_string; } 1; - - -=back - -=head2 INI PARAMETERS - -NONE - -=head2 AUTHOR - -Jason Sharpee -jason@sharpee.com - -Special Thanks to: -Bruce Winter - MH - -=head2 SEE ALSO - -For example see g_phone.pl - -=head2 LICENSE - -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. - -=cut diff --git a/lib/CID_Lookup.pm b/lib/CID_Lookup.pm index a2e383da0..a2d6aad29 100644 --- a/lib/CID_Lookup.pm +++ b/lib/CID_Lookup.pm @@ -1,45 +1,54 @@ -=head1 B - -=head2 SYNOPSIS - -Example initialization: +use strict; +=begin comment +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - use CID_Lookup; - $cid = new CID_Lookup($telephony_driver); +File: + CID_Lookup.pm -Constructor Parameters: +Description: + Translates a caller name and number to more information based on file data - $x = new CID_lookup($y); - $x - Reference to the class - $y - Telephony driver reference +Author: + Jason Sharpee + jason@sharpee.com -Input states: +License: + This free software is licensed under the terms of the GNU public license. - "cid" - Caller ID event - "ring" - Ring event 'to pass along to other consumers of this object' +Usage: -Output states: + Example initialization: - "cid" - Caller ID event - "ring" - Ring event 'to pass along to other consumers of this object' + use CID_Lookup; + $cid = new CID_Lookup($telephony_driver); -=head2 DESCRIPTION + Constructor Parameters: + ex. $x = new CID_lookup($y); + $x - Reference to the class + $y - Telephony driver reference -Translates a caller name and number to more information based on file data + Input states: + "cid" - Caller ID event + "ring" - Ring event 'to pass along to other consumers of this object' -=head2 INHERITS + Output states: + "cid" - Caller ID event + "ring" - Ring event 'to pass along to other consumers of this object' -B + For example see g_phone.pl -=head2 METHODS +Bugs: + There isnt a whole lot of error handling currently present in this version. Drop me + an email if you are seeing something odd. -=over +Special Thanks to: + Bruce Winter - MH + Tim Doyle - New Area Code format + Clive Freeman -=item B +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut - -use strict; package CID_Lookup; @CID_Lookup::ISA = ('Telephony_Item'); @@ -265,14 +274,14 @@ sub parse_name my $l_middle; #first determine if "Last, First" or not (possibly company instead) -# &::print_log("CID H in loop :$p_name:"); + &::print_log("CID H in loop :$p_name:") if $main::Debug{callerid}; if ($p_name =~ /,/g) { -# &::print_log("CID L in loop :$p_name:"); + &::print_log("CID L in loop :$p_name:") if $main::Debug{callerid}; #put a space after a comma for scrunched CID names. $p_name=~ s/,(\S)/, $1/g; $self->cid_name($p_name); -# &::print_log("CID J in loop :$p_name:"); + &::print_log("CID J in loop :$p_name:") if $main::Debug{callerid}; ($l_last,$l_first,$l_middle) = split(' ',$p_name); $l_last =~ s/,//g; # remove commas @@ -438,6 +447,7 @@ sub lookup_areacode my ($areacode,$state,$city,$timeoffset); my (%state_by_abbrv,$state_abbrv,$state_name); +print "CID_Lookup: looking up state names in $p_state_file"; open (STATENAME, $p_state_file) or print "\nError, could not find the state file $p_state_file\n"; while () { @@ -449,17 +459,18 @@ sub lookup_areacode close STATENAME; +print "\nCID_Lookup: looking up area codes for $::config_parms{country} in $p_area_code_file\n"; open (AREACODE, $p_area_code_file) or print "\nError, could not find the area code file $p_area_code_file\n"; while () { next if /^\#/; - if ($::config_parms{country} =~ /US|CANADA/i) + if ($::config_parms{country} =~ /US|CANADA|CA/i) { + $_ =~ s/\(.*\)$//; #remove end junk # Delete descriptors like (Southern) Texas ... too much to speak $_ =~ s/\(.+?\)//; #406 All parts of Montana -# &::print_log("Line ". $_); #Old Format code # ($areacode, $state) = $_ =~ /(\d\d\d) All parts of (.+)/; # ($areacode, $city, $state) = $_ =~ /(\d\d\d)(.*), *(.+)/ unless $state; @@ -472,10 +483,11 @@ sub lookup_areacode if ($city =~ /.*,.*/) { ($city) = $city =~ /(.*),.*/; } - + $city =~ s/^\s+|\s+$//g; #remove leading/trailing whitespace + $city =~ s/[\-\-|\;].*//; #remove stuff after --, ; next unless $city; -# &::print_log($self->areacode() . ", $areacode, $city, $state"); +print "CID_Lookup: \$self->areacode=" .$self->areacode() . ", areacode=$areacode, city=$city, state=$state, \n"; next unless $city; if ($areacode eq $self->areacode()) { @@ -502,7 +514,7 @@ sub lookup_areacode $city =~ s/ .+$//; $state=$city; - print "Checking: areacode=$areacode city=$city\n" if $::config_parms{debug} eq 'phone'; + print "Checking: areacode=$areacode city=$city\n" if $main::Debug{phone}; next unless $city; if ($self->number() =~ /^$areacode/) { $self->city($city); @@ -534,34 +546,3 @@ sub lookup_areacode } 1; - -=back - -=head2 INI PARAMETERS - -NONE - -=head2 AUTHOR - -Jason Sharpee -jason@sharpee.com - -Special Thanks to: -Bruce Winter - MH -Tim Doyle - New Area Code format -Clive Freeman - -=head2 SEE ALSO - -For example see g_phone.pl - -=head2 LICENSE - -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. - -=cut - diff --git a/lib/Caller_ID.pm b/lib/Caller_ID.pm index 6dadcd386..16674b5d1 100644 --- a/lib/Caller_ID.pm +++ b/lib/Caller_ID.pm @@ -1,38 +1,18 @@ -=head1 B - -=head2 SYNOPSIS - -None - -=head2 DESCRIPTION - -NONE - -=head2 INHERITS - -NONE - -=head2 METHODS - -=over - -=item B - -=cut - package Caller_ID; use strict; -use vars '%name_by_number', '%reject_name_by_number', '%state_by_areacode', '%wav_by_number', '%group_by_number'; +use vars '%name_by_number', '%reject_name_by_number', '%city_by_areacode', '%state_by_areacode', '%wav_by_number', '%group_by_number'; my ($my_areacode, @my_areacodes, $my_state); my $caller_file_2 = 1; my $caller_id_debug = 0; +$caller_id_debug = 1 if $main::Debug{callerid}; +print "Caller ID debugging enabled" if $caller_id_debug; sub make_speakable { my($data, $format,$local_area_code_language) = @_; -=cut +=cut begin format=1: Weeder CID data looks like this: I03/18 22:05 507-288-1030 WINTER BRUCE LA @@ -90,7 +70,7 @@ Format=4 NetCallerID (http://ugotcall.com/nci.htm) ###DATE06191942...NMBR...NAME-UNKNOWN CALLER-+++ -=cut +=cut end # Switch name strings so first last, not last first. @@ -101,6 +81,8 @@ Format=4 NetCallerID (http://ugotcall.com/nci.htm) # Last First M # Last M First +print "CallerID DBG: data=[$data] format=$format\n"; + if ($format == 2) { ($date) = $data =~ /DATE *= *(\S+)/s; ($time) = $data =~ /TIME *= *(\S+)/s; @@ -108,7 +90,8 @@ Format=4 NetCallerID (http://ugotcall.com/nci.htm) ($number) = $data =~ /NMBR *= *(\S+)/s; # ($name) = $data =~ /NAME *= *(.+)/s; - ($name) = $data =~ /NAME *= *([^\n]+)/s; +# ($name) = $data =~ /NAME *= *([^\n]+)/s; + ($name) = $data =~ /NAME *= *(.+)\sNMBR/s; #HPC name is between NAME = & NMBR ($number) = $data =~ /FM:(\S+)/s unless $number; ($numberTo) = $data =~ /TO:(\S+)/s; @@ -116,8 +99,9 @@ Format=4 NetCallerID (http://ugotcall.com/nci.htm) print "phone number=$number numberTo=$numberTo name=$name\n" if $caller_id_debug; $name = substr($name, 0, 15); -# $name = 'Unavailable' if $name =~ /^O$/; # Jay's & Chaz's exceptions - $name = 'Out of Area' if $name =~ /^O$/; # Jay's & Chaz's exceptions + $name = 'Unavailable' if $name =~ /^O$/; # Jay's & Chaz's exceptions +# $name = 'Out of Area' if $name =~ /^O$/; # Jay's & Chaz's exceptions +## $name = '' if $name =~ /^O$/; # No name announce number. $name = 'Private' if $name =~ /^P$/; # Chaz's exception $name = 'Pay' if $name =~ /^TEL PUBLIC BELL$/; # Chaz's exception ($last, $first, $middle) = split(/[\s,]+/, $name, 3); @@ -262,10 +246,9 @@ sub read_areacode_list { my %parms = @_; -# &main::print_log("Reading area code table ... "); -# print "Reading area code table ... "; + &main::print_log("Reading area code table ... "); - my ($area_code_file, %city_by_areacode, $city, $state, $areacode, $areacode_cnt); + my ($area_code_file, $city, $state, $areacode, $areacode_cnt,$timeoffset); if ($parms{area_code_file}) { open (AREACODE, $parms{area_code_file}) or print "\nError, could not find the area code file $parms{area_code_file}: $!\n"; @@ -273,19 +256,34 @@ sub read_areacode_list { while () { next if /^\#/; $areacode_cnt++; - $_ =~ s/\(.+?\)//; # Delete descriptors like (Southern) Texas ... too much to speak - #406 All parts of Montana - ($areacode, $state) = $_ =~ /(\d\d\d) All parts of (.+)/; - ($areacode, $city, $state) = $_ =~ /(\d\d\d)(.*), *(.+)/ unless $state; - next unless $city; +# $_ =~ s/\(.+?\)//; # Delete descriptors like (Southern) Texas ... too much to speak +# #406 All parts of Montana +# +# ($areacode, $state) = $_ =~ /(\d\d\d) All parts of (.+)/; +# ($areacode, $city, $state) = $_ =~ /(\d\d\d)(.*), *(.+)/ unless $state; + + $_ =~ s/\(.*\)$//; #remove end junk + # Delete descriptors like (Southern) Texas ... too much to speak + $_ =~ s/\(.+?\)//; + ($areacode,$state,$timeoffset,$city) = $_ =~ /(\S*)\s*(\S*)\s*(\S*)\s*(.*)/; + + if ($city =~ /.*:.*/) { + ($city) = $city =~ /.*:(.*)/; + } + if ($city =~ /.*,.*/) { + ($city) = $city =~ /(.*),.*/; + } + $city =~ s/^\s+|\s+$//g; #remove leading/trailing whitespace + $city =~ s/[\-\-|\;].*//; #remove stuff after --, ; + next unless $city; + $city_by_areacode{$areacode} = $city; $state_by_areacode{$areacode} = $state; # print "db code=$areacode state=$state city=$city\n"; } close AREACODE; -# &main::print_log("read in $areacode_cnt area codes from $parms{area_code_file}"); - print "Read $areacode_cnt codes from $parms{area_code_file}\n" if $caller_id_debug; + &main::print_log("Read in $areacode_cnt area codes from $parms{area_code_file}"); } # If in-state, store city name instead of state name. @@ -456,28 +454,3 @@ sub read_callerid_list { # 1; - -=back - -=head2 INI PARAMETERS - -NONE - -=head2 AUTHOR - -UNK - -=head2 SEE ALSO - -NONE - -=head2 LICENSE - -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. - -=cut - diff --git a/lib/Event_Logger.pm b/lib/Event_Logger.pm new file mode 100755 index 000000000..2a5ea017e --- /dev/null +++ b/lib/Event_Logger.pm @@ -0,0 +1,407 @@ +package Event_Logger; + +use strict; + +@Event_Logger::ISA = ('Generic_Item'); + +sub new { + my ($class,$method) = @_; + my $self={}; + bless $self,$class; + @{$$self{_active_IDs}} = (); + $$self{event} = (); + $$self{_current_ID} = 0; + $$self{icalname} = "MisterHouse autogenerated"; + $$self{enabled} = 1; +# object data +# array of active "ID's" (events in memory that haven't ended) +# ID of the most current event +# filename +# method and ical name +# hash {event}{$ID} +# => {summary} +# => {start} +# => {end} + return $self; +} + + +sub event_start() { +# create a new event and make it active + + my ($self, $summary) = @_; + + if ($$self{enabled}) { + + $$self{_current_ID}++; + my $id = $$self{_current_ID}; + my ($now_time) = $main::Time_Date; + push @{$$self{_active_IDs}}, $id; +#print "aid2=@{$$self{_active_IDs}}, id=$id\n"; + $summary = "none" if ($summary eq ""); + $$self{event}{$id}{summary} = $summary; + $$self{event}{$id}{start} = $now_time; + +# print "start, [$$self{event}{$id}{summary}][$$self{event}{$id}{start}]\n"; + return $id; + + } else { + return -1; #event_logging is disabled + } +} + +sub event_end { +# close an active event and flush it to a file + + my ($self, $id) = @_; + my $found = 0; + + if ($$self{enabled}) { + if ($id) { + my $count; + for my $item (@{$$self{_active_IDs}}) { +#print "it=$item, id=$id\n"; + $count++; + if ($item == $id) { + splice (@{$$self{_active_IDs}}, $count,1 ); + $found = 1; + } + } + } else { + $id = pop @{$$self{_active_IDs}}; + $found = 1 if $id; + } + + if ($found) { + my ($now_time) = $main::Time_Date; + $$self{event}{$id}{end} = $now_time; +# print "end, [id=$id][$$self{event}{$id}{summary}][$$self{event}{$id}{start}][$$self{event}{$id}{end}]\n"; + $self->_write_event($id); + } else { + print "cannot find id $id!\n"; + } + return $found; + } else { + return -1; #disabled + } +} + +sub active_ids { +# returns array of active events (ie events that have not ended) + my ($self) = @_; + + return \@{$$self{_active_IDs}}; #doesn't return array? +} + +sub current_id { +#returns the last used id number + my ($self) = @_; + + return $$self{_current_ID}; +} + +sub get_filename { + + my ($self) = @_; + my $filepath = $main::config_parms{data_dir}; + $filepath = $main::config_parms{ical_data_dir} if $main::config_parms{ical_data_dir}; + my $file = $$self{object_name}; + $file =~ s/^\$//; + my $fname = $filepath . "/eventlog_" . $file . ".data"; + return $fname; +} + +sub in_event { + + my ($self,$epoch_seconds) = @_; + my $fname = $self->get_filename; + + if (open ELFILE, "$fname") { + my @data = ; + close ELFILE; + foreach my $line (@data) { + + my ($start,$end,$entry) = split /\t/,$line; +#print "db in_event start= " . &main::my_str2time($start) . " sec= $epoch_seconds end= ". &main::my_str2time($end) . "\n"; + return 1 if ((&main::my_str2time($start) <= $epoch_seconds) and + (&main::my_str2time($end) >= $epoch_seconds)); + } + } else { + print "problem opening data file $fname\n"; + } + # check if in memory... + + return 0; +} + +sub first_event { + + my ($self) = @_; + my ($self) = @_; + my $fname = $self->get_filename; +print "reading firest_event $fname...\n"; + if (open ICFILE, "$fname") { + my @data = ; + close ICFILE; + my $last = shift (@data); + my ($start,$end,$entry) = split /\t/,$last; + return &main::my_str2time($start); + + } else { + + my $current_id = $self->current_id; + if (defined $$self{event}{$current_id}{start}) { + + return &main::my_str2time($$self{event}{$current_id}{start}) + } + } + return 0; +} + +sub last_event { + + my ($self) = @_; + my $fname = $self->get_filename; +print "reading last_event $fname...\n"; + if (open ICFILE, "$fname") { + my @data = ; + close ICFILE; + my $last = pop (@data); + my ($start,$end,$entry) = split /\t/,$last; + return &main::my_str2time($end); + + } else { + return 0; + } +} + +sub enable_logging { + + my ($self) = @_; + $$self{enabled} = 1; +} + +sub disable_logging { + + my ($self) = @_; + $$self{enabled} = 0; +} + +#sub log_object { +# +# ideally for any object to enable logging + +# $target = new Generic_item; +# $target_logger = new Event_Logger; +# +# $target_logger->log_object($target, start => 'on', end => 'off'); +# +# my ($self, $target, %hash) = @_; +# this would be useful a tie method that would specify start, end and summary conditions +# ie $item->tie_interface($object, "start => open, end => closed, summary => \"garage door event\"); +# } + +sub _write_event { +# writes closed events to log + + my ($self, $id) = @_; + my $fname = $self->get_filename; +print "writing to $fname...\n"; + if (open ICFILE, ">>$fname") { + #format: startendsummary + print "[id=$id][$$self{event}{$id}{start}][$$self{event}{$id}{end}][$$self{event}{$id}{summary}]\n"; + print ICFILE "$$self{event}{$id}{start}\t$$self{event}{$id}{end}\t$$self{event}{$id}{summary}\n"; + close (ICFILE); + } else { + print "error opening $fname for write!\n"; + print "abandoning cal entry $id\n"; + } + +} + +#----------------- +# Event_Logger display methods + +sub generate_ical_file { +# creates an ical formatted .ics file on the webserver for import into external clients, phpicalendar, etc... + + my ($self, $file, $start, $end) = @_; + my $filepath = $main::Pgm_Root . "/web"; + #my $filepath ="/usr/local/mh/web"; + $filepath = $main::config_parms{ical_publish_dir} if $main::config_parms{ical_publish_dir}; + $file = $$self{object_name} if (!$file); + $file =~ s/^\$//; + my $fname = $filepath . "/" . $file . ".ics"; + my $retcode; + + if (open (ICDATA, ">$fname")) { + print ICDATA $self->generate_ical_data($start,$end); + close (ICDATA); + $retcode = 1; + } else { + print "cannot open $fname for writing!"; + $retcode = 0; + } + return $retcode +} + +sub generate_ical_data { +# returnes a generated ical formatted string of events between start and end +# if start & end ommitted, then entire range is returned (TODO) + +use Date::ICal; #included in MH +use Data::ICal; #included in MH +use Data::ICal::Entry::Event; #new Dep + + my ($self, $start, $end) = @_; + my $filepath = $main::config_parms{data_dir}; + $filepath = $main::config_parms{ical_data_dir} if $main::config_parms{ical_data_dir}; + my $file = $$self{object_name}; + $file =~ s/^\$//; + my $fname = $filepath . "/eventlog_" . $file . ".data"; + my $calendar = new Data::ICal; + $calendar->add_properties( + method => $$self{method}, + # name => $$self{name}, + ); + + my $event = Data::ICal::Entry::Event->new(); + + if ( -e $fname ) { + if (open ICFILE, "$fname") { + foreach my $line () { + my ($start,$end,$summary) = split /\t/,$line; + #start,end not implemented yet + my ($start1,$end1); + $start1 = &main::my_str2time($start); + $end1 = &main::my_str2time($end); + $event->add_properties( + summary => $summary, + #description => "FreeFormText.\\nMore FreeFormText.\\n\\n", + #dtstart => Date::ICal->new( epoch => time )->ical, + dtstart => Date::ICal->new( epoch => $start1 )->ical, + dtend => Date::ICal->new( epoch => $end1 )->ical, + dtstamp => Date::ICal->new( epoch => time )->ical, + #class => "PUBLIC", + #organizer => "MAILTO:foo\@bar", + #location => "Phone call", + #priority => 5, + #transp => "OPAQUE", + #sequence => 0, + #uid => "123", + ); + $calendar->add_entry($event); + } + close (ICFILE); + } else { + print "error opening $fname for read\n"; + } + } + + + foreach my $id (@{$$self{_active_IDs}}) { + my ($start1,$end1); + my $start = $$self{event}{$id}{start}; + $start1 = &main::my_str2time($start); + $event->add_properties( + summary => $$self{event}{$id}{summary}, + #description => "FreeFormText.\\nMore FreeFormText.\\n\\n", + #dtstart => Date::ICal->new( epoch => time )->ical, + dtstart => Date::ICal->new( epoch => $start1 )->ical, + dtend => Date::ICal->new( epoch => time )->ical, + dtstamp => Date::ICal->new( epoch => time )->ical, + #class => "PUBLIC", + #organizer => "MAILTO:foo\@bar", + #location => "Phone call", + #priority => 5, + #transp => "OPAQUE", + #sequence => 0, + #uid => "123", + ); + $calendar->add_entry($event); + } + + return $calendar->as_string; + +} + +sub generate_graph_file { +# creates an ical formatted .ics file on the webserver for import into external clients, phpicalendar, etc... + + my ($self, $file, $start, $end, $step) = @_; + my $filepath = $main::Pgm_Root . "/web"; + #my $filepath ="/usr/local/mh/web"; + $filepath = $main::config_parms{ical_publish_dir} if $main::config_parms{ical_publish_dir}; + $file = $$self{object_name} if (!$file); + $file =~ s/^\$//; + my $fname = $filepath . "/" . $file . ".gif"; + my $retcode; + + ##if (open (ELIMG, ">$fname")) { + ## binmode ELIMG; + ## print ELIMG $self->generate_graph_data($start,$end,$step); + ## close ELIMG; + ## $retcode = 1; + ##} else { + ## print "cannot open $fname for writing!"; + ## $retcode = 0; + ##} +$self->generate_graph_data($start,$end,$step); + return $retcode +} + + +sub generate_graph_data { +# returnes a generated ical formatted string of events between start and end +# if start & end ommitted, then entire range is returned (TODO) + + use GD::Graph; #new dep + use GD::Graph::lines; #new dep + my ($self, $start, $end, $step) = @_; + + my $height = 400; + my $width = 300; + + $start = $self->first_event if !$start; + $end = $self->last_event if !$end; + $step = 300 if !$step; #default to 5 minutes + my @graph_data_time; + my @graph_data_status; + my @graph_data; + +print "db:start=$start,end=$end,step=$step\n"; + + my $count = 0; + for (my $index = $start; $index <= $end; $index=$index+$step) { +# $data{$index} = $object->in_event($index); +# @graph_data[0]->[$count] = &main::time_date_stamp(14,$index); +# @graph_data[1]->[$count] = $self->in_event($index); + @graph_data_time[$count] = &main::time_date_stamp(14,$index); +# my $temp = $self->in_event($index); #get graphing working first... + my $temp_rnd = rand() * 100; + my $temp = 0; + $temp = 1 if ($temp_rnd > 80); + @graph_data_status[$count] = $temp; +print "db:count=$count, index=$index end=$end" . &main::time_date_stamp(14,$index) . " $temp\n"; + $count++; + } + + @graph_data = (\@graph_data_time,\@graph_data_status); + print @graph_data_time,@graph_data_status; + + my $graph = GD::Graph::lines->new($height,$width); + $graph->set ( + x_label => 'Time', + y_label => 'State', + title => 'Graph of self->get_name'); + + my $fname ="/usr/local/mh/web/test.gif"; + + if (open (ELIMG, ">$fname")) { + binmode ELIMG; + print ELIMG $graph->plot(\@graph_data); + close ELIMG; + } + +} + diff --git a/lib/Telephony_Interface.pm b/lib/Telephony_Interface.pm index 2e56fab25..395c84fe9 100644 --- a/lib/Telephony_Interface.pm +++ b/lib/Telephony_Interface.pm @@ -94,7 +94,9 @@ sub check_for_data { $main::Serial_Ports{$port}{data_record} = undef; # Ignore garbage data (ascii is between ! thru ~) $data = '' if $data !~ /^[\n\r\t !-~]+$/; +print "Caller_id_data{$port} raw data: $caller_id_data{$port}" if $main::Debug{phone}; $caller_id_data{$port} .= ' ' . $data; +print "Phone raw data: $data.\n" if $main::Debug{phone}; print "Phone data: $data.\n" if $main::Debug{phone}; if (($caller_id_data{$port} =~ /NAME.+NU?MBE?R/s) or ($caller_id_data{$port} =~ /NU?MBE?R.+NAME/s) or @@ -172,7 +174,8 @@ sub process_cid_data { else { ($date) = $data =~ /DATE *= *(\S+)/s; ($time) = $data =~ /TIME *= *(\S+)/s; - ($name) = $data =~ /NAME *= *(.{1,15})/s; + ($name) = $data =~ /NAME *= *(.+)\sNMBR/s if ($type eq 'rockwell'); + ($name) = $data =~ /NAME *= *(.{1,15})/s unless $name; ($name) = $data =~ /MESG *= *([^\n]+)/s unless $name; $name = 'private' if $name eq '080150'; $name = 'unavailable' if $name eq '08014F'; @@ -182,6 +185,7 @@ sub process_cid_data { $name = '' unless $name; $number = '' unless $number; + $number = '' if $number eq 'O'; unless ($name or $number) { print "\nCallerid data not parsed: p=$port t=$type d=$data date=$date time=$time number=$number name=$name\n"; @@ -200,6 +204,8 @@ sub process_cid_data { $name=''; } $cid_type = 'U' if uc $name eq 'O' or $number eq 'O'; + $name = 'unknown' if uc $name eq 'O'; + $name = 'unknown' if $name eq $number; #if name is same as number than ignore the name $cid_type = 'N' if $number =~ /^[\d\- ]+$/; # Override the type if the number is known diff --git a/lib/handy_net_utilities.pl b/lib/handy_net_utilities.pl index 78cf3efd7..be9e0e199 100644 --- a/lib/handy_net_utilities.pl +++ b/lib/handy_net_utilities.pl @@ -29,7 +29,7 @@ package handy_net_utilities; use HTML::FormatText; use HTML::Parse; use LWP::Simple; -use Encode qw(encode decode); +use Encode qw(encode decode find_encoding); #require "$main::Pgm_Root/lib/site/HTML/Formatter.pm"; @@ -1488,11 +1488,61 @@ sub main::net_mail_summary { # Parse any unicode from headers... $from =~ s/\"//g; - if ($from =~ m/=\?/) { - print "Unicode detected. Decoding MIME-Header from $from to " if $parms{debug} or $main::Debug{net}; - $from = decode("MIME-Header", $from); - print "$from.\n" if $parms{debug} or $main::Debug{net}; + if ($from =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'from' from $from to " if $parms{debug} or $main::Debug{net}; + $from = decode("MIME-Header", $from); + print "$from.\n" if $parms{debug} or $main::Debug{net}; + } + else { + print "WARNING: Unknown unicode detected $1 for 'from' $from\n"; + } + } + if ($sender =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'sender' from $sender to " if $parms{debug} or $main::Debug{net}; + $sender = decode("MIME-Header", $sender); + print "$sender.\n" if $parms{debug} or $main::Debug{net}; + } + else { + print "WARNING: Unknown unicode detected $1 for 'sender' $sender\n"; + } + } + if ($to =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'to' from $to to " if $parms{debug} or $main::Debug{net}; + $to = decode("MIME-Header", $to); + print "$to.\n" if $parms{debug} or $main::Debug{net}; + } + else { + print "WARNING: Unknown unicode detected $1 for 'to' $to\n"; + } + } + if ($cc =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'cc' from $cc to " if $parms{debug} or $main::Debug{net}; + $cc = decode("MIME-Header", $cc); + print "$cc.\n" if $parms{debug} or $main::Debug{net}; + } + else { + print "WARNING: Unknown unicode detected $1 for 'cc' $cc\n"; + } } + if ($subject =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'subject' from $subject to " if $parms{debug} or $main::Debug{net}; + $subject = decode("MIME-Header", $subject); + print "$subject.\n" if $parms{debug} or $main::Debug{net}; + } + else { + print "WARNING: Unknown unicode detected $1 for 'subject' $subject\n"; + } + } # Process 'from' into speakable name ($from_name) = $from =~ /\((.+)\)/; ($from_name) = $from =~ / *(.+?) * - -=head2 SYNOPSIS - -NONE - -=head2 DESCRIPTION - -Adds IMAP message scan and gmail sending ability. - -Requires the following perl modules: - - Mail::IMAPClient - IO::Socket::SSL - IO::Socket::INET - Time::Zone - -if the IMAP scan hangs before authenticating against the gmail account, reinstall the -IO::Socket::SSL - -Todo: parse unread messages - -=head2 INHERITS - -B - -=head2 METHODS - -=over - -=item B - -=cut - #!/usr/bin/perl # v 0.1 - initial test concept, inspired by Pete's script - H. Plato - 2 June 2008 @@ -39,6 +5,19 @@ =head2 METHODS # v 0.3 - removed gmail send to it's own library, added ssl as an option. +# Adds IMAP message scan and gmail sending ability. +# Requires the following perl modules +# Mail::IMAPClient +# IO::Socket::SSL +# IO::Socket::INET +# Time::Zone +# +# if the IMAP scan hangs before authenticating against the gmail account, reinstall the +# IO::Socket::SSL +# +# Todo: +# - parse unread messages + package imap_utils; use strict; @@ -48,7 +27,7 @@ package imap_utils; use IO::Socket::INET; use POSIX; use Time::Zone; -use Encode qw(encode decode); +use Encode qw(encode decode find_encoding); sub main::get_imap { @@ -206,14 +185,60 @@ sub main::get_imap { $processed_count++; my $from = $client->get_header($msgid, "From"); my $to = $client->get_header($msgid, "To"); - my $cc = $client->get_header($msgid, "CC"); + my $cc = ""; + $cc = $client->get_header($msgid, "CC"); + my $subject = ""; + $subject = $client->get_header($msgid, "Subject"); my $msgdate = $client->get_header($msgid, "Date"); $from =~ s/\"//g; - if ($from =~ m/=\?/) { - print "Unicode detected. Decoding MIME-Header from $from to " if $debug; - $from = decode("MIME-Header", $from); - print "$from.\n" if $debug; - } + if ($from =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'from' from $from to " if $debug; + $from = decode("MIME-Header", $from); + print "$from.\n" if $debug; + } + else { + print "WARNING: Unknown unicode detected $1 for 'from' $from\n"; + } + } + + if ($to =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'to' from $to to " if $debug; + $to = decode("MIME-Header", $to); + print "$to.\n" if $debug; + } + else { + print "WARNING: Unknown unicode detected $1 for 'to' $to\n"; + } + } + + if ($cc =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'cc' from $cc to " if $debug; + $cc = decode("MIME-Header", $cc); + print "$cc.\n" if $debug; + } + else { + print "WARNING: Unknown unicode detected $1 for 'cc' $cc\n"; + } + } + + if ($subject =~ m/\=\?([0-9A-Za-z\-_]+)\?.\?.*\?\=/) { + my $enc_check = find_encoding($1); + if ($enc_check) { + print "Unicode $1 detected. Decoding MIME-Header 'subject' from $subject to " if $debug; + $subject = decode("MIME-Header", $subject); + print "$subject.\n" if $debug; + } + else { + print "WARNING: Unknown unicode detected $1 for 'subject' $subject\n"; + } + } + # decode("MIME-Header", $from) if ($from =~ m/=\?/); $email_addresses{$from}++; my $name = $from; @@ -223,7 +248,6 @@ sub main::get_imap { $name =~ s/\s$//g; $email_names{$name}++; - my $subject= $client->get_header($msgid, "Subject"); my $body; if ($size) { $body = $client->bodypart_string($msgid,1,$size); @@ -379,29 +403,3 @@ sub _check_age { } 1; - - -=back - -=head2 INI PARAMETERS - -NONE - -=head2 AUTHOR - -UNK - -=head2 SEE ALSO - -NONE - -=head2 LICENSE - -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. - -=cut - diff --git a/lib/site/DateTime/TimeZone.pm b/lib/site/DateTime/TimeZone.pm index f26a9b459..39b5dca73 100644 --- a/lib/site/DateTime/TimeZone.pm +++ b/lib/site/DateTime/TimeZone.pm @@ -1,13 +1,11 @@ package DateTime::TimeZone; -use 5.006; - use strict; -use warnings; -our $VERSION = '0.84'; +use vars qw( $VERSION ); +$VERSION = '0.6603'; -use DateTime::TimeZone::Catalog; +use DateTime::TimeZoneCatalog; use DateTime::TimeZone::Floating; use DateTime::TimeZone::Local; use DateTime::TimeZone::OffsetOnly; @@ -26,7 +24,7 @@ use constant OFFSET => 4; use constant IS_DST => 5; use constant SHORT_NAME => 6; -my %SpecialName = map { $_ => 1 } qw( EST MST HST CET EET MET WET EST5EDT CST6CDT MST7MDT PST8PDT ); +my %SpecialName = map { $_ => 1 } qw( EST MST HST EST5EDT CST6CDT MST7MDT PST8PDT ); sub new { @@ -35,13 +33,13 @@ sub new { name => { type => SCALAR } }, ); - if ( exists $DateTime::TimeZone::Catalog::LINKS{ $p{name} } ) + if ( exists $DateTime::TimeZone::LINKS{ $p{name} } ) { - $p{name} = $DateTime::TimeZone::Catalog::LINKS{ $p{name} }; + $p{name} = $DateTime::TimeZone::LINKS{ $p{name} }; } - elsif ( exists $DateTime::TimeZone::Catalog::LINKS{ uc $p{name} } ) + elsif ( exists $DateTime::TimeZone::LINKS{ uc $p{name} } ) { - $p{name} = $DateTime::TimeZone::Catalog::LINKS{ uc $p{name} }; + $p{name} = $DateTime::TimeZone::LINKS{ uc $p{name} }; } unless ( $p{name} =~ m,/, @@ -102,7 +100,7 @@ sub new $zone->can('olson_version') ? $zone->olson_version() : 'unknown'; - my $catalog_version = DateTime::TimeZone::Catalog->OlsonVersion(); + my $catalog_version = __PACKAGE__->catalog_olson_version(); if ( $object_version ne $catalog_version ) { @@ -406,13 +404,9 @@ sub category { (split /\//, $_[0]->{name}, 2)[0] } sub is_valid_name { - my $tz; - { - local $@; - $tz = eval { $_[0]->new( name => $_[1] ) }; - } + my $tz = eval { $_[0]->new( name => $_[1] ) }; - return $tz && $tz->isa('DateTime::TimeZone') ? 1 : 0 + return $tz && UNIVERSAL::isa( $tz, 'DateTime::TimeZone') ? 1 : 0 } sub STORABLE_freeze @@ -450,11 +444,7 @@ sub STORABLE_thaw # sub offset_as_seconds { - { - local $@; - shift if eval { $_[0]->isa('DateTime::TimeZone') }; - } - + shift if eval { $_[0]->isa('DateTime::TimeZone') }; my $offset = shift; return undef unless defined $offset; @@ -489,11 +479,7 @@ sub offset_as_seconds sub offset_as_string { - { - local $@; - shift if eval { $_[0]->isa('DateTime::TimeZone') }; - } - + shift if eval { $_[0]->isa('DateTime::TimeZone') }; my $offset = shift; return undef unless defined $offset; @@ -515,54 +501,54 @@ sub offset_as_string ); } -# These methods all operate on data contained in the DateTime/TimeZone/Catalog.pm file. +# These methods all operate on data contained in the DateTime/TimeZoneCatalog.pm file. sub all_names { - return wantarray ? @DateTime::TimeZone::Catalog::ALL : [@DateTime::TimeZone::Catalog::ALL]; + return wantarray ? @DateTime::TimeZone::ALL : [@DateTime::TimeZone::ALL]; } sub categories { return wantarray - ? @DateTime::TimeZone::Catalog::CATEGORY_NAMES - : [@DateTime::TimeZone::Catalog::CATEGORY_NAMES]; + ? @DateTime::TimeZone::CATEGORY_NAMES + : [@DateTime::TimeZone::CATEGORY_NAMES]; } sub links { return - wantarray ? %DateTime::TimeZone::Catalog::LINKS : {%DateTime::TimeZone::Catalog::LINKS}; + wantarray ? %DateTime::TimeZone::LINKS : {%DateTime::TimeZone::LINKS}; } sub names_in_category { shift if $_[0]->isa('DateTime::TimeZone'); - return unless exists $DateTime::TimeZone::Catalog::CATEGORIES{ $_[0] }; + return unless exists $DateTime::TimeZone::CATEGORIES{ $_[0] }; return wantarray - ? @{ $DateTime::TimeZone::Catalog::CATEGORIES{ $_[0] } } - : [ $DateTime::TimeZone::Catalog::CATEGORIES{ $_[0] } ]; + ? @{ $DateTime::TimeZone::CATEGORIES{ $_[0] } } + : [ $DateTime::TimeZone::CATEGORIES{ $_[0] } ]; } sub countries { wantarray - ? ( sort keys %DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY ) - : [ sort keys %DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY ]; + ? ( sort keys %DateTime::TimeZone::ZONES_BY_COUNTRY ) + : [ sort keys %DateTime::TimeZone::ZONES_BY_COUNTRY ]; } sub names_in_country { shift if $_[0]->isa('DateTime::TimeZone'); - return unless exists $DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] }; + return unless exists $DateTime::TimeZone::ZONES_BY_COUNTRY{ lc $_[0] }; return wantarray - ? @{ $DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] } } - : $DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] }; + ? @{ $DateTime::TimeZone::ZONES_BY_COUNTRY{ lc $_[0] } } + : $DateTime::TimeZone::ZONES_BY_COUNTRY{ lc $_[0] }; } @@ -637,21 +623,34 @@ C object is returned. If the "name" parameter is "local", then the module attempts to determine the local time zone for the system. -The method for finding the local zone varies by operating system. See -the appropriate module for details of how we check for the local time -zone. - -=over 4 +First it checks C<$ENV> for keys named "TZ", "SYS$TIMEZONE_RULE", +"SYS$TIMEZONE_NAME", "UCX$TZ", or "TCPIP$TZC" (the last 4 are for +VMS). If this is defined, and it is not the string "local", then it +is treated as any other valid name (including "floating"), and the +constructor tries to create a time zone based on that name. -=item * L +Next, it checks for the existence of a symlink at F. +It follows this link to the real file and figures out what the file's +name is. It then tries to turn this name into a valid time zone. For +example, if this file is linked to F, +it will end up trying "US/Central", which will then be converted to +"America/Chicago" internally. -=item * L +Some systems just copy the relevant file to F instead +of making a symlink. In this case, we look in F +for a file that has the same size and content as F to +determine the local time zone. -=item * L +Then it checks for a file called F or F. +If one of these exists, it is read and it tries to create a time zone +with the name contained in the file. -=back +Finally, it checks for a file called F. If this +file exists, it looks for a line inside the file matching +C. If this line exists, it tries the +value as a time zone name. -If a local time zone is not found, then an exception will be thrown. +If none of these methods work, it gives up and dies. =head2 $tz->offset_for_datetime( $dt ) @@ -757,7 +756,7 @@ use C to do so. =head2 DateTime::TimeZone->names_in_country( $country_code ) -Given a two-letter ISO3166 country code, this method returns a list of +Given a two-letter ISO3066 country code, this method returns a list of time zones used in that country. The country code may be of any case. In scalar context, it returns an array reference, while in list context it returns an array. @@ -794,33 +793,12 @@ your module with Storable. =head1 SUPPORT Support for this module is provided via the datetime@perl.org email -list. See http://datetime.perl.org/?MailingList for details. +list. See http://lists.perl.org/ for more details. Please submit bugs to the CPAN RT system at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=datetime%3A%3Atimezone or via email at bug-datetime-timezone@rt.cpan.org. -=head1 DONATIONS - -If you'd like to thank me for the work I've done on this module, -please consider making a "donation" to me via PayPal. I spend a lot of -free time creating free software, and would appreciate any support -you'd care to offer. - -Please note that B in order -for me to continue working on this particular software. I will -continue to do so, inasmuch as I have in the past, for as long as it -interests me. - -Similarly, a donation made in this way will probably not make me work -on this software much more, unless I get so many donations that I can -consider working on free software full time, which seems unlikely at -best. - -To donate, log into PayPal and send money to autarch@urth.org or use -the button on this page: -L - =head1 AUTHOR Dave Rolsky @@ -833,7 +811,7 @@ datetime@perl.org list. =head1 COPYRIGHT -Copyright (c) 2003-2008 David Rolsky. All rights reserved. This +Copyright (c) 2003-2007 David Rolsky. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/vsDB.pm b/lib/vsDB.pm index 5c8ce0c49..2f648594f 100644 --- a/lib/vsDB.pm +++ b/lib/vsDB.pm @@ -1,1067 +1,1161 @@ -# ---------------------------------------------------------------------------- -# vsDB (verysimple database) Module -# Copyright (c) 2001 Jason M. Hinkle. All rights reserved. This module is -# free software; you may redistribute it and/or modify it under the same -# terms as Perl itself. -# For more information see: http://www.verysimple.com/scripts/ -# -# LEGAL DISCLAIMER: -# This software is provided as-is. Use it at your own risk. The -# author takes no responsibility for any damages or losses directly -# or indirectly caused by this software. -# ---------------------------------------------------------------------------- -package vsDB; -require 5.000; -$VERSION = "1.3.9"; -$ID = "vsDB.pm"; - - -#_____________________________________________________________________________ -sub new { - my $class = shift; - my %keyValues = @_; - my (%fieldNames,@fileArray,@row,@filterArray); - - # if no delimiter is specified, then make it a tab char. - $keyValues{'delimiter'} = "\t" unless defined($keyValues{'delimiter'}); - - my $this = { - fileName => $keyValues{'file'}, - delimiter => $keyValues{'delimiter'}, - fieldNames => \%fieldNames, - fileArray => \@fileArray, - filterArray => \@filterArray, - row => \@row, - recordCount => 0, - filterRecordCount => 0, - absolutePosition => 0, - pageSize => 10, - EOF => 1, - isOpen => 0, - lastError => '', - appendOnly => 1, - isDirty => 0, - originalCount => 0, - CR => '', - LF => '', - }; - bless $this; - return $this; -} - - -# ########################################################################### -# PUBLIC PROPERTIES - - -#_____________________________________________________________________________ -sub Version { - return $VERSION; -} - -#_____________________________________________________________________________ -sub ID { - return $ID; -} - -#_____________________________________________________________________________ -sub LastError { - my ($this) = shift; - return $this->{'lastError'}; -} - -#_____________________________________________________________________________ -sub AbsolutePosition { - my ($this) = shift; - my ($newValue) = shift; - if (defined($newValue)) { - $this->{'absolutePosition'} = $newValue; - $this->_RefreshRow; - } else { - return $this->{'absolutePosition'}; - } -} - -#_____________________________________________________________________________ -sub ActivePage { - my ($this) = shift; - my ($newValue) = shift; - if (defined($newValue)) { - $newValue = $this->PageCount if ($newValue > $this->PageCount); - $this->{'absolutePosition'} = ($this->{'pageSize'} * ($newValue-1)) + 1; - # make sure we are on the right page if filtered - while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { - $this->{'absolutePosition'}++; - } - $this->_RefreshRow; - return 1; - } else { - # BUG - when filtering, this returns the wrong value - return 1 if ($this->{'absolutePosition'} == 1); - my ($count) = (($this->{'absolutePosition'} - 1) / $this->{'pageSize'}); #/ - my ($activePage) = int($count) + 1; - $activePage = $this->PageCount if ($activePage > $this->PageCount); - return $activePage; - } - -} - -#_____________________________________________________________________________ -sub PageSize { - my ($this) = shift; - my ($newValue) = shift; - if (defined($newValue)) { - $this->{'pageSize'} = int($newValue) if (int($newValue) > 0); - return 1; - } else { - return $this->{'pageSize'}; - } -} - -#_____________________________________________________________________________ -sub PageCount { - my ($this) = shift; - my ($count) = ($this->{'filterRecordCount'} / $this->{'pageSize'}); #/ - if (int($count) < $count) {$count = int($count)+1} - return $count; -} - -#_____________________________________________________________________________ -sub File { - my ($this) = shift; - my ($newVal) = shift; - # if the file has changed, then we can't just append... - if ($newVal) {$this->{'appendOnly'} = 0} - return $this->_GetSetProperty("fileName",$newVal); -} - -#_____________________________________________________________________________ -sub CR { - return shift->_GetSetProperty("CR",shift); -} - -#_____________________________________________________________________________ -sub LF { - return shift->_GetSetProperty("LF",shift); -} - -#_____________________________________________________________________________ -sub Delimiter { - return shift->_GetSetProperty("delimiter",shift); -} - -#_____________________________________________________________________________ -sub RecordCount { - return shift->{'filterRecordCount'}; -} - -#_____________________________________________________________________________ -sub EOF { - my ($this) = shift; - return 1 if ($this->RecordCount < 1); - return $this->{'EOF'}; -} - -#_____________________________________________________________________________ -sub FieldValue { - my ($this) = shift; - return "EOF" if ($this->{'EOF'}); - my ($fieldName) = shift || return "ERROR: FieldValue(): Field Name Required"; - my ($newValue) = shift; - my ($fieldNumber) = $this->{'fieldNames'}{$fieldName}; - my ($lineFeed) = chr(10); - my ($carriageReturn) = chr(13); - my ($crReplacement) = $this->{'CR'}; - my ($lfReplacement) = $this->{'LF'}; - return "ERROR: FieldValue('" . $fieldName . "') Field Not Found." if (!defined($fieldNumber)); - - # if a new value is defined, update, otherwise return current value - if (defined($newValue)) { - $this->{'isDirty'} = 1; - if ($this->{'absolutePosition'} <= $this->{'originalCount'}) { - $this->{'appendOnly'} = 0 - }; - # make sure we don't corrupt the file with a delimiter or - # line break in the data. - $newValue =~ s/$this->{'delimiter'}//g; - $newValue =~ s/$carriageReturn/$crReplacement/g; - $newValue =~ s/$lineFeed/$lfReplacement/g; - # $newValue =~ s/\n//g; # (should already be dealt with) - $this->{'row'}[$fieldNumber] = $newValue; - - # update the fileArray to match the current row. originally used - # a join on the row array, but that caused unititialize var errors - # when there are blank fields. this could probably be improved by - # only updating when the cursor is moved or commit is called - my ($newRow,$newField); - my ($colNum) = 0; - foreach ($this->FieldNames) { - $newField = $this->{'row'}[$colNum]; - if (!defined($newField)) {$newField = ""} - $newRow .= $newField; - $newRow .= $this->{'delimiter'}; - $colNum++; - } - # get rid of the last delimiter - for (my $x = 1;$x <= length($this->{'delimiter'}); $x++) { - chop($newRow); - } - - $this->{'fileArray'}[$this->{'absolutePosition'}] = $newRow . "\n"; - return 1; - } else { - # make sure we return a defined value || won't work because it doesn;t - # differentiate between 0 and null - if (defined($this->{'row'}[$fieldNumber])) { - my $returnVal = $this->{'row'}[$fieldNumber]; - $returnVal =~ s/$crReplacement/$carriageReturn/g; - $returnVal =~ s/$lfReplacement/$lineFeed/g; - return $returnVal; - - } else { - return ""; - } - } -} - -#_____________________________________________________________________________ -sub FieldNames { - # returns all fieldnames as an array - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - my ($fieldRow) = $this->{'fileArray'}[0]; - chop ($fieldRow); - my (@tempfieldNames) = split($this->{'delimiter'},$fieldRow); - return @tempfieldNames; -} - -#_____________________________________________________________________________ -sub Row { - # returns current row values as an array - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - my ($tempRow) = $this->{'row'}; - return @$tempRow; -} - -#_____________________________________________________________________________ -sub xml { - # returns current recordset as xml - my ($this) = shift; - my ($strRootName) = shift || "vsDB"; - my ($strElementName) = shift || "Record"; - my ($strXml); - - $strXml = "\n"; - $strXml .= "\n"; - $strXml .= "<$strRootName>\n"; - $this->MoveFirst; - my (@fields) = $this->FieldNames; - my ($field, $fieldValue); - until ($this->EOF) { - $strXml .= "<$strElementName>\n"; - foreach $field (@fields) { - $fieldValue = $this->FieldValue($field); - $strXml .= "<$field>$fieldValue\n"; - } - $strXml .= "\n"; - $this->MoveNext; - } - $strXml .= "\n"; - - return $strXml; -} - -#_____________________________________________________________________________ -sub MoveNext { - # moves the curser to the next row in the data file - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - return 0 if ($this->{'EOF'}); - $this->{'absolutePosition'}++; - while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { - $this->{'absolutePosition'}++; - } - $this->_RefreshRow; - return 1; -} - -#_____________________________________________________________________________ -sub MovePrevious { - # moves the curser to the previous row in the data file - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - return 0 if ($this->{'absolutePosition'} < 2); - $this->{'absolutePosition'}--; - while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { - $this->{'absolutePosition'}--; - } - $this->_RefreshRow; - return 1; -} - -#_____________________________________________________________________________ -sub MoveFirst { - # moves the curser to the first row in the data file - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - $this->{'absolutePosition'} = 1; - while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { - $this->{'absolutePosition'}++; - } - $this->_RefreshRow; - return 1; -} - -#_____________________________________________________________________________ -sub MoveLast { - # moves the curser to the last row in the data file - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - $this->{'absolutePosition'} = $this->{'recordCount'}; - while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { - $this->{'absolutePosition'}--; - } - $this->_RefreshRow; - return 1; -} - -#_____________________________________________________________________________ -sub Delete { - # delete the current row - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - return 0 if $this->{'recordCount'} < 1; - return 0 if ($this->{'EOF'}); - $this->{'isDirty'} = 1; - if ($this->{'absolutePosition'} <= $this->{'originalCount'}) { - $this->{'appendOnly'} = 0 - }; - my ($tempArray) = $this->{'fileArray'}; - splice(@$tempArray,$this->{'absolutePosition'},1); - $this->{'recordCount'}--; - $this->{'filterRecordCount'}--; - $this->_RefreshRow; - return 1; -} - -#_____________________________________________________________________________ -sub AddNew { - # add a new row to the end of the recordset - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - $this->{'isDirty'} = 1; - $this->{'recordCount'}++; - $this->{'absolutePosition'} = $this->{'recordCount'}; - $this->{'filterRecordCount'}++; - # the number of delimiter chars is fieldCount - 1 - my ($delimiterCount) = 0; - foreach ($this->FieldNames) { - $delimiterCount++; - } - $delimiterCount--; - - # add the correct number of colums using the delimiterCount - $this->{'fileArray'}[$this->{'absolutePosition'}] = ($this->{'delimiter'} x $delimiterCount) . "\n"; - - $this->MoveLast; - - return 1; -} - -#_____________________________________________________________________________ -sub AddNewField { - # add a new row to the end of the recordset - my ($this) = shift; - my ($newFieldName) = shift || return 0; - my ($defaultValue) = shift; - $defaultValue = '' unless (defined($defaultValue)); - - $this->{'isOpen'} = 1; - $this->{'appendOnly'} = 0; - $this->{'isDirty'} = 1; - - # update the fieldnames array - my ($nextField) = 0; - foreach ($this->FieldNames) { - $nextField++; - } - $this->{'fieldNames'}{$newFieldName} = $nextField; - - chop($this->{'fileArray'}[0]); - - # update the first row in the file array - if ($nextField > 0) { - $this->{'fileArray'}[0] .= $this->{'delimiter'}; - } - $this->{'fileArray'}[0] .= $newFieldName . "\n"; - - return 1; -} - -#_____________________________________________________________________________ -sub Max { - # returns the maximum value for the specified column - my ($this) = shift; - my ($fieldName) = shift || return 0; - my ($alpha) = shift || 0; - my ($curVal); - my ($curPos) = $this->{'absolutePosition'}; - $this->MoveFirst; - my ($maxVal) = $this->FieldValue($fieldName); - while (!$this->EOF) { - $curVal = $this->FieldValue($fieldName); - if (!$alpha) { - if ($curVal ne "") { - if (int($curVal) > int($maxVal)) {$maxVal = $curVal}; - } - } else { - if ((lc($curVal) cmp lc($maxVal)) > 0) {$maxVal = $curVal}; - } - $this->MoveNext; - } - $this->AbsolutePosition($curPos); - return $maxVal; -} - -#_____________________________________________________________________________ -sub Min { - # returns the maximum value for the specified column - my ($this) = shift; - my ($fieldName) = shift || return 0; - my ($alpha) = shift || 0; - my ($curVal); - my ($curPos) = $this->{'absolutePosition'}; - $this->MoveFirst; - my ($minVal) = $this->FieldValue($fieldName); - while (!$this->EOF) { - $curVal = $this->FieldValue($fieldName); - if ($alpha) { - if ($curVal ne "") { - if (int($curVal) < int($minVal)) {$minVal = $curVal}; - } - } else { - if ((lc($curVal) cmp lc($minVal)) < 0) {$minVal = $curVal}; - } - $this->MoveNext; - } - $this->AbsolutePosition($curPos); - return $minVal; -} - -#_____________________________________________________________________________ -sub Filter { - # $obj->Filter($fieldName,$operator,$criteria); - - # TODO: > and < are not working properly... maybe text comparison problem?? - my ($this) = shift; - my ($fieldName) = shift || return 0; - my ($operator) = shift || "eq"; - my ($criteria) = shift; - $criteria = "" unless defined($criteria); - - my ($filterSetting); - - $this->{'filterArray'}[0] = 0; - $this->MoveFirst; - - while (!$this->EOF) { - $filterSetting = 0; - if ($operator eq "eq" && $this->FieldValue($fieldName) ne $criteria) { - $filterSetting = 1; - } elsif ($operator eq "ne" && !($this->FieldValue($fieldName) ne $criteria)) { - $filterSetting = 1; - } elsif ($operator eq "like" && !(index(lc($this->FieldValue($fieldName)),lc($criteria),0) + 1)) { - $filterSetting = 1; - } elsif ($operator eq ">" && !($this->FieldValue($fieldName) > $criteria) ) { - $filterSetting = 1; - } elsif ($operator eq "<" && !($this->FieldValue($fieldName) < $criteria) ) { - $filterSetting = 1; - } - - # print "

    " . $this->{'absolutePosition'} . ": " . $filterSetting . "

    "; - $this->{'filterArray'}[$this->{'absolutePosition'}] = $filterSetting; - - $this->{'filterRecordCount'} -= 1 if ($filterSetting); - $this->MoveNext; - } - $this->MoveFirst; - return 1; -} - -#_____________________________________________________________________________ -sub RemoveFilter { - my ($this) = shift; - my (@newArray); - $this->{'filterArray'} = \@newArray; - $this->{'filterRecordCount'} = $this->{'recordCount'}; - return 1; -} - -#_____________________________________________________________________________ -sub Commit { - # update the file, saving all changes made - my ($this) = shift; - my ($useFlock) = shift || 0; - my ($fileName) = $this->{'fileName'}; - - # if no changes were made, don't bother writing to the file - if (!$this->{'isDirty'}) {return 1}; - - if ($this->{'appendOnly'}) { - # if only new records were added, just append to the file - my ($nCount); - if (!open (OUTPUTFILE, ">>$fileName")) { - $this->{'lastError'} = "Commit: Couldn't Open DataFile '$fileName' For Appending"; - return 0 - }; - flock(OUTPUTFILE,2) if ($useFlock); - my ($tempArray) = $this->{'fileArray'}; - for ($nCount = $this->{'originalCount'} + 1; $nCount <= $this->{'recordCount'}; $nCount++) { - print OUTPUTFILE @$tempArray[$nCount]; - } - } else { - # if records were changed or deleted, we have to replace them all - if (!open (OUTPUTFILE, ">$fileName")) { - $this->{'lastError'} = "Commit: Couldn't Open DataFile '$fileName' For Writing"; - return 0 - }; - flock(OUTPUTFILE,2) if ($useFlock); - my ($tempArray) = $this->{'fileArray'}; - print OUTPUTFILE join('',@$tempArray); - } - close (OUTPUTFILE); - flock(OUTPUTFILE,8) if ($useFlock); - return 1; -} - -#_____________________________________________________________________________ -sub Sort { - # sorts the datafile on the given column - # obj->Sort($field); - # if $field is ommited, or an invalid fieldname is used, defaults to - # the left-most column. - - my ($this) = shift; - my ($fieldName) = shift || '0'; - my ($desc) = shift || '0'; - my ($delimiter) = $this->{'delimiter'}; - - # can't append once we've changed the sort order - $this->{'appendOnly'} = 0; - - # sorting will mess up filter, so lets remove it - $this->RemoveFilter; - - # get the fieldnumber (or default to leftmost column) - my ($fieldNumber) = $this->{'fieldNames'}{$fieldName} || 0; - # make a copy of the unsorted array pointer - my ($unsortedArray) = $this->{'fileArray'}; - # remove the column names from the array - my ($fieldNames) = shift(@$unsortedArray); - - # now we sort the unsorted array - my (@sortedArray) = sort { - # custom sorting comparison routine - my (@aVals) = split($delimiter,$a); - my (@bVals) = split($delimiter,$b); - if ($desc) { - return $bVals[$fieldNumber] cmp $aVals[$fieldNumber]; - } else { - return $aVals[$fieldNumber] cmp $bVals[$fieldNumber]; - } - undef(@aVals); - undef(@bVals); - } @$unsortedArray; - # get rid of the unsorted array - undef($unsortedArray); - - # put the column names back in and update the array pointer - unshift(@sortedArray,$fieldNames); - $this->{'fileArray'} = \@sortedArray; - - $this->MoveFirst; - return 1; -} - - -#_____________________________________________________________________________ -sub Open { - # open the file, store the contents as an array, get the number of - # records and retreive the first row - my $this = shift; - my $fileName = $this->{'fileName'}; - my $delimiter = $this->{'delimiter'}; - my (@tempFileArray); - - # security check to make sure a command is not being attempted - $fileName =~ s/;//g; - $fileName =~ s/|//g; - - if (!(-e $fileName)) { - $this->{'lastError'} = "Open: Datafile '$fileName' Not Found"; - return 0; - } elsif (!(-r $fileName)) { - $this->{'lastError'} = "Open: Couldn't Open DataFile '$fileName' For Reading"; - return 0; - } - - # try to open the file - if (open(THISFILE, "$fileName")) { - @tempFileArray = ; - close(THISFILE); - } else { - return 0; - } - - # get the entire contents of the file - $this->{'fileArray'} = \@tempFileArray; - - # get the number of rows - $this->{'recordCount'} = @tempFileArray - 1; - - # get the top row, which should be fieldnames - my $fileRow = $tempFileArray[0]; - chop($fileRow); - - # split the top row into fields - my (@tempfieldNames) = split($delimiter,$fileRow); - my ($fieldName) = ""; - my ($counter) = 0; - foreach $fieldName (@tempfieldNames) { - $this->{'fieldNames'}{$fieldName} = $counter; - $counter++ - } - - $this->MoveFirst; - $this->{'isOpen'} = 1; - $this->_RefreshRow; - $this->{'filterRecordCount'} = $this->{'recordCount'}; - $this->{'originalCount'} = $this->{'recordCount'}; - return 1; -} - -#_____________________________________________________________________________ -sub Close { - my ($this) = shift; - $this->{'isOpen'} = 0; - return 1; -} - - -# ########################################################################### -# PRIVATE METHODS - - -#_____________________________________________________________________________ -sub DESTROY { - my ($this) = shift; - $this->Close; -} - -#_____________________________________________________________________________ -sub _RefreshRow { - # sync the current row with the fileArray. also do some validation to - # make sure we haven't moved the curser out of range - my ($this) = shift; - return 0 unless ($this->{'isOpen'}); - # make sure absolutePosition is a legit value and set EOF - $this->{'EOF'} = 0; - $this->{'absolutePosition'} = 1 if ($this->{'absolutePosition'} < 1); - if ($this->{'absolutePosition'} > $this->{'recordCount'}) { - $this->{'EOF'} = 1; - $this->{'absolutePosition'} = $this->{'recordCount'}; - return 1; - } - - # now grab the next row - my ($tempRow) = $this->{'fileArray'}[$this->{'absolutePosition'}]; - chop ($tempRow); - my (@row) = split($this->{'delimiter'},$tempRow); - $this->{'row'} = \@row; - return 1; -} - - -#_____________________________________________________________________________ -sub _GetSetProperty { - # private fuction that is used by properties to get/set values - # if a parameter is sent in, then the property is set and true is returned. - # if no parameter is sent, then the current value is returned - my $this = shift; - my $fieldName = shift; - my $newValue = shift; - if (defined($newValue)) { - $this->{$fieldName} = $newValue; - } else { - return $this->{$fieldName}; - } - return 1; -} - -1; # for require - - -__END__ - -=head1 NAME - -vsDB - Simple interface to text-delimited data files - -=head1 SYNOPSIS - - use vsDB; - - # create the object - my (objDB) = new vsDB(filename=>'C:\\datafile.txt', delimiter=>'\t'); - - # open the datafile - $objDB->Open; - - # add a new record - $objDB->AddNew; - - # update the first name field for the new record - $objDB->FieldValue('FirstName','Jason'); - - # commit the changes to disk - $objDB->Commit; - - # move the cursor to the beginning of the resultset - $objDB->MoveFirst; - - # print all of the first name fields - while (!$objDB->EOF) { - print $objDB->FieldValue('FirstName'); - $objDB->MoveNext; - } - - # close the datafile (optional) - $objDB->Close; - -=head1 DESCRIPTION - -vsDB provides a simple object-oriented interface for delimited -text files. The object model is based off of Microsoft's -ADO RecordSet object, so anyone familiar with this will -find vsDB somewhat familiar. vsDB has been tested on Win32 and -Linux. - -=head1 OBJECT MODEL REFERENCE: PROPERTIES - -=head2 AbsolutePosition([nNewPosition]) - -AbsolutePosition returns the current cursor position in the RecordSet. If -[nNewPosition] is specified, then AbsolutePosition attempts to move the -cursor to that position. If [nNewPosition] is out of range, AbsolutePosition -will be set to the closest valid position (usually the last record) - -=head2 ActivePage([nNewPage]) - -ActivePage returns the current "Page" in the RecordSet. If [nNewPage] is -specified, then ActivePage attempts to move the cursor to the first record -on the given page. If [nNewPage] is out of range, The cursor will be -set to the closest valid position (usually the first record of the last page) - -ActivePage is used along with PageSize. See PageSize for more information. - -=head2 CR([strCR]) - -CR is a character or string that vsDB uses to replace a Carriage Return -character that is inserted in the database. Default is "". The value -is used only during storage in the file and is converted back into -a Carriage Return when you request the field value. See also: LF - -=head2 Delimiter([strNewDelimiter]) - -Delimiter returns the delimiter character that is used to separate fields -in the datafile. If strNewDelimiter is specified, then the delimiter is -changed. - -Warning: changing the delimiter property after calling the Open method -is a very bad thing to do! Your file may become corrupted. - -=head2 EOF() - -EOF (End Of File) indicates that there are no more records in the RecordSet. -If you have applied a filter, this indicates when you have reached the end -of the matching records. This property is commonly used to loop through -a recordset, for example: while (!$objDB->EOF) { $objDB->MoveNext; } - -Note: unlike the MS RecordSet object, FieldValue will not give an EOF error -if you try to access a FieldValue when the RecordSet is at EOF. Instead -is will continue to return values for the last record in the RecordSet - -=head2 FieldValue(FieldName,[NewValue]) - -If [NewValue] is NOT specified, then FieldValue returns the value of the -field specified (FieldName) for the record at the current cursor position. - -If [NewValue] is specified, then the value of the field specified (FieldName) -for the record at the current cursor position is updated to [NewValue] and -1 is returned. - -Note: any changes you make to the data will not be saved to disk until you call -the Commit method. - -=head2 FieldNames() - -FieldNames returns an array containing all of the field names in the datafile. -For example: - - my (@fieldNames) = $objDB->FieldNames; - -=head2 File([strNewFilePath]) - -File specifies the full path to the datafile. This can be specified when -the object is created or anytime before calling the Open method. Once the -file has been opened the RecordSet will not change if you change the File -property. However, if you change the File property and then call the -Commit method, this will save the current RecordSet to the new filepath. -In other words, it will copy the original file. - -Warning: Changing the File property then calling Open again may produce -unexpected results. If you need to access another datafile, it is recommended -that you create another vsDB object instead. - -=head2 ID() - -Returns module identification - -=head2 LastError() - -When a non-fatal error has occured, the LastError property may contain information -decribing the error. Most methods will return 1 or 0 to indicate success or -failure. You do not need to check these return values, but should in cases where -you suspect the method could fail. - -=head2 LF([strLF]) - -LF is a character or string that vsDB uses to replace a Line Feed -character that is inserted in the database. Default is "". The value -is used only during storage in the file and is converted back into -a Line Feed when you request the field value. See also: CR - -=head2 Max(fieldName, [alpha]) - -Max returns the maximum value for he specified fieldName. alpha is an optional -value that is set to 1 or 0 to indicate alphabetical characters. By default, alpha -is set to 0, indicating that the field is numeric. - -Warning: if your field contains non-numeric values, you must set alpha=1 or Max -will produce a type-mismatch error. - -=head2 Min(fieldName, [alpha]) - -Min returns the minimum value for he specified fieldName. alpha is an optional -value that is set to 1 or 0 to indicate alphabetical characters. By default, alpha -is set to 0, indicating that the field is numeric. - -Warning: if your field contains non-numeric values, you must set alpha=1 or Min -will produce a type-mismatch error. - -=head2 PageCount() - -PageCount returns the number of pages in the RecordSet. This is essentially -the RecordCount devided by the PageSize. If you have applied a -filter, the PageCount will indicate only matching records. - -=head2 PageSize([nNewSize]) - -PageSize returns the current page size. If [nNewSize] is specified, then -the PageSize is set to the new value. PageSize is used along with ActivePage -to simplify displaying a subset of the total records. For example, the -file contains 1,000 rows, but you want to display them to the user only -10 at a time. The PageSize is set to 10 and you can navigate through the -results by changing the ActivePage. - -=head2 RecordCount() - -Returns the number of records in the RecordSet. If you have applied a filter, -RecordCount will indicate the number of matching records. - -=head2 Row() - -Row returns an array containing all of the values of the current record. -For example: - - my (@row) = $objDB->Row; - -=head2 Version() - -Returns current version - -=head2 xml([strRootName] [,strElementName]) - -Returns current recordset as xml. strRootName and strElementName are -optional. Default values are "vsDB" and "Record" - - -=head1 OBJECT MODEL REFERENCE: METHODS - -=head2 AddNew() - -Adds a new record to the RecordSet and moves the cursor to this new record. -The default values for all fields is an empty string. After you add a new -record, you will want to change the FieldValues as needed. - -If you are using one of the fields as a primary key, you can use the Max -property to obtain the highest ID number. - -Note: any changes you make to the data will not be saved to disk until you call -the Commit method. - -=head2 AddNewField(strFieldName [,strDefaultValue]) - -Adds a new field to the RecordSet. strFieldName is the name of the new field. -The new field will be added to all records and set to strDefaultValue. If -strDefaultValue is not specified, then the field will be empty. - -Note: any changes you make to the data will not be saved to disk until you call -the Commit method. - -=head2 Close() - -In theory this would close the file, however vsDB does not keep the file handle -open. Currently this method simply marks the object as closed. This method -is also called automatically when the object is destroyed. - -Although it is not necessary to call this method, it is recommended that you do -in case vsDB is later modified to keep the file handle open. This might be -useful for a persistent connection to the file...? - -=head2 Commit([blnUseFLock]) - -Commit writes the current RecordSet in memory to the filepath specified by -the File property. This method should be called any time there have been data -modifications. blnUseFLock is an optional argument that should be 1 if flock -should be used while writing to the file. - -Commit re-opens the datafile with the least amount of privledges required. If you -have not made any changes to the RecordSet, calling Close will not access the -datafile at all. If you have only added new records, Commit will open the datafile -for appending and append the new record. If you have modified existing records, -the file will be opening for writing and the entire file will be updated. - -=head2 Delete() - -Deletes the current record in the RecordSet. - -Note: any changes you make to the data will not be saved to disk until you call -the Commit method. - -=head2 Filter(strFieldName,strOperator,strCriteria) - -Filter provides a way to either search the RecordSet or to get a specific record -based on a primary key field. strFieldName indicates the field that you want to -filter. strOperator is one of the following "eq", "ne", "like", "<" or ">" to indicate -how the field is to be compared. strCriteria indicates the search pattern that you -wish to find. - -You can apply the Filter method more than once to further filter out records. The -filters are applied as "AND." Currently there is no support for "OR" filtering. - -If you are using a primary key field, you can use the Filter method to locate -the row that you want. For example: - - $objDB->Filter("ID","eq","25") - -The Filter method moves the cursor to the first matching record in the recordset -as well as updates RecordCount and PageCount accordingly. - -Note: calling Sort will remove any filters that you have applied. Call Sort -first if you need to sort and filter the results. - -=head2 MoveNext() - -Advances the cursor to the next row in the RecordSet. In other words, -it "moves" to the next record. - -=head2 MovePrevious() - -Moves the cursor to the previous row in the RecordSet. In other words, -it "moves" to the previous record. - -=head2 MoveFirst() - -Moves the cursor to the first row in the RecordSet. - -=head2 MoveLast() - -Moves the cursor to the last row in the RecordSet. - -=head2 Open() - -Opens the datafile for reading and populates the RecordSet bases on the data in -the file. The datafile handle is actually closed immediately after reading -the file, however the RecordSet is stored in memory. (To sync the datafile up -with the RecordSet, refer to the Commit method.) - -Warning: calling Open more than once may cause unexpected results. - -=head2 RemoveFilter() - -RemoveFilter removes any filtering that you have done using the Filter method -and moves the cursor to the first row in the RecordSet. - -=head2 Sort(strFieldName [,Descending]) - -Sort sorts the RecordSet by the strFieldName. You can sort by multiple fields -by calling the Sort method more than once with a different fieldname each time. - -Descending should be 1 if you want the sort to be in descending order instead -of the default ascending order. - -Note: Calling Sort will remove any Filters that you have applied. If you want to -sort and filter, then call Sort first, then Filter. - -Warning: If you call the Commit method after sorting, the records will be saved -to the datafile in the order in which they are sorted. This may be desirable -if you always sort the same way, but proceed with caution. - -=head1 VERSION HISTORY - - 1.3.9: don't allow FieldValue or Delete if EOF is true - 1.3.8: added UseFLock argument to Commit method - 1.3.7: Updated error messages - 1.3.6: AddNew now moves to the last record properly - 1.3.5: fixed record jumbling bug in xml property - 1.3.4: filter "like" option made case-insensitive, updated ActivePage - 1.3.3: fixed EOF not being set properly when filtering - 1.3.2: added xml Property - 1.3.1: added Desc option to sort routine - 1.3.0: added AddNewField - 1.2.7: CR and LF properties added. fixed bug with line breaks - in the data. Added filename security check to ->Open - 1.2.6: resolved filter + sort problem. cleaned up documentation - 1.2.5: added destructer, removed file locking code - 1.2.4: fixed PageCount and RecordCount bug when using filter - 1.2.3: optimized AddNew, fixed null field bug in FieldValue - 1.2.2: optimized file access, added more error checking - 1.2.1: Fixed ActivePage/Filter bug filtering out 1st record - 1.2.0: Added ActivePage, PageSize, PageCount properties - 1.1.4: Updated Max property to deal with numbers - 1.1.3: Added Filter method - 1.1.2: Added Sort, Min, Max methods - 1.0.1: Original Release - -=head1 KNOWN ISSUES & LIMITATIONS - -vsDB loads the entire datafile into an array which could cause performance -problems if your datafile grows large. (largest test file was 17,000 records) - -ActivePage and possibly other page-related properties may return unexpected -values when used in combination with filtering. - -=head1 AUTHOR - -Jason M. Hinkle - -=head1 COPYRIGHT - -Copyright (c) 2001 Jason M. Hinkle. All rights reserved. -This module is free software; you can redistribute it and/or modify -it under the same terms as Perl itself. - -=cut - +# ---------------------------------------------------------------------------- +# vsDB (verysimple database) Module +# Copyright (c) 2001 Jason M. Hinkle. All rights reserved. This module is +# free software; you may redistribute it and/or modify it under the same +# terms as Perl itself. +# For more information see: http://www.verysimple.com/scripts/ +# +# LEGAL DISCLAIMER: +# This software is provided as-is. Use it at your own risk. The +# author takes no responsibility for any damages or losses directly +# or indirectly caused by this software. +# ---------------------------------------------------------------------------- +package vsDB; +require 5.000; +$VERSION = "1.4.3"; +$ID = "vsDB.pm"; + + +#_____________________________________________________________________________ +sub new { + my $class = shift; + my %keyValues = @_; + my (%fieldNames,@fileArray,@row,@filterArray); + + # normalize the input for backwards compatibility & to set default delim. + $keyValues{'file'} = $keyValues{'file'} || $keyValues{'File'} || ""; + $keyValues{'delimiter'} = $keyValues{'Delimiter'} unless defined($keyValues{'delimiter'}); + $keyValues{'delimiter'} = "\t" unless defined($keyValues{'delimiter'}); + + my $this = { + fileName => $keyValues{'file'}, + delimiter => $keyValues{'delimiter'}, + fieldNames => \%fieldNames, + fileArray => \@fileArray, + filterArray => \@filterArray, + row => \@row, + recordCount => 0, + filterRecordCount => 0, + absolutePosition => 0, + pageSize => 10, + EOF => 1, + isOpen => 0, + lastError => '', + appendOnly => 1, + isDirty => 0, + originalCount => 0, + CR => '', + LF => '', + noFieldNames => $keyValues{'NoFieldNames'}, + }; + bless $this; + return $this; +} + + +# ########################################################################### +# PUBLIC PROPERTIES + + +#_____________________________________________________________________________ +sub Version { + return $VERSION; +} + +#_____________________________________________________________________________ +sub ID { + return $ID; +} + +#_____________________________________________________________________________ +sub LastError { + my ($this) = shift; + return $this->{'lastError'}; +} + +#_____________________________________________________________________________ +sub AbsolutePosition { + my ($this) = shift; + my ($newValue) = shift; + if (defined($newValue)) { + $this->{'absolutePosition'} = $newValue; + $this->_RefreshRow; + } else { + return $this->{'absolutePosition'}; + } +} + +#_____________________________________________________________________________ +sub ActivePage { + my ($this) = shift; + my ($newValue) = shift; + if (defined($newValue)) { + $newValue = $this->PageCount if ($newValue > $this->PageCount); + $this->MoveFirst; + # don't need to do anything if page 1 + return 1 if ($newValue == 1); + # set the new page, just move next until we hit the right spot + my ($records) = ($newValue * $this->PageSize) - $this->PageSize; + for (my $count = 0; $count < $records && !$this->EOF; $count++) { + $this->MoveNext; + } + # old code- faster, but doesnt work right with filters + #$this->{'absolutePosition'} = ($this->{'pageSize'} * ($newValue-1)) + 1; + # make sure we are on the right page if filtered + #while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { + # $this->{'absolutePosition'}++; + #} + #$this->_RefreshRow; + return 1; + } else { + # BUG - when filtering, this returns the wrong value + return 1 if ($this->{'absolutePosition'} == 1); + my ($count) = (($this->{'absolutePosition'} - 1) / $this->{'pageSize'}); #/ + my ($activePage) = int($count) + 1; + $activePage = $this->PageCount if ($activePage > $this->PageCount); + return $activePage; + } +} + +#_____________________________________________________________________________ +sub PageSize { + my ($this) = shift; + my ($newValue) = shift; + if (defined($newValue)) { + $this->{'pageSize'} = int($newValue) if (int($newValue) > 0); + return 1; + } else { + return $this->{'pageSize'}; + } +} + +#_____________________________________________________________________________ +sub PageCount { + my ($this) = shift; + my ($count) = ($this->{'filterRecordCount'} / $this->{'pageSize'}); #/ + if (int($count) < $count) {$count = int($count)+1} + return $count; +} + +#_____________________________________________________________________________ +sub File { + my ($this) = shift; + my ($newVal) = shift; + # if the file has changed, then we can't just append... + if ($newVal) {$this->{'appendOnly'} = 0} + return $this->_GetSetProperty("fileName",$newVal); +} + +#_____________________________________________________________________________ +sub CR { + return shift->_GetSetProperty("CR",shift); +} + +#_____________________________________________________________________________ +sub NoFieldNames { + return shift->_GetSetProperty("noFieldNames",shift); +} + +#_____________________________________________________________________________ +sub LF { + return shift->_GetSetProperty("LF",shift); +} + +#_____________________________________________________________________________ +sub Delimiter { + return shift->_GetSetProperty("delimiter",shift); +} + +#_____________________________________________________________________________ +sub RecordCount { + return shift->{'filterRecordCount'}; +} + +#_____________________________________________________________________________ +sub EOF { + my ($this) = shift; + return 1 if ($this->RecordCount < 1); + return $this->{'EOF'}; +} + +#_____________________________________________________________________________ +sub FieldValue { + my ($this) = shift; + my ($fieldName) = shift || return "ERROR: FieldValue(): Field Name Required"; + my ($newValue) = shift; + my ($fieldNumber) = $this->{'fieldNames'}{$fieldName}; + my ($lineFeed) = chr(10); + my ($carriageReturn) = chr(13); + my ($crReplacement) = $this->{'CR'}; + my ($lfReplacement) = $this->{'LF'}; + return "ERROR: FieldValue('" . $fieldName . "') Field Not Found." if (!defined($fieldNumber)); + + # if a new value is defined, update, otherwise return current value + if (defined($newValue)) { + $this->{'isDirty'} = 1; + if ($this->{'absolutePosition'} <= $this->{'originalCount'}) { + $this->{'appendOnly'} = 0 + }; + # make sure we don't corrupt the file with a delimiter or + # line break in the data. + $newValue =~ s/$this->{'delimiter'}//g; + $newValue =~ s/$carriageReturn/$crReplacement/g; + $newValue =~ s/$lineFeed/$lfReplacement/g; + # $newValue =~ s/\n//g; # (should already be dealt with) + $this->{'row'}[$fieldNumber] = $newValue; + + # update the fileArray to match the current row. originally used + # a join on the row array, but that caused unititialize var errors + # when there are blank fields. this could probably be improved by + # only updating when the cursor is moved or commit is called + my ($newRow,$newField); + my ($colNum) = 0; + foreach ($this->FieldNames) { + $newField = $this->{'row'}[$colNum]; + if (!defined($newField)) {$newField = ""} + $newRow .= $newField; + $newRow .= $this->{'delimiter'}; + $colNum++; + } + # get rid of the last delimiter + for (my $x = 1;$x <= length($this->{'delimiter'}); $x++) { + chop($newRow); + } + + $this->{'fileArray'}[$this->{'absolutePosition'}] = $newRow . "\n"; + return 1; + } else { + # make sure we return a defined value || won't work because it doesn;t + # differentiate between 0 and null + if (defined($this->{'row'}[$fieldNumber])) { + my $returnVal = $this->{'row'}[$fieldNumber]; + $returnVal =~ s/$crReplacement/$carriageReturn/g; + $returnVal =~ s/$lfReplacement/$lineFeed/g; + return $returnVal; + + } else { + return ""; + } + } +} + +#_____________________________________________________________________________ +sub FieldNames { + # returns all fieldnames as an array + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + my ($fieldRow) = $this->{'fileArray'}[0]; + chop ($fieldRow); + my (@tempfieldNames) = split($this->{'delimiter'},$fieldRow); + return @tempfieldNames; +} + +#_____________________________________________________________________________ +sub Row { + # returns current row values as an array + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + my ($tempRow) = $this->{'row'}; + return @$tempRow; +} + +#_____________________________________________________________________________ +sub xml { + # returns current recordset as xml + my ($this) = shift; + my ($strRootName) = shift || "vsDB"; + my ($strElementName) = shift || "Record"; + my ($strXml); + + $strXml = "\n"; + $strXml .= "\n"; + $strXml .= "<$strRootName>\n"; + $this->MoveFirst; + my (@fields) = $this->FieldNames; + my ($field, $fieldValue); + until ($this->EOF) { + $strXml .= "<$strElementName>\n"; + foreach $field (@fields) { + $fieldValue = $this->FieldValue($field); + $strXml .= "<$field>$fieldValue\n"; + } + $strXml .= "\n"; + $this->MoveNext; + } + $strXml .= "\n"; + + return $strXml; +} + +#_____________________________________________________________________________ +sub MoveNext { + # moves the curser to the next row in the data file + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + return 0 if ($this->{'EOF'}); + $this->{'absolutePosition'}++; + while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { + $this->{'absolutePosition'}++; + } + $this->_RefreshRow; + return 1; +} + +#_____________________________________________________________________________ +sub MovePrevious { + # moves the curser to the previous row in the data file + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + return 0 if ($this->{'absolutePosition'} < 2); + $this->{'absolutePosition'}--; + while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { + $this->{'absolutePosition'}--; + } + $this->_RefreshRow; + return 1; +} + +#_____________________________________________________________________________ +sub MoveFirst { + # moves the curser to the first row in the data file + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + $this->{'absolutePosition'} = 1; + while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { + $this->{'absolutePosition'}++; + } + $this->_RefreshRow; + return 1; +} + +#_____________________________________________________________________________ +sub MoveLast { + # moves the curser to the last row in the data file + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + $this->{'absolutePosition'} = $this->{'recordCount'}; + while ($this->{'filterArray'}[$this->{'absolutePosition'}]) { + $this->{'absolutePosition'}--; + } + $this->_RefreshRow; + return 1; +} + +#_____________________________________________________________________________ +sub Delete { + # delete the current row + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + return 0 if $this->{'recordCount'} < 1; + return 0 if ($this->{'EOF'}); + $this->{'isDirty'} = 1; + if ($this->{'absolutePosition'} <= $this->{'originalCount'}) { + $this->{'appendOnly'} = 0 + }; + my ($tempArray) = $this->{'fileArray'}; + splice(@$tempArray,$this->{'absolutePosition'},1); + $this->{'recordCount'}--; + $this->{'filterRecordCount'}--; + $this->_RefreshRow; + return 1; +} + +#_____________________________________________________________________________ +sub AddNew { + # add a new row to the end of the recordset + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + $this->{'isDirty'} = 1; + $this->{'recordCount'}++; + $this->{'absolutePosition'} = $this->{'recordCount'}; + $this->{'filterRecordCount'}++; + # the number of delimiter chars is fieldCount - 1 + my ($delimiterCount) = 0; + foreach ($this->FieldNames) { + $delimiterCount++; + } + $delimiterCount--; + + # add the correct number of colums using the delimiterCount + $this->{'fileArray'}[$this->{'absolutePosition'}] = ($this->{'delimiter'} x $delimiterCount) . "\n"; + + $this->MoveLast; + + return 1; +} + +#_____________________________________________________________________________ +sub AddNewField { + # add a new row to the end of the recordset + my ($this) = shift; + my ($newFieldName) = shift || return 0; + my ($defaultValue) = shift; + $defaultValue = '' unless (defined($defaultValue)); + + $this->{'isOpen'} = 1; + $this->{'appendOnly'} = 0; + $this->{'isDirty'} = 1; + + # update the fieldnames array + my ($nextField) = 0; + foreach ($this->FieldNames) { + $nextField++; + } + $this->{'fieldNames'}{$newFieldName} = $nextField; + + chop($this->{'fileArray'}[0]); + + # update the first row in the file array + if ($nextField > 0) { + $this->{'fileArray'}[0] .= $this->{'delimiter'}; + } + $this->{'fileArray'}[0] .= $newFieldName . "\n"; + + return 1; +} + +#_____________________________________________________________________________ +sub Max { + # returns the maximum value for the specified column + my ($this) = shift; + my ($fieldName) = shift || return 0; + my ($alpha) = shift || 0; + my ($curVal); + my ($curPos) = $this->{'absolutePosition'}; + $this->MoveFirst; + my ($maxVal) = $this->FieldValue($fieldName); + while (!$this->EOF) { + $curVal = $this->FieldValue($fieldName); + if (!$alpha) { + if ($curVal ne "") { + if (int($curVal) > int($maxVal)) {$maxVal = $curVal}; + } + } else { + if ((lc($curVal) cmp lc($maxVal)) > 0) {$maxVal = $curVal}; + } + $this->MoveNext; + } + $this->AbsolutePosition($curPos); + return $maxVal; +} + +#_____________________________________________________________________________ +sub Min { + # returns the maximum value for the specified column + my ($this) = shift; + my ($fieldName) = shift || return 0; + my ($alpha) = shift || 0; + my ($curVal); + my ($curPos) = $this->{'absolutePosition'}; + $this->MoveFirst; + my ($minVal) = $this->FieldValue($fieldName); + while (!$this->EOF) { + $curVal = $this->FieldValue($fieldName); + if ($alpha) { + if ($curVal ne "") { + if (int($curVal) < int($minVal)) {$minVal = $curVal}; + } + } else { + if ((lc($curVal) cmp lc($minVal)) < 0) {$minVal = $curVal}; + } + $this->MoveNext; + } + $this->AbsolutePosition($curPos); + return $minVal; +} + +#_____________________________________________________________________________ +sub Filter { + # $obj->Filter($fieldName,$operator,$criteria [,$filterOr]); + + # TODO: > and < are not working properly... maybe text comparison problem?? + my ($this) = shift; + my ($fieldName) = shift || return 0; + my ($operator) = shift || "eq"; + my ($criteria) = shift; + my ($filterOr) = shift || 0; + $criteria = "" unless defined($criteria); + + my ($filterSetting); + my ($absolutePosition) = 0; + + # manually cycle through because MoveNext will skip past any prev. filtered records + $this->{'absolutePosition'} = 1; + $this->_RefreshRow; + while ($this->{'absolutePosition'} != $absolutePosition) { + $filterSetting = 0; + $absolutePosition = $this->{'absolutePosition'}; + + #print "original val=" . ($this->{'filterArray'}[$absolutePosition] || 'x') . " "; + + if ($filterOr && !$this->{'filterArray'}[$absolutePosition]) { + # leave record alone if it already passed and OR is specified + $filterSetting = 0; + } elsif (!$filterOr && $this->{'filterArray'}[$absolutePosition]) { + # don't undue any previous filters if AND is specified + $filterSetting = 1; + } elsif ($operator eq "eq" && $this->FieldValue($fieldName) ne $criteria) { + $filterSetting = 1; + } elsif ($operator eq "ne" && !($this->FieldValue($fieldName) ne $criteria)) { + $filterSetting = 1; + } elsif ($operator eq "like" && !(index(lc($this->FieldValue($fieldName)),lc($criteria),0) + 1)) { + $filterSetting = 1; + } elsif ($operator eq ">" && !($this->FieldValue($fieldName) > $criteria) ) { + $filterSetting = 1; + } elsif ($operator eq "<" && !($this->FieldValue($fieldName) < $criteria) ) { + $filterSetting = 1; + } + + # print $absolutePosition . ": " . $filterSetting; + + # the filtercount may need to be decremented or incremented, depending on if + # the current record was filtered, re-filtered, or unfiltered + if ($this->{'filterArray'}[$absolutePosition]) { + $this->{'filterRecordCount'} = $this->{'filterRecordCount'} + 1 unless ($filterSetting); + } else { + $this->{'filterRecordCount'} = $this->{'filterRecordCount'} - $filterSetting; + } + + $this->{'filterArray'}[$absolutePosition] = $filterSetting; + + #print " filterCount=" . $this->{'filterRecordCount'} . "
    "; + + $this->{'absolutePosition'}++; + $this->_RefreshRow; + } + + $this->MoveFirst; + return 1; +} + +#_____________________________________________________________________________ +sub RemoveFilter { + my ($this) = shift; + my (@newArray); + $this->{'filterArray'} = \@newArray; + $this->{'filterRecordCount'} = $this->{'recordCount'}; + return 1; +} + +#_____________________________________________________________________________ +sub Commit { + # update the file, saving all changes made + my ($this) = shift; + my ($useFlock) = shift || 0; + my ($fileName) = $this->{'fileName'}; + + # if no changes were made, don't bother writing to the file + if (!$this->{'isDirty'}) {return 1}; + + if ($this->{'appendOnly'}) { + # if only new records were added, just append to the file + my ($nCount); + if (!open (OUTPUTFILE, ">>$fileName")) { + $this->{'lastError'} = "Commit: Couldn't Open DataFile '$fileName' For Appending"; + return 0 + }; + flock(OUTPUTFILE,2) if ($useFlock); + my ($tempArray) = $this->{'fileArray'}; + for ($nCount = $this->{'originalCount'} + 1; $nCount <= $this->{'recordCount'}; $nCount++) { + print OUTPUTFILE @$tempArray[$nCount]; + } + } else { + # if records were changed or deleted, we have to replace them all + if (!open (OUTPUTFILE, ">$fileName")) { + $this->{'lastError'} = "Commit: Couldn't Open DataFile '$fileName' For Writing"; + return 0 + }; + flock(OUTPUTFILE,2) if ($useFlock); + my ($tempArray) = $this->{'fileArray'}; + if ($this->NoFieldNames) { + # if no field names was specified, remove the dummy row before saving, then + # put it back on after the save is complete + my ($headerRow) = shift(@$tempArray); + print OUTPUTFILE join('',@$tempArray); + unshift(@$tempArray,$headerRow); + } else { + print OUTPUTFILE join('',@$tempArray); + } + } + close (OUTPUTFILE); + flock(OUTPUTFILE,8) if ($useFlock); + return 1; +} + +#_____________________________________________________________________________ +sub Sort { + # sorts the datafile on the given column + # obj->Sort($field [, $sortMode]); + # if $field is ommited, or an invalid fieldname is used, defaults to + # the left-most column. + # $sortMode 0 = alpha ascending, 1 = alpha descending, 2 = numeric ascending, 3 = numeric descending + + my ($this) = shift; + my ($fieldName) = shift || '0'; + my ($sortMode) = shift || '0'; + my ($delimiter) = $this->{'delimiter'}; + + # can't append once we've changed the sort order + $this->{'appendOnly'} = 0; + + # sorting will mess up filter, so lets remove it + $this->RemoveFilter; + + # get the fieldnumber (or default to leftmost column) + my ($fieldNumber) = $this->{'fieldNames'}{$fieldName} || 0; + # make a copy of the unsorted array pointer + my ($unsortedArray) = $this->{'fileArray'}; + # remove the column names from the array + my ($fieldNames) = shift(@$unsortedArray); + + # now we sort the unsorted array + my (@sortedArray) = sort { + # custom sorting comparison routine + my (@aVals) = split($delimiter,$a); + my (@bVals) = split($delimiter,$b); + if ($sortMode eq "1") { + # alpha descending + return lc($bVals[$fieldNumber]) cmp lc($aVals[$fieldNumber]); + } elsif ($sortMode eq "2") { + # numeric ascending + return $bVals[$fieldNumber] > $aVals[$fieldNumber]; + } elsif ($sortMode eq "3") { + # numeric descending + return $bVals[$fieldNumber] > $aVals[$fieldNumber]; + } else { + # alpha ascending + return lc($aVals[$fieldNumber]) cmp lc($bVals[$fieldNumber]); + } + undef(@aVals); + undef(@bVals); + } @$unsortedArray; + # get rid of the unsorted array + undef($unsortedArray); + + # put the column names back in and update the array pointer + unshift(@sortedArray,$fieldNames); + $this->{'fileArray'} = \@sortedArray; + + $this->MoveFirst; + return 1; +} + + +#_____________________________________________________________________________ +sub Open { + # open the file, store the contents as an array, get the number of + # records and retreive the first row + my $this = shift; + my $fileName = $this->{'fileName'}; + my $delimiter = $this->{'delimiter'}; + my (@tempFileArray); + + # security check to make sure a command is not being attempted + $fileName =~ s/;//g; + $fileName =~ s/|//g; + + if (!(-e $fileName)) { + $this->{'lastError'} = "Open: Datafile '$fileName' Not Found"; + return 0; + } elsif (!(-r $fileName)) { + $this->{'lastError'} = "Open: Couldn't Open DataFile '$fileName' For Reading"; + return 0; + } + + # try to open the file + if (open(THISFILE, "$fileName")) { + @tempFileArray = ; + close(THISFILE); + } else { + return 0; + } + + # if no fieldnames is specified, then insert a dummy row at the front + # so the module will use numbers as the fieldnames + if ($this->NoFieldNames) { + my ($dummyRow) = ""; + my (@DummyFieldNames) = split($delimiter, ($tempFileArray[0] || "") ); + my ($dummyDelim) = ""; + my ($count) = 1; + foreach (@DummyFieldNames) { + $dummyRow .= $dummyDelim . $count; + $dummyDelim = $delimiter; + $dummyDelim = "\|" if ($dummyDelim eq "\\|"); # glitch on windows servers with | as delim + $count++; + } + unshift(@tempFileArray,$dummyRow . "\n") + } + + # get the top row, which should be fieldnames + my $fileRow = $tempFileArray[0] || "\n"; + chop($fileRow); + + # get the entire contents of the file + $this->{'fileArray'} = \@tempFileArray; + + # get the number of rows + $this->{'recordCount'} = @tempFileArray - 1; + + # split the top row into fields + my (@tempfieldNames) = split($delimiter,$fileRow); + my ($fieldName) = ""; + my ($counter) = 0; + foreach $fieldName (@tempfieldNames) { + $this->{'fieldNames'}{$fieldName} = $counter; + $counter++ + } + + + $this->MoveFirst; + $this->{'isOpen'} = 1; + $this->_RefreshRow; + $this->{'filterRecordCount'} = $this->{'recordCount'}; + $this->{'originalCount'} = $this->{'recordCount'}; + return 1; +} + +#_____________________________________________________________________________ +sub Close { + my ($this) = shift; + $this->{'isOpen'} = 0; + return 1; +} + + +# ########################################################################### +# PRIVATE METHODS + + +#_____________________________________________________________________________ +sub DESTROY { + my ($this) = shift; + $this->Close; +} + +#_____________________________________________________________________________ +sub _RefreshRow { + # sync the current row with the fileArray. also do some validation to + # make sure we haven't moved the curser out of range + my ($this) = shift; + return 0 unless ($this->{'isOpen'}); + # make sure absolutePosition is a legit value and set EOF + $this->{'EOF'} = 0; + $this->{'absolutePosition'} = 1 if ($this->{'absolutePosition'} < 1); + if ($this->{'absolutePosition'} > $this->{'recordCount'}) { + $this->{'EOF'} = 1; + $this->{'absolutePosition'} = $this->{'recordCount'}; + return 1; + } + + # now grab the next row + my ($tempRow) = $this->{'fileArray'}[$this->{'absolutePosition'}]; + chop ($tempRow); + my (@row) = split($this->{'delimiter'},$tempRow); + $this->{'row'} = \@row; + return 1; +} + + +#_____________________________________________________________________________ +sub _GetSetProperty { + # private fuction that is used by properties to get/set values + # if a parameter is sent in, then the property is set and true is returned. + # if no parameter is sent, then the current value is returned + my $this = shift; + my $fieldName = shift; + my $newValue = shift; + if (defined($newValue)) { + $this->{$fieldName} = $newValue; + } else { + return $this->{$fieldName}; + } + return 1; +} + +1; # for require + + +__END__ + +=head1 NAME + +vsDB - Simple interface to text-delimited data files + +=head1 SYNOPSIS + + use vsDB; + + # create the object + my (objDB) = new vsDB(File=>'C:\\datafile.txt', Delimiter=>'\t'); + + # open the datafile + $objDB->Open; + + # add a new record + $objDB->AddNew; + + # update the first name field for the new record + $objDB->FieldValue('FirstName','Jason'); + + # commit the changes to disk + $objDB->Commit; + + # move the cursor to the beginning of the resultset + $objDB->MoveFirst; + + # print all of the first name fields + while (!$objDB->EOF) { + print $objDB->FieldValue('FirstName'); + $objDB->MoveNext; + } + + # close the datafile (optional) + $objDB->Close; + +=head1 DESCRIPTION + +vsDB provides a simple object-oriented interface for delimited +text files. The object model is based off of Microsoft's +ADO RecordSet object, so anyone familiar with this will +find vsDB somewhat familiar. vsDB has been tested on Win32 and +Linux. + +=head1 OBJECT MODEL REFERENCE: PROPERTIES + +=head2 AbsolutePosition([nNewPosition]) + +AbsolutePosition returns the current cursor position in the RecordSet. If +[nNewPosition] is specified, then AbsolutePosition attempts to move the +cursor to that position. If [nNewPosition] is out of range, AbsolutePosition +will be set to the closest valid position (usually the last record) + +=head2 ActivePage([nNewPage]) + +ActivePage returns the current "Page" in the RecordSet. If [nNewPage] is +specified, then ActivePage attempts to move the cursor to the first record +on the given page. If [nNewPage] is out of range, The cursor will be +set to the closest valid position (usually the first record of the last page) + +ActivePage is used along with PageSize. See PageSize for more information. + +=head2 CR([strCR]) + +CR is a character or string that vsDB uses to replace a Carriage Return +character that is inserted in the database. Default is "". The value +is used only during storage in the file and is converted back into +a Carriage Return when you request the field value. See also: LF + +=head2 Delimiter([strNewDelimiter]) + +Delimiter returns the delimiter character that is used to separate fields +in the datafile. If strNewDelimiter is specified, then the delimiter is +changed. + +Warning: changing the delimiter property after calling the Open method +is a very bad thing to do! Your file may become corrupted. + +=head2 EOF() + +EOF (End Of File) indicates that there are no more records in the RecordSet. +If you have applied a filter, this indicates when you have reached the end +of the matching records. This property is commonly used to loop through +a recordset, for example: while (!$objDB->EOF) { $objDB->MoveNext; } + +Note: unlike the MS RecordSet object, FieldValue will not give an EOF error +if you try to access a FieldValue when the RecordSet is at EOF. Instead +is will continue to return values for the last record in the RecordSet + +=head2 FieldValue(FieldName,[NewValue]) + +If [NewValue] is NOT specified, then FieldValue returns the value of the +field specified (FieldName) for the record at the current cursor position. + +If [NewValue] is specified, then the value of the field specified (FieldName) +for the record at the current cursor position is updated to [NewValue] and +1 is returned. + +Note: any changes you make to the data will not be saved to disk until you call +the Commit method. + +=head2 FieldNames() + +FieldNames returns an array containing all of the field names in the datafile. +For example: + + my (@fieldNames) = $objDB->FieldNames; + +=head2 File([strNewFilePath]) + +File specifies the full path to the datafile. This can be specified when +the object is created or anytime before calling the Open method. Once the +file has been opened the RecordSet will not change if you change the File +property. However, if you change the File property and then call the +Commit method, this will save the current RecordSet to the new filepath. +In other words, it will copy the original file. + +Warning: Changing the File property then calling Open again may produce +unexpected results. If you need to access another datafile, it is recommended +that you create another vsDB object instead. + +=head2 ID() + +Returns module identification + +=head2 LastError() + +When a non-fatal error has occured, the LastError property may contain information +decribing the error. Most methods will return 1 or 0 to indicate success or +failure. You do not need to check these return values, but should in cases where +you suspect the method could fail. + +=head2 LF([strLF]) + +LF is a character or string that vsDB uses to replace a Line Feed +character that is inserted in the database. Default is "". The value +is used only during storage in the file and is converted back into +a Line Feed when you request the field value. See also: CR + +=head2 Max(fieldName, [alpha]) + +Max returns the maximum value for he specified fieldName. alpha is an optional +value that is set to 1 or 0 to indicate alphabetical characters. By default, alpha +is set to 0, indicating that the field is numeric. + +Warning: if your field contains non-numeric values, you must set alpha=1 or Max +will produce a type-mismatch error. + +=head2 Min(fieldName, [alpha]) + +Min returns the minimum value for he specified fieldName. alpha is an optional +value that is set to 1 or 0 to indicate alphabetical characters. By default, alpha +is set to 0, indicating that the field is numeric. + +Warning: if your field contains non-numeric values, you must set alpha=1 or Min +will produce a type-mismatch error. + +=head2 NoFieldNames([nNewVal]) + +Use this option if the datafile does not have fieldnames in the first row. +You must set this property = 1 BEFORE you open the file, though. +If you set this property = 1 before you open the file, then the first row will +be treated as data and not fieldnames. This can also be set when you +create the object if you specify NoFieldNames => 1 as a parameter. + +If you enable this property, then you can refer to the fields by their order. +So, the first field would be FieldValue("1"), the second FieldValue("2"), etc. + +=head2 PageCount() + +PageCount returns the number of pages in the RecordSet. This is essentially +the RecordCount devided by the PageSize. If you have applied a +filter, the PageCount will indicate only matching records. + +=head2 PageSize([nNewSize]) + +PageSize returns the current page size. If [nNewSize] is specified, then +the PageSize is set to the new value. PageSize is used along with ActivePage +to simplify displaying a subset of the total records. For example, the +file contains 1,000 rows, but you want to display them to the user only +10 at a time. The PageSize is set to 10 and you can navigate through the +results by changing the ActivePage. + +=head2 RecordCount() + +Returns the number of records in the RecordSet. If you have applied a filter, +RecordCount will indicate the number of matching records. + +=head2 Row() + +Row returns an array containing all of the values of the current record. +For example: + + my (@row) = $objDB->Row; + +=head2 Version() + +Returns current version + +=head2 xml([strRootName] [,strElementName]) + +Returns current recordset as xml. strRootName and strElementName are +optional. Default values are "vsDB" and "Record" + + +=head1 OBJECT MODEL REFERENCE: METHODS + +=head2 AddNew() + +Adds a new record to the RecordSet and moves the cursor to this new record. +The default values for all fields is an empty string. After you add a new +record, you will want to change the FieldValues as needed. + +If you are using one of the fields as a primary key, you can use the Max +property to obtain the highest ID number. + +Note: any changes you make to the data will not be saved to disk until you call +the Commit method. + +=head2 AddNewField(strFieldName [,strDefaultValue]) + +Adds a new field to the RecordSet. strFieldName is the name of the new field. +The new field will be added to all records and set to strDefaultValue. If +strDefaultValue is not specified, then the field will be empty. + +Note: any changes you make to the data will not be saved to disk until you call +the Commit method. + +=head2 Close() + +In theory this would close the file, however vsDB does not keep the file handle +open. Currently this method simply marks the object as closed. This method +is also called automatically when the object is destroyed. + +Although it is not necessary to call this method, it is recommended that you do +in case vsDB is later modified to keep the file handle open. This might be +useful for a persistent connection to the file...? + +=head2 Commit([blnUseFLock]) + +Commit writes the current RecordSet in memory to the filepath specified by +the File property. This method should be called any time there have been data +modifications. blnUseFLock is an optional argument that should be 1 if flock +should be used while writing to the file. + +Commit re-opens the datafile with the least amount of privledges required. If you +have not made any changes to the RecordSet, calling Close will not access the +datafile at all. If you have only added new records, Commit will open the datafile +for appending and append the new record. If you have modified existing records, +the file will be opening for writing and the entire file will be updated. + +=head2 Delete() + +Deletes the current record in the RecordSet. + +Note: any changes you make to the data will not be saved to disk until you call +the Commit method. + +=head2 Filter(strFieldName,strOperator,strCriteria [,blnOR)) + +Filter provides a way to either search the RecordSet or to get a specific record +based on a primary key field. strFieldName indicates the field that you want to +filter. strOperator is one of the following "eq", "ne", "like", "<" or ">" to indicate +how the field is to be compared. strCriteria indicates the search pattern that you +wish to find. + +You can apply the Filter method more than once to further filter out records. The +filters are applied as "AND." If you specify a value (other than 0) for blnOR, then +"OR" filtering will be used instead of AND. + +If you are using a primary key field, you can use the Filter method to locate +the row that you want. For example: + + $objDB->Filter("ID","eq","25") + +The Filter method moves the cursor to the first matching record in the recordset +as well as updates RecordCount and PageCount accordingly. + +Note: calling Sort will remove any filters that you have applied. Call Sort +first if you need to sort and filter the results. + +=head2 MoveNext() + +Advances the cursor to the next row in the RecordSet. In other words, +it "moves" to the next record. + +=head2 MovePrevious() + +Moves the cursor to the previous row in the RecordSet. In other words, +it "moves" to the previous record. + +=head2 MoveFirst() + +Moves the cursor to the first row in the RecordSet. + +=head2 MoveLast() + +Moves the cursor to the last row in the RecordSet. + +=head2 Open() + +Opens the datafile for reading and populates the RecordSet bases on the data in +the file. The datafile handle is actually closed immediately after reading +the file, however the RecordSet is stored in memory. (To sync the datafile up +with the RecordSet, refer to the Commit method.) + +Warning: calling Open more than once may cause unexpected results. + +=head2 RemoveFilter() + +RemoveFilter removes any filtering that you have done using the Filter method +and moves the cursor to the first row in the RecordSet. + +=head2 Sort(strFieldName [,SortMethod]) + +Sort sorts the RecordSet by the strFieldName. You can sort by multiple fields +by calling the Sort method more than once with a different fieldname each time. + +SortMethod is optional and has four options (below). If you specify a numeric sort, +then all field values for that column must be numeric or you will get a type mismatch +runtime error. + 0 = Alpha sort ascending (default) + 1 = Alpha sort descending + 2 = Numeric sort ascending + 3 = Numeric sort descending + +Note: Calling Sort will remove any Filters that you have applied. If you want to +sort and filter, then call Sort first, then Filter. + +Warning: If you call the Commit method after sorting, the records will be saved +to the datafile in the order in which they are sorted. This may be desirable +if you always sort the same way, but proceed with caution. + +=head1 VERSION HISTORY + + 1.4.3: Added NoFieldNames property for files without fieldnames in the first row + 1.4.2: Sort is updated to support numeric in addition to alphabetic sort + 1.4.1: Sort is now case-insensitive + 1.4.0: fixed active page bug when filter is on & added OR argument to Filter + 1.3.9: don't allow delete if EOF is true, FieldValue returns "EOF" + 1.3.8: added UseFLock argument to Commit method + 1.3.7: Updated error messages + 1.3.6: AddNew now moves to the last record properly + 1.3.5: fixed record jumbling bug in xml property + 1.3.4: filter "like" option made case-insensitive, updated ActivePage + 1.3.3: fixed EOF not being set properly when filtering + 1.3.2: added xml Property + 1.3.1: added Desc option to sort routine + 1.3.0: added AddNewField + 1.2.7: CR and LF properties added. fixed bug with line breaks + in the data. Added filename security check to ->Open + 1.2.6: resolved filter + sort problem. cleaned up documentation + 1.2.5: added destructer, removed file locking code + 1.2.4: fixed PageCount and RecordCount bug when using filter + 1.2.3: optimized AddNew, fixed null field bug in FieldValue + 1.2.2: optimized file access, added more error checking + 1.2.1: Fixed ActivePage/Filter bug filtering out 1st record + 1.2.0: Added ActivePage, PageSize, PageCount properties + 1.1.4: Updated Max property to deal with numbers + 1.1.3: Added Filter method + 1.1.2: Added Sort, Min, Max methods + 1.0.1: Original Release + +=head1 KNOWN ISSUES & LIMITATIONS + +vsDB loads the entire datafile into an array which could cause performance +problems if your datafile grows large. (largest test file was 17,000 records) + +ActivePage and possibly other page-related properties may return unexpected +values when used in combination with filtering. + +=head1 AUTHOR + +Jason M. Hinkle + +=head1 COPYRIGHT + +Copyright (c) 2001 Jason M. Hinkle. All rights reserved. +This module is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + diff --git a/lib/vsLock.pm b/lib/vsLock.pm index 4277ded9d..a242c957c 100644 --- a/lib/vsLock.pm +++ b/lib/vsLock.pm @@ -1,612 +1,611 @@ -;# $Id -;# vsLock v 0.103 -;# Modified by Jason M. Hinkle, 2001 -;# -;# Based on File::Lock -;# Copyright (c) 1998, Raphael Manfredi -;# -;# You may redistribute only under the terms of the Artistic License, -;# as specified in the README file that comes with the distribution. -;# -;# $Log: vsLock.pm,v $ -;# Revision 1.3 2004/02/01 19:24:35 winter -;# - 2.87 release -;# -;# Revision 0.1.1.1 1998/05/12 07:42:19 ram -;# patch1: Baseline for first alpha release. -;# - -######################################################################## -package vsLock; @ISA = qw(Exporter); - -# -# This package extracts the simple locking logic used by mailagent-3.0 -# into a standalone Perl module to be reused in other applications. -# - -use strict; -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); -use Sys::Hostname; - -require Exporter; - -@ISA = qw(Exporter); -@EXPORT = (); -@EXPORT_OK = qw(lock trylock unlock); -$VERSION = '0.103'; - -$vsLock::LOCKER = undef; # Default locking object - -# -# ->make -# -# Create a file locking object, responsible for holding the locking -# parameters to be used by all the subsequent locks requested from -# this locking object. -# -# Configuration attributes: -# -# max max number of attempts -# delay seconds to wait between attempts -# format how to derive lockfile from file to be locked -# hold max amount of seconds before breaking lock (0 for never) -# ext lock extension -# nfs true if lock must "work" on top of NFS -# warn flag to turn warnings on -# wmin warn once after that many waiting seconds -# wafter warn every that many seconds after first warning -# wfunc warning function to be called -# -# The creation routine first and sole argument is a "hash table list" listing -# all the configuration attributes. Missing attributes are given a default -# value. A call to ->configure can alter the configuration parameters of -# an existing object. -# -sub new { - my $self = bless {}, shift; - my (@hlist) = @_; - $self->configure(@hlist); - - # Set configuration defaults - $self->{'max'} = 30 unless $self->{'max'}; - $self->{'delay'} = 2 unless $self->{'delay'}; - $self->{'hold'} = 3600 unless $self->{'hold'}; - $self->{'ext'} = '.lock' unless defined $self->{'ext'}; - $self->{'nfs'} = 0 unless defined $self->{'nfs'}; - $self->{'warn'} = 1 unless defined $self->{'warn'}; - $self->{'wfunc'} = \&core_warn unless defined $self->{'wfunc'}; - $self->{'wmin'} = 15 unless $self->{'wmin'}; - $self->{'wafter'} = 20 unless $self->{'wafter'}; - - return $self; -} - -# -# ->configure -# -# Extract known configuration parameters from the specified hash list -# and use their values to change the object's corresponding parameters. -# -# Parameters are specified as (-warn => 1, -ext => '.lock') for instance. -# -sub configure { - my $self = shift; - my (%hlist) = @_; - my @known = qw(max delay hold format ext nfs warn wfunc wmin wafter); - foreach my $attr (@known) { - $self->{$attr} = $hlist{"-$attr"} if defined $hlist{"-$attr"}; - } -} - -# -# Attribute access -# - -sub max { $_[0]->{'max'} } -sub delay { $_[0]->{'delay'} } -sub format { $_[0]->{'format'} } -sub hold { $_[0]->{'hold'} } -sub nfs { $_[0]->{'nfs'} } -sub ext { $_[0]->{'ext'} } -sub warn { $_[0]->{'warn'} } -sub wmin { $_[0]->{'wmin'} } -sub wafter { $_[0]->{'wafter'} } -sub wfunc { $_[0]->{'wfunc'} } -sub Version { return $VERSION; } - -sub core_warn { CORE::warn(@_) } - -# -# ->lock -# -# Lock specified file, possibly using alternate file "format". -# Returns whether file was locked or not at the end of the configured -# blocking period. -# -# For quick and dirty scripts wishing to use locks, create the locking -# object if not invoked as a method, turning on warnings. -# -sub lock { - my $self = shift; - unless (ref $self) { # Not invoked as a method - unshift(@_, $self); - $self = $vsLock::LOCKER || - vsLock->make('-warn' => 1); - } - my ($file, $format) = @_; # File to be locked, lock format - return $self->_acs_lock($file, $format, 0); -} - -# -# ->trylock -# -# Attempt to lock specified file, possibly using alternate file "format". -# If the file is already locked, don't block and return false. -# -# For quick and dirty scripts wishing to use locks, create the locking -# object if not invoked as a method, turning on warnings. -# -sub trylock { - my $self = shift; - unless (ref $self) { # Not invoked as a method - unshift(@_, $self); - $self = $vsLock::LOCKER || - vsLock->make('-warn' => 1); - } - my ($file, $format) = @_; # File to be locked, lock format - return $self->_acs_lock($file, $format, 1); -} - -# -# ->unlock -# -# Unlock file. -# Returns true if file was unlocked. -# -sub unlock { - my $self = shift; - unless (ref $self) { # Not invoked as a method - unshift(@_, $self); - $self = $vsLock::LOCKER || - vsLock->make('-warn' => 1); - } - my ($file, $format) = @_; # File to be unlocked, lock format - return $self->_acs_unlock($file, $format); -} - -# -# ->lockfile -# -# Return the name of the lockfile, given the file name to lock and the custom -# string provided by the user. The following macros are substituted: -# %D: the file dir name -# %f: the file name (full path) -# %F: the file base name (last path component) -# %p: the process's pid -# %%: a plain % character -# -sub lockfile { - my $self = shift; - my ($file, $format) = @_; - local $_ = defined($format) ? $format : $self->format; - s/%%/\01/g; # Protect double percent signs - s/%/\02/g; # Protect against substitutions adding their own % - s/\02f/$file/g; # %f is the full path name - s/\02D/&dir($file)/ge; # %D is the dir name - s/\02F/&base($file)/ge; # %F is the base name - s/\02p/$$/g; # %p is the process's pid - s/\02/%/g; # All other % kept as-is - s/\01/%/g; # Restore escaped % signs - $_; -} - -# Return file basename (last path component) -sub base { - my ($file) = @_; - my ($base) = $file =~ m|^.*/(.*)|; - $base; -} - -# Return dirname -sub dir { - my ($file) = @_; - my ($dir) = $file =~ m|^(.*)/.*|; - $dir; -} - -# -# _acs_lock -- private -# -# Internal locking routine. -# -# If $try is true, don't wait if the file is already locked. -# Returns true if the file was locked. -# -sub _acs_lock { ## private - my $self = shift; - my ($file, $format, $try) = @_; - my $max = $self->max; - my $delay = $self->delay; - my $stamp = $$; - - # For NFS, we need something more unique than the process's PID - $stamp .= hostname if $self->nfs; - - # Compute locking file name -- hardwired default format is "%f.lock" - my $lockfile = $file . $self->ext; - $format = $self->format unless defined $format; - $lockfile = $self->lockfile($file, $format) if defined $format; - - # Break lock if held for too long - $self->_acs_check($file, $lockfile) if $self->hold; - - my $waited = 0; # Amount of time spent sleeping - my $lastwarn = 0; # Last time we warned them... - my $warn = $self->warn; - my ($wmin, $wafter, $wfunc); - ($wmin, $wafter, $wfunc) = - ($self->wmin, $self->wafter, $self->wfunc) if $warn; - my $locked = 0; - my $mask = umask(0333); # No write permission - local *FILE; - - while ($max-- > 0) { - if (-f $lockfile) { - next unless $try; - umask($mask); - return 0; # Already locked - } - - # Attempt to create lock - if (open(FILE, ">$lockfile")) { - print FILE "$stamp\n"; - close FILE; - open(FILE, $lockfile); # Check lock - my $l; - chop($l = ); - $locked = $l eq $stamp; - $l = ; # Must be EOF - $locked = 0 if defined $l; - close FILE; - last if $locked; # Lock seems to be ours - } elsif ($try) { - umask($mask); - return 0; # Already locked, or cannot create lock - } - } continue { - sleep($delay); # Busy: wait - $waited += $delay; - - # Warn them once after $wmin seconds and then every $wafter seconds - if ( - $warn && - ((!$lastwarn && $waited > $wmin) || - ($waited - $lastwarn) > $wafter) - ) { - my $waiting = $lastwarn ? 'still waiting' : 'waiting'; - my $after = $lastwarn ? 'after' : 'since'; - my $s = $waited == 1 ? '' : 's'; - &$wfunc("WARNING $waiting for $file lock $after $waited second$s"); - $lastwarn = $waited; - } - } - - umask($mask); - return $locked; -} - -# -# ->_acs_unlock -- private -# -# Unlock file. If lock format is specified, it must match the one used -# at lock time. -# -# Return true if file was indeed locked by us and is now properly unlocked. -# -sub _acs_unlock { ## private - my $self = shift; - my ($file, $format) = @_; # Locked file, locking format - my $stamp = $$; - $stamp .= hostname if $self->nfs; - - # Compute locking file name -- hardwired default format is "%f.lock" - my $lockfile = $file . $self->ext; - $format = $self->format unless defined $format; - $lockfile = $self->lockfile($file, $format) if defined $format; - - local *FILE; - my $unlocked = 0; - - if (-f $lockfile) { - open(FILE, $lockfile); - my $l; - chop($l = ); - close FILE; - if ($l eq $stamp) { # Pid (plus hostname possibly) is OK - $unlocked = 1; - unlink $lockfile or $unlocked = 0; - } - } - - # It's reasonable to expect $! to be meaningful at this point - &{$self->wfunc}("WARNING did not unlock $file: $!") - if !$unlocked && $self->warn; - - return $unlocked; # Did we successfully unlock? -} - -# -# ->_acs_check -# -# Make sure lock lasts only for a reasonable time. If it has expired, -# then remove the lockfile. -# -sub _acs_check { - my $self = shift; - my ($file, $lockfile) = @_; - return unless -f $lockfile; - - my $mtime = (stat($lockfile))[9]; - my $hold = $self->hold; - - # If file too old to be considered stale? - if ((time - $mtime) > $hold) { - unlink $lockfile; - if ($self->warn) { - $file =~ s|.*/(.*)|$1|; # Keep only basename - my $s = $hold == 1 ? '' : 's'; - &{$self->wfunc}("UNLOCKED $file (lock older than $hold second$s)"); - } - } -} - -1; - -######################################################################## - -=head1 NAME - -vsLock - simple file locking scheme - -=head1 SYNOPSIS - - use vsLock qw(lock trylock unlock); - - # Simple locking using default settings - lock("/some/file") || die "can't lock /some/file\n"; - warn "already locked\n" unless trylock("/some/file"); - unlock("/some/file"); - - # Build customized locking manager object - $lockmgr = new vsLock(-format => '%f.lck', - -max => 20, -delay => 1, -nfs => 1); - - $lockmgr->lock("/some/file") || die "can't lock /some/file\n"; - $lockmgr->trylock("/some/file"); - $lockmgr->unlock("/some/file"); - - $lockmgr->configure(-nfs => 0); - -=head1 DESCRIPTION - -This simple locking scheme is not based on any file locking system calls -such as C or C but rather relies on basic file system -primitives and properties, such as the atomicity of the C system -call. It is not meant to be exempt from all race conditions, especially over -NFS. The algorithm used is described below in the B section. - -It is possible to customize the locking operations to attempt locking -once every 5 seconds for 30 times, or delete stale locks (files that are -deemed too ancient) before attempting the locking. - -=head1 ALGORITHM - -The locking alogrithm attempts to create a I using a temporarily -redefined I (leaving only read rights to prevent further create -operations). It then writes the process ID (PID) of the process and closes -the file. That file is then re-opened and read. If we are able to read the -same PID we wrote, and only that, we assume the locking is successful. - -When locking over NFS, i.e. when the one of the potentially locking processes -could access the I via NFS, then writing the PID is not enough. -We also write the hostname where locking is attempted to ensure the data -are unique. - -=head1 CUSTOMIZING - -Customization is only possible by using the object-oriented interface, -since the configuration parameters are stored within the object. The -object creation routine C can be given configuration parmeters in -the form a "hash table list", i.e. a list of key/value pairs. Those -parameters can later be changed via C by specifying a similar -list of key/value pairs. - -To benefit from the bareword quoting Perl offers, all the parameters must -be prefixed with the C<-> (minus) sign, as in C<-format> for the I -parameter.. However, when querying the object, the minus must be omitted, -as in C<$obj-Eformat>. - -Here are the available configuration parmeters along with their meaning, -listed in alphabetical order: - -=over 4 - -=item I - -The amount of seconds to wait between locking attempts when the file appears -to be already locked. Default is 2 seconds. - -=item I - -The locking extension that must be added to the file path to be locked to -compute the I path. Default is C<.lock> (note that C<.> is part -of the extension and can therefore be changed). Ignored when I is -also used. - -=item I - -Using this parmeter supersedes the I parmeter. The formatting string -specified is run through a rudimentary macro expansion to derive the -I path from the file to be locked. The following macros are -available: - - %% A real % sign - %f The full file path name - %D The directory where the file resides - %F The base name of the file - %p The process ID (PID) - -The default is to use the locking extension, which itself is C<.lock>, so -it is as if the format used was C<%f.lock>, but one could imagine things -like C, i.e. the I does not necessarily lie besides -the locked file (which could even be missing). - -When locking, the locking format can be specified to supersede the object -configuration itself. Be sure to use the same locking format when unlocking! -For instance, you can say: - - $obj->lock('ppp', '/var/run/ppp.%p'); - $obj->configure(-format => '/var/run/ppp.%p'); - $obj->unlock('ppp'); # Okay, since format changed - -This also works when the calling C without an object, and this is -where it is most useful since in that case you have no object to configure! -The example above becomes: - - lock('ppp', '/var/run/ppp.%p'); # file ppp may not even exist! - - unlock('ppp', '/var/run/ppp.%p'); # MUST specify here - -=item I - -Maximum amount of seconds we may hold a lock. Past that amount of time, -an existing I is removed, being taken for a stale lock. Default -is 3600 seconds. Specifying 0 prevents any forced unlocking. - -=item I - -Amount of times we retry locking when the file is busy, sleeping I -seconds between attempts. Defaults to 30. - -=item I - -A boolean flag, false by default. Setting it to true means we could lock -over NFS and therefore the hostname must be included along with the process -ID in the stamp written to the lockfile. - -=item I - -Stands for I. It is the number of seconds past the first -warning during locking time after which a new warning should be emitted. -See I and I below. Default is 20. - -=item I - -A boolean flag, true by default. To suppress any warning, set it to false. - -=item I - -A function pointer to dereference when a warning is to be issued. By default, -it points to Perl's C function. - -=item I - -The minimal amount of time when waiting for a lock after which a first -warning must be emitted, if I is true. After that, a warning will -be emitted every I seconds. Defaults to 15. - -=back - -Each of those configuration attributes can be queried on the object directly: - - $obj = vsLock->make(-nfs => 1); - $on_nfs = $obj->nfs; - -Those are pure query routines, i.e. you cannot say: - - $obj->nfs(0); # WRONG - $obj->configure(-nfs => 0); # Right - -to turn of the NFS attribute. That is because my OO background chokes -at having querying functions with side effects. - -=head1 INTERFACE - -The OO interface documented below specifies the signature and the -semantics of the operations. Only the C, C and -C operation can be imported and used via a non-OO interface, -with the exact same signature nonetheless. - -The interface contains all the attribute querying routines, one for -each configuration parmeter documented in the B section -above, plus, in alphabetical order: - -=over 4 - -=item configure(I<-key =E value, -key2 =E value2, ...>) - -Change the specified configuration parameters and silently ignore -the invalid ones. - -=item lock(I, I) - -Attempt to lock the file, using the optional locking I if -specified, otherwise using the default I scheme configured -in the object, or by simply appending the I extension to the file. - -If the file is already locked, sleep I seconds before retrying, -repeating try/sleep at most I times. If warning is configured, -a first warning is emitted after waiting for I seconds, and -then once every I seconds, via the I routine. - -Before the first attempt, and if I is non-zero, any existing -I is checked for being too old, and it is removed if found -to be stale. A warning is emitted via the I routine in that -case, if allowed. - -Returns true if the file has been successfully locked. - -=item lockfile(I, I) - -Simply compute the path of the I that would be used by the -I procedure if it were passed the same parameters. - -=item make(I<-key =E value, -key2 =E value2, ...>) - -The creation routine for the simple lock object. Returns a blessed hash -reference. - -=item trylock(I, I) - -Same as I except that it immediately returns false and does not -sleep if the to-be-locked file is busy, i.e. already locked. Any -stale locking file is removed, as I would do anyway. - -Returns true if the file has been successfully locked. - -=item unlock(I, I) - -Unlock the I. If the optional I parameter is given, it -must be the same as the one that was used at I time. - -=back - -=head1 BUGS - -The algorithm is not bullet proof. It's only reasonably safe. Don't bet -the integrity of a mission-critical database on it though. - -The sysopen() call should probably be used with the C flags -to be on the safer side. Still, over NFS, this is not an atomic operation -anyway. - -=head1 AUTHOR - -Raphael Manfredi FRaphael_Manfredi@grenoble.hp.comE> - -=head1 SEE ALSO - -File::Flock(3). - -=cut - +;# $Id +;# vsLock v 0.103 +;# Modified by Jason M. Hinkle, 2001 +;# +;# Based on File::Lock +;# Copyright (c) 1998, Raphael Manfredi +;# +;# You may redistribute only under the terms of the Artistic License, +;# as specified in the README file that comes with the distribution. +;# +;# $Log: vsLock.pm,v $ +;# Revision 0.1.1.1 1998/05/12 07:42:19 ram +;# patch1: Baseline for first alpha release. +;# + +######################################################################## +package vsLock; @ISA = qw(Exporter); + +# +# This package extracts the simple locking logic used by mailagent-3.0 +# into a standalone Perl module to be reused in other applications. +# + +use strict; +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); +use Sys::Hostname; + +require Exporter; + +@ISA = qw(Exporter); +@EXPORT = (); +@EXPORT_OK = qw(lock trylock unlock); +$VERSION = '0.103'; + +$vsLock::LOCKER = undef; # Default locking object + +# +# ->make +# +# Create a file locking object, responsible for holding the locking +# parameters to be used by all the subsequent locks requested from +# this locking object. +# +# Configuration attributes: +# +# max max number of attempts +# delay seconds to wait between attempts +# format how to derive lockfile from file to be locked +# hold max amount of seconds before breaking lock (0 for never) +# ext lock extension +# nfs true if lock must "work" on top of NFS +# warn flag to turn warnings on +# wmin warn once after that many waiting seconds +# wafter warn every that many seconds after first warning +# wfunc warning function to be called +# +# The creation routine first and sole argument is a "hash table list" listing +# all the configuration attributes. Missing attributes are given a default +# value. A call to ->configure can alter the configuration parameters of +# an existing object. +# +sub new { + my $self = bless {}, shift; + my (@hlist) = @_; + $self->configure(@hlist); + + # Set configuration defaults + $self->{'max'} = 30 unless $self->{'max'}; + $self->{'delay'} = 2 unless $self->{'delay'}; + $self->{'hold'} = 3600 unless $self->{'hold'}; + $self->{'ext'} = '.lock' unless defined $self->{'ext'}; + $self->{'nfs'} = 0 unless defined $self->{'nfs'}; + $self->{'warn'} = 1 unless defined $self->{'warn'}; + $self->{'wfunc'} = \&core_warn unless defined $self->{'wfunc'}; + $self->{'wmin'} = 15 unless $self->{'wmin'}; + $self->{'wafter'} = 20 unless $self->{'wafter'}; + + return $self; +} + +# +# ->configure +# +# Extract known configuration parameters from the specified hash list +# and use their values to change the object's corresponding parameters. +# +# Parameters are specified as (-warn => 1, -ext => '.lock') for instance. +# +sub configure { + my $self = shift; + my (%hlist) = @_; + my @known = qw(max delay hold format ext nfs warn wfunc wmin wafter); + foreach my $attr (@known) { + $self->{$attr} = $hlist{"-$attr"} if defined $hlist{"-$attr"}; + } +} + +# +# Attribute access +# + +sub max { $_[0]->{'max'} } +sub delay { $_[0]->{'delay'} } +sub format { $_[0]->{'format'} } +sub hold { $_[0]->{'hold'} } +sub nfs { $_[0]->{'nfs'} } +sub ext { $_[0]->{'ext'} } +sub warn { $_[0]->{'warn'} } +sub wmin { $_[0]->{'wmin'} } +sub wafter { $_[0]->{'wafter'} } +sub wfunc { $_[0]->{'wfunc'} } +sub Version { return $VERSION; } + +sub core_warn { CORE::warn(@_) } + +# +# ->lock +# +# Lock specified file, possibly using alternate file "format". +# Returns whether file was locked or not at the end of the configured +# blocking period. +# +# For quick and dirty scripts wishing to use locks, create the locking +# object if not invoked as a method, turning on warnings. +# +sub lock { + my $self = shift; + unless (ref $self) { # Not invoked as a method + unshift(@_, $self); + $self = $vsLock::LOCKER || + vsLock->make('-warn' => 1); + } + my ($file, $format) = @_; # File to be locked, lock format + return $self->_acs_lock($file, $format, 0); +} + +# +# ->trylock +# +# Attempt to lock specified file, possibly using alternate file "format". +# If the file is already locked, don't block and return false. +# +# For quick and dirty scripts wishing to use locks, create the locking +# object if not invoked as a method, turning on warnings. +# +sub trylock { + my $self = shift; + unless (ref $self) { # Not invoked as a method + unshift(@_, $self); + $self = $vsLock::LOCKER || + vsLock->make('-warn' => 1); + } + my ($file, $format) = @_; # File to be locked, lock format + return $self->_acs_lock($file, $format, 1); +} + +# +# ->unlock +# +# Unlock file. +# Returns true if file was unlocked. +# +sub unlock { + my $self = shift; + unless (ref $self) { # Not invoked as a method + unshift(@_, $self); + $self = $vsLock::LOCKER || + vsLock->make('-warn' => 1); + } + my ($file, $format) = @_; # File to be unlocked, lock format + return $self->_acs_unlock($file, $format); +} + +# +# ->lockfile +# +# Return the name of the lockfile, given the file name to lock and the custom +# string provided by the user. The following macros are substituted: +# %D: the file dir name +# %f: the file name (full path) +# %F: the file base name (last path component) +# %p: the process's pid +# %%: a plain % character +# +sub lockfile { + my $self = shift; + my ($file, $format) = @_; + local $_ = defined($format) ? $format : $self->format; + s/%%/\01/g; # Protect double percent signs + s/%/\02/g; # Protect against substitutions adding their own % + s/\02f/$file/g; # %f is the full path name + s/\02D/&dir($file)/ge; # %D is the dir name + s/\02F/&base($file)/ge; # %F is the base name + s/\02p/$$/g; # %p is the process's pid + s/\02/%/g; # All other % kept as-is + s/\01/%/g; # Restore escaped % signs + $_; +} + +# Return file basename (last path component) +sub base { + my ($file) = @_; + my ($base) = $file =~ m|^.*/(.*)|; + $base; +} + +# Return dirname +sub dir { + my ($file) = @_; + my ($dir) = $file =~ m|^(.*)/.*|; + $dir; +} + +# +# _acs_lock -- private +# +# Internal locking routine. +# +# If $try is true, don't wait if the file is already locked. +# Returns true if the file was locked. +# +sub _acs_lock { ## private + my $self = shift; + my ($file, $format, $try) = @_; + my $max = $self->max; + my $delay = $self->delay; + my $stamp = $$; + + # For NFS, we need something more unique than the process's PID + $stamp .= hostname if $self->nfs; + + # Compute locking file name -- hardwired default format is "%f.lock" + my $lockfile = $file . $self->ext; + $format = $self->format unless defined $format; + $lockfile = $self->lockfile($file, $format) if defined $format; + + # Break lock if held for too long + $self->_acs_check($file, $lockfile) if $self->hold; + + my $waited = 0; # Amount of time spent sleeping + my $lastwarn = 0; # Last time we warned them... + my $warn = $self->warn; + my ($wmin, $wafter, $wfunc); + ($wmin, $wafter, $wfunc) = + ($self->wmin, $self->wafter, $self->wfunc) if $warn; + my $locked = 0; + my $mask = umask(0333); # No write permission + local *FILE; + + while ($max-- > 0) { + if (-f $lockfile) { + next unless $try; + umask($mask); + return 0; # Already locked + } + + # Attempt to create lock + if (open(FILE, ">$lockfile")) { + print FILE "$stamp\n"; + close FILE; + open(FILE, $lockfile); # Check lock + my $l; + chop($l = ); + $locked = $l eq $stamp; + $l = ; # Must be EOF + $locked = 0 if defined $l; + close FILE; + last if $locked; # Lock seems to be ours + } elsif ($try) { + umask($mask); + return 0; # Already locked, or cannot create lock + } + } continue { + sleep($delay); # Busy: wait + $waited += $delay; + + # Warn them once after $wmin seconds and then every $wafter seconds + if ( + $warn && + ((!$lastwarn && $waited > $wmin) || + ($waited - $lastwarn) > $wafter) + ) { + my $waiting = $lastwarn ? 'still waiting' : 'waiting'; + my $after = $lastwarn ? 'after' : 'since'; + my $s = $waited == 1 ? '' : 's'; + &$wfunc("WARNING $waiting for $file lock $after $waited second$s"); + $lastwarn = $waited; + } + } + + umask($mask); + return $locked; +} + +# +# ->_acs_unlock -- private +# +# Unlock file. If lock format is specified, it must match the one used +# at lock time. +# +# Return true if file was indeed locked by us and is now properly unlocked. +# +sub _acs_unlock { ## private + my $self = shift; + my ($file, $format) = @_; # Locked file, locking format + my $stamp = $$; + $stamp .= hostname if $self->nfs; + + # Compute locking file name -- hardwired default format is "%f.lock" + my $lockfile = $file . $self->ext; + $format = $self->format unless defined $format; + $lockfile = $self->lockfile($file, $format) if defined $format; + + local *FILE; + my $unlocked = 0; + + if (-f $lockfile) { + open(FILE, $lockfile); + my $l; + chop($l = ); + close FILE; + if ($l eq $stamp) { # Pid (plus hostname possibly) is OK + $unlocked = 1; + unlink $lockfile or $unlocked = 0; + } + } + + # It's reasonable to expect $! to be meaningful at this point + &{$self->wfunc}("WARNING did not unlock $file: $!") + if !$unlocked && $self->warn; + + return $unlocked; # Did we successfully unlock? +} + +# +# ->_acs_check +# +# Make sure lock lasts only for a reasonable time. If it has expired, +# then remove the lockfile. +# +sub _acs_check { + my $self = shift; + my ($file, $lockfile) = @_; + return unless -f $lockfile; + + my $mtime = (stat($lockfile))[9]; + my $hold = $self->hold; + + # If file too old to be considered stale? + if ((time - $mtime) > $hold) { + unlink $lockfile; + if ($self->warn) { + $file =~ s|.*/(.*)|$1|; # Keep only basename + my $s = $hold == 1 ? '' : 's'; + &{$self->wfunc}("UNLOCKED $file (lock older than $hold second$s)"); + } + } +} + +1; + +######################################################################## + +=head1 NAME + +vsLock - simple file locking scheme + +=head1 SYNOPSIS + + use vsLock qw(lock trylock unlock); + + # Simple locking using default settings + lock("/some/file") || die "can't lock /some/file\n"; + warn "already locked\n" unless trylock("/some/file"); + unlock("/some/file"); + + # Build customized locking manager object + $lockmgr = new vsLock(-format => '%f.lck', + -max => 20, -delay => 1, -nfs => 1); + + $lockmgr->lock("/some/file") || die "can't lock /some/file\n"; + $lockmgr->trylock("/some/file"); + $lockmgr->unlock("/some/file"); + + $lockmgr->configure(-nfs => 0); + +=head1 DESCRIPTION + +This simple locking scheme is not based on any file locking system calls +such as C or C but rather relies on basic file system +primitives and properties, such as the atomicity of the C system +call. It is not meant to be exempt from all race conditions, especially over +NFS. The algorithm used is described below in the B section. + +It is possible to customize the locking operations to attempt locking +once every 5 seconds for 30 times, or delete stale locks (files that are +deemed too ancient) before attempting the locking. + +=head1 ALGORITHM + +The locking alogrithm attempts to create a I using a temporarily +redefined I (leaving only read rights to prevent further create +operations). It then writes the process ID (PID) of the process and closes +the file. That file is then re-opened and read. If we are able to read the +same PID we wrote, and only that, we assume the locking is successful. + +When locking over NFS, i.e. when the one of the potentially locking processes +could access the I via NFS, then writing the PID is not enough. +We also write the hostname where locking is attempted to ensure the data +are unique. + +=head1 CUSTOMIZING + +Customization is only possible by using the object-oriented interface, +since the configuration parameters are stored within the object. The +object creation routine C can be given configuration parmeters in +the form a "hash table list", i.e. a list of key/value pairs. Those +parameters can later be changed via C by specifying a similar +list of key/value pairs. + +To benefit from the bareword quoting Perl offers, all the parameters must +be prefixed with the C<-> (minus) sign, as in C<-format> for the I +parameter.. However, when querying the object, the minus must be omitted, +as in C<$obj-Eformat>. + +Here are the available configuration parmeters along with their meaning, +listed in alphabetical order: + +=over 4 + +=item I + +The amount of seconds to wait between locking attempts when the file appears +to be already locked. Default is 2 seconds. + +=item I + +The locking extension that must be added to the file path to be locked to +compute the I path. Default is C<.lock> (note that C<.> is part +of the extension and can therefore be changed). Ignored when I is +also used. + +=item I + +Using this parmeter supersedes the I parmeter. The formatting string +specified is run through a rudimentary macro expansion to derive the +I path from the file to be locked. The following macros are +available: + + %% A real % sign + %f The full file path name + %D The directory where the file resides + %F The base name of the file + %p The process ID (PID) + +The default is to use the locking extension, which itself is C<.lock>, so +it is as if the format used was C<%f.lock>, but one could imagine things +like C, i.e. the I does not necessarily lie besides +the locked file (which could even be missing). + +When locking, the locking format can be specified to supersede the object +configuration itself. Be sure to use the same locking format when unlocking! +For instance, you can say: + + $obj->lock('ppp', '/var/run/ppp.%p'); + $obj->configure(-format => '/var/run/ppp.%p'); + $obj->unlock('ppp'); # Okay, since format changed + +This also works when the calling C without an object, and this is +where it is most useful since in that case you have no object to configure! +The example above becomes: + + lock('ppp', '/var/run/ppp.%p'); # file ppp may not even exist! + + unlock('ppp', '/var/run/ppp.%p'); # MUST specify here + +=item I + +Maximum amount of seconds we may hold a lock. Past that amount of time, +an existing I is removed, being taken for a stale lock. Default +is 3600 seconds. Specifying 0 prevents any forced unlocking. + +=item I + +Amount of times we retry locking when the file is busy, sleeping I +seconds between attempts. Defaults to 30. + +=item I + +A boolean flag, false by default. Setting it to true means we could lock +over NFS and therefore the hostname must be included along with the process +ID in the stamp written to the lockfile. + +=item I + +Stands for I. It is the number of seconds past the first +warning during locking time after which a new warning should be emitted. +See I and I below. Default is 20. + +=item I + +A boolean flag, true by default. To suppress any warning, set it to false. + +=item I + +A function pointer to dereference when a warning is to be issued. By default, +it points to Perl's C function. + +=item I + +The minimal amount of time when waiting for a lock after which a first +warning must be emitted, if I is true. After that, a warning will +be emitted every I seconds. Defaults to 15. + +=back + +Each of those configuration attributes can be queried on the object directly: + + $obj = vsLock->make(-nfs => 1); + $on_nfs = $obj->nfs; + +Those are pure query routines, i.e. you cannot say: + + $obj->nfs(0); # WRONG + $obj->configure(-nfs => 0); # Right + +to turn of the NFS attribute. That is because my OO background chokes +at having querying functions with side effects. + +=head1 INTERFACE + +The OO interface documented below specifies the signature and the +semantics of the operations. Only the C, C and +C operation can be imported and used via a non-OO interface, +with the exact same signature nonetheless. + +The interface contains all the attribute querying routines, one for +each configuration parmeter documented in the B section +above, plus, in alphabetical order: + +=over 4 + +=item configure(I<-key =E value, -key2 =E value2, ...>) + +Change the specified configuration parameters and silently ignore +the invalid ones. + +=item lock(I, I) + +Attempt to lock the file, using the optional locking I if +specified, otherwise using the default I scheme configured +in the object, or by simply appending the I extension to the file. + +If the file is already locked, sleep I seconds before retrying, +repeating try/sleep at most I times. If warning is configured, +a first warning is emitted after waiting for I seconds, and +then once every I seconds, via the I routine. + +Before the first attempt, and if I is non-zero, any existing +I is checked for being too old, and it is removed if found +to be stale. A warning is emitted via the I routine in that +case, if allowed. + +Returns true if the file has been successfully locked. + +=item lockfile(I, I) + +Simply compute the path of the I that would be used by the +I procedure if it were passed the same parameters. + +=item make(I<-key =E value, -key2 =E value2, ...>) + +The creation routine for the simple lock object. Returns a blessed hash +reference. + +=item trylock(I, I) + +Same as I except that it immediately returns false and does not +sleep if the to-be-locked file is busy, i.e. already locked. Any +stale locking file is removed, as I would do anyway. + +Returns true if the file has been successfully locked. + +=item unlock(I, I) + +Unlock the I. If the optional I parameter is given, it +must be the same as the one that was used at I time. + +=back + +=head1 BUGS + +The algorithm is not bullet proof. It's only reasonably safe. Don't bet +the integrity of a mission-critical database on it though. + +The sysopen() call should probably be used with the C flags +to be on the safer side. Still, over NFS, this is not an atomic operation +anyway. + +=head1 AUTHOR + +Raphael Manfredi FRaphael_Manfredi@grenoble.hp.comE> + +=head1 SEE ALSO + +File::Flock(3). + +=cut + + + diff --git a/web/bin/button_hp.pl b/web/bin/button_hp.pl new file mode 100644 index 000000000..e095982a2 --- /dev/null +++ b/web/bin/button_hp.pl @@ -0,0 +1,209 @@ + +$^W = 0; # Avoid redefined sub msgs + +# Create jpeg buttons on-the-fly with GD module +# For text buttons: +# For item buttons: + +# Authority: anyone + +my ($text, $state, $bg_color, $file_name_only) = @ARGV; + +my $type = 'item'; + +print "db v6 t=$text t=$type s=$state bg=$bg_color\n"; + +#my ($state, $x, $y) = $state_xy =~ /(\S+)\?(\d+),(\d+)/; + +$state = '' unless $state; +$type = '' unless $type; +$bg_color = 'white' unless $bg_color; + +unless ($Info{module_GD}) { + return; +} + +my $image_file = $text; +$image_file =~ s/^\$//; # Drop leading $ on object name +$image_file =~ s/ *$//; # Drop trailing blanks +$image_file .= "_$type" if $type; +$image_file .= "_$state" if $state; +$image_file =~ s/ /_/g; # Blanks in file names are nasty +$image_file = "/cache/$image_file.jpg"; + + +my $nocache = 0; +#$nocache = 1; +##if (-e "$config_parms{data_dir}$image_file" or $nocache) { +## return $image_file if $file_name_only; +## print "Returning data from: $image_file\n"; +## my $data = file_read "$config_parms{data_dir}$image_file"; +## return &mime_header($image_file, 1, length $data) . $data; +##} + + # Look for an icon +my ($icon, $light); + + my $object = &get_object_by_name($text); +# ($icon) = &http_get_local_file(&html_find_icon_image($object, 'voice')); +# $light = 1 if $text =~ /light/i or $text =~ /lite/i; +# $light = 1 if $object->isa('X10_Item') and !$object->isa('X10_Appliance'); + +if ($state eq 'door') { + $icon = './../web/graphics/icon_door.jpg'; + } +elsif ($state eq 'motion') { + $icon = './../web/graphics/icon_motion.jpg'; + } +elsif ($state eq 'brightness') { + $icon = './../web/graphics/icon_motion.jpg'; + } + + +my $tmp1 = &html_find_icon_image($object,'voice'); +my $s2 = $object->state; +print "db icon=$icon state_s2=$s2 tmp1=$tmp1\n"; + +undef $icon if $icon and $icon !~ /.jpg$/i; # GD does not do gifs :( +my $image_icon = GD::Image->newFromJpeg($icon) if $icon; + +my $image; +if ($image_icon or $type eq 'item') { + + # Template = blank_on/off/unk or blank_light_on/off/dim + my $template = 'blank'; + $template .= '_light' if $light; + if ($state eq 'on' or $state eq 'off') { + $template .= "_$state"; + } + else { + $template .= ($light) ? '_dim' : ''; + } + $template = 'blank_textbutton' unless $type; + $template .= "_$bg_color" if $bg_color and $bg_color ne 'white'; + + # GD in 5.8 gives gray for white jpg?? Allow for png, which is still gray :( + my $file = "$config_parms{html_dir}/graphics/$template.png"; + if (-f $file) { + $image = GD::Image->newFromPng($file); + } + else { + $file = "$config_parms{html_dir}/graphics/$template.jpg"; + $image = GD::Image->newFromJpeg($file); + } + + if ($image_icon and $template !~ /_light/) { + my ($iw, $ih) = $image_icon->getBounds(); + $image->copyResized($image_icon, 7, 6, 0, 0, 40, 40, $iw, $ih); + } + + my $color; + + # Filter out starting keyword Sensor + my $filter = &pretty_object_name($text); + $filter =~ s/^sensor//i; + + my @lines = split ' ', $filter, 3; + + # Combine lines if they are short enough + if (length $lines[0] . $lines[1] < 12) { + $lines[0] .= " $lines[1]"; + $lines[1] = $lines[2]; + $lines[2] = ''; + } + if (length $lines[1] . $lines[2] < 12) { + $lines[1] .= " $lines[2]"; + $lines[2] = ''; + } + + # gdGiantFont, gdLargeFont, gdMediumBoldFont, gdSmallFont and gdTinyFont + my ($font, $x); + if ($type) { + $font = gdMediumBoldFont; + $x = 55; + } + else { + $font = gdMediumBoldFont; +# $font = gdTinyFont; +# $font = gdSmallFont; + $x = 35; + } + $image->string($font, $x , 6, ucfirst $lines[0], $color); + $image->string($font, $x , 18, ucfirst $lines[1], $color) if $lines[1]; + $image->string($font, $x , 30, ucfirst $lines[2], $color) if $lines[2]; + +my $sname; + if ($state eq 'door') { + $x += 45; + if ($s2 eq 'on') { + $sname = "Open"; + $color = $image->colorClosest(255,0,0); + } + else { + $sname = "Closed"; + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x , 30, ucfirst $sname, $color); +} + elsif ($state eq 'motion') { + $x += 45; + if ($s2 eq 'motion') { + $color = $image->colorClosest(255,0,0); + } + else { + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x , 30, ucfirst $s2, $color); +} + elsif ($state eq 'brightness') { + $x += 45; + if ($s2 eq 'light') { + $color = $image->colorClosest(255,0,0); + } + else { + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x , 30, ucfirst $s2, $color); +} + + + +} +else { + my $template = 'blank_textbutton'; + $template .= "_$bg_color" if $bg_color and $bg_color ne 'white'; + + # GD in 5.8 gives gray for white jpg?? Allow for png + my $file = "$config_parms{html_dir}/graphics/$template.png"; + if (-f $file) { + $image = GD::Image->newFromPng($file); + } + else { + $file = "$config_parms{html_dir}/graphics/$template.jpg"; + $image = GD::Image->newFromJpeg($file); + } + + # calculate size of image and text for offset + + my $textsize = length $text; + my $font = gdMediumBoldFont; # Choices: gdTinyFont, gdLargeFont, gdSmallFont, gdMediumBoldFont + $font = gdTinyFont if $textsize > 14; + + my ($imwidth,$imheight) = $image->getBounds(); + my $ftwidth = ($font->width); + my $offset = $imwidth - ($ftwidth * $textsize); + my $black = $image->colorClosest(50,50,50); + $image->string($font, $offset/2-3, 12, $text, $black); +} + + # make the background transparent +my $white = $image->colorClosest(255,255,255); +$image->transparent($white); + + # Write out a copy to the cache +print "Writing image to cache: $config_parms{data_dir}$image_file\n"; +my $jpeg = $image->jpeg; +file_write "$config_parms{data_dir}$image_file", $jpeg; + +return $image_file if $file_name_only; +return &mime_header($image_file, 1, length $jpeg) . $jpeg; diff --git a/web/bin/button_sensor.pl b/web/bin/button_sensor.pl new file mode 100644 index 000000000..35abcf6b1 --- /dev/null +++ b/web/bin/button_sensor.pl @@ -0,0 +1,174 @@ + +$^W = 0; # Avoid redefined sub msgs + +# Create jpeg buttons on-the-fly with GD module +# For text buttons: +# For item buttons: + +# Authority: anyone + +my ($text, $type, $bg_color) = @ARGV; + +##print "db v6 t=$text s=$state bg=$bg_color\n"; + +#my ($state, $x, $y) = $state_xy =~ /(\S+)\?(\d+),(\d+)/; + +$bg_color = 'white' unless $bg_color; + +unless ($Info{module_GD}) { + return; +} + +my $image_file = $text; +$image_file =~ s/^\$//; # Drop leading $ on object name +$image_file =~ s/ *$//; # Drop trailing blanks +$image_file =~ s/ /_/g; # Blanks in file names are nasty +$image_file = "/cache/$image_file.jpg"; + + +my $nocache = 0; +#$nocache = 1; +##if (-e "$config_parms{data_dir}$image_file" or $nocache) { +## return $image_file if $file_name_only; +## print "Returning data from: $image_file\n"; +## my $data = file_read "$config_parms{data_dir}$image_file"; +## return &mime_header($image_file, 1, length $data) . $data; +##} + + # Look for an icon +my ($icon, $light); + + my $object = &get_object_by_name($text); +# ($icon) = &http_get_local_file(&html_find_icon_image($object, 'voice')); +# $light = 1 if $text =~ /light/i or $text =~ /lite/i; +# $light = 1 if $object->isa('X10_Item') and !$object->isa('X10_Appliance'); + +if ($type eq 'door') { + $icon = './../web/graphics/icon_door.jpg'; + } +elsif ($type eq 'motion') { + $icon = './../web/graphics/icon_motion.jpg'; + } +elsif ($type eq 'brightness') { + $icon = './../web/graphics/icon_motion.jpg'; + } +elsif ($type eq 'water') { + $icon = './../web/graphics/icon_water.jpg'; + } + + +#my $tmp1 = &html_find_icon_image($object,'voice'); +my $state = $object->state; + +undef $icon if $icon and $icon !~ /.jpg$/i; # GD does not do gifs :( +my $image_icon = GD::Image->newFromJpeg($icon) if $icon; + +my $image; + + # Template = blank_on/off/unk or blank_light_on/off/dim + my $template = 'blank_sens'; + $template .= "_$bg_color" if $bg_color and $bg_color ne 'white'; + + # GD in 5.8 gives gray for white jpg?? Allow for png, which is still gray :( + my $file = "$config_parms{html_dir}/graphics/$template.png"; + if (-f $file) { + $image = GD::Image->newFromPng($file); + } + else { + $file = "$config_parms{html_dir}/graphics/$template.jpg"; + $image = GD::Image->newFromJpeg($file); + } + + if ($image_icon and $template !~ /_light/) { + my ($iw, $ih) = $image_icon->getBounds(); + $image->copyResized($image_icon, 7, 6, 0, 0, 40, 40, $iw, $ih); + } + + my $color; + + # Filter out starting keyword Sensor + my $filter = &pretty_object_name($text); + $filter =~ s/^sensor//i; + + my @lines = split ' ', $filter, 3; + + # Combine lines if they are short enough + if (length $lines[0] . $lines[1] < 12) { + $lines[0] .= " $lines[1]"; + $lines[1] = $lines[2]; + $lines[2] = ''; + } + if (length $lines[1] . $lines[2] < 12) { + $lines[1] .= " $lines[2]"; + $lines[2] = ''; + } + + # gdGiantFont, gdLargeFont, gdMediumBoldFont, gdSmallFont and gdTinyFont + my ($font, $x); + $x = 50; + $font = gdMediumBoldFont; +# $font = gdTinyFont; +# $font = gdSmallFont; + + $image->string($font, $x , 6, ucfirst $lines[0], $color); + $image->string($font, $x , 18, ucfirst $lines[1], $color) if $lines[1]; + $image->string($font, $x , 30, ucfirst $lines[2], $color) if $lines[2]; + +my $sname; + if ($type eq 'door') { + $x += 45; + if (($state eq 'on') or ($state eq 'open')) { + $sname = "Open"; + $color = $image->colorClosest(255,0,0); + } + else { + $sname = "Closed"; + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x , 30, ucfirst $sname, $color); +} + elsif ($type eq 'motion') { + $x += 45; + if ($state eq 'motion') { + $color = $image->colorClosest(255,0,0); + } + else { + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x , 30, ucfirst $state, $color); +} + elsif ($type eq 'brightness') { + $x += 45; + if ($state eq 'light') { + $color = $image->colorClosest(255,0,0); + } + else { + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x , 30, ucfirst $state, $color); +} + + elsif ($type eq 'water') { + $x += 65; + if ($state eq 'on') { + $sname = "Wet"; + $color = $image->colorClosest(255,0,0); + } + else { + $sname = "Dry"; + $color = $image->colorClosest(0,0,0); + } + $image->string($font, $x, 30, ucfirst $sname, $color); +} + + + # make the background transparent +my $white = $image->colorClosest(255,255,255); +$image->transparent($white); + + # Write out a copy to the cache +print "Writing image to cache: $config_parms{data_dir}$image_file\n"; +my $jpeg = $image->jpeg; +file_write "$config_parms{data_dir}$image_file", $jpeg; + +return &mime_header($image_file, 1, length $jpeg) . $jpeg; diff --git a/web/bin/icon_panic.pl b/web/bin/icon_panic.pl new file mode 100644 index 000000000..848b2d7ba --- /dev/null +++ b/web/bin/icon_panic.pl @@ -0,0 +1,16 @@ + +# Return a panic mode button - red to start panic process, green to cancel + +# Authority: anyone + +my $icon = '/ia5/images/panicred.gif'; +my $action = ''; +#my $action = '/SET;referer/ia5/top.shtml?hpc_panic_mode=on'; +# # Return a user specific icon, or default logout icon if not found. +#if (active $hpc_timer_panic) { +# $icon = '/ia5/images/panicgreen.gif'; +# $action = '/SET;referer/ia5/top.shtml?hpc_panic_mode=off'; +#} + +#print "\ndbx a=$Authorized i=$icon a=$action\n"; +return "Panic Mode"; diff --git a/web/bin/phone_in.pl b/web/bin/phone_in.pl index 9b063c837..7b69367ac 100644 --- a/web/bin/phone_in.pl +++ b/web/bin/phone_in.pl @@ -2,6 +2,7 @@ # read_phone_logs* is from phone_logs.pl code files my $html_calls; my $display_name; +my $display_num; my @logs = &read_phone_logs1('callerid'); my @calls = &read_phone_logs2(100, @logs); for my $r (@calls) { @@ -10,11 +11,15 @@ $display_name = $name; $display_name =~ s/_/ /g; # remove underscores to make it print pretty next unless $num; + $display_num = $num; + if ($display_num =~ /\d\d\d\d\d\d\d\d\d\d/) { + $display_num = (substr $num,0,3) . "-" . (substr $num,3,3) . "-" . (substr $num,6,4); + } # next unless $line; $html_calls .= ""; # $html_calls .= "Show last call from $num Show last call from $num"; - $html_calls .= "$timeShow last call from $num $num"; + $html_calls .= "$timeShow last call from $num $display_num"; $html_calls .= "Add $num to phone.callerid.list file $display_name$line"; $html_calls .= ""; } diff --git a/web/bin/statuspanel.pl b/web/bin/statuspanel.pl new file mode 100644 index 000000000..5486d10d6 --- /dev/null +++ b/web/bin/statuspanel.pl @@ -0,0 +1,64 @@ + +# This web page example is from Brian Klier. It is called from statuspannel.shtml + +return " +{state}.gif\"> + +ALARM: $alarmactive->{state}
    + +
    +Time: $Time_Now    +Temperature: $CurrentTemp° +
    +Wind: $WXWindDirVoice at $WXWindSpeed MPH.
    +
    + +Last Tracked: $GPSSpeakString
    +Last Weather: $WXSpeakString
    +Last Incoming Call: $PhoneName ($PhoneNumber), at $PhoneTime on $PhoneDate
    +
    +{state}.gif\"> +Living Room: $living_room->{state}    +{state}.gif\"> +Front Entryway: $front_entryway->{state}
    +{state}.gif\"> +Bedroom: $bedroom_lamp->{state}    +{state}.gif\"> +Back Porch: $back_porch_light->{state}
    +{state}.gif\"> +Kitchen: $kitchen_light->{state}
    +
    + +{state}.gif\"> +Projector: $projector->{state}    +{state}.gif\"> +Air Conditioner: $air_cond_fan->{state}    +{state}.gif\"> +Floor Fan: $circ_fan->{state}
    +{state}.gif\"> +Boombox: $boombox_bedroom->{state}    +{state}.gif\"> +Bed Heater: $bed_heater->{state}    +{state}.gif\"> +Music: $request_music_stuff->{state}
    +
    +{state}.gif\"> +Entryway Motion: $motion_detector_frontdoor->{state}    +{state}.gif\"> +Back Door Motion: $motion_detector_backdoor->{state}
    +{state}.gif\"> +Kitchen Motion: $motion_detector_kitchen->{state}    +{state}.gif\"> +Kitchen Low Light: $low_light_kitchen->{state}
    +{state}.gif\"> +Living Room Motion: $motion_detector_living_room->{state}    +{state}.gif\"> +Living Room Low Light: $low_light_living_room->{state}
    +{state}.gif\"> +Garage Motion: $motion_detector_garage->{state}    +{state}.gif\"> +Garage Low Light: $low_light_garage->{state} +
    +"; + diff --git a/web/graphics/icons/pcomm2.png b/web/graphics/icons/pcomm2.png index de0b15935276a1b12dd0c68f3ea59a80b133f723..3eec4958262f80230b2422a72939ecf4abb5d264 100644 GIT binary patch delta 281 zcmV+!0p|X~1j7W77YZB%0{{R3^6a1yks%WSFp)JY4FCWD00000007%HTR4-D0jZH^ zM1RytL_t(2&xMd762dSLMHhhxRh398xBw1~!uJTAp`r(<4RRaP88C^$ZgV*{y9sR- zf2K2?H@pA;4MYkeUqul_r$4v=Oqy4~0>Ewqnx;8z!1*soDn{ fVh?~SLQ(tz`-|IGr(_F{14Z+=!#r5W^R@)xSMs>p zgP#jTDCs4E +MisterHouse NewClock index2 + + + + + + + + + + + + + +

    + + + + + +
    +
    +
    + + +
    + + + +
    + + + + + + + + + +
    +
    +

    +
    + + + + + + +
    + + + + + + + + + + +
    +

    + + + + +
    + + + +
    + + + +
    +
    +
    + + diff --git a/web/newclock/maps/0.jpg b/web/newclock/maps/0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..275b51f63e25ed5c1893c288b3afcaa24e4b85b9 GIT binary patch literal 26868 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 G=6?aE25iaz literal 0 HcmV?d00001 diff --git a/web/newclock/maps/1.jpg b/web/newclock/maps/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..87cf8334909c9f511340bafdc625f388b1c5f64a GIT binary patch literal 26715 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

    q-%0DCxS_`DIN*PgkB=q>rSbey~97C&4t5GZY? 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/12.jpg b/web/newclock/maps/12.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d882f97efefd942d908fe958ed7fedf8bba39715 GIT binary patch literal 24888 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/14.jpg b/web/newclock/maps/14.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ede6bf52f58700c9a2c6ad3a4f4fab4e584e8ebe GIT binary patch literal 25186 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/16.jpg b/web/newclock/maps/16.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c04543563a1af4ee20d8759e5890ed84c84dc339 GIT binary patch literal 25745 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 I=kL`20Ip9t-v9sr literal 0 HcmV?d00001 diff --git a/web/newclock/maps/17.jpg b/web/newclock/maps/17.jpg new file mode 100644 index 0000000000000000000000000000000000000000..42151f90e1aeea1cddc652997b59840d423a1602 GIT binary patch literal 25806 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/18.jpg b/web/newclock/maps/18.jpg new file mode 100644 index 0000000000000000000000000000000000000000..595bf48bdde96fa9233f05b3f5141d2db1add643 GIT binary patch literal 27276 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

    nQ4P6o!J#{^LdLU^8XVQ8n*{&)iD;`+<()rD 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/19.jpg b/web/newclock/maps/19.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebdd5fd63a70bee33a5ca4a28f87c3423c727d93 GIT binary patch literal 25866 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 U@BVwb{j9#2GJ`Uj|L^<%1B#_+ApigX literal 0 HcmV?d00001 diff --git a/web/newclock/maps/21.jpg b/web/newclock/maps/21.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5f54e3bf02f725f3517997aa62bd30384fd235b GIT binary patch literal 26140 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/22.jpg b/web/newclock/maps/22.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f379c276e5870ae4d59817ffa017d9730ce6d16c GIT binary patch literal 26614 zcmbSybx<6^!{x#f++7w61a}LrVX@$@!QI^n5Nxx!!{Y7|Y>@ zS9O2gT~AH_GhI{tx@%r{zxU?f^1m$rt|C|g3_w5t01#d;z`r$s3;-1w83p+bDhdh; z8X77(CLtCk1_mY>J^>CPB{?+}B{>L0L(joT^OlVc1Y&y6%*Msd$Hzy_C?qDxBg(2Ps=TZOrUOt^45)z zCp57Dl}@^$he%`QH$AVpdl(uzF$pOd`8x(iCT12seu4LbLc%g1WWjRs3W}Oq+B&*= z`UV!3R@OGQcJ>~gUfw>we*WQKA|j)rV<1V%DXD4c8JSsyMa3nhW#tu>jo+G@TUy)T z?Y({d1A{}uBcro(^9zeh%PXtfJG*=P2Zu+;Czn^(H@A29e;ywHg9`xw{BKyV^?w8V zKXBo_;zC410wSUO2NwdO&+84uLwZBYjf^j)j$-CU@RlbOl~6jdprHqij#uM1k-7T} zIx#)p_PfjfK>IIb|98N`{=bm@FJS*0*D3%Li10djKsENxO#nZv>b(%X z2TGiMQ%$`4%)M;$WS6(10fb)|Qg3YOG%6BFawp2rtML3yQFrA{8&!iV#G%yjqw$1a zeS><6uxj_y3Pd{k-xhzh+u|k_^F4?&ZAe-2p{xGlfBT|WzIm`c^msE>Q1cv{+--JS zULv39BdWejDx|KIi9IN6+_Emd8xQU-@=c(I)jix-ox3;(yD(XNU$=bG7G5z--4^cz z{R0RKCFl(-|Gldx+!Dfj2QAZ7s#0D1gNgK_ zkoM(=nw@qi*E<8cBu2_m-}Ycudb~`an^~Y1olnlim+&D|#(GN8#;O3+L~aK7y1v=q zGY`=tmraHZl2_$^&daY5jlua%@JBm4$40(s$FOPS>7jrV;Ny0gU zVS}Bke+}k$;dpBfi?MNTeE0)-{qeu6Q!ekL6Q!>3^+tN@!4KbIb9;_a228ip{j(SR zhcmu|59~7+O=H6&Wb6qjCvsgjKlhzCu%B}(-c-6W5 z0r1Z1I86)u9|=QchX!uGe|BmI!|=;?lK4-uW7`L;yn{ohiV?mBShmk)JG^UpLYKNzJGdR z`Um)OFU6V|g!^meNAFjz!+4KUb2|j_+o2!*v1p`quqzxb@X!eh%Af3_!I1fNSX`KB z2(QXieOIwa&Jdqv)=tQ_)$tw&-^PbMo9L)N^QxE9y+VK>fm^kEE5M6>i8a-%*yI^O zmbYq++oA+MpW#^fmhD9iGz$H?u+ru<>qK^4*5f)@D>pl(xTHZ`;g>?P_QVakq|I7x zCXKdIJgyyad}9!JRp7=>lCP&JR+}NOZ4vj<`!k8e>SEsgjdez#-S$P^YQhV0TH!&w zhi{S!uQE^NZ#`Y{O^#_5`2{+Z66EITfLD`nKOK=s2~ zK{Gu!>7qBawG?^V2?!77HN_2>K>C^sqma*qeQXM@>k(r?I+Y{oWGvkQZYtLbjwKzV zWd$SUC27`^Yhb_)(tilW4$!xEcwjvRFMeJYGg2#1dwwi$%kVC!Rt1{m0>xOJAum%NP1VLJLf6l_jD61*y$VKElVoB*8bnc%-RsDMqn|PTDx<_or`eI!C zN!58i&-!XkX!sw%^A%35Co35}19wi@bbQ&L>2e++&0hv=iMt2t@n$ehg9YgeZA{*! z)#L=d@`ft6Zi&!hrbPD(!0jN*?uTly9I$~ERNQ7kh7L^26L$kC6%PJ^A@> zZL*X~RBzZNKQVD`lu(4eL?*&Qe9g0FZ-!4L@gtod?n_f?F7tku#z+ZL252q&09~Y( zh&ZKEVbCpH2`W3JBi#C7=J}l7k`$!pUCi>mYrWNovp_kg0OzFgG4NIGna`k|k@e?= zg7t!8vS66yrG{L&939a$ww&5n{QMH<`)IypClkMB5xqT0Z=^QqPvSX)D&P=;Q6EH>Y{FHMX_D0t=)by`)RV}=<7%KjgY;DB2 zKSAtrwzC8LeQ|FtO`$gaQ3kE#`n-=@=tyYWZQ9hUre(fFGM*k6XB&z2Z9|VB3pP%j z5XiH-M6h&WqKHg^(V{cmAbeCe60J;w2bIF(>^-i+qc*A6*V6CYs+M=Z?~)Yi70v)d z6M1#^hwNpoX_P-!Jq$kLMtM;1nBvVHj7&WcIrbDPJ0@&8)On=+#3sZl^8w(ap@;%h zMb)wbfM3Dqx3~Q!29jru;$Ra7%O`niPW7d5{l*jj_6Jv^HEC@~6xX_+Gs=Am^?u^5 zP&IFzt%fRlmC5F0D|e(zld$mFr#ls}hz;5CZLNpNO(Ttmqi3UCBK>OSR) z7i#lkjv;ShPo{=oo+IC~J&fpkOS^)+1Y4wWr;QkIa)PVJAdm8gc4)9WNebg+$3FKt z*A?;>8hzptrEeYCC%jV=ol7>Z@M}zLdsr^b!i9xZ)_#f16Wh7cN!N1Mj}P&soXuTy zN-bCn-+(f>lMmZ(qITo{0aQW^_WuF4U5*-J%&ps#cy{fg*%ajhLx(AY91bgx=2`+b z#{D@|Z_CN(O^o$|WF;&Uo{MZ|<%UcszBA2;+BQ6mT4h-umaSui8@8){?PPF_bbkA{ zQ~()>Yjx9eK5|!=oSLbkl_2M8uPW>wvN#;F1&*EzLt)M{9NPJTvewS`o)!_4B_`YU zi5Jo?PW#8_CB)+Cbhp1fmp3)_<;!Bp_wq(Ae5}%z2w@jxXqj;0;*xj+HEy7&ze(Mp zCSx2Q$+2%8vY z&4E{+lx`ULtDG(|Oj_u8tK!`>eqS9;5VMG>U$m~O#O>E|oY#ychSM=^JS=7Wnb~BN zcU>i*FPD1MrPJc?B*B`5CM?ju%x_o=J(ld2d#N zmn8#mn@-d~q1()Cx__I3<8L~4H>ZDwXq(aEUvX~WfSrLm2GGsT{Qz(^7L-Utu2K+3lX}zEvz=()@TlRTnn)`g8;1mhdAyS0 zf)ZUOg@+T$b1J$7?Z?9MP*q00jFRGf4DO#By&W3fz@K@C$`0^NDLQ$jxIaW#;!Gxf zh~VO6jd`8w;=LP2@|+!LP{HG2uCg3Mffo~P?K>8~RSMu$6$7gh-QT&xQHd{})CLe` zX`61@xObGPZ=~%y1|+d()lBq%) zTWlM*CFvHX<%@N_*D>5=a0Z&x%C##pg{LZT{1345^D5q1S<>JD^GlKzWp4xZ@GSkT zD`p$n5w7Wc41IQelHyASFutNvd*gDv!?qJs`TFQ)}`62C!9NlH%aJ(GBPZb^R6EVNeQ;z!h)1}uqbj~Mp}v%+~&`hV8I z^Tl|pn`=$|Ze&vuD!c|Z8m*Ets0-02*f^)31bXiNGIx%7(7T&{o`He1T<*R`ak4hJ z&5u64G_6Ltr`mbN+x+0FjXt^`8yb;IFs>sfKAtg@k>aBLVy!efXC8F2**Pm z)l8?7S5bN_DIe$LqVk$*`JS%MCR_dd%~e)J|139Up_iz~AQ~|Dp>$ThZC>NVpR{wa z3?QBik1|f8eJQGY7%={_{(Rh&4!vLbBw=8$ZZIO;)SfaeAo;18=NDzGc?TuQ4svP< zHP_lHvUCTkaQJY(=eB-BULVA$n#lH6%8;D2CUr$?2us$II482G^RT_OVU(oXrE_Lu3 z(SM6|Cja6^z-DS?3e;jLE^I;LpRvncH>}sa$!FLWTETkB_2uXH zN*?C}tm1vzZy&R__MW=mQ`)2Hna09z$tt;h!+Xv`ca|*T#fbLFsErnf(lIJKWv##z z;b$je9-CpwsusL`o2OeI=m0H~YSh!DYnEi?2rXu^>rq zKc`N2>G`C~!moB^4bQ1U8ARLJnJKQ}|J$G7W~wZZ%5sAXilhU!NTNu-Q24c)!2xIU z$;6=!018csorcc)Y~(b!8PatIeKMHmTY?x%GeiS?&@>p4aHWtEJmyNwjO-1*RkXei zeYmErs^%dK@8^2_^|Ag}zF#g~+G(P@bc~Fr&&))7=m^n7Lu}$MDl7LDcgcp_Bh1da z@Rl@JpHGuVzTy_dQ>R*a3aO~8GmRku60^+*kasaw`na&|1WT?b9LIkE#Zp(Ow}>g! zvkauHJwK@R_Fh)IAwJBRgmi?$)o!PEy$VM#?aXbqL{EECw7YBxq-nAo!FWKes$&1W zLH(AzfNG$FsI!HF{Z_wb;WMVz{oNrX%`M2n`&+E#hDF#rizErkzOsG2bdoV7Uj0>f$ITyF1ySiZ>TkzXfcGw!(Mu>R8{x zsUdkj@7O3?@xuK~FZUolN#b##*o>$qih1PNL#mk~^FW{KC><207y5Itc7WUWQl!88 z4$0$jk-T71WFeVqq1)B4tv1Wu4z%z#(^^g`ZzHz8@>YnHu32F5s9T@AlifBOhhbuds|TO8sU5lm&!0u_ zne~|ZC%?!tvc`h@CmcMBlM_Qb#N-Z7XP{|xSqY&Xj%SH^|=gcfx<6q=wL%1gk{|IUBBqc0;yJNeZxM&LcP|Ze=#et zCDDIzJ82&=i7?lPe1noTFLYMAawG|d(MPa0tlhSwD6M_r4dIBTUcxtA?h`kCr)qhH z75vaOhNhHV%JE$ljpYSD{CDeTL_x~BNiQpMk6S^XC(M=I*n7rn4JlE~)EBWkezeC$ zZ!fJ{J=7)pag&&W(d#K3#A2-3Kl21=rrSzz4Ogl6IiHVbkX^V9SU;57!%a2GOr!?% zAdj}uwBO4c_{7wDAA~&Czd44>uuTj#EU?fe6 z`$96$uR*>+R>a=aM$MADBF4OG)zv<3*%_Oog%~Z&eZ$a>5$k`}uR}&WE7rDR|6NTY zeg6;oK~0SMEb>kHHS2fV`V`CWnO;K7e}K-A5*sBavu|k5T@RQmlVX!B zuvUhT5gN1H$FO9bngXwAj2sA#B*j6kzDfbEVRv33AV^>vryFD7x6%hXYA$K)WRd(lXI`hO9urv zzg;h#9i^_5fyFNo_M&r4XX}bp;Df?&oTd26)LZy&2#MGC(H`J~ZV=$@$!=U!-VnrB zk_7PAI-cDvJaNEs!jdtzW$cbU{k3(Gi9x$*)QVso$wTEg2EX_4JBHrzmetK0mqr2eh*<-@4 zvf+;Ed&-u8g=obEQ{3A^pO0}L9?3o}_us&&A>@u(qU9q|I?!Dm^$Q+8~hjZZ}xL;UXD*L*=1>7;gQ6Of3a@5I?6i(kZI_^6e7kmNbHT0MKHXM*6=u_}`P0;b3Co>m1bQ#Sx| ztD^JOB6(!sV{-K1w5!kvVr6x99O{5lE6Rc3l)fY6^^4>b^KZbNuyj>;gG0* z0F$y7-0JX&hr&V2-o1zmS@W3uuW7XLPD6+ZC)GSeU+eBeq9kMn+=A4924SZfuUQlr zOZV=C2yc&bu|6eo7~dUIzL>zShf1`C7xV5?{MZ#!4?Ugrl22qTgY{~?4zjEn-Jy2| zP*;D@l49T7na#Pi&*I}!gbP{3<#(f8(GE|YK|!*O6IJ!qg7!Hls}1oyclvKA_M?Av z=wrL_a=YZVjnrP*PLm9Y;AZ0E?*xYpc5}<`^v2JG zJg>}XE<2wNxLFY(wXK@^VrRJQrph{(uRCqw3%GY8+qk+`dwI-nO ztG_i&NVSdAs=p^h-TYuP==L~&wiZ2tSP~PpVR6P>UfRDm%R3-K#FxO!Z3&3C|FQ?d zn~>3A7dfNEmg=dRIr+x&MnqM@_jW+-R6$u`$V`yg`BPV?VX9g_l)OZqSLs9X-bF9B zx{j1Ap#~FEAq2ow2-U>(9suCGqA(O+EdwTcFOwvC!>Yn8=qqp8KdVk#NAdwWKayIh zur9q&|LIehZBn3b3tQpD^R5#0^A65GtnvNJ?U4+<*)sp)0SXO0-i)kkYu}*y5mFQ4 zFgaU!9YGgH zFU^w`V|mIQnJPuix%={}=}e}oPsmMlQrC(NIBp8_KAf#McOf(_4Rm3|h7Sgy$o4XRv_PePz?6GB38ch2HoF?8ztcPbxXWV8 zev;u^1KsTVo5z1kH%f;3{4$`BHgzUFPF5Dzy^jlG_Tc4Zk*8A`h=Lan`jI`^KaHWG zu;|Va4vSXfqOqvzm^2%$66Qb*xm3U`dg>oDT;=}(Y-;R!%HGZ9amY&YhJj^2X8DDg z5sZZCvZfBx-yvyu$a?Z-e)+_3fbz3TC-FYPKlIqZN8UVyLmLxju3Y>%;B$~YtQ^4# zQH{F~*L#6en%s)fvA%m(BBt0^>X0%hV7^4$6%KejC{uFRg&p$yaIoxatxea;AD`=p zPwp$>qBQW^FABa>o;mNPx4Z}3R#5KB8M${2V3E6L>f^!bpZ%)rd7)K!YlzhfqO`6t zQ(YJ=yCT>@mHOF`w`xGp?R|wGKfOnlCFbR^#-NV2*(Bkv;Gn%Dhte(ad%S$wpyOow z7&3KkHPt%)yrAHj16JP*A;I;r@ITni(fcuMD{TIUHQwqy@KZ+152u0w4Z=eFBDyc! zdFHa<_SovONYfxg$e|C!=%DoxD)Nofg`>1)v3eiUjaYZzK6f)P??ncYELcWV5&)

    WS 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 LEDYN5clQ4Pcdc|u literal 0 HcmV?d00001 diff --git a/web/newclock/maps/4.jpg b/web/newclock/maps/4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5ba1be542156e9b8dd057f6a46d1f94b4abd64fb GIT binary patch literal 26272 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

    0yL4t1?E-H@S;e_5fZonr0 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 K5+=v|o%vsv98}c+ literal 0 HcmV?d00001 diff --git a/web/newclock/maps/6.jpg b/web/newclock/maps/6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64996b8b8e8f3fb0e33e7b38e675b7c3431fac0d GIT binary patch literal 26379 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 PB=RRX8*Ih!&&>Y-c_C|8 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/7.jpg b/web/newclock/maps/7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0eac8452248541fda258ff2da15b82241b2fcfbb GIT binary patch literal 26289 zcmbTdWmFtb^esBLJ0v&+2^I+MPVnHa!GZ=E+#Q0$;O>LF4Fm}UcXtUexZB`?%kO{h zdT*`w>AhFIsy=j|)vI@RSDmxZKJ~ivx(UElkdc=Gz`+3kaBm0Tbrm25Kt)DIK}JGF zK|w)7Lq*3V#KOeDz$C#Zz#*g{rJ|%DB`2q*V`HGE`9w=j&dA5~iJgO+o12P3K!l%D zn2n2@^S_(Gp`oE+Vqg+uVG(nFApgMm|GB+(1MpDc#Nh)G;AjBwcyI`KaIbv;s<-Dv zg8QEa_&*H}9sv;v83h#$9plZQ85aN#hkyW&h=7EIi1=ph_jVnCh=+v#fkOhBK+P0| z21v*m1S&wKm8|a}QlGw{<1%v&Mnix1o|uG`{v!h;6EimtFCV{vpwt&>8Cf}b1r1Ft zZ5>@beRGTNmR8m_wl1!2?jD|A-XWo3;XflHqY{&nQ&Q8?GcpT{ioqqNW#ttOjZMuh zkk+K`v-?dmsi&}u-m))hsXbL!2uBdH>|hu ze*^n};KF;u1&@e`fQa%RE;x9Pw~l~^i1dL28DByT#S}Tv^3IG!U?(O3t-~q${H%ohUOI%JF=TBn+ z`WgDuODE22w`O+S5sOvU0h>g0meii&ICcFFQ?bGZDGyRffA_ebu1eu2w z-4`N01hJDw70_)U$CA~vP2RFPLfe@E)%vD(g94E_2S|!ene!=G&50{@M72#m4yBd{ zjWgt`F~(JhMXT=vZ}<;ySaGz?CI_*I=U$viee$9QZB?i@>_xY1V{dEl@n*81`d@5P zw<)X)EC=!sR)Z1?s3~S(4+t8ytcgP7Wx9(z6R5IlAMPqo9Uc4~8O^)aEMBw(mkm<3 zL_5e|0fGVvy8TN}w>1WFy*%97GgMt9ItwlEG9Ao#ad}Kdj51^}!nQvH7OBdWDNcR< zM7lo_cjX3~?YAk`I($0ChD%XtdN3w?kH~rk5 zjaK+fgLKHH6Tt(dmAQd=`Q^fqI2Q!>T3I<(a!uO?O~WUlZB#~W{VK=>-(83kF(Qg3 zolp~86j!Jlx%5}uNH`$7Uss^P#Z+MHN&nQV24U+cfzNa!MU|`gQhWgLt1W^pETs3 zvM@5t)e;Z#F|DZj;{y~#Du^QG+@3~c7c!ELwaIiE_>ec`9L(KR`?&o&#Ss%(1aHsQ zCDR=&dNHpNoJ9VKrjM&1V}YXTZ;cTUz> zElb|47}#hLNSdFis`Q;Vay!yokGj3duzp0I>_W5+1ybr=`7UVNCV^-7w6pF=o1bI} z(Z|cMdn3`EY_sDdbgGNF?14=s68Gj2C7KRDjRkwU+b$}zIotWzS&>MNpDLO z%ozwC=vaBupM$pHt=cWb#sRtU`}6wZFRGF+?;=1FSNOWaf9hl&y0T|?e*DyDgiZC$ zobl{Wdk#FXPM!^y;FvtI$Fj%^;5A=Kz2mN^kSeHQm?J|pTaww)0CT~CH7%MT>xcQnFvaj*5 z_~PimC)}?}V&t6?i|K2dPEzj6p~1M@?rQWpytZH@g4y4KIbM>ZFHw5O*m(#iJ5*i> z^?nNbOp?4B2AV1vm>EC~%+4tjJO3ZBFL}7!!`(n|EYqPI#G-U>lm{IPtiUtDa*0SS* zg#);hC+j+kg>nYDEi$(Qw!Rt(JFSfFfcXwl%?rXDkmba4df}C2o z7w_VksLZ9rS#hDOBf7yhr7Sm3OIwSNuG_rM4C|=$88VhZ4B89+o2HLk+@7`J9N??A zU&C*z3zRHEs;ME%+e(0YFsm-E$3&p3J~IpmEbRRx@3a;+>Ze^XoJPXj?E_T4mj3~6 zA1N&uE(51pPOQoRZV>-lP;3CbJNtW>j9I9YmgbYyvz;Uh6N1S|Sp6VffI|9`{VPDX;_o?Wa`*660Z9+S zhV7FLMF(b(N3;-KohJLA%kwvH{l#5V}t(U-&-$@SojY)2~*kl-6WF^7+>0PGu^X$1Ki0(T@XnE1G@}+1aUO@rW*s5(-~gkxv|4&ibyK{*HmGdM?njad(%vVoUE4y&@4ycMNw z>4eB@elPOUaTkK}@G(x2HVGwjhkTxm$z_&9JyYXm5kHro>fQ} zFox>KyxSu89O&mNRLF~^H4&vtP$$iLab+#jd6;MV6(DEI-R0M6$smFv%qED5-Q2R0 zT!yz34EIbOW}(f`fF#ia+oz37NiHQ^X=^fPqC4?OJXZBmxai?Nl951g@C)BY6H_R{ zu6JD=U^>a&pq!ECMH2}merSZ98tyjTUiCtrl0qs6lv)n~QE5WR*Y?_Up2 zhO3fVQ9s$&yc|&OlBsqlL17ix>)Y0*=Fukp7?Pn+M668NO#WcApyh!{d88cxqr^C>`)9wg&iZOxFSXYRR(3M z&Mr&v91B(JwjiS~@uuia7cy#U$in?%?33$56fUcDb0R2q$qx4P?^Pw!oQ#V5aEVsz zJFG;wk`kO;1~`@IT!fUv8uA1m64P6Vy%3xb6XlNMIy8P57_`j#ul=}*)})pTR$6@o zu9kNX3hd=a_x*y+7Z)nBvTd&q2$yeY_dJFK-AsDNX+B1djGDLnPF<95ytbSxd=jPw zCT`=(O?C+;ohCgl(up!2M$t!xt%1_WzDk&B9481M)K)v`c#G_nb7TsfVkVLg+EZmE zoq*G*YHj=5W@}I1`}G%Je!k1)L~{F+Or?|tB9F#n5N69@zS{6hB1-C z__EH!-b*j&G1zXV2`7^M+X(2}=Y+vv83W2!fC2@Z`j$TD6ilW)G9db<_{l;fvyrtV z>WC;6P5X;!!iY1LQcD^$cbm^C&IM4uJNYCtj zsT%Ey{@yH%0zkncCmB+PoIxv%zgvea!j-9?;sXC9KfTu6zurVP)B{R(cs?HUwLT{( z=Cvn|klj1GZqV~~>clbb^t|Ocg!t3W`VQ!QDg{kBqcaZf*$oNXZ5xT{w(6dorbjF# z(S>4rn**$1)rWj@d$kT4@$Oy*Y`YOK?&QIQR{$yz#(RewZ#;aZLa7T)attfUE+KK z9?LCUXdUeHUjYpTvO7%4TDVL*dr5>%$#9lJPpvw(%H2NK&xh3>LPK;mXu!Yl1*QEM zlhkr^iMIMt^U2w{p-_zuwfH76!|UO)ojEozB2OP!MM%oTU(2H3#v;Jm_d)g|F63BE zzU=EgYJFc(Lw=bOpXcBW>pF{qbiiNbUU+$&Z5s-U*eHGrk-1A1yR7i1MSSxqUZ!_w z#j2xZ>>@bTMyGA>x_aSgJK0o38dm~2ojf0BDpF~uvMp{(RwNtEtt%x*bb77{5O0Mw zFZCn|?gHPRVQ8z@DhoBtGx61g@jDpfMcf8^CkXIxr_Bg_bfyX2T(IvU#%YT~%0AV}|?Oh?R+mD%Dl$YQ4!ki5U*l_dG6;t_d$m17z4sP@C zpT$H6)5jfN0qd@GRxb#dSMv7YR)Ie(*qkb2V5wVyz#X`F4Si-%>LbQo!i->^#Pa>B z%v>?v%EoF_ACP2HOqomHN*y9Dg*qQ`gpG6jjf;MLAm?)iW;J0ttursSXmg%Rb-44@ zn9)C@h5&u{c;^&7x5%5Fm48iNs?yxy;#oRx@x{0@gOz0T^8Qio4M8C*^0YU#so=$* z7x1=?c4@+=LN4dBzoIp=4*UdwZdHweq<;}+43oYpY4pA8-Mg|lg*ZRCEFJ~0J=A7Z zJ?m5~)5;R{;MGrXWK-!k0=Ip#4&;9QPPv8=R~j}*CKvyrl5P3zz3J7WhA()Ug8E|q z{HQs7A3QEN{4Z`Km&%bb)h7}`Cx5!Wzbv+h^)l>;4HT1xwGED;_{)_Ppv);( zOW&%Lr1g_ZRTZd57ivBnrAF zn!pyZ@xjE5aVu2+p@2s5Mew2HfuS=yqz7NXBtU;AYDqqX9K@OHlA9Oi^dle8^K-US zuW_!l$+xoJIK67ELe>&H*}@~*oCHZY08r$?dQK4js;i{P+m?5*hB#W_FHlVB| z%iE0BGMQP(%a76}afU*h?Q&}P?4hj6QfK0AVOf2esM}Hzhyp;OnrLtkQESLPwHWUd zQcw76ux79!OEG8V>PSljcwXKOp9L-QANPC9q*RJ5u&}0|>Gz~YhU*adIp_S}PQQOYjjInCuiNU}9 zQyvlzOc|?;)MQ7?2DPJ57A2{A8uU#TDgo_pP2I@jbHIV$PX_rXu-^xZJwA6N8jX#A z(8KoE06}6jLX`;Z=@xxVQTDqT5QHDSRV)7541 zgc;-fFd@iI>6KZj?kuwtV8fSOB?6emEe(;iQbpeJNS&_uE~*+xd8F(8_CDb?^=o`K z`uR=YcTsI=eG(IH z_pH1HfIo%d!n)5m1@}rtIa^v=nrHRsg9p|)04=wmNCvhwf^6ucc0(y+HC|@r`?&-A zW&{uPw7SMDGKp(@b;+0A_%R@?rc&Y86b}Jb&O{20QA{2TDG=RR0I5|N`e@m)OUsA% zFsnp}#~JuN%u^cXfWm*Ivaj#1;~{9a8XKS+em3YsY(+ivzD5de@;faFRY6&3c9UhJ zf?y$}uNM1XO2NFT^$hk03+ot%@!?5Y)?Dvfq8|MT`+b})E#uf-(HYqiyS7V6+^L)W z=O+S{NN^Ko16g_G+L#dYE!98o>#6Pba)wb$ujZXj@U}-6`9f*)B~s~fOnnb+;2#^= zF>F2yO0L6Mp3{2#%SyaC%ML2>xq>d-)p3Mj8g{lnmYAw|L!%~mH>a!BKBr9*rm9>q zK)s`yjCtftC9G8$fr`h00^}5nJAz?S02LrWe$LvxUM5*tsPEIN-it0Bj#J{gU1jKs!*KaGS7$%Dr zxL%tiR(NVn<`ZSt<`xmzrn%(mrroNo+8MJoym!u_UK8e&ZR!!W+qIheYzzIEFizVh zZ8XTQ7AP8A3SqAb9BEIQB0c=%T8pzT7j~Y)hp)>W1Ru0mK5o$Tweu)u2bJ2j7*k@B zDx&7MHsm6#GmACnU*96NZ#yn1&)JFNrbe~K3qEr$gH6X$iZ*9?(`pLca@#u-1a(wd z$gI!4DK#~O=QtdvG7Bhr3EO&j`2gQ35FWCa}b3~H^n>f>enXY>^}Izx7nF#x(3ZE z#B+9xB;E&nnf?2H_jK&47K{9Y_~g^zqqb|y0vUVi@!y2;Z0Oo{`KYtSkQOT~WyUa% z?aF)Iz_z9Q8@Mjp-Q=Uy%&*HnbSEIhsb0>mRnH>2f1Ap(o7iFsWj|JPs&E!cvKf89 z*I^50IyEMKVMX9QK1CScn-a#votZm|uhD5ACkSW^wJZD*@Y}~qE zD0RhI{vWf=$1n7n4zw|#jmF1$MLj*D6OH+d5T#aia^|7@eUjscxvMoUw(cO>0ruX! zSunm5U8!UGrjHZz$H?UUJ%QtJZm_Wigq`uqmzH%`0x#~12lMg+aPU~2b;SNt!Q+j< z&_Cc;>cUgBW|tMvhau|Q9RdY6GIJFP3Hu4vysT_1;(RgV8vX;XXV215I47JI>#Id# z0dj4nzs`xDWD%VQ6UMsotMP3vA@Ir~K$SogzT(u9^gjNYf^iICY=`?1`iY}K4h#cD3R;iw*8}4P~de3DL?@YnZ>t@9O zKnrO=ayKFxXdNLgpKHGm`i@WQrn+XB#1T^~*tCS6kpv9LG&BorO{d!U$GCdNWP?MK z9?FkGQjKAE6TT+JJT!VlMqIVcglse5z;-~7L~kh=r6$9Ky{(S7krTzvh!NA>FHcO? z<)G8~kx3STqD+pA9+}r~GAI8+aMt64iMz=Y)n$pA^3GBnR%JjH&MN>;73aEa2coX{ zWjVfH#8yyno;UYEo~MyW-@rzKM4M>-;MYzfV|4it^!bHoq$*58TXl3XckX`2&MPz_ zkz;B>qog=m-TJ1=D<(9C@-)Wk@KLLbwfEnZoM@+QTu9X`z;4x~`THdF=G>5oAi7=&QbGMwp2`|~mOBa=Q8ti?6a~_{Rxp5rVi}-6)ohu5 zou=xfn}|ZWSDJx+aBHvbf6cf4{0d+mT@lSMwO0VHtB}1Drrgnb1vFtqGZBoMe@2oM zppJvWD%i|VQ-}YFk><{Lo?bWWsfegZXmo*@XvS)P)bVc55&^>0!Y5cJYuQTMS}pUO zbB8=vgmj#)zQnjwE15Kzx{t9KEX zaw#wKr1eY{srhOSaA$~9IW_{!oYHN?nxn z5i-vbHRJE{X;P0|;_RZ|^O5Er3pthZ`wT%HKe4&$$lKSjsmTerLJ=S;-m%={vg)&E zJn&he7WnR_+2%=uC~KLgJ8tPQ4K(aWUWndWPbB%;>~cqNA2i`&%y~ElhFP1>_`+kr zT|eW8N{P^Fphg$f^ZjFK-o9$XWaOL-vdwQ-2oYa{qlX{8XO+t(kP&|_D5%D5AziHy?4@*y8-wTgKs9{ zA?3rOu=0<29;LE`i|qDsUvZuJUjYbO`=JkJUwS`!$2I?i#B)Y1!?>MnjLw(J_-pkq zN{aFijZy8Dwi=^ga1jpiuJP16+OzgY^F<^jO=N9J&4n z>jnun34c~Un73ww=HdruY68T~ z>`^ug7Z(V%_m}Ab%oos2po>w^ix@}wJ{K%Z#40ScCT^Fe$4c(Jtu!u-?OyTOpwWm#&MC4x#GH? ze@Mm0)~qdIO7PO9#@EcP>a=ZZViF>3G0Be6tSii5-vqr(>d%HKD}qH0qXMlEW1=#+ zuMz(+Bq7%5uq4Qzpcd~%9awRpb}5bF{PrH1Yeov=!{@)L&QX`J1X}`X_>&v~arspG z*Rgi0d>T#G zTBa_zvw?IMXpb7vm`;qCjCF?AP|=%?*`RR&IqD~kx-49BbiOGS6Q)iiI8#L*b$k)5 zkJkC_?}3l16-IR(*_KHp{H4guU=#rCE?)$ZC1g=_I~kXWi4nlh044_aBdWqrOB13p z7DfTyx(T70G~lhX!`+nAx*=fQ*$Og6R%Y_@B`zDHeaJ#pp>~q?R!*y-pRba4slScy zoPCDXvbH1)?k9B=M1~;Qx7|#Zy%D|2nhS7$a%z{>Y{L)^nF@pG-+i!)PAp8C1>Ck_ zGZQ=`6X)Cb0IPJy-^WTtxmR*|1UfbmL+R5&!THeVo9aW048xaG1*jO)Fu4asyH3tX zTMa+B^CxS*lfG~{c@Tu;6+l^1VPtbobFv;3EP-+cQMta?oHk;~E^tf^cCS^KYmqCB zPN0at%9#KCF@^7T+*jR=!s~lT`xO0pk^dk632y zNrG|F$kT-328oIAQ`RTEe2On7;z%@O%6rJ#Uja#4j-O_BG1-XlPUfotk_uUP2G^#&uAsxO%1J`d;rEl zMIN=YUjbH-PTng6np#O`?|}F%jYCLSf_*ys_mMj4eUT%+*-}|Aij=o`J6kIm@TWdU zM}UuYG&E8B3&eeacVTY}&2yMUQdarRn`Tf&R105HboIt9t*M9>WrlC%8nr<1Gfbrt zIIe0J7aBjGD(%>j?MzY!pZkubXwgCW%tI|OJLa{&dQ`_M82H4>cvY)Q z2dy&G4V?Qz|ME!Wayx{G_VOH5s}1`I5nf5N1)%$e3hin}DRta{&1&G{d}c2}8zAjs zRh!5Lv1$@(69RRjq3xocGn=qs>MV1L<>50H1NnAE#d`lox^XreL@4bEBZ6Oc14gFim#s$u8T3yvx3bkmH z&Eeg=q|6`iwKR?#O!X7|dafniq}&%?eD^VwR!Psgol93hLBCVa$W_3BOHg*FXPHcB zaw{s8e@lC{&NtA3nH@z;WdSmy9XWPLg9}hyItX~n7mvIt!=&od=Vtj9wa#3#1y}6K>sy~Et zFj*F!K}08}NGC-Aj%|B@6;{{;1Iiv*a7ea454iMD`!O?^bvKNih@Q?O_nzWdTIuTA zKwQjeSN3Vb+nz#6f9k!;uV1FH;$>^Ts(L3-*{&{MPC?w`gmy12k#mWJQc@0`A~Irru@65EoE%{OKKN%nUuoFq<<} zC~#6E#2_;^*m{L)?Ex$>l22Do1=YK%EYejyx)q-6UxcvI%F<0t{1b<>i^AiqCzGe# zZh^5Zbj=mFn(1+cq108lAOeF|?UupPk}PfG%xS4vBrz3aXPdLSOz3wJS3GjRWnbU9lwS?hKZ3CXw~@7E#$=$fXF_>JgN@%92#-3V17+pR#tHkp=l zzr?d#i-f3cb#VVwV`OBHY!{C5rKxA3L{(i~V#V9H^g-iAchO0D2)0+#w-y%hl_0*< zJEmpZ^H|P#LcWyf&kjt4*%1gfO-46Vi$ZZK2KN)^OGF{D5+ePHV$6q+dOKT*s=x%4 zVT5A@7ZC!I)wg3*;nla(2-EWs{&Pw^@%$5T)%MuXla*`@6LSyI6q#jHyD)-w8k6S{ zu8FrG3_a!js2BrYs*u5N@)zG3g|jirsDs;enhmC@wSF?>p2V-1HOQRe#|DC0P0mIg zb!(DI4ghK~jzNt_|K?Fon#VUuw}xzo(W4GQ*lqvf*+1p4!Qs-hL|}Z=*Aj8xH8id} zCw`(mt@uw>uCkgLKY=ByWDp2Cy67zy33d6;FU!=5-~fhm15E9&ce)jlTUBa@{Nb#%L2PIim$Jdqtj&_-7l6|Frw7Q}h`X z-y`1eZ@y)`I*oqMq*%7(xGp=;#oomx0y^9UPF!*`Hnm#yF-+vp6EEcGFyoK4*kCfB z8#vi<82%pX4>UDFr05yJHM?HgUKZ2@)`Ey($w9~0MB|33y0plO8KK`8;ffhHt}VPw&a1oHjLr_i|~+ z-(f^V>CUb4m(qliyd+<8`<_Y4ABF@n;u&75wNZFJYR;q&A?fbG5H(Rqj>SKeGGwFY z!jXJOa$er&?*IDS7l=|VkMN+u!X8g@U`;Z6@QVlh{KHCIFlU5CnSwy2DWfz>94{bT z&mH=&DsEn8;b^>o#bKLX3$c;Iko2SP_R7^8c zk>LQNUR!g$)o}@gFBPadi|-KVj+T9s+ojDY!kLpD?35>p15+~*6-R?6G*GTZnB{Ja zw9$xOMh>=GF$JDGS$+|gN$4N`1`X|NJKVa{L*_t*c0z5${h-+WxK6)V!zflM#n4Gi zc^6w{kI(#6YO@d8nk$|y?N^RDMK9OXn6g_S4I>rgEbCkM*(?z6uyAkpSF8LZEIt__ zShkp-UY=TlpTy(rb02}DIxssy$+-ik>&cbpaq`!1JV>N)BiE;L<{Qi!9Kw2=_l>g2 z+TVo(xkCQ2_^8Ztx2xV^N1VmL$OE07b(177L5(KN>cG=7t=R=RkoJTf25wR(9DV#HyZYghhA-o|SR|G#?MZ`@Tn{=r+aFU^&}()j{T?!Rq`eB{pT zE_uoUv|7&`X0G=oExE(yS?;$Psy|#_E`(pGwUyRyTJ$5{Z5W7GhkZ{z8@!Vjc`0mp z-$&_|(m{izJveEnDVa%rEQ{WaYZ3Owu!9a&dxA?)UYS>|vqMZcJ zaPvBT@Y$|*mUVN^H4PG(s_d^v3>eH#-6y}@yp9T2!PJ)(e6KpfX`w?GgS{`%{YJ62 z$QRr>D|TK8Ce3oU_bgP&d0m>aLtw+zPgL0Bt|e+f3yG07OKieN3)rzinlVC@6i=T= z-8oAeQ4vcM!2wQ*SGAw0kdi1IY62wP)35d0b-(qwnayn-qrrP#XYo>6}W>csI1R3!Q z`d+sm$S}_pSi=Mg2$+PzXCE}auMH964f?0}Rs_1t-SKF9u4uDTp%5SU)aq5T;&5`d zGleC9!YO>T%D!P_P$f;hgm1o>yzjBDn$hH}h0EVl!+!<)HeuaH924#WFDch*FKM87j0hYma*F zy(sxQ(#N^FVdulcDz(j0WEx_8SY;UFydJrd=jM7Vu`ce`4} zhN8`BhUC396U_x0HMvQ=ci;oiQS;xP3?CFgy2e7(E*kP`S_0 zp0KsJ|Bcl}aYzR;zB%5)ZdRfX6#V%B<`bo%gK{SkCa{ez8)p>oo2>(5-P!*!ER50y z6oU(cBJ~Be#*;nBcu*}8dC9aA#j|kA9}H+Ie)8l+F~Pfwyzka1o)u~An)jE2=YJj) zUMzs~`hiu7Ope5$TmAiSl(eYWTHTADXuw0g%%rbJP`kgv~c-o4WD+#4FKQb?Q zP`olQ&vvh00x}T|R-+%8=r80Nh%?9M{hPj@eR=;1c&r%1mqsYz4Sw%ZjuH|Rw94?e zbfK==QFPUkY>d2)nQ-$u==n;Fv>|aENS8ny%*i{=rZRba{FZ2hUE2;S7U^dhy@Q2a zpFZz_lCsUq-~8{s`ib88XKJm(RyP5Px9CAw>o=Y)$BKAMJa8U_R@l0huT z#RI=lPF{d+|6`MtWI=LV22tv|K&@pgx~sW0EeZoeIo@!oy%5?sgSVh|KKrcvxIkBO zAdWsVH^G!^X-sYt;<#L;LJyFLSt5))fElg3|I4?-uLgad z53#~`U@eMq;=SA=Ix8?h;IbR5)qEB#y0M}dIm3N4NRI9-15RcM$&(g%)X^!q*S^9y zh@JS5Tmy@CT9f?>d*kO-6?2+UcNh;?L5tqHFjD{f>Xgl<3oSf^ z2$m#V=;IYsg*-UD6W+(jPC8Q(y_RYK?WGrwjt@)ihG@I$o_p%#@ z9BL4P+P_}?CcM{wJi1w1&L6A?SQHSMs-mv)PY}M&uR+XU(q8i`{=b65Xwm4n^P1^R#9v zvH9!&U*JIpDL*`TUTjk#o4k+_`ph@VvjP3}a*CYVOw?PZ%& z$m?RuWP=N44UVEcETRcrzMzb@sB{LMz45-!Y*umfNo0lOd}YH3BL2;a(otmNS2U+_ zT(PICtFUUo#n7%P$jBuG((Dz-P}RcHQ!ZKm@NN+=Sb000gjV<>fONcbRyacajD<`- zfnZN=1S8Np`_X64pJpbLjIVRn1j8u?Gwgk;+Uehg(u-6G*1racPZexlIx)FLa{*zt zWz9I&%`(_kS}K+|ewYRWky}=o8|>w?Oi7c5P!bXfl|xm7KaP?md8j&$=|Okiz#iCh z*bmPlh188l-X_;@PFc1GtrNjq6l%AA^Q8@?VxtHA;tyati&knJOT$eoVLxVaqn|~V z;Od%ci-aLf$tr&jS5+BhuDFmRuYIs~2j|%0JM9J`FEOv>h&5~#1b*6(_&YnZzv=`A zvfl$fIqs3a3*CWIyVACJU#m1-+~~)=_*E)7I*bry=W9E0^7ovf4Odk$AlYL}Qs_(3 z0l$4UO%l?+q9(c=Aeq#N^Sa^r{o}4u*n`!L@azDyDnk{T%|3`oRjb@F3#&!a??Lb2 z;&7w3H%)NS<9#Q%kQ~+WHQurPb;6T><=Uq&1i1VHE6U6Oj#CMeqGaFGABJh#Is5V^aV#2SX))dH z3NE0KIcSSKEs=~or5>p~Mb6T0Mx4B8Nj5SQ@AX0$VGGK~I}&L*0Qf)EOjF|$B;Sw{ z47DyjA^cI;15AHAjX(^3W?Eu()5G?wKyAzo^e9JZ01)|KSHFTOL_I}bnS#$ zGHEs@CS6nMH?eKSy3X*6SsaNxB6%>&Ow+04iXCH(De5;9`TVq;H8-k%4k>$^X1|Q! z4SVCZYw(no(-)G|ehI~XyguP8?xh_>EGGQn!0ffTDDPTpWecfKQ6=-#G^U~e-sAh| zkbLJh3C*}IFmYMEK3(|bStCAo!Ez1op>nGT^~TV-r^gqfZ_2a9iQC+sYm&O} z>2NtSrpv-I@zqNshr8Z=qtluAYN(5t31@mgk77xG(slkUd+cyDPVKcIStF(kbeCPV2x>zfv0iI_N4 znAPfzByPCfZYyx3m^CSW>YJ2nV!DXqb-9G7LWI#X9_M7MslR+D6JhM3uvHsWkO~n$ zmDDVd#I+`{^4Y|Kv&v{zOrOt>IEmG@a(R5An&>3f>!?c1YtfOO!r)lBAsqzY71=x?^K!}N;+jP5XYj^A;?8R+gy`X%| z^;HtMe*2DTv`g4;X8O%POmOuZTF8D=EmSoZJL9FiZGW)IRbTD%Etpi7zpEm3Z~M(@ z$$F#XZzmaLF0&(ui+cVFSg4T4^Wf-p6Z}n<;e}{okyi_O1sJ!y0=(!>$eA$iH7q#{ zo{u79oW&@cqCd3ErjOJc)}!XJGp~C5Q!(z8rXP>`Ceo*eVGV~F`tSPubfGCCYHyu_ zxZ!QyyFNZGh(+A^tUe+0>L71u(Ng2{ePoxVGMu(|kmcZs13S7AMq;v65Ms-PEdJ;ly-NGg~y<+;6TcaqA(`Je(G*B-~3YAi-v}64eYVxlrYC#*| z_7wcA-ueq&LV?YgU>yBTVMUX)NV=9j_RyDu@=tQF06LuBPH(LHjk1ulcPoPwoo^+# z-E-7YE_*e4nkLK_95<8?do{@q7R6!e=s=*KvdqH8h5?#c3e;&R4Uc+2MQPnhVo)?? zNslkdaS!xCn|KXH|>&xtek))}fuz$0wsAg%E1urL+KHO$1^ zy^BLa?hk<0SJxBHG^LIWVrU=!QPFh09T_KC>wp6phSzSkIn<^aiEIV|=tomAb?Zfb z7EqUe26FchgwU}+0{Dt z*|NPgp<||H-`u$59n9Q2>foy0UM;1(U36d~quMV|J>2UQY7&Rx?BNDusP@*5-B#k( zA3Ag;O#R4jc=c9P+)cE-xv4ek^K%9HI%O|L8JUOZ@L;GHTB@{CQg#Llz?@c@V_cyEJfkBKMJ`AnCuCXk7m4!b(c8}XGln5rFTjF<0NtYwMBcea zn%w2CWXUl5yAGm9zhuPmM}?bm4NBWai|U; zJ1`!i4#(dPs0?619+vFvDAy#cAT4pzxu{t`z0^v{$z{FEI;^GR>>EL-~zO^#EIoOVy&ZDF&vMi~He#jHT*k(MWxT zQH;cD%7sDy(XS#x>}@s0C_~i`P3j=|C}cv*AjSXaRrh~v><pFta}=>TR<|aUw$cdeV~Xa|Mquf`kCi%yx0rqzG@$blI<+ zEuFC-dbdphcSIkz$z%Or{*^XBd=?JbZyhm%DA#v`4|{Ef7Sr*@MPT6k=)nZ9!azmS zc}=NRKyiTeucjLJpsSh zf&>0xv%*uF9=>N%gZhzo2+<@FTi`SfCBXK$(m03lS{{TdAViztYmwNI3!`t5dTygq zICf|vO5ETLlkl+G7Rwu`V$3f~(qEFRIPnJ|*Tsmr%^wtOG!Esy zM^K<*&@v*Fjy_M8?yH55nm5Z3CaG88$~ZCBQPyv<%820f;vr_s?{lQgqfp-*-=i<8 z5J(e-jHbtA*NMdcuJ-y~y{xH;i8`p??g;GcK@6ID()k%#%BbnPuJR2lGM*+%wrcfw z%+xE04O@<4@)b~1(_bdfOfe?$2w<7qYE2}gE@DQ95z&`4T(C5ooc@zuvEp`n^G*k1b!wV6r; zqXbj??cs8N%0Hkz^-}LY%v!d`59=05_VNqg-0}SBshv23OQIzYMj8EQdvx4#(I?rM zL6#_fK2xNg82Q`j%aD^CLa7MO!&`dU7O$yKIz^lB|FION7+VXfd;81M%lTigO56VO5E7asI2`_w zIO$Gn+)j2**$Ftl*I}r6mdx}_rJ(El&EQ)VLgx)|i84?DjB=7VIKn6TAJr%e?PTsg zE7VzMzL7z~gg=#S#j1aK1vW4LcDimIm_AhHaXn)jO8cs?uIzq`%<=IOIUO}zo0u6} zYvec{5CiLAVo4`4t;r4F5Q70xH;Wctjz)THN}@C#J8nd%f}`Du zE^r=T)$#byFBfZX@3FqHj5a$>1dZY9`8Y4wD%{O8eyr5#TsF4(qdhja-A}755}hN! zTRleRW#yC|ewu9ULv9Iah3hw0uTc(5x;ug5!l-j=L1PC2MHz-vI zC@s_=-A1G&G$m9C9qAxMK}L1Q&Yk&u zX2#9ObXA=?JRvH%h_?6WlZixYvt*>p-B?Ys)4Z8qUf6NsV-*`=F6>ZJbCymX-_V13kXyE77_8%>VCCmyE8ZlVR6?fk^ z%YaLH;!xSmq%7d|GbvUbF~08KF~0X`7Kx=rD}z*9H#oBwua@@l=&2{Epr)*W?hZzC zD9$fWJSvoSVFrXcg6E!~%%Doy`q2-26SnUKZQSr?6U&amT*o@yvgpcYAHeZ$HntbV zSmb|0PsF4@jHhD0b4k5T!>5GRPCTG)dPrP*8H`t#>rtx^;E!c2QcRfArg%p#F8g}d zlA5B7FCD~x2qt5)$bFXEPBtsrNEJH5|3_RSvH`FnqA=G?Gx;v?(>6kezEk|hSg-$? zjR1YZesZOVw|IlAjRLkvC5idHy4W{6ZCAb=mA;sZhsr#)+xGR}>|cfi5)!189(@)1 zRV92Kt+$;z6d?ItB83*ai6t(kka3)%J!T*@5q7E?Pyk-JuMhl9zmjJh>m zRUv8m3qcb0;Z7_vHyu$Ffe*15=PAcMqI3d$gh!p1A0mF&G^?Q|R5ILu+xolO=5)Bo zZJQc6dT_1-|9IJ%QFu{{#*A#+YBgaqnclW&>ua^I@T&5PexBum9NYv7G!dZuU;^Jtl>*zkIG~e8T#U z+VI#H+&vjn6Wqyr+{QWhBr)K3?JvE%WYdShsBGKB;UE6~!o(D!K~H9~r5Vft^`X(f z!no+t0Jf*gNHIS*=_7YDueXxSjPDX$e(DSgz7Op?>023gAX<+4x4a)6Z1 zU0&36=Q2cyeRwlU+tE=-|7^3bh+};QgVnkM0Rbtt|53|5981y63IE7t7g86aMc;Ev z1#^@yrO4uvK{Cvqy#i;F=xDKg%Ay{YwCKNKQ8Ew~O8}n5!kAJBZ5Sj1qQ62?RB{Dl zd_nK#bsUOoz#!?pyhNY1re~!~<&`3;^1_PDck4N_%iL%D=TKHw*)Xpa5d&g9Gn5Zg zpcss`&|7-|AZ6AGB3=sV3%*a`08Ki)zrRV31e%{6Kvzem`_csrTu>fIKYDB=*Pkmp zC;VPUpQ-GpWLeG~ii3BB@4d?fW1guo(+X&BL~P9|n$rZV001*es(#234z&ms8#+tb zIo_z33CN^nHS0GbdW_n+ACA&z1WfRE+>(~$1BO>`}4+m?E1iKn$6ohK>%@ zlG9RgjJfmohwIHRE`T2Ml@9iTF9gzR$ll+w5>;yH`I=Jo)!{znpSSfhj&wenlI6C* z1-gS=_2;OL$qV_gg8qU6dBR&vXn0{rp1lR+vOs*5Oi}!>FM3gG@|oyKWuNUX#1!3KSpknoc6cj z)SqJv*F$*Z?kXG)PrF8=wFI7v&i|^WVTV|jx!0?DP6Ql*g<@qpKJ5VxE~Bp2gUf1T z^TqWvVEw!1bx)ki^XdEm`DwaJ+9OE#Fx`(d$M}aU9H8hVeAzuMQrh#Ix^Y`0D5uDQ zd)jJYue3VNGVb}FqmUG>FLr`K55-p;mE!;faQ8bWReXOMiWtl61%Wez=`B@hMQ{es zEM?_#;p@qpaFMc7lhS)?2iH?itO6}IBtop`jvwwx+j8`som4q!523yWDU~W$4okX2 zmBj)Dhg8(+7c8Qs%?#PakV*Jh2uhUGc`3BEIjDF%1=b+JtpBDkbnp+otiF&-wy77D z+QgVNo!u`gRdkoseXpEvT2-=}7oRe@zrWA9^AI9*KE-cpm*+5ySMl3#oNlmukT9ul zu$M+LL<(@!830Q2OD}JM4)j-*Ek@pydO=9E*ZXy$xZJeXUIE`%Ea+MD^W#EpGaI3w zg?OXq+Mg1D<7PC}(a3f@Wj5JJqYRmA9}F!^t)wiH%3>~S@|?m~gHQ-K=X!8#nyGeJ zrc)T_-`{amUX)q2ufkU`%P{~lSWDe1wsX5m$V>mGY$9kY zUaC!HCUw3~&bk_NfUOu0tinIO9^?rXc(JUJw|-{z9Tw9Ja^4-5xNp<&($Zk% zop(qYxl0;Zyq+n*b@y~70^%x~`=Z8h(C(tADY1QJ;rbu{GuxM9U~d&*@_SzMmdA0W z!2UxvIgtbhRu~Gh|bR&J5{U#iP7S3ZA18(x_^c4P92u2 z=`Udm?O7T_XZDAMfC?RbpYK@|f-9 z%eD|_vED|jqB{8=8~>8f*GR(y!(RgeVZe+Bt|`zhFJ{C#+PV6Tnlq>!rc_cT#wmV2 z`k9mv#Gq=)rZEB6LMEipOcvZ87|~3mj3&h;gR7+HK!bA=VTF`vYHVdmro)k)?~m6NXUPsYeHX@meDl<5w?f278?HOKm&)&?-7iYs+<85o za$!B;@M60n&+1*REnUZ2AFcBG{HU#}2jX}Z%c(R{gvty^0}Wb^@%n(upf?Frl?kuv zX$!g(iRGU@MowymB%o}O?yd}3jDyt2oF2Z0OH!-$RYh1m*4G$yiSi@?o9wh;y&#YZ z;(oAx^U~Y5h1&fiPF|%ezpdjr$!RCnfoty*>^im$?gDV;p~dRtwOJL=cV7rAQ;K&T zW-rQfk6zlDYz2?mydV@6FOiC-#1hxChD5<|Nma0=+ZZn7+BJv>0;v`IN|a+43hN-l z#yAC^GaQmp`%k9*l$)vi-ow--=t_#p=e+-hhNUR*IwC%sxJ(MHL;gD0-Gp_=uPSC@ z`v9o1%ICEJVPKrLi|1nrCs&raF@L*Y&k>ePJa~@`CpH2GdL*R*2nAUHBdOj$>L_=cU6pzUSvy(d`xc<`hlbkEUAm; z#K=60_wpP0Yu>!zKevT>&hC;tei4&EoNml6Kosksz3nQ#{f!Wa)-h8$D#oZ@#x^QH zBbJT`FEBKGI3#M*P!c&NALgo9(xq;xgADR9D&{mrp$d z;UVRh0$71u`(6AjZMqF7C*W4EA$BfSxySBjQzp&RTXwVV(AzD3oR+J|Sbg=F4j_7H zUm(}f>=V}P6`R3w6?DBMQj^a1`CW&)BlYIq384*DqxT0m&u=f&_&t;_G)F2u?Ry<& zjB5^@*;2a#DEept(5G{Jn1~ubO_i1G zh+R5&9v{iQJ4xoi<&1JtA&CHe4s~Si@+K*YW6$S1Tvkemf@;F2zb}&JbV@x$a`rP) zfgHFCbjsVFX?$}hh)-NL-RwcNkjt!fLSa^>eHz~>(=za2*qeL{39i~B+>BqsBN(I^ zXo|Fc429c0l*Na9$+oUgdFMza*;apwPJD4PB&2{KRMj(;+;;HM_cxS|K#+-oWdO*@ zr)#(gGHzbP(=t1xi69nA1G7AHq94gdF&uG8uq_p(PwWC>wN~N`=y^oV6%^ye>2B-a zw_WdCm|LA z^+9oPwlu3Z-5&DgwyO|g*4R|)C{O8HB^gDZ^&k61am^}V9Z7X;nwL5IqF{Eo#`bt} zUB5Rdt^-Nbqgx$58O+xx1dm62drRlD;=KLUC@AT_FGiHVZsmq$XP*cEFrJX_qO_cW z@N$lOE33cQkZ`b?TT8K3_Gc9=Ysud3w~u!n4nL_hp%%?JHK=0IpVGLn5$dP7?x4a} zM4eYw*d)_X!(^Rqrs;6(eV}4#@x3q6bP!RGpStP&NC83N8cK_wx=6UR?mChmJH>Zj z-}-JG{1a@hlg$QTJG`JFMKMqfd(*Ls8op|40awd|HfudV9A|g^B{ma_aeTm^k&R@J z(G)ivAKc87=F?KA@$#{|~EA)#9#oeBmR zjM$vG3J6}ocnjKsd&B~lRI4fKFWuOMbav;Ib(ntZ_TRQBTYLM4xo>ru4peY2hO65! z5u|75|2At_|j`L7q7Q^d{r-kaq6hN=_YI1?IM1{ zGWOI3A8ens)_+(6a`Y7IDJflbY?sYN$Yqx)$*XkKsdq=KScl1+5*st{roZX<8Pne< zExp#e!qang@c#OUX;+(ZdCDCv%{H1<;K8mvlB;ws77c4J-Yai@{wxWO0V1 z7}`LxplkkGbOmT}DRaxU6tolx|M;svNHzosIDwOVkb(sk3!GRp$(n$)OSnQ_QKi&w zt!0UPdh^4Y{Q(3LT3J z8(iP%o&LJP1(&=_eqAEk^jq!jnf8fKF5$<#sO7WCh&%ST3Ym4U>&DnV^p4hWJ%ZDP zxSG@7yNnR6?B9> zyi#Rl-5k3{T$Yj4?TcyF{N7K&Q^%-TqcI%0BYes|#~#y8%&yHuP#d%=Sp|L+1VmD01E(9J7@Rh()ysv4wBa*u~%tY7}|q#s5)Dk|^Xb(>b|3g?P{(XK?XY2h=lZ7N9%u<)yC$j5mK zz7o}3BnenPQRav=fqf|FhofxdNMgc+m?LsbKiTpD81DKyS#ser$LqGUb|hd6)KEoP zmT=&x`0VIeL(U^N42nNd2_RWy$Sbu+!{ZQb|*je5OWCeSYXfNHYe(rgoBQ8lgV!spuT*g@g`_u22KZYGHzV#Nv(Fg~{ay=_>eyAysk1`8 z32WA6pv}?5*)`$Z!_zv`oi8rk*%PDCdiCvswcwlS#|a*yqhB7MqaP;|noIXnha_U} zJe3SxORKTy5t61LFZc%Ob8-Ajx^!ppyXH>pER}8r>@*1x%6$4uD_S^ALzI1CP$&eM z(>aDSCO*J06lCdnl1*4z-~bLdk&!`$$sI2)^B&ZV5ja;!4UQnuLM^@}Yet<82VCjP@b+t5VT%iy+#;+}G*VrlA zKjmyYS7z5<-c@%`KsdqD0%A`3#tcbc8-$%qZ0IJ-Gbv9}3m}lJdUNu%1fqo&wdddb zDAF#Oo@YTc9Vuf9V1@ZGVBNN*B1Z8x)?}ZAOs2|bl?;7g65sIJFx^;8dpK}~gwjmx zB%KxuAZ-tur{}J)!q37mJCqof8+q;IQCIV2QGr#n?Odcpozmi0%2*o@dI>sLmx`PZ z1W_JFS9{x*Vxm+^D^Ym#i5$WrGw&pGrdc5LXk_W@Wstj53U^P|z#O}#xDqggLJe!) zQ%t+Y6&NDS+HU{FRCpb*d(Jmmwq1YsuB6Q`q+mo~;30p=neM;Q>8aZ*!J5q$*HvqH>ih^a9ox4BS8UnDp3-n7OMufZl1C&ebhEKNzx&)Zq_;-a|x58^RV z2?%Q@7}5DffD$fT-bmSvkKQ;cSafbj!YF45JSJfJ2AF4PGv$2CI zpb&C)@L!LQlO!MT@z&4T*fW?I>5fF_c_DFiIfxveVxuY;Gu;3~AdT;=rGnG&sc1kz z>S(0Z95FTS*S^VLw5d?K01f)9_WuD+e?h1Jcl%iUsquKSUx;<6^Aesvc__stjdbYP zbj^JmM9@HJ_sD_sCyNzCEKLG0S>9(1HC}58!g5^)K3Ig)#|&61tS~cHg>J*kac$mV z!v5bJu2>7SOxwTaOUJAPs4(C)j-Cl9QovOWQ91H&D+M2{tu2xnt0e^2aA@RJI>Bk# zY?xG`d>+oRUsQ$5iVH^+h@V*&u-POe0k|$SQ^w1ny!=kZ%2}wj%KK$mxLyapq_lW; z#&8&iW};{3@v;WWaL#}A3j)rjY7sp`q9+oA?(HnkT3N{Sm!$a)W{voHEebPcQEVX! z?(0$P7xW-mkCit{eihfK)59fq7&wtf#V?_5mP?BK0R}AKs}PGGG0qV~Og`tFDMXGx zu#6*aCMVB9mK)nqn9uQZVgdObPn`{VVae!h(lP91j@0Ho%eNH}+BO7S4WOBrc;fa` zQ{4GJ%&@Du7q|~SwIW!MJY>8C-dO#Jz2^!fPB_%&eMocTCApET!Du{6b-Vr<+fu~`_jZK}m_)FX{Elv2GG zxJ^fo9hS((@{#LVvYydQR(sjfbv;<}mc|VRnB=e&5ZTaTrSktHQ-7%vW0FApgCPH} zzW6UWLZ)vh_Kzj`w*%qCgGp9`@feQeZ9s8ykj_q0XH2~IPwD<`A?*KJGUo}HXTGGW ze3`R`E#-l|yd*Qn3QZ$;r$_7?4;xa?i&lgowxqHODetUv-IOjI2$M0SVt1*N>v=7vPbkTK(|v_ncDAM%t48n9U59wsY72N`oR%~&ZA(kE$Ft7f(7F0f^x z-1iv~`6(JY09e2%m6TSYr;pdN$u#l|v7@)iIQ~Ec!{B>#>K(=G+~YN6F1dDYjFi>f zNl)$2u3=g$axy1tS!+@Oex4uw@7BBx0XEY`ri}r04>;Dx!C{0Q#&;mf!a#~&MS-d^ zJao64lr2L^A+aXi?@aB^agN;<0dJ)kKWL

    !X}qFCjN2p2o+;Ok7%VM$qd(*cq&# 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 literal 0 HcmV?d00001 diff --git a/web/newclock/maps/8.jpg b/web/newclock/maps/8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0c5bb692398f325438f6d7838d4841e4ba141bac GIT binary patch literal 26058 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 cIw6aWAK literal 0 HcmV?d00001 diff --git a/web/newclock/maps/9.jpg b/web/newclock/maps/9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0aa57d513638c04610c39ca9da26eb6e416a236d GIT binary patch literal 25652 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

    @=BFUn)S;T`@Jkc|d25fbf*gNae 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 DW%@TK literal 0 HcmV?d00001 diff --git a/web/newclock/numbers/text.gif b/web/newclock/numbers/text.gif index 72b521f73bb2487e90646197832bbbfb718ce20e..392fc636d561d088fce18c688ddf50d53b1b61bb 100644 GIT binary patch delta 764 zcmX@Xx`LbA-P6s&GEs<$kKy}7?xcwUveZDV^*Rhd05X+<<3EEUhm6OD1qYisgtcN$ zY*=`>T|n7uj>pDDN4q7Av+kVOxcGR#f^(OQ=cXkmCu;<+iaEJy>FMbP$*1OcZeDhF zwuLxf!Gi<_7Dhf69-e>$j*Se=oE8iU7Z@2iyedC^;S5<2G~d$D&9uv8<;6yarB)eQ z-!kj3ZI(T36_C)N5U?yj);lkELN42fNSv(g_b5<2d+*JWREBZ98&~FGx8- zVS?i$mc7C%1|N28d^^P9clc}m*DYW$ zi{Y5!T>ekySHR`apU?-#fbo*eIbj$?M=)MJ(_yP2-lOFUS{MXr|V*rM3@(yIWNkq_6*Z^4gvC#~&_}&KGQQkE>f? zwEE(zq={1>uD%Jee##iCxE`?98rubEsMuis66?+xtzP9bbbdF8l(?aG%RZow~ zF%3Q7!20KQ$i81mDeL2J_C+hGFg{?KmUmFVUFYKwk?@?4M;BupU|juO08ON1BYV(5jX^+K*3Q6 zj0g^Z$U%UhwOX!Mgyvp{Z?gGJ9(myoYk+NcMt=?p1|(i;ZVnA13R{Iihgu?*U|oM- z5LuI+T9A}P0S^yqrhyO(siq4e0H%tkjtCD2lMWBC4|}6NvACvo5C_AU4havJ5Dy7( zgug+$4#o%!A`GSp*$D{-tg6yHUdqhWTAI4MvAqzb2+8i_Kcx=^1F0g$T=@sQ4-_dLn;b*@XC$EjsybQQiO3w#WlN1|EwEOz?7L% zYFH60nWK~g1Rs-q%I5iLsz96FO~ zjgt`8nQEkEX*_o}0T_$swFb;(aH9U>S@35-S_wz!0sugl!nQDwsxl^kpI(2c%@FVf zKoaoRlvlMS0N`yzM5m4)KV7^B&?&T1w`QJE%3j~W5BlBW1;HoYx(8DV9qpo_RtOQL zY6^>ZCvDAj+k}+bv6;BvFTjEo)UNcq*T`d(_REZwM2@U;2c5?|Z}b(xRzgrJDgoy1 z7N4d2o_v`#Y%Nub_RW5Ua=2gT9N2Vzwf>7N1EA5xZer-c2P|yxLCi%Qju7Kt33-%( z3`u>F#GoK>?DilEha|E_0V5Ou;u;!mXu^e?0D=h^Yyj9vf+of& Date: Wed, 2 Oct 2013 19:46:20 -0600 Subject: [PATCH 153/330] X10_Items.pm updated for MS13 fix --- lib/X10_Items.pm | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/X10_Items.pm b/lib/X10_Items.pm index b8695edd9..1cfaa580f 100644 --- a/lib/X10_Items.pm +++ b/lib/X10_Items.pm @@ -1537,8 +1537,12 @@ sub init { # Note: name is require, as $self->{object_name} is not # set yet on startup :( sub new { - my ($class, $id, $name, $type) = @_; - my $self = X10_Item->new(); +## my ($class, $id, $name, $type) = @_; +## my $self = X10_Item->new(); + my ($class, $id, $name, $type, $interface) = @_; + print "[X10_Sensor] class=$class, id=$id, name=$name, interface=$interface\n" if $main::Debug{x10}; + my $self = X10_Item->new($id, $interface, $type); + $$self{state} = ''; bless $self, $class; From 18942e68ec28c27521e7604fa66265b530685f62 Mon Sep 17 00:00:00 2001 From: bpgross Date: Thu, 3 Oct 2013 10:51:25 -0600 Subject: [PATCH 154/330] added clesius support to setpoints --- code/support/hai-omnistat/omnistat.pl | 191 ++++++++++++++------------ 1 file changed, 105 insertions(+), 86 deletions(-) diff --git a/code/support/hai-omnistat/omnistat.pl b/code/support/hai-omnistat/omnistat.pl index dd23ca76b..6adefa851 100644 --- a/code/support/hai-omnistat/omnistat.pl +++ b/code/support/hai-omnistat/omnistat.pl @@ -55,7 +55,16 @@ foreach my $omnistat (@omnilist) { - my $temprange = join(",", (50..90)); + my $temprange; + if( $config_parms{Omnistat_celcius} ) + { + $temprange = join(",", (10..32)); + } + else + { + $temprange = join(",", (50..90)); + } + $stat_reset_timer[$omnistat] = new Timer(); # little trick to support an index if you have more than one stat, and no index otherwise my $statidx = " "; @@ -84,85 +93,95 @@ foreach my $omnistat (@omnilist) { if ($Reload or $Reread or $New_Day) { - # Talking to Omnistats can be a bit expensive for mh, due to the main loop hangs this can create, so we'll wait - # 60 seconds after the event to space things out from whatever else might be happening at those magic times - # (plus an offset for each omnistat id) - $stat_reset_timer[$omnistat]->set(60 + $omnistat*4); + # Talking to Omnistats can be a bit expensive for mh, due to the main loop hangs this can create, so we'll wait + # 60 seconds after the event to space things out from whatever else might be happening at those magic times + # (plus an offset for each omnistat id) + $stat_reset_timer[$omnistat]->set(60 + $omnistat*4); } if ($stat_reset_timer[$omnistat]->expired) { - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Resetting time"); - #$omnistat[$omnistat]->cooling_anticipator('10'); - #$omnistat[$omnistat]->heating_anticipator('10'); - #$omnistat[$omnistat]->cooling_cycle_time('8'); - #$omnistat[$omnistat]->heating_cycle_time('8'); - $omnistat[$omnistat]->set_time; + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Resetting time"); + #$omnistat[$omnistat]->cooling_anticipator('10'); + #$omnistat[$omnistat]->heating_anticipator('10'); + #$omnistat[$omnistat]->cooling_cycle_time('8'); + #$omnistat[$omnistat]->heating_cycle_time('8'); + $omnistat[$omnistat]->set_time; } # update data once a minute, per omnistat offset seconds. if ($New_Second and $Second eq $omnioffset[$omnistat]) { - # we make the extended group1 call that also retreives the stat's output status - my ($cool_sp, $heat_sp, $mode, $fan, $hold, $temp, $output) = $omnistat[$omnistat]->read_group1("true"); - my $stat_type = $omnistat[$omnistat]->get_stat_type; - # Remember the queried values in our own cache so that we don't query this from other places unless - # necessary (this is important in a multiple thermostat sharing the same cable situation - # where querying two stats in code later will cause collisions on the cable). - $omnicache{$omniname[$omnistat]}->{'cool_sp'} = $cool_sp; - $omnicache{$omniname[$omnistat]}->{'heat_sp'} = $heat_sp; - $omnicache{$omniname[$omnistat]}->{'mode'} = $mode; - $omnicache{$omniname[$omnistat]}->{'fan'} = $fan; - $omnicache{$omniname[$omnistat]}->{'hold'} = $hold; - $omnicache{$omniname[$omnistat]}->{'temp'} = $temp; - $omnicache{$omniname[$omnistat]}->{'output'} = $output; - $omnicache{$omniname[$omnistat]}->{'stat_type'} = $stat_type; - - # This mashes $hold and $mode together from registers cached in the group1 call and outputs a combined string - $mode = $omnistat[$omnistat]->get_mode; - - Omnistat::omnistat_log("".$omniname[$omnistat]." Omnistat $stat_type: Indoor temp is $temp, HVAC Command: $output, heat to $heat_sp, cool to $cool_sp, mode: $mode"); - - # only store the temperature from the first stat (which we'll assume is master) - $Weather{TempIndoor} = $temp if ($omnistat == $omnilist[0]); + # we make the extended group1 call that also retreives the stat's output status + my ($cool_sp, $heat_sp, $mode, $fan, $hold, $temp, $output) = $omnistat[$omnistat]->read_group1("true"); + my $stat_type = $omnistat[$omnistat]->get_stat_type; + # Remember the queried values in our own cache so that we don't query this from other places unless + # necessary (this is important in a multiple thermostat sharing the same cable situation + # where querying two stats in code later will cause collisions on the cable). + $omnicache{$omniname[$omnistat]}->{'cool_sp'} = $cool_sp; + $omnicache{$omniname[$omnistat]}->{'heat_sp'} = $heat_sp; + $omnicache{$omniname[$omnistat]}->{'mode'} = $mode; + $omnicache{$omniname[$omnistat]}->{'fan'} = $fan; + $omnicache{$omniname[$omnistat]}->{'hold'} = $hold; + $omnicache{$omniname[$omnistat]}->{'temp'} = $temp; + $omnicache{$omniname[$omnistat]}->{'output'} = $output; + $omnicache{$omniname[$omnistat]}->{'stat_type'} = $stat_type; + + # This mashes $hold and $mode together from registers cached in the group1 call and outputs a combined string + $mode = $omnistat[$omnistat]->get_mode; + + Omnistat::omnistat_log("".$omniname[$omnistat]." Omnistat $stat_type: Indoor temp is $temp, HVAC Command: $output, heat to $heat_sp, cool to $cool_sp, mode: $mode"); + + # only store the temperature from the first stat (which we'll assume is master) + $Weather{TempIndoor} = $temp if ($omnistat == $omnilist[0]); + + if( $omniname[$omnistat] ) + { + my $tempname = "TempIndoor$omniname[$omnistat]"; + $Weather{$tempname} = $temp; + } } if ($state = $v_omnistat_fan[$omnistat]->said) { - $omnistat[$omnistat]->fan($state); + $omnistat[$omnistat]->fan($state); + respond "$omniname[$omnistat] fan $state"; } if ($state = $v_omnistat_resume[$omnistat]->said) { - $omnistat[$omnistat]->restore_setpoints; + $omnistat[$omnistat]->restore_setpoints; + respond "Restore $omniname[$omnistat] Omnistat"; } if ($state = $v_omnistat_hold[$omnistat]->said) { - $omnistat[$omnistat]->hold($state); + $omnistat[$omnistat]->hold($state); + respond "$omniname[$omnistat] hold $state"; } if ($state = $v_omnistat_mode[$omnistat]->said) { - $omnistat[$omnistat]->mode($state); + $omnistat[$omnistat]->mode($state); + respond "$omniname[$omnistat] mode $state"; } if ($state = $v_omnistat_cool_sp[$omnistat]->said) { - $omnistat[$omnistat]->cool_setpoint($state); - speak "Air conditioning set to $state degrees for $omniname[$omnistat] Omnistat"; - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Air conditioning set to $state degrees", 2); + $omnistat[$omnistat]->cool_setpoint($state); + respond "Air conditioning set to $state degrees for $omniname[$omnistat] Omnistat"; + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Air conditioning set to $state degrees", 2); } if ($state = $v_omnistat_heat_sp[$omnistat]->said) { - $omnistat[$omnistat]->heat_setpoint($state); - speak "Heat set to $state degrees for $omniname[$omnistat] Omnistat"; - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Heat set to $state degrees", 2); + $omnistat[$omnistat]->heat_setpoint($state); + respond "Heat set to $state degrees for $omniname[$omnistat] Omnistat"; + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Heat set to $state degrees", 2); } if ($state = $v_omnistat_setting[$omnistat]->said) { - my ($heat,$cool); - $cool = $omnistat[$omnistat]->get_cool_sp; - $heat = $omnistat[$omnistat]->get_heat_sp; - speak "cool setpoint $cool, heat setpoint $heat"; - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: cool setpoint $cool, heat setpoint $heat", 2); + my ($heat,$cool); + $cool = $omnistat[$omnistat]->get_cool_sp; + $heat = $omnistat[$omnistat]->get_heat_sp; + respond "cool setpoint $cool, heat setpoint $heat"; + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: cool setpoint $cool, heat setpoint $heat", 2); } if ($state = $v_omnistat_background[$omnistat]->said) { - $omnistat[$omnistat]->set_background_color($state); + $omnistat[$omnistat]->set_background_color($state); } # Old code left over in case it's useful to some -- merlin @@ -184,55 +203,55 @@ #} if (time_now '7:45 PM') { - my $filter_days = $omnistat[$omnistat]->get_filter_reminder; - if ($filter_days == 0) - { - speak "Replace the furnace filter linked to $omniname[$omnistat] Omnistat"; - print_log "$omniname[$omnistat] Omnistat: replace the filter"; - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: replace the filter", 0); - # reset the timer to 6 months - $omnistat[$omnistat]->set_filter_reminder(180); - } - else - { - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: $filter_days days before filter replacement", 1); - } + my $filter_days = $omnistat[$omnistat]->get_filter_reminder; + if ($filter_days == 0) + { + speak "Replace the furnace filter linked to $omniname[$omnistat] Omnistat"; + print_log "$omniname[$omnistat] Omnistat: replace the filter"; + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: replace the filter", 0); + # reset the timer to 6 months + $omnistat[$omnistat]->set_filter_reminder(180); + } + else + { + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: $filter_days days before filter replacement", 1); + } } # set stat temperature every 5 minutes at an offset to reduce hangs if ($Minute % 5 == 0 and $New_Second and $Second eq ($omnioffset[$omnistat]+5)) { - # Set the outside temp on the thermostat if available (refreshing this value should cause the - # stat to display the outside temperature on the display). - if ($Weather{TempOutdoor}) { - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Setting outside temperature to $Weather{TempOutdoor}", 2); - $omnistat[$omnistat]->outdoor_temp($Weather{TempOutdoor}); - } - - if ($omnistat[$omnistat]->is_omnistat2 and defined ($Weather{TempOutdoor})) - { - # Change the backlight based on outside temp - my $background_color; - - if ($Weather{TempOutdoor} >= 95) { $background_color = "Red"; } - elsif ($Weather{TempOutdoor} >= 85) { $background_color = "Yello"; } - elsif ($Weather{TempOutdoor} >= 65) { $background_color = "Green"; } - elsif ($Weather{TempOutdoor} >= 55) { $background_color = "Purple"; } - elsif ($Weather{TempOutdoor} < 55) { $background_color = "Blue"; } - else { $background_color = "Orange"; } - - Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Setting background color to $background_color", 2); - $omnistat[$omnistat]->set_background_color($state); - } + # Set the outside temp on the thermostat if available (refreshing this value should cause the + # stat to display the outside temperature on the display). + if ($Weather{TempOutdoor}) { + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Setting outside temperature to $Weather{TempOutdoor}", 2); + $omnistat[$omnistat]->outdoor_temp($Weather{TempOutdoor}); + } + + if ($omnistat[$omnistat]->is_omnistat2 and defined ($Weather{TempOutdoor})) + { + # Change the backlight based on outside temp + my $background_color; + + if ($Weather{TempOutdoor} >= 95) { $background_color = "Red"; } + elsif ($Weather{TempOutdoor} >= 85) { $background_color = "Yello"; } + elsif ($Weather{TempOutdoor} >= 65) { $background_color = "Green"; } + elsif ($Weather{TempOutdoor} >= 55) { $background_color = "Purple"; } + elsif ($Weather{TempOutdoor} < 55) { $background_color = "Blue"; } + else { $background_color = "Orange"; } + + Omnistat::omnistat_log("$omniname[$omnistat] Omnistat: Setting background color to $background_color", 2); + $omnistat[$omnistat]->set_background_color($state); + } } # WARNING: Reading state_now here empties the 'state_now' flag. If you need it elsewhere, that won't work. # For debugging, Omnistat.pl will also output state changes like so: # 25/11/2011 23:30:37 Omnistat[2]->read_reg: set state->now to temp_change - + # If you plan on using state_now elsewhere in your code, you should leave this commented this out: # if ($state = $omnistat[$omnistat]->state_now) { - # Omnistat::omnistat_log("".$omniname[$omnistat]." Omnistat State set to: $state", 3); + # Omnistat::omnistat_log("".$omniname[$omnistat]." Omnistat State set to: $state", 3); # } } From 2897c73192942360b93ed0063496adb0c7718428 Mon Sep 17 00:00:00 2001 From: hplato Date: Thu, 3 Oct 2013 18:23:09 -0600 Subject: [PATCH 155/330] modified: lib/X10_Items.pm --- lib/X10_Items.pm | 1 - web/bin/button_action.pl | 2 +- web/bin/button_sensor.pl | 23 ++++++++++++++++++----- web/bin/list_buttons.pl | 21 +++++++++++++++++++-- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/X10_Items.pm b/lib/X10_Items.pm index 1cfaa580f..76ee79559 100644 --- a/lib/X10_Items.pm +++ b/lib/X10_Items.pm @@ -76,7 +76,6 @@ Since this item is inherits from Generic_Item, you can use the set_with_timer me set_with_timer $watchdog_light '20%', 5 if file_unchanged $watchdog_file; - =head1 DESCRIPTION =head1 INHERITS diff --git a/web/bin/button_action.pl b/web/bin/button_action.pl index 3d635755c..6fb05b00b 100644 --- a/web/bin/button_action.pl +++ b/web/bin/button_action.pl @@ -64,7 +64,7 @@ # experimental, and this delay causes MH to pause for the duration of the # delay, this is currently disabled by default. But feel free to enable # to see if things improve. -#sleep(1); +sleep(2) if $object->isa('Insteon::BaseDevice'); my $h = &referer("/bin/list_buttons.pl?$list_name"); diff --git a/web/bin/button_sensor.pl b/web/bin/button_sensor.pl index 35abcf6b1..a2c21513f 100644 --- a/web/bin/button_sensor.pl +++ b/web/bin/button_sensor.pl @@ -9,7 +9,7 @@ my ($text, $type, $bg_color) = @ARGV; -##print "db v6 t=$text s=$state bg=$bg_color\n"; +#print "db v6 t=$text s=$type bg=$bg_color\n"; #my ($state, $x, $y) = $state_xy =~ /(\S+)\?(\d+),(\d+)/; @@ -43,17 +43,21 @@ # $light = 1 if $text =~ /light/i or $text =~ /lite/i; # $light = 1 if $object->isa('X10_Item') and !$object->isa('X10_Appliance'); +my $graphic_dir = "./../web/graphics"; +$graphic_dir = $config_parms{html_alias_graphics} if defined $config_parms{html_alias_graphics}; + if ($type eq 'door') { - $icon = './../web/graphics/icon_door.jpg'; +# $icon = './../web/graphics/icon_door.jpg'; + $icon = $graphic_dir . '/icon_door.jpg'; } elsif ($type eq 'motion') { - $icon = './../web/graphics/icon_motion.jpg'; + $icon = $graphic_dir . '/icon_motion.jpg'; } elsif ($type eq 'brightness') { - $icon = './../web/graphics/icon_motion.jpg'; + $icon = $graphic_dir . '/icon_motion.jpg'; } elsif ($type eq 'water') { - $icon = './../web/graphics/icon_water.jpg'; + $icon = $graphic_dir . '/icon_water.jpg'; } @@ -61,6 +65,11 @@ my $state = $object->state; undef $icon if $icon and $icon !~ /.jpg$/i; # GD does not do gifs :( + +die "Cannot find source graphic file!" unless (-f $icon); + +print "graphic file = $icon\n"; + my $image_icon = GD::Image->newFromJpeg($icon) if $icon; my $image; @@ -71,11 +80,15 @@ # GD in 5.8 gives gray for white jpg?? Allow for png, which is still gray :( my $file = "$config_parms{html_dir}/graphics/$template.png"; + $file = "$config_parms{html_alias_graphics}/$template.png" if defined $config_parms{html_alias_graphics}; + if (-f $file) { $image = GD::Image->newFromPng($file); } else { $file = "$config_parms{html_dir}/graphics/$template.jpg"; + $file = "$config_parms{html_alias_graphics}/$template.jpg" if defined $config_parms{html_alias_graphics}; + die "Cannot find source template file!" unless (-f $file); $image = GD::Image->newFromJpeg($file); } diff --git a/web/bin/list_buttons.pl b/web/bin/list_buttons.pl index 3dedf71f5..f66329f69 100644 --- a/web/bin/list_buttons.pl +++ b/web/bin/list_buttons.pl @@ -60,6 +60,22 @@ } } +my $prefix = "light"; + if ($object->get_type eq 'Generic_Item') { + my @states = $object->get_states; + my $index; + for ($index = 0; $index < @states; $index++) { +# print "db: item=$item states[$index]=[$states[$index]] state=[$state]\n"; + last if ($states[$index] eq $state); + } + $index++; + $index = 0 if ($index > ($#states)); + print "db: state_new=[$states[$index]] index=[$index] states=$#states\n"; + $state_new = $states[$index]; + $prefix = "generic"; + } + + my $name = &pretty_object_name($item); my $icon; @@ -68,10 +84,11 @@ $state = 'on' if $state eq '100%'; $state = 'off' if $state eq '0%'; $icon = $state; - $icon = 'dim' if $state =~ /d+/; - my $image = "/graphics/light-" . lc $item . "_" . $icon . ".gif"; + $icon = 'dim' if (($state =~ /d+/) and ($prefix ne "generic")); + my $image = "/graphics/" . $prefix . "-" . lc $item . "_" . $icon . ".gif"; $image =~ s/ /_/g; $image =~ s/\$//g; +#print "db: image=$image\n"; my ($file) = (&http_get_local_file($image)); $image = "/bin/button.pl?$item&item&$state" unless -e $file; # $image = "/bin/button.pl?$item&item&$state" unless -e "$config_parms{html_dir}$image"; From 6cd77297e749f9f2afe91b28bb408fa52316360f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 3 Oct 2013 19:16:34 -0700 Subject: [PATCH 156/330] Insteon: Add Success_Callback to Message Object Success_callback is eval'd when the ACK for the message is received. - Note: There are so many callbacks all over the place within the code, it might be nice to condense as many as possible into this routine. --- lib/Insteon/BaseInsteon.pm | 9 ++++++++- lib/Insteon/Message.pm | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7e857a6d3..48aeee65b 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -831,8 +831,15 @@ sub _process_message . $self->get_object_name . " in response to a " . $pending_cmd . " command, but the command code " . $msg{cmd_code} . " is incorrect. Ignorring received message."); - $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); + $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); $p_setby->active_message->no_hop_increase(1); + } elsif ($clear_message && $p_setby->active_message->success_callback){ + main::print_log("[Insteon::BaseObject] DEBUG4: Now calling message success callback: " + . $p_setby->active_message->success_callback) if $main::Debug{insteon} >= 4; + package main; + eval $p_setby->active_message->success_callback; + ::print_log("[Insteon::BaseObject] problem w/ success callback: $@") if $@; + package Insteon::BaseObject; } } elsif ($msg{is_nack}) diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 70ea818f3..fea11d116 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -104,6 +104,22 @@ sub failure_callback return $$self{failure_callback}; } +=item C + +Data will be evaluated after the receipt of an ACK from the device for this command. + +=cut + +sub success_callback +{ + my ($self, $callback) = @_; + if ($callback) + { + $$self{success_callback} = $callback; + } + return $$self{success_callback}; +} + =item C Stores and retrieves the number of times Misterhouse has tried to send the message. From 25905c98799e146109c9692da7739ef4aa8c4b7c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 3 Oct 2013 19:27:59 -0700 Subject: [PATCH 157/330] Insteon: Add routine for Linking I2CS devices The routine requires a few steps to complete --- lib/Insteon/BaseInsteon.pm | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 48aeee65b..ee5fd36c9 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -25,6 +25,7 @@ L package Insteon::BaseObject; +use Switch; use strict; use Insteon::AllLinkDatabase; @@ -1393,6 +1394,53 @@ sub link_to_interface } } +=item C + +Performs the same task as C however this routine is designed +to perform the initial link to I2CS devices. These devices cannot be initially +linked to the PLM in the normal way. This process requires more steps than the +normal routine which will take longer to perform and therefore is more prone to +faile. As such, this should likely only be used if necessary. + +=cut + +sub link_to_interface_i2cs +{ + my ($self,$p_group, $p_data3, $step) = @_; + my $success_callback_prefix = $self->get_object_name."->link_to_interface_i2cs('$p_group','$p_data3',"; + my $success_callback = ""; + my $failure_callback = "::print_log('[Insteon::BaseInsteon] Error Link_To_Interface_I2CS ". + "routine failed for device: ".$self->get_object_name."')"; + $step = 0 if ($step eq ''); + switch ($step){ + case (0) { #Put PLM into initiate linking mode + $success_callback = $success_callback_prefix . "'1')"; + $self->interface()->initiate_linking_as_controller('00', $success_callback, $failure_callback); + } + case (1) { #Ask device to respond to link request + $success_callback = $success_callback_prefix . "'2')"; + $self->enter_linking_mode($p_group, $success_callback, $failure_callback); + } + case (2) { #Scan device to get an accurate link table + $success_callback = $success_callback_prefix . "'3')"; + $self->scan_link_table($success_callback, $failure_callback); + } + case (3) { #Add link from device->PLM + $success_callback = $success_callback_prefix . "'4')"; + my $group = $p_group; + $group = '01' unless $group; + my $link_info = "deviceid=" . lc $self->device_id . " group=$group is_controller=0 ". + "callback=$success_callback failure_callback=$failure_callback"; + $self->interface->add_link("$link_info"); + } + case (4) { + ::print_log('[Insteon::BaseInsteon] Link_To_Interface_I2CS successfully completed'. + ' for device ' .$self->get_object_name); + } + } +} + + =item C Will delete the contoller link from the device to the interface if such a link exists. From 20e48aca2aaad26d3f31cf6bd7a96a51e0943e88 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 3 Oct 2013 19:31:29 -0700 Subject: [PATCH 158/330] Insteon: Add Callback to Routines used by Link to I2CS Each routine needs to have success and failure callback parameters --- lib/Insteon/BaseInsteon.pm | 16 +++++++++------- lib/Insteon_PLM.pm | 13 +++++++++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ee5fd36c9..eccd36445 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1440,7 +1440,6 @@ sub link_to_interface_i2cs } } - =item C Will delete the contoller link from the device to the interface if such a link exists. @@ -1471,7 +1470,7 @@ sub unlink_to_interface } } -=item C +=item C BETA -- Can be used to create the initial link with i2cs devices. i1 devices will not respond to this command. In the future, this will be incorporated into a @@ -1490,12 +1489,14 @@ Returns: nothing sub enter_linking_mode { - my ($self,$p_group) = @_; + my ($self,$p_group, $success_callback, $failure_callback) = @_; my $group = $p_group; $group = '01' unless $group; my $extra = sprintf("%02x", $group); $extra .= '0' x (30 - length $extra); my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'linking_mode', $extra); + $message->success_callback($success_callback); + $message->failure_callback($failure_callback); $self->_send_cmd($message); } @@ -1827,11 +1828,12 @@ Returns: nothing =cut sub get_engine_version { - my ($self) = @_; + my ($self, $success_callback, $failure_callback) = @_; my $message = new Insteon::InsteonMessage('insteon_send', $self, 'get_engine_version'); my $self_object_name = $self->get_object_name; - $message->failure_callback("$self_object_name->_get_engine_version_failure()"); + $message->failure_callback("$self_object_name->_get_engine_version_failure();$failure_callback"); + $message->success_callback($success_callback); $self->_send_cmd($message); } @@ -3157,7 +3159,7 @@ C. sub initiate_linking_as_controller { - my ($self, $p_group) = @_; + my ($self, $p_group, $success_callback, $failure_callback) = @_; # iterate over the members if ($$self{members}) { foreach my $member_ref (keys %{$$self{members}}) { @@ -3169,7 +3171,7 @@ sub initiate_linking_as_controller } } } - $self->interface()->initiate_linking_as_controller($p_group); + $self->interface()->initiate_linking_as_controller($p_group, $success_callback, $failure_callback); } =item C diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index dd6315f74..743144d55 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -308,7 +308,7 @@ controller will be added for this group, otherwise it will be for group 00. sub initiate_linking_as_controller { - my ($self, $group) = @_; + my ($self, $group, $success_callback, $failure_callback) = @_; $group = '00' unless $group; # set up the PLM as the responder @@ -316,6 +316,8 @@ sub initiate_linking_as_controller $cmd .= $group; # WARN - must be 2 digits and in hex!! my $message = new Insteon::InsteonMessage('all_link_start', $self); $message->interface_data($cmd); + $message->success_callback($success_callback); + $message->failure_callback($failure_callback); $self->queue_message($message); } @@ -509,6 +511,13 @@ sub _parse_data { } elsif ($record_type eq $prefix{all_link_start}) { + if ($self->active_message->success_callback){ + package main; + eval ($self->active_message->success_callback); + &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) + if $@ and $main::Debug{insteon} >= 1; + package Insteon_PLM; + } # clear the active message because we're done $self->clear_active_message(); } @@ -538,7 +547,7 @@ sub _parse_data { { $callback = $pending_message->callback(); #$$self{_mem_callback}; $$self{_mem_callback} = undef; - } + } if ($callback){ package main; eval ($callback); From ea07cb2d611be9179339497c333cb91f6343e9b3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 3 Oct 2013 19:32:16 -0700 Subject: [PATCH 159/330] Insteon: Convert Link_to_Interface to Handle i2cs or non-i2cs devices The routine first checks to see if the device will respond to a get_engine_version request. If any ACK is received the routine continues as normal. In this case even if the device is an I2CS, the device must already have link entries as it responds to the PLM. If a NAK is received, it is assumes that this device is an i2cs and the link_to_interface_i2cs routine is called. --- lib/Insteon/BaseInsteon.pm | 54 ++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index eccd36445..c3ced8cc3 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1373,25 +1373,45 @@ the device. sub link_to_interface { - my ($self,$p_group, $p_data3) = @_; + my ($self,$p_group, $p_data3, $step) = @_; my $group = $p_group; $group = '01' unless $group; - # add a link first to this device back to interface - # and, add a reference to creating a link from interface back to device via hook - my $callback_instance = $self->interface->get_object_name; - my $callback_info = "deviceid=" . lc $self->device_id . " group=$group is_controller=0"; - my %link_info = ( object => $self->interface, group => $group, is_controller => 1, -# on_level => '100%', ramp_rate => '0.1s', Controllers don't use on_level or ramp_rate - callback => "$callback_instance->add_link('$callback_info')"); - $link_info{data3} = $p_data3 if $p_data3; - if ($self->_aldb) { - $self->_aldb->add_link(%link_info); - } - else - { - &main::print_log("[BaseInsteon] This item " . $self->get_object_name . - " does not have an ALDB object. Linking is not permitted."); - } + my $success_callback_prefix = $self->get_object_name."->link_to_interface(\"$p_group\",\"$p_data3\","; + my $success_callback = ""; + my $failure_callback = '::print_log("[Insteon::BaseInsteon] Error: The Link_To_Interface '. + 'routine failed for device: '.$self->get_object_name.'")'; + $step = 0 if ($step eq ''); + switch ($step){ + case (0) { #If NAK on get_engine, then this is an I2CS device + $success_callback = $success_callback_prefix . "\"1\")"; + $failure_callback = $self->get_object_name."->link_to_interface_i2cs(\"$p_group\",\"$p_data3\")"; + $self->get_engine_version($success_callback, $failure_callback); + } + case (1) { #Add Link from object->PLM + $success_callback = $success_callback_prefix . "\"2\")"; + my %link_info = ( object => $self->interface, group => $group, is_controller => 1, + callback => "$success_callback", failure_callback=> "$failure_callback"); + $link_info{data3} = $p_data3 if $p_data3; + if ($self->_aldb) { + $self->_aldb->add_link(%link_info); + } + else + { + &main::print_log("[Insteon::BaseInsteon] Error: This item, " . $self->get_object_name . + ", does not have an ALDB object. Linking is not permitted."); + } + } + case (2){ #Add Link from PLM->object + $success_callback = $success_callback_prefix . "\"3\")"; + my $link_info = "deviceid=" . lc $self->device_id . " group=$group is_controller=0 " . + "callback=$success_callback failure_callback=$failure_callback"; + $self->interface->add_link($link_info); + } + case (3){ + ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. + ' for device ' .$self->get_object_name); + } + } } =item C From 8319d8da71c71dc9c171523db815129bd9840e4f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 3 Oct 2013 21:07:23 -0700 Subject: [PATCH 160/330] Insteon: Linking Mode Requires an ACK, Don't Clear Messages on All_Link_Complete --- lib/Insteon/BaseInsteon.pm | 1 + lib/Insteon_PLM.pm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c3ced8cc3..1ea23ff7d 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -976,6 +976,7 @@ sub _process_command_stack or $message->command eq 'get_operating_flags' or $message->command eq 'read_write_aldb' or $message->command eq 'ping' + or $message->command eq 'linking_mode' ) { $$self{awaiting_ack} = 1; diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 743144d55..b14308a88 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -743,7 +743,7 @@ sub _parse_data { { #ALL-Linking Completed my $link_address = substr($message_data,4,6); &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $main::Debug{insteon} >= 2; - $self->clear_active_message(); + #$self->clear_active_message(); } elsif ($parsed_prefix eq $prefix{all_link_clean_failed} and ($message_length == 12)) { #ALL-Link Cleanup Failure Report From 16797ffe75392b8f93785359e727f43c27a34d88 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 4 Oct 2013 17:34:00 -0700 Subject: [PATCH 161/330] Insteon: Enhance HopCount, Change to Average of 10 A moving average of the last 20 hop_counts was a bit excessive and required 10 messages before a hop count would change. --- lib/Insteon/BaseInsteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 74bfcd6a1..0b19a33e2 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -268,7 +268,7 @@ sub default_hop_count #Calculate a simple moving average unshift(@{$$self{hop_array}}, $hop_count); $$self{hop_sum} += ${$$self{hop_array}}[0]; - $$self{hop_sum} -= pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >20); + $$self{hop_sum} -= pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >10); $$self{default_hop_count} = int(($$self{hop_sum} / scalar(@{$$self{hop_array}})) + 0.5); #Allow for per-device settings From 2159a5186f382267737be919a9c8b5037735dc18 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 5 Oct 2013 12:34:00 -0700 Subject: [PATCH 162/330] Insteon_I2CS_Linking: Clear Active Message When PLM Reports All-Linking-Complete Rather than clear the linking mode command when the device ACKs, wait to clear the message until the PLM reports the linking success. --- lib/Insteon/BaseInsteon.pm | 9 +++++++++ lib/Insteon_PLM.pm | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 1ea23ff7d..73a61c7f0 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -801,6 +801,15 @@ sub _process_message $clear_message = 1; } } + elsif ($pending_cmd eq 'linking_mode'){ + $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); + if (!$corrupt_cmd){ + &::print_log("[Insteon::BaseObject] received linking mode ACK from " . $self->{object_name}) + if $main::Debug{insteon}; + $self->interface->_set_timeout('xmit', 2000); + $clear_message = 0; + } + } else { if (($pending_cmd eq 'do_read_ee') && diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index b14308a88..0dc156830 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -743,7 +743,15 @@ sub _parse_data { { #ALL-Linking Completed my $link_address = substr($message_data,4,6); &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $main::Debug{insteon} >= 2; - #$self->clear_active_message(); + if ($self->active_message->success_callback){ + main::print_log("[Insteon::Insteon_PLM] DEBUG4: Now calling message success callback: " + . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; + package main; + eval $self->active_message->success_callback; + ::print_log("[Insteon::Insteon_PLM] problem w/ success callback: $@") if $@; + package Insteon::BaseObject; + } + $self->clear_active_message(); } elsif ($parsed_prefix eq $prefix{all_link_clean_failed} and ($message_length == 12)) { #ALL-Link Cleanup Failure Report From 93575699464e1bceddbf5121f0037088119b8a1a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 5 Oct 2013 13:36:00 -0700 Subject: [PATCH 163/330] Insteon_Scene_Builder: Make Code More Readable, Add Controller and Responder Hash Attempted to make the code more readable by using more descriptive variable names. Created a responders and controllers hash, this makes the looping of the hash members a bit more straightforward at the cost of adding another global variable. Alternatively, another level of keys could be added to global scene hash to decrease the number of global variables --- lib/read_table_A.pl | 58 ++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 165dd708c..aaff6ee51 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -23,7 +23,8 @@ sub read_table_init_A { %objects=(); %packages=(); %addresses=(); - %scenes=(); + %scene_build_controllers=(); + %scene_build_responders=(); } sub read_table_A { @@ -994,13 +995,18 @@ sub read_table_A { $object = ''; } elsif($type eq "SCENE_BUILD") { - #SCENE_BUILD, scene_name, scene_member, controller?, responder?, onlevel, ramprate - my ($scene_member, $scene_controller, $scene_responder, $on_level, $ramp_rate); - ($name, $scene_member, $scene_controller, $scene_responder, $on_level, $ramp_rate) = @item_info; + #SCENE_BUILD, scene_name, scene_member, is_controller?, is_responder?, onlevel, ramprate + my ($scene_member, $is_scene_controller, $is_scene_responder, $on_level, $ramp_rate); + ($name, $scene_member, $is_scene_controller, $is_scene_responder, $on_level, $ramp_rate) = @item_info; if( ! $packages{Scene}++ ) { # first time for this object type? $code .= "use Scene;\n"; } - $scenes{$name}{$scene_member} = "$scene_controller,$scene_responder,$on_level,$ramp_rate"; + if ($is_scene_controller){ + $scene_build_controllers{$name}{$scene_member} = "1"; + } + if ($is_scene_responder){ + $scene_build_responders{$name}{$scene_member} = "$on_level,$ramp_rate"; + } $object = ''; } elsif ($type eq "PHILIPS_HUE"){ @@ -1059,45 +1065,47 @@ sub read_table_A { } sub read_table_finish_A { - my ($code, $scene, $scene_member, %scene_data, $member_data); - foreach $scene (sort keys %scenes) { + my $code = ''; + #a scene cannot exist without a responder, but it could lack a controller if + #scene is a PLM Scene + foreach my $scene (sort keys %scene_build_responders) { $code .= "\n#SCENE_BUILD Definition for scene: $scene\n"; - #Doesn't work because object technically doesn't exist yet. Is it even necessary to limit to ICONTROLLER's? + #Doesn't work because object technically doesn't exist yet. Is it even + #necessary to limit to ICONTROLLER's? #my $object = &get_object_by_name($scene); #if(defined($object) and $object->isa("Insteon::InterfaceController")) { if($objects{$scene}) { - #Since an INSTEON_ICONTROLLER exists with the same name as the scene, make it a controller of the scene, too. - $scenes{$scene}{$scene}="1,0"; + #Since an INSTEON_ICONTROLLER exists with the same name as the scene, + #make it a controller of the scene, too. + $scene_build_controllers{$scene}{$scene}="1"; } - #Make a hash copy so we can iterate through the hash inside another iteration of the same hash. Is there a better way? - my %scenememberlist=%{$scenes{$scene}}; - #Loop through the hash and find all controller->responder combinations that make sense. - while (($scene_member, $member_data) = each($scenes{$scene})) { - my ($scene_controller, $scene_responder) = split(',', $member_data); + #Loop through the controller hash + foreach my $scene_controller (keys $scene_build_controllers{$scene}) { + if ($objects{$scene_controller}) { + #Make a link to each responder in the responder hash + while (my ($scene_responder, $responder_data) = each($scene_build_responders{$scene})) { + my ($on_level, $ramp_rate) = split(',', $responder_data); - if (($objects{$scene_member}) and ($scene_controller)) { - while (my($scene2_member, $member2_data) = each(%scenememberlist)) { - my ($scene2_controller, $scene2_responder, $on_level, $ramp_rate) = split(',', $member2_data); - - if (($objects{$scene2_member}) and ($scene2_responder) and ($scene_member ne $scene2_member)) { + if (($objects{$scene_responder}) and ($scene_responder ne $scene_controller)) { if ($on_level) { if ($ramp_rate) { $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", - $scene_member, $scene2_member, $on_level, $ramp_rate; - } else { - $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", $scene_member, $scene2_member, $on_level; + $scene_controller, $scene_responder, $on_level, $ramp_rate; + } else { + $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", + $scene_controller, $scene_responder, $on_level; } } else { - $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_member, $scene2_member; + $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_controller, $scene_responder; } } } } else { - print "\nThere is no object called $scene_member defined. Ignoring SCENE_BUILD entry.\n" unless $objects{$scene_member}; + print "\nThere is no object called $scene_controller defined. Ignoring SCENE_BUILD entry.\n"; } } } From d59152740bd3f1e372be2ed7127ec7e1f8a43cb5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 5 Oct 2013 13:37:00 -0700 Subject: [PATCH 164/330] Insteon_Scene_Builder: Declare Global Variables Oops, forgot to declare the new hashes as globals --- lib/read_table_A.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index aaff6ee51..8e28ae6c0 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -14,7 +14,7 @@ #print_log "Using read_table_A.pl"; -my (%groups, %objects, %packages, %addresses, %scenes); +my (%groups, %objects, %packages, %addresses, %scene_build_controllers, %scene_build_responders); sub read_table_init_A { # reset known groups From 6e950faa2699cd0c8267f822cb0f92a73a6b7946 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 7 Oct 2013 17:47:00 -0700 Subject: [PATCH 165/330] Insteon_HopCount: Check Per Device Setting on All Calls to Default_Hop_Count Moved per device setting outside of logic so that they are checked even if a hop_count is not passed. --- lib/Insteon/BaseInsteon.pm | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 0b19a33e2..a3d137992 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -271,17 +271,18 @@ sub default_hop_count $$self{hop_sum} -= pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >10); $$self{default_hop_count} = int(($$self{hop_sum} / scalar(@{$$self{hop_array}})) + 0.5); - #Allow for per-device settings - $$self{default_hop_count} = $$self{max_hops} if ($$self{max_hops} && - $$self{default_hop_count} > $$self{max_hops}); - $$self{default_hop_count} = $$self{min_hops} if ($$self{min_hops} && - $$self{default_hop_count} < $$self{min_hops}); - ::print_log("[Insteon::BaseObject] DEBUG4: ".$self->get_object_name ."->default_hop_count()=".$$self{default_hop_count} ." :: hop_array[]=". join("",@{$$self{hop_array}})) if $main::Debug{insteon} >= 4; } + + #Allow for per-device settings + $$self{default_hop_count} = $$self{max_hops} if ($$self{max_hops} && + $$self{default_hop_count} > $$self{max_hops}); + $$self{default_hop_count} = $$self{min_hops} if ($$self{min_hops} && + $$self{default_hop_count} < $$self{min_hops}); + return $$self{default_hop_count}; } From c3e335028906e00817be310cf7368f11bd6b4517 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 8 Oct 2013 18:44:04 -0700 Subject: [PATCH 166/330] Insteon_Thermo_i2: Decode Status Flag Only Cooling and Heating statuses appear to be supported. Bits 4 and 6 are also always enabled, I don't currently know what these are. Sadly, humidifying and dehumidifying do not appear to be supported. --- lib/Insteon/Thermostat.pm | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 5ed8c940c..7d5707c46 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -866,9 +866,19 @@ sub hex_short_temp{ } sub hex_status{ - ### Not sure about this one yet, was 80 when set to auto but no activity - ## need to call _cooling, _heating, _high_humid, and _low_humid when - ## figured out + my ($self, $hex_status) = @_; + # Bit Value Bit Value + # 0 Cooling 4 1?? + # 1 Heating 5 0?? + # 2 0?? 6 1?? + # 3 0?? 7 0?? + # Sadly, dehumidifying and humidifying do not appear to be reported here + my ($pre_cooling, $pre_heating) = ($$self{cooling}, $$self{heating}); + $$self{cooling} = ($hex_status & 0x01) ? 'on' : 'off'; + $$self{heating} = ($hex_status & 0x02) ? 'on' : 'off'; + if (($pre_cooling ne $$self{cooling}) || ($pre_heating ne $$self{heating})){ + $self->set_receive('status_change'); + } } sub hex_heat{ From 101143b7435b274677d49b4fa66847f4fd144ab7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 8 Oct 2013 18:45:28 -0700 Subject: [PATCH 167/330] Insteon_Thermo_i2: Update status on Humidifying or Dehumdifying --- lib/Insteon/Thermostat.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 7d5707c46..cb2e1d1ec 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -914,6 +914,7 @@ sub _high_humid { my ($self,$p_state) = @_; if ($p_state ne $$self{high_humid}) { $$self{high_humid} = $p_state; + $self->set_receive('status_change'); } return $$self{high_humid}; } @@ -922,6 +923,7 @@ sub _low_humid { my ($self,$p_state) = @_; if ($p_state ne $$self{low_humid}) { $$self{low_humid} = $p_state; + $self->set_receive('status_change'); } return $$self{low_humid}; } From c2fee31b6f8442b67bc3786ce16e76c1aa593660 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 8 Oct 2013 19:44:24 -0700 Subject: [PATCH 168/330] Insteon_I2CS_Linking: Move Success Callback Eval to BaseInterface This allows for per device _process_message subs to exist without needing to reproduce all of the eval code. --- lib/Insteon/BaseInsteon.pm | 9 +-------- lib/Insteon/BaseInterface.pm | 10 +++++++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 73a61c7f0..f465feef7 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -843,14 +843,7 @@ sub _process_message . $msg{cmd_code} . " is incorrect. Ignorring received message."); $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); $p_setby->active_message->no_hop_increase(1); - } elsif ($clear_message && $p_setby->active_message->success_callback){ - main::print_log("[Insteon::BaseObject] DEBUG4: Now calling message success callback: " - . $p_setby->active_message->success_callback) if $main::Debug{insteon} >= 4; - package main; - eval $p_setby->active_message->success_callback; - ::print_log("[Insteon::BaseObject] problem w/ success callback: $@") if $@; - package Insteon::BaseObject; - } + } } elsif ($msg{is_nack}) { diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index e562aa763..e6c1fd906 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -562,7 +562,15 @@ sub on_standard_insteon_received # ask the object to process the received message and update its state # Object will return true if this is the end of the send transaction if($object->_process_message($self, %msg)) { - $self->clear_active_message(); + if ($self->active_message->success_callback){ + main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " + . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; + package main; + eval $self->active_message->success_callback; + ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; + package Insteon::BaseInterface; + } + $self->clear_active_message(); } } else From 608d81cc188d43bf1d3f2b0e21f5aad0b1cf8d88 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 8 Oct 2013 19:50:57 -0700 Subject: [PATCH 169/330] Inston_Thermo_i2: Convert Poll_Status to an Internal Sub, Replace with Request_Status This more closely matches what the user would expect --- lib/Insteon/Thermostat.pm | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index cb2e1d1ec..8f863bd5b 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -83,7 +83,7 @@ message are also NOT sent when the humidity setpoints are exceeded. Instead, you must define the heating, high_humdid, and _low_humid groups and link them to MH. (The base group 01 is the cooling group and should always be linked to MH). When linked, these groups will send on/off commands to MH when these events -occur. Alternatively, you can periodically call the poll_simple method to check +occur. Alternatively, you can periodically call request_status() to check the status of these attributes. Linking: @@ -621,16 +621,21 @@ sub sync_links{ return $self->SUPER::sync_links($audit_mode, $callback, $failure_callback); } -=item C +=item C<_poll_simple()> Requests the status of all Thermostat data points (temp, fan, mode ...) in a single -request. Only available for I2CS devices. +request. Called by C, you likely don't need to call this directly +Only available for I2CS devices. + =cut -sub poll_simple{ - my ($self) = @_; + +sub _poll_simple{ + my ($self, $success_callback, $failure_callback) = @_; my $extra = "020000000000000000000000000000"; my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); $$message{add_crc16} = 1; + $message->failure_callback($failure_callback); + $message->success_callback($success_callback); $self->_send_cmd($message); } @@ -976,7 +981,7 @@ sub sync_time { #points such as mode and what not becuase we can't just set the time without #setting these variables too. $$self{sync_time} = 1; - $self->poll_simple(); + $self->_poll_simple(); } =item C From 3de1e9d86982c55cbc9ea76526604adeb645021e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 8 Oct 2013 19:52:24 -0700 Subject: [PATCH 170/330] Insteon_Thermo_i2: Add Request_status Routine --- lib/Insteon/Thermostat.pm | 68 ++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 8f863bd5b..4c50a2f37 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -658,6 +658,57 @@ sub get_status() { return $output; } +=item C + +Prints the currently known status to the log as a text string. + +=cut +sub print_status() { + my ($self) = @_; + my $root = $self->get_root(); + my $output = "[Insteon:Thermo_i2CaS] The status of " . $root->get_object_name . " is:\n"; + $output .= "Mode: "; + $output .= $root->get_mode(); + $output .= "; Status: "; + $output .= "Heating, " if ($$root{heating} eq 'on'); + $output .= "Cooling, " if ($$root{cooling} eq 'on'); + $output .= "Dehumidifying, " if ($$root{high_humid} eq 'on'); + $output .= "Humidifying" if ($$root{low_humid} eq 'on'); + $output .= 'Off' if ($output eq ''); + $output .= "; Temp: "; + $output .= $root->get_temp(); + $output .= "; Humid: "; + $output .= $root->get_humid(); + $output .= "; Heat SP: "; + $output .= $root->get_heat_sp(); + $output .= "; Cool SP: "; + $output .= $root->get_cool_sp(); + $output .= "; High Humid SP: "; + $output .= $root->_high_humid(); + $output .= "; Low Humid SP: "; + $output .= $root->_low_humid(); + ::print_log($output); +} + +=item C + +Returns the current humidity at the thermostat. +=cut +sub get_humid() { + my ($self) = @_; + return $$self{'humid'}; +} + +sub request_status { + my ($self) = @_; + $self = $self->get_root(); + my $self_name = $self->get_object_name; + my $failure_callback = "::print_log('[Insteon:Thermo_i2CS] ERROR: Failed to get status for $self_name.');"; + my $print_callback = $self_name . "->print_status"; + my $humid_callback = $self_name . "->_poll_humid_setpoints(\'$print_callback\', \"$failure_callback\")"; + $self->_poll_simple($humid_callback, $failure_callback); +} + sub _process_message { my ($self,$p_setby,%msg) = @_; my $clear_message = 0; @@ -733,11 +784,8 @@ sub _process_message { #10 = humid high #24 = 1 = Status Report Enabled #12 = firmware #26 = 1 = External Power On #28 = 1 = Int, 2=Ext Temp - main::print_log("[Insteon::Thermo_i2CS] Humidity setpoints for ". - $self->get_object_name . " are High: " . - $self->_high_humid(hex(substr($msg{extra}, 8, 2))) . - " Low: " . $self->_low_humid(hex(substr($msg{extra}, 10, 2))) - ) if $main::Debug{insteon}; + $self->_high_humid(hex(substr($msg{extra}, 8, 2))); + $self->_low_humid(hex(substr($msg{extra}, 10, 2))); $clear_message = 1; $self->_process_command_stack(%msg); } @@ -1022,16 +1070,20 @@ sub low_humid_setpoint { $self->_send_cmd($message); } -=item C +=item C<_poll_humid_setpoints()> Retreives and prints the current humidity high and low setpoints. Only available for I2CS devices. + =cut -sub get_humid_setpoints{ - my ($self) = @_; + +sub _poll_humid_setpoints{ + my ($self, $success_callback, $failure_callback) = @_; my $extra = "00000001"; $extra .= '0' x (30 - length $extra); my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); $$self{_ext_set_get_action} = 'get'; + $message->failure_callback($failure_callback); + $message->success_callback($success_callback); $self->_send_cmd($message); } From 86b5dc16e140cb6b791e6438d0ba35e2de4929b9 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Wed, 9 Oct 2013 02:18:40 -0400 Subject: [PATCH 171/330] Initial addition of Audrey aupport --- code/common/pa_control.pl | 51 +++++++-- lib/Audrey_Play.pm | 65 ++++++++++++ lib/PAobj.pm | 216 +++++++++++++++++++++++++++----------- lib/read_table_A.pl | 10 +- 4 files changed, 265 insertions(+), 77 deletions(-) create mode 100644 lib/Audrey_Play.pm diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index bff9da14b..da32500e7 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -48,8 +48,10 @@ $pactrl->init() if $Startup or $Reload; # Hooks to flag which rooms to turn on based on "rooms=" parm in speak command +&Speak_parms_add_hook(\&pa_parms_stub) if $Reload; &Speak_pre_add_hook(\&pa_control_stub) if $Reload; -&Play_pre_add_hook (\&pa_control_stub) if $Reload; +&Play_parms_add_hook(\&pa_parms_stub) if $Reload; +&Play_pre_add_hook(\&pa_control_stub) if $Reload; if (said $v_pa_test) { my $state = $v_pa_test->{state}; @@ -67,38 +69,65 @@ $pactrl->set('allspeakers',$state,'unmuted'); } -sub pa_control_stub { - my (%parms) = @_; - my @pazones; - my $mode = $parms{mode}; - unless ($mode) { +sub pa_parms_stub { + my ($parms) = @_; + unless ($parms->{mode}) { if (defined $mode_mh) { # *** Outdated (?) - $mode = state $mode_mh; + $parms->{mode} = state $mode_mh; } else { - $mode = $Save{mode}; + $parms->{mode} = $Save{mode}; } } + return if $parms->{mode} eq 'mute' or $parms->{mode} eq 'offline'; + + my $results = $pactrl->prep_parms($parms); + + my %pa_zones = $pactrl->get_pa_zones(); + push(@{$parms->{web_hook}},\&pa_web_hook) if $pa_zones{audrey} ne ''; + + print "PA: parms_stub set results: $results\n" if $Debug{pa} >=2; + +} + +sub pa_control_stub { + my (%parms) = @_; + my @pazones; + my $mode = $parms{mode}; +# unless ($mode) { +# if (defined $mode_mh) { # *** Outdated (?) +# $mode = state $mode_mh; +# } else { +# $mode = $Save{mode}; +# } +# } return if $mode eq 'mute' or $mode eq 'offline'; my $rooms = $parms{rooms}; print "PA: control_stub: rooms=$rooms, mode=$mode\n" if $Debug{pa}; - my $results = $pactrl->set($rooms,ON,$mode,%parms); + my $results = $pactrl->audio_hook(ON,%parms); print "PA: control_stub set results: $results\n" if $Debug{pa} >=2; set $pa_speaker_timer $pa_timer if $results; + return $results; } +sub pa_web_hook { + my (%parms) = @_; + $pactrl->web_hook(\%parms); +} + + #Turn off speakers when MH says it's done speaking/playing if (state_now $mh_speakers eq OFF) { unset $pa_speaker_timer; print "PA: Turning speakers off\n" if $Debug{pa}; - $pactrl->set('allspeakers',OFF,'normal'); + $pactrl->audio_hook(OFF,'normal'); } #Setup Fail-safe speaker shutoff $pa_speaker_timer = new Timer; set $pa_speaker_timer 60 if state_now $mh_speakers eq ON; if (expired $pa_speaker_timer) { - print "PA: Timer expired.\n" if $Debug{pa}; + print "PA: Timer expired. Forcing PA speakers off.\n" if $Debug{pa}; set $mh_speakers OFF; } diff --git a/lib/Audrey_Play.pm b/lib/Audrey_Play.pm new file mode 100644 index 000000000..550739e14 --- /dev/null +++ b/lib/Audrey_Play.pm @@ -0,0 +1,65 @@ +use strict; + +package Audrey_Play; + +=head1 NAME + +B - This object can be used to play sound files on the Audrey. + +=head1 SYNOPSIS + + +blah blah + + + +=head1 DESCRIPTION + +=head1 INHERITS + +B + +=head1 METHODS + +=over + +=cut + +@Audrey_Play::ISA = ('Generic_Item'); + +my $address; + +sub Init { + #&::MainLoop_pre_add_hook( \&Weather_Item::check_weather, 1 ); +} + +=item C + +$ip is the IP address of the Audrey. + +=cut + +sub new { + my ($class, $ip) = @_; + my $self = { }; + $self->{address}=$ip; + + if ($ip) { + &::print_log("Creating Audrey_Play object..."); + } else { + warn 'Empty expression is not allowed.'; + } + + bless $self, $class; + return $self; +} + +sub play { + my ($self,$web_file) = @_; + &::print_log("Called 'play' in Audrey_Play object..."); + my $MHWeb = $::Info{IPAddress_local} . ":" . $::config_parms{http_port}; + &::print_log($MHWeb); + &::run("get_url -quiet http://" . $self->{address} . "/mhspeak.shtml?http://" . $MHWeb . "/" . $web_file . " /dev/null"); +} + +1; \ No newline at end of file diff --git a/lib/PAobj.pm b/lib/PAobj.pm index e0d96cbd9..f75832ffe 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -27,7 +27,7 @@ B use strict; -my (%pa_weeder_max_port,%pa_zone_types,%pa_zone_type_by_zone); +my (%pa_weeder_max_port,%pa_zone_types,%pa_zones); package PAobj; @@ -63,12 +63,12 @@ sub init { $self->check_group('default'); my @speakers = $self->get_speakers('allspeakers'); - my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl); + my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey); for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); my $type = $ref->get_type(); - &::print_log("PAObj: init: room=$room, zonetype=$type"); + &::print_log("PAobj: init: room=$room, zonetype=$type"); $pa_zone_types{$type}++ unless $pa_zone_types{$type}; if($type eq 'wdio') { @@ -86,26 +86,30 @@ sub init { if($type eq 'object') { push(@speakers_obj,$room); } + if($type eq 'audrey') { + push(@speakers_audrey,$room); + } } - &::print_log("PAObj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} || $#speakers_wdio gt -1; - &::print_log("PAObj: speakers_x10: $#speakers_x10") if $main::Debug{pa} || $#speakers_x10 gt -1; - &::print_log("PAObj: speakers_xap: $#speakers_xap") if $main::Debug{pa} || $#speakers_xap gt -1; - &::print_log("PAObj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} || $#speakers_xpl gt -1; - &::print_log("PAObj: speakers_obj: $#speakers_obj") if $main::Debug{pa} || $#speakers_obj gt -1; + &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} || $#speakers_wdio gt -1; + &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa} || $#speakers_x10 gt -1; + &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa} || $#speakers_xap gt -1; + &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} || $#speakers_xpl gt -1; + &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa} || $#speakers_obj gt -1; + &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa} || $#speakers_audrey gt -1; if ($#speakers_wdio > -1) { $self->init_weeder(@speakers_wdio); return 0 unless %pa_weeder_max_port; } if ($pa_zone_types{'x10'}) { - &::print_log("PAObj: x10 PA type initialized...") if $main::Debug{pa}; + &::print_log("PAobj: x10 PA type initialized...") if $main::Debug{pa}; } if ($pa_zone_types{'xap'}) { - &::print_log("PAObj: xAP PA type initialized...") if $main::Debug{pa}; + &::print_log("PAobj: xAP PA type initialized...") if $main::Debug{pa}; } if ($pa_zone_types{'xpl'}) { - &::print_log("PAObj: xPL PA type initialized...") if $main::Debug{pa}; + &::print_log("PAobj: xPL PA type initialized...") if $main::Debug{pa}; } return 1; } @@ -116,7 +120,7 @@ sub init_weeder my (%weeder_ref,%weeder_max); undef %pa_weeder_max_port; for my $room (@speakers) { - &::print_log("PAObj: init PA Room loaded: $room") if $main::Debug{pa}; + &::print_log("PAobj: init PA Room loaded: $room") if $main::Debug{pa}; my $ref = &::get_object_by_name('pa_' . $room . '_obj'); $ref->{state} = 'off'; my ($card,$id); @@ -124,83 +128,146 @@ sub init_weeder $weeder_ref{$card} = '' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; - &::print_log("PAObj: init card: $card, id: $id, Room: $room, List: $weeder_ref{$card}") if $main::Debug{pa}; + &::print_log("PAobj: init card: $card, id: $id, Room: $room, List: $weeder_ref{$card}") if $main::Debug{pa}; } for my $card ('A' .. 'P','a' .. 'p') { if ($weeder_ref{$card}) { my $data = $weeder_ref{$card}; $weeder_max{$card}=$self->last_char($data); - &::print_log("PAObj: init weeder board=$card, ports=$data, max port=" . $weeder_max{$card}) if $main::Debug{pa}; + &::print_log("PAobj: init weeder board=$card, ports=$data, max port=" . $weeder_max{$card}) if $main::Debug{pa}; } } %pa_weeder_max_port = %weeder_max; } -sub set +sub prep_parms { - my ($self,$rooms,$state,$mode,%voiceparms) = @_; - my $results = 0; - &::print_log("PAObj: delay: $$self{pa_delay}\n") if $main::Debug{pa} >=3; - &::print_log("PAObj: set,mode: " . $mode . ",rooms: " . $rooms) if $main::Debug{pa} >=3; + my ($self,$parms) = @_; + #my $self = {}; + &::print_log("PAobj: delay: $$self{pa_delay}\n") if $main::Debug{pa} >=3; + &::print_log("PAobj: set,mode: " . $parms->{mode} . ",rooms: " . $parms->{rooms}) if $main::Debug{pa} >=3; - my @speakers = $self->get_speakers($rooms); + my @speakers = $self->get_speakers($parms->{rooms}); @speakers = $self->get_speakers('') if $#speakers == -1; - &::print_log("PAObj: Proposed rooms: ".join(', ', @speakers)) if $main::Debug{pa} >=2; - @speakers = $self->get_speakers_speakable($mode,@speakers); - &::print_log("PAObj: Will speak in rooms: ".join(', ', @speakers)) if $main::Debug{pa}; - - my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl); + &::print_log("PAobj: Proposed rooms: ".join(', ', @speakers)) if $main::Debug{pa} >=2; + @speakers = $self->get_speakers_speakable($parms->{mode},@speakers); + &::print_log("PAobj: Will speak in rooms: ".join(', ', @speakers)) if $main::Debug{pa}; + + $parms->{pa_zones} = join(',', @speakers); + + my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey); for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); my $type = lc $ref->get_type(); if($type eq 'wdio' || $type eq 'wdio_old') { - &::print_log("PAObj: speakers_wdio: Adding $room") if $main::Debug{pa} >=3; + &::print_log("PAobj: speakers_wdio: Adding $room") if $main::Debug{pa} >=3; push(@speakers_wdio,$room); } if($type eq 'x10') { - &::print_log("PAObj: speakers_x10: Adding $room") if $main::Debug{pa} >=3; + &::print_log("PAobj: speakers_x10: Adding $room") if $main::Debug{pa} >=3; push(@speakers_x10,$room); } if($type eq 'xap') { - &::print_log("PAObj: speakers_xap: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_xap,$room) if $state eq 'on'; #Only need to send if speech is starting + &::print_log("PAobj: speakers_xap: Adding $room") if $main::Debug{pa} >=3; + push(@speakers_xap,$room); } if($type eq 'xpl') { - &::print_log("PAObj: speakers_xpl: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_xpl,$room) if $state eq 'on'; #Only need to send if speech is starting + &::print_log("PAobj: speakers_xpl: Adding $room") if $main::Debug{pa} >=3; + push(@speakers_xpl,$room); } if($type eq 'object') { - &::print_log("PAObj: speakers_object: Adding $room") if $main::Debug{pa} >=3; + &::print_log("PAobj: speakers_object: Adding $room") if $main::Debug{pa} >=3; push(@speakers_obj,$room); } + if($type eq 'audrey') { + &::print_log("PAobj: speakers_audrey: Adding $room") if $main::Debug{pa} >=3; + push(@speakers_audrey,$room); + } } - &::print_log("PAObj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} >=2 || $#speakers_wdio gt -1; - &::print_log("PAObj: speakers_x10: $#speakers_x10") if $main::Debug{pa} >=2 || $#speakers_x10 gt -1; - &::print_log("PAObj: speakers_xap: $#speakers_xap") if $main::Debug{pa} >=2 || $#speakers_xap gt -1; - &::print_log("PAObj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} >=2 || $#speakers_xpl gt -1; - &::print_log("PAObj: speakers_obj: $#speakers_obj") if $main::Debug{pa} >=2 || $#speakers_obj gt -1; + &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} >=2 || $#speakers_wdio gt -1; + &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa} >=2 || $#speakers_x10 gt -1; + &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa} >=2 || $#speakers_xap gt -1; + &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} >=2 || $#speakers_xpl gt -1; + &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa} >=2 || $#speakers_obj gt -1; + &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa} >=2 || $#speakers_audrey gt -1; + + + $pa_zones{wdio}=join(',',@speakers_wdio); + $pa_zones{x10}=join(',',@speakers_x10); + $pa_zones{xap}=join(',',@speakers_xap); + $pa_zones{xpl}=join(',',@speakers_xpl); + $pa_zones{obj}=join(',',@speakers_obj); + $pa_zones{audrey}=join(',',@speakers_audrey); + + $parms->{web_file}="web_file";# if $#speakers_wdio gt -1; + + if( + 1 + && $pa_zones{wdio} eq '' + && $pa_zones{x10} eq '' + && $pa_zones{xap} eq '' + && $pa_zones{xpl} eq '' + && $pa_zones{obj} eq '' + + ) { + $$parms{to_file}='/dev/null'; + } + + return 1; + +} + +sub audio_hook +{ + my ($self,$state,%voiceparms) = @_; + my $results = 0; +# my @speakers = split(',', $voiceparms{pa_zones}); + + my @speakers_wdio=split(',',$pa_zones{wdio}); + my @speakers_x10=split(',',$pa_zones{x10}); + my @speakers_xap=split(',',$pa_zones{xap}); + my @speakers_xpl=split(',',$pa_zones{xpl}); + my @speakers_obj=split(',',$pa_zones{obj}); #TODO: Properly handle $results across multiple types #TODO: Break up the wdio zones based on serial port, in case there are more than one. + $results=0; $results = $self->set_weeder($state,'weeder',@speakers_wdio) if $#speakers_wdio > -1; $results = $self->set_x10($state,@speakers_x10) if $#speakers_x10 > -1; $results = $self->set_xap($state,\@speakers_xap,\%voiceparms) if $#speakers_xap > -1; $results = $self->set_xpl($state,\@speakers_xpl,\%voiceparms) if $#speakers_xpl > -1; $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; - + select undef, undef, undef, $$self{pa_delay} if $results; + &::print_log("PAobj: set results: $results"); + + $results=0; + if($pa_zones{wdio} ne '') {$results=1;print_log('------> wdio detected, talking...');} return $results; } +sub web_hook +{ + my ($self,$parms) = @_; + &::print_log("PAobj: web_hook! Audrey: " . $pa_zones{audrey}); + return unless $pa_zones{audrey} ne ''; + my $results=0; + my @speakers_audrey=split(',', $pa_zones{audrey}); + + $results = $self->set_audrey($parms->{web_file},@speakers_audrey); + + return $results; +} + sub set_obj { my ($self,$state,@speakers) = @_; for my $room (@speakers) { - &::print_log("PAObj: set_obj: " . $room . " / " . $state) if $main::Debug{pa} >=2; + &::print_log("PAobj: set_obj: " . $room . " / " . $state) if $main::Debug{pa} >=2; my $ref = &::get_object_by_name("pa_$room"); if ($ref) { $ref->set($state); @@ -208,18 +275,34 @@ sub set_obj } } +sub set_audrey +{ + my ($self,$speakFile,@speakers) = @_; + &::print_log("PAobj: set_audrey: file: " . $speakFile) if $main::Debug{pa} >=4; + &::print_log("PAobj: set_audrey: count: " . $#speakers) if $main::Debug{pa} >=4; + + for my $room (@speakers) { + #my $ref = &::get_object_by_name('pa_'.$room); + my $refobj = &::get_object_by_name('pa_'.$room.'_obj'); + if ($refobj) { + &::print_log("PAobj: set_audrey: " . $room . " / " . $speakFile) if $main::Debug{pa} >=2; + $refobj->play($speakFile); + } + } +} + sub set_x10 { my ($self,$state,@speakers) = @_; my ($x10_list,$pa_x10_hc,$ref,$refobj); for my $room (@speakers) { - &::print_log("PAObj: set_x10: " . $room . " / " . $state) if $main::Debug{pa} >=3; + &::print_log("PAobj: set_x10: " . $room . " / " . $state) if $main::Debug{pa} >=3; $ref = &::get_object_by_name('pa_'.$room); $refobj = &::get_object_by_name('pa_'.$room.'_obj'); if ($refobj && $ref) { my ($id) = $ref->get_address(); - &::print_log("PAObj: set_x10 ID: $id, State: $state, Room: $room") if $main::Debug{pa} >=2; + &::print_log("PAobj: set_x10 ID: $id, State: $state, Room: $room") if $main::Debug{pa} >=2; $refobj->set($state); } } @@ -231,13 +314,13 @@ sub set_xap { my %voiceparms = %$param2; return unless $#speakers > -1; for my $room (@speakers) { - &::print_log("PAObj: set_xap: " . $room . " / " . $state) if $main::Debug{pa} >=3; + &::print_log("PAobj: set_xap: " . $room . " / " . $state) if $main::Debug{pa} >=3; my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { $ref->send_message($ref->target_address, $ref->class_name => {say => $voiceparms{text}, volume => $voiceparms{volume}, mode => $voiceparms{mode}, voice => $voiceparms{voice} }); - &::print_log("PAObj: xap cmd: $ref->{object_name} is sending voice text: $voiceparms{text}") if $main::Debug{pa}; + &::print_log("PAobj: xap cmd: $ref->{object_name} is sending voice text: $voiceparms{text}") if $main::Debug{pa}; } else { - &::print_log("PAObj: Unable to locate object for: pa_$room"); + &::print_log("PAobj: Unable to locate object for: pa_$room"); } } } @@ -248,7 +331,7 @@ sub set_xpl { my %voiceparms = %$param2; return unless $#speakers > -1; for my $room (@speakers) { - &::print_log("PAObj: set_xpl: " . $room . " / " . $state) if $main::Debug{pa} >=3; + &::print_log("PAobj: set_xpl: " . $room . " / " . $state) if $main::Debug{pa} >=3; my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { my $max_length = $::config_parms{"pa_$room" . "_maxlength"}; @@ -258,9 +341,9 @@ sub set_xpl { $text = substr($text, 0, $max_length) if $max_length < length($text); } $ref->send_cmnd($ref->class_name => {speech => $text, voice => $voiceparms{voice}, volume => $voiceparms{volume}, mode => $voiceparms{mode} }); - &::print_log("PAObj: set_xpl: $ref->{object_name} is sending voice text: $voiceparms{text}") if $main::Debug{pa}; + &::print_log("PAobj: set_xpl: $ref->{object_name} is sending voice text: $voiceparms{text}") if $main::Debug{pa}; } else { - &::print_log("PAObj: Unable to locate object for: pa_$room"); + &::print_log("PAobj: Unable to locate object for: pa_$room"); } } } @@ -272,14 +355,14 @@ sub set_weeder my $weeder_command=''; my $command=''; for my $room (@speakers) { - &::print_log("PAObj: set_weeder: " . $room . " / " . $state) if $main::Debug{pa} >=3; + &::print_log("PAobj: set_weeder: " . $room . " / " . $state) if $main::Debug{pa} >=3; my $ref = &::get_object_by_name('pa_'.$room.'_obj'); if ($ref) { $ref->{state} = $state; my ($card,$id) = $ref->{id_by_state}{'on'} =~ /^D?(.)H(.)/s; $weeder_ref{$card}='' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; - &::print_log("PAObj: card: $card, id: $id, Room: $room") if $main::Debug{pa} >=2; + &::print_log("PAobj: card: $card, id: $id, Room: $room") if $main::Debug{pa} >=2; } } @@ -294,7 +377,7 @@ sub set_weeder } } return 0 unless $weeder_command; - &::print_log("PAObj: Sending $weeder_command to the weeder card(s)") if $main::Debug{pa}; + &::print_log("PAobj: Sending $weeder_command to the weeder card(s)") if $main::Debug{pa}; $weeder_command =~ s/\\r/\r/g; &Serial_Item::send_serial_data($weeder_port, $weeder_command) if $main::Serial_Ports{$weeder_port}{object}; return 1; @@ -323,7 +406,7 @@ sub get_weeder_string } $bit_flag = ($state eq 'on') ? 1 : 0; # get 0 or 1 - &::print_log("PAObj: get_weeder_string card: $card, bit=$bit state=$bit_flag") if $main::Debug{pa} >=2; + &::print_log("PAobj: get_weeder_string card: $card, bit=$bit state=$bit_flag") if $main::Debug{pa} >=2; $byte_code += ($bit_flag << $bit_counter); # get bit in byte position if ($bit_counter++ >= 3) { @@ -351,7 +434,7 @@ sub get_speakers my ($self,$rooms) = @_; my @pazones; - &::print_log("PAObj: get_speakers,rooms: " . $rooms) if $main::Debug{pa} >=2; + &::print_log("PAobj: get_speakers,rooms: " . $rooms) if $main::Debug{pa} >=2; if ($::mh_speakers->{rooms}) { $rooms = $::mh_speakers->{rooms}; $::mh_speakers->{rooms} = ''; @@ -363,22 +446,22 @@ sub get_speakers no strict 'refs'; my $ref = &::get_object_by_name("pa_$room"); if ($ref) { - &::print_log("PAObj: name=$ref->{object_name}") if $main::Debug{pa}; + &::print_log("PAobj: name=$ref->{object_name}") if $main::Debug{pa}; if (UNIVERSAL::isa($ref,'Group')) { - &::print_log("PAObj: It's a group!") if $main::Debug{pa} >=2; + &::print_log("PAobj: It's a group!") if $main::Debug{pa} >=2; for my $grouproom ($ref->list) { $grouproom = $grouproom->get_object_name; $grouproom =~ s/^\$pa_//; $grouproom =~ s/^\$paxpl_//; $grouproom =~ s/^\$paxap_//; - &::print_log("PAObj: - member: $grouproom\n") if $main::Debug{pa} >=2; + &::print_log("PAobj: - member: $grouproom") if $main::Debug{pa} >=2; push(@pazones, $grouproom); } } else { push(@pazones, $room); } } else { - &::print_log("PAObj: WARNING: PA zone of '$room' not found!"); + &::print_log("PAobj: WARNING: PA zone of '$room' not found!"); } } return @pazones; @@ -387,13 +470,13 @@ sub get_speakers sub check_group { my ($self,$group) = @_; - &::print_log("PAObj: check group=$group") if $main::Debug{pa} >=2; + &::print_log("PAobj: check group=$group") if $main::Debug{pa} >=2; my $ref = &::get_object_by_name("pa_$group"); - if (!$ref) {&::print_log("PAObj: check group: Error! Group does not exist: $group"); return;} + if (!$ref) {&::print_log("PAobj: check group: Error! Group does not exist: $group"); return;} my @list = $ref->list; - &::print_log("PAObj: check group=$group, list=$#list") if $main::Debug{pa} >=2; + &::print_log("PAobj: check group=$group, list=$#list") if $main::Debug{pa} >=2; if ($#list == -1) { - &::print_log("PAObj: check populating group: $group!") if $main::Debug{pa}; + &::print_log("PAobj: check populating group: $group!") if $main::Debug{pa}; for my $room ($self->get_speakers('allspeakers')) { my $ref2 = &::get_object_by_name("pa_$room"); $ref->add($ref2); @@ -411,19 +494,26 @@ sub get_speakers_speakable for my $room (@zones) { my $ref = &::get_object_by_name("pa_$room"); - &::print_log("PAObj: speakable: name=$ref->{object_name}") if $main::Debug{pa} >=3; + &::print_log("PAobj: speakable: name=$ref->{object_name}") if $main::Debug{pa} >=3; if ($ref->{sleeping} == 0) { $ref->{mode} = 'normal' unless $ref->{mode}; my $gss_mode = $ref->{mode}; if ($gss_mode ne 'sleeping' && ($gss_mode eq 'normal' || $mode eq 'unmuted')) { push(@pazones,$room); - &::print_log("PAObj: speakable: Pushing $room into pazones array:$#pazones") if $main::Debug{pa} >=2; + &::print_log("PAobj: speakable: Pushing $room into pazones array:$#pazones") if $main::Debug{pa} >=2; } } } return @pazones; } +sub get_pa_zones +{ + my ($self) = @_; + &::print_log("PAobj: get_pa_zones");# if $main::Debug{pa} >=3; + return %pa_zones; +} + sub set_delay { my ($self,$delay) = @_; @@ -439,7 +529,7 @@ sub print_speaker_states $ref = &::get_object_by_name("pa_$speaker"); $room = $ref->{object_name}; $room =~ s/^\$pa_//; - &::print_log("PAObj: name=$room, state=$ref->{state}") if $main::Debug{pa}; + &::print_log("PAobj: name=$room, state=$ref->{state}") if $main::Debug{pa}; } } diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 0d197677e..f924d1343 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -22,12 +22,12 @@ sub read_table_init_A { %groups=(); %objects=(); %packages=(); - %addresses=(); + %addresses=(); } sub read_table_A { my ($record) = @_; - + if($record =~ /^#/ or $record =~ /^\s*$/) { return; } @@ -551,6 +551,10 @@ sub read_table_A { $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n",$name,$address; $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n",$name,$address; } + } elsif (lc $pa_type eq 'audrey') { + require 'Audrey_Play.pm'; + $code .= sprintf "\$%-35s = new Audrey_Play('%s');\n",$name.'_obj',$address; + $code .= sprintf "\$%-35s -> hidden(1);\n", $name.'_obj'; } elsif (lc $pa_type eq 'x10') { $other = join ', ', (map {"'$_'"} @other); # Quote data $code .= sprintf "\$%-35s = new X10_Appliance('%s','%s');\n",$name.'_obj',$address, $serial; @@ -1130,4 +1134,4 @@ sub read_table_A { # Revision 1.3 2000/10/01 23:29:40 winter # - 2.29 release # -# +# \ No newline at end of file From a970d64da40c8da0f015ffb779e857effe021bfe Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 172/330] Insteon_Thermo_i2: Fix Typos in Comments --- lib/Insteon/Thermostat.pm | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 4c50a2f37..61ab1dc21 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -80,7 +80,7 @@ the EF group as described above and run sync links. Broadcast messages are NOT sent when the heater turns on/off. Broadcast message are also NOT sent when the humidity setpoints are exceeded. Instead, -you must define the heating, high_humdid, and _low_humid groups and link them +you must define the heating, high_humid, and low_humid groups and link them to MH. (The base group 01 is the cooling group and should always be linked to MH). When linked, these groups will send on/off commands to MH when these events occur. Alternatively, you can periodically call request_status() to check @@ -116,17 +116,20 @@ thermostat can be created in user code: $thermo_setpoint_c = new Insteon::Thermo_setpoint_c($thermostat); $thermo_humidity = new Insteon::Thermo_humidity($thermostat); #Only available on i2CS devices $thermo_status = new Insteon::Thermo_status($thermostat); #Only available on i2CS devices + $thermo_humidity_setpoint_h = new Insteon::Thermo_setpoint_humid_h($thermostat); #Only available on i2CS devices + $thermo_humidity_setpoint_l = new Insteon::Thermo_setpoint_humid_l($thermostat); #Only available on i2CS devices + where $thermostat is the parent object to track. The state of these child objects -will be the state of the various objects. This makes the display of the various -states easier within MH. The child objects also make it easier to change the -various states on the thermostat. +will be the state of the various attributes of the thermostat. This makes the +display of the various states easier within MH. The child objects also make it +easier to change the various states on the thermostat. see code/examples/Insteon_thermostat.pl for more. =head1 BUGS -This code has not been tested on older Venstar thermsotats, however it is believed +This code has not been tested on older Venstar thermostats, however it is believed that the basic functionality should work as it did in the old code. =head1 AUTHOR From 3fa31ea29413d6046ff9f055f0573c7758690cb1 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 173/330] Insteon_thermo_i2: Add Humidity Setpoint Child Objects --- lib/Insteon/Thermostat.pm | 88 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 61ab1dc21..58167fa9d 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -469,6 +469,12 @@ sub parent_event { elsif ($p_state eq 'status_change'){ $$self{child_status}->set_receive($self->get_status(), $self); } + elsif ($p_state eq 'low_humid_setpoint_change'){ + $$self{child_setpoint_humid_l}->set_receive($self->get_low_humid_sp(), $self); + } + elsif ($p_state eq 'high_humid_setpoint_change'){ + $$self{child_setpoint_humid_h}->set_receive($self->get_high_humid_sp(), $self); + } } # Overload methods we don't use, but would otherwise cause Insteon traffic. @@ -1304,6 +1310,88 @@ sub set_receive { $self->SUPER::set($p_state); } +package Insteon::Thermo_setpoint_humid_h; +use strict; + +@Insteon::Thermo_setpoint_humid_h::ISA = ('Generic_Item'); + +sub new { + my ($class, $parent) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{parent} = $parent; + @{$$self{states}} = ('Lower' , 'Higher'); + $$self{parent}{child_setpoint_humid_h} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "high_humid_setpoint_change"); + return $self; +} + +sub set { + my ($self, $p_state, $p_setby, $p_response) = @_; + my $found_state = 0; + foreach my $test_state (@{$$self{states}}){ + if (lc($test_state) eq lc($p_state)){ + $found_state = 1; + } + } + if ($found_state){ + ::print_log("[Insteon::Thermo_i2CS] Received request to set high humidity setpoint to " + . $p_state . " for device " . $self->get_object_name); + if (lc($p_state) eq 'lower'){ + $$self{parent}->high_humid_setpoint($$self{parent}->get_high_humid_sp - 1); + } + elsif (lc($p_state) eq 'higher'){ + $$self{parent}->high_humid_setpoint($$self{parent}->get_high_humid_sp + 1); + } + } +} + +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} + +package Insteon::Thermo_setpoint_humid_l; +use strict; + +@Insteon::Thermo_setpoint_humid_l::ISA = ('Generic_Item'); + +sub new { + my ($class, $parent) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{parent} = $parent; + @{$$self{states}} = ('Lower', 'Higher'); + $$self{parent}{child_setpoint_humid_l} = $self; + $$self{parent} -> tie_event ('$object->parent_event("$state")', "low_humid_setpoint_change"); + return $self; +} + +sub set { + my ($self, $p_state, $p_setby, $p_response) = @_; + my $found_state = 0; + foreach my $test_state (@{$$self{states}}){ + if (lc($test_state) eq lc($p_state)){ + $found_state = 1; + } + } + if ($found_state){ + ::print_log("[Insteon::Thermo_i2CS] Received request to set low humidity setpoint to " + . $p_state . " for device " . $self->get_object_name); + if (lc($p_state) eq 'lower'){ + $$self{parent}->low_humid_setpoint($$self{parent}->get_low_humid_sp - 1); + } + elsif (lc($p_state) eq 'higher'){ + $$self{parent}->low_humid_setpoint($$self{parent}->get_low_humid_sp + 1); + } + } +} + +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} + 1; =back From ba1831fc3c58c9b924e01a5d1fd507436b4a1396 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 174/330] Insteon_Thermo_i2: Change Name of (De)Humidifying Functions to Match Nomenclature Heating, Cooling, ... should be humidifying and the like --- lib/Insteon/Thermostat.pm | 47 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 58167fa9d..34d7200b3 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -584,7 +584,7 @@ sub init { my ($self) = @_; $$self{message_types} = \%message_types; #Set saved state unique to i2CS devices - $self->restore_data('humid', 'cooling', 'heating', 'high_humid', 'low_humid'); + $self->restore_data('humid', 'cooling', 'heating', 'humidifying', 'dehumidifying', 'high_humid_sp', 'low_humid_sp'); } sub set { @@ -605,10 +605,10 @@ sub set { $root->_heating($link_state); } elsif ($self->group eq '03') { - $root->_high_humid($link_state); + $root->_dehumidifying($link_state); } elsif ($self->group eq '04') { - $root->_low_humid($link_state); + $root->_humidifying($link_state); } #Update the status of linked devices $self->set_linked_devices($link_state); @@ -661,8 +661,8 @@ sub get_status() { my $output = ""; $output .= "Heating, " if ($$root{heating} eq 'on'); $output .= "Cooling, " if ($$root{cooling} eq 'on'); - $output .= "Dehumidifying, " if ($$root{high_humid} eq 'on'); - $output .= "Humidifying" if ($$root{low_humid} eq 'on'); + $output .= "Dehumidifying, " if ($$root{dehumidifying} eq 'on'); + $output .= "Humidifying" if ($$root{humidifying} eq 'on'); $output = 'Off' if ($output eq ''); return $output; } @@ -679,11 +679,13 @@ sub print_status() { $output .= "Mode: "; $output .= $root->get_mode(); $output .= "; Status: "; - $output .= "Heating, " if ($$root{heating} eq 'on'); - $output .= "Cooling, " if ($$root{cooling} eq 'on'); - $output .= "Dehumidifying, " if ($$root{high_humid} eq 'on'); - $output .= "Humidifying" if ($$root{low_humid} eq 'on'); - $output .= 'Off' if ($output eq ''); + my $output_status = ''; + $output_status .= "Heating, " if ($$root{heating} eq 'on'); + $output_status .= "Cooling, " if ($$root{cooling} eq 'on'); + $output_status .= "Dehumidifying, " if ($$root{dehumidifying} eq 'on'); + $output_status .= "Humidifying" if ($$root{humidifying} eq 'on'); + $output_status .= 'Off' if ($output_status eq ''); + $output .= $output_status; $output .= "; Temp: "; $output .= $root->get_temp(); $output .= "; Humid: "; @@ -972,22 +974,31 @@ sub _heating { } -sub _high_humid { +sub _dehumidifying { my ($self,$p_state) = @_; - if ($p_state ne $$self{high_humid}) { - $$self{high_humid} = $p_state; + if ($p_state ne $$self{dehumidifying}) { + $$self{dehumidifying} = $p_state; $self->set_receive('status_change'); } - return $$self{high_humid}; + return $$self{dehumidifying}; } -sub _low_humid { +sub _humidifying { my ($self,$p_state) = @_; - if ($p_state ne $$self{low_humid}) { - $$self{low_humid} = $p_state; + if ($p_state ne $$self{humidifying}) { + $$self{humidifying} = $p_state; $self->set_receive('status_change'); } - return $$self{low_humid}; + return $$self{humidifying}; +} + +sub _high_humid_sp { + my ($self,$p_state) = @_; + if ($p_state ne $$self{high_humid_sp}) { + $$self{high_humid_sp} = $p_state; + $self->set_receive('high_humid_setpoint_change'); + } + return $$self{high_humid_sp}; } =item C From a7d18a21fbe8acf5deb79c9a1a24499efe0a7518 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 175/330] Insteon_Thermo_i2: Use Simple_Message Whenever Possible --- lib/Insteon/Thermostat.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 34d7200b3..4f6457d55 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -621,8 +621,8 @@ sub sync_links{ if (!$audit_mode && ref $bcast_obj && $self->is_root){ #Make sure thermostat is set to broadcast changes ::print_log("[Insteon::Thermo_i2CS] (sync_links) Enabling thermostat broadcast setting.") unless $audit_mode; - my $extra = "000008000000000000000000000000"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + my $extra = "000008"; + my $message = $self->simple_message('extended_set_get', $extra); $$self{_ext_set_get_action} = 'set'; $self->_send_cmd($message); } @@ -640,8 +640,8 @@ Only available for I2CS devices. sub _poll_simple{ my ($self, $success_callback, $failure_callback) = @_; - my $extra = "020000000000000000000000000000"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + my $extra = "02"; + my $message = $self->simple_message('extended_set_get', $extra); $$message{add_crc16} = 1; $message->failure_callback($failure_callback); $message->success_callback($success_callback); From 3aa224a5c7d7e1fe3de3e7f7aaa9c53acb2838e9 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 176/330] Insteon_Thermo_i2: Fix Typo in Debug Log Entry --- lib/Insteon/Thermostat.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 4f6457d55..56ab41bd6 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -675,7 +675,7 @@ Prints the currently known status to the log as a text string. sub print_status() { my ($self) = @_; my $root = $self->get_root(); - my $output = "[Insteon:Thermo_i2CaS] The status of " . $root->get_object_name . " is:\n"; + my $output = "[Insteon:Thermo_i2CS] The status of " . $root->get_object_name . " is:\n"; $output .= "Mode: "; $output .= $root->get_mode(); $output .= "; Status: "; From 0680d3914b8b80fa555afe5f0755808a69a57917 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 177/330] Insteon_Thermo_i2: Create Get_Humid_Setpoint Functions --- lib/Insteon/Thermostat.pm | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 56ab41bd6..5c150f71f 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -695,9 +695,9 @@ sub print_status() { $output .= "; Cool SP: "; $output .= $root->get_cool_sp(); $output .= "; High Humid SP: "; - $output .= $root->_high_humid(); + $output .= $root->get_high_humid_sp(); $output .= "; Low Humid SP: "; - $output .= $root->_low_humid(); + $output .= $root->get_low_humid_sp(); ::print_log($output); } @@ -1001,6 +1001,34 @@ sub _high_humid_sp { return $$self{high_humid_sp}; } +sub _low_humid_sp { + my ($self,$p_state) = @_; + if ($p_state ne $$self{low_humid_sp}) { + $$self{low_humid_sp} = $p_state; + $self->set_receive('low_humid_setpoint_change'); + } + return $$self{low_humid_sp}; +} + +=item C + +Returns the current high humidity setpoint. +=cut +sub get_high_humid_sp { + my ($self) = @_; + return $$self{high_humid_sp}; +} + +=item C + +Returns the current low humidity setpoint. +=cut +sub get_low_humid_sp { + my ($self) = @_; + return $$self{low_humid_sp}; +} + + =item C Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', From 0e48368743813ae3d6be165264656d9eaa3f9224 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 178/330] Insteon_Thermo_i2: Manually Set (De)Humidifying States on Request Status There does not appear to be any request which can be made to the device to get the status of the Humidifying or Dehumidifying group (perhaps a direct request to each device->group would work) But in order to make sure that the two groups are in the proper state after a call to request_status we just manually set them inside the code --- lib/Insteon/Thermostat.pm | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 5c150f71f..fdaf83549 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -795,8 +795,23 @@ sub _process_message { #10 = humid high #24 = 1 = Status Report Enabled #12 = firmware #26 = 1 = External Power On #28 = 1 = Int, 2=Ext Temp - $self->_high_humid(hex(substr($msg{extra}, 8, 2))); - $self->_low_humid(hex(substr($msg{extra}, 10, 2))); + $self->_high_humid_sp(hex(substr($msg{extra}, 8, 2))); + $self->_low_humid_sp(hex(substr($msg{extra}, 10, 2))); + + #Humidifying and Dehumidifying are only reported by the + #thermostat as scene-commands. When a user calls + #request_status, we manually check the values and update + #as appropriate + if ($self->get_high_humid_sp > $self->get_humid){ + $self->_dehumidifying('off'); + } else { + $self->_dehumidifying('on'); + } + if ($self->get_low_humid_sp < $self->get_humid){ + $self->_humidifying('off'); + } else { + $self->_humidifying('on'); + } $clear_message = 1; $self->_process_command_stack(%msg); } From 95597cbe160ef4683eb8442f9889dfdecaf369b5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 179/330] Insteon_Thermo_i2: Set Humidity Setpoint Pending States on Extended Message ACK --- lib/Insteon/Thermostat.pm | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index fdaf83549..296ad1ad0 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -740,6 +740,24 @@ sub _process_message { $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); } + elsif ($$self{_ext_set_get_action} eq 'set_high_humid'){ + main::print_log("[Insteon::Thermostat] Received ACK of high humid setpoint ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->_high_humid_sp($$self{_high_humid_pending}); + $clear_message = 1; + $$self{_ext_set_get_action} = undef; + $$self{_high_humid_pending} = undef; + $self->_process_command_stack(%msg); + } + elsif ($$self{_ext_set_get_action} eq 'set_low_humid'){ + main::print_log("[Insteon::Thermostat] Received ACK of low humid setpoint ". + "for ". $self->get_object_name) if $main::Debug{insteon}; + $self->_low_humid_sp($$self{_low_humid_pending}); + $clear_message = 1; + $$self{_ext_set_get_action} = undef; + $$self{_low_humid_pending} = undef; + $self->_process_command_stack(%msg); + } } elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { if (substr($msg{extra},0,4) eq "0201") { From 47aeab1e398033d358d19af9ee8528133edaded8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 180/330] Insteon_Thermo_i2: Humidity Setpoints must be in range of [1-99], Set Pending Flag --- lib/Insteon/Thermostat.pm | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 296ad1ad0..8c9109817 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -1122,13 +1122,18 @@ sub high_humid_setpoint { my ($self, $value) = @_; main::print_log("[Insteon::Thermo_i2CS] Setting high humid setpoint -> $value") if $main::Debug{insteon}; if($value !~ /^\d+$/){ - main::print_log("[Insteon::Thermostat] ERROR: Setpoint $value not numeric"); + main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint $value not numeric"); return; } + if($value > 99 || $value < 1){ + main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint must be between 1-99, not $value"); + return; + } my $extra = "00000B" . sprintf("%02x", $value); $extra .= '0' x (30 - length $extra); my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); - $$self{_ext_set_get_action} = 'set'; + $$self{_ext_set_get_action} = 'set_high_humid'; + $$self{_high_humid_pending} = $value; $self->_send_cmd($message); } @@ -1141,13 +1146,18 @@ sub low_humid_setpoint { my ($self, $value) = @_; main::print_log("[Insteon::Thermo_i2CS] Setting low humid setpoint -> $value") if $main::Debug{insteon}; if($value !~ /^\d+$/){ - main::print_log("[Insteon::Thermostat] ERROR: Setpoint $value not numeric"); + main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint $value not numeric"); return; } + if($value > 99 || $value < 1){ + main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint must be between 1-99, not $value"); + return; + } my $extra = "00000C" . sprintf("%02x", $value); $extra .= '0' x (30 - length $extra); my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); - $$self{_ext_set_get_action} = 'set'; + $$self{_ext_set_get_action} = 'set_low_humid'; + $$self{_low_humid_pending} = $value; $self->_send_cmd($message); } From 503dc19fee3c77092891d966c4ade9bf27aa5137 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 181/330] Fix Nabble Links in MailList Page Closes #230 --- docs/maillist.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/maillist.html b/docs/maillist.html index 5d026fb5a..0e83c4e54 100644 --- a/docs/maillist.html +++ b/docs/maillist.html @@ -25,8 +25,8 @@

    Mailing list info

    or use the
    Gmane web interface -
  • Nabler also has nice web interface -as well as a RSS feed +
  • Nabler also has nice web interface +as well as a RSS feed
  • We also have an IRC channel, details on the Wiki From d63e458a42402092798aba8fd9015621f4eaf811 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 17:37:00 -0700 Subject: [PATCH 182/330] Insteon_i2CS_Linking: Add Success_Callback to Extended Messages --- lib/Insteon/BaseInterface.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index e6c1fd906..a461b284a 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -718,6 +718,14 @@ sub on_extended_insteon_received } &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $main::Debug{insteon} >=3; if($object->_process_message($self, %msg)) { + if ($self->active_message->success_callback){ + main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " + . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; + package main; + eval $self->active_message->success_callback; + ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; + package Insteon::BaseInterface; + } $self->clear_active_message(); } } From dd130c693cfb1fdf8fc58446b8489b3fe9031802 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 18:30:21 -0700 Subject: [PATCH 183/330] Change Referenced to Web Cache Directory to html_alias_cache html_alias_cache is defined as data_dir/cache by default. Most installations will only require the data_dir to be defined, only those with special installations would need the cache dir to be outside the data_dir. Closes #229 --- bin/mh | 4 ++-- code/common/mh_control.pl | 8 +++----- code/public/whole_house_audio_speech.pl | 4 ++-- lib/Voice_Text.pm | 2 +- web/bin/ListManager.pl | 2 +- web/bin/button.pl | 18 +++++++++--------- web/bin/button2.pl | 18 +++++++++--------- web/bin/resizephoto.pl | 2 +- web/cache/empty_file.txt | 5 ----- 9 files changed, 28 insertions(+), 35 deletions(-) delete mode 100644 web/cache/empty_file.txt diff --git a/bin/mh b/bin/mh index 40fe04342..e85103685 100755 --- a/bin/mh +++ b/bin/mh @@ -736,7 +736,7 @@ sub setup { # Make various directories, if missing mkdir ("$config_parms{data_dir}/logs", 0777) unless -d "$config_parms{data_dir}/logs"; mkdir ("$config_parms{data_dir}/web", 0777) unless -d "$config_parms{data_dir}/web"; - mkdir ("$config_parms{data_dir}/cache", 0777) unless -d "$config_parms{data_dir}/cache"; + mkdir ("$config_parms{html_alias_cache}", 0777) unless -d "$config_parms{html_alias_cache}"; mkdir ("$config_parms{html_dir}/tv", 0777) unless -d "$config_parms{html_dir}/tv"; mkdir ("$config_parms{html_dir}/tv/clicktv", 0777) unless -d "$config_parms{html_dir}/tv/clicktv"; @@ -3819,7 +3819,7 @@ sub play { # Let the speak code push the wav file # For speak (TTS), this gets done in Voice_Text.pm if ($parms{address}) { - copy $file, "$config_parms{data_dir}/cache/speak_address.$Second.wav"; + copy $file, "$config_parms{html_alias_cache}/speak_address.$Second.wav"; for my $address (split ',', $parms{address}) { my $address_code = $config_parms{voice_text_address_code}; $address_code =~ s|\$address|$address|; diff --git a/code/common/mh_control.pl b/code/common/mh_control.pl index 6117eea36..211f6430f 100644 --- a/code/common/mh_control.pl +++ b/code/common/mh_control.pl @@ -529,14 +529,12 @@ () # Clear the web cache directory $v_clear_cache = new Voice_Cmd 'Clear the web cache directory', ''; $v_clear_cache->set_info( - 'Delete all the auto-generated .jpg files in mh/web/cache'); + 'Delete all the auto-generated .jpg files in html_alias_cache directory'); $v_clear_cache->tie_event('&handle_clear_cache_state()'); # noloop sub handle_clear_cache_state() { my $cmd = ($OS_win) ? 'del' : 'rm'; - $cmd .= " $config_parms{html_dir}/cache/*.jpg"; - $cmd =~ s|/|\\|g if $OS_win; - system $cmd; - $cmd .= " $config_parms{html_dir}/cache/*.wav"; + $cmd .= " $config_parms{html_alias_cache}/*.jpg"; + $cmd .= " $config_parms{html_alias_cache}/*.wav"; $cmd =~ s|/|\\|g if $OS_win; system $cmd; print_log "Ran: $cmd"; diff --git a/code/public/whole_house_audio_speech.pl b/code/public/whole_house_audio_speech.pl index 64b24e00c..638a872db 100644 --- a/code/public/whole_house_audio_speech.pl +++ b/code/public/whole_house_audio_speech.pl @@ -254,7 +254,7 @@ sub pre_speak_hook { print_log "Speech: player string is: $player_str"; if ($player_str) { $SpeechCount++; - $parms->{'to_file'} = "$config_parms{data_dir}/cache/speak_festival.${Hour}.${Minute}.${Second}.${SpeechCount}.wav"; + $parms->{'to_file'} = "$config_parms{html_alias_cache}/speak_festival.${Hour}.${Minute}.${Second}.${SpeechCount}.wav"; $parms->{'use_players'} = $player_str; } else { $parms->{'no_speak'} = 1; @@ -282,7 +282,7 @@ sub post_speak_hook { ##################################################################### if ($New_Hour) { - system('find', "$config_parms{data_dir}/cache", '-mmin', '+5', '-exec', 'rm', '-f', '{}', ';'); + system('find', "$config_parms{html_alias_cache}", '-mmin', '+5', '-exec', 'rm', '-f', '{}', ';'); } if ($Reload) { diff --git a/lib/Voice_Text.pm b/lib/Voice_Text.pm index bf515a4b1..8c1002e73 100644 --- a/lib/Voice_Text.pm +++ b/lib/Voice_Text.pm @@ -134,7 +134,7 @@ sub speak_text { if ($parms{address}) { my @address = split ',', $parms{address}; delete $parms{address}; - $parms{to_file} = "$main::config_parms{html_dir}/cache/speak_address.$main::Second.wav"; + $parms{to_file} = "$main::config_parms{html_alias_cache}/speak_address.$main::Second.wav"; &speak_text(%parms); package main; # So the we do not have to use $main:: diff --git a/web/bin/ListManager.pl b/web/bin/ListManager.pl index 51961b2df..0b61d38b9 100644 --- a/web/bin/ListManager.pl +++ b/web/bin/ListManager.pl @@ -1298,7 +1298,7 @@ =head1 Credits and contact information # ======================== POD END ================================== ]; - open DOC, "echo \"$POD\" | pod2html --cachedir=$config_parms{data_dir}/cache --flush 2>/dev/null |"; + open DOC, "echo \"$POD\" | pod2html --cachedir=$config_parms{html_alias_cache} --flush 2>/dev/null |"; my $content = 0; while () { $content = 1 if /transparent($white); # Write out a copy to the cache -print "Writing image to cache: $config_parms{data_dir}$image_file\n"; +print "Writing image to cache: $config_parms{html_alias_cache}$image_file\n"; my $jpeg = $image->jpeg; -file_write "$config_parms{data_dir}$image_file", $jpeg; +file_write "$config_parms{html_alias_cache}$image_file", $jpeg; -return $image_file if $file_name_only; -return &mime_header($image_file, 1, length $jpeg) . $jpeg; +return "/cache".$image_file if $file_name_only; +return &mime_header("/cache".$image_file, 1, length $jpeg) . $jpeg; diff --git a/web/bin/button2.pl b/web/bin/button2.pl index da5cda189..270125887 100644 --- a/web/bin/button2.pl +++ b/web/bin/button2.pl @@ -78,15 +78,15 @@ $ImageFile =~ s/^\$//; # Drop leading blank on object name $ImageFile =~ s/ *$//; # Drop trailing blanks $ImageFile =~ s/ /_/g; # Blanks in file names are nasty -$ImageFile = "/cache/$ImageFile.$ButtonType"; +$ImageFile = "/$ImageFile.$ButtonType"; print "$ScriptName: Cache file should be $config_parms{data_dir}/$ImageFile\n" if $Debug{$ScriptName}; # We hit the cache, so we give back the image and exit -if ( -f "$config_parms{data_dir}/$ImageFile" ) { - print "$ScriptName: Hit cached file $config_parms{data_dir}/$ImageFile\n" if $Debug{$ScriptName}; - my $data = file_read("$config_parms{data_dir}$ImageFile"); - return &mime_header( $ImageFile, 1, length $data ) . $data; +if ( -f "$config_parms{html_alias_cache}/$ImageFile" ) { + print "$ScriptName: Hit cached file $config_parms{html_alias_cache}/$ImageFile\n" if $Debug{$ScriptName}; + my $data = file_read("$config_parms{html_alias_cache}$ImageFile"); + return &mime_header( "/cache".$ImageFile, 1, length $data ) . $data; } print "$ScriptName: Cache file not found\n" if $Debug{$ScriptName}; @@ -306,13 +306,13 @@ } my $ButtonFile = $GDTemplate->$ButtonType(); if ( $ButtonOK ) { - print "$ScriptName: Writing button to cache: $config_parms{data_dir}/$ImageFile\n" if $Debug{$ScriptName}; - file_write( "$config_parms{data_dir}/$ImageFile", $ButtonFile ); + print "$ScriptName: Writing button to cache: $config_parms{html_alias_cache}/$ImageFile\n" if $Debug{$ScriptName}; + file_write( "$config_parms{html_alias_cache}/$ImageFile", $ButtonFile ); } else { - print "$ScriptName: Button $config_parms{data_dir}/$ImageFile not written to cache\n"; + print "$ScriptName: Button $config_parms{html_alias_cache}/$ImageFile not written to cache\n"; } - return &mime_header( $ImageFile, 1, length $ButtonFile ) . $ButtonFile; + return &mime_header( "/cache".$ImageFile, 1, length $ButtonFile ) . $ButtonFile; } else { print "$ScriptName: Error generating image\n"; diff --git a/web/bin/resizephoto.pl b/web/bin/resizephoto.pl index 8f81c03fb..dc5c97492 100644 --- a/web/bin/resizephoto.pl +++ b/web/bin/resizephoto.pl @@ -20,7 +20,7 @@ my $img; my $nocache = 0; #$nocache = 1; -$image_file = "$config_parms{data_dir}/cache/$image_file.jpg"; +$image_file = "$config_parms{html_alias_cache}/$image_file.jpg"; unless (-e "$image_file" or $nocache) { $url = $config_parms{html_alias_photos} .$url; my $image = Image::Resize->new($url); diff --git a/web/cache/empty_file.txt b/web/cache/empty_file.txt deleted file mode 100644 index ef89395af..000000000 --- a/web/cache/empty_file.txt +++ /dev/null @@ -1,5 +0,0 @@ - -This is an empty file :) - -Put here to make sure unzip programs will re-created this directory. - From 0098ae6d48219175bbdb51dddc8c58ccbbc2a88a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 18:47:40 -0700 Subject: [PATCH 184/330] Insteon_Thermo_i2: Remove Extra Space --- lib/Insteon_PLM.pm | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 70688b02b..dd6315f74 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -402,7 +402,6 @@ sub _send_cmd { $self->_set_timeout('command', $cmd_timeout); # a commmand needs to be PLM ack'd w/i 3 seconds or it gets dropped } } - my $is_extended = ($message->can('command_type') && $message->command_type eq "insteon_ext_send") ? 1 : 0; if (length($command) != (Insteon::MessageDecoder::insteon_cmd_len(substr($command,0,4), 0, $is_extended)*2)){ &::print_log( "[Insteon_PLM]: ERROR!! Command sent to PLM " . lc($command) From 8cbe4a9c002c7dd6322fb53efef8ba140c288815 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 9 Oct 2013 19:41:50 -0700 Subject: [PATCH 185/330] Insteon_TriggerLinc: Add Initial Support for TriggerLinc Devices Basically just a copy of code for RemoteLincs, however, TriggerLincs support even fewer commands so much of the code was cut down. I put the code in the Security.pm file as these are ostensible security devices, however as noted above, these are very similar to RemoteLincs. Although, they are not that dissimilar from Motion sensors which are also in this file. I don't own a TriggerLinc so this coding is all done blind. Thanks to @JaredF for his testing work. Closes #245 --- lib/Insteon/Security.pm | 216 +++++++++++++++++++++++++++++++++++++++- lib/read_table_A.pl | 6 ++ 2 files changed, 221 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index ede4466e3..fe97e4014 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -703,5 +703,219 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY 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 B + +=head2 SYNOPSIS + +Configuration: + +In user code: + + use Insteon::TriggerLinc; + $trigger = new Insteon::TriggerLinc('12.34.56:01',$myPLM); + +In items.mht: + + INSTEON_TRIGGERLINC, 12.34.56:01, $trigger, $trigger_group + +=head2 DESCRIPTION + +Provides support for Insteon TriggerLinc devices. These are relatively simple +devices that provide open and closed messages but not much else. Other than the +awake time, there are no other software configurable options. + +NOTE: There is a hardware configurable option which allows ON messages to be sent +as group 1 and OFF messages to be sent as group 2. If you enable this function +you will likely want to define and link the group 2 object by replacing ":01" +with ":02" in the above examples. + +=head3 Link Management + +As battery operated devices, these devices are generally asleep. If you want +MH to manage the links on these devices you need to wake them up, generally this +involves holding down the set button until the light flashes. + +Alternatively, if you set the awake time sufficiently high MH will be able to +initiate a communication with these devices for that many seconds after there +is activity (open/close) from these devices. This of course comes at the +expense of additional battery usage. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::TriggerLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::TriggerLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); + +my %message_types = ( + %Insteon::BaseDevice::message_types +); + +=item C + +Instantiates a new object. + =cut -1 + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); + $$self{message_types} = \%message_types; + bless $self,$class; + return $self; +} + +=item C + +Handles messages received from the device. Calls C. + +=cut + +sub set +{ + my ($self,$p_state,$p_setby,$p_response) = @_; + return if &main::check_for_tied_filters($self, $p_state); + + # Override any set_with_timer requests + if ($$self{set_timer}) { + &Timer::unset($$self{set_timer}); + delete $$self{set_timer}; + } + + my $setby_name = $p_setby; + $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); + &::print_log("[Insteon::TriggerLinc] " . $self->get_object_name() + . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; + $self->set_receive($p_state,$p_setby); + return; +} + +=item C + +Sets the amount of time, in seconds, that the TriggerLinc will remain "awake" +after sending a command. While awake, the device can be queried by MH such as +with scan link table or sync links. + +=cut + +sub set_awake_time { + my ($self, $awake) = @_; + $awake = sprintf("%02x", $awake); + my $root = $self->get_root(); + my $extra = '000102' . $awake . '0000000000000000000000'; + $$root{_ext_set_get_action} = "set"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $root, 'extended_set_get', $extra); + $root->_send_cmd($message); + return; +} + +=item C + +Requests the status of various settings on the device. Currently this is only +used to obtain the awake time. If the device is awake, the awake time setting +will be printed to the log. + +=cut + +sub get_extended_info { + my ($self,$no_retry) = @_; + my $root = $self->get_root(); + my $extra = '000100000000000000000000000000'; + $$root{_ext_set_get_action} = "get"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $root, 'extended_set_get', $extra); + if ($no_retry){ + $message->retry_count(1); + } + $root->_send_cmd($message); + return; +} + +=item C<_process_message()> + +Checks for and handles unique TriggerLinc messages. +All other messages are transferred to L. + +=cut + +sub _process_message { + my ($self,$p_setby,%msg) = @_; + my $clear_message = 0; + my $root = $self->get_root(); + my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; + my $ack_setby = (ref $$self{m_status_request_pending}) ? $$self{m_status_request_pending} : $p_setby; + if ($msg{is_ack} && $self->_is_info_request($pending_cmd,$ack_setby,%msg)) { + $clear_message = 1; + $$self{m_status_request_pending} = 0; + $self->_process_command_stack(%msg); + } + elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); + #If this was a get request don't clear until data packet received + main::print_log("[Insteon::TriggerLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + if ($$self{_ext_set_get_action} eq 'set'){ + main::print_log("[Insteon::TriggerLinc] Clearing active message") if $main::Debug{insteon}; + $clear_message = 1; + $$self{_ext_set_get_action} = undef; + $self->_process_command_stack(%msg); + } + } + elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { + if (substr($msg{extra},0,6) eq "000001") { + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); + #D3 = Awake Time; + my $awake = (hex(substr($msg{extra}, 6, 2))); + main::print_log("[Insteon::TriggerLinc] The awake seconds ". + "for device ". $self->get_object_name . " is set to: ". + $awake); + $clear_message = 1; + $self->_process_command_stack(%msg); + } else { + main::print_log("[Insteon::TriggerLinc] WARN: Corrupt Extended " + ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; + } + } + else { + $clear_message = $self->SUPER::_process_message($p_setby,%msg); + } + return $clear_message; +} + +sub is_responder +{ + return 0; +} + +=back + +=head2 INI PARAMETERS + +None. + +=head2 AUTHOR + +Kevin Robert Keegan + +=head2 LICENSE + +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. + +=cut + +1; diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 8e28ae6c0..2cf4ab553 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -140,6 +140,12 @@ sub read_table_A { $other = join ', ', (map {"'$_'"} @other); # Quote data $object = "Insteon::MotionSensor(\'$address\', $other)"; } + elsif($type eq "INSTEON_TRIGGERLINC") { + require Insteon::Security; + ($address, $name, $grouplist, @other) = @item_info; + $other = join ', ', (map {"'$_'"} @other); # Quote data + $object = "Insteon::TriggerLinc(\'$address\', $other)"; + } elsif($type eq "INSTEON_IOLINC") { require Insteon::IOLinc; ($address, $name, $grouplist, @other) = @item_info; From 9bc0a24fc489bb243f75df9d865ff2bf8f29bb98 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Thu, 10 Oct 2013 18:26:52 -0400 Subject: [PATCH 186/330] Fix web_hook parameter passing, commonly used in Audrey speech. --- code/common/speech_clash.pl | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/code/common/speech_clash.pl b/code/common/speech_clash.pl index 032d089bd..dfa66cf7f 100644 --- a/code/common/speech_clash.pl +++ b/code/common/speech_clash.pl @@ -38,41 +38,46 @@ sub speak_clash_stub { my ($ref) = @_; &print_log("Clash control stub called!") if $main::Debug{voice}; - if (1 == &Voice_Text::is_speaking() && $$ref{to_file} eq '') { + if (1 == &Voice_Text::is_speaking() && $ref->{to_file} eq '') { if ($main::Debug{voice}) { - $$ref{clash_retry}=0 unless $$ref{clash_retry}; - $$ref{clash_retry}++; #To track how many loops are made - &print_log("SPEAK CLASH($$ref{clash_retry}): Delaying speech call for " . $$ref{text} . "\n"); + $ref->{clash_retry}=0 unless $ref->{clash_retry}; + $ref->{clash_retry}++; #To track how many loops are made + &print_log("SPEECH_CLASH($ref->{clash_retry}): Delaying speech call for " . $ref->{text} . "\n"); } - $$ref{nolog}=1; #To stop MH from logging the speech again + $ref->{nolog}=1; #To stop MH from logging the speech again + delete($ref->{web_hook}); + delete($ref->{audreySpeakRooms}); #Method One: my $parmstxt; my ($pkey,$pval); + while (($pkey,$pval) = each(%{$ref})) { $parmstxt.=', ' if $parmstxt; # WLA: quote the text, otherwise if the spoken text contains a ' character we # get an error message $parmstxt .= "$pkey => q($pval)"; } - &print_log("CLASH Parameters: $parmstxt") if $main::Debug{voice}; - &run_after_delay($sc_delay, "speak($parmstxt)"); + &print_log("SPEECH_CLASH Parameters: $parmstxt") if $main::Debug{voice}; + &run_after_delay($sc_delay, "speak(".$parmstxt.")"); #Method Two - Doesn't work :( # run_after_delay $sc_delay, sub {&speak(%{$ref})}; #Doesn't work :( - $$ref{no_speak}=1; #To stop MH from speaking this time around + $ref->{no_speak}=1; #To stop MH from speaking this time around return; } - if ($$ref{clash_retry}) { - &print_log("SPEAK CLASH: Resolved, continuing speech.");# if $main::Debug{voice}; + if ($ref->{clash_retry}) { + &print_log("SPEECH_CLASH: Resolved, continuing speech.");# if $main::Debug{voice}; $is_speaking=0; $is_speaking_flag=0; } } - -&Speak_parms_add_hook(\&speak_clash_stub) if $Reload; +if ($Reload) { + print_log("SPEECH_CLASH: Hooking into speech events"); + &Speak_parms_add_hook(\&speak_clash_stub); +} =begin comment @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ From 0236d3e34285859e6ff8cf3f55ddcf78124fd34f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 10 Oct 2013 17:27:00 -0700 Subject: [PATCH 187/330] Insteon_ApplianceLink: Add DeviceController to Allow for Sync_Links Closes #271 --- lib/Insteon/Lighting.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index fd33381f1..561a7cdc8 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -431,6 +431,7 @@ Provides support for the Insteon ApplianceLinc. =head2 INHERITS L +L =head2 METHODS @@ -443,7 +444,7 @@ package Insteon::ApplianceLinc; use strict; use Insteon::BaseInsteon; -@Insteon::ApplianceLinc::ISA = ('Insteon::BaseLight'); +@Insteon::ApplianceLinc::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); =item C From c0c7abf43fdef57742f43d3a56433c42d9a7f77e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 10 Oct 2013 17:27:00 -0700 Subject: [PATCH 188/330] Insteon_Scene_Builder: Make Sure a Controller Exists Before Creating a Scene If no contoller existed, prior code would throw a recoverable error. This addition prevents the error and provides a readable error message in the print_log --- lib/read_table_A.pl | 64 +++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 8e28ae6c0..872305324 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1071,42 +1071,44 @@ sub read_table_finish_A { foreach my $scene (sort keys %scene_build_responders) { $code .= "\n#SCENE_BUILD Definition for scene: $scene\n"; - #Doesn't work because object technically doesn't exist yet. Is it even - #necessary to limit to ICONTROLLER's? - #my $object = &get_object_by_name($scene); - #if(defined($object) and $object->isa("Insteon::InterfaceController")) { - if($objects{$scene}) { - #Since an INSTEON_ICONTROLLER exists with the same name as the scene, - #make it a controller of the scene, too. + #Since an object exists with the same name as the scene, + #make it a controller of the scene, too. Hopefully it can be a controller $scene_build_controllers{$scene}{$scene}="1"; } #Loop through the controller hash - foreach my $scene_controller (keys $scene_build_controllers{$scene}) { - if ($objects{$scene_controller}) { - #Make a link to each responder in the responder hash - while (my ($scene_responder, $responder_data) = each($scene_build_responders{$scene})) { - my ($on_level, $ramp_rate) = split(',', $responder_data); - - if (($objects{$scene_responder}) and ($scene_responder ne $scene_controller)) { - if ($on_level) { - if ($ramp_rate) { - $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", - $scene_controller, $scene_responder, $on_level, $ramp_rate; - } else { - $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", - $scene_controller, $scene_responder, $on_level; - } - } else { - $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_controller, $scene_responder; - } - } - } - - } else { - print "\nThere is no object called $scene_controller defined. Ignoring SCENE_BUILD entry.\n"; - } + if (exists $scene_build_controllers{$scene}){ + foreach my $scene_controller (keys $scene_build_controllers{$scene}) { + if ($objects{$scene_controller}) { + #Make a link to each responder in the responder hash + while (my ($scene_responder, $responder_data) = each($scene_build_responders{$scene})) { + my ($on_level, $ramp_rate) = split(',', $responder_data); + + if (($objects{$scene_responder}) and ($scene_responder ne $scene_controller)) { + if ($on_level) { + if ($ramp_rate) { + $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", + $scene_controller, $scene_responder, $on_level, $ramp_rate; + } else { + $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", + $scene_controller, $scene_responder, $on_level; + } + } else { + $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_controller, $scene_responder; + } + } + } + + } else { + ::print_log("[Read_Table_A] ERROR: There is no object ". + "called $scene_controller defined. Ignoring SCENE_BUILD entry."); + } + } + } + else { + ::print_log("[Read_Table_A] ERROR: There is no controller ". + "defined for $scene. Ignoring SCENE_BUILD entry."); } } return $code; From b0c32815c78faa3ada3172b5c191736948ce5584 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Thu, 10 Oct 2013 21:19:16 -0400 Subject: [PATCH 189/330] Add audrey documentation, clean up code, adjust debug clauses on several lines --- code/common/pa_control.pl | 83 ++++++++++++++++++++++++++++++++------- lib/PAobj.pm | 43 ++++++++++++++------ 2 files changed, 98 insertions(+), 28 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index da32500e7..b12884b8d 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -48,11 +48,13 @@ $pactrl->init() if $Startup or $Reload; # Hooks to flag which rooms to turn on based on "rooms=" parm in speak command -&Speak_parms_add_hook(\&pa_parms_stub) if $Reload; -&Speak_pre_add_hook(\&pa_control_stub) if $Reload; -&Play_parms_add_hook(\&pa_parms_stub) if $Reload; -&Play_pre_add_hook(\&pa_control_stub) if $Reload; - +if ($Reload) { + print_log("PA: Hooking into speech events"); + &Speak_parms_add_hook(\&pa_parms_stub); + &Speak_pre_add_hook(\&pa_control_stub); + &Play_parms_add_hook(\&pa_parms_stub); + &Play_pre_add_hook(\&pa_control_stub); +} if (said $v_pa_test) { my $state = $v_pa_test->{state}; $v_pa_test->respond('app=pa Testing PA...'); @@ -65,7 +67,7 @@ my $state = $v_pa_speakers->{state}; $v_pa_speakers->respond("app=pa Turning speakers $state..."); $state = ($state eq 'on') ? ON : OFF; - print "PA: Turning speakers $state\n" if $Debug{pa}; + print_log("PA: Turning speakers $state") if $Debug{pa}; $pactrl->set('allspeakers',$state,'unmuted'); } @@ -85,7 +87,7 @@ sub pa_parms_stub { my %pa_zones = $pactrl->get_pa_zones(); push(@{$parms->{web_hook}},\&pa_web_hook) if $pa_zones{audrey} ne ''; - print "PA: parms_stub set results: $results\n" if $Debug{pa} >=2; + print_log("PA: parms_stub set results: $results") if $Debug{pa} >=2; } @@ -103,9 +105,9 @@ sub pa_control_stub { return if $mode eq 'mute' or $mode eq 'offline'; my $rooms = $parms{rooms}; - print "PA: control_stub: rooms=$rooms, mode=$mode\n" if $Debug{pa}; + print_log("PA: control_stub: rooms=$rooms, mode=$mode") if $Debug{pa}; my $results = $pactrl->audio_hook(ON,%parms); - print "PA: control_stub set results: $results\n" if $Debug{pa} >=2; + print_log("PA: control_stub set results: $results") if $Debug{pa} >=2; set $pa_speaker_timer $pa_timer if $results; return $results; } @@ -119,7 +121,7 @@ sub pa_web_hook { #Turn off speakers when MH says it's done speaking/playing if (state_now $mh_speakers eq OFF) { unset $pa_speaker_timer; - print "PA: Turning speakers off\n" if $Debug{pa}; + print_log("PA: Turning speakers off") if $Debug{pa}; $pactrl->audio_hook(OFF,'normal'); } @@ -127,7 +129,7 @@ sub pa_web_hook { $pa_speaker_timer = new Timer; set $pa_speaker_timer 60 if state_now $mh_speakers eq ON; if (expired $pa_speaker_timer) { - print "PA: Timer expired. Forcing PA speakers off.\n" if $Debug{pa}; + print_log("PA: Timer expired. Forcing PA speakers off.") if $Debug{pa}; set $mh_speakers OFF; } @@ -137,7 +139,7 @@ sub pa_web_hook { Example pa.mht file: # -#Type Address Name Groups Serial Other +#Type Address Name Groups Serial Type # PA, AA, kitchen, all|default|mainfloor, weeder, wdio PA, AB, server, all|basement, weeder, wdio @@ -146,16 +148,17 @@ sub pa_web_hook { PA, objname, living, all|mainfloor, , object PA, 192.168.0.1,family, all|mainfloor, , xap PA, 192.168.0.2,dining, all|mainfloor, , xpl +PA, 192.168.0.3,table, all|mainfloor, , audrey Type: "PA", constant. This must be there. Address: Address or Object name. - If Other is "object", then this should be an object name that can accept an ON or OFF + If Type is "object", then this should be an object name that can accept an ON or OFF For Weeder, 2 characters. First character is the weeder address, the second is the pin if the command to turn on the pin you want is: BHC, then the Address is: BC For X10, the X10 address of the (likely) relay device. - For xAP and xPL, use the IP address or hostname of the target device. + For xAP, xPL and audrey, use the IP address or hostname of the target device. For "object", use the name of the object (without the $). You may use anything that responds ON and OFF set commands. Tested with and Insteon device. @@ -174,7 +177,57 @@ sub pa_web_hook { The default is "weeder". Note that this can be changed with an INI parm. Other: Optional. Sets the type of PA control. Defaults to 'wdio'. Available options are: - wdio,wdio_old,X10,xpl,xap,object + wdio,wdio_old,X10,xpl,xap,audrey,object @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=begin Audrey Config + +Exerpt from audreyspeak.pl + +You must make certain modifications to your Audrey, as follows: + +- Update the software and obtain root shell access capabilities (this + should be available by using Bruce's CF card image or by following + instructions available on the internet.) + +- Open the Audrey's web server to outside http requests + 1) Start the "Root Shell" + 2) type: cd /config + 3) type: cp rm-apps rm-apps.copy + 4) type: vi rm-apps + You'll be in the editor, editing the "rm-apps" file + About the 14th line down is "rb,/kojak/kojak-slinger, -c -e -s -i 127.1" + You need to delete the "-i 127.1" from the line. + To do this, place the cursor under the space right after the "-s" + Type the "x" key to start deleting from the line. + The line should end up looking like this: + "rb,/kojak/kojak-slinger, -c -e -s" + If you need to start over type a colon to get to the vi command line + At the colon prompt type "q!" and hit "enter" (this quits without saving) + If it looks good then at the colon prompt type "wq" to save changes + Now restart the Audrey by unplugging it, waiting 30 seconds and + plugging it back in. + +- Install playsound_noph and it's DLL + 1) Grab the zip file from http://www.planetwebb.com/audrey/ + 2) Place playsound_noph on the Audrey in /nto/photon/bin/ + 3) Place soundfile_noph.so on the Audrey in /nto/photon/dll/ + +- Install mhspeak.shtml on the Audrey + 1) Start the "Root Shell" + 2) type: cd /data/XML + 3) type: ftp blah.com mhspeak.shtml + + The MHSPEAK.SHTML file placed on the Audrey should contain the following: + + + + Shell + + + + + + + =cut diff --git a/lib/PAobj.pm b/lib/PAobj.pm index f75832ffe..b9c3e6e6a 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -68,7 +68,7 @@ sub init { for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); my $type = $ref->get_type(); - &::print_log("PAobj: init: room=$room, zonetype=$type"); + &::print_log("PAobj: init: room=$room, zonetype=$type") if $main::Debug{pa}; $pa_zone_types{$type}++ unless $pa_zone_types{$type}; if($type eq 'wdio') { @@ -91,12 +91,12 @@ sub init { } } - &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} || $#speakers_wdio gt -1; - &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa} || $#speakers_x10 gt -1; - &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa} || $#speakers_xap gt -1; - &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} || $#speakers_xpl gt -1; - &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa} || $#speakers_obj gt -1; - &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa} || $#speakers_audrey gt -1; + &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa};# || $#speakers_wdio gt -1; + &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa};# || $#speakers_x10 gt -1; + &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa};# || $#speakers_xap gt -1; + &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa};# || $#speakers_xpl gt -1; + &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa};# || $#speakers_obj gt -1; + &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa};# || $#speakers_audrey gt -1; if ($#speakers_wdio > -1) { $self->init_weeder(@speakers_wdio); @@ -152,7 +152,7 @@ sub prep_parms @speakers = $self->get_speakers('') if $#speakers == -1; &::print_log("PAobj: Proposed rooms: ".join(', ', @speakers)) if $main::Debug{pa} >=2; @speakers = $self->get_speakers_speakable($parms->{mode},@speakers); - &::print_log("PAobj: Will speak in rooms: ".join(', ', @speakers)) if $main::Debug{pa}; + &::print_log("PAobj: Will speak in rooms: ".join(', ', @speakers)); $parms->{pa_zones} = join(',', @speakers); @@ -241,11 +241,20 @@ sub audio_hook $results = $self->set_xpl($state,\@speakers_xpl,\%voiceparms) if $#speakers_xpl > -1; $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; + &::print_log("PAobj: set results: $results") if $main::Debug{pa}; select undef, undef, undef, $$self{pa_delay} if $results; - &::print_log("PAobj: set results: $results"); $results=0; - if($pa_zones{wdio} ne '') {$results=1;print_log('------> wdio detected, talking...');} + if( + lc $state eq 'on' + && ( + $pa_zones{wdio} ne '' + || $pa_zones{x10} ne '' + || $pa_zones{obj} ne '' + ) + ) { + $results=1; + } return $results; } @@ -253,7 +262,7 @@ sub audio_hook sub web_hook { my ($self,$parms) = @_; - &::print_log("PAobj: web_hook! Audrey: " . $pa_zones{audrey}); + &::print_log("PAobj: web_hook! Audrey: " . $pa_zones{audrey}) if $main::Debug{pa}; return unless $pa_zones{audrey} ne ''; my $results=0; my @speakers_audrey=split(',', $pa_zones{audrey}); @@ -267,10 +276,12 @@ sub set_obj { my ($self,$state,@speakers) = @_; for my $room (@speakers) { - &::print_log("PAobj: set_obj: " . $room . " / " . $state) if $main::Debug{pa} >=2; my $ref = &::get_object_by_name("pa_$room"); if ($ref) { + &::print_log("PAobj: set_obj: " . $room . " / " . $state) if $main::Debug{pa} >=2; $ref->set($state); + } else { + &::print_log("PAobj: Unable to locate object for: pa_$room"); } } } @@ -287,6 +298,8 @@ sub set_audrey if ($refobj) { &::print_log("PAobj: set_audrey: " . $room . " / " . $speakFile) if $main::Debug{pa} >=2; $refobj->play($speakFile); + } else { + &::print_log("PAobj: Unable to locate object for: pa_$room"); } } } @@ -304,6 +317,8 @@ sub set_x10 my ($id) = $ref->get_address(); &::print_log("PAobj: set_x10 ID: $id, State: $state, Room: $room") if $main::Debug{pa} >=2; $refobj->set($state); + } else { + &::print_log("PAobj: Unable to locate object for: pa_$room"); } } } @@ -363,6 +378,8 @@ sub set_weeder $weeder_ref{$card}='' unless $weeder_ref{$card}; $weeder_ref{$card} .= $id; &::print_log("PAobj: card: $card, id: $id, Room: $room") if $main::Debug{pa} >=2; + } else { + &::print_log("PAobj: Unable to locate object for: pa_$room"); } } @@ -510,7 +527,7 @@ sub get_speakers_speakable sub get_pa_zones { my ($self) = @_; - &::print_log("PAobj: get_pa_zones");# if $main::Debug{pa} >=3; + &::print_log("PAobj: get_pa_zones") if $main::Debug{pa} >=3; return %pa_zones; } From 0a7b4f621b48d65e11cdc1751a976bdea528ae02 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 10 Oct 2013 18:25:37 -0700 Subject: [PATCH 190/330] Insteon_i2CS_Linking: Clear Awaiting Ack Flag on All-Link-Complete If flag not cleared, all messages to that device will stall --- lib/Insteon_PLM.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 0dc156830..ff2af022e 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -751,6 +751,8 @@ sub _parse_data { ::print_log("[Insteon::Insteon_PLM] problem w/ success callback: $@") if $@; package Insteon::BaseObject; } + #Clear awaiting_ack flag + $self->active_message->setby->_process_command_stack(0); $self->clear_active_message(); } elsif ($parsed_prefix eq $prefix{all_link_clean_failed} and ($message_length == 12)) From f6b096250c4988f784f8df5a7bdbc3ecceaa96c7 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Thu, 10 Oct 2013 21:32:21 -0400 Subject: [PATCH 191/330] Add comments to audreyspeak.pl, pointing people to pa_control.pl. --- code/common/audreyspeak.pl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code/common/audreyspeak.pl b/code/common/audreyspeak.pl index f35145bb3..d7471c29a 100644 --- a/code/common/audreyspeak.pl +++ b/code/common/audreyspeak.pl @@ -13,6 +13,13 @@ 1.0 Original version by Tim Doyle - 9/10/2002 +********************************************************************* +*This script is now deprecated, as support for Audrey has been added +*to the PAobj object. Enable pa_control.pl and follow examples to +*add audrey zones to your pa.mht file. Example: +*PA, 192.168.0.1,family, all|mainfloor, , audrey +********************************************************************* + This script allows MisterHouse to capture and send speech and played wav files to an Audrey unit. The original version was based upon Keith Webb's work outlined in his email of 12/23/01. From ae81eaa1e4e382df78833ca919244851c0f2bf1e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 10 Oct 2013 18:53:38 -0700 Subject: [PATCH 192/330] Insteon_Thermostat: Add Voice Command for Sync Time Generate Voice Commands needed to be called after the thermo version is set --- lib/Insteon.pm | 2 +- lib/Insteon/Thermostat.pm | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 67c3d5610..9038ff491 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -1193,8 +1193,8 @@ sub _active_interface &main::Reload_post_add_hook(\&Insteon::BaseInterface::poll_all, 1); $init_complete = 0; &main::MainLoop_pre_add_hook(\&Insteon::init, 1); - &main::Reload_post_add_hook(\&Insteon::generate_voice_commands, 1); &main::Reload_post_add_hook(\&Insteon::check_thermo_versions, 1); + &main::Reload_post_add_hook(\&Insteon::generate_voice_commands, 1); } $$self{active_interface} = $interface if $interface; return $$self{active_interface}; diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index 8c9109817..f224d0484 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -1178,6 +1178,30 @@ sub _poll_humid_setpoints{ $self->_send_cmd($message); } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'sync time' => "$object_name->sync_time()" + ); + return \%voice_cmds; +} + package Insteon::Thermo_mode; use strict; From aac169be5eb4b1358bd6194ba78ab8cd71e5fd98 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 10 Oct 2013 19:50:08 -0700 Subject: [PATCH 193/330] Insteon: Enabling Link_To_Interfaces for Child Objects in BaseDevice Enabled group and data3 determination if not specified. Based off of Insteon_I2CS_Linking Branch so as not to require a manual merge. Need to merge that branch first. Closed #274 --- lib/Insteon/BaseInsteon.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f465feef7..f7334ea69 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1377,8 +1377,8 @@ the device. sub link_to_interface { my ($self,$p_group, $p_data3, $step) = @_; - my $group = $p_group; - $group = '01' unless $group; + $p_group = $self->group unless (defined $p_group); + $p_data3 = $self->group unless (defined $p_data3); my $success_callback_prefix = $self->get_object_name."->link_to_interface(\"$p_group\",\"$p_data3\","; my $success_callback = ""; my $failure_callback = '::print_log("[Insteon::BaseInsteon] Error: The Link_To_Interface '. @@ -1392,7 +1392,7 @@ sub link_to_interface } case (1) { #Add Link from object->PLM $success_callback = $success_callback_prefix . "\"2\")"; - my %link_info = ( object => $self->interface, group => $group, is_controller => 1, + my %link_info = ( object => $self->interface, group => $p_group, is_controller => 1, callback => "$success_callback", failure_callback=> "$failure_callback"); $link_info{data3} = $p_data3 if $p_data3; if ($self->_aldb) { @@ -1406,7 +1406,7 @@ sub link_to_interface } case (2){ #Add Link from PLM->object $success_callback = $success_callback_prefix . "\"3\")"; - my $link_info = "deviceid=" . lc $self->device_id . " group=$group is_controller=0 " . + my $link_info = "deviceid=" . lc $self->device_id . " group=$p_group is_controller=0 " . "callback=$success_callback failure_callback=$failure_callback"; $self->interface->add_link($link_info); } From e2a0da9b1713cbd6f381a98c02ae2da9ab2b6989 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 10 Oct 2013 19:53:23 -0700 Subject: [PATCH 194/330] Insteon: Remove Unneeded Link_to_Interface Routine in DeviceController No longer needed as all functions are condensed into BaseDevice link_to_interface function. --- lib/Insteon/BaseInsteon.pm | 45 -------------------------------------- 1 file changed, 45 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f7334ea69..41bf0d512 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3376,51 +3376,6 @@ sub request_status } } -=item C - -If a controller link from the device to the interface does not exist, this will -create that link on the device. - -Next, if a responder link from the device to the interface does not exist on the -interface, this will create that link on the interface. - -The group is the group on the device that is the controller, such as a button on -a keypad link. It will default to 01. - -Data3 is optional and is used to set the Data3 value in the controller link on -the device. - -=cut - -sub link_to_interface -{ - my ($self, $p_group, $p_data3) = @_; - my $group = $p_group; - $group = $self->group unless $group; - # get the surrogate device for this if group is not '01' - if ($self->group ne '01') { - my $surrogate_obj = &Insteon::get_object($self->device_id,'01'); - if ($p_data3) { - $surrogate_obj->link_to_interface($group,$p_data3); - } elsif ($surrogate_obj->isa('Insteon::KeyPadLincRelay') or $surrogate_obj->isa('Insteon::KeyPadLinc')) { - $surrogate_obj->link_to_interface($group,$self->group); - } else { - $surrogate_obj->link_to_interface($group); - } - # next, if the link is a keypadlinc, then create the reverse link to permit - # control over the button's light - if ($surrogate_obj->isa('Insteon::KeyPadLincRelay') or $surrogate_obj->isa('Insteon::KeyPadLinc')) { - - } - } else { - if ($p_data3) { - $self->SUPER::link_to_interface($group, $p_data3); - } else { - $self->SUPER::link_to_interface($group); - } - } -} - =item C Will delete the contoller link from the device to the interface if such a link exists. From 7036d311fd4f232e009820ed7cdca4ba45c6ffbb Mon Sep 17 00:00:00 2001 From: Pmatis Date: Fri, 11 Oct 2013 10:59:13 -0400 Subject: [PATCH 195/330] Roll back $parms->{mode} use, interferred with Voice_Text.pm when using cepstral swift for voice --- code/common/pa_control.pl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index b12884b8d..c0475412a 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -73,14 +73,15 @@ sub pa_parms_stub { my ($parms) = @_; - unless ($parms->{mode}) { + my $mode = $parms->{mode}; + unless ($mode) { if (defined $mode_mh) { # *** Outdated (?) - $parms->{mode} = state $mode_mh; + $mode = state $mode_mh; } else { - $parms->{mode} = $Save{mode}; + $mode = $Save{mode}; } } - return if $parms->{mode} eq 'mute' or $parms->{mode} eq 'offline'; + return if $mode eq 'mute' or $mode eq 'offline'; my $results = $pactrl->prep_parms($parms); From 2b6c6a223e39c0698de594ac82bad52dd020f21e Mon Sep 17 00:00:00 2001 From: Pmatis Date: Fri, 11 Oct 2013 12:06:17 -0400 Subject: [PATCH 196/330] Fix so web_file is only used when needed. Clean up a little standardize on arrow notation for parms. --- code/common/pa_control.pl | 21 +++++++++------------ lib/PAobj.pm | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index c0475412a..5f9c87b11 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -81,31 +81,28 @@ sub pa_parms_stub { $mode = $Save{mode}; } } + $parms->{pa_mode} = $mode; return if $mode eq 'mute' or $mode eq 'offline'; my $results = $pactrl->prep_parms($parms); - my %pa_zones = $pactrl->get_pa_zones(); - push(@{$parms->{web_hook}},\&pa_web_hook) if $pa_zones{audrey} ne ''; + + if (defined $pa_zones{audrey} && $pa_zones{audrey} ne '') { + print_log("PA: audrey zone detected, hooking via web_hook. (".$pa_zones{audrey}.")") if $Debug{pa}; + push(@{$parms->{web_hook}},\&pa_web_hook); + } print_log("PA: parms_stub set results: $results") if $Debug{pa} >=2; } sub pa_control_stub { - my (%parms) = @_; + my ($parms) = @_; my @pazones; - my $mode = $parms{mode}; -# unless ($mode) { -# if (defined $mode_mh) { # *** Outdated (?) -# $mode = state $mode_mh; -# } else { -# $mode = $Save{mode}; -# } -# } + my $mode = $parms->{pa_mode}; return if $mode eq 'mute' or $mode eq 'offline'; - my $rooms = $parms{rooms}; + my $rooms = $parms->{rooms}; print_log("PA: control_stub: rooms=$rooms, mode=$mode") if $Debug{pa}; my $results = $pactrl->audio_hook(ON,%parms); print_log("PA: control_stub set results: $results") if $Debug{pa} >=2; diff --git a/lib/PAobj.pm b/lib/PAobj.pm index b9c3e6e6a..1e2d00033 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -202,7 +202,7 @@ sub prep_parms $pa_zones{obj}=join(',',@speakers_obj); $pa_zones{audrey}=join(',',@speakers_audrey); - $parms->{web_file}="web_file";# if $#speakers_wdio gt -1; + $parms->{web_file}="web_file" if $#speakers_audrey gt -1; if( 1 From 041f1785cbacd457d57d7775456bd2d4b183e6b7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 17:27:00 -0700 Subject: [PATCH 197/330] Insteon: Fix BaseDevice Unlink_to_Interface for use with Subgroup Objects Used a similar step method as found in link_to_interface to make the code more readable Removed DeviceController unlink_to_interface routine as no longer needed --- lib/Insteon/BaseInsteon.pm | 102 +++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 41bf0d512..242de1914 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1477,34 +1477,59 @@ a keypad link. It will default to 01. sub unlink_to_interface { - my ($self,$p_group) = @_; - my $group = $p_group; - $group = '01' unless $group; - my $callback_instance = $self->interface->get_object_name; - my $callback_info = "deviceid=" . lc $self->device_id . " group=$group is_controller=0"; - if ($self->_aldb) { - $self->_aldb->delete_link(object => $self->interface, group => $group, is_controller => 1, - callback => "$callback_instance->delete_link('$callback_info')"); - } - else - { - &main::print_log("[BaseInsteon] This item " . $self->get_object_name . - " does not have an ALDB object. Unlinking is not permitted."); - } + my ($self,$p_group,$step) = @_; + $p_group = $self->group unless $p_group; + # get the surrogate device for this if group is not '01' + if ($self->group ne '01') { + $self = &Insteon::get_object($self->device_id,'01'); + } + #It is possible to nest all of the callbacks in at once, but the quoting + #becomes very complicated and happers readability + my $success_callback_prefix = $self->get_object_name."->unlink_to_interface('$p_group',"; + my $success_callback = ""; + my $failure_callback = "::print_log('[Insteon::BaseInsteon] ERROR: Unlink_To_Interface ". + "failed for device: ".$self->get_object_name."')"; + $step = 0 if ($step eq ''); + switch ($step){ + case (0) { + if ($self->_aldb) { + $success_callback = $success_callback_prefix . "'1')"; + $self->_aldb->delete_link(object => $self->interface, + group => $p_group, + data3=> $p_group, is_controller => 1, + callback => $success_callback, + failure_callback=> $failure_callback); + } + else + { + &main::print_log("[BaseInsteon] This item " . $self->get_object_name . + " does not have an ALDB object. Unlinking is not permitted."); + } + } + case (1) { + $success_callback = $success_callback_prefix . "'2')"; + $self->interface->delete_link( + deviceid => lc $self->device_id, + group=> $p_group, is_controller=>0, + callback=>$success_callback, + failure_callback=>$failure_callback); + } + case (2) { + ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". + " successfully completed for device " + . $self->get_object_name); + } + } } =item C -BETA -- Can be used to create the initial link with i2cs devices. i1 devices -will not respond to this command. In the future, this will be incorporated into a -one-step process -- BETA - -Places the object into linking mode as if you had held down the set button on -the device. To create a link wherein the PLM is the controller, first run the -PLM voice command "initiate link as controller". Then run this command. Finally, -run the voice command "scan link table" on this device. +Places an i2 object into linking mode as if you had held down the set button on +the device. i1 objects will not respond to this command. This is needed to +link i2CS devices that will not respond without a manual link. -The group argument is optional and not needed for group 01. +This process is included as part of the link_to_interface voice command and +should not need to be called seperately. Returns: nothing @@ -3376,37 +3401,6 @@ sub request_status } } -=item C - -Will delete the contoller link from the device to the interface if such a link exists. - -Next, will delete the responder link from the device to the interface on the -interface, if such a link exists. - -The group is the group on the device that is the controller, such as a button on -a keypad link. It will default to 01. - -=cut - -sub unlink_to_interface -{ - my ($self,$p_group) = @_; - my $group = $p_group; - $group = $self->group unless $group; - # get the surrogate device for this if group is not '01' - if ($self->group ne '01') { - my $surrogate_obj = &Insteon::get_object($self->device_id,'01'); - $surrogate_obj->unlink_to_interface($group); - # next, if the link is a keypadlinc, then delete the reverse link to permit - # control over the button's light - if ($surrogate_obj->isa('Insteon::KeyPadLincRelay') or $surrogate_obj->isa('Insteon::KeyPadLinc')) { - - } - } else { - $self->SUPER::unlink_to_interface($group); - } -} - =back =head2 AUTHOR From 179ef284d38eaabe21bfb0e7a03c32f2b8fb07ba Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 17:37:00 -0700 Subject: [PATCH 198/330] Insteon: Reset ALDB Scan Time on Completed Successful Scan Only Previously we reset when we started a scan Now we reset when a scan has completed successfully and each time when the device tells us that the ALDB Delta has not changed. We also reset if serial query_aldb_delta requests are received in less than 2 seconds. This allows for sequentail aldb actions without slowing down to send a request to the device for its ALDB Delta when it is unlikely that it changed. --- lib/Insteon/AllLinkDatabase.pm | 4 ++-- lib/Insteon/BaseInsteon.pm | 2 ++ lib/Insteon_PLM.pm | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index ea238256c..6ad2f9568 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -152,6 +152,8 @@ sub query_aldb_delta #if we just did a aldb_query less than 2 seconds ago, don't repeat &::print_log("[Insteon::AllLinkDatabase] The link table for " . $self->{device}->get_object_name . " is in sync."); + #Further extend Scan Time in case of serial aldb requests + $self->scandatetime(&main::get_tickcount); if (defined $self->{_aldb_unchanged_callback}) { package main; my $callback = $self->{_aldb_unchanged_callback}; @@ -288,7 +290,6 @@ sub scan_link_table $$self{_mem_activity} = 'scan'; $$self{_success_callback} = ($success_callback) ? $success_callback : undef; $$self{_failure_callback} = ($failure_callback) ? $failure_callback : undef; - $self->scandatetime(&main::get_tickcount); $self->health('out-of-sync'); # allow acknowledge to set otherwise if($self->isa('Insteon::ALDB_i1')) { $self->_peek('0FF8',0); @@ -2715,7 +2716,6 @@ Sends the request for the first alllink entry on the PLM. sub get_first_alllink { my ($self) = @_; - $self->scandatetime(&main::get_tickcount); $self->health('out-of-sync'); # set as corrupt and allow acknowledge to set otherwise $$self{device}->queue_message(new Insteon::InsteonMessage('all_link_first_rec', $$self{device})); } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7e857a6d3..ee7a5e403 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -675,6 +675,8 @@ sub _is_info_request if ($self->_aldb->aldb_delta() eq $msg{cmd_code}){ &::print_log("[Insteon::BaseObject] The link table for " . $self->{object_name} . " is in sync."); + #Link Table Scan Successful, Record Current Time + $self->_aldb->scandatetime(&main::get_tickcount); if (defined $self->_aldb->{_aldb_unchanged_callback}) { $callback = $self->_aldb->{_aldb_unchanged_callback}; $self->_aldb->{_aldb_unchanged_callback} = undef; diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index dd6315f74..27b2b4b70 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -567,6 +567,7 @@ sub _parse_data { { $self->_aldb->health("good"); } + $self->_aldb->scandatetime(&main::get_tickcount); &::print_log("[Insteon_PLM] " . $self->get_object_name . " completed link memory scan: status: " . $self->_aldb->health()) if $main::Debug{insteon}; From 0746d3a4c1c912364a1352cc4a18ad0fef4f0d19 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 17:37:00 -0700 Subject: [PATCH 199/330] Insteon: Link_to_Interface will add Surrogate Links if they Exist Surrogate links allow the KPL light buttons to be directly controlled from MH. In the future other similar surrogates may exist. The Link_to_Interface routine now syncs these links if a surrogate is defined. Also cut out the duplicate logic in Link_to_Interface_i2CS and instead sent the callbacks to Link_to_Interface since the functions are the same after the initial link. --- lib/Insteon/BaseInsteon.pm | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 242de1914..d9fe111c4 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1410,7 +1410,32 @@ sub link_to_interface "callback=$success_callback failure_callback=$failure_callback"; $self->interface->add_link($link_info); } - case (3){ + case (3){ #Add surrogate link on device if surrogate exists + if (ref $$self{surrogate}){ + $success_callback = $success_callback_prefix . "\"4\")"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( object => $self->interface, + group => $surrogate_group, is_controller => 0, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $p_group); + $self->_aldb->add_link(%link_info); + } else { + ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. + ' for device ' .$self->get_object_name); + } + } + case (4){ #Add surrogate link on PLM if surrogate exists + $success_callback = $success_callback_prefix . "\"5\")"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( deviceid=> lc $self->device_id, + group => $surrogate_group, is_controller => 1, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $surrogate_group); + $self->interface->add_link(%link_info); + } + case (5){ ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. ' for device ' .$self->get_object_name); } @@ -1445,21 +1470,11 @@ sub link_to_interface_i2cs $self->enter_linking_mode($p_group, $success_callback, $failure_callback); } case (2) { #Scan device to get an accurate link table - $success_callback = $success_callback_prefix . "'3')"; + #return to normal link_to_interface routine if successful + $success_callback_prefix = $self->get_object_name."->link_to_interface('$p_group','$p_data3',"; + $success_callback = $success_callback_prefix . "'2')"; $self->scan_link_table($success_callback, $failure_callback); } - case (3) { #Add link from device->PLM - $success_callback = $success_callback_prefix . "'4')"; - my $group = $p_group; - $group = '01' unless $group; - my $link_info = "deviceid=" . lc $self->device_id . " group=$group is_controller=0 ". - "callback=$success_callback failure_callback=$failure_callback"; - $self->interface->add_link("$link_info"); - } - case (4) { - ::print_log('[Insteon::BaseInsteon] Link_To_Interface_I2CS successfully completed'. - ' for device ' .$self->get_object_name); - } } } @@ -1479,7 +1494,7 @@ sub unlink_to_interface { my ($self,$p_group,$step) = @_; $p_group = $self->group unless $p_group; - # get the surrogate device for this if group is not '01' + # get the root device for this if group is not '01' if ($self->group ne '01') { $self = &Insteon::get_object($self->device_id,'01'); } @@ -1491,7 +1506,7 @@ sub unlink_to_interface "failed for device: ".$self->get_object_name."')"; $step = 0 if ($step eq ''); switch ($step){ - case (0) { + case (0) { #Delete link on the device if ($self->_aldb) { $success_callback = $success_callback_prefix . "'1')"; $self->_aldb->delete_link(object => $self->interface, @@ -1506,7 +1521,7 @@ sub unlink_to_interface " does not have an ALDB object. Unlinking is not permitted."); } } - case (1) { + case (1) { #Delete link on the PLM $success_callback = $success_callback_prefix . "'2')"; $self->interface->delete_link( deviceid => lc $self->device_id, From 52b065349cf092b610a51e45bdeb108b60c4fb51 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 17:47:00 -0700 Subject: [PATCH 200/330] Insteon: Add Delete All-Link Record Descriptor Minor addition. --- lib/Insteon/MessageDecoder.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/MessageDecoder.pm b/lib/Insteon/MessageDecoder.pm index a324ec269..a456dbc25 100644 --- a/lib/Insteon/MessageDecoder.pm +++ b/lib/Insteon/MessageDecoder.pm @@ -660,7 +660,8 @@ sub plm_decode { '01'=>'Find Next All-Link Record', '20'=>'Update/Add All-Link Record', '40'=>'Update/Add Controller All-Link Record', - '41'=>'Update/Add Responder All-Link Record'); + '41'=>'Update/Add Responder All-Link Record', + '80'=>'Delete All-Link Record'); $plm_message .= sprintf("%20s: (",'Control code').substr($plm_string,4,2).") ".$control_string{substr($plm_string,4,2)}."\n"; $plm_message .= sprintf("%20s: ",'All-Link Flags').substr($plm_string,6,2)."\n"; my $flags = hex(substr($plm_string,6,2)); From 48d154c86229ec8247edfe91f59a0f70d8bae597 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 17:47:00 -0700 Subject: [PATCH 201/330] Insteon: Delete Surrogate Links in Unlink_to_Interface The companion function to the recently added feature in Link_to_Interface Removed root finding code, as _aldb always returns the root object --- lib/Insteon/BaseInsteon.pm | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index d9fe111c4..4fd3f8eb7 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1494,10 +1494,6 @@ sub unlink_to_interface { my ($self,$p_group,$step) = @_; $p_group = $self->group unless $p_group; - # get the root device for this if group is not '01' - if ($self->group ne '01') { - $self = &Insteon::get_object($self->device_id,'01'); - } #It is possible to nest all of the callbacks in at once, but the quoting #becomes very complicated and happers readability my $success_callback_prefix = $self->get_object_name."->unlink_to_interface('$p_group',"; @@ -1529,7 +1525,33 @@ sub unlink_to_interface callback=>$success_callback, failure_callback=>$failure_callback); } - case (2) { + case (2){ #Delete surrogate link on device if surrogate exists + if (ref $$self{surrogate}){ + $success_callback = $success_callback_prefix . "'3')"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( object => $self->interface, + group => $surrogate_group, is_controller => 0, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $p_group); + $self->_aldb->delete_link(%link_info); + } else { + ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". + " successfully completed for device " + . $self->get_object_name); + } + } + case (3){ #Delete surrogate link on PLM if surrogate exists + $success_callback = $success_callback_prefix . "'4')"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( deviceid=> lc $self->device_id, + group => $surrogate_group, is_controller => 1, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $surrogate_group); + $self->interface->delete_link(%link_info); + } + case (4) { ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". " successfully completed for device " . $self->get_object_name); From de01e76f324e94e9b4c072b72a277b522bbcfb8b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 17:57:00 -0700 Subject: [PATCH 202/330] Insteon_RemoteLinc: Remove Arcane Reference to is_battery_low Routine was removed in favor of child battery object --- lib/Insteon/Controller.pm | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 20b081bcb..f18a4bda0 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -264,15 +264,6 @@ sub _process_message { { $$root{battery_object}->set_receive($voltage, $root); } - if ($self->_is_battery_low($voltage)){ - main::print_log("[Insteon::RemoteLinc] The battery level ". - "is below the set threshold running low battery event."); - package main; - eval $$root{low_battery_event}; - ::print_log("[Insteon::RemoteLinc] " . $self->{device}->get_object_name . ": error during low battery event eval $@") - if $@; - package Insteon::RemoteLinc; - } $clear_message = 1; $self->_process_command_stack(%msg); } else { From a50529e8ebb00775412df9df2107f2ef5e15f343 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 18:23:12 -0700 Subject: [PATCH 203/330] Insteon_RemoteLinc: Fix for Setting Linked Devices Needed to set DeviceController as the primary, this underscores the need to get rid of that package. --- lib/Insteon/Controller.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index f18a4bda0..871ed28b6 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -54,7 +54,7 @@ package Insteon::RemoteLinc; use strict; use Insteon::BaseInsteon; -@Insteon::RemoteLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); +@Insteon::RemoteLinc::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); my %message_types = ( %Insteon::BaseDevice::message_types, From 8e1602cb15c66fc6cfb3486e773dd8f58ec7c8a6 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 18:48:41 -0700 Subject: [PATCH 204/330] Insteon: Add Is Deaf Routine for Battery Devices Closed #279 --- lib/Insteon/AllLinkDatabase.pm | 8 ++++---- lib/Insteon/BaseInsteon.pm | 18 ++++++++++++++++++ lib/Insteon/Controller.pm | 1 + lib/Insteon/Security.pm | 2 ++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index aa47c807b..8d2ff2926 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -421,7 +421,7 @@ sub delete_orphan_links # first, make sure that the health of ALDB is ok if ($self->health ne 'good') { - if ($$self{device}->isa('Insteon::RemoteLinc') or $$self{device}->isa('Insteon::MotionSensor')) + if (!$self->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from deaf device: $selfname"); @@ -588,7 +588,7 @@ sub delete_orphan_links } else # is a non-PLM device { - if ($linked_device->isa('Insteon::RemoteLinc') or $linked_device->isa('Insteon::MotionSensor')) + if (!$self->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); } @@ -754,7 +754,7 @@ sub delete_orphan_links { $member = $member->get_root; } - if ($member->isa('Insteon::RemoteLinc') or $member->isa('Insteon::MotionSensor')) + if (!$self->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] ignoring link from " . $link->get_object_name . " to " . $member->get_object_name); @@ -2830,7 +2830,7 @@ sub delete_orphan_links } if ($member->isa('Insteon::BaseDevice')) { - if ($member->isa('Insteon::RemoteLinc') or $member->isa('Insteon::MotionSensor')) + if (!$self->is_deaf) { &::print_log("[Insteon::ALDB_PLM] ignoring link from PLM to " . $member->get_object_name); diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 43717c4ca..9c6250448 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1260,6 +1260,7 @@ sub new $$self{hops_left_count} = 0; $$self{max_hops_count} = 0; $$self{outgoing_hop_count} = 0; + $$self{is_deaf} = 0; return $self; } @@ -1308,6 +1309,23 @@ sub set_receive $self->SUPER::set_receive($p_state, $p_setby, $p_response); } +=item C + +Returns true if the device must be awake in order to respond to messages. Most +devices are not deaf, currently devices that are deaf are battery operated +devices such as the Motion Sensor, RemoteLinc and TriggerLinc. + +At the BaseObject level all devices are defined as deaf. Objects which inherit +BaseObject should redefine is_deaf as necessary. + +=cut + +sub is_deaf +{ + my ($self) = @_; + return $$self{is_deaf}; +} + =item C Returns true if the device is a controller. diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 871ed28b6..1d2e18299 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -79,6 +79,7 @@ sub new $$self{queue_timer} = new Timer; } bless $self,$class; + $$self{is_deaf} = 1; return $self; } diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index fe97e4014..65f4d3484 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -126,6 +126,7 @@ sub new $$self{queue_timer} = new Timer; } bless $self,$class; + $$self{is_deaf} = 1; return $self; } @@ -775,6 +776,7 @@ sub new my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); $$self{message_types} = \%message_types; bless $self,$class; + $$self{is_deaf} = 1; return $self; } From 4b7e268950740e16453f0712ca8d31cc1f3bc182 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 18:55:35 -0700 Subject: [PATCH 205/330] Insteon: Add is_deaf Check to Sync Links, Fix Dumb Typo Fix Delete Orphans Logic, had that backwards. --- lib/Insteon.pm | 7 +++---- lib/Insteon/AllLinkDatabase.pm | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index fa01d60d6..0a5afe9a1 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -400,9 +400,8 @@ sub scan_all_linktables { my $candidate_object = $_; if ($candidate_object->is_root and - !($candidate_object->isa('Insteon::RemoteLinc') - or $candidate_object->isa('Insteon::InterfaceController') - or $candidate_object->isa('Insteon::MotionSensor'))) + !($self->is_deaf + or $candidate_object->isa('Insteon::InterfaceController'))) { push @_scan_devices, $candidate_object; &main::print_log("[Scan all linktables] INFO1: " @@ -511,7 +510,7 @@ sub sync_all_links # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links for my $obj (&Insteon::find_members('Insteon::BaseController')) { - if ($obj->isa('Insteon::RemoteLinc') or $obj->isa('Insteon::MotionSensor')) + if ($self->is_deaf) { &main::print_log("[Sync all links] Ignoring links from 'deaf' device: " . $obj->get_object_name); } diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 8d2ff2926..9d35b4005 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -421,7 +421,7 @@ sub delete_orphan_links # first, make sure that the health of ALDB is ok if ($self->health ne 'good') { - if (!$self->is_deaf) + if ($self->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from deaf device: $selfname"); @@ -588,7 +588,7 @@ sub delete_orphan_links } else # is a non-PLM device { - if (!$self->is_deaf) + if ($self->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); } @@ -754,7 +754,7 @@ sub delete_orphan_links { $member = $member->get_root; } - if (!$self->is_deaf) + if ($self->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] ignoring link from " . $link->get_object_name . " to " . $member->get_object_name); @@ -2830,7 +2830,7 @@ sub delete_orphan_links } if ($member->isa('Insteon::BaseDevice')) { - if (!$self->is_deaf) + if ($self->is_deaf) { &::print_log("[Insteon::ALDB_PLM] ignoring link from PLM to " . $member->get_object_name); From 726997a580288967850fc9cd316d9a0760771ed3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 19:01:16 -0700 Subject: [PATCH 206/330] Insteon: Not All References are to Self Reminder to self: don't use find and replace --- lib/Insteon.pm | 2 +- lib/Insteon/AllLinkDatabase.pm | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 0a5afe9a1..ced96dbdb 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -510,7 +510,7 @@ sub sync_all_links # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links for my $obj (&Insteon::find_members('Insteon::BaseController')) { - if ($self->is_deaf) + if ($obj->is_deaf) { &main::print_log("[Sync all links] Ignoring links from 'deaf' device: " . $obj->get_object_name); } diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 9d35b4005..003db8636 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -588,7 +588,7 @@ sub delete_orphan_links } else # is a non-PLM device { - if ($self->is_deaf) + if ($linked_device->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); } @@ -754,7 +754,7 @@ sub delete_orphan_links { $member = $member->get_root; } - if ($self->is_deaf) + if ($member->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] ignoring link from " . $link->get_object_name . " to " . $member->get_object_name); @@ -2830,7 +2830,7 @@ sub delete_orphan_links } if ($member->isa('Insteon::BaseDevice')) { - if ($self->is_deaf) + if ($member->is_deaf) { &::print_log("[Insteon::ALDB_PLM] ignoring link from PLM to " . $member->get_object_name); From f6762440a0796149ecad68fdd94cef6147bad24a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 11 Oct 2013 19:03:32 -0700 Subject: [PATCH 207/330] Insteon: One More Typo --- lib/Insteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index ced96dbdb..e55ee7961 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -400,7 +400,7 @@ sub scan_all_linktables { my $candidate_object = $_; if ($candidate_object->is_root and - !($self->is_deaf + !($candidate_object->is_deaf or $candidate_object->isa('Insteon::InterfaceController'))) { push @_scan_devices, $candidate_object; From 61aa33484fe83fc577120f678aa5ac62a5f577da Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sat, 12 Oct 2013 01:40:07 -0400 Subject: [PATCH 208/330] Don't need to set volume if in mute or offline mode. --- code/common/mh_sound.pl | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/code/common/mh_sound.pl b/code/common/mh_sound.pl index c6a33e735..66411b192 100644 --- a/code/common/mh_sound.pl +++ b/code/common/mh_sound.pl @@ -21,9 +21,9 @@ $mh_speakers_timer = new Timer; $Info{Volume_Control} = 'Command Line' if $Reload and $config_parms{volume_master_get_cmd} and $config_parms{volume_master_set_cmd}; - - # Allow for default volume control. Reset on startup. - +################################################ +# Allow for default volume control. Reset on startup. +################################################ &set_volume_master_wrapper($mh_volume->{state}) if $Startup and defined $mh_volume->{state}; #noloop &set_volume_wav($config_parms{volume_wav_default_volume}) if $Startup and defined $config_parms{volume_wav_default_volume}; #noloop @@ -161,7 +161,7 @@ sub set_volume_wav { #return $volume_wav_previous; } - # Set hooks so set_volume is called whenever speak or play is called +# Set hooks so set_volume is called whenever speak or play is called &Speak_pre_add_hook(\&set_volume_pre_hook) if $Reload; &Play_pre_add_hook (\&set_volume_pre_hook) if $Reload; @@ -179,6 +179,7 @@ sub set_volume_pre_hook { undef $volume_wav_previous; my $volume = $parms{volume}; + my $mode = $parms{mode}; # *** Oops the following line is wrong--mh_volume is linked to mixer # Not to be used as the default for playing WAV's, speaking, etc. @@ -186,6 +187,15 @@ sub set_volume_pre_hook { #$volume = $mh_volume->{state} unless $volume; return unless $volume; + unless ($mode) { + if (defined $mode_mh) { # *** Outdated (?) + $mode = state $mode_mh; + } else { + $mode = $Save{mode}; + } + } + return if $mode eq 'mute' or $mode eq 'offline'; + # Set a timer since we can not detect when a wav file is done if ($parms{time}) { set $mh_speakers_timer $parms{time}, '&put_volume_back(1)'; # flag to say WAV did it! @@ -193,22 +203,21 @@ sub set_volume_pre_hook { if ($parms{time} or ($parms{text} and $Voice_Text::VTxt_version ne 'msv5')) { - print_log "Setting wav volume to $volume"; + print_log "Setting wav volume to $volume"; $volume = 100 if $volume > 100; - $volume_wav_previous = &set_volume_wav($volume); - + $volume_wav_previous = &set_volume_wav($volume); + if($parms{mhvolume}) { $volume_master_changed=1; - &set_volume_master_wrapper($parms{mhvolume}); + &set_volume_master_wrapper($parms{mhvolume}); } } } - - # Allow for a pre-speak/play wav file +# Allow for a pre-speak/play wav file &Speak_pre_add_hook(\&sound_pre_speak) if $Reload and $config_parms{sound_pre_speak}; &Play_pre_add_hook (\&sound_pre_play) if $Reload and $config_parms{sound_pre_play}; @@ -219,7 +228,7 @@ sub sound_pre_speak { # *** Config parm for this pause! - #&sleep_time(400); # So the TTS engine doesn't grab the sound card first + #&sleep_time(400); # So the TTS engine doesn't grab the sound card first } sub sound_pre_play { my %parms = @_; @@ -228,7 +237,7 @@ sub sound_pre_play { } - # Allow for restarting of TTS engine +# Allow for restarting of TTS engine $restart_tts = new Voice_Cmd 'Restart the TTS engine'; $restart_tts-> set_info('This will restart the voice Text To Speech engine, in case it died for some reason'); From b4d1a92371f42d6ba3d16f21190b4e5027162734 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 15:19:31 -0700 Subject: [PATCH 209/330] Insteon: Rework Set Routine in BaseObject The old routine contained a lot of unnecessary logic that made it difficult to parse. Indeed, I am not sure I understand everything that it was doing. A number of functions which were handled by the set routine in BaseDevice were also copied over. --- lib/Insteon/BaseInsteon.pm | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 43717c4ca..f7c8225eb 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -354,23 +354,16 @@ sub set my $setby_name = $p_setby; $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - if (ref $p_setby and (($p_setby eq $self->interface()) - or (($p_setby->isa('Insteon::BaseObject')) - and (($p_setby eq $self) - or (&main::set_by_to_target($p_setby) eq $self->interface))))) - { - # don't reset the object w/ the same state if set from the interface - return if (lc $p_state eq lc $self->state) and $self->is_acknowledged - and not(($p_setby->isa('Insteon::BaseObject') and ($p_setby eq $self))); + if (ref $p_setby and ($p_setby eq $self)) + { #If set by device, update MH state &::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . "::set($p_state, $setby_name)") if $main::Debug{insteon}; + . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; - } else { + my $link_state = &Insteon::BaseObject::derive_link_state($p_state); + $self->set_linked_devices($link_state); + } else { # Not called by device, send set command my $message = $self->derive_message($p_state); $self->_send_cmd($message); - -# $self->_send_cmd(command => $p_state, -# type => (($self->isa('Insteon::Insteon_Link') and !($self->is_root)) ? 'alllink' : 'standard')); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") if $main::Debug{insteon}; $self->is_acknowledged(0); @@ -379,7 +372,6 @@ sub set $$self{pending_response} = $p_response; } $self->level($p_state) if ($self->isa("Insteon::BaseDevice") && $self->can('level')); # update the level value -# $self->SUPER::set($p_state,$p_setby,$p_response) if defined $p_state; } else { &::print_log("[Insteon::BaseObject] failed state validation with state=$p_state"); } From 453eba54b2d69e9dcbbacb44275c0732a39d4e73 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 15:22:45 -0700 Subject: [PATCH 210/330] Insteon: Result of Request Status Call BaseObject Set_Receive --- lib/Insteon/BaseInsteon.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f7c8225eb..2a01079bf 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -635,11 +635,11 @@ sub _is_info_request . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; $self->level($ack_on_level) if $self->can('level'); # update the level value if ($ack_on_level == 0) { - $self->SUPER::set('off', $ack_setby); + $self->set_receive('off', $ack_setby); } elsif ($ack_on_level > 0 and !($self->isa('Insteon::DimmableLight'))) { - $self->SUPER::set('on', $ack_setby); + $self->set_receive('on', $ack_setby); } else { - $self->SUPER::set($ack_on_level . '%', $ack_setby); + $self->set_receive($ack_on_level . '%', $ack_setby); } # if this were a scene controller, then also propogate the result to all members my $callback; From e9654526f2bc9aa0eef988e4a7fb828e2d5ac27c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 15:26:04 -0700 Subject: [PATCH 211/330] Insteon: Change calls from DeviceController to BaseObject Set --- lib/Insteon/IOLinc.pm | 2 +- lib/Insteon/Lighting.pm | 11 +++++------ lib/Insteon/Security.pm | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index 2983a61b9..6a104e1ee 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -174,7 +174,7 @@ sub set } else { my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - $self->Insteon::BaseDevice::set($link_state, $p_setby, $p_respond); + $self->SUPER::set($link_state, $p_setby, $p_respond); #$$self{momentary_timer}->set(int($$self{momentary_time/10), '$self->Generic_Item::set('off')'); } return; diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 561a7cdc8..b4fe5bf69 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -476,7 +476,7 @@ sub set my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - return $self->Insteon::BaseDevice::set($link_state, $p_setby, $p_respond); + return $self->SUPER::set($link_state, $p_setby, $p_respond); } =back @@ -626,7 +626,7 @@ sub set my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - return $self->Insteon::DeviceController::set($link_state, $p_setby, $p_respond); + return $self->SUPER::set($link_state, $p_setby, $p_respond); } =back @@ -710,7 +710,7 @@ sub set { my ($self, $p_state, $p_setby, $p_respond) = @_; - return $self->Insteon::DeviceController::set($p_state, $p_setby, $p_respond); + return $self->SUPER::set($p_state, $p_setby, $p_respond); } =back @@ -833,8 +833,7 @@ sub set } else { - $link_state = $p_state if $self->can('level'); - return $self->Insteon::DeviceController::set($link_state, $p_setby, $p_respond); + return $self->SUPER::set($link_state, $p_setby, $p_respond); } return 0; @@ -1101,7 +1100,7 @@ sub set { my ($self, $p_state, $p_setby, $p_respond) = @_; if ($self->is_root()){ - return $self->Insteon::DeviceController::set($p_state, $p_setby, $p_respond); + return $self->SUPER::set($p_state, $p_setby, $p_respond); } else { if ($self->_is_valid_state($p_state)) { # always reset the is_locally_set property unless set_by is the device diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index fe97e4014..b549b8c99 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -141,7 +141,7 @@ sub set my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - return $self->Insteon::DeviceController::set($link_state, $p_setby, $p_respond); + return $self->SUPER::set($link_state, $p_setby, $p_respond); } =item C From 3b92ec45fbba8b5ba190f61ce379cc0ad81b2554 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 15:27:21 -0700 Subject: [PATCH 212/330] Insteon: Remove DeviceController Set Routine --- lib/Insteon/BaseInsteon.pm | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 2a01079bf..ee7b78a21 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3365,28 +3365,6 @@ sub new return $self; } -=item C - -If C returns a true value returns that. - -Else, calls C and returns 0 - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - my $rslt_code = $self->Insteon::BaseController::set($p_state, $p_setby, $p_respond); - return $rslt_code if $rslt_code; - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - $self->Insteon::BaseObject::set((($self->is_root) ? $p_state : $link_state), $p_setby, $p_respond); - - return 0; -} - =item C Requests the current status of the device and calls C on the response. From 1d964fef6acf7219c8dec528a4769fccaa23c1ce Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 15:40:09 -0700 Subject: [PATCH 213/330] Add Details for Set_By_To_Target Routine A confusing routine that took me a while to decode. --- docs/mh.pod | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/mh.pod b/docs/mh.pod index 6f994c7a6..f2dfb1f97 100644 --- a/docs/mh.pod +++ b/docs/mh.pod @@ -398,6 +398,22 @@ The list of modules that are not items is L. =over +=item set_by_to_target + +Checks to see if identified device is already in the chain of set_by devices. +Basically identifies never ending loops of setting objects. + +Returns: + +If no_strip is set to true: + +The original object which initiated this action, OR the same set_by +object if it appears in the set_by list already. + +If no_strip is set to false or undef: + +Undef, unless would return something which is not an object, then it returns +the text preceding a [, such as web, email, ... =item add_sound From 965c2d0db9c6c74e99626842df80e58d1bd8bc44 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 16:29:07 -0700 Subject: [PATCH 214/330] Insteon: Remove or Condense Unnecessary Set Routines Many devices had their own set routines that duplicated the functionality of the BaseObject routine. --- lib/Insteon/Controller.pm | 28 ++++---------------- lib/Insteon/IOLinc.pm | 3 --- lib/Insteon/Lighting.pm | 54 --------------------------------------- lib/Insteon/Security.pm | 34 +++++------------------- 4 files changed, 11 insertions(+), 108 deletions(-) diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 871ed28b6..1c06d25f6 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -84,38 +84,20 @@ sub new =item C -Handles messages received from the device. Ignores attempts to set the same state -in less than 1 second. (This can likely be removed as the same process exists -now in BaseInsteon set_receive.) If this is not a duplicate message calls -C. +Handles messages received from the device. Calls C. =cut sub set { my ($self,$p_state,$p_setby,$p_response) = @_; - return if &main::check_for_tied_filters($self, $p_state); - - # Override any set_with_timer requests - if ($$self{set_timer}) { - &Timer::unset($$self{set_timer}); - delete $$self{set_timer}; - } - - # if it can't be controlled (i.e., a responder), then don't send out any signals - # motion sensors seem to get multiple fast reports; don't trigger on both - my $setby_name = $p_setby; - $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - if (not defined($self->get_idle_time) or $self->get_idle_time > 1 or $self->state ne $p_state) { - &::print_log("[Insteon::RemoteLinc] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; + if (ref $p_setby && $p_setby eq $self){ $self->set_receive($p_state,$p_setby); - } else { + } + else { &::print_log("[Insteon::RemoteLinc] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name) deferred due to repeat within 1 second") - if $main::Debug{insteon}; + . " is not a responder and cannot be set to a state."); } - return; } =item C diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index 6a104e1ee..721b244c0 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -152,8 +152,6 @@ and control the relay state. sub set { my ($self, $p_state, $p_setby, $p_respond) = @_; - #Commands sent by the IOLinc itself represent the sensor - #Commands sent by MH to IOLinc represent the relay if (ref $p_setby && $p_setby->isa('Insteon::BaseObject') && $p_setby->equals($self)){ my $curr_milli = sprintf('%.0f', &main::get_tickcount); my $window = 1000; @@ -175,7 +173,6 @@ sub set else { my $link_state = &Insteon::BaseObject::derive_link_state($p_state); $self->SUPER::set($link_state, $p_setby, $p_respond); - #$$self{momentary_timer}->set(int($$self{momentary_time/10), '$self->Generic_Item::set('off')'); } return; } diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index b4fe5bf69..c15511e50 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -461,24 +461,6 @@ sub new return $self; } -=item C - -Handles setting and receiving states from the device. - -NOTE - Maybe this should be moved to BaseLight, or something farther up the stack? -The only thing this routine does is convert p_state with derive_link_state. - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - return $self->SUPER::set($link_state, $p_setby, $p_respond); -} - =back =head2 AUTHOR @@ -611,24 +593,6 @@ sub new return $self; } -=item C - -Handles setting and receiving states from the device. - -NOTE - Maybe this should be moved to BaseLight, or something farther up the stack? -The only thing this routine does is convert p_state with derive_link_state. - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - return $self->SUPER::set($link_state, $p_setby, $p_respond); -} - =back =head2 AUTHOR @@ -695,24 +659,6 @@ sub new return $self; } -=item C - -Handles setting and receiving states from the device. - -NOTE - This is just silly, the only thing this routine does is push the set -command to the L -class. Simply reording the class -inheritance of this object would remove the need to do this. - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - return $self->SUPER::set($p_state, $p_setby, $p_respond); -} - =back =head2 AUTHOR diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index b549b8c99..531159f0c 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -129,21 +129,6 @@ sub new return $self; } -=item C - -Handles setting and receiving states from the device. - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - return $self->SUPER::set($link_state, $p_setby, $p_respond); -} - =item C Only available for Motion Sensor Version 2 models. @@ -787,20 +772,13 @@ Handles messages received from the device. Calls C. sub set { my ($self,$p_state,$p_setby,$p_response) = @_; - return if &main::check_for_tied_filters($self, $p_state); - - # Override any set_with_timer requests - if ($$self{set_timer}) { - &Timer::unset($$self{set_timer}); - delete $$self{set_timer}; + if (ref $p_setby && $p_setby eq $self){ + $self->set_receive($p_state,$p_setby); + } + else { + &::print_log("[Insteon::TriggerLinc] " . $self->get_object_name() + . " is not a responder and cannot be set to a state."); } - - my $setby_name = $p_setby; - $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - &::print_log("[Insteon::TriggerLinc] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; - $self->set_receive($p_state,$p_setby); - return; } =item C From 10da6c84416952be21ead2d4c61b0bc5a1bc3d2c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 12 Oct 2013 18:25:38 -0700 Subject: [PATCH 215/330] Insteon: Only Call Success_Callback if Active_Message Exists --- lib/Insteon/BaseInterface.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index a461b284a..3842c5692 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -718,7 +718,7 @@ sub on_extended_insteon_received } &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $main::Debug{insteon} >=3; if($object->_process_message($self, %msg)) { - if ($self->active_message->success_callback){ + if (ref $self->active_message && $self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; package main; From aaa64e8e71a26f4a990313330bf30a14d3367c62 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sat, 12 Oct 2013 23:41:45 -0400 Subject: [PATCH 216/330] Start changes to make sure only the desired speakers are on, even if they were on before starting speech. --- code/common/pa_control.pl | 8 +++--- lib/PAobj.pm | 55 +++++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index 5f9c87b11..74e23e55c 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -97,14 +97,14 @@ sub pa_parms_stub { } sub pa_control_stub { - my ($parms) = @_; + my (%parms) = @_; my @pazones; - my $mode = $parms->{pa_mode}; + my $mode = $parms{pa_mode}; return if $mode eq 'mute' or $mode eq 'offline'; - my $rooms = $parms->{rooms}; + my $rooms = $parms{rooms}; print_log("PA: control_stub: rooms=$rooms, mode=$mode") if $Debug{pa}; - my $results = $pactrl->audio_hook(ON,%parms); + my $results = $pactrl->audio_hook(ON,\%parms); print_log("PA: control_stub set results: $results") if $Debug{pa} >=2; set $pa_speaker_timer $pa_timer if $results; return $results; diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 1e2d00033..0862edbd8 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -98,6 +98,13 @@ sub init { &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa};# || $#speakers_obj gt -1; &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa};# || $#speakers_audrey gt -1; + $pa_zones{all}{wdio}=join(',',@speakers_wdio); + $pa_zones{all}{x10}=join(',',@speakers_x10); + $pa_zones{all}{xap}=join(',',@speakers_xap); + $pa_zones{all}{xpl}=join(',',@speakers_xpl); + $pa_zones{all}{obj}=join(',',@speakers_obj); + $pa_zones{all}{audrey}=join(',',@speakers_audrey); + if ($#speakers_wdio > -1) { $self->init_weeder(@speakers_wdio); return 0 unless %pa_weeder_max_port; @@ -195,22 +202,22 @@ sub prep_parms &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa} >=2 || $#speakers_audrey gt -1; - $pa_zones{wdio}=join(',',@speakers_wdio); - $pa_zones{x10}=join(',',@speakers_x10); - $pa_zones{xap}=join(',',@speakers_xap); - $pa_zones{xpl}=join(',',@speakers_xpl); - $pa_zones{obj}=join(',',@speakers_obj); - $pa_zones{audrey}=join(',',@speakers_audrey); + $pa_zones{active}{wdio}=join(',',@speakers_wdio); + $pa_zones{active}{x10}=join(',',@speakers_x10); + $pa_zones{active}{xap}=join(',',@speakers_xap); + $pa_zones{active}{xpl}=join(',',@speakers_xpl); + $pa_zones{active}{obj}=join(',',@speakers_obj); + $pa_zones{active}{audrey}=join(',',@speakers_audrey); $parms->{web_file}="web_file" if $#speakers_audrey gt -1; if( 1 - && $pa_zones{wdio} eq '' - && $pa_zones{x10} eq '' - && $pa_zones{xap} eq '' - && $pa_zones{xpl} eq '' - && $pa_zones{obj} eq '' + && $pa_zones{active}{wdio} eq '' + && $pa_zones{active}{x10} eq '' + && $pa_zones{active}{xap} eq '' + && $pa_zones{active}{xpl} eq '' + && $pa_zones{active}{obj} eq '' ) { $$parms{to_file}='/dev/null'; @@ -226,16 +233,16 @@ sub audio_hook my $results = 0; # my @speakers = split(',', $voiceparms{pa_zones}); - my @speakers_wdio=split(',',$pa_zones{wdio}); - my @speakers_x10=split(',',$pa_zones{x10}); - my @speakers_xap=split(',',$pa_zones{xap}); - my @speakers_xpl=split(',',$pa_zones{xpl}); - my @speakers_obj=split(',',$pa_zones{obj}); + my @speakers_wdio=split(',',$pa_zones{active}{wdio}); + my @speakers_x10=split(',',$pa_zones{active}{x10}); + my @speakers_xap=split(',',$pa_zones{active}{xap}); + my @speakers_xpl=split(',',$pa_zones{active}{xpl}); + my @speakers_obj=split(',',$pa_zones{active}{obj}); #TODO: Properly handle $results across multiple types #TODO: Break up the wdio zones based on serial port, in case there are more than one. $results=0; - $results = $self->set_weeder($state,'weeder',@speakers_wdio) if $#speakers_wdio > -1; + $results = $self->set_weeder($state,'weeder',@speakers_wdio) if $#speakers_wdio > -1; $results = $self->set_x10($state,@speakers_x10) if $#speakers_x10 > -1; $results = $self->set_xap($state,\@speakers_xap,\%voiceparms) if $#speakers_xap > -1; $results = $self->set_xpl($state,\@speakers_xpl,\%voiceparms) if $#speakers_xpl > -1; @@ -248,9 +255,9 @@ sub audio_hook if( lc $state eq 'on' && ( - $pa_zones{wdio} ne '' - || $pa_zones{x10} ne '' - || $pa_zones{obj} ne '' + $pa_zones{active}{wdio} ne '' + || $pa_zones{active}{x10} ne '' + || $pa_zones{active}{obj} ne '' ) ) { $results=1; @@ -262,10 +269,10 @@ sub audio_hook sub web_hook { my ($self,$parms) = @_; - &::print_log("PAobj: web_hook! Audrey: " . $pa_zones{audrey}) if $main::Debug{pa}; - return unless $pa_zones{audrey} ne ''; + &::print_log("PAobj: web_hook! Audrey: " . $pa_zones{active}{audrey}) if $main::Debug{pa}; + return unless $pa_zones{active}{audrey} ne ''; my $results=0; - my @speakers_audrey=split(',', $pa_zones{audrey}); + my @speakers_audrey=split(',', $pa_zones{active}{audrey}); $results = $self->set_audrey($parms->{web_file},@speakers_audrey); @@ -469,8 +476,6 @@ sub get_speakers for my $grouproom ($ref->list) { $grouproom = $grouproom->get_object_name; $grouproom =~ s/^\$pa_//; - $grouproom =~ s/^\$paxpl_//; - $grouproom =~ s/^\$paxap_//; &::print_log("PAobj: - member: $grouproom") if $main::Debug{pa} >=2; push(@pazones, $grouproom); } From 1965326b8e1e360e0c2124890d4bcaead6637227 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 10:12:38 -0700 Subject: [PATCH 217/330] Insteon: All_Link Cleanup Check for Active Message Before calling setby on the active_message, make sure that an active message exists. --- lib/Insteon_PLM.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index c97637985..9181f3d85 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -815,7 +815,7 @@ sub _parse_data { my $message_to_string = ($self->active_message) ? $self->active_message->to_string() : ""; &::print_log("[Insteon_PLM] Received all-link cleanup success: $message_to_string") if $main::Debug{insteon}; - if (ref $self->active_message->setby){ + if (ref $self->active_message && ref $self->active_message->setby){ my $object = $self->active_message->setby; $object->is_acknowledged(1); $object->_process_command_stack(); From 43c38bac46bf64ba267452b9464c0d9cc1ace4bc Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 10:34:55 -0700 Subject: [PATCH 218/330] Insteon: Define Derive_Link_State for All Objects Rather than testing and running derive_link_state only if a device is not_dimmable, call derive_link_state for all objects. BaseObject::Derive_Link_State remains the same DimmableLight::Derive_Link_State will return on,off, or 0%-100% from the input link_state. Remove all logic which selectively ran derive_link_state --- lib/Insteon/BaseInsteon.pm | 14 ++++++-------- lib/Insteon/Lighting.pm | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ee7b78a21..ed8a19392 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -53,8 +53,8 @@ state of either ON or OFF. sub derive_link_state { - my ($p_state) = @_; - + my ($self, $p_state) = @_; + $p_state = $self if !(ref $self); #Old code made direct calls my $link_state = 'on'; if ($p_state eq 'off' or $p_state eq 'off_fast') { @@ -359,8 +359,7 @@ sub set &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - $self->set_linked_devices($link_state); + $self->set_linked_devices($p_state); } else { # Not called by device, send set command my $message = $self->derive_message($p_state); $self->_send_cmd($message); @@ -3119,6 +3118,8 @@ sub set_linked_devices # remember the current state to support resume $$self{members}{$member_ref}{resume_state} = $light->state; $member->manual($light, $ramp_rate); + $local_state = $light->derive_link_state($local_state) + if $light->can('derive_link_state'); $light->set_receive($local_state,$self); } else @@ -3133,10 +3134,7 @@ sub set_linked_devices $$self{members}{$member_ref}{resume_state} = $member->state; # if they are Insteon_Device objects, then simply set_receive their state to # the member on level - if (!($member->isa('Insteon::DimmableLight')) and $member->isa('Insteon::BaseLight')) - { - $local_state = &Insteon::BaseObject::derive_link_state($local_state); - } + $local_state = $member->derive_link_state($local_state); $member->set_receive($local_state,$self); } } diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index c15511e50..f4091b6e7 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -165,6 +165,29 @@ my %ramp_h2n = ( '1f' => .1 ); +=item C + +Overrides routine in BaseObject. Takes the various states available to insteon +devices and returns a derived state of on, off, or 0%-100%. + +=cut + +sub derive_link_state +{ + my ($self, $p_state) = @_; + my $link_state = 'on'; + if ($p_state eq 'off' or $p_state eq 'off_fast') + { + $link_state = 'off'; + } + elsif ($p_state =~ /\d+%?/) + { + $p_state =~ /(\d+)%?/; + $link_state = $1 . '%'; + } + return $link_state; +} + =item C Takes ramp_seconds in numeric seconds and returns the hexadecimal value of that From 3ca90371d2c7920a75e3cf820db0b15117504124 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 10:39:05 -0700 Subject: [PATCH 219/330] Insteon: Call Set on Status_Request Response Necessary in order to cause set_linked_devices to run --- lib/Insteon/BaseInsteon.pm | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ed8a19392..7161da741 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -354,8 +354,9 @@ sub set my $setby_name = $p_setby; $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - if (ref $p_setby and ($p_setby eq $self)) - { #If set by device, update MH state + if (ref $p_setby and (($p_setby eq $self) or ($p_setby eq $self->interface))) + { #If set by device, update MH state, + #If set by interface, this was a status_request response &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; @@ -634,11 +635,11 @@ sub _is_info_request . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; $self->level($ack_on_level) if $self->can('level'); # update the level value if ($ack_on_level == 0) { - $self->set_receive('off', $ack_setby); + $self->set('off', $ack_setby); } elsif ($ack_on_level > 0 and !($self->isa('Insteon::DimmableLight'))) { - $self->set_receive('on', $ack_setby); + $self->set('on', $ack_setby); } else { - $self->set_receive($ack_on_level . '%', $ack_setby); + $self->set($ack_on_level . '%', $ack_setby); } # if this were a scene controller, then also propogate the result to all members my $callback; From 93368b4d2ccc51ebec4831f251b24a4c1dacaed3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 10:41:02 -0700 Subject: [PATCH 220/330] Insteon: Condense Not_Responser Set Routines Add logic to BaseObject::Set to test is_responder before trying to send set command. --- lib/Insteon/BaseInsteon.pm | 24 +++++++++++++++--------- lib/Insteon/Controller.pm | 18 ------------------ lib/Insteon/Security.pm | 18 ------------------ 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7161da741..a61944be8 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -362,15 +362,21 @@ sub set $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; $self->set_linked_devices($p_state); } else { # Not called by device, send set command - my $message = $self->derive_message($p_state); - $self->_send_cmd($message); - &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") - if $main::Debug{insteon}; - $self->is_acknowledged(0); - $$self{pending_state} = $p_state; - $$self{pending_setby} = $p_setby; - $$self{pending_response} = $p_response; - } + if ($self->is_responder){ + my $message = $self->derive_message($p_state); + $self->_send_cmd($message); + &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") + if $main::Debug{insteon}; + $self->is_acknowledged(0); + $$self{pending_state} = $p_state; + $$self{pending_setby} = $p_setby; + $$self{pending_response} = $p_response; + } + else { + ::print_log("[Insteon::BaseObject] " . $self->get_object_name() + . " is not a responder and cannot be set to a state."); + } + } $self->level($p_state) if ($self->isa("Insteon::BaseDevice") && $self->can('level')); # update the level value } else { &::print_log("[Insteon::BaseObject] failed state validation with state=$p_state"); diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 1c06d25f6..5d99ce27e 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -82,24 +82,6 @@ sub new return $self; } -=item C - -Handles messages received from the device. Calls C. - -=cut - -sub set -{ - my ($self,$p_state,$p_setby,$p_response) = @_; - if (ref $p_setby && $p_setby eq $self){ - $self->set_receive($p_state,$p_setby); - } - else { - &::print_log("[Insteon::RemoteLinc] " . $self->get_object_name() - . " is not a responder and cannot be set to a state."); - } -} - =item C Only available for RemoteLinc 2 models. diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index 531159f0c..58a05acc6 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -763,24 +763,6 @@ sub new return $self; } -=item C - -Handles messages received from the device. Calls C. - -=cut - -sub set -{ - my ($self,$p_state,$p_setby,$p_response) = @_; - if (ref $p_setby && $p_setby eq $self){ - $self->set_receive($p_state,$p_setby); - } - else { - &::print_log("[Insteon::TriggerLinc] " . $self->get_object_name() - . " is not a responder and cannot be set to a state."); - } -} - =item C Sets the amount of time, in seconds, that the TriggerLinc will remain "awake" From 05b10542e4442acab8ff45df0195602ee08de9b1 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 10:42:30 -0700 Subject: [PATCH 221/330] Insteon: Simplify Link_State Determination in Set_Linked_Devices Makes the code easier to read. Also allows for non standard link_states (0%-100%) to be passed to linked devices. --- lib/Insteon/BaseInsteon.pm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index a61944be8..c09069cc9 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3103,12 +3103,12 @@ sub set_linked_devices foreach my $member_ref (keys %{$$self{members}}) { my $member = $$self{members}{$member_ref}{object}; - my $on_state = $$self{members}{$member_ref}{on_level}; - $on_state = '100%' unless $on_state; - my $local_state = $on_state; - $local_state = 'on' if $local_state eq '100%' - && $member->isa('Insteon::BaseDevice') && !($member->is_root); - $local_state = 'off' if $local_state eq '0%' or $link_state eq 'off'; + # If controller is on, set member to stored on_level + # else set to controller value + my $local_state = $$self{members}{$member_ref}{on_level}; + $local_state = 'on' unless $local_state; + $local_state = $link_state if (lc $link_state ne 'on'); + if ($member->isa('Light_Item')) { # if they are Light_Items, then set their on_dim attrib to the member on level From de355de2d601a1bab326947258fd57ea19e99eaf Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 10:51:58 -0700 Subject: [PATCH 222/330] Insteon: Condense Is_Responder Routines Individual object routines are not needed, just set the flag on creating the object. --- lib/Insteon/Controller.pm | 6 +----- lib/Insteon/Security.pm | 18 ++---------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 5d99ce27e..cf221055e 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -79,6 +79,7 @@ sub new $$self{queue_timer} = new Timer; } bless $self,$class; + $$self{is_responder} = 0; return $self; } @@ -241,11 +242,6 @@ sub _process_message { return $clear_message; } -sub is_responder -{ - return 0; -} - =back =head2 AUTHOR diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index 58a05acc6..91a26b363 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -126,6 +126,7 @@ sub new $$self{queue_timer} = new Timer; } bless $self,$class; + $$self{is_responder} = 0; return $self; } @@ -462,17 +463,6 @@ sub _process_message { return $clear_message; } -=item C - -Always returns 0. - -=cut - -sub is_responder -{ - return 0; -} - =item C Returns a hash of voice commands where the key is the voice command name and the @@ -760,6 +750,7 @@ sub new my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); $$self{message_types} = \%message_types; bless $self,$class; + $$self{is_responder} = 0; return $self; } @@ -853,11 +844,6 @@ sub _process_message { return $clear_message; } -sub is_responder -{ - return 0; -} - =back =head2 INI PARAMETERS From 87c60d53cc35e2f8f6c94361b332f7691c7955a1 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 11:08:07 -0700 Subject: [PATCH 223/330] Insteon: Remove Unnecessary Code from Set Routine Bright and Dim are not valid states for non-dimmable devices and will be rejected. Toggle state rewritten to be much more condensed. --- lib/Insteon/BaseInsteon.pm | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c09069cc9..ab6392624 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -336,20 +336,9 @@ sub set # always reset the is_locally_set property unless set_by is the device $$self{m_is_locally_set} = 0 unless ref $p_setby and $p_setby eq $self; - # handle invalid state for non-dimmable devices - if (($p_state eq 'dim' or $p_state eq 'bright') and !($self->isa('Insteon::DimmableLight'))) { - $p_state = 'on'; - } - elsif ($p_state eq 'toggle') + if ($p_state eq 'toggle') { - if ($self->state eq 'on') - { - $p_state = 'off'; - } - elsif ($self->state eq 'off') - { - $p_state = 'on'; - } + $p_state = ($self->state eq 'on')? 'off' : 'on'; } my $setby_name = $p_setby; @@ -377,7 +366,7 @@ sub set . " is not a responder and cannot be set to a state."); } } - $self->level($p_state) if ($self->isa("Insteon::BaseDevice") && $self->can('level')); # update the level value + $self->level($p_state) if $self->can('level'); # update the level value } else { &::print_log("[Insteon::BaseObject] failed state validation with state=$p_state"); } From 0119aa65ffdb19bcbbe9fba587cdedc1f5002101 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 11:19:38 -0700 Subject: [PATCH 224/330] Insteon: Remove Unnecessary Set Subs in BaseController and InterfaceController InterfaceController will defer to BaseObject No need for routine in BaseController --- lib/Insteon/BaseInsteon.pm | 45 -------------------------------------- 1 file changed, 45 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ab6392624..7f5efed12 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3050,31 +3050,6 @@ sub _process_sync_queue { } } -=item C - -Returns -1 if setby was set by this object. - -Returns -1 if setby a tied_filter. - -Otherwise calls C and returns 0. - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - # prevent reciprocal setby loops - return -1 if (ref $p_setby and ($p_setby ne $self) and $p_setby->can('get_set_by') and - $p_setby->{set_by} eq $self); - return -1 if &main::check_for_tied_filters($self, $p_state); - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - $self->set_linked_devices($link_state); - - return 0; -} - =item C Checks each linked member of device. If the linked member is a C, @@ -3452,26 +3427,6 @@ sub new return $self; } -=item C - -If C returns a true value returns that. - -Else, calls C and returns 0 - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - - my $rslt_code = $self->Insteon::BaseController::set($p_state, $p_setby, $p_respond); - return $rslt_code if $rslt_code; - - $self->Insteon::BaseObject::set($p_state, $p_setby, $p_respond); - - return 0; -} - sub is_root { return 0; From 7bce2ea9651c6b32d73084d004960950298c5f59 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 13:05:43 -0700 Subject: [PATCH 225/330] Insteon: Hijack is_Acknowledged for InterfaceController InterfaceController is different in that when its state is set by an outside object, MH needs to update the linked devices. By moving this to is_acknowledged, the states of the linked devices will only be updated if the scene command was successfull. Is_responder also always needs to be set to true for interfacecontroller even though it is not a root device. --- lib/Insteon/BaseInsteon.pm | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 625e56a27..d15a3104e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -423,6 +423,7 @@ sub set_receive "less than $window milliseconds") if $main::Debug{insteon}; } else { $$self{set_milliseconds} = $curr_milli; + $self->level($p_state) if $self->can('level'); # update the level value $self->SUPER::set($p_state, $p_setby, $p_response); } } @@ -1284,20 +1285,6 @@ sub rate return $$self{rate}; } -=item C - -Updates the device's level if it can level, then calls -C. - -=cut - -sub set_receive -{ - my ($self, $p_state, $p_setby, $p_response) = @_; - $self->level($p_state) if $self->can('level'); # update the level value - $self->SUPER::set_receive($p_state, $p_setby, $p_response); -} - =item C Returns true if the device is a controller. @@ -3430,11 +3417,32 @@ sub new return $self; } + +#Otherwise BaseController will call Generic_Item::Set +sub set +{ + my ($self,$p_state,$p_setby,$p_response) = @_; + $self->Insteon::BaseObject::set($p_state,$p_setby,$p_response); +} + sub is_root { return 0; } +sub is_responder { + return 1; +} + +# For IFaceControllers, need to call set_linked_devices +sub is_acknowledged { + my ($self, $p_ack) = @_; + if ($p_ack) { + $self->set_linked_devices($$self{pending_state}) if defined $$self{pending_state}; + } + return $self->Insteon::BaseObject::is_acknowledged($p_ack); +} + =item C Returns a hash of voice commands where the key is the voice command name and the From 2a75b3beae30e41f14d1448b3b553e2e3e159abc Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 14:04:06 -0700 Subject: [PATCH 226/330] Insteon:Simply Set Routine in KeypadLinc --- lib/Insteon/Lighting.pm | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index f4091b6e7..4d05f04a8 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -777,36 +777,20 @@ subordinate buttons. sub set { my ($self, $p_state, $p_setby, $p_respond) = @_; - - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - - if (!($self->is_root)) + if (!($self->is_root) and !(ref $p_setby && $p_setby eq $self)) { - my $rslt_code = $self->Insteon::BaseController::set($p_state, $p_setby, $p_respond); - return $rslt_code if $rslt_code; - - if (ref $p_setby and $p_setby->isa('Insteon::BaseDevice')) - { - $self->Insteon::BaseObject::set($p_state, $p_setby, $p_respond); - } - elsif (ref $$self{surrogate} && ($$self{surrogate}->isa('Insteon::InterfaceController'))) - { - $$self{surrogate}->set($link_state, $p_setby, $p_respond) - unless ref $p_setby and $p_setby eq $self; + if (ref $$self{surrogate} && ($$self{surrogate}->isa('Insteon::InterfaceController'))) { + $$self{surrogate}->set($p_state, $p_setby, $p_respond); } - else - { - &::print_log("[Insteon::KeyPadLinc] You may not directly attempt to set a keypadlinc's button " - . "unless you have defined a reverse link with the \"surrogate\" keyword"); + else { + ::print_log("[Insteon::KeyPadLinc] You may not directly attempt to set a keypadlinc's button " + ."unless you have defined a reverse link with the \"surrogate\" keyword"); } } else { - return $self->SUPER::set($link_state, $p_setby, $p_respond); + return $self->SUPER::set($p_state, $p_setby, $p_respond); } - - return 0; - } =item C From 2049a85d4031e56bcc3935e5348c5cf4c73beb06 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 14:47:31 -0700 Subject: [PATCH 227/330] Insteon: Replace Set Sub with Derive_Message in FanLinc Trying to keep the code in the two set routines in sync is just asking for trouble. Derive Message allows MH to insert a specialized message into the set command. --- lib/Insteon/Lighting.pm | 69 ++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 4d05f04a8..ca5942e08 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -1042,53 +1042,36 @@ sub new return $self; } -=item C +=item C -Handles setting and receiving states from the device and specifically its -fan object. +Generates set commands for the fan, light requests are passed to BaseObject =cut -sub set +sub derive_message { - my ($self, $p_state, $p_setby, $p_respond) = @_; - if ($self->is_root()){ - return $self->SUPER::set($p_state, $p_setby, $p_respond); - } else { - if ($self->_is_valid_state($p_state)) { - # always reset the is_locally_set property unless set_by is the device - $$self{m_is_locally_set} = 0 unless ref $p_setby and $p_setby eq $self; - - # handle invalid state for non-dimmable devices - my $level = $p_state; - if ($p_state eq 'dim' or $p_state eq 'bright') { - $p_state = 'on'; - } - elsif ($p_state eq 'toggle') - { - $p_state = 'off' if ($self->state eq 'on'); - $p_state = 'on' if ($self->state eq 'off'); - } - $level = '00' if ($p_state eq 'off'); - $level = 'ff' if ($p_state eq 'on'); - # Setting Fan Level - my $setby_name = $p_setby; - $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - my $parent = $self->get_root(); - $level = ::Insteon::DimmableLight::convert_level($level) if ($level ne '00' && $level ne 'ff'); - my $extra = $level ."0200000000000000000000000000"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $parent, 'on', $extra); - $parent->_send_cmd($message); - ::print_log("[Insteon::FanLinc] " . $self->get_object_name() . "::set($p_state, $setby_name)") - if $main::Debug{insteon}; - $self->is_acknowledged(0); - $$self{pending_state} = $p_state; - $$self{pending_setby} = $p_setby; - $$self{pending_response} = $p_respond; - $$parent{child_pending_state} = $self->group(); - } else { - ::print_log("[Insteon::FanLinc] failed state validation with state=$p_state"); - } + my ($self, $p_command, $p_extra) = @_; + if ($self->is_root){ + $self->SUPER::derive_message($self, $p_command, $p_extra); + } + else { + my $level; + + #msg id + my ($command, $subcommand) = split(/:/, $p_command, 2); + $command=lc($command); + + if ($command eq 'on') + { + $command='100'; + } + elsif ($command eq 'off'){ + $command = '00'; + } + $command = ::Insteon::DimmableLight::convert_level($command); + my $extra = $command ."0200000000000000000000000000"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'on', $extra); + return $message; } } @@ -1131,7 +1114,7 @@ sub _is_info_request if ($$parent{child_status_request_pending}) { $is_info_request++; my $child_obj = Insteon::get_object($self->device_id, '02'); - my $child_state = &Insteon::BaseObject::derive_link_state(hex($msg{extra})); + my $child_state = $child_obj->derive_link_state(hex($msg{extra})); &::print_log("[Insteon::FanLinc] received status for " . $child_obj->{object_name} . " of: $child_state " . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; From 10954bb5d9c46284ce8bd89025692da737078fc4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 15:14:17 -0700 Subject: [PATCH 228/330] Insteon: Remove Set Routine from Thermo_i2CS Trying to keep the code in the routine in sync with the BaseObject code is a bad idea. Instead just tie_events to each of the subgroup objects --- lib/Insteon/Thermostat.pm | 58 ++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index f224d0484..f7a6eef45 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -192,6 +192,7 @@ sub new { $self->restore_data('temp','mode','fan_mode','heat_sp','cool_sp'); $$self{m_pending_setpoint} = undef; $$self{message_types} = \%message_types; + $$self{is_responder} = 0; return $self; } @@ -585,33 +586,18 @@ sub init { $$self{message_types} = \%message_types; #Set saved state unique to i2CS devices $self->restore_data('humid', 'cooling', 'heating', 'humidifying', 'dehumidifying', 'high_humid_sp', 'low_humid_sp'); -} - -sub set { - my ($self, $p_state, $p_setby, $p_respond) = @_; - my $root = $self->get_root(); - if (!(ref $p_setby) || !($p_setby->equals($self))) { - ::print_log("[Insteon::Thermo_i2CS] Sorry, you cannot control the ". - "thermostat in this manner. Please read the documentation ". - "for Insteon::Thermostat for help."); - return; + if ($self->group eq 01){ + $self -> tie_event ('$object->_cooling("$state")'); } - #Update the root object state - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); - if ($self->group eq '01'){ - $root->_cooling($link_state); - } elsif ($self->group eq '02') { - $root->_heating($link_state); + $self -> tie_event ('$object->_heating("$state")'); } elsif ($self->group eq '03') { - $root->_dehumidifying($link_state); + $self -> tie_event ('$object->_dehumidifying("$state")'); } elsif ($self->group eq '04') { - $root->_humidifying($link_state); + $self -> tie_event ('$object->_humidifying("$state")'); } - #Update the status of linked devices - $self->set_linked_devices($link_state); } sub sync_links{ @@ -994,35 +980,39 @@ sub _humid { sub _cooling { my ($self,$p_state) = @_; - $$self{cooling} = $p_state; - $self->set_receive('status_change'); - return $$self{cooling}; + my $root = $self->get_root(); + $$root{cooling} = $p_state; + $root->set_receive('status_change'); + return $$root{cooling}; } sub _heating { my ($self,$p_state) = @_; - $$self{heating} = $p_state; - $self->set_receive('status_change'); - return $$self{heating}; + my $root = $self->get_root(); + $$root{heating} = $p_state; + $root->set_receive('status_change'); + return $$root{heating}; } sub _dehumidifying { my ($self,$p_state) = @_; - if ($p_state ne $$self{dehumidifying}) { - $$self{dehumidifying} = $p_state; - $self->set_receive('status_change'); + my $root = $self->get_root(); + if ($p_state ne $$root{dehumidifying}) { + $$root{dehumidifying} = $p_state; + $root->set_receive('status_change'); } - return $$self{dehumidifying}; + return $$root{dehumidifying}; } sub _humidifying { my ($self,$p_state) = @_; - if ($p_state ne $$self{humidifying}) { - $$self{humidifying} = $p_state; - $self->set_receive('status_change'); + my $root = $self->get_root(); + if ($p_state ne $$root{humidifying}) { + $$root{humidifying} = $p_state; + $root->set_receive('status_change'); } - return $$self{humidifying}; + return $$root{humidifying}; } sub _high_humid_sp { From 748459138751b1af9b2c5ad037dd92b3fc95c730 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 15:19:31 -0700 Subject: [PATCH 229/330] Insteon: Cleanup Set Routine in IOLinc No way to avoid hijacking BaseObject::Set --- lib/Insteon/IOLinc.pm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index 721b244c0..814ff0fa3 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -152,7 +152,7 @@ and control the relay state. sub set { my ($self, $p_state, $p_setby, $p_respond) = @_; - if (ref $p_setby && $p_setby->isa('Insteon::BaseObject') && $p_setby->equals($self)){ + if (ref $p_setby && $p_setby->can('equals') && $p_setby->equals($self)){ my $curr_milli = sprintf('%.0f', &main::get_tickcount); my $window = 1000; if ($p_state eq $$self{child_state} && @@ -171,7 +171,6 @@ sub set } } else { - my $link_state = &Insteon::BaseObject::derive_link_state($p_state); $self->SUPER::set($link_state, $p_setby, $p_respond); } return; From 2c250fc7fff84560c0e9fd64348ee264f11d382f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 15:25:45 -0700 Subject: [PATCH 230/330] Insteon: Remove BaseController:Derive_Message Routine It did absolutely nothing --- lib/Insteon/BaseInsteon.pm | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index d15a3104e..ea86dc812 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3199,22 +3199,6 @@ sub initiate_linking_as_controller $self->interface()->initiate_linking_as_controller($p_group, $success_callback, $failure_callback); } -=item C - -Generates and returns a basic on/off message from a command. - -=cut - -sub derive_message -{ - my ($self, $p_state, $p_extra) = @_; - if ($self->is_root) { - return $self->Insteon::BaseObject::derive_message($p_state, $p_extra); - } else { - return $self->Insteon::BaseObject::derive_message($p_state, $p_extra); - } -} - =item C Returns a list of objects that are members of device. If type is specified, only From da5e03e2c7c9a0d437d082e82a1aa489fb735d34 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 15:28:43 -0700 Subject: [PATCH 231/330] Insteon: Replace leading spaces with tabs in BaseObject::Set --- lib/Insteon/BaseInsteon.pm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ea86dc812..e137c82aa 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -337,12 +337,12 @@ sub set $$self{m_is_locally_set} = 0 unless ref $p_setby and $p_setby eq $self; if ($p_state eq 'toggle') - { - $p_state = ($self->state eq 'on')? 'off' : 'on'; - } + { + $p_state = ($self->state eq 'on')? 'off' : 'on'; + } - my $setby_name = $p_setby; - $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); + my $setby_name = $p_setby; + $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); if (ref $p_setby and (($p_setby eq $self) or ($p_setby eq $self->interface))) { #If set by device, update MH state, #If set by interface, this was a status_request response @@ -352,8 +352,8 @@ sub set $self->set_linked_devices($p_state); } else { # Not called by device, send set command if ($self->is_responder){ - my $message = $self->derive_message($p_state); - $self->_send_cmd($message); + my $message = $self->derive_message($p_state); + $self->_send_cmd($message); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") if $main::Debug{insteon}; $self->is_acknowledged(0); From de89b40a152bce0646c030e43b0243be4a52adb1 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 15:38:01 -0700 Subject: [PATCH 232/330] Insteon: Fix Small Bug in IOLinc --- lib/Insteon/IOLinc.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index 814ff0fa3..f40537757 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -171,7 +171,7 @@ sub set } } else { - $self->SUPER::set($link_state, $p_setby, $p_respond); + $self->SUPER::set($p_state, $p_setby, $p_respond); } return; } From c6cf214d1863a60c47bec52aa9e79f8a15c4137a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 13 Oct 2013 20:10:28 -0700 Subject: [PATCH 233/330] Insteon: Use Relative State Change on Manual Dimming Determine relative percentage change based on interval between start and stop manual changes Set device's state based on relative change, no longer request state after change, cuts down on excess messages Set linked device states based on relative change, this permits more accurate representation of dimming of a scene with varying on_levels. For Example, imagine a Scene where Light1 is linked to Light2 such that Light2's on_level is 75%. After the scene is turned on, Light1 is then manually dimmed by 15%, Light2 will then dim by 15% as well and its state will be updated. The resulting states would be Light1 85% and Light2 60%. --- lib/Insteon/BaseInsteon.pm | 62 ++++++++++++++++++++++++++++++-------- lib/Insteon/Message.pm | 2 ++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index e137c82aa..330825171 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -346,10 +346,22 @@ sub set if (ref $p_setby and (($p_setby eq $self) or ($p_setby eq $self->interface))) { #If set by device, update MH state, #If set by interface, this was a status_request response + $self->set_linked_devices($p_state); + if ($p_state =~ /^([+-])(\d+)/) { + #This is a relative state + my $rel_state; + $rel_state = -$2 if ($1 eq '-'); + $rel_state = +$2 if ($1 eq '+'); + my $curr_state = '100'; + $curr_state = '0' if ($self->state eq 'off'); + $curr_state = $1 if $self->state =~ /(\d{1,3})/; + $p_state = $curr_state + $rel_state; + $p_state = 100 if ($p_state > 100); + $p_state = 0 if ($p_state < 0); + } &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; - $self->set_linked_devices($p_state); } else { # Not called by device, send set command if ($self->is_responder){ my $message = $self->derive_message($p_state); @@ -865,10 +877,19 @@ sub _process_message } elsif ($msg{command} eq 'start_manual_change') { - # do nothing; although, maybe anticipate change? we should always get a stop + $$self{manual_direction} = $msg{extra}; + $$self{manual_start} = ::get_tickcount() - (($msg{maxhops}-$msg{hopsleft})*50); } elsif ($msg{command} eq 'stop_manual_change') { - # request status so that the final state can be known - $self->request_status($self); + # Determine percent change based on time interval + my $finish_time = &main::get_tickcount - (($msg{maxhops}-$msg{hopsleft})*50); + my $total_time = $finish_time - $$self{manual_start}; + my $percent_change = int($total_time / 42); + if ($$self{manual_direction} eq '00') { + $percent_change = "-".$percent_change; + } else { + $percent_change = "+".$percent_change; + } + $self->set($percent_change, $self); } elsif ($msg{command} eq 'read_write_aldb') { if ($self->_aldb){ $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; @@ -1000,7 +1021,7 @@ sub _is_valid_state my ($msg, $substate) = split(/:/, $state, 2); $msg=lc($msg); - if ($msg=~/^([1]?[0-9]?[0-9])/) + if ($msg=~/^[+-]?([1]?[0-9]?[0-9])/) { if ($1 < 1) { $msg='off'; @@ -3060,8 +3081,27 @@ sub set_linked_devices # If controller is on, set member to stored on_level # else set to controller value my $local_state = $$self{members}{$member_ref}{on_level}; - $local_state = 'on' unless $local_state; - $local_state = $link_state if (lc $link_state ne 'on'); + $local_state = '100' unless $local_state; + + if (lc $link_state ne 'on'){ + # Not setting member to default + if ($link_state =~ /^([+-])(\d+)/) { + #This is a relative state + my $rel_state; + $rel_state = -$2 if ($1 eq '-'); + $rel_state = +$2 if ($1 eq '+'); + my $curr_state = '100'; + $curr_state = '0' if ($member->state eq 'off'); + $curr_state = $1 if $member->state =~ /(\d{1,3})/; + $local_state = $curr_state + $rel_state; + $local_state = 100 if ($local_state > 100); + $local_state = 0 if ($local_state < 0); + } + else { + #An absolute state + $local_state = $link_state; + } + } if ($member->isa('Light_Item')) { @@ -3091,17 +3131,15 @@ sub set_linked_devices } elsif ($member->isa('Insteon::BaseDevice')) { - # remember the current state to support resume + # remember the current state to support resume $$self{members}{$member_ref}{resume_state} = $member->state; - # if they are Insteon_Device objects, then simply set_receive their state to - # the member on level + # if they are Insteon_Device objects, then simply set_receive their state to + # the member on level $local_state = $member->derive_link_state($local_state); $member->set_receive($local_state,$self); } } } - - } =item C diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index fea11d116..38e4e43fd 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -434,6 +434,8 @@ sub command_to_hash { $msg{type} = 'alllink'; $msg{group} = substr($p_state,10,2); + $msg{extra} = substr($p_state,16,2) + if (length($p_state) >= 18); } else { From 9232a5b3b3b5f617ce312741cb20b0cfd715ed79 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 14 Oct 2013 21:12:26 -0400 Subject: [PATCH 234/330] Added support for the aviosys USB Power 8840 relay board --- lib/PAobj.pm | 63 +++++++++++++++++++++++++++++++++++++-------- lib/read_table_A.pl | 6 ++++- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 0862edbd8..e79680945 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -63,7 +63,7 @@ sub init { $self->check_group('default'); my @speakers = $self->get_speakers('allspeakers'); - my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey); + my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey,@speakers_aviosys); for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); @@ -89,21 +89,27 @@ sub init { if($type eq 'audrey') { push(@speakers_audrey,$room); } + if($type eq 'aviosys') { + push(@speakers_aviosys,$room); + } } &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa};# || $#speakers_wdio gt -1; &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa};# || $#speakers_x10 gt -1; &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa};# || $#speakers_xap gt -1; &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa};# || $#speakers_xpl gt -1; - &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa};# || $#speakers_obj gt -1; &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa};# || $#speakers_audrey gt -1; + &::print_log("PAobj: speakers_aviosys: $#speakers_aviosys") if $main::Debug{pa};# || $#speakers_aviosys gt -1; + &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa};# || $#speakers_obj gt -1; + $pa_zones{all}{wdio}=join(',',@speakers_wdio); $pa_zones{all}{x10}=join(',',@speakers_x10); $pa_zones{all}{xap}=join(',',@speakers_xap); $pa_zones{all}{xpl}=join(',',@speakers_xpl); - $pa_zones{all}{obj}=join(',',@speakers_obj); $pa_zones{all}{audrey}=join(',',@speakers_audrey); + $pa_zones{all}{aviosys}=join(',',@speakers_aviosys); + $pa_zones{all}{obj}=join(',',@speakers_obj); if ($#speakers_wdio > -1) { $self->init_weeder(@speakers_wdio); @@ -163,7 +169,7 @@ sub prep_parms $parms->{pa_zones} = join(',', @speakers); - my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey); + my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey,@speakers_aviosys); for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); @@ -184,30 +190,36 @@ sub prep_parms &::print_log("PAobj: speakers_xpl: Adding $room") if $main::Debug{pa} >=3; push(@speakers_xpl,$room); } - if($type eq 'object') { - &::print_log("PAobj: speakers_object: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_obj,$room); - } if($type eq 'audrey') { &::print_log("PAobj: speakers_audrey: Adding $room") if $main::Debug{pa} >=3; push(@speakers_audrey,$room); } + if($type eq 'aviosys') { + &::print_log("PAobj: speakers_aviosys: Adding $room") if $main::Debug{pa} >=3; + push(@speakers_aviosys,$room); + } + if($type eq 'object') { + &::print_log("PAobj: speakers_object: Adding $room") if $main::Debug{pa} >=3; + push(@speakers_obj,$room); + } } &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} >=2 || $#speakers_wdio gt -1; &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa} >=2 || $#speakers_x10 gt -1; &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa} >=2 || $#speakers_xap gt -1; &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} >=2 || $#speakers_xpl gt -1; - &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa} >=2 || $#speakers_obj gt -1; &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa} >=2 || $#speakers_audrey gt -1; + &::print_log("PAobj: speakers_aviosys: $#speakers_aviosys") if $main::Debug{pa} >=2 || $#speakers_aviosys gt -1; + &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa} >=2 || $#speakers_obj gt -1; $pa_zones{active}{wdio}=join(',',@speakers_wdio); $pa_zones{active}{x10}=join(',',@speakers_x10); $pa_zones{active}{xap}=join(',',@speakers_xap); $pa_zones{active}{xpl}=join(',',@speakers_xpl); - $pa_zones{active}{obj}=join(',',@speakers_obj); $pa_zones{active}{audrey}=join(',',@speakers_audrey); + $pa_zones{active}{aviosys}=join(',',@speakers_aviosys); + $pa_zones{active}{obj}=join(',',@speakers_obj); $parms->{web_file}="web_file" if $#speakers_audrey gt -1; @@ -217,6 +229,7 @@ sub prep_parms && $pa_zones{active}{x10} eq '' && $pa_zones{active}{xap} eq '' && $pa_zones{active}{xpl} eq '' + && $pa_zones{active}{aviosys} eq '' && $pa_zones{active}{obj} eq '' ) { @@ -237,15 +250,17 @@ sub audio_hook my @speakers_x10=split(',',$pa_zones{active}{x10}); my @speakers_xap=split(',',$pa_zones{active}{xap}); my @speakers_xpl=split(',',$pa_zones{active}{xpl}); + my @speakers_aviosys=split(',',$pa_zones{active}{aviosys}); my @speakers_obj=split(',',$pa_zones{active}{obj}); #TODO: Properly handle $results across multiple types - #TODO: Break up the wdio zones based on serial port, in case there are more than one. + #TODO: Break up the wdio and aviosys zones based on serial port, in case there are more than one. $results=0; $results = $self->set_weeder($state,'weeder',@speakers_wdio) if $#speakers_wdio > -1; $results = $self->set_x10($state,@speakers_x10) if $#speakers_x10 > -1; $results = $self->set_xap($state,\@speakers_xap,\%voiceparms) if $#speakers_xap > -1; $results = $self->set_xpl($state,\@speakers_xpl,\%voiceparms) if $#speakers_xpl > -1; + $results = $self->set_aviosys($state,'aviosys',@speakers_aviosys) if $#speakers_aviosys > -1; $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; &::print_log("PAobj: set results: $results") if $main::Debug{pa}; @@ -257,6 +272,7 @@ sub audio_hook && ( $pa_zones{active}{wdio} ne '' || $pa_zones{active}{x10} ne '' + || $pa_zones{active}{aviosys} ne '' || $pa_zones{active}{obj} ne '' ) ) { @@ -453,6 +469,31 @@ sub get_weeder_string return $card . "W$weeder_code"; } +sub set_aviosys +{ + my ($self,$state,$aviosys_port,@speakers) = @_; + my $aviosysref = {'on' => {'1' => '!','2' => '#','3' => '%','4' => '&','5' => '(','6' => '_','7' => '{','8' => '}' },'off' => {'1' => '@','2' => '$','3' => '^','4' => '*','5' => ')','6' => '-','7' => '[','8' => ']'}}; + my %aviosys_ref; + my $aviosys_command=''; + for my $room (@speakers) { + &::print_log("PAobj: set_aviosys: " . $room . " / " . $state) if $main::Debug{pa} >=3; + my $ref = &::get_object_by_name('pa_'.$room); + if ($ref) { + my ($id) = $ref->get_address(); + $aviosys_command .= $aviosysref->{$state}{$id}; + &::print_log("PAobj: port: $id, Room: $room") if $main::Debug{pa} >=2; + } else { + &::print_log("PAobj: Unable to locate object for: pa_$room"); + } + } + + return 0 unless $aviosys_command; + &::print_log("PAobj: Sending $aviosys_command to the aviosys card") if $main::Debug{pa}; + #$aviosys_command =~ s/\\r/\r/g; + #&Serial_Item::send_serial_data($aviosys_port, $aviosys_command) if $main::Serial_Ports{$aviosys_port}{object}; + return 1; +} + sub get_speakers { my ($self,$rooms) = @_; diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index f924d1343..fb3282303 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -573,6 +573,10 @@ sub read_table_A { $code .= sprintf "\$%-35s -> target_address('%s');\n",$name.'_obj',$address; $code .= sprintf "\$%-35s -> class_name('%s');\n",$name.'_obj',$serial; $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n",$name,$name.'_obj'; + } elsif (lc $pa_type eq 'aviosys') { + my $aviosysref = {'on' => {'1' => '!','2' => '#','3' => '%','4' => '&','5' => '(','6' => '_','7' => '{','8' => '}' },'off' => {'1' => '@','2' => '$','3' => '^','4' => '*','5' => ')','6' => '-','7' => '[','8' => ']'}}; + $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n",$name.'_obj',$aviosysref->{'on'}{$address},$serial; + $code .= sprintf "\$%-35s -> add ('%s','off');\n",$name.'_obj',$aviosysref->{'off'}{$address}; } else { print "\nUnrecognized .mht entry for PA: $record\n"; return; @@ -1134,4 +1138,4 @@ sub read_table_A { # Revision 1.3 2000/10/01 23:29:40 winter # - 2.29 release # -# \ No newline at end of file +# From 19e06d14f11c11a15dc2186068de9a2363b83162 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 14 Oct 2013 22:34:33 -0400 Subject: [PATCH 235/330] Add support for multiple serial ports for weeder and aviosys. Fix erroneous code comment. --- lib/PAobj.pm | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/PAobj.pm b/lib/PAobj.pm index e79680945..eceac04fd 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -244,23 +244,39 @@ sub audio_hook { my ($self,$state,%voiceparms) = @_; my $results = 0; -# my @speakers = split(',', $voiceparms{pa_zones}); - my @speakers_wdio=split(',',$pa_zones{active}{wdio}); + my (%speakers_aviosys,%speakers_wdio); my @speakers_x10=split(',',$pa_zones{active}{x10}); my @speakers_xap=split(',',$pa_zones{active}{xap}); my @speakers_xpl=split(',',$pa_zones{active}{xpl}); - my @speakers_aviosys=split(',',$pa_zones{active}{aviosys}); my @speakers_obj=split(',',$pa_zones{active}{obj}); - + #TODO: Properly handle $results across multiple types - #TODO: Break up the wdio and aviosys zones based on serial port, in case there are more than one. + $results=0; - $results = $self->set_weeder($state,'weeder',@speakers_wdio) if $#speakers_wdio > -1; $results = $self->set_x10($state,@speakers_x10) if $#speakers_x10 > -1; $results = $self->set_xap($state,\@speakers_xap,\%voiceparms) if $#speakers_xap > -1; $results = $self->set_xpl($state,\@speakers_xpl,\%voiceparms) if $#speakers_xpl > -1; - $results = $self->set_aviosys($state,'aviosys',@speakers_aviosys) if $#speakers_aviosys > -1; + + for my $room (split(',',$pa_zones{active}{aviosys})) { + my $ref = &::get_object_by_name('pa_'.$room); + my $serial=$ref->get_serial(); + &::print_log("PAobj: aviosys serial: " . $room . " / " . $serial) if $main::Debug{pa} >=3; + push(@{$speakers_aviosys{$serial}},$room); + } + foreach my $serial (keys(%speakers_aviosys)) { + $results = $self->set_aviosys($state,$serial,@{$speakers_aviosys{$serial}}); + } + for my $room (split(',',$pa_zones{active}{wdio})) { + my $ref = &::get_object_by_name('pa_'.$room); + my $serial=$ref->get_serial(); + &::print_log("PAobj: wdio serial: " . $room . " / " . $serial) if $main::Debug{pa} >=3; + push(@{$speakers_wdio{$serial}},$room); + } + foreach my $serial (keys(%speakers_wdio)) { + $results = $self->set_weeder($state,$serial,@{$speakers_wdio{$serial}}); + } + $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; &::print_log("PAobj: set results: $results") if $main::Debug{pa}; @@ -490,7 +506,7 @@ sub set_aviosys return 0 unless $aviosys_command; &::print_log("PAobj: Sending $aviosys_command to the aviosys card") if $main::Debug{pa}; #$aviosys_command =~ s/\\r/\r/g; - #&Serial_Item::send_serial_data($aviosys_port, $aviosys_command) if $main::Serial_Ports{$aviosys_port}{object}; + &Serial_Item::send_serial_data($aviosys_port, $aviosys_command) if $main::Serial_Ports{$aviosys_port}{object}; return 1; } From fd3270da684ac159371cc3242dd39728d343309f Mon Sep 17 00:00:00 2001 From: Pmatis Date: Tue, 15 Oct 2013 13:21:24 -0400 Subject: [PATCH 236/330] Added debug print_log calls for serial port loops --- lib/PAobj.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/PAobj.pm b/lib/PAobj.pm index eceac04fd..68603e594 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -265,6 +265,7 @@ sub audio_hook push(@{$speakers_aviosys{$serial}},$room); } foreach my $serial (keys(%speakers_aviosys)) { + &::print_log("PAobj: calling set for aviosys serial port: $serial") if $main::Debug{pa} >=3; $results = $self->set_aviosys($state,$serial,@{$speakers_aviosys{$serial}}); } for my $room (split(',',$pa_zones{active}{wdio})) { @@ -274,6 +275,7 @@ sub audio_hook push(@{$speakers_wdio{$serial}},$room); } foreach my $serial (keys(%speakers_wdio)) { + &::print_log("PAobj: calling set for wdio serial port: $serial") if $main::Debug{pa} >=3; $results = $self->set_weeder($state,$serial,@{$speakers_wdio{$serial}}); } From 8ffe6d00f6fbbc4efaba46ffac52a1a24ba23335 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 15 Oct 2013 17:37:00 -0700 Subject: [PATCH 237/330] Insteon: Move Relative State Conversion to Derive Link State Function Cuts down on duplication of code --- lib/Insteon/BaseInsteon.pm | 44 +++++++++++--------------------------- lib/Insteon/Lighting.pm | 11 ++++++++++ 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 330825171..1f5f10720 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -55,6 +55,17 @@ sub derive_link_state { my ($self, $p_state) = @_; $p_state = $self if !(ref $self); #Old code made direct calls + #Convert Relative State to Absolute State + if ($p_state =~ /^([+-])(\d+)/) { + my $rel_state = $1 . $2; + my $curr_state = '100'; + $curr_state = '0' if ($self->state eq 'off'); + $curr_state = $1 if $self->state =~ /(\d{1,3})/; + $p_state = $curr_state + $rel_state; + $p_state = 'on' if ($p_state > 0); + $p_state = 'off' if ($p_state <= 0); + } + my $link_state = 'on'; if ($p_state eq 'off' or $p_state eq 'off_fast') { @@ -347,18 +358,7 @@ sub set { #If set by device, update MH state, #If set by interface, this was a status_request response $self->set_linked_devices($p_state); - if ($p_state =~ /^([+-])(\d+)/) { - #This is a relative state - my $rel_state; - $rel_state = -$2 if ($1 eq '-'); - $rel_state = +$2 if ($1 eq '+'); - my $curr_state = '100'; - $curr_state = '0' if ($self->state eq 'off'); - $curr_state = $1 if $self->state =~ /(\d{1,3})/; - $p_state = $curr_state + $rel_state; - $p_state = 100 if ($p_state > 100); - $p_state = 0 if ($p_state < 0); - } + $p_state = $self->derive_link_state($p_state); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; @@ -3083,26 +3083,6 @@ sub set_linked_devices my $local_state = $$self{members}{$member_ref}{on_level}; $local_state = '100' unless $local_state; - if (lc $link_state ne 'on'){ - # Not setting member to default - if ($link_state =~ /^([+-])(\d+)/) { - #This is a relative state - my $rel_state; - $rel_state = -$2 if ($1 eq '-'); - $rel_state = +$2 if ($1 eq '+'); - my $curr_state = '100'; - $curr_state = '0' if ($member->state eq 'off'); - $curr_state = $1 if $member->state =~ /(\d{1,3})/; - $local_state = $curr_state + $rel_state; - $local_state = 100 if ($local_state > 100); - $local_state = 0 if ($local_state < 0); - } - else { - #An absolute state - $local_state = $link_state; - } - } - if ($member->isa('Light_Item')) { # if they are Light_Items, then set their on_dim attrib to the member on level diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index ca5942e08..a4e23830a 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -175,6 +175,17 @@ devices and returns a derived state of on, off, or 0%-100%. sub derive_link_state { my ($self, $p_state) = @_; + #Convert Relative State to Absolute State + if ($p_state =~ /^([+-])(\d+)/) { + my $rel_state = $1 . $2; + my $curr_state = '100'; + $curr_state = '0' if ($self->state eq 'off'); + $curr_state = $1 if $self->state =~ /(\d{1,3})/; + $p_state = $curr_state + $rel_state; + $p_state = 100 if ($p_state > 100); + $p_state = 0 if ($p_state < 0); + } + my $link_state = 'on'; if ($p_state eq 'off' or $p_state eq 'off_fast') { From fd29fe67be8e936e45211baafa7f050b1e09883f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 15 Oct 2013 17:38:00 -0700 Subject: [PATCH 238/330] Insteon: Move DeviceController Down in Inheritance Otherwise calling the set commands on these objects results in calling the Generic_Item::Set command. --- lib/Insteon/Controller.pm | 2 +- lib/Insteon/Irrigation.pm | 2 +- lib/Insteon/Security.pm | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index cf221055e..368de52ae 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -54,7 +54,7 @@ package Insteon::RemoteLinc; use strict; use Insteon::BaseInsteon; -@Insteon::RemoteLinc::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); +@Insteon::RemoteLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); my %message_types = ( %Insteon::BaseDevice::message_types, diff --git a/lib/Insteon/Irrigation.pm b/lib/Insteon/Irrigation.pm index 50633d7a9..1bd7209f4 100755 --- a/lib/Insteon/Irrigation.pm +++ b/lib/Insteon/Irrigation.pm @@ -59,7 +59,7 @@ use Insteon::BaseInsteon; package Insteon::Irrigation; -@Insteon::Irrigation::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); +@Insteon::Irrigation::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); our %message_types = ( %Insteon::BaseDevice::message_types, diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index 91a26b363..a51be4db7 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -108,7 +108,7 @@ package Insteon::MotionSensor; use strict; use Insteon::BaseInsteon; -@Insteon::MotionSensor::ISA = ('Insteon::DeviceController','Insteon::BaseDevice'); +@Insteon::MotionSensor::ISA = ('Insteon::BaseDevice', 'Insteon::DeviceController'); =item C From f159e820edd64985aa89b9b35229dfc13a3669fe Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 15 Oct 2013 17:39:00 -0700 Subject: [PATCH 239/330] Insteon: Fix Typos and Remove a Few Excess Items Convert @lights[0] code to $lights[0] to get rid of perl warning. Add space in a debug print log. Ger rid of unnecessary if statement. --- lib/Insteon/BaseInsteon.pm | 6 +++--- lib/Insteon/BaseInterface.pm | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 1f5f10720..a5d73dcc5 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -361,7 +361,7 @@ sub set $p_state = $self->derive_link_state($p_state); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; - $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; + $self->set_receive($p_state,$p_setby,$p_response); } else { # Not called by device, send set command if ($self->is_responder){ my $message = $self->derive_message($p_state); @@ -3095,7 +3095,7 @@ sub set_linked_devices my @lights = $member->find_members('Insteon::BaseDevice'); if (@lights) { - my $light = @lights[0]; + my $light = $lights[0]; # remember the current state to support resume $$self{members}{$member_ref}{resume_state} = $light->state; $member->manual($light, $ramp_rate); @@ -3175,7 +3175,7 @@ sub update_members # if they are Light_Items, then locate the Light_Item's Insteon_Device member my @lights = $member->find_members('Insteon::BaseDevice'); if (@lights) { - $device = @lights[0]; + $device = $lights[0]; } } elsif ($member->isa('Insteon::BaseDevice')) { $device = $member; diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 3842c5692..5d0faf805 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -616,7 +616,7 @@ sub on_standard_insteon_received else { &main::print_log("[Insteon::BaseInterface] ERROR: received cleanup message from " - . $setby_object->get_object_name . "that does not correspond to a valid PLM group. Corrupted message is assumed " + . $setby_object->get_object_name . " that does not correspond to a valid PLM group. Corrupted message is assumed " . "and will be skipped! Was group " . $msg{extra}); $setby_object->corrupt_count_log(1) if $setby_object->can('corrupt_count_log'); } From e9264c8124b7939c23fb6c92562a88105b4e1797 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 15 Oct 2013 17:42:00 -0700 Subject: [PATCH 240/330] Insteon: Call Derive_Link_State on Passed State in Set_Linked_Devices Prior commit inserted a bug where the state of linked devices was always set to on. --- lib/Insteon/BaseInsteon.pm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index a5d73dcc5..a9fb6eab3 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3099,8 +3099,9 @@ sub set_linked_devices # remember the current state to support resume $$self{members}{$member_ref}{resume_state} = $light->state; $member->manual($light, $ramp_rate); - $local_state = $light->derive_link_state($local_state) - if $light->can('derive_link_state'); + if ($light->can('derive_link_state') && lc $link_state ne 'on'){ + $local_state = $light->derive_link_state($link_state); + } $light->set_receive($local_state,$self); } else @@ -3115,7 +3116,9 @@ sub set_linked_devices $$self{members}{$member_ref}{resume_state} = $member->state; # if they are Insteon_Device objects, then simply set_receive their state to # the member on level - $local_state = $member->derive_link_state($local_state); + if (lc $link_state ne 'on'){ + $local_state = $member->derive_link_state($link_state); + } $member->set_receive($local_state,$self); } } From 6e1c3d0f847352a42f0ed977f8fb0c68d52215eb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 15 Oct 2013 17:43:00 -0700 Subject: [PATCH 241/330] Insteon: Set Device Before Setting Linked Devices; Don't Set Linked Devices on PLM Status Request Need to set the main device state first before setting linked devices, otherwise you can get set_by loop errors if you have a three way controller. Don't change the state of linked devices if the PLM requested the status. As turning off the linked devices would not always make sense. --- lib/Insteon/BaseInsteon.pm | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index a9fb6eab3..c4e909cc5 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -354,15 +354,21 @@ sub set my $setby_name = $p_setby; $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - if (ref $p_setby and (($p_setby eq $self) or ($p_setby eq $self->interface))) + if (ref $p_setby and $p_setby eq $self) { #If set by device, update MH state, - #If set by interface, this was a status_request response + my $derived_state = $self->derive_link_state($p_state); + &::print_log("[Insteon::BaseObject] " . $self->get_object_name() + . "::set_receive($derived_state, $setby_name)") if $main::Debug{insteon}; + $self->set_receive($derived_state,$p_setby,$p_response); $self->set_linked_devices($p_state); - $p_state = $self->derive_link_state($p_state); + } + elsif (ref $p_setby and $p_setby eq $self->interface) + { #If set by interface, this was a manual status_request response &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; $self->set_receive($p_state,$p_setby,$p_response); - } else { # Not called by device, send set command + } + else { # Not called by device, send set command if ($self->is_responder){ my $message = $self->derive_message($p_state); $self->_send_cmd($message); From a8f31f33058ca0fd8b00d9aaf7fe48949dce40f1 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Tue, 15 Oct 2013 23:24:11 -0400 Subject: [PATCH 242/330] Embed speech clash resolution into pa_control.pl. Works better and speaks more reliably in desired rooms, due to correcting a race condition. --- code/common/pa_control.pl | 48 +++++++++++++++++++++++++++++++-------- lib/PAobj.pm | 19 ++++++++++++++-- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index 74e23e55c..3418fb753 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -35,7 +35,9 @@ #noloop=start my $pa_delay = $config_parms{pa_delay}; +my $pa_clash_delay = $config_parms{pa_clash_delay}; my $pa_timer = $config_parms{pa_timer}; +$pa_clash_delay = 1 unless $pa_clash_delay; $pa_delay = 0.5 unless $pa_delay; $pa_timer = 60 unless $pa_timer; $pactrl = new PAobj(); @@ -83,17 +85,42 @@ sub pa_parms_stub { } $parms->{pa_mode} = $mode; return if $mode eq 'mute' or $mode eq 'offline'; - - my $results = $pactrl->prep_parms($parms); - my %pa_zones = $pactrl->get_pa_zones(); - if (defined $pa_zones{audrey} && $pa_zones{audrey} ne '') { - print_log("PA: audrey zone detected, hooking via web_hook. (".$pa_zones{audrey}.")") if $Debug{pa}; - push(@{$parms->{web_hook}},\&pa_web_hook); - } + if($pactrl->active(1)) { + my $results = $pactrl->prep_parms($parms); + my %pa_zones = $pactrl->get_pa_zones(); + + if (defined $pa_zones{audrey} && $pa_zones{audrey} ne '') { + print_log("PA: audrey zone detected, hooking via web_hook. (".$pa_zones{audrey}.")") if $Debug{pa}; + push(@{$parms->{web_hook}},\&pa_web_hook); + } + + print_log("PA: parms_stub set results: $results") if $Debug{pa} >=2; + + } else { + #MH is already speaking, and other PA zones are already active. Delay speech. + if ($main::Debug{voice}) { + $$parms{clash_retry}=0 unless $$parms{clash_retry}; + &print_log("PA SPEECH CLASH($$parms{clash_retry}): Delaying speech call for " . $$parms{text} . "\n") unless $$parms{clash_retry} lt 1; + $$parms{clash_retry}++; #To track how many loops are made + } + $$parms{nolog}=1; #To stop MH from logging the speech again + + my $parmstxt; + my ($pkey,$pval); + while (($pkey,$pval) = each(%{$parms})) { + $parmstxt.=', ' if $parmstxt; + $parmstxt .= "$pkey => q($pval)"; + } + &print_log("PA SPEECH CLASH Parameters: $parmstxt") if $main::Debug{voice} && $$parms{clash_retry} eq 0; + &run_after_delay($pa_clash_delay, "speak($parmstxt)"); - print_log("PA: parms_stub set results: $results") if $Debug{pa} >=2; - + $$parms{no_speak}=1; #To stop MH from speaking this time around + return; + } + if ($$parms{clash_retry}) { + &print_log("PA SPEECH CLASH: Resolved, continuing speech."); + } } sub pa_control_stub { @@ -121,6 +148,7 @@ sub pa_web_hook { unset $pa_speaker_timer; print_log("PA: Turning speakers off") if $Debug{pa}; $pactrl->audio_hook(OFF,'normal'); + $pactrl->active(0); } #Setup Fail-safe speaker shutoff @@ -139,7 +167,7 @@ sub pa_web_hook { # #Type Address Name Groups Serial Type # -PA, AA, kitchen, all|default|mainfloor, weeder, wdio +PA, AA, kitchen, all|default|mainfloor, weeder, wdio PA, AB, server, all|basement, weeder, wdio PA, AG, master, all|default|upstairs, weeder2, wdio_old PA, B12, garage, all|outside, , X10 diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 68603e594..8091f851c 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -33,6 +33,8 @@ package PAobj; @PAobj::ISA = ('Generic_Item'); +my $active; + sub last_char { my ($self,$string) = @_; @@ -61,6 +63,7 @@ sub init { return 0; } $self->check_group('default'); + $self->active(0); my @speakers = $self->get_speakers('allspeakers'); my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey,@speakers_aviosys); @@ -113,7 +116,6 @@ sub init { if ($#speakers_wdio > -1) { $self->init_weeder(@speakers_wdio); - return 0 unless %pa_weeder_max_port; } if ($pa_zone_types{'x10'}) { &::print_log("PAobj: x10 PA type initialized...") if $main::Debug{pa}; @@ -154,10 +156,23 @@ sub init_weeder %pa_weeder_max_port = %weeder_max; } +sub active +{ + my ($self,$setactive) = @_; + &::print_log("PAobj: setactive: active: $active / set: $setactive\n") if $main::Debug{pa} >=4; + return $active unless defined $setactive; + if($active && $setactive) { + &::print_log("PAobj: Cannot make active, already active\n") if $main::Debug{pa} >=3; + return 0; + } + &::print_log("PAobj: setting active to: ".$setactive."\n") if $main::Debug{pa} >=3; + $active=$setactive; + return 1; +} + sub prep_parms { my ($self,$parms) = @_; - #my $self = {}; &::print_log("PAobj: delay: $$self{pa_delay}\n") if $main::Debug{pa} >=3; &::print_log("PAobj: set,mode: " . $parms->{mode} . ",rooms: " . $parms->{rooms}) if $main::Debug{pa} >=3; From de9e7d88d50e2aa5f8085dda8a7fb77a47665542 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Wed, 16 Oct 2013 00:35:57 -0400 Subject: [PATCH 243/330] Clean up lib/Audrey_Play.pm, add proper description. --- lib/Audrey_Play.pm | 73 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/lib/Audrey_Play.pm b/lib/Audrey_Play.pm index 550739e14..78cdc48e0 100644 --- a/lib/Audrey_Play.pm +++ b/lib/Audrey_Play.pm @@ -8,19 +8,23 @@ B - This object can be used to play sound files on the Audrey. =head1 SYNOPSIS +Created for use with PAobj.pm, but can be used separately. -blah blah - +=head1 DESCRIPTION +Tells an Audrey to download and play a file, already in the data/web folder, +by passing the name of the file. The Audrey must be modified to respond to +this request. -=head1 DESCRIPTION +$audrey1 = new Audrey_Play('192.168.0.11'); +#Create file data/web/tempfile.wav - perhaps by speaking to a file? +my $speakFile = 'tempfile.wav'; +$audrey1->play($speakFile); =head1 INHERITS B -=head1 METHODS - =over =cut @@ -29,10 +33,6 @@ B my $address; -sub Init { - #&::MainLoop_pre_add_hook( \&Weather_Item::check_weather, 1 ); -} - =item C $ip is the IP address of the Audrey. @@ -62,4 +62,57 @@ sub play { &::run("get_url -quiet http://" . $self->{address} . "/mhspeak.shtml?http://" . $MHWeb . "/" . $web_file . " /dev/null"); } -1; \ No newline at end of file +1; + +#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=begin Audrey Config + +Exerpt from audreyspeak.pl + +You must make certain modifications to your Audrey, as follows: + +- Update the software and obtain root shell access capabilities (this + should be available by using Bruce's CF card image or by following + instructions available on the internet.) + +- Open the Audrey's web server to outside http requests + 1) Start the "Root Shell" + 2) type: cd /config + 3) type: cp rm-apps rm-apps.copy + 4) type: vi rm-apps + You'll be in the editor, editing the "rm-apps" file + About the 14th line down is "rb,/kojak/kojak-slinger, -c -e -s -i 127.1" + You need to delete the "-i 127.1" from the line. + To do this, place the cursor under the space right after the "-s" + Type the "x" key to start deleting from the line. + The line should end up looking like this: + "rb,/kojak/kojak-slinger, -c -e -s" + If you need to start over type a colon to get to the vi command line + At the colon prompt type "q!" and hit "enter" (this quits without saving) + If it looks good then at the colon prompt type "wq" to save changes + Now restart the Audrey by unplugging it, waiting 30 seconds and + plugging it back in. + +- Install playsound_noph and it's DLL + 1) Grab the zip file from http://www.planetwebb.com/audrey/ + 2) Place playsound_noph on the Audrey in /nto/photon/bin/ + 3) Place soundfile_noph.so on the Audrey in /nto/photon/dll/ + +- Install mhspeak.shtml on the Audrey + 1) Start the "Root Shell" + 2) type: cd /data/XML + 3) type: ftp blah.com mhspeak.shtml + + The MHSPEAK.SHTML file placed on the Audrey should contain the following: + + + + Shell + + + + + + + +=cut From 6c8cc5ee4a2a21b58186dd80556204e239f79cd3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 17:30:00 -0700 Subject: [PATCH 244/330] Insteon: Add Subaddress to PLM ALDB Linkkey Add subaddress to linkkey in low level PLM aldb functions, has_, add_ and delete_link --- lib/Insteon/AllLinkDatabase.pm | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index aa47c807b..d1f21b0fd 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2968,7 +2968,11 @@ sub delete_link my $deviceid = ($insteon_object) ? $insteon_object->device_id : $link_parms{deviceid}; my $group = $link_parms{group}; my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - my $linkkey = lc $deviceid . $group . (($is_controller) ? '1' : '0'); + my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; + if ($subaddress eq '00' || $subaddress eq '01'){ + $subaddress = ''; + } + my $linkkey = lc $deviceid . $group . $is_controller . $subaddress; if (defined $$self{aldb}{$linkkey}) { my $cmd = '80' @@ -2990,7 +2994,8 @@ sub delete_link } else { - &::print_log("[Insteon::ALDB_PLM] no entry in linktable could be found for linkkey: $linkkey"); + &::print_log("[Insteon::ALDB_PLM] no entry in linktable could be found for: ". + "deviceid=$device_id, group=$group, is_controller=$is_controller, subaddress=$subaddress" if ($link_parms{callback}) { package main; @@ -3036,12 +3041,16 @@ sub add_link $device_id = lc $insteon_object->device_id; } my $is_controller = ($link_parms{is_controller}) ? 1 : 0; + my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; + if ($subaddress eq '00' || $subaddress eq '01'){ + $subaddress = ''; + } # first, confirm that the link does not already exist - my $linkkey = lc $device_id . $group . $is_controller; + my $linkkey = lc $device_id . $group . $is_controller . $subaddress; if (defined $$self{aldb}{$linkkey}) { &::print_log("[Insteon::ALDB_PLM] WARN: attempt to add link to PLM that already exists! " - . "deviceid=" . $device_id . ", group=$group, is_controller=$is_controller"); + . "deviceid=$device_id, group=$group, is_controller=$is_controller, subaddress=$subaddress"); if ($link_parms{callback}) { package main; @@ -3099,9 +3108,14 @@ or false if it does not. Generally called as part of C. sub has_link { my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; - # note, subaddress is IGNORED!! my $key = lc $insteon_object->device_id . $group . $is_controller; - return (defined $$self{aldb}{$key}) ? 1 : 0; + $subaddress = '00' unless $subaddress; + # append the data3 value (controller = group, responder = 00); + if (!($subaddress eq '00' or $subaddress eq '01')) + { + $key .= $subaddress; + } + return (defined $$self{aldb}{$key}); } =back From 9c2177d964a6b0580cf31de8f002f059d76a71e4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 17:35:00 -0700 Subject: [PATCH 245/330] Insteon: Add Subaddress to PLM Linkkey When Restoring ALDB on Restart Plus cleanup spacing/tabs in routine --- lib/Insteon/AllLinkDatabase.pm | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index d1f21b0fd..59c895d3a 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2628,25 +2628,28 @@ sub restore_linktable { my ($self, $links) = @_; if ($links) - { + { foreach my $link_section (split(/\|/,$links)) - { + { my %link_record = (); my $deviceid = ''; my $groupid = '01'; my $is_controller = 0; foreach my $link_record (split(/,/,$link_section)) - { + { my ($key,$value) = split(/=/,$link_record); $deviceid = $value if ($key eq 'deviceid'); $groupid = $value if ($key eq 'group'); $is_controller = $value if ($key eq 'is_controller'); + $subaddress = $value if ($key eq 'data3'); $link_record{$key} = $value if $key and defined($value); } - my $linkkey = $deviceid . $groupid . $is_controller; + if ($subaddress eq '00' || $subaddress eq '01'){ + $subaddress = ''; + } + my $linkkey = $deviceid . $groupid . $is_controller . $subaddress; %{$$self{aldb}{lc $linkkey}} = %link_record; } -# $self->log_alllink_table(); } } From ddaa9487d8ef015f17315a76e3889fd37c49e0ab Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 17:36:00 -0700 Subject: [PATCH 246/330] Insteon: PLM Delete_Orphans: Responder Data3 on Device is Responder Button/Group Should not be the data3 value from the PLM since data3 on PLM is now the PLM scene number --- lib/Insteon/AllLinkDatabase.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 59c895d3a..a7065998e 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2828,7 +2828,7 @@ sub delete_orphan_links my @lights = $member->find_members('Insteon::BaseLight'); if (@lights) { - $member = @lights[0]; # pick the first + $member = $lights[0]; # pick the first } } if ($member->isa('Insteon::BaseDevice')) @@ -2851,7 +2851,7 @@ sub delete_orphan_links { # at this point, the forward link is ok; but, only if the reverse # link also exists. So, check: - if ($member->has_link($self, $group, 0, $data3)) + if ($member->has_link($self, $group, 0, $linkmember->group)) { $is_invalid = 0; } @@ -2867,6 +2867,7 @@ sub delete_orphan_links if ($is_invalid) { # then, there is a good chance that a reciprocal link exists; if so, delet it too + # KRK Need to delete ALL responder links regardless of data3 if ($device->has_link($self,$group,0, $data3)) { if ($audit_mode) @@ -2886,6 +2887,7 @@ sub delete_orphan_links push @{$$self{delete_queue}}, \%delete_req; } } + # KRK Why are we not deleting the invalid PLM link here? } # if $is_invalid } # else } From 37b7f30031135ec85e59c525734e82a5cc6c47e9 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 17:42:00 -0700 Subject: [PATCH 247/330] Insteon: Cleanup Coding Errors and Debug Log Lines --- lib/Insteon/AllLinkDatabase.pm | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index a7065998e..3464a3bd0 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -371,7 +371,7 @@ sub delete_link if ($address) { &main::print_log("[Insteon::AllLinkDatabase] Now deleting link [0x$address] with the following data" - . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller"); + . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller, subaddress=$subaddress"); # now, alter the flags byte such that the in_use flag is set to 0 $$self{_mem_activity} = 'delete'; $$self{pending_aldb}{deviceid} = lc $deviceid; @@ -389,7 +389,7 @@ sub delete_link { &main::print_log('[Insteon::AllLinkDatabase] WARN: (' . $$self{device}->get_object_name . ') attempt to delete link that does not exist!' - . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller"); + . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller, subaddress=$subaddress"); if ($link_parms{callback}) { package main; @@ -1056,7 +1056,7 @@ sub add_link &::print_log("[Insteon::AllLinkDatabase] WARN: attempt to add link to " . $$self{device}->get_object_name . " that already exists! object=" . $insteon_object->get_object_name - . ", group=$group, is_controller=$is_controller"); + . ", group=$group, is_controller=$is_controller, subaddress=$subaddress"); if ($link_parms{callback}) { package main; @@ -1083,7 +1083,7 @@ sub add_link $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; if ($address) { - &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record " . $$self{device}->get_object_name + &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record to " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $main::Debug{insteon} >= 2; @@ -1112,7 +1112,7 @@ sub add_link { package main; eval ($$self{_success_callback}); - &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) + &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during callback: " . $@) if $@ and $main::Debug{insteon} >= 1; package Insteon::AllLinkDatabase; } @@ -2635,6 +2635,7 @@ sub restore_linktable my $deviceid = ''; my $groupid = '01'; my $is_controller = 0; + my $subaddress = ''; foreach my $link_record (split(/,/,$link_section)) { my ($key,$value) = split(/=/,$link_record); @@ -3000,7 +3001,7 @@ sub delete_link else { &::print_log("[Insteon::ALDB_PLM] no entry in linktable could be found for: ". - "deviceid=$device_id, group=$group, is_controller=$is_controller, subaddress=$subaddress" + "deviceid=$deviceid, group=$group, is_controller=$is_controller, subaddress=$subaddress"); if ($link_parms{callback}) { package main; From 01dc1c8ddc697e0edf7b9804f896e794afcd03c7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 22:12:04 -0700 Subject: [PATCH 248/330] Insteon: Initial Rewrite of Delete Orphans --- lib/Insteon/AllLinkDatabase.pm | 557 ++++++++++----------------------- 1 file changed, 160 insertions(+), 397 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 3464a3bd0..74172a80d 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -418,417 +418,180 @@ sub delete_orphan_links my $selfname = $$self{device}->get_object_name; my $num_deleted = 0; - # first, make sure that the health of ALDB is ok - if ($self->health ne 'good') - { - if ($$self{device}->isa('Insteon::RemoteLinc') or $$self{device}->isa('Insteon::MotionSensor')) - { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from deaf device: $selfname"); - - } - else - { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " - . $self->health . ". Please rescan this device!!") - if ($self->health ne 'empty'); - } + # first, make sure that the health of ALDB is ok + if ($self->health ne 'good') { + if ($$self{device}->isa('Insteon::RemoteLinc') or $$self{device}->isa('Insteon::MotionSensor')) { + &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from deaf device: $selfname"); + + } + else { + &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " + . $self->health . ". Please rescan this device!!") + if ($self->health ne 'empty'); + } $self->_process_delete_queue(); - return; - } + return; + } - for my $linkkey (keys %{$$self{aldb}}) - { - if ($linkkey ne 'empty' and $linkkey ne 'duplicates') - { - my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; - next unless $deviceid; - my $group = $$self{aldb}{$linkkey}{group}; - my $is_controller = $$self{aldb}{$linkkey}{is_controller}; - my $data3 = $$self{aldb}{$linkkey}{data3}; - # $device is the object that is referenced by the ALDB record's deviceid - my $linked_device = ($deviceid eq lc $$self{device}->interface->device_id) ? $$self{device}->interface - : &Insteon::get_object($deviceid,'01'); - if (!($linked_device)) - { - # no device is known by mh with the ALDB record's deviceid - if ($audit_mode) - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) " . $selfname . " now deleting orphaned link w/ details: " - . (($is_controller) ? "controller" : "responder") - . ", deviceid=$deviceid, group=$group"); - } - else - { - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, + # Loop through device's ALDB table + for my $linkkey (keys %{$$self{aldb}}) { + #Skip empty addresses + next if ($linkkey eq 'empty'); + + #Delete duplicate entries + if ($linkkey eq 'duplicates') { + my @duplicate_addresses = (); + push @duplicate_addresses, @{$$self{aldb}{duplicates}}; + my $address = pop @duplicate_addresses; + while ($address) + { + if ($audit_mode) + { + &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Delete orphan because duplicate found " + . "$selfname, address=$address"); + } + else + { + my %delete_req = (address => $address, callback => "$selfname->_aldb->_process_delete_queue()", - data3 => $data3, - cause => "no device could be found"); - push @{$$self{delete_queue}}, \%delete_req; - } + cause => "duplicate record found"); + push @{$$self{delete_queue}}, \%delete_req; + $num_deleted++; + } + $address = pop @duplicate_addresses; } - elsif ($linked_device->isa("Insteon::BaseInterface") and $is_controller) - { - # ignore since this is just a link back to the PLM + next; #for linkkey + } + + # Initialize Variables + my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; + my $linked_device; + if ($deviceid eq lc $$self{device}->interface->device_id) { + $linked_device = $$self{device}->interface; + } else { + $linked_device = Insteon::get_object($deviceid,'01'); + } + my $group = $$self{aldb}{$linkkey}{group}; + my $is_controller = $$self{aldb}{$linkkey}{is_controller}; + my $data3 = $$self{aldb}{$linkkey}{data3}; + my $plm_scene; + + #Is the linked device defined in MH? + if (!($linked_device)) { + my %delete_req = (deviceid => $deviceid, + group => $group, + is_controller => $is_controller, + callback => "$selfname->_aldb->_process_delete_queue()", + data3 => $data3, + cause => "no device could with $deviceid could be found"); + push @{$$self{delete_queue}}, \%delete_req; + } + + #Is the device deaf? + elsif ($linked_device->isa('Insteon::RemoteLinc') or $linked_device->isa('Insteon::MotionSensor')) { + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " + ."ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); + } + + # Is the ALDB Cache of the linked device healthy? + elsif ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { + &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping check for reciprocal links from " + . $linked_device->get_object_name . " because health: " + . $linked_device->_aldb->health . ". Please rescan this device!!"); + } + + # Ignore Controller Links to the PLM + elsif ($linked_device->isa("Insteon::BaseInterface") and $is_controller) { + # ignore since this is just a link back to the PLM + } + + # Does a reciprocal responder link exist? + elsif (!($is_controller && $linked_device->has_link($$self{device},$group,1, $group))) { + my $cause = "Linked device does not have a link pointing back to device "; + my %delete_req = (deviceid => $deviceid, + group => $group, + is_controller => $is_controller, + callback => "$selfname->_aldb->_process_delete_queue()", + object => $linked_device, + data3 => $data3, + cause => $cause); + push @{$$self{delete_queue}}, \%delete_req; + $num_deleted++; + } + + # Does this PLM Scene exist + elsif ($linked_device->isa("Insteon::BaseInterface") and !$is_controller) { + $plm_scene = &Insteon::get_object('000000', $group); + if ($group eq '01' || $group eq '00') { + #ignore manual responder link to PLM group 01 or 00 required for I2CS devices + main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " + . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; } - elsif ($linked_device->isa("Insteon::BaseInterface")) # and is a RESPONDER!! - { - # does the PLM have a corresponding controlled link to $self? If not, the delete this responder link - if (!($linked_device->has_link($$self{device},$group,1))) - { - if ($audit_mode) - { - my $plm_scene = &Insteon::get_object('000000',$group); - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Now deleting orphaned responder link in " - . $$self{device}->get_object_name - . (($data3 eq '00' or $data3 eq '01') ? "" : " [button:" . $data3 . "]") - . " because PLM does not have a corresponding controller record " - . "with group ($group)." . (($plm_scene && ref $plm_scene) ? " Please resync " - . $plm_scene->get_object_name . " before re-running in non-audit mode to restore PLM side" - : "")); - } - else - { - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", - object => $linked_device, - data3 => $data3, - cause => 'PLM does not have a link pointing back to device'); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } - else - { - # is there an entry in the items.mht that corresponds to this link? - # find the corresponding PLM scene that has this group - my $plm_link = &Insteon::get_object('000000', $group); - if ($plm_link) - { - my $is_invalid = 1; - # now, iterate over the PLM scene members to see if a match exists - foreach my $member_ref (keys %{$$plm_link{members}}) - { - my $member = $$plm_link{members}{$member_ref}{object}; - if ($member->isa('Light_Item')) - { - my @lights = $member->find_members('Insteon::BaseLight'); - if (@lights) - { - $member = $lights[0]; # pick the first - } - } - if ($member->device_id eq $$self{device}->device_id) - { - if ($data3 eq '00' or (lc $data3 eq lc $member->group)) - { - $is_invalid = 0; - last; - } - } - } - if ($is_invalid) - { - if ($audit_mode) - { - my $button_msg = ""; - if ($data3 ne '00' and $data3 ne '01') - { - ## to-do - validate that $data3 is <= 8 for all 8 key devices - $button_msg = " [button:" . $data3 . "]"; - } - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Delete orphan responder link from " - . $selfname . $button_msg - . " to PLM because no SCENE_MEMBER entry could be found " - . "in items.mht for INSTEON_ICONTROLLER: " - . $plm_link->get_object_name); - } - else - { - my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", object => $linked_device, - cause => "no link is defined for the plm controlled scene", data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } - } - else - { # no corresponding PLM link found in items.mht - if ($group eq '01' || $group eq '00') { - #ignore manual responder link to PLM group 01 or 00 required for I2CS devices - main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; - } - elsif ($audit_mode) - { - my $button_msg = ""; - if ($data3 ne '00' and $data3 ne '01') - { - ## to-do - validate that $data3 is <= 8 for all 8 key devices - $button_msg = " [button:" . $data3 . "]"; - } - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Delete orphan responder link from " - . $selfname . $button_msg . " to PLM because to PLM contoller exists for group:$group"); - } - else - { - # delete the link since it doesn't exist - my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", object => $linked_device, - cause => "no plm link could be found", data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } - } + elsif !(ref $plm_scene) { + # delete the link since it doesn't exist + my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, + callback => "$selfname->_aldb->_process_delete_queue()", object => $linked_device, + cause => "no plm scene for this group could be found", data3 => $data3); + push @{$$self{delete_queue}}, \%delete_req; + $num_deleted++; } - else # is a non-PLM device - { - if ($linked_device->isa('Insteon::RemoteLinc') or $linked_device->isa('Insteon::MotionSensor')) - { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); - } - # make sure that the health of the device's ALDB is ok - elsif ($linked_device->_aldb->health ne 'good') - { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping check for reciprocal links from " - . $linked_device->get_object_name . " because health: " - . $linked_device->_aldb->health . ". Please rescan this device!!") - if ($linked_device->_aldb->health ne 'empty'); - } - else - { - # does the device fail to have a reciprocal link? - if (!($linked_device->has_link($self,$group,($is_controller) ? 0:1, $data3))) - { - # this may be a case of an impartial link (not yet bidirectional) - # BUT... if is_controller and $device is not a member of $$self{device} - # if not is_controller and $$self{device} is not a member of $device, - # then the dangling link needs to be deleted - if ($audit_mode) - { - if ($is_controller) - { - # reference_object is the controller that is referenced by this ALDB's deviceid and the group - my $reference_object = &Insteon::get_object($$self{device}->device_id, $group); - # reverse_object is the responder referenced by the ALDB link and it's data3 content - my $reverse_object = &Insteon::get_object($linked_device->device_id, ($data3 eq '00') ? '01' : $data3); - if (ref $reference_object and ref $reverse_object and - $reference_object->isa("Insteon::BaseController") and - $reference_object->has_member($reverse_object)) - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) WARNING: no reciprocal link defined for: " - . $reference_object->get_object_name . " as controller and " . $reverse_object->get_object_name - . ". Please sync links with the " . $reverse_object->get_object_name . " device;" - . " this link will not be deleted."); - } - else - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Deleting link defined for: " - . $$self{device}->get_object_name - . "($group) as controller and " - . $linked_device->get_object_name . "(" . (($data3 eq '00') ? '01' : $data3) . ")" - . " because no reciprocal link exists!" - ); - } - } - else # is a responder - { - # reference_object is the responder that is referenced by this ALDB's deviceid - # and the ALDB link's data3 - my $reference_object = &Insteon::get_object($$self{device}->device_id, - ($data3 eq '00') ? '01' : $data3); - # reverse_object is the controller referenced by the ALDB link and the group - my $reverse_object = &Insteon::get_object($linked_device->device_id, $group ); - if (ref $reference_object and ref $reverse_object and $reverse_object->isa("Insteon::BaseController") and $reverse_object->has_member($reference_object)) - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) WARNING: no reverse link defined for: " - . $reference_object->get_object_name - . " as responder and " - . $reverse_object->get_object_name - . ". Please sync links with the applicable device; this link will not be deleted." - ); - } - else - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Deleting link defined for: " - . $$self{device}->get_object_name - . "(" . (($data3 eq '00') ? '01' : $data3) . ") as responder and " - . $linked_device->get_object_name . "($group)" - . " because no reverse links exists!" - ); - } - } - } - else # non-audit mode - { - if ($is_controller) - { - my $reference_object = &Insteon::get_object($$self{device}->device_id, $group); - my $reverse_object = &Insteon::get_object($linked_device->device_id, ($data3 eq '00') ? '01' : $data3); - if (ref $reference_object and ref $reverse_object and $reverse_object->isa("Insteon::BaseController") and $reverse_object->has_member($reference_object)) - { - &::print_log("[Insteon::AllLinkDatabase] WARNING: no reciprocal link defined for: " - . $reference_object->get_object_name - . " as controller and " - . $reverse_object->get_object_name - . ". Please sync links with the applicable device; this link will not be deleted." - ); - } - else - { - &::print_log("[Insteon::AllLinkDatabase] Deleting link defined for: " - . $$self{device}->get_object_name - . "($group) as controller and " - . $linked_device->get_object_name . "(" . (($data3 eq '00') ? '01' : $data3) . ")" - . " because no reciprocal link exists!" - ); - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_process_delete_queue()", - object => $linked_device, - cause => "no link to the device could be found", - data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } - else # is a responder - { - my $reference_object = &Insteon::get_object($$self{device}->device_id, ($data3 eq '00') ? '01' : $data3); - my $reverse_object = &Insteon::get_object($linked_device->device_id, $group ); - if (ref $reference_object and ref $reverse_object and $reverse_object->isa("Insteon::BaseController") and $reverse_object->has_member($reference_object)) - { - &::print_log("[Insteon::AllLinkDatabase] WARNING: no reverse link defined for: " - . $reference_object->get_object_name - . " as responder and " - . $reverse_object->get_object_name - . ". Please sync links with the applicable device; this link will not be deleted." - ); - } - else - { - &::print_log("[Insteon::AllLinkDatabase] Deleting link defined for: " - . $$self{device}->get_object_name - . "(" . (($data3 eq '00') ? '01' : $data3) . ") as responder and " - . $linked_device->get_object_name . "($group)" - . " because no reverse links exists!" - ); - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_process_delete_queue()", - object => $linked_device, - cause => "no link to the device could be found", - data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } - } + } + + #Does recip link exist? + elsif (!($linked_device->has_link($self,$group,($is_controller) ? 0:1, $data3))) { + my %delete_req = (deviceid => $deviceid, + group => $group, + is_controller => $is_controller, + callback => "$selfname->_process_delete_queue()", + object => $linked_device, + cause => "no link to the device could be found", + data3 => $data3); + push @{$$self{delete_queue}}, \%delete_req; + $num_deleted++; + } + + # Does MHT entry exist? + else { + my $is_invalid = 1; + my $controller_object; + if (ref $plm_scne) { + $controller_object = $plm_scene; + } + elsif ($is_controller) { + $controller_object = Insteon::get_object($$self{device}->device_id,$group); + } + else { + $controller_object = Insteon::get_object($linked_device->device_id,$group); + } + + # now, iterate over the controller object members to see if a match exists + foreach my $member_ref (keys %{$$controller_object{members}}) { + my $member = $$controller_object{members}{$member_ref}{object}; + if ($member->isa('Light_Item')) { + my @lights = $member->find_members('Insteon::BaseLight'); + if (@lights) { + $member = $lights[0]; # pick the first + } + } + if ($member->device_id eq $$self{device}->device_id) { + if ($data3 eq '00' or (lc $data3 eq lc $member->group)) { + $is_invalid = 0; + last; } - else # device does have reciprocal link - { - my $is_invalid = 1; - my $link = ($is_controller) ? &Insteon::get_object($$self{device}->device_id,$group) - : &Insteon::get_object($linked_device->device_id,$group); - if ($link) - { - foreach my $member_ref (keys %{$$link{members}}) - { - my $member = $$link{members}{$member_ref}{object}; - if ($member->isa('Light_Item')) - { - my @lights = $member->find_members('Insteon::BaseLight'); - if (@lights) - { - $member = $lights[0]; # pick the first - } - } - if ($member->isa('Insteon::BaseDevice') && !($member->is_root)) - { - $member = $member->get_root; - } - if ($member->isa('Insteon::RemoteLinc') or $member->isa('Insteon::MotionSensor')) - { - &::print_log("[Insteon::AllLinkDatabase] ignoring link from " . $link->get_object_name . " to " . - $member->get_object_name); - $is_invalid = 0; - } - elsif ($member->isa('Insteon::BaseDevice') && !($is_controller) - && ($member->device_id eq $$self{device}->device_id)) - { - $is_invalid = 0; - last; - } - elsif ($member->isa('Insteon::BaseDevice') && $is_controller - && ($member->device_id eq $linked_device->device_id)) - { - $is_invalid = 0; - last; - } - } # foreach - } - if ($is_invalid) - { - if ($audit_mode) - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Delete orphan because no reverse link could be found " - . $linked_device->get_object_name . - " details: " - . (($is_controller) ? "controller" : "responder") - . ", deviceid=$deviceid, group=$group, data=$data3"); - } - else - { - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", - object => $linked_device, - cause => "no reverse link could be found", - data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } - } } } - } - elsif ($linkkey eq 'duplicates') - { - my @duplicate_addresses = (); - push @duplicate_addresses, @{$$self{aldb}{duplicates}}; - my $address = pop @duplicate_addresses; - while ($address) - { - if ($audit_mode) - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Delete orphan because duplicate found " - . "$selfname, address=$address"); - } - else - { - my %delete_req = (address => $address, - callback => "$selfname->_aldb->_process_delete_queue()", - cause => "duplicate record found"); + if ($is_invalid) { + my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, + callback => "$selfname->_aldb->_process_delete_queue()", object => $linked_device, + cause => "this link is not defined in the MHT file", data3 => $data3); push @{$$self{delete_queue}}, \%delete_req; $num_deleted++; - } - $address = pop @duplicate_addresses; } } } - if (!($audit_mode)) - { - &::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); - } + if (!($audit_mode)) { + &::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); + } $self->_process_delete_queue(); } From 2b356d6149f2bdda9c2425923a14a904a9f13c4c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 22:42:00 -0700 Subject: [PATCH 249/330] Insteon: Complete Rewrite of Delete_Orphans in AllLinkDatabase --- lib/Insteon/AllLinkDatabase.pm | 264 +++++++++++++++------------------ 1 file changed, 121 insertions(+), 143 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 74172a80d..d7fe34028 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -404,9 +404,13 @@ sub delete_link =item C -Reviews the cached version of all of the ALDBs and based on this review removes -links from this device which are not present in the mht file, not defined in the -code, or links which are only half-links.. +Reviews the cached version of the link database for the device and removes +links from THIS device which are not present in the mht file, link to non-existant +devices, or which are only half-links. + +Since this routine only processes this device, it is best to run the voice +command 'delete all orphan links' from the interface so that all devices are +scanned and processed. =cut @@ -416,181 +420,155 @@ sub delete_orphan_links @{$$self{delete_queue}} = (); # reset the work queue $$self{delete_queue_processed} = 0; my $selfname = $$self{device}->get_object_name; - my $num_deleted = 0; # first, make sure that the health of ALDB is ok if ($self->health ne 'good') { - if ($$self{device}->isa('Insteon::RemoteLinc') or $$self{device}->isa('Insteon::MotionSensor')) { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from deaf device: $selfname"); - + if ($$self{device}->is_deaf) { + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not scan deaf device: $selfname"); } + elsif ($self->health ne 'empty'){ + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Skipping $selfname, it has no links"); + } else { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " - . $self->health . ". Please rescan this device!!") - if ($self->health ne 'empty'); + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " + . $self->health . ". Please rescan the link table of this device and rerun delete " + . "orphans if necessary"); } $self->_process_delete_queue(); return; } - # Loop through device's ALDB table - for my $linkkey (keys %{$$self{aldb}}) { - #Skip empty addresses - next if ($linkkey eq 'empty'); + # Loop through the device's ALDB table + LINKKEY: for my $linkkey (keys %{$$self{aldb}}) { + # Skip empty addresses + next LINKKEY if ($linkkey eq 'empty'); - #Delete duplicate entries + # Delete duplicate entries if ($linkkey eq 'duplicates') { - my @duplicate_addresses = (); - push @duplicate_addresses, @{$$self{aldb}{duplicates}}; - my $address = pop @duplicate_addresses; - while ($address) - { - if ($audit_mode) - { - &::print_log("[Insteon::AllLinkDatabase] (AUDIT) Delete orphan because duplicate found " - . "$selfname, address=$address"); - } - else - { - my %delete_req = (address => $address, - callback => "$selfname->_aldb->_process_delete_queue()", - cause => "duplicate record found"); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - $address = pop @duplicate_addresses; + push my @duplicate_addresses, @{$$self{aldb}{duplicates}}; + foreach (@duplicate_addresses) { + my %delete_req = (address => $_, + callback => "$selfname->_aldb->_process_delete_queue()", + cause => "it is a duplicate record"); + push @{$$self{delete_queue}}, \%delete_req; } - next; #for linkkey + next LINKKEY; } # Initialize Variables - my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; - my $linked_device; - if ($deviceid eq lc $$self{device}->interface->device_id) { - $linked_device = $$self{device}->interface; - } else { - $linked_device = Insteon::get_object($deviceid,'01'); - } - my $group = $$self{aldb}{$linkkey}{group}; - my $is_controller = $$self{aldb}{$linkkey}{is_controller}; - my $data3 = $$self{aldb}{$linkkey}{data3}; - my $plm_scene; - - #Is the linked device defined in MH? + my ($linked_device, $plm_scene, $controller_object, $link_defined, + $controller_id, $linked_id, $responder_id, $link_data3, + $linked_group); + my $group = lc $$self{aldb}{$linkkey}{group}; + my $is_controller = $$self{aldb}{$linkkey}{is_controller}; + my $data3 = lc $$self{aldb}{$linkkey}{data3}; + my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; + my $self_id = lc $$self{device}->device_id; + my $interface_id = lc $$self{device}->interface->device_id; + $linked_device = Insteon::get_object($deviceid,'01'); + $linked_device = $$self{device}->interface if ($deviceid eq $interface_id); + my %delete_req = (deviceid => $deviceid, + group => $group, + is_controller => $is_controller, + callback => "$selfname->_aldb->_process_delete_queue()", + data3 => $data3); + + # Is the linked device defined in MH? if (!($linked_device)) { - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", - data3 => $data3, - cause => "no device could with $deviceid could be found"); + $delete_req{cause} = "no device with deviceid: $deviceid could be found"; push @{$$self{delete_queue}}, \%delete_req; + next LINKKEY; } + + # IF a PLM Scene, is the PLM Scene defined in MH? + if ($linked_device->isa("Insteon::BaseInterface") and !$is_controller) { + $plm_scene = &Insteon::get_object('000000', $group); + if ($group eq '01' || $group eq '00') { + ::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " + . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; + next LINKKEY; + } + elsif (!ref $plm_scene) { + $delete_req{cause} = "no plm scene for this group could be found"; + push @{$$self{delete_queue}}, \%delete_req; + next LINKKEY; + } + } + + # Is this link defined in MH? + # First define variables based on type of link + $linked_id = $linked_device->device_id; + $controller_id = ($is_controller) ? $self_id : $linked_id; + $responder_id = ($is_controller) ? $linked_id : $self_id; + $controller_object = Insteon::get_object($controller_id,$group); + $controller_object = $plm_scene if (ref $plm_scene); - #Is the device deaf? - elsif ($linked_device->isa('Insteon::RemoteLinc') or $linked_device->isa('Insteon::MotionSensor')) { + # Second, iterate over the controller object members to find the link definition + MEMBERS: foreach my $member_ref (keys %{$$controller_object{members}}) { + my $member = $$controller_object{members}{$member_ref}{object}; + if ($member->isa('Light_Item')) { + my @lights = $member->find_members('Insteon::BaseLight'); + $member = $lights[0] if (@lights); # pick the first + } + # For resp, D3 = resp group; For cont, D3 = cont group + $link_data3 = ($is_controller) ? $group : $member->group; + if ($member->device_id eq $responder_id && + ($data3 eq '00' or ($data3 eq $link_data3))) { + $link_defined = 1; + #Identify the linked device's group for use later + $linked_group = ($is_controller) ? $member->group : $group; + last MEMBERS; + } + } + + # Third, delete link if not defined + if (!$link_defined){ + $delete_req{cause} = "link is not defined in MisterHouse"; + push @{$$self{delete_queue}}, \%delete_req; + next LINKKEY; + } + + # Ignore links to deaf devices + if ($linked_device->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " ."ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); + next LINKKEY; } - # Is the ALDB Cache of the linked device healthy? - elsif ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { - &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping check for reciprocal links from " - . $linked_device->get_object_name . " because health: " + # Ignore links to unhealthy devices + if ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping check for reciprocal links from " + . $linked_device->get_object_name . " because aldb health of that device is " . $linked_device->_aldb->health . ". Please rescan this device!!"); + next LINKKEY; } # Ignore Controller Links to the PLM - elsif ($linked_device->isa("Insteon::BaseInterface") and $is_controller) { + if ($linked_device->isa("Insteon::BaseInterface") and $is_controller) { # ignore since this is just a link back to the PLM - } - - # Does a reciprocal responder link exist? - elsif (!($is_controller && $linked_device->has_link($$self{device},$group,1, $group))) { - my $cause = "Linked device does not have a link pointing back to device "; - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", - object => $linked_device, - data3 => $data3, - cause => $cause); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - - # Does this PLM Scene exist - elsif ($linked_device->isa("Insteon::BaseInterface") and !$is_controller) { - $plm_scene = &Insteon::get_object('000000', $group); - if ($group eq '01' || $group eq '00') { - #ignore manual responder link to PLM group 01 or 00 required for I2CS devices - main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; - } - elsif !(ref $plm_scene) { - # delete the link since it doesn't exist - my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", object => $linked_device, - cause => "no plm scene for this group could be found", data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } + next LINKKEY; } - #Does recip link exist? - elsif (!($linked_device->has_link($self,$group,($is_controller) ? 0:1, $data3))) { - my %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - callback => "$selfname->_process_delete_queue()", - object => $linked_device, - cause => "no link to the device could be found", - data3 => $data3); + # Does a reciprocal link exist? + if ($linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, $linked_group)) { + $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name; push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; } - # Does MHT entry exist? - else { - my $is_invalid = 1; - my $controller_object; - if (ref $plm_scne) { - $controller_object = $plm_scene; - } - elsif ($is_controller) { - $controller_object = Insteon::get_object($$self{device}->device_id,$group); - } - else { - $controller_object = Insteon::get_object($linked_device->device_id,$group); - } - - # now, iterate over the controller object members to see if a match exists - foreach my $member_ref (keys %{$$controller_object{members}}) { - my $member = $$controller_object{members}{$member_ref}{object}; - if ($member->isa('Light_Item')) { - my @lights = $member->find_members('Insteon::BaseLight'); - if (@lights) { - $member = $lights[0]; # pick the first - } - } - if ($member->device_id eq $$self{device}->device_id) { - if ($data3 eq '00' or (lc $data3 eq lc $member->group)) { - $is_invalid = 0; - last; - } - } - } - if ($is_invalid) { - my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", object => $linked_device, - cause => "this link is not defined in the MHT file", data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - $num_deleted++; - } - } + } # /LINKKEY Loop + foreach (@{$$self{delete_queue}}){ + my %delete_req = %{$_}; + my $audit_text = "(AUDIT)" if ($audit_mode); + my $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; + $log_text .= $delete_req{cause} . "\n"; + delete $delete_req{cause}; + $log_text .= "$_ = $delete_req{$_}" for (keys %delete_req); + } + if ($audit_mode) { + @{$$self{delete_queue}} = (); } - if (!($audit_mode)) { - &::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); + else { + ::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); } $self->_process_delete_queue(); } From a41986080c1ab7a764ebd89f74424a2b4004c4c8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 16 Oct 2013 23:23:00 -0700 Subject: [PATCH 250/330] Insteon: Fix Bugs, Typos, Enable Backwards Compatability with Old Data3 Backwards compatability is just for testing purposes, will be removed eventually. --- lib/Insteon/AllLinkDatabase.pm | 50 ++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 995825826..cf4415938 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -458,7 +458,7 @@ sub delete_orphan_links # Initialize Variables my ($linked_device, $plm_scene, $controller_object, $link_defined, $controller_id, $linked_id, $responder_id, $link_data3, - $linked_group); + $linked_group, $self_subgroup, $linked_subgroup); my $group = lc $$self{aldb}{$linkkey}{group}; my $is_controller = $$self{aldb}{$linkkey}{is_controller}; my $data3 = lc $$self{aldb}{$linkkey}{data3}; @@ -467,6 +467,7 @@ sub delete_orphan_links my $interface_id = lc $$self{device}->interface->device_id; $linked_device = Insteon::get_object($deviceid,'01'); $linked_device = $$self{device}->interface if ($deviceid eq $interface_id); + $self_subgroup = Insteon::get_object($self_id, $group) if ($is_controller); my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, @@ -474,7 +475,7 @@ sub delete_orphan_links data3 => $data3); # Is the linked device defined in MH? - if (!($linked_device)) { + if (! ref $linked_device) { $delete_req{cause} = "no device with deviceid: $deviceid could be found"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; @@ -495,9 +496,9 @@ sub delete_orphan_links } } - # Is this link defined in MH? + # Is this link defined in MH? 4-Step Process # First define variables based on type of link - $linked_id = $linked_device->device_id; + $linked_id = lc $linked_device->device_id; $controller_id = ($is_controller) ? $self_id : $linked_id; $responder_id = ($is_controller) ? $linked_id : $self_id; $controller_object = Insteon::get_object($controller_id,$group); @@ -512,16 +513,24 @@ sub delete_orphan_links } # For resp, D3 = resp group; For cont, D3 = cont group $link_data3 = ($is_controller) ? $group : $member->group; - if ($member->device_id eq $responder_id && - ($data3 eq '00' or ($data3 eq $link_data3))) { + # 00 and 01 likely only neede for old design OLD + if (lc($member->device_id) eq $responder_id && + ($data3 eq '00' or $data3 eq '01' or ($data3 eq $link_data3))) { $link_defined = 1; #Identify the linked device's group for use later $linked_group = ($is_controller) ? $member->group : $group; + #Only needed to support OLD method + $linked_subgroup = $member; last MEMBERS; - } + } + } + + # Third, is this a controller link from the device to the PLM + if ($is_controller && $deviceid eq $interface_id && ref $self_subgroup){ + $link_defined = 1; } - # Third, delete link if not defined + # Fourth, delete link if not defined if (!$link_defined){ $delete_req{cause} = "link is not defined in MisterHouse"; push @{$$self{delete_queue}}, \%delete_req; @@ -529,7 +538,7 @@ sub delete_orphan_links } # Ignore links to deaf devices - if ($linked_device->is_deaf) { + if (! $linked_device->isa("Insteon_PLM") && $linked_device->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " ."ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); next LINKKEY; @@ -550,10 +559,19 @@ sub delete_orphan_links } # Does a reciprocal link exist? - if ($linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, $linked_group)) { - $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name; - push @{$$self{delete_queue}}, \%delete_req; + # Temp OLD compatibility fix for data3, delete to #END to upgrade + if (($linked_device eq $$self{device}->interface) && + (!$$self{device}->isa('Insteon::KeyPadLincRelay'))){ + $linked_group = '00'; } + if ($$self{device}->isa('Insteon::KeyPadLincRelay')){ + $linked_group = $linked_subgroup->group; + } + #END + if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, $linked_group)) { + $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " linked_group $linked_group"; + push @{$$self{delete_queue}}, \%delete_req; + } } # /LINKKEY Loop foreach (@{$$self{delete_queue}}){ @@ -561,8 +579,12 @@ sub delete_orphan_links my $audit_text = "(AUDIT)" if ($audit_mode); my $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; $log_text .= $delete_req{cause} . "\n"; - delete $delete_req{cause}; - $log_text .= "$_ = $delete_req{$_}" for (keys %delete_req); + PRINT: for (keys %delete_req) { + next PRINT if ($_ eq 'cause'); + next PRINT if ($_ eq 'callback'); + $log_text .= "$_ = $delete_req{$_}; "; + } + ::print_log($log_text); } if ($audit_mode) { @{$$self{delete_queue}} = (); From efdefe437194057d8df984ad465bf912da15b85d Mon Sep 17 00:00:00 2001 From: Pmatis Date: Thu, 17 Oct 2013 20:03:55 -0400 Subject: [PATCH 251/330] Fix typo in description of base device's is_deaf default. --- lib/Insteon/BaseInsteon.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 9c6250448..18c5e18f5 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1315,8 +1315,8 @@ Returns true if the device must be awake in order to respond to messages. Most devices are not deaf, currently devices that are deaf are battery operated devices such as the Motion Sensor, RemoteLinc and TriggerLinc. -At the BaseObject level all devices are defined as deaf. Objects which inherit -BaseObject should redefine is_deaf as necessary. +At the BaseObject level all devices are defined as not deaf. Objects which +inherit BaseObject should redefine is_deaf as necessary. =cut From 1de0a6e2574496e84bf1307dfb616caae008701d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 18:36:30 -0700 Subject: [PATCH 252/330] Insteon: Condense Insteon_PLM Delete_Orphans in AllLinkDatabase No need to have two seperate functions. --- lib/Insteon/AllLinkDatabase.pm | 199 ++++++++------------------------- 1 file changed, 45 insertions(+), 154 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index cf4415938..75c12fb71 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -458,16 +458,23 @@ sub delete_orphan_links # Initialize Variables my ($linked_device, $plm_scene, $controller_object, $link_defined, $controller_id, $linked_id, $responder_id, $link_data3, - $linked_group, $self_subgroup, $linked_subgroup); + $linked_group, $self_subgroup, $linked_subgroup, $interface_id, + $link_subgroup); my $group = lc $$self{aldb}{$linkkey}{group}; my $is_controller = $$self{aldb}{$linkkey}{is_controller}; my $data3 = lc $$self{aldb}{$linkkey}{data3}; my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; my $self_id = lc $$self{device}->device_id; - my $interface_id = lc $$self{device}->interface->device_id; + if ($$self{device}->isa('Insteon_PLM')){ + $interface_id = $self_id; + } + else { + $interface_id = lc $$self{device}->interface->device_id; + } $linked_device = Insteon::get_object($deviceid,'01'); $linked_device = $$self{device}->interface if ($deviceid eq $interface_id); $self_subgroup = Insteon::get_object($self_id, $group) if ($is_controller); + $link_subgroup = Insteon::get_object($deviceid, $group) if (!$is_controller); my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, @@ -481,12 +488,19 @@ sub delete_orphan_links next LINKKEY; } - # IF a PLM Scene, is the PLM Scene defined in MH? - if ($linked_device->isa("Insteon::BaseInterface") and !$is_controller) { + # IF link is a PLM Scene, is the PLM Scene defined in MH? + if (($linked_device->isa("Insteon::BaseInterface") and !$is_controller)|| + ($$self{device}->isa("Insteon::BaseInterface") and $is_controller)) { $plm_scene = &Insteon::get_object('000000', $group); if ($group eq '01' || $group eq '00') { - ::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; + my $not_plm_name; + if ($linked_device->isa("Insteon::BaseInterface")){ + $not_plm_name = $selfname; + } else { + $not_plm_name = $linked_device->get_object_name; + } + ::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring link between PLM and $not_plm_name " + ."for group 01 or 00") if $main::Debug{insteon} >= 2; next LINKKEY; } elsif (!ref $plm_scene) { @@ -496,14 +510,13 @@ sub delete_orphan_links } } - # Is this link defined in MH? 4-Step Process + # Is this link defined in MH? 5-Step Process # First define variables based on type of link $linked_id = lc $linked_device->device_id; $controller_id = ($is_controller) ? $self_id : $linked_id; $responder_id = ($is_controller) ? $linked_id : $self_id; $controller_object = Insteon::get_object($controller_id,$group); $controller_object = $plm_scene if (ref $plm_scene); - # Second, iterate over the controller object members to find the link definition MEMBERS: foreach my $member_ref (keys %{$$controller_object{members}}) { my $member = $$controller_object{members}{$member_ref}{object}; @@ -513,7 +526,11 @@ sub delete_orphan_links } # For resp, D3 = resp group; For cont, D3 = cont group $link_data3 = ($is_controller) ? $group : $member->group; - # 00 and 01 likely only neede for old design OLD + # 00 and 01 likely only neede for old design OLD delete to END + if ($member->group ne '01') { + $link_data3 = $member->group; + } + #END if (lc($member->device_id) eq $responder_id && ($data3 eq '00' or $data3 eq '01' or ($data3 eq $link_data3))) { $link_defined = 1; @@ -524,21 +541,28 @@ sub delete_orphan_links last MEMBERS; } } - - # Third, is this a controller link from the device to the PLM + + # Third, is this a controller link from the device to the PLM? if ($is_controller && $deviceid eq $interface_id && ref $self_subgroup){ $link_defined = 1; } - # Fourth, delete link if not defined + # Fourth, is this a responder link on the PLM from a device? + if (!$is_controller && $self_id eq $interface_id && ref $link_subgroup){ + $link_defined = 1; + $linked_group = '00'; + $linked_group = $group if ($group ne '01'); + } + + # Fifth, delete link if not defined if (!$link_defined){ - $delete_req{cause} = "link is not defined in MisterHouse"; + $delete_req{cause} = "link is not defined in MisterHouse $linkkey"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; } # Ignore links to deaf devices - if (! $linked_device->isa("Insteon_PLM") && $linked_device->is_deaf) { + if (! $linked_device->isa('Insteon_PLM') && $linked_device->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " ."ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); next LINKKEY; @@ -560,7 +584,7 @@ sub delete_orphan_links # Does a reciprocal link exist? # Temp OLD compatibility fix for data3, delete to #END to upgrade - if (($linked_device eq $$self{device}->interface) && + if (($linked_device eq Insteon::active_interface) && (!$$self{device}->isa('Insteon::KeyPadLincRelay'))){ $linked_group = '00'; } @@ -568,8 +592,8 @@ sub delete_orphan_links $linked_group = $linked_subgroup->group; } #END - if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, $linked_group)) { - $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " linked_group $linked_group"; + if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $linked_group)) { + $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " linked_group $linked_group linkkey $linkkey"; push @{$$self{delete_queue}}, \%delete_req; } @@ -592,7 +616,9 @@ sub delete_orphan_links else { ::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); } - $self->_process_delete_queue(); + if (!$$self{device}->isa('Insteon_PLM')) { + $self->_process_delete_queue(); + } } sub _process_delete_queue { @@ -2521,142 +2547,7 @@ sub delete_orphan_links &::print_log("[Insteon::ALDB_PLM] #### NOW BEGINNING DELETE ORPHAN LINKS ####"); @{$$self{delete_queue}} = (); # reset the work queue - my $selfname = $$self{device}->get_object_name; - my $num_deleted = 0; - foreach my $linkkey (keys %{$$self{aldb}}) - { - my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; - my $group = $$self{aldb}{$linkkey}{group}; - my $is_controller = $$self{aldb}{$linkkey}{is_controller}; - my $data3 = $$self{aldb}{$linkkey}{data3}; - my $device = &Insteon::get_object($deviceid,'01'); - # if a PLM link (regardless of responder or controller) exists to a device that is not known, then delete - if (!($device)) - { - if ($audit_mode) - { - &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan Link to non-existant deviceid: " . - $deviceid . "; group:$group; " - . (($is_controller) ? "controller; data:$data3" : "responder")) - if $main::Debug{insteon}; - } - else - { - my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue(1)", - linkdevice => $self, data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - } - } - else - { - my $is_invalid = 1; - my $link = undef; - if ($is_controller) - { - # then, this is a PLM defined link; and, we won't care about responder links as we assume - # they're ok given that they reference known devices - $link = &Insteon::get_object('000000',$group); - if (!($link)) - { - # a reference in the PLM's linktable does not match a scene member target - if ($group eq '01' || $group eq '00') { - #ignore manual controller link from PLM group 01 or 00 to device required for I2CS devices - main::print_log("[Insteon::ALDB_PLM] DEBUG2 Ignoring orphan PLM controller(01 or 00) link to " - . $device->get_object_name() ) if $main::Debug{insteon} >= 2; - } - elsif ($audit_mode) - { - &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan PLM controller link ($group) to: " - . $device->get_object_name() . "($data3)") - if $main::Debug{insteon}; - } - else - { - my %delete_req = (object => $device, group => $group, is_controller => 1, - callback => "$selfname->_aldb->_process_delete_queue(1)", - linkdevice => $self, data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - } - } - else - { - # iterate over all of the members of the Insteon_Link item - foreach my $member_ref (keys %{$$link{members}}) - { - my $member = $$link{members}{$member_ref}{object}; - # member will correspond to a scene member item - # and, if it is a light item, then get the real device - if ($member->isa('Light_Item')) - { - my @lights = $member->find_members('Insteon::BaseLight'); - if (@lights) - { - $member = $lights[0]; # pick the first - } - } - if ($member->isa('Insteon::BaseDevice')) - { - if ($member->is_deaf) - { - &::print_log("[Insteon::ALDB_PLM] ignoring link from PLM to " . - $member->get_object_name); - $is_invalid = 0; - } - else - { - my $linkmember = $member; - # make sure that this is a root device - if (!($member->is_root)) - { - $member = $member->get_root; - } - if (lc $member->device_id eq $$self{aldb}{$linkkey}{deviceid}) - { - # at this point, the forward link is ok; but, only if the reverse - # link also exists. So, check: - if ($member->has_link($self, $group, 0, $linkmember->group)) - { - $is_invalid = 0; - } - last; - } - } - } - else - { - $is_invalid = 0; - } - } # foreach $$link{members} - if ($is_invalid) - { - # then, there is a good chance that a reciprocal link exists; if so, delet it too - # KRK Need to delete ALL responder links regardless of data3 - if ($device->has_link($self,$group,0, $data3)) - { - if ($audit_mode) - { - &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete orphan controller link from PLM to " - . $device->get_object_name() - . " because no SCENE_MEMBER entry could be found " - . "in items.mht for INSTEON_ICONTROLLER: " - . $link->get_object_name()); - - } - else - { - my %delete_req = (object => $device, group => $group, is_controller => 1, - callback => "$selfname->_aldb->_process_delete_queue(1)", - linkdevice => $self, data3 => $data3); - push @{$$self{delete_queue}}, \%delete_req; - } - } - # KRK Why are we not deleting the invalid PLM link here? - } # if $is_invalid - } # else - } - } - } + $self->SUPER::delete_orphan_links($audit_mode); $$self{delete_queue_processed} = 0; # reset the counter From 474679738606aff38822ffb6f44c443c01a46d77 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 21:13:39 -0700 Subject: [PATCH 253/330] Insteon: Rewrite of Sync_Links to Make it More Readable Plus set data3 to the proper value And clean up perl warnings --- lib/Insteon/AllLinkDatabase.pm | 2 +- lib/Insteon/BaseInsteon.pm | 372 ++++++++++++++------------------- 2 files changed, 159 insertions(+), 215 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 75c12fb71..b5fcb5e9d 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -584,7 +584,7 @@ sub delete_orphan_links # Does a reciprocal link exist? # Temp OLD compatibility fix for data3, delete to #END to upgrade - if (($linked_device eq Insteon::active_interface) && + if (($linked_device eq Insteon::active_interface()) && (!$$self{device}->isa('Insteon::KeyPadLincRelay'))){ $linked_group = '00'; } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7a575d43d..1bfaa1291 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2819,243 +2819,187 @@ MisterHouse will add the link. If audit_mode is true, MisterHouse will print the actions it would have taken to the log, but will not take any actions. +The process does the following 5 checks in order: + + 1. Does a controller link exist for Device-> PLM + 2. Does a responder link exist on the PLM + +it then loops through all of the links defined for a device and checks: + + 3. Does the responder link exist + 4. Is the responder link accurate + 5. Does the controller link on this device exist + =cut sub sync_links { my ($self, $audit_mode, $callback, $failure_callback) = @_; + + # Intialize Variables @{$$self{sync_queue}} = (); # reset the work queue $$self{sync_queue_callback} = ($callback) ? $callback : undef; + my $subaddress = $self->group; + my $self_link_name = $self->get_object_name; my $insteon_object = $self->interface; - if (!($self->isa('Insteon::InterfaceController'))) - { + my $interface_object = Insteon::active_interface(); + my $interface_name = $interface_object->get_object_name; + if (!($self->isa('Insteon::InterfaceController'))) { $insteon_object = &Insteon::get_object($self->device_id,'01'); - if (!(defined($insteon_object))) - { - &main::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " - . "Please double check your items.mht file."); - } + &main::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " + . "Please double check your items.mht file.") if (!(defined($insteon_object))); } - my $self_link_name = $self->get_object_name; - # abort if $insteon_object doesn't exist + + # Abort if $insteon_object doesn't exist $self->_process_sync_queue() unless $insteon_object; - if ($$self{members}) - { - foreach my $member_ref (keys %{$$self{members}}) - { - my $member = $$self{members}{$member_ref}{object}; - # find real device if member is a Light_Item - if ($member->isa('Light_Item')) - { - my @children = $member->find_members('Insteon::BaseDevice'); - $member = $children[0]; + + # 1. Does a controller link exist for Device-> PLM + if (!($self->isa('Insteon::InterfaceController') && + $insteon_object->has_link($self->interface,$self->group,1,$subaddress))) { + my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, + group => $self->group, is_controller => 1, + callback => "$self_link_name->_process_sync_queue()", + data3 => $subaddress); + $link_req{cause} = "Adding controller record from $self_link_name to $interface_name"; + $link_req{data3} = $self->group; + push @{$$self{sync_queue}}, \%link_req; + } + + # 2. Does a responder link exist on the PLM + if (!($self->isa('Insteon::InterfaceController') && + $self->interface->has_link($insteon_object,$self->group,0,'00'))) { + my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, + group => $self->group, is_controller => 0, + callback => "$self_link_name->_process_sync_queue()", + data3 => '00'); + $link_req{cause} = "Adding responder record to $interface_name from $self_link_name"; + push @{$$self{sync_queue}}, \%link_req; + } + + if (!$$self{members}) { + #No members to sync + $self->_process_sync_queue(); + } + + # Loop members + foreach my $member_ref (keys %{$$self{members}}) { + my $member = $$self{members}{$member_ref}{object}; + + # find real device if member is a Light_Item + if ($member->isa('Light_Item')) { + my @children = $member->find_members('Insteon::BaseDevice'); + $member = $children[0]; + } + + #Initialize Loop Variables + my $member_name = $member->get_object_name; + my $member_root = $member->get_root; + my $requires_update = 0; + my $has_link = 1; + my $cause; + my $tgt_on_level = $$self{members}{$member_ref}{on_level}; + $tgt_on_level = '100' unless defined $tgt_on_level; + my $tgt_ramp_rate = $$self{members}{$member_ref}{ramp_rate}; + $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; + $tgt_on_level =~ s/(\d+)%?/$1/; + $tgt_ramp_rate =~ s/(\d)s?/$1/; + my $resp_aldbkey = lc $insteon_object->device_id . $self->group . '0'; + if ($member->group ne '01') { + $resp_aldbkey .= $member->group; + } + + # 3. Does the responder link exist + if (!$member_root->has_link($insteon_object, $self->group, 0, $member->group)){ + my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, + group => $self->group, is_controller => 0, + on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, + callback => "$self_link_name->_process_sync_queue()", + data3 => $member->group); + push @{$$self{sync_queue}}, \%link_req; + $has_link = 0; + } + + # 4. Is the responder link accurate + if ($member->isa('Insteon::DimmableLight') && $has_link) { + my $member_aldb = $member->_aldb; + my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; + my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; + my $cur_on_level = hex($data1)/2.55; + my $raw_cur_ramp_rate = $data2; + my $raw_tgt_ramp_rate = Insteon::DimmableLight::convert_ramp($tgt_ramp_rate); + if ($raw_cur_ramp_rate ne $raw_tgt_ramp_rate) { + $requires_update = 1; + $cause .= "Ramp rate "; } - my $linkmember = $member; - # find real device if member's group is not '01'; for example, cross-linked KeypadLincs - if ($member->group ne '01') - { - $member = &Insteon::get_object($member->device_id,'01'); + elsif ($cur_on_level != $tgt_on_level){ + $requires_update = 1; + $cause .= "On level "; } - my $tgt_on_level = $$self{members}{$member_ref}{on_level}; - $tgt_on_level = '100%' unless defined $tgt_on_level; - - my $tgt_ramp_rate = $$self{members}{$member_ref}{ramp_rate}; - $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; - # first, check existance for each link; if found, then perform an update (unless link is to PLM) - # if not, then add the link - if ($member->has_link($insteon_object, $self->group, 0, $linkmember->group)) - { - # TO-DO: only update link if the on_level and ramp_rate are different - my $requires_update = 0; - $tgt_on_level =~ s/(\d+)%?/$1/; - $tgt_ramp_rate =~ s/(\d)s?/$1/; - my $aldbkey = lc $insteon_object->device_id . $self->group . '0'; - if (($member->isa('Insteon::KeyPadLincRelay') or $member->isa('Insteon::KeyPadLinc')) - and $linkmember->group ne '01') { - $aldbkey .= $linkmember->group; - } - if (!($member->isa('Insteon::DimmableLight'))) - { - my $member_aldb = $member->_aldb; - if ($tgt_on_level >= 1 and $$member_aldb{aldb}{$aldbkey}{data1} ne 'ff') - { - $requires_update = 1; - $tgt_on_level = 100; - } - elsif ($tgt_on_level == 0 and $$member_aldb{aldb}{$aldbkey}{data1} ne '00') - { - $requires_update = 1; - } - if ($$member_aldb{aldb}{$aldbkey}{data2} ne '00') - { - $tgt_ramp_rate = 0; - } - } - else - { - my $member_aldb = $member->_aldb; - $tgt_ramp_rate = 0.1 unless $tgt_ramp_rate; - my $link_on_level = hex($$member_aldb{aldb}{$aldbkey}{data1})/2.55; - my $raw_ramp_rate = $$member_aldb{aldb}{$aldbkey}{data2}; - my $raw_tgt_ramp_rate = &Insteon::DimmableLight::convert_ramp($tgt_ramp_rate); - if (($raw_ramp_rate ne $raw_tgt_ramp_rate) && ($raw_ramp_rate ne '00' and $raw_tgt_ramp_rate ne '1f')) - { - $requires_update = 1; - &::print_log("[Insteon::BaseController] DEBUG: flagging " . $self->get_object_name - . " for update because existing ramp rate ($raw_ramp_rate) != target ($raw_tgt_ramp_rate)") - if $main::Debug{insteon}; - - } - elsif (($link_on_level > $tgt_on_level + 1) or ($link_on_level < $tgt_on_level -1)) - { - $requires_update = 1; - &::print_log("[Insteon::BaseController] DEBUG: flagging " . $self->get_object_name - . " for update because existing on level ($link_on_level) != target ($tgt_on_level)") - if $main::Debug{insteon}; - } - } - if ($requires_update) - { - if ($audit_mode) - { - &::print_log("[Insteon::BaseController] (AUDIT) - updating responder record to " - . $member->get_object_name . " for " - . $insteon_object->get_object_name . " with group:" . $self->group - . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate"); - } - else - { - my %link_req = ( member => $member, cmd => 'update', object => $insteon_object, - group => $self->group, is_controller => 0, - on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, - callback => "$self_link_name->_process_sync_queue()" ); - # set data3 is device is a KeypadLinc - if ($member->isa('Insteon::KeyPadLincRelay') or $member->isa('Insteon::KeyPadLinc')) - { - $link_req{data3} = $linkmember->group; - } - main::print_log("[Insteon::BaseController] DEBUG4: queuing update for responder record to " - . $member->get_object_name . " for " - . $insteon_object->get_object_name . " with group:" . $self->group - . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate") - if $main::Debug{insteon} >= 4; - push @{$$self{sync_queue}}, \%link_req; - } - } + } + elsif ($has_link){ + my $member_aldb = $member->_aldb; + my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; + my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; + if ($tgt_on_level >= 1 and $data1 ne 'ff') { + $requires_update = 1; + $tgt_on_level = 100; + $cause .= "On level "; } - else - { - if ($audit_mode) - { - &::print_log("[Insteon::BaseController] (AUDIT) - adding responder record to " - . $member->get_object_name . " for " - . $insteon_object->get_object_name . " with group:" . $self->group - . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate"); - } - else - { - my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, - group => $self->group, is_controller => 0, - on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, - callback => "$self_link_name->_process_sync_queue()" ); - # set data3 is device is a KeypadLinc - if ($member->isa('Insteon::KeyPadLincRelay') or $member->isa('Insteon::KeyPadLinc')) - { - $link_req{data3} = $linkmember->group; - } - main::print_log("[Insteon::BaseController] DEBUG4: queuing add for responder record to " - . $member->get_object_name . " for " - . $insteon_object->get_object_name . " with group:" . $self->group - . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate") - if $main::Debug{insteon} >= 4; - push @{$$self{sync_queue}}, \%link_req; - } + elsif ($tgt_on_level == 0 and $data1 ne '00') { + $requires_update = 1; + $cause .= "On level "; } - if (!($insteon_object->has_link($member, $self->group, 1, $linkmember->group))) - { - if ($audit_mode) - { - &::print_log("[Insteon::BaseController] (AUDIT) - adding controller record to " - . $insteon_object->get_object_name . " for " . $member->get_object_name - . " with group:" . $self->group); - } - else - { - my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, - group => $self->group, is_controller => 1, - callback => "$self_link_name->_process_sync_queue()" ); - # set data3 is device is a KeypadLinc - if ($member->isa('Insteon::KeyPadLincRelay') or $member->isa('Insteon::KeyPadLinc')) - { - $link_req{data3} = $linkmember->group; - } - main::print_log("[Insteon::BaseController] DEBUG4: queuing add for controller record to " - . $insteon_object->get_object_name . " for " . $member->get_object_name - . " with group:" . $self->group) if $main::Debug{insteon} >= 4; - push @{$$self{sync_queue}}, \%link_req; - } + if ($data2 ne '00') { + $requires_update = 1; + $tgt_ramp_rate = 0; + $cause .= "Ramp rate "; } } - } - # if not a plm controlled link, then confirm that a link back to the plm exists - if (!($self->isa('Insteon::InterfaceController'))) - { - my $subaddress = ($self->isa('Insteon::KeyPadLincRelay') or $self->isa('Insteon::KeyPadLinc')) ? $self->group : '00'; - #Make sure this device has a controller link to the PLM - if (!($insteon_object->has_link($self->interface,$self->group,1,$subaddress))) - { - if ($audit_mode) - { - &::print_log("[Insteon::BaseController] (AUDIT) - adding controller record to " - . $insteon_object->get_object_name . " for " - . $self->interface->get_object_name . " with group:" . $self->group); - } - else - { - my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, - group => $self->group, is_controller => 1, - callback => "$self_link_name->_process_sync_queue()" ); - $link_req{data3} = $self->group if $insteon_object->isa('Insteon::KeyPadLincRelay') or $insteon_object->isa('Insteon::KeyPadLinc'); - main::print_log("[Insteon::BaseController] DEBUG4: queuing add for controller record to " - . $insteon_object->get_object_name . " for " - . $self->interface->get_object_name . " with group:" . $self->group) - if $main::Debug{insteon} >= 4; - push @{$$self{sync_queue}}, \%link_req; - } - } - #Make sure the PLM has a responder link to this device - if (!($self->interface->has_link($insteon_object,$self->group,0,$subaddress))) - { - if ($audit_mode) - { - &::print_log("[Insteon::BaseController] (AUDIT) - adding responder record to " - . $self->interface->get_object_name . " for " - . $insteon_object->get_object_name . " with group:" . $self->group); - } - else - { - my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, - group => $self->group, is_controller => 0, - callback => "$self_link_name->_process_sync_queue()" ); - main::print_log("[Insteon::BaseController] DEBUG4: queuing add for responder record to " - . $self->interface->get_object_name . " for " - . $insteon_object->get_object_name . " with group:" . $self->group) - if $main::Debug{insteon} >= 4; - push @{$$self{sync_queue}}, \%link_req; - } + if ($requires_update) { + my %link_req = ( member => $member, cmd => 'update', object => $insteon_object, + group => $self->group, is_controller => 0, + on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, + callback => "$self_link_name->_process_sync_queue()", + data3 => $member->group); + $link_req{cause} = "Updating responder record on $member_name " + . "to fix $cause"; + push @{$$self{sync_queue}}, \%link_req; + } + + # 5. Does the controller link on this device exist + if (!($insteon_object->has_link($member, $self->group, 1, $subaddress))) { + my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, + group => $self->group, is_controller => 1, + callback => "$self_link_name->_process_sync_queue()", + data3 => $subaddress); + $link_req{cause} = "Adding controller record to $self_link_name for $member_name"; + push @{$$self{sync_queue}}, \%link_req; } } + my $num_sync_queue = @{$$self{sync_queue}}; if (!($num_sync_queue)) - { + { &::print_log("[Insteon::BaseController] Nothing to do when syncing links for " . $self->get_object_name) if $main::Debug{insteon}; } + foreach (@{$$self{sync_queue}}){ + my %sync_req = %{$_}; + my $audit_text = "(AUDIT)" if ($audit_mode); + my $log_text = "[Insteon::BaseController] $audit_text $sync_req{cmd}ing the following link on $self_link_name because "; + $log_text .= $sync_req{cause} . "\n"; + PRINT: for (keys %sync_req) { + next PRINT if ($_ eq 'cause'); + next PRINT if ($_ eq 'callback'); + $log_text .= "$_ = $sync_req{$_}; "; + } + ::print_log($log_text); + } + if ($audit_mode) { + @{$$self{sync_queue}} = (); + } + $self->_process_sync_queue(); - - # TO-DO: consult links table to determine if any "orphaned links" refer to this device; if so, then delete - # WARN: can't immediately do this as the link tables aren't finalized on the above operations - # until the end of the actual insteon memory poke sequences; therefore, may need to handle separately } sub _process_sync_queue { @@ -3144,7 +3088,7 @@ sub set_linked_devices my @lights = $member->find_members('Insteon::BaseDevice'); if (@lights) { - my $light = @lights[0]; + my $light = $lights[0]; # remember the current state to support resume $$self{members}{$member_ref}{resume_state} = $light->state; $member->manual($light, $ramp_rate); @@ -3227,7 +3171,7 @@ sub update_members # if they are Light_Items, then locate the Light_Item's Insteon_Device member my @lights = $member->find_members('Insteon::BaseDevice'); if (@lights) { - $device = @lights[0]; + $device = $lights[0]; } } elsif ($member->isa('Insteon::BaseDevice')) { $device = $member; From b767b795ef3f9ec2a5507854a5a9322c2476fd2e Mon Sep 17 00:00:00 2001 From: Pmatis Date: Fri, 18 Oct 2013 00:16:55 -0400 Subject: [PATCH 254/330] Initial rework of insteon debugging --- lib/Insteon.pm | 12 ++-- lib/Insteon/AllLinkDatabase.pm | 110 ++++++++++++++++----------------- lib/Insteon/BaseInsteon.pm | 99 +++++++++++++++++------------ lib/Insteon/BaseInterface.pm | 53 ++++++++++------ lib/Insteon/Controller.pm | 10 +-- lib/Insteon/IOLinc.pm | 18 +++--- lib/Insteon/Irrigation.pm | 8 +-- lib/Insteon/Lighting.pm | 6 +- lib/Insteon/Message.pm | 17 +++++ lib/Insteon/Security.pm | 14 ++--- lib/Insteon/Thermostat.pm | 48 +++++++------- lib/Insteon_PLM.pm | 58 ++++++++--------- 12 files changed, 252 insertions(+), 201 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 9038ff491..369b8e225 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -407,7 +407,7 @@ sub scan_all_linktables push @_scan_devices, $candidate_object; &main::print_log("[Scan all linktables] INFO1: " . $candidate_object->get_object_name - . " will be scanned.") if $main::Debug{insteon} >= 1; + . " will be scanned.") if $candidate_object->debuglevel(1); } else { @@ -1076,13 +1076,13 @@ sub check_all_aldb_versions { main::print_log("[Insteon] DEBUG4 Checking aldb version for " . $ALDB_device->get_object_name() - . " ($count of $ALDB_cnt)") if ($main::Debug{insteon} >= 4); + . " ($count of $ALDB_cnt)") if ($ALDB_device->debuglevel(4)); $ALDB_device->check_aldb_version(); } else { main::print_log("[Insteon] DEBUG4 " . $ALDB_device->get_object_name . " does not have its own aldb ($count of $ALDB_cnt)") - if ($main::Debug{insteon} >= 4); + if ($ALDB_device->debuglevel(4)); } } main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); @@ -1100,18 +1100,18 @@ sub check_thermo_versions $thermo_device->get_root()->engine_version eq "I2CS"){ main::print_log("[Insteon] DEBUG4 Setting thermostat " . $thermo_device->get_object_name() . " to i2CS") - if ($main::Debug{insteon} >= 4); + if ($thermo_device->debuglevel(4)); bless $thermo_device, 'Insteon::Thermo_i2CS'; $thermo_device->init(); } else { main::print_log("[Insteon] DEBUG4 Setting thermostat " . $thermo_device->get_object_name() . " to i1") - if ($main::Debug{insteon} >= 4); + if ($thermo_device->debuglevel(4)); bless $thermo_device, 'Insteon::Thermo_i1'; } } - #main::print_log("[Insteon] DEBUG4 Checking thermostat version of all devices completed") if ($main::Debug{insteon} >= 4); + #main::print_log("[Insteon] DEBUG4 Checking thermostat version of all devices completed") if ($self->debuglevel(4)); } =back diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index aa47c807b..f1f5721d8 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -145,7 +145,7 @@ sub query_aldb_delta $self->{_aldb_changed_callback} = undef; eval ($callback); &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDatabase; } } elsif ($action eq "check" && ((&main::get_tickcount - $self->scandatetime()) <= 2000)){ @@ -160,7 +160,7 @@ sub query_aldb_delta $self->{_aldb_unchanged_callback} = undef; eval ($callback); &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDatabase; } } else { @@ -209,7 +209,7 @@ sub restore_string } $aldb .= $record; } -# &::print_log("[AllLinkDataBase] aldb restore string: $aldb") if $main::Debug{insteon}; +# &::print_log("[AllLinkDataBase] aldb restore string: $aldb") if $self->debuglevel(); if (defined $self->scandatetime) { $restore_string .= $$self{device}->get_object_name . "->_aldb->scandatetime(q~" . $self->scandatetime . "~) if " @@ -337,7 +337,7 @@ sub delete_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDatabase; } } elsif ($link_parms{address} && $link_parms{aldb_check} eq "ok") @@ -395,7 +395,7 @@ sub delete_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] error encountered during delete_link callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDataBase; } } @@ -561,7 +561,7 @@ sub delete_orphan_links if ($group eq '01' || $group eq '00') { #ignore manual responder link to PLM group 01 or 00 required for I2CS devices main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01 or 00") if $main::Debug{insteon} >= 2; + . $selfname . " to PLM for group 01 or 00") if $self->debuglevel(2); } elsif ($audit_mode) { @@ -857,7 +857,7 @@ sub _process_delete_queue { else { &::print_log("[Insteon::AllLinkDatabase] Nothing else to do for " . $$self{device}->get_object_name . " after deleting " - . $$self{delete_queue_processed} . " links") if $main::Debug{insteon}; + . $$self{delete_queue_processed} . " links") if $self->debuglevel(); $$self{device}->interface->_aldb->_process_delete_queue($$self{delete_queue_processed}); } } @@ -978,10 +978,10 @@ sub get_first_empty_address } $first_address = ($high_address > 0) ? sprintf('%04x', $high_address - 8) : 0; main::print_log("[Insteon::AllLinkDatabase] DEBUG4: No empty link entries; using next lowest link address [" - .$first_address."]") if $main::Debug{insteon} >= 4; + .$first_address."]") if $self->debuglevel(4); } else { main::print_log("[Insteon::AllLinkDatabase] DEBUG4: Found empty address [" - .$first_address."] in empty array") if $main::Debug{insteon} >= 4; + .$first_address."] in empty array") if $self->debuglevel(4); } return $first_address; @@ -1047,7 +1047,7 @@ sub add_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDatabase; } } @@ -1063,7 +1063,7 @@ sub add_link eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDatabase; } } @@ -1086,7 +1086,7 @@ sub add_link &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") - if $main::Debug{insteon} >= 2; + if $self->debuglevel(2); my ($data1, $data2); if($link_parms{is_controller}) { $data1 = '03'; #application retries == 3 @@ -1106,14 +1106,14 @@ sub add_link . $$self{device}->get_object_name . " does not have a record of the first empty ALDB record." . " Please rescan this device's link table") - if $main::Debug{insteon}; + if $self->debuglevel(); if ($$self{_success_callback}) { package main; eval ($$self{_success_callback}); &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon::AllLinkDatabase; } } @@ -1145,7 +1145,7 @@ sub update_link my $ramp_rate = $link_parms{ramp_rate}; $ramp_rate =~ s/(\d)s?/$1/; &::print_log("[Insteon::AllLinkDatabase] updating " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name - . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $main::Debug{insteon}; + . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->debuglevel(); my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; @@ -1171,7 +1171,7 @@ sub update_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::AllLinkDatabase; } } @@ -1185,14 +1185,14 @@ sub update_link &::print_log("[Insteon::AllLinkDatabase] ERROR: updating link record failed because " . $$self{device}->get_object_name . " does not have an existing ALDB entry key=$key") - if $main::Debug{insteon}; + if $self->debuglevel(); if ($$self{_success_callback}) { package main; eval ($$self{_success_callback}); &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon::AllLinkDatabase; } } @@ -1580,7 +1580,7 @@ sub _on_peek my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); if ($msg{is_extended}) { &::print_log("[Insteon::ALDB_i1]: extended peek for " . $$self{device}->{object_name} - . " is " . $msg{extra}) if $main::Debug{insteon}; + . " is " . $msg{extra}) if $self->debuglevel(); } else { @@ -1632,7 +1632,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); my $flag = hex($msg{extra}); $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; @@ -1662,7 +1662,7 @@ sub _on_peek } &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " completed link memory scan") - if $main::Debug{insteon}; + if $self->debuglevel(); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -1712,7 +1712,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{pending_aldb}{group} = lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devhi'; @@ -1732,7 +1732,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{pending_aldb}{deviceid} = lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devmid'; @@ -1753,7 +1753,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{pending_aldb}{deviceid} .= lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devlo'; @@ -1774,7 +1774,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{pending_aldb}{deviceid} .= lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_data1'; @@ -1797,7 +1797,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{_mem_action} = 'aldb_data2'; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{pending_aldb}{data1} = $msg{extra}; @@ -1820,7 +1820,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{pending_aldb}{data2} = $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_data3'; @@ -1843,7 +1843,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{pending_aldb}{data3} = $msg{extra}; if ($$self{_stress_test_act}){ @@ -1925,7 +1925,7 @@ sub _on_peek else { ::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) - if $main::Debug{insteon}; + if $self->debuglevel(); } } } @@ -2008,7 +2008,7 @@ sub _write_link if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) { &::print_log("[Insteon::ALDB_i1] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $main::Debug{insteon}; + if $self->debuglevel(); $data3 = $$self{device}->group; } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; @@ -2024,7 +2024,7 @@ sub _write_link package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i1] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon::ALDB_i1; } } @@ -2132,7 +2132,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for _mem_activity=".$$self{_mem_activity} - ." _mem_action=". $$self{_mem_action}) if $main::Debug{insteon} >= 3; + ." _mem_action=". $$self{_mem_action}) if $self->debuglevel(3); if ($$self{_mem_action} eq 'aldb_i2read') { @@ -2144,12 +2144,12 @@ sub on_read_write_aldb $$self{_mem_action} = 'aldb_i2readack'; &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received ack") - if $main::Debug{insteon} >= 3; + if $self->debuglevel(3); } else { #otherwise just ignore the message because it is out of sequence &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] ack not received. " - . "ignoring message") if $main::Debug{insteon} >= 3; + . "ignoring message") if $self->debuglevel(3); } } @@ -2158,14 +2158,14 @@ sub on_read_write_aldb if($msg{is_ack}) { &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received duplicate ack. Ignoring.") - if $main::Debug{insteon} >= 3; + if $self->debuglevel(3); $clear_message = 0; } elsif(length($msg{extra})<30) { &::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #can't clear message, if valid message doesn't arrive #resend logic will kick in @@ -2176,7 +2176,7 @@ sub on_read_write_aldb . " address received did not match address requested: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $main::Debug{insteon} >= 3; + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #can't clear message, if valid message doesn't arrive #resend logic will kick in @@ -2194,7 +2194,7 @@ sub on_read_write_aldb if (lc $$self{_mem_msb} eq '00' and lc $$self{_mem_lsb} eq '00') { main::print_log("[Insteon::ALDB_i2] DEBUG4: Start of scan; initializing aldb structure") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); # reinit the aldb hash as there will be a new one $$self{aldb} = undef; # reinit the empty address list @@ -2236,10 +2236,10 @@ sub on_read_write_aldb . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) - if(($$self{pending_aldb}{inuse}) and $main::Debug{insteon} >= 3); + if(($$self{pending_aldb}{inuse}) and $self->debuglevel(3)); main::print_log("[Insteon::ALDB_i2] DEBUG4: scan done; adding last address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); # scan done; clear out state flags $$self{_mem_action} = undef; @@ -2256,7 +2256,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " completed link memory scan: status: " . $self->health()) - if $main::Debug{insteon}; + if $self->debuglevel(); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -2267,7 +2267,7 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] DEBUG4: inuse flag == false; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); $self->add_empty_address($$self{pending_aldb}{address}); } else @@ -2293,14 +2293,14 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] DEBUG4: duplicate link found; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to duplicates array") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); $self->add_duplicate_link_address($$self{pending_aldb}{address}); } else { main::print_log("[Insteon::ALDB_i2] DEBUG4: active link found; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to aldb") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; } } @@ -2340,7 +2340,7 @@ sub on_read_write_aldb $$self{pending_aldb} = undef; main::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " link write completed for [".$$self{aldb}{$aldbkey}{address}."]") - if $main::Debug{insteon} >= 3; + if $self->debuglevel(3); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -2373,7 +2373,7 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) - if $main::Debug{insteon}; + if $self->debuglevel(); $clear_message = 0; } return $clear_message; @@ -2420,7 +2420,7 @@ sub _write_link if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) { &::print_log("[Insteon::ALDB_i2] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $main::Debug{insteon}; + if $self->debuglevel(); $data3 = $$self{device}->group; } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; @@ -2441,7 +2441,7 @@ sub _write_link package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon::ALDB_i2; } } @@ -2491,7 +2491,7 @@ sub _write_delete package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon::ALDB_i2; } } @@ -2771,7 +2771,7 @@ sub delete_orphan_links &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan Link to non-existant deviceid: " . $deviceid . "; group:$group; " . (($is_controller) ? "controller; data:$data3" : "responder")) - if $main::Debug{insteon}; + if $self->debuglevel(); } else { @@ -2796,13 +2796,13 @@ sub delete_orphan_links if ($group eq '01' || $group eq '00') { #ignore manual controller link from PLM group 01 or 00 to device required for I2CS devices main::print_log("[Insteon::ALDB_PLM] DEBUG2 Ignoring orphan PLM controller(01 or 00) link to " - . $device->get_object_name() ) if $main::Debug{insteon} >= 2; + . $device->get_object_name() ) if $self->debuglevel(2); } elsif ($audit_mode) { &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan PLM controller link ($group) to: " . $device->get_object_name() . "($data3)") - if $main::Debug{insteon}; + if $self->debuglevel(); } else { @@ -2926,7 +2926,7 @@ sub _process_delete_queue { . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") - if $main::Debug{insteon}; + if $self->debuglevel(); $self->delete_link(%delete_req); } elsif ($delete_req{linkdevice}) @@ -2996,7 +2996,7 @@ sub delete_link package main; eval ($link_parms{callback}); &::print_log("[Insteon_PLM] error in add link callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon_PLM; } } @@ -3047,7 +3047,7 @@ sub add_link package main; eval ($link_parms{callback}); &::print_log("[Insteon::ALDB_PLM] error in add link callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon_PLM; } } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f4b63ef8a..ea31a20e2 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -183,7 +183,7 @@ sub device_id Used to store and return the associated group of a device. -If provided, stores group as the device's group. +If provided, stores group as the device group. =cut @@ -194,6 +194,23 @@ sub group return $$self{m_group}; } +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($self, $debug_level) = @_; + $debug_level = 1 unless $debug_level; + my $objname = lc $self->get_object_name; + &::print_log("debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; + return 1 if $main::Debug{insteon} >= $debug_level; + return 1 if $main::Debug{$objname} >= $debug_level; + return 0; +} + =item C Changes the amount of time MH will wait to receive a response from a device before @@ -262,7 +279,7 @@ sub default_hop_count unshift(@{$$self{hop_array}}, $$self{default_hop_count}) if (!defined(@{$$self{hop_array}})); if (defined($hop_count)){ ::print_log("[Insteon::BaseObject] DEBUG3: Adding hop count of " . $hop_count . " to hop_array of " - . $self->get_object_name) if $main::Debug{insteon} >= 3; + . $self->get_object_name) if $self->debuglevel(3); unshift(@{$$self{hop_array}}, $hop_count) } pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >20); @@ -315,7 +332,7 @@ sub equals =item C -Used to set the device's state. If called by device or a device linked to device, +Used to set the device state. If called by device or a device linked to device, calls C. If called by something else, will send the command to the device. @@ -363,7 +380,7 @@ sub set return if (lc $p_state eq lc $self->state) and $self->is_acknowledged and not(($p_setby->isa('Insteon::BaseObject') and ($p_setby eq $self))); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . "::set($p_state, $setby_name)") if $main::Debug{insteon}; + . "::set($p_state, $setby_name)") if $self->debuglevel(); $self->set_receive($p_state,$p_setby,$p_response) if defined $p_state; } else { my $message = $self->derive_message($p_state); @@ -372,7 +389,7 @@ sub set # $self->_send_cmd(command => $p_state, # type => (($self->isa('Insteon::Insteon_Link') and !($self->is_root)) ? 'alllink' : 'standard')); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") - if $main::Debug{insteon}; + if $self->debuglevel(); $self->is_acknowledged(0); $$self{pending_state} = $p_state; $$self{pending_setby} = $p_setby; @@ -416,7 +433,7 @@ sub is_acknowledged =item C -Updates the device's state in MisterHouse. Triggers state_now, state_changed, and +Updates the device state in MisterHouse. Triggers state_now, state_changed, and state_final variables to update accordingly. Which causes tie_events to occur. If state was set to the same state within the last 1 second, then this is ignored. @@ -433,7 +450,7 @@ sub set_receive && ($curr_milli - $$self{set_milliseconds} < $window)){ ::print_log("[Insteon::BaseObject] Ignoring duplicate set " . $p_state . " state command for " . $self->get_object_name . " received in " . - "less than $window milliseconds") if $main::Debug{insteon}; + "less than $window milliseconds") if $self->debuglevel(); } else { $$self{set_milliseconds} = $curr_milli; $self->SUPER::set($p_state, $p_setby, $p_response); @@ -556,7 +573,7 @@ sub derive_message # confirm that the resulting $msg is legitimate if (!(defined($self->message_type_code($command)))) { - &::print_log("[Insteon::BaseInsteon] invalid state=$command") if $main::Debug{insteon}; + &::print_log("[Insteon::BaseInsteon] invalid state=$command") if $self->debuglevel(); return undef; } @@ -640,7 +657,7 @@ sub _is_info_request my $ack_on_level = sprintf("%d", int((hex($msg{extra}) * 100 / 255)+.5)); &::print_log("[Insteon::BaseObject] received status for " . $self->{object_name} . " with on-level: $ack_on_level%, " - . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; + . "hops left: $msg{hopsleft}") if $self->debuglevel(); $self->level($ack_on_level) if $self->can('level'); # update the level value if ($ack_on_level == 0) { $self->SUPER::set('off', $ack_setby); @@ -698,7 +715,7 @@ sub _is_info_request package main; eval ($callback); &::print_log("[Insteon::BaseObject] " . $self->get_object_name . ": error during scan callback $@") - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::BaseObject; } } @@ -709,7 +726,7 @@ sub _is_info_request $self->engine_version($version); &::print_log("[Insteon::BaseObject] received engine version for " . $self->{object_name} . " of $version. " - . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; + . "hops left: $msg{hopsleft}") if $self->debuglevel(); } return $is_info_request; } @@ -726,7 +743,7 @@ sub _process_message # by Insteon_Link. main::print_log("[Insteon::BaseObject] WARN: Message has invalid checksum") - if ($main::Debug{insteon} && !($msg{crc_valid}) + if ($self->debuglevel() && !($msg{crc_valid}) && $msg{is_extended} && $self->engine_version() eq 'I2CS'); my $clear_message = 0; @@ -792,7 +809,7 @@ sub _process_message if (!$corrupt_cmd){ $self->_process_command_stack(%msg); &::print_log("[Insteon::BaseObject] received ping acknowledgement from " . $self->{object_name}) - if $main::Debug{insteon}; + if $self->debuglevel(); $self->ping(); $clear_message = 1; } @@ -801,7 +818,7 @@ sub _process_message $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); if (!$corrupt_cmd){ &::print_log("[Insteon::BaseObject] received linking mode ACK from " . $self->{object_name}) - if $main::Debug{insteon}; + if $self->debuglevel(); $self->interface->_set_timeout('xmit', 2000); $clear_message = 0; } @@ -818,7 +835,7 @@ sub _process_message # signal receipt of message to the command stack in case commands are queued $self->_process_command_stack(%msg); &::print_log("[Insteon::BaseObject] received command/state (awaiting) acknowledge from " . $self->{object_name} - . ": $pending_cmd and data: $msg{extra}") if $main::Debug{insteon}; + . ": $pending_cmd and data: $msg{extra}") if $self->debuglevel(); } } else @@ -830,7 +847,7 @@ sub _process_message $self->_process_command_stack(%msg); &::print_log("[Insteon::BaseObject] received command/state acknowledge from " . $self->{object_name} . ": " . (($msg{command}) ? $msg{command} : "(unknown)") - . " and data: $msg{extra}") if $main::Debug{insteon}; + . " and data: $msg{extra}") if $self->debuglevel(); } if ($corrupt_cmd) { main::print_log("[Insteon::BaseObject] WARN: received a message from " @@ -850,7 +867,7 @@ sub _process_message . $self->get_nack_msg_for( $msg{extra} ) .") for " . $self->{object_name} . ". It may be unplugged, have a burned out bulb, or this may be a new I2CS " . "type device that must first be manually linked to the PLM using the set button.") - if $main::Debug{insteon}; + if $self->debuglevel(); } else { @@ -864,7 +881,7 @@ sub _process_message if($p_setby->active_message->failure_callback) { main::print_log("[Insteon::BaseObject] WARN: Now calling message failure callback: " - . $p_setby->active_message->failure_callback) if $main::Debug{insteon}; + . $p_setby->active_message->failure_callback) if $self->debuglevel(); $self->failure_reason('NAK'); package main; eval $p_setby->active_message->failure_callback; @@ -899,7 +916,7 @@ sub _process_message if ($msg{command} eq 'link_cleanup_report'){ if ($msg{extra} == 0){ ::print_log("[Insteon::BaseObject] DEBUG Received AllLink Cleanup Success for " - . $self->{object_name}) if $main::Debug{insteon} >= 1; + . $self->{object_name}) if $self->debuglevel(1); } else { ::print_log("[Insteon::BaseObject] WARN " . $msg{extra} . " Device(s) failed to " . "acknowledge the command from " . $self->{object_name}); @@ -915,7 +932,7 @@ sub _process_message my $timeout = (scalar(@links)+1) * 300; ::print_log("[Insteon::BaseObject] DEBUG3 Delaying any outgoing messages ". "by $timeout milliseconds to avoid collision with subsequent cleanup ". - "messages from " . $self->get_object_name) if ($main::Debug{insteon} >= 3); + "messages from " . $self->get_object_name) if ($self->debuglevel(3)); $self->interface->_set_timeout('xmit', $timeout); } } @@ -924,14 +941,14 @@ sub _process_message if (($self->state eq $p_state or $self->state_final eq $p_state) and $$self{_pending_cleanup}){ ::print_log("[Insteon::BaseObject] Ignoring Received Direct AllLink Cleanup Message for " - . $self->{object_name} . " since AllLink Broadcast Message was Received.") if $main::Debug{insteon}; + . $self->{object_name} . " since AllLink Broadcast Message was Received.") if $self->debuglevel(); } else { $self->set($p_state, $self); } $$self{_pending_cleanup} = 0; } else { main::print_log("[Insteon::BaseObject] Ignoring unsupported command from " - . $self->{object_name}) if $main::Debug{insteon}; + . $self->{object_name}) if $self->debuglevel(); $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); } } @@ -994,11 +1011,11 @@ sub _process_command_stack package main; eval ($callback); &::print_log("[Insteon::BaseObject] error in queue timer callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::BaseObject; } } else { -# &::print_log("[Insteon_Device] " . $self->get_object_name . " command queued but not yet sent; awaiting ack from prior command") if $main::Debug{insteon}; +# &::print_log("[Insteon_Device] " . $self->get_object_name . " command queued but not yet sent; awaiting ack from prior command") if $self->debuglevel(); } } @@ -1820,7 +1837,7 @@ sub delete_link =item C -Scans a device's link table and caches a copy. +Scans a device link table and caches a copy. =cut @@ -1931,7 +1948,7 @@ sub _get_engine_version_failure my $failure_reason = $self->failure_reason(); main::print_log("[Insteon::BaseDevice::_get_engine_version_failure] DEBUG4: " - ."failure reason: $failure_reason") if $main::Debug{insteon} >= 4; + ."failure reason: $failure_reason") if $self->debuglevel(4); if($failure_reason eq 'NAK') { @@ -1995,7 +2012,7 @@ sub ping package main; eval ($complete_callback); &::print_log("[Insteon::BaseDevice] error in ping callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::BaseDevice; delete $$self{ping_callback}; } @@ -2098,7 +2115,7 @@ sub get_devcat =item C -Sets and returns the device's firmware version. Value can be obtained from the +Sets and returns the device firmware version. Value can be obtained from the device by calling C. =cut @@ -2301,7 +2318,7 @@ sub stress_test package main; eval ($complete_callback); &::print_log("[Insteon::BaseDevice] error in stress_test callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::BaseDevice; delete $$self{stress_test_callback}; } @@ -2616,7 +2633,7 @@ sub check_aldb_version if ($new_version) { main::print_log("[Insteon::BaseDevice] DEBUG4: aldb_version is " .$self->_aldb->aldb_version()." but device is ".$engine_version. - ". Remapping aldb version to $new_version") if $main::Debug{insteon} >= 4; + ". Remapping aldb version to $new_version") if $self->debuglevel(4); my $restore_string = ''; if ($self->_aldb) { $restore_string = $self->_aldb->restore_string(); @@ -2633,7 +2650,7 @@ sub check_aldb_version package main; eval ($restore_string); &::print_log("[Insteon::BaseDevice] error in eval creating ALDB object: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); package Insteon::BaseDevice; } } @@ -2885,7 +2902,7 @@ sub sync_links $requires_update = 1; &::print_log("[Insteon::BaseController] DEBUG: flagging " . $self->get_object_name . " for update because existing ramp rate ($raw_ramp_rate) != target ($raw_tgt_ramp_rate)") - if $main::Debug{insteon}; + if $self->debuglevel(); } elsif (($link_on_level > $tgt_on_level + 1) or ($link_on_level < $tgt_on_level -1)) @@ -2893,7 +2910,7 @@ sub sync_links $requires_update = 1; &::print_log("[Insteon::BaseController] DEBUG: flagging " . $self->get_object_name . " for update because existing on level ($link_on_level) != target ($tgt_on_level)") - if $main::Debug{insteon}; + if $self->debuglevel(); } } if ($requires_update) @@ -2920,7 +2937,7 @@ sub sync_links . $member->get_object_name . " for " . $insteon_object->get_object_name . " with group:" . $self->group . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); push @{$$self{sync_queue}}, \%link_req; } } @@ -2949,7 +2966,7 @@ sub sync_links . $member->get_object_name . " for " . $insteon_object->get_object_name . " with group:" . $self->group . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate") - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); push @{$$self{sync_queue}}, \%link_req; } } @@ -2973,7 +2990,7 @@ sub sync_links } main::print_log("[Insteon::BaseController] DEBUG4: queuing add for controller record to " . $insteon_object->get_object_name . " for " . $member->get_object_name - . " with group:" . $self->group) if $main::Debug{insteon} >= 4; + . " with group:" . $self->group) if $self->debuglevel(4); push @{$$self{sync_queue}}, \%link_req; } } @@ -3001,7 +3018,7 @@ sub sync_links main::print_log("[Insteon::BaseController] DEBUG4: queuing add for controller record to " . $insteon_object->get_object_name . " for " . $self->interface->get_object_name . " with group:" . $self->group) - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); push @{$$self{sync_queue}}, \%link_req; } } @@ -3022,7 +3039,7 @@ sub sync_links main::print_log("[Insteon::BaseController] DEBUG4: queuing add for responder record to " . $self->interface->get_object_name . " for " . $insteon_object->get_object_name . " with group:" . $self->group) - if $main::Debug{insteon} >= 4; + if $self->debuglevel(4); push @{$$self{sync_queue}}, \%link_req; } } @@ -3031,7 +3048,7 @@ sub sync_links if (!($num_sync_queue)) { &::print_log("[Insteon::BaseController] Nothing to do when syncing links for " . $self->get_object_name) - if $main::Debug{insteon}; + if $self->debuglevel(); } $self->_process_sync_queue(); @@ -3058,7 +3075,7 @@ sub _process_sync_queue { package main; eval ($$self{sync_queue_callback}); &::print_log("[Insteon::BaseController] error in sync links callback: " . $@) - if $@ and $main::Debug{insteon}; + if $@ and $self->debuglevel(); $$self{sync_queue_callback} = undef; package Insteon::BaseController; } else { @@ -3218,7 +3235,7 @@ sub update_members my %current_record = $device->get_link_record($self->device_id . $self->group); if (%current_record) { &::print_log("[Insteon::BaseController] remote record: $current_record{data1}") - if $::Debug{insteon}; + if $self->debuglevel(); } } } diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 3842c5692..15f7e45b2 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -119,6 +119,23 @@ sub equals return 0; } } + +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($self, $debug_level) = @_; + $debug_level = 1 unless $debug_level; + my $objname = lc $self->get_object_name; + &::print_log("debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; + return 1 if $main::Debug{insteon} >= $debug_level; + return 1 if $main::Debug{$objname} >= $debug_level; + return 0; + } =item C<_is_duplicate(cmd)> @@ -296,7 +313,7 @@ sub queue_message my $setby = $message->setby; if ($self->_is_duplicate($message->interface_data) && !($message->isa('Insteon::X10Message'))) { - &main::print_log("[Insteon::BaseInterface] Attempt to queue command already in queue; skipping ...") if $main::Debug{insteon}; + &main::print_log("[Insteon::BaseInterface] Attempt to queue command already in queue; skipping ...") if $self->debuglevel(); } else { @@ -354,7 +371,7 @@ sub process_queue &::print_log("[Insteon::BaseInterface] WARN: number of retries (" . $self->active_message->send_attempts . ") for " . $self->active_message->to_string() - . " exceeds limit. Now moving on...") if $main::Debug{insteon}; + . " exceeds limit. Now moving on...") if $self->debuglevel(); # !!!!!!!!! TO-DO - handle failure timeout ??? my $failed_message = $self->active_message; # make sure to let the sending object know!!! @@ -375,7 +392,7 @@ sub process_queue if ($failed_message->failure_callback) { &::print_log("[Insteon::BaseInterface] WARN: Message Timeout: Now calling callback: " . - $failed_message->failure_callback) if $main::Debug{insteon}; + $failed_message->failure_callback) if $self->debuglevel(); $failed_message->setby->failure_reason('timeout') if (defined($failed_message->setby) and $failed_message->setby->can('failure_reason')); package main; @@ -488,7 +505,7 @@ sub on_interface_info_received my ($self) = @_; &::print_log("[Insteon_PLM] PLM id: " . $self->device_id . " firmware: " . $self->firmware) - if $main::Debug{insteon}; + if $self->debuglevel(); $self->clear_active_message(); } @@ -521,7 +538,7 @@ sub on_standard_insteon_received #time has been required. Extra 50 millis helps prevent dupes $wait_time = ($wait_time * 100) + 50; $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; - ::print_log($wait_message) if ($main::Debug{insteon} >= 3 && $wait_time > 50); + ::print_log($wait_message) if ($self->debuglevel(3) && $wait_time > 50); $self->_set_timeout('xmit', $wait_time); # get the matching object @@ -536,13 +553,13 @@ sub on_standard_insteon_received $msg{command} = $object->message_type($msg{cmd_code}); &::print_log("[Insteon::BaseInterface] Received message from: ". $object->get_object_name ."; command: $msg{command}; type: $msg{type}; group: $msg{group}") - if (!($msg{is_ack} or $msg{is_nack})) and $main::Debug{insteon}; + if (!($msg{is_ack} or $msg{is_nack})) and $self->debuglevel(); } if ($msg{is_ack} or $msg{is_nack}) { main::print_log("[Insteon::BaseInterface] DEBUG3: PLM command:insteon_received; " . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") - if $main::Debug{insteon} >=3; + if $self->debuglevel(3); # need to confirm that this message corresponds to the current active one before clearing it # TO-DO!!! This is a brute force and poor compare technique; needs to be replaced by full compare if ($self->active_message && ref $self->active_message->setby) @@ -564,7 +581,7 @@ sub on_standard_insteon_received if($object->_process_message($self, %msg)) { if ($self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; + . $self->active_message->success_callback) if $self->debuglevel(4); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; @@ -594,11 +611,11 @@ sub on_standard_insteon_received if (($msg{extra} == $self->active_message->setby->group)){ &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received for scene " . $object->get_object_name . " from " . $setby_object->get_object_name) - if $main::Debug{insteon} >= 3; + if $self->debuglevel(3); } elsif ($self->active_message->command_type eq 'all_link_direct_cleanup' && lc($self->active_message->setby->device_id) eq $msg{source}) { - &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $main::Debug{insteon} >= 2; + &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $self->debuglevel(2); $self->clear_active_message(); } else { @@ -607,7 +624,7 @@ sub on_standard_insteon_received . $object->get_object_name . ", but group in recent message " . $msg{extra}. " did not match group in " . "prior sent message group " . $self->active_message->setby->group) - if $main::Debug{insteon} >= 3; + if $self->debuglevel(3); } # If ACK or NACK received then PLM is still working on the ALL Link Command # Increase the command timeout to wait for next one @@ -645,7 +662,7 @@ sub on_standard_insteon_received # then, the above cleanup handler would be run &main::print_log("[Insteon::BaseInterface] DEBUG3: received cleanup message responding to " . "PLM controller group: $msg{extra}. Ignoring as this has already been processed") - if $main::Debug{insteon} >= 3; + if $self->debuglevel(3); } else { @@ -698,7 +715,7 @@ sub on_extended_insteon_received #time has been required. Extra 50 millis helps prevent dupes $wait_time = ($wait_time * 200) + 50; $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; - ::print_log($wait_message) if ($main::Debug{insteon} >= 3 && $wait_time > 50); + ::print_log($wait_message) if ($self->debuglevel(3) && $wait_time > 50); $self->_set_timeout('xmit', $wait_time); # get the matching object @@ -713,14 +730,14 @@ sub on_extended_insteon_received $msg{command} = $object->message_type($msg{cmd_code}); main::print_log("[Insteon::BaseInterface] DEBUG: PLM command:insteon_ext_received; " . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") - if( (!($msg{is_ack} or $msg{is_nack}) and $main::Debug{insteon}) - or $main::Debug{insteon} >= 3); + if( (!($msg{is_ack} or $msg{is_nack}) and $self->debuglevel()) + or $self->debuglevel(3)); } - &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $main::Debug{insteon} >=3; + &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $self->debuglevel(3); if($object->_process_message($self, %msg)) { if (ref $self->active_message && $self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; + . $self->active_message->success_callback) if $self->debuglevel(4); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; @@ -879,7 +896,7 @@ sub _is_duplicate_received { $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); }; ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " - . $message_data . ", from $source.") if $main::Debug{insteon}; + . $message_data . ", from $source.") if $self->debuglevel(); } else { #Message was not in hash, so add it $$self{received_commands}{$key} = $curr_milli + $delay; diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 871ed28b6..40dbed355 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -108,12 +108,12 @@ sub set $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); if (not defined($self->get_idle_time) or $self->get_idle_time > 1 or $self->state ne $p_state) { &::print_log("[Insteon::RemoteLinc] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; + . "::set_receive($p_state, $setby_name)") if $self->debuglevel(); $self->set_receive($p_state,$p_setby); } else { &::print_log("[Insteon::RemoteLinc] " . $self->get_object_name() . "::set_receive($p_state, $setby_name) deferred due to repeat within 1 second") - if $main::Debug{insteon}; + if $self->debuglevel(); } return; } @@ -243,9 +243,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::RemoteLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::RemoteLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::RemoteLinc] Clearing active message") if $main::Debug{insteon}; + main::print_log("[Insteon::RemoteLinc] Clearing active message") if $self->debuglevel(); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -268,7 +268,7 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::RemoteLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); } } else { diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index 2983a61b9..b6acd4d2a 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -160,11 +160,11 @@ sub set if ($p_state eq $$self{child_state} && ($curr_milli - $$self{child_set_milliseconds} < $window)) { ::print_log("[Insteon::IOLinc] Received duplicate ". $self->get_object_name - . " sensor " . $p_state . " message, ignoring.") if $main::Debug{insteon}; + . " sensor " . $p_state . " message, ignoring.") if $self->debuglevel(); } else { ::print_log("[Insteon::IOLinc] Received ". $self->get_object_name - . " sensor " . $p_state . " message.") if $main::Debug{insteon}; + . " sensor " . $p_state . " message.") if $self->debuglevel(); $$self{child_state} = $p_state; $$self{child_set_milliseconds} = $curr_milli; if (ref $$self{child_sensor}){ @@ -217,7 +217,7 @@ sub _is_info_request my $child_state = &Insteon::BaseObject::derive_link_state(hex($msg{extra})); &::print_log("[Insteon::IOLinc] received status for " . $self->get_object_name . "sensor of: $child_state " - . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; + . "hops left: $msg{hopsleft}") if $self->debuglevel(); $ack_setby = $$self{child_sensor} if ref $$self{child_sensor}; if (ref $$self{child_sensor}){ $$self{child_sensor}->set_receive($child_state, $ack_setby); @@ -269,9 +269,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::IOLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::IOLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::IOLinc] Clearing active message") if $main::Debug{insteon}; + main::print_log("[Insteon::IOLinc] Clearing active message") if $self->debuglevel(); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -288,12 +288,12 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::IOLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); } } elsif ($msg{command} eq "set_operating_flags" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::IOLinc] Acknowledged flag set for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::IOLinc] Acknowledged flag set for " . $self->get_object_name) if $self->debuglevel(); $clear_message = 1; $self->_process_command_stack(%msg); } @@ -318,12 +318,12 @@ sub set_momentary_time my $root = $self->get_root(); if ($momentary_time == 0){ ::print_log("[Insteon::IOLinc] Setting " . $self->get_object_name . - " to Latching Relay Mode." ) if $main::Debug{insteon}; + " to Latching Relay Mode." ) if $self->debuglevel(); } elsif ($momentary_time <= 255) { $momentary_time = 2 if $momentary_time == 1; #Can't set to 1 ::print_log("[Insteon::IOLinc] Setting Momentary Time to $momentary_time " . - "tenths of a second for " . $self->get_object_name) if $main::Debug{insteon}; + "tenths of a second for " . $self->get_object_name) if $self->debuglevel(); } else { ::print_log("[Insteon::IOLinc] WARN Invalid Momentary Time of $momentary_time " . diff --git a/lib/Insteon/Irrigation.pm b/lib/Insteon/Irrigation.pm index 50633d7a9..32f7d34bb 100755 --- a/lib/Insteon/Irrigation.pm +++ b/lib/Insteon/Irrigation.pm @@ -128,7 +128,7 @@ sub set_valve { } unless ($cmd and $subcmd) { &::print_log("Insteon::Irrigation] ERROR: You must specify a valve number and a valid state (ON or OFF)") - if $main::Debug{insteon}; + if $self->debuglevel(); return; } my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); @@ -154,7 +154,7 @@ sub set_program { } unless ($cmd and $subcmd) { &::print_log("Insteon::Irrigation] ERROR: You must specify a program number and a valid state (ON or OFF)") - if $main::Debug{insteon}; + if $self->debuglevel(); return; } my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); @@ -257,7 +257,7 @@ sub _is_info_request { or $cmd eq 'sprinkler_program_off') { $is_info_request = 1; my $val = hex($msg{extra}); - &::print_log("[Insteon::Irrigation] Processing data for $cmd with value: $val") if $main::Debug{insteon}; + &::print_log("[Insteon::Irrigation] Processing data for $cmd with value: $val") if $self->debuglevel(); $$self{'active_valve_id'} = ($val & 7) + 1; $$self{'active_program_number'} = (($val >> 3) & 3) + 1; $$self{'program_is_running'} = ($val >> 5) & 1; @@ -265,7 +265,7 @@ sub _is_info_request { $$self{'valve_is_running'} = ($val >> 7) & 1; &::print_log("[Insteon::Irrigation] active_valve_id: $$self{'active_valve_id'}," . " valve_is_running: $$self{'valve_is_running'}, active_program: $$self{'active_program_number'}," - . " program_is_running: $$self{'program_is_running'}, pump_enabled: $$self{'pump_enabled'}") if $main::Debug{insteon}; + . " program_is_running: $$self{'program_is_running'}, pump_enabled: $$self{'pump_enabled'}") if $self->debuglevel(); } else { #Check if this was a generic info_request diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 561a7cdc8..7f796c4ec 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -1128,7 +1128,7 @@ sub set my $message = new Insteon::InsteonMessage('insteon_ext_send', $parent, 'on', $extra); $parent->_send_cmd($message); ::print_log("[Insteon::FanLinc] " . $self->get_object_name() . "::set($p_state, $setby_name)") - if $main::Debug{insteon}; + if $self->debuglevel(); $self->is_acknowledged(0); $$self{pending_state} = $p_state; $$self{pending_setby} = $p_setby; @@ -1182,7 +1182,7 @@ sub _is_info_request my $child_state = &Insteon::BaseObject::derive_link_state(hex($msg{extra})); &::print_log("[Insteon::FanLinc] received status for " . $child_obj->{object_name} . " of: $child_state " - . "hops left: $msg{hopsleft}") if $main::Debug{insteon}; + . "hops left: $msg{hopsleft}") if $self->debuglevel(); $ack_setby = $$child_obj{m_status_request_pending} if ref $$child_obj{m_status_request_pending}; $child_obj->SUPER::set($child_state, $ack_setby); delete($$parent{child_status_request_pending}); @@ -1213,7 +1213,7 @@ sub is_acknowledged $$child_obj{pending_setby} = undef; $$child_obj{pending_response} = undef; $$parent{child_pending_state} = undef; - &::print_log("[Insteon::FanLinc] received command/state acknowledge from " . $child_obj->{object_name}) if $main::Debug{insteon}; + &::print_log("[Insteon::FanLinc] received command/state acknowledge from " . $child_obj->{object_name}) if $self->debuglevel(); return $$self{is_acknowledged}; } else { return $self->SUPER::is_acknowledged($p_ack); diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index fea11d116..296791fc8 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -135,6 +135,23 @@ sub send_attempts } return $$self{send_attempts}; } + +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($self, $debug_level) = @_; + $debug_level = 1 unless $debug_level; + my $objname = lc $self->get_object_name; + &::print_log("debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; + return 1 if $main::Debug{insteon} >= $debug_level; + return 1 if $main::Debug{$objname} >= $debug_level; + return 0; +} =item C diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index fe97e4014..88bf1ff59 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -405,7 +405,7 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::MotionSensor] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::MotionSensor] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); if ($$self{_ext_set_get_action} eq 'set'){ if (defined($$root{_set_bit_action})){ ::print_log("[Insteon::MotionSensor] Set of ". @@ -413,7 +413,7 @@ sub _process_message { $root->get_object_name); $$root{_set_bit_action} = undef; } else { - main::print_log("[Insteon::MotionSensor] Clearing active message") if $main::Debug{insteon}; + main::print_log("[Insteon::MotionSensor] Clearing active message") if $self->debuglevel(); } $clear_message = 1; $$self{_ext_set_get_action} = undef; @@ -468,7 +468,7 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::MotionSensor] WARN: Unknown Extended " - ."Set/Get Data Message Received for ". $self->get_object_name) if $main::Debug{insteon}; + ."Set/Get Data Message Received for ". $self->get_object_name) if $self->debuglevel(); } } else { @@ -798,7 +798,7 @@ sub set my $setby_name = $p_setby; $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); &::print_log("[Insteon::TriggerLinc] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name)") if $main::Debug{insteon}; + . "::set_receive($p_state, $setby_name)") if $self->debuglevel(); $self->set_receive($p_state,$p_setby); return; } @@ -864,9 +864,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::TriggerLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::TriggerLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::TriggerLinc] Clearing active message") if $main::Debug{insteon}; + main::print_log("[Insteon::TriggerLinc] Clearing active message") if $self->debuglevel(); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -884,7 +884,7 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::TriggerLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); } } else { diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index f224d0484..bb7c840cc 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -214,7 +214,7 @@ Sets fan to 'on' or 'auto' sub fan{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon::Thermostat] Fan $state") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Fan $state") if $self->debuglevel(); my $fan; if (($state eq 'on') or ($state eq 'fan_on')) { $fan = '07'; @@ -235,7 +235,7 @@ Sets a new cool setpoint. =cut sub cool_setpoint{ my ($self, $temp) = @_; - main::print_log("[Insteon::Thermostat] Cool setpoint -> $temp") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Cool setpoint -> $temp") if $self->debuglevel(); if($temp !~ /^\d+$/){ main::print_log("[Insteon::Thermostat] ERROR: cool_setpoint $temp not numeric"); return; @@ -249,7 +249,7 @@ Sets a new heat setpoint. =cut sub heat_setpoint{ my ($self, $temp) = @_; - main::print_log("[Insteon::Thermostat] Heat setpoint -> $temp") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Heat setpoint -> $temp") if $self->debuglevel(); if($temp !~ /^\d+$/){ main::print_log("[Insteon::Thermostat] ERROR: heat_setpoint $temp not numeric"); return; @@ -373,7 +373,7 @@ sub _is_info_request { my $is_info_request = ($cmd eq 'thermostat_get_zone_info') ? 1 : 0; if ($is_info_request) { my $val = $msg{extra}; - main::print_log("[Insteon::Thermostat] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(); if ($$self{_zone_action} eq "temp") { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { @@ -419,21 +419,21 @@ sub _process_message elsif ($msg{command} eq "thermostat_setpoint_cool" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of cool setpoint ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + "for ". $self->get_object_name) if $self->debuglevel(); $self->_cool_sp((hex($msg{extra})/2)); $clear_message = 1; } elsif ($msg{command} eq "thermostat_setpoint_heat" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of heat setpoint ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + "for ". $self->get_object_name) if $self->debuglevel(); $self->_heat_sp((hex($msg{extra})/2)); $clear_message = 1; } elsif ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); # we got our cool setpoint in auto mode - main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $self->debuglevel(); my $val = (hex $msg{extra})/2; $self->_cool_sp($val); $$self{m_setpoint_pending} = 0; @@ -497,7 +497,7 @@ Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', sub mode{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon::Thermostat] Mode $state") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Mode $state") if $self->debuglevel(); my $mode; if ($state eq 'off') { $mode = "09"; @@ -526,7 +526,7 @@ sub _is_info_request { my $is_info_request; if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { my $val = $msg{extra}; - main::print_log("[Insteon::Thermo_i1] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i1] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(); if ($val eq '00') { $self->_mode('off'); } elsif ($val eq '01') { @@ -733,16 +733,16 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get ACK Received for " . $self->get_object_name) if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::Thermo_i2CS] Clearing active message") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Clearing active message") if $self->debuglevel(); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); } elsif ($$self{_ext_set_get_action} eq 'set_high_humid'){ main::print_log("[Insteon::Thermostat] Received ACK of high humid setpoint ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + "for ". $self->get_object_name) if $self->debuglevel(); $self->_high_humid_sp($$self{_high_humid_pending}); $clear_message = 1; $$self{_ext_set_get_action} = undef; @@ -751,7 +751,7 @@ sub _process_message { } elsif ($$self{_ext_set_get_action} eq 'set_low_humid'){ main::print_log("[Insteon::Thermostat] Received ACK of low humid setpoint ". - "for ". $self->get_object_name) if $main::Debug{insteon}; + "for ". $self->get_object_name) if $self->debuglevel(); $self->_low_humid_sp($$self{_low_humid_pending}); $clear_message = 1; $$self{_ext_set_get_action} = undef; @@ -763,7 +763,7 @@ sub _process_message { if (substr($msg{extra},0,4) eq "0201") { $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get Data ". - "Received for ". $self->get_object_name) if $main::Debug{insteon}; + "Received for ". $self->get_object_name) if $self->debuglevel(); #0 = 2 #14 = Cool SP #2 = 1 #16 = humidity #3 = day #18 = temp in Celsius High byte @@ -835,37 +835,37 @@ sub _process_message { } else { main::print_log("[Insteon::Thermo_i2CS] WARN: Unknown Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $main::Debug{insteon}; + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); } } elsif ($msg{command} eq "status_temp" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Temp Change Message ". - "from ". $self->get_object_name) if $main::Debug{insteon}; + "from ". $self->get_object_name) if $self->debuglevel(); $self->hex_short_temp($msg{extra}); } elsif ($msg{command} eq "status_mode" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Mode Change Message ". - "from ". $self->get_object_name) if $main::Debug{insteon}; + "from ". $self->get_object_name) if $self->debuglevel(); $self->status_mode($msg{extra}); } elsif ($msg{command} eq "status_cool" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Cool Setpoint Change Message ". - "from ". $self->get_object_name) if $main::Debug{insteon}; + "from ". $self->get_object_name) if $self->debuglevel(); $self->hex_cool($msg{extra}); } elsif ($msg{command} eq "status_humid" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Humidity Change Message ". - "from ". $self->get_object_name) if $main::Debug{insteon}; + "from ". $self->get_object_name) if $self->debuglevel(); $self->hex_humid($msg{extra}); } elsif ($msg{command} eq "status_heat" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Heat Setpoint Change Message ". - "from ". $self->get_object_name) if $main::Debug{insteon}; + "from ". $self->get_object_name) if $self->debuglevel(); $self->hex_heat($msg{extra}); } else { @@ -879,7 +879,7 @@ sub _is_info_request { my $is_info_request; if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { my $val = $msg{extra}; - main::print_log("[Insteon::Thermo_i2CS] Processing is_info_request for $cmd with value: $val") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(); if ($val eq '09') { $self->_mode('Off'); } elsif ($val eq '04') { @@ -1072,7 +1072,7 @@ Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', sub mode{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon::Thermostat] Mode $state") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermostat] Mode $state") if $self->debuglevel(); my $mode; if ($state eq 'off') { $mode = "09"; @@ -1120,7 +1120,7 @@ Sets the high humidity setpoint. =cut sub high_humid_setpoint { my ($self, $value) = @_; - main::print_log("[Insteon::Thermo_i2CS] Setting high humid setpoint -> $value") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Setting high humid setpoint -> $value") if $self->debuglevel(); if($value !~ /^\d+$/){ main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint $value not numeric"); return; @@ -1144,7 +1144,7 @@ Sets the low humidity setpoint. =cut sub low_humid_setpoint { my ($self, $value) = @_; - main::print_log("[Insteon::Thermo_i2CS] Setting low humid setpoint -> $value") if $main::Debug{insteon}; + main::print_log("[Insteon::Thermo_i2CS] Setting low humid setpoint -> $value") if $self->debuglevel(); if($value !~ /^\d+$/){ main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint $value not numeric"); return; diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 9181f3d85..019451663 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -218,7 +218,7 @@ sub check_for_data { } else { - &::print_log("[Insteon_PLM] DEBUG2: PLM command timer expired but no transmission in place. Moving on...") if $main::Debug{insteon} >= 2; + &::print_log("[Insteon_PLM] DEBUG2: PLM command timer expired but no transmission in place. Moving on...") if $self->debuglevel(2); $self->clear_active_message(); $self->process_queue(); } @@ -390,7 +390,7 @@ sub _send_cmd { my $incurred_delay_time = $message->seconds_delayed; &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " . sprintf('%.2f',$incurred_delay_time) . " seconds; starting hop-count: " - . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $main::Debug{insteon} >= 2; + . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $self->debuglevel(2); if ($message->isa('Insteon::X10Message')) { # is x10; so, be slow $command = $prefix{x10_send} . $command; @@ -412,8 +412,8 @@ sub _send_cmd { } else { - &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $main::Debug{insteon} >= 3; - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $self->debuglevel(3); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $self->debuglevel(4); my $data = pack("H*",$command); $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; @@ -442,7 +442,7 @@ sub _parse_data { # it is possible that a fragment exists from a previous attempt; so, if it exists, prepend it if ($$self{_data_fragment}) { - &::print_log("[Insteon_PLM] DEBUG3: Prepending prior data fragment: $$self{_data_fragment}") if $main::Debug{insteon} >= 3; + &::print_log("[Insteon_PLM] DEBUG3: Prepending prior data fragment: $$self{_data_fragment}") if $self->debuglevel(3); # maintain a copy of the parsed data fragment $$self{_prior_data_fragment} = $$self{_data_fragment}; # append if not a repeat @@ -456,7 +456,7 @@ sub _parse_data { $$self{_prior_data_fragment} = ''; } - &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $main::Debug{insteon} >= 3; + &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $self->debuglevel(3); # begin by pulling out any PLM ack/nacks my $prev_cmd = ''; @@ -492,7 +492,7 @@ sub _parse_data { $entered_ack_loop = 1; if ($parsed_data =~ /^($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($prefix{all_link_first_rec}15)|($prefix{all_link_next_rec}15)|($badcmd)$/) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); my $ret_code = substr($parsed_data,length($parsed_data)-2,2); my $record_type = substr($parsed_data,0,4); my $message_data = substr($parsed_data,4,length($parsed_data)-4); @@ -515,7 +515,7 @@ sub _parse_data { package main; eval ($self->active_message->success_callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon_PLM; } # clear the active message because we're done @@ -524,7 +524,7 @@ sub _parse_data { else { &::print_log("[Insteon_PLM] DEBUG3: Received PLM acknowledge: " - . $pending_message->to_string) if $main::Debug{insteon} >= 3; + . $pending_message->to_string) if $self->debuglevel(3); } # X10 messages don't ACK back on the powerline, so clear them if the PLM acknowledges @@ -552,7 +552,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon_PLM; } } @@ -579,7 +579,7 @@ sub _parse_data { $self->_aldb->scandatetime(&main::get_tickcount); &::print_log("[Insteon_PLM] " . $self->get_object_name . " completed link memory scan: status: " . $self->_aldb->health()) - if $main::Debug{insteon}; + if $self->debuglevel(); if ($$self{_mem_callback}) { my $callback = $$self{_mem_callback}; @@ -587,7 +587,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during nack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon_PLM; } } @@ -637,7 +637,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $main::Debug{insteon} >= 1; + if $@ and $self->debuglevel(1); package Insteon_PLM; } # clear the active message because we're done @@ -665,7 +665,7 @@ sub _parse_data { # is $parsed_data an accidental anomoly? (there are other cases; but, this is a good start) if ($parsed_data =~ /^($prefix{insteon_send}\w{12}06)|($prefix{insteon_send}\w{12}15)$/) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); # first, parse the content to confirm that it could be a legitimate ACK my $unknown_deviceid = substr($parsed_data,4,6); my $unknown_msg_flags = substr($parsed_data,10,2); @@ -710,10 +710,10 @@ sub _parse_data { { #ignore blanks.. the split does odd things next if $parsed_data eq ''; - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $main::Debug{insteon} >= 4; + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); if ($previous_parsed_data eq $parsed_data){ # guard against repeats - ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $main::Debug{insteon} >= 3; + ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $self->debuglevel(3); next; } $previous_parsed_data = $parsed_data; # and, now reinitialize @@ -737,16 +737,16 @@ sub _parse_data { { #X10 Received my $x10_message = new Insteon::X10Message($parsed_data); my $x10_data = $x10_message->get_formatted_data(); - &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $main::Debug{insteon} >= 3; + &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $self->debuglevel(3); &::process_serial_data($x10_data,undef,$self); } elsif ($parsed_prefix eq $prefix{all_link_complete} and ($message_length == 20)) { #ALL-Linking Completed my $link_address = substr($message_data,4,6); - &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $main::Debug{insteon} >= 2; + &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $self->debuglevel(2); if ($self->active_message->success_callback){ main::print_log("[Insteon::Insteon_PLM] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $main::Debug{insteon} >= 4; + . $self->active_message->success_callback) if $self->debuglevel(4); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::Insteon_PLM] problem w/ success callback: $@") if $@; @@ -765,7 +765,7 @@ sub _parse_data { my $failure_device = substr($message_data,2,6); &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure from device: " - . "$failure_device and group: $failure_group") if $main::Debug{insteon} >= 2; + . "$failure_device and group: $failure_group") if $self->debuglevel(2); my $failed_object = &Insteon::get_object($failure_device,'01'); if (ref $failed_object){ @@ -779,13 +779,13 @@ sub _parse_data { } } else { &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure." - . " But there is no pending message.") if $main::Debug{insteon} >= 2; + . " But there is no pending message.") if $self->debuglevel(2); } } elsif ($parsed_prefix eq $prefix{all_link_record} and ($message_length == 20)) { #ALL-Link Record Response - &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $main::Debug{insteon} >= 2; + &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $self->debuglevel(2); $self->_aldb->parse_alllink($message_data); # before doing the next, make sure that the pending command # (if it sitll exists) is pulled from the queue @@ -804,7 +804,7 @@ sub _parse_data { { &::print_log("[Insteon_PLM] WARN1: All-link cleanup failure for scene: " . $self->active_message->setby->get_object_name . ". Retrying in 1 second.") - if $main::Debug{insteon} >= 1; + if $self->debuglevel(1); $self->retry_active_message(); # except that we should cause a bit of a delay to let things settle out $self->_set_timeout('xmit', 1000); @@ -814,7 +814,7 @@ sub _parse_data { { my $message_to_string = ($self->active_message) ? $self->active_message->to_string() : ""; &::print_log("[Insteon_PLM] Received all-link cleanup success: $message_to_string") - if $main::Debug{insteon}; + if $self->debuglevel(); if (ref $self->active_message && ref $self->active_message->setby){ my $object = $self->active_message->setby; $object->is_acknowledged(1); @@ -831,14 +831,14 @@ sub _parse_data { if ($self->active_message){ my $nack_delay = ($::config_parms{Insteon_PLM_disable_throttling}) ? 0.3 : 1.0; &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy. Resending command" - . " after delaying for $nack_delay second") if $main::Debug{insteon} >= 3; + . " after delaying for $nack_delay second") if $self->debuglevel(3); $self->_set_timeout('xmit',$nack_delay * 1000); $self->active_message->no_hop_increase(1); $self->retry_active_message(); $process_next_command = 0; } else { &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy." - . " No message to resend.") if $main::Debug{insteon} >= 3; + . " No message to resend.") if $self->debuglevel(3); } $nack_count++; } @@ -847,7 +847,7 @@ sub _parse_data { if ($parsed_data ne ''){ $$self{_data_fragment} .= $parsed_data; ::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " - . $parsed_data) if( $main::Debug{insteon} >= 3); + . $parsed_data) if( $self->debuglevel(3)); } } else @@ -857,7 +857,7 @@ sub _parse_data { unless (($parsed_data eq $$self{_prior_data_fragment}) or ($parsed_data eq $$self{_data_fragment})) { $$self{_data_fragment} .= $parsed_data; main::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " - . $parsed_data) if( $main::Debug{insteon} >= 3); + . $parsed_data) if( $self->debuglevel(3)); } } } @@ -865,7 +865,7 @@ sub _parse_data { unless( $entered_rcv_loop or $$self{_data_fragment}) { $$self{_data_fragment} = $residue_data; main::print_log("[Insteon_PLM] DEBUG3: Saving residue data fragment: " - . $residue_data) if( $residue_data and $main::Debug{insteon} >= 3); + . $residue_data) if( $residue_data and $self->debuglevel(3)); } if ($process_next_command) { From f3e5deabe951b4524c6fbdbe174fccafee95fdf3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 22:24:00 -0700 Subject: [PATCH 255/330] Insteon: Fix Typos and Bugs in New Sync_Links --- lib/Insteon/BaseInsteon.pm | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 1bfaa1291..1e936176a 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2854,20 +2854,20 @@ sub sync_links $self->_process_sync_queue() unless $insteon_object; # 1. Does a controller link exist for Device-> PLM - if (!($self->isa('Insteon::InterfaceController') && - $insteon_object->has_link($self->interface,$self->group,1,$subaddress))) { + if ((!$self->isa('Insteon::InterfaceController') && + !$insteon_object->has_link($self->interface,$self->group,1,$subaddress))) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", data3 => $subaddress); - $link_req{cause} = "Adding controller record from $self_link_name to $interface_name"; + $link_req{cause} = "Adding controller record to $self_link_name for $interface_name"; $link_req{data3} = $self->group; push @{$$self{sync_queue}}, \%link_req; } # 2. Does a responder link exist on the PLM - if (!($self->isa('Insteon::InterfaceController') && - $self->interface->has_link($insteon_object,$self->group,0,'00'))) { + if ((!$self->isa('Insteon::InterfaceController') && + !$self->interface->has_link($insteon_object,$self->group,0,'00'))) { my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, group => $self->group, is_controller => 0, callback => "$self_link_name->_process_sync_queue()", @@ -2876,11 +2876,6 @@ sub sync_links push @{$$self{sync_queue}}, \%link_req; } - if (!$$self{members}) { - #No members to sync - $self->_process_sync_queue(); - } - # Loop members foreach my $member_ref (keys %{$$self{members}}) { my $member = $$self{members}{$member_ref}{object}; @@ -2915,6 +2910,7 @@ sub sync_links on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, callback => "$self_link_name->_process_sync_queue()", data3 => $member->group); + $link_req{cause} = "Adding responder record to $member_name from $self_link_name"; push @{$$self{sync_queue}}, \%link_req; $has_link = 0; } @@ -2931,7 +2927,7 @@ sub sync_links $requires_update = 1; $cause .= "Ramp rate "; } - elsif ($cur_on_level != $tgt_on_level){ + elsif ($cur_on_level-1 > $tgt_on_level && $cur_on_level+1 < $tgt_on_level){ $requires_update = 1; $cause .= "On level "; } @@ -2986,11 +2982,13 @@ sub sync_links foreach (@{$$self{sync_queue}}){ my %sync_req = %{$_}; my $audit_text = "(AUDIT)" if ($audit_mode); - my $log_text = "[Insteon::BaseController] $audit_text $sync_req{cmd}ing the following link on $self_link_name because "; + my $log_text = "[Insteon::BaseController] $audit_text "; $log_text .= $sync_req{cause} . "\n"; PRINT: for (keys %sync_req) { next PRINT if ($_ eq 'cause'); next PRINT if ($_ eq 'callback'); + next PRINT if ($_ eq 'member'); + next PRINT if ($_ eq 'object'); $log_text .= "$_ = $sync_req{$_}; "; } ::print_log($log_text); From 7b7becdf476712ab976529ad9be95f6403d1980c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 22:25:55 -0700 Subject: [PATCH 256/330] Insteon: Fix Bug in Call to Is_Deaf in Sync_Links --- lib/Insteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index ce028a87e..84fabc82b 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -510,7 +510,7 @@ sub sync_all_links # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links for my $obj (&Insteon::find_members('Insteon::BaseController')) { - if ($obj->is_deaf) + if (!$obj->isa('Insteon::InterfaceController') && $obj->is_deaf) { &main::print_log("[Sync all links] Ignoring links from 'deaf' device: " . $obj->get_object_name); } From 12fefacaf71040ac96c8897db67d628452084a34 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 22:42:00 -0700 Subject: [PATCH 257/330] Insteon: Add Function to Calculate ALDB Key It is a simple function, but this calculation is repeated all over the place. Unifying the calculation ensures that it is always consistent. Plus, if changes need to be made in the future, they can be more easily implemented. --- lib/Insteon/AllLinkDatabase.pm | 183 ++++++++++++--------------------- lib/Insteon/BaseInsteon.pm | 8 +- 2 files changed, 69 insertions(+), 122 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 58d51133f..af93ac7d1 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -80,6 +80,26 @@ sub health return $$self{health}; } +=item C + +Used to track the health of MisterHouse's copy of a device's ALDB. + +If provided, saves status to memory. + +Returns the saved health status. + +=cut + +sub get_linkkey { + my ($self, $deviceid, $group, $is_controller, $data3) = @_; + my $linkkey = $deviceid . $group . $is_controller; + # '00' and '01' are generally interchangable for $data3 values and are + # the most common values. So to make searching easier we only + # add data3 if it is unique + $linkkey .= $data3 if ($data3 ne '00' and $data3 ne '01'); + return lc $linkkey; +} + =item C Used to track the time, in unix time seconds, of the last ALDB scan. @@ -266,11 +286,8 @@ sub restore_aldb @{$$self{aldb}{duplicates}} = @aldb_duplicates; } elsif (scalar %aldb_record) { next unless $deviceid; - my $aldbkey = $deviceid . $groupid . $is_controller; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - if ($subaddress ne '00' and $subaddress ne '01') { - $aldbkey .= $subaddress; - } + my $aldbkey = $self->get_linkkey($deviceid, $groupid, + $is_controller, $subaddress); %{$$self{aldb}{$aldbkey}} = %aldb_record; } } @@ -361,12 +378,7 @@ sub delete_link my $is_controller = ($link_parms{is_controller}) ? 1 : 0; my $subaddress = ($link_parms{data3}) ? $link_parms{data3} : '00'; # get the address via lookup into the hash - my $key = lc $deviceid . $groupid . $is_controller; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - if ($subaddress ne '00' and $subaddress ne '01') - { - $key .= $subaddress; - } + my $key = $self->get_linkkey($deviceid, $groupid, $is_controller, $subaddress); my $address = $$self{aldb}{$key}{address}; if ($address) { @@ -821,13 +833,7 @@ sub add_link $data3_default = '01'; } my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; - # get the address via lookup into the hash - my $key = lc $device_id . $group . $is_controller; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - if (!($data3 eq '00' or $data3 eq '01')) - { - $key .= $data3; - } + my $key = $self->get_linkkey($device_id, $group, $is_controller, $data3); if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ ## Check whether ALDB is in sync $self->{callback_parms} = \%link_parms; @@ -850,7 +856,7 @@ sub add_link &::print_log("[Insteon::AllLinkDatabase] WARN: attempt to add link to " . $$self{device}->get_object_name . " that already exists! object=" . $insteon_object->get_object_name - . ", group=$group, is_controller=$is_controller, subaddress=$subaddress"); + . ", group=$group, is_controller=$is_controller, data3=$data3"); if ($link_parms{callback}) { package main; @@ -949,14 +955,7 @@ sub update_link } my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; my $deviceid = $insteon_object->device_id; - my $subaddress = $data3; - # get the address via lookup into the hash - my $key = lc $deviceid . $group . $is_controller; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - if (!($subaddress eq '00' or $subaddress eq '01')) - { - $key .= $subaddress; - } + my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ ## Check whether ALDB is in sync $self->{callback_parms} = \%link_parms; @@ -1142,22 +1141,14 @@ or false if it does not. Generally called as part of C. sub has_link { - my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; - my $key = ""; - if ($insteon_object->isa('Insteon::BaseObject') || $insteon_object->isa('Insteon::BaseInterface')) - { - $key = lc $insteon_object->device_id . $group . $is_controller; - } - elsif ($insteon_object->isa('Insteon::AllLinkDatabase')) - { - $key = lc $$insteon_object{device}->device_id . $group . $is_controller; - } - $subaddress = '00' unless $subaddress; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - if (!($subaddress eq '00' or $subaddress eq '01')) - { - $key .= $subaddress; + my ($self, $insteon_object, $group, $is_controller, $data3) = @_; + my $deviceid; + if ($insteon_object->isa('Insteon::AllLinkDatabase')) { + $deviceid = $$insteon_object{device}->device_id; + } else { + $deviceid = lc $insteon_object->device_id; } + my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); return (defined $$self{aldb}{$key}); } @@ -1289,15 +1280,10 @@ sub _on_poke elsif ($$self{_mem_action} eq 'aldb_data3') { ## update the aldb records w/ the changes that were made - my $aldbkey = $$self{pending_aldb}{deviceid} - . $$self{pending_aldb}{group} - . $$self{pending_aldb}{is_controller}; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - my $subaddress = $$self{pending_aldb}{data3}; - if (($subaddress ne '00') and ($subaddress ne '01')) - { - $aldbkey .= $subaddress; - } + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); $$self{aldb}{$aldbkey}{data1} = $$self{pending_aldb}{data1}; $$self{aldb}{$aldbkey}{data2} = $$self{pending_aldb}{data2}; $$self{aldb}{$aldbkey}{data3} = $$self{pending_aldb}{data3}; @@ -1356,15 +1342,10 @@ sub _on_poke $self->delete_duplicate_link_address($$self{pending_aldb}{address}); if (exists $$self{pending_aldb}{deviceid}) { - my $key = lc $$self{pending_aldb}{deviceid} - . $$self{pending_aldb}{group} - . $$self{pending_aldb}{is_controller}; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - my $subaddress = $$self{pending_aldb}{data3}; - if ($subaddress ne '00' and $subaddress ne '01') - { - $key .= $subaddress; - } + my $key = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); delete $$self{aldb}{$key}; } $self->health("good"); @@ -1656,15 +1637,10 @@ sub _on_peek if ($$self{pending_aldb}{inuse}) { # save pending_aldb and then clear it out - my $aldbkey = lc $$self{pending_aldb}{deviceid} - . $$self{pending_aldb}{group} - . $$self{pending_aldb}{is_controller}; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - my $subaddress = $$self{pending_aldb}{data3}; - if ($subaddress ne '00' and $subaddress ne '01') - { - $aldbkey .= $subaddress; - } + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); # check for duplicates if (exists $$self{aldb}{$aldbkey} && $$self{aldb}{$aldbkey}{inuse}) { @@ -2078,15 +2054,10 @@ sub on_read_write_aldb $$self{pending_aldb}{data3} = lc substr($msg{extra},26,2); # save pending_aldb and then clear it out - my $aldbkey = lc $$self{pending_aldb}{deviceid} - . $$self{pending_aldb}{group} - . $$self{pending_aldb}{is_controller}; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - my $subaddress = $$self{pending_aldb}{data3}; - if ($subaddress ne '00' and $subaddress ne '01') - { - $aldbkey .= $subaddress; - } + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); # check for duplicates if (exists $$self{aldb}{$aldbkey} && $$self{aldb}{$aldbkey}{inuse}) { @@ -2114,15 +2085,10 @@ sub on_read_write_aldb { unless ($$self{_mem_activity} eq 'delete') { ## update the aldb records w/ the changes that were made - my $aldbkey = $$self{pending_aldb}{deviceid} - . $$self{pending_aldb}{group} - . $$self{pending_aldb}{is_controller}; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - my $subaddress = $$self{pending_aldb}{data3}; - if (($subaddress ne '00') and ($subaddress ne '01')) - { - $aldbkey .= $subaddress; - } + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); $$self{aldb}{$aldbkey}{data1} = $$self{pending_aldb}{data1}; $$self{aldb}{$aldbkey}{data2} = $$self{pending_aldb}{data2}; $$self{aldb}{$aldbkey}{data3} = $$self{pending_aldb}{data3}; @@ -2152,15 +2118,10 @@ sub on_read_write_aldb $self->delete_duplicate_link_address($$self{pending_aldb}{address}); if (exists $$self{pending_aldb}{deviceid}) { - my $key = lc $$self{pending_aldb}{deviceid} - . $$self{pending_aldb}{group} - . $$self{pending_aldb}{is_controller}; - # append the device "sub-address" (e.g., a non-root button on a keypadlinc) if it exists - my $subaddress = $$self{pending_aldb}{data3}; - if ($subaddress ne '00' and $subaddress ne '01') - { - $key .= $subaddress; - } + my $key = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); delete $$self{aldb}{$key}; } $self->health("good"); @@ -2444,10 +2405,7 @@ sub restore_linktable $subaddress = $value if ($key eq 'data3'); $link_record{$key} = $value if $key and defined($value); } - if ($subaddress eq '00' || $subaddress eq '01'){ - $subaddress = ''; - } - my $linkkey = $deviceid . $groupid . $is_controller . $subaddress; + my $linkkey = $self->get_linkkey($deviceid, $groupid, $is_controller, $subaddress); %{$$self{aldb}{lc $linkkey}} = %link_record; } } @@ -2512,7 +2470,8 @@ sub parse_alllink $link{data1} = substr($data,10,2); $link{data2} = substr($data,12,2); $link{data3} = substr($data,14,2); - my $key = $link{deviceid} . $link{group} . $link{is_controller}; + my $key = $self->get_linkkey($link{deviceid}, $link{group}, + $link{is_controller}, $link{data3}); %{$$self{aldb}{lc $key}} = %link; } } @@ -2639,10 +2598,7 @@ sub delete_link my $group = $link_parms{group}; my $is_controller = ($link_parms{is_controller}) ? 1 : 0; my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; - if ($subaddress eq '00' || $subaddress eq '01'){ - $subaddress = ''; - } - my $linkkey = lc $deviceid . $group . $is_controller . $subaddress; + my $linkkey = $self->get_linkkey($deviceid, $group, $is_controller, $subaddress); if (defined $$self{aldb}{$linkkey}) { my $cmd = '80' @@ -2712,11 +2668,7 @@ sub add_link } my $is_controller = ($link_parms{is_controller}) ? 1 : 0; my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; - if ($subaddress eq '00' || $subaddress eq '01'){ - $subaddress = ''; - } - # first, confirm that the link does not already exist - my $linkkey = lc $device_id . $group . $is_controller . $subaddress; + my $linkkey = $self->get_linkkey($device_id, $group, $is_controller, $subaddress); if (defined $$self{aldb}{$linkkey}) { &::print_log("[Insteon::ALDB_PLM] WARN: attempt to add link to PLM that already exists! " @@ -2777,14 +2729,9 @@ or false if it does not. Generally called as part of C. sub has_link { - my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; - my $key = lc $insteon_object->device_id . $group . $is_controller; - $subaddress = '00' unless $subaddress; - # append the data3 value (controller = group, responder = 00); - if (!($subaddress eq '00' or $subaddress eq '01')) - { - $key .= $subaddress; - } + my ($self, $insteon_object, $group, $is_controller, $data3) = @_; + my $key = $self->get_linkkey($insteon_object->device_id, + $group, $is_controller, $data3); return (defined $$self{aldb}{$key}); } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 1e936176a..f5ad01eee 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2898,10 +2898,10 @@ sub sync_links $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; $tgt_on_level =~ s/(\d+)%?/$1/; $tgt_ramp_rate =~ s/(\d)s?/$1/; - my $resp_aldbkey = lc $insteon_object->device_id . $self->group . '0'; - if ($member->group ne '01') { - $resp_aldbkey .= $member->group; - } + my $resp_aldbkey = $self->_aldb->get_linkkey($insteon_object->device_id, + $self->group, + '0', + $member->group); # 3. Does the responder link exist if (!$member_root->has_link($insteon_object, $self->group, 0, $member->group)){ From c60ef1868c3018d0edb90692141fec35df68002b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 22:46:00 -0700 Subject: [PATCH 258/330] Insteon: Move _aldb to BaseObject, Define get_root for InterfaceController This way we don't need to treat BaseObjects and InterfaceControlle objects differently. --- lib/Insteon/BaseInsteon.pm | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f5ad01eee..28403edfa 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1100,6 +1100,13 @@ sub get_voice_cmds return \%voice_cmds; } +sub _aldb +{ + my ($self) = @_; + my $root_obj = $self->get_root(); + return $$root_obj{aldb}; +} + =back =head2 INI PARAMETERS @@ -1595,13 +1602,6 @@ sub enter_linking_mode $self->_send_cmd($message); } -sub _aldb -{ - my ($self) = @_; - my $root_obj = $self->get_root(); - return $$root_obj{aldb}; -} - =item C Sets the defined flag on the device. @@ -3476,6 +3476,17 @@ sub is_root return 0; } +=item C + +Returns the root object of a device, in this case the interface. + +=cut + +sub get_root { + my ($self) = @_; + return $self->interface; +} + =item C Returns a hash of voice commands where the key is the voice command name and the From 5c8fd382515ddb33f592bda4152bd4a7272964a7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 23:16:00 -0700 Subject: [PATCH 259/330] Insteon: Only Change Data3 on Responders, Don't Do Processing in _write_link The distinction in the data3 value between i2CS and everything else only applies to repsonder links. Remove any processing from the low level _write_link routine. All processing of the data should occur higher up than this. --- lib/Insteon/AllLinkDatabase.pm | 40 +++++++++++++++------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index af93ac7d1..b07d13d3b 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -826,13 +826,17 @@ sub add_link $device_id = lc $insteon_object->device_id; } my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - # check whether the link already exists - # for I2CS devices the default data3 should be 01 no 00 - my $data3_default = '00'; + + # For I2CS devices the default data3 for responders is 01 + # For all other devices the default data3 for responders is 00 + my $data3_resp_default = '00'; if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { - $data3_default = '01'; + $data3_resp_default = '01'; } - my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; + my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; + $data3 = $data3_resp_default if (!$is_controller && ($data3 eq '00' || $data3 eq '01')); + + # check whether the link already exists my $key = $self->get_linkkey($device_id, $group, $is_controller, $data3); if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ ## Check whether ALDB is in sync @@ -948,12 +952,16 @@ sub update_link . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $main::Debug{insteon}; my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; - # for I2CS devices the default data3 should be 01 no 00 - my $data3_default = '00'; + + # For I2CS devices the default data3 for responders is 01 + # For all other devices the default data3 for responders is 00 + my $data3_resp_default = '00'; if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { - $data3_default = '01'; + $data3_resp_default = '01'; } - my $data3 = ($link_parms{data3}) ? $link_parms{data3} : $data3_default; + my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; + $data3 = $data3_resp_default if (!$is_controller && ($data3 eq '00' || $data3 eq '01')); + my $deviceid = $insteon_object->device_id; my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ @@ -1779,13 +1787,6 @@ sub _write_link $$self{pending_aldb}{is_controller} = $is_controller; $$self{pending_aldb}{data1} = (defined $data1) ? lc $data1 : '00'; $$self{pending_aldb}{data2} = (defined $data2) ? lc $data2 : '00'; - # Note: if device is a KeypadLinc, then $data3 must be assigned the value of the applicable button (01) - if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) - { - &::print_log("[Insteon::ALDB_i1] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $main::Debug{insteon}; - $data3 = $$self{device}->group; - } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; $self->_peek($address); } @@ -2176,13 +2177,6 @@ sub _write_link $message_extra .= $$self{pending_aldb}{data1}; $$self{pending_aldb}{data2} = (defined $data2) ? lc $data2 : '00'; $message_extra .= $$self{pending_aldb}{data2}; - # Note: if device is a KeypadLinc, then $data3 must be assigned the value of the applicable button (01) - if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) - { - &::print_log("[Insteon::ALDB_i2] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $main::Debug{insteon}; - $data3 = $$self{device}->group; - } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; $message_extra .= $$self{pending_aldb}{data3}; $message_extra .= '00'; #byte 14 From 7b32a9dced2c97e0c1a087aa9194a1ce322f21a3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 17 Oct 2013 23:17:00 -0700 Subject: [PATCH 260/330] Insteon: Remove Old Style Data3 Code from Delete_Orphans Ready to start testing the outputted results. --- lib/Insteon/AllLinkDatabase.pm | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index b07d13d3b..aea443c4f 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -470,7 +470,7 @@ sub delete_orphan_links # Initialize Variables my ($linked_device, $plm_scene, $controller_object, $link_defined, $controller_id, $linked_id, $responder_id, $link_data3, - $linked_group, $self_subgroup, $linked_subgroup, $interface_id, + $linked_group, $self_subgroup, $interface_id, $link_subgroup); my $group = lc $$self{aldb}{$linkkey}{group}; my $is_controller = $$self{aldb}{$linkkey}{is_controller}; @@ -548,8 +548,6 @@ sub delete_orphan_links $link_defined = 1; #Identify the linked device's group for use later $linked_group = ($is_controller) ? $member->group : $group; - #Only needed to support OLD method - $linked_subgroup = $member; last MEMBERS; } } @@ -595,15 +593,6 @@ sub delete_orphan_links } # Does a reciprocal link exist? - # Temp OLD compatibility fix for data3, delete to #END to upgrade - if (($linked_device eq Insteon::active_interface()) && - (!$$self{device}->isa('Insteon::KeyPadLincRelay'))){ - $linked_group = '00'; - } - if ($$self{device}->isa('Insteon::KeyPadLincRelay')){ - $linked_group = $linked_subgroup->group; - } - #END if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $linked_group)) { $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " linked_group $linked_group linkkey $linkkey"; push @{$$self{delete_queue}}, \%delete_req; From 15d62f182eb34af726e025011bdb9a7f28372fe8 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Fri, 18 Oct 2013 19:15:42 -0400 Subject: [PATCH 261/330] Move debuglevel sub to Insteon::debuglevel, convert other subs to stubs that call this main sub. Insert more stubs, cleanup and further testing. --- lib/Insteon.pm | 42 ++++++++++++++++++++++++++++++---- lib/Insteon/AllLinkDatabase.pm | 14 +++++++++++- lib/Insteon/BaseInsteon.pm | 9 ++------ lib/Insteon/BaseInterface.pm | 7 +----- lib/Insteon/Message.pm | 17 +++++--------- 5 files changed, 60 insertions(+), 29 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 369b8e225..684b5299f 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -906,6 +906,28 @@ sub init { @_insteon_link = (); } + +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($object, $debug_level) = @_; + $debug_level = 1 unless $debug_level; + my $objname; + #try { + $objname = lc $object->get_object_name if defined $object; + #} catch { + # &::print_log("$object doesn't have a get_object_name function.") if $main::Debug{insteon} >= 2; + #} + &::print_log("Insteon::debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; + return 1 if $main::Debug{insteon} >= $debug_level; + return 1 if defined $objname && $main::Debug{$objname} >= $debug_level; + return 0; +} =item C @@ -1062,7 +1084,7 @@ Walks through every Insteon device and checks the aldb object version for I1 vs. sub check_all_aldb_versions { - main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices") if ($main::Debug{insteon} >= 4); + main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices") if Insteon::debuglevel(undef,4); my @ALDB_devices = (); push @ALDB_devices, Insteon::find_members("Insteon::BaseDevice"); @@ -1085,12 +1107,12 @@ sub check_all_aldb_versions if ($ALDB_device->debuglevel(4)); } } - main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); + main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if Insteon::debuglevel(undef,4); } sub check_thermo_versions { - main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if ($main::Debug{insteon} >= 4); + main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if Insteon::debuglevel(undef,4); my @thermo_devices = (); push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); @@ -1187,7 +1209,7 @@ sub _active_interface my ($self, $interface) = @_; # setup hooks the first time that an interface is made active if (!($$self{active_interface}) and $interface) { - &main::print_log("[Insteon] Setting up initialization hooks") if $main::Debug{insteon}; + &main::print_log("[Insteon] Setting up initialization hooks") if $self->debuglevel(); &main::MainLoop_pre_add_hook(\&Insteon::BaseInterface::check_for_data, 1); &main::Reload_post_add_hook(\&Insteon::check_all_aldb_versions, 1); &main::Reload_post_add_hook(\&Insteon::BaseInterface::poll_all, 1); @@ -1199,6 +1221,18 @@ sub _active_interface $$self{active_interface} = $interface if $interface; return $$self{active_interface}; } + +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($self, $debug_level) = @_; + return Insteon::debuglevel(undef, $debug_level); +} =item C diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index f1f5721d8..f501937a8 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1361,6 +1361,18 @@ sub has_link } return (defined $$self{aldb}{$key}); } + +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($self, $debug_level) = @_; + return Insteon::debuglevel(undef, $debug_level); +} =back @@ -2517,7 +2529,7 @@ sub send_read_aldb $message->failure_callback($$self{_failure_callback}); $self->_send_cmd($message); } - + =back =head2 INI PARAMETERS diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ea31a20e2..5e4118378 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -196,19 +196,14 @@ sub group =item C -Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. +Returns 1 if insteon or this device is at least debug level 'level', otherwise returns 0. =cut sub debuglevel { my ($self, $debug_level) = @_; - $debug_level = 1 unless $debug_level; - my $objname = lc $self->get_object_name; - &::print_log("debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; - return 1 if $main::Debug{insteon} >= $debug_level; - return 1 if $main::Debug{$objname} >= $debug_level; - return 0; + return Insteon::debuglevel($self, $debug_level); } =item C diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 15f7e45b2..5c1f3839e 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -129,12 +129,7 @@ Returns 1 if Insteon or this device is at least debug level 'level', otherwise r sub debuglevel { my ($self, $debug_level) = @_; - $debug_level = 1 unless $debug_level; - my $objname = lc $self->get_object_name; - &::print_log("debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; - return 1 if $main::Debug{insteon} >= $debug_level; - return 1 if $main::Debug{$objname} >= $debug_level; - return 0; + return Insteon::debuglevel($self, $debug_level); } =item C<_is_duplicate(cmd)> diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 296791fc8..61ebe4924 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -145,12 +145,7 @@ Returns 1 if Insteon or this device is at least debug level 'level', otherwise r sub debuglevel { my ($self, $debug_level) = @_; - $debug_level = 1 unless $debug_level; - my $objname = lc $self->get_object_name; - &::print_log("debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; - return 1 if $main::Debug{insteon} >= $debug_level; - return 1 if $main::Debug{$objname} >= $debug_level; - return 0; + return Insteon::debuglevel(undef, $debug_level); } =item C @@ -250,7 +245,7 @@ sub send { &::print_log("[Insteon::BaseMessage] WARN: now resending " . $self->to_string() . " after " . $self->send_attempts - . " attempts.") if $main::Debug{insteon}; + . " attempts.") if $self->debuglevel(); # revise default hop count to reflect retries if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') && !defined($$self{no_hop_increase})) @@ -265,7 +260,7 @@ sub send && $self->setby->isa('Insteon::BaseObject')){ &main::print_log("[Insteon::BaseMessage] Hop count not increased for " . $self->setby->get_object_name . " because no_hop_increase flag was set.") - if $main::Debug{insteon}; + if $self->debuglevel(); $$self{no_hop_increase} = undef; } } @@ -1101,7 +1096,7 @@ sub generate_commands } if ($uc eq undef) { - &main::print_log("[Insteon::Message] Message is for entire HC") if $main::Debug{insteon}; + &main::print_log("[Insteon::Message] Message is for entire HC") if Insteon::debuglevel(undef); } else { @@ -1110,7 +1105,7 @@ sub generate_commands $msg.= substr(unpack("H*",pack("C",$x10_unit_codes{substr($id,2,1)})),1,1); $msg.= "00"; &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $uc) . " as insteon msg: " - . $msg) if $main::Debug{insteon}; + . $msg) if Insteon::debuglevel(undef); push @data, $msg; } @@ -1138,7 +1133,7 @@ sub generate_commands $msg.= "80"; &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $x10_arg) . " as insteon msg: " - . $msg) if $main::Debug{insteon}; + . $msg) if Insteon::debuglevel(undef); push @data, $msg; From f4a01b32ae0031e07f8eff53960f01f936f95a2b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:13:37 -0700 Subject: [PATCH 262/330] Insteon: Dont Use Data3 in PLM Linkkey Data3 on controller links on the PLM should be set to the group. The old code set this to 0 or the responding device's button. It doesn't appear that this error had any major effect. So rather than replace all of these links, simply ignore the data3 value in the linkkey so that old links will be accepted by sync and delete links. The Data3 value of the responder links on the PLM should always be 00. Therefore no concern that a different data3 value makes a link on the PLM unique. --- lib/Insteon/AllLinkDatabase.pm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index aea443c4f..0819d4ffc 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -93,6 +93,12 @@ Returns the saved health status. sub get_linkkey { my ($self, $deviceid, $group, $is_controller, $data3) = @_; my $linkkey = $deviceid . $group . $is_controller; + # Data3 is irrelevant for the PLM itself, b/c for controller records + # data3 will always be equal to group. And for responder records it + # should always be 00, therefor blank data3 so it isn't used + if ($$self{device}->isa('Insteon_PLM')){ + $data3 = '00'; + } # '00' and '01' are generally interchangable for $data3 values and are # the most common values. So to make searching easier we only # add data3 if it is unique From d74e4d2939dd72b6deba8380647599aab81bf72b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:17:15 -0700 Subject: [PATCH 263/330] Insteon: Fix Comments in Link to Interface to be more clear --- lib/Insteon/BaseInsteon.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 28403edfa..d1afc62e0 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1407,7 +1407,7 @@ sub link_to_interface $failure_callback = $self->get_object_name."->link_to_interface_i2cs(\"$p_group\",\"$p_data3\")"; $self->get_engine_version($success_callback, $failure_callback); } - case (1) { #Add Link from object->PLM + case (1) { #Add Controller Link to device for object->PLM link $success_callback = $success_callback_prefix . "\"2\")"; my %link_info = ( object => $self->interface, group => $p_group, is_controller => 1, callback => "$success_callback", failure_callback=> "$failure_callback"); @@ -1421,7 +1421,7 @@ sub link_to_interface ", does not have an ALDB object. Linking is not permitted."); } } - case (2){ #Add Link from PLM->object + case (2){ #Add Responder Link to PLM for object->PLM link $success_callback = $success_callback_prefix . "\"3\")"; my $link_info = "deviceid=" . lc $self->device_id . " group=$p_group is_controller=0 " . "callback=$success_callback failure_callback=$failure_callback"; From ee2f7741c7c08e35a1d31b7aeb697492690cb9b9 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:18:04 -0700 Subject: [PATCH 264/330] Insteon: ALDB Cache is in the Member Root --- lib/Insteon/BaseInsteon.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index d1afc62e0..2a06cdf5e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2898,7 +2898,7 @@ sub sync_links $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; $tgt_on_level =~ s/(\d+)%?/$1/; $tgt_ramp_rate =~ s/(\d)s?/$1/; - my $resp_aldbkey = $self->_aldb->get_linkkey($insteon_object->device_id, + my $resp_aldbkey = $member_root->_aldb->get_linkkey($insteon_object->device_id, $self->group, '0', $member->group); @@ -2917,7 +2917,7 @@ sub sync_links # 4. Is the responder link accurate if ($member->isa('Insteon::DimmableLight') && $has_link) { - my $member_aldb = $member->_aldb; + my $member_aldb = $member_root->_aldb; my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; my $cur_on_level = hex($data1)/2.55; From 949f7859f6b3ec4755f1b8f16e81732a8e3396f0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:19:00 -0700 Subject: [PATCH 265/330] Insteon: Fix Logic In Sync Links --- lib/Insteon/AllLinkDatabase.pm | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 0819d4ffc..f027002b5 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -444,15 +444,17 @@ sub delete_orphan_links if ($$self{device}->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not scan deaf device: $selfname"); } - elsif ($self->health ne 'empty'){ - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Skipping $selfname, it has no links"); + elsif ($self->health eq 'empty'){ + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Skipping $selfname, because it has no links"); } else { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " . $self->health . ". Please rescan the link table of this device and rerun delete " . "orphans if necessary"); } - $self->_process_delete_queue(); + if (!$$self{device}->isa('Insteon_PLM')){ + $self->_process_delete_queue(); + } return; } From a3388624f51e3ddd17818c5d3661e5761ef5ac5a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:29:39 -0700 Subject: [PATCH 266/330] Insteon: Revise Delete_Orphans Rewrite --- lib/Insteon/AllLinkDatabase.pm | 121 ++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index f027002b5..808ac3a57 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -477,9 +477,9 @@ sub delete_orphan_links # Initialize Variables my ($linked_device, $plm_scene, $controller_object, $link_defined, - $controller_id, $linked_id, $responder_id, $link_data3, - $linked_group, $self_subgroup, $interface_id, - $link_subgroup); + $controller_id, $responder_id, $link_data3, + $recip_data3, $group_object, $interface_id, + $data3_object); my $group = lc $$self{aldb}{$linkkey}{group}; my $is_controller = $$self{aldb}{$linkkey}{is_controller}; my $data3 = lc $$self{aldb}{$linkkey}{data3}; @@ -492,9 +492,9 @@ sub delete_orphan_links $interface_id = lc $$self{device}->interface->device_id; } $linked_device = Insteon::get_object($deviceid,'01'); - $linked_device = $$self{device}->interface if ($deviceid eq $interface_id); - $self_subgroup = Insteon::get_object($self_id, $group) if ($is_controller); - $link_subgroup = Insteon::get_object($deviceid, $group) if (!$is_controller); + $linked_device = Insteon::active_interface() if ($deviceid eq $interface_id); + $group_object = ($is_controller) ? Insteon::get_object($self_id, $group) : Insteon::get_object($deviceid, $group); + $data3_object = Insteon::get_object($self_id, $data3); my %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, @@ -509,35 +509,24 @@ sub delete_orphan_links } # IF link is a PLM Scene, is the PLM Scene defined in MH? - if (($linked_device->isa("Insteon::BaseInterface") and !$is_controller)|| - ($$self{device}->isa("Insteon::BaseInterface") and $is_controller)) { + if (($linked_device->isa("Insteon_PLM") and !$is_controller)|| + ($$self{device}->isa("Insteon_PLM") and $is_controller)) { $plm_scene = &Insteon::get_object('000000', $group); - if ($group eq '01' || $group eq '00') { - my $not_plm_name; - if ($linked_device->isa("Insteon::BaseInterface")){ - $not_plm_name = $selfname; - } else { - $not_plm_name = $linked_device->get_object_name; - } - ::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring link between PLM and $not_plm_name " - ."for group 01 or 00") if $main::Debug{insteon} >= 2; - next LINKKEY; - } - elsif (!ref $plm_scene) { + if (!ref $plm_scene && $group ne '01' && $group ne '00') { $delete_req{cause} = "no plm scene for this group could be found"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; } } - # Is this link defined in MH? 5-Step Process - # First define variables based on type of link - $linked_id = lc $linked_device->device_id; - $controller_id = ($is_controller) ? $self_id : $linked_id; - $responder_id = ($is_controller) ? $linked_id : $self_id; + # Is this link defined in MH? 3-Step Process + # Define variables based on type of link + $controller_id = ($is_controller) ? $self_id : lc $linked_device->device_id; + $responder_id = ($is_controller) ? lc $linked_device->device_id : $self_id; $controller_object = Insteon::get_object($controller_id,$group); $controller_object = $plm_scene if (ref $plm_scene); - # Second, iterate over the controller object members to find the link definition + + # First, iterate over the controller object members to find the link definition MEMBERS: foreach my $member_ref (keys %{$$controller_object{members}}) { my $member = $$controller_object{members}{$member_ref}{object}; if ($member->isa('Light_Item')) { @@ -546,47 +535,66 @@ sub delete_orphan_links } # For resp, D3 = resp group; For cont, D3 = cont group $link_data3 = ($is_controller) ? $group : $member->group; - # 00 and 01 likely only neede for old design OLD delete to END - if ($member->group ne '01') { - $link_data3 = $member->group; - } - #END - if (lc($member->device_id) eq $responder_id && - ($data3 eq '00' or $data3 eq '01' or ($data3 eq $link_data3))) { - $link_defined = 1; - #Identify the linked device's group for use later - $linked_group = ($is_controller) ? $member->group : $group; - last MEMBERS; + if (lc($member->device_id) eq $responder_id) { + if ($data3 eq $link_data3){ + $link_defined = 1; + $recip_data3 = ($is_controller) ? $member->group : $group; + last MEMBERS; + } + elsif (($link_data3 eq '00' || $link_data3 eq '01') && + ($data3 eq '00' || $data3 eq '01' )){ + # Allow for 00 or 01 interchangability + $link_defined = 1; + $recip_data3 = ($is_controller) ? $member->group : $group; + last MEMBERS; + } } } - # Third, is this a controller link from the device to the PLM? - if ($is_controller && $deviceid eq $interface_id && ref $self_subgroup){ - $link_defined = 1; - } - - # Fourth, is this a responder link on the PLM from a device? - if (!$is_controller && $self_id eq $interface_id && ref $link_subgroup){ - $link_defined = 1; - $linked_group = '00'; - $linked_group = $group if ($group ne '01'); + # Second, is this a PLM->Device, Device->PLM link, these are not members + if ($$self{device}->isa("Insteon_PLM") && ($data3 eq '00' || $data3 eq '01')){ + if ($is_controller && ($group eq '00' || $group eq '01')){ + #Valid Controller for PLM->Device link + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " + ."Skipping reciprocal link check for group 00 or 01 link from " + ."$selfname to " . $linked_device->get_object_name); + next LINKKEY; + } + elsif (!$is_controller && ref $group_object){ + #Valid Responder for Device->PLM link + $link_defined = 1; + $recip_data3 = $group; + } + } + elsif($deviceid eq $interface_id && (ref $data3_object || ($data3 eq '00' || $data3 eq '01'))){ + if ($is_controller && ref $group_object){ + #Valid Controller for Device->PLM link + $link_defined = 1; + $recip_data3 = '00'; + } + elsif (!$is_controller && ($group eq '00' || $group eq '01')) { + #Valid Responder for PLM->Device link + $link_defined = 1; + $recip_data3 = '00'; + } + } - # Fifth, delete link if not defined + # Third, delete link if not defined if (!$link_defined){ $delete_req{cause} = "link is not defined in MisterHouse $linkkey"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; } - # Ignore links to deaf devices - if (! $linked_device->isa('Insteon_PLM') && $linked_device->is_deaf) { + # Do not delete links to deaf devices + if (!$linked_device->isa('Insteon_PLM') && $linked_device->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " ."ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); next LINKKEY; } - # Ignore links to unhealthy devices + # Do not delete links to unhealthy devices if ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping check for reciprocal links from " . $linked_device->get_object_name . " because aldb health of that device is " @@ -594,15 +602,16 @@ sub delete_orphan_links next LINKKEY; } - # Ignore Controller Links to the PLM - if ($linked_device->isa("Insteon::BaseInterface") and $is_controller) { - # ignore since this is just a link back to the PLM + # Do not delete responder links from the PLM (prevents locking i2CS devices) + if ($linked_device->isa("Insteon_PLM") and !$is_controller && ($group eq '00' || $group eq '01')) { + ::print_log("[Insteon::AllLinkDatabase] Skipping check for reciprocal link on PLM for responder " + ."link on $selfname for group 00 or 01."); next LINKKEY; } # Does a reciprocal link exist? - if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $linked_group)) { - $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " linked_group $linked_group linkkey $linkkey"; + if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $recip_data3)) { + $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " recip_data3 $recip_data3 linkkey $linkkey"; push @{$$self{delete_queue}}, \%delete_req; } From 3ffbd1a4ad07591d5d262fdf9849ad0e07b2b046 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:30:31 -0700 Subject: [PATCH 267/330] Insteon: Revise Default Data3 Distinction for i2CS/not-i2CS --- lib/Insteon/AllLinkDatabase.pm | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 808ac3a57..cef851bb9 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -833,14 +833,14 @@ sub add_link } my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - # For I2CS devices the default data3 for responders is 01 - # For all other devices the default data3 for responders is 00 - my $data3_resp_default = '00'; + # For I2CS devices the default data3 for links is 01 + # For all other devices the default data3 for links is 00 + my $data3_default = '00'; if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { - $data3_resp_default = '01'; + $data3_default = '01'; } my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; - $data3 = $data3_resp_default if (!$is_controller && ($data3 eq '00' || $data3 eq '01')); + $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); # check whether the link already exists my $key = $self->get_linkkey($device_id, $group, $is_controller, $data3); @@ -959,14 +959,14 @@ sub update_link my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; - # For I2CS devices the default data3 for responders is 01 - # For all other devices the default data3 for responders is 00 - my $data3_resp_default = '00'; + # For I2CS devices the default data3 for links is 01 + # For all other devices the default data3 for links is 00 + my $data3_default = '00'; if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { - $data3_resp_default = '01'; + $data3_default = '01'; } my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; - $data3 = $data3_resp_default if (!$is_controller && ($data3 eq '00' || $data3 eq '01')); + $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); my $deviceid = $insteon_object->device_id; my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); From 96689bbb66b22ea822a1b7bab9d5cb9e3e3a5785 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 14:32:01 -0700 Subject: [PATCH 268/330] Insteon: Add Comments re Oddity in PLM Control Codes Control Codes for Manage All Link Database entries do not appear to match reality. Did not alter the code to address this, as it doesn't appear that d1-3 matter on the PLM and thus there is no reason to update a link. --- lib/Insteon/AllLinkDatabase.pm | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index cef851bb9..641db4914 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2684,6 +2684,15 @@ sub add_link } else { + # The modem developers guide appears to be wrong regarding control + # codes. 40 and 41 will respond with a NACK if a record for that + # group/device/is_controller combination already exist. It appears + # that code 20 can be used to edit existing but not create new records. + # However, since data1-3 are consistent for all PLM links we never + # really need to update a PLM link. NB prior MH code did not set + # data3 on control records to the group, however this does not + # appear to have any adverse effects, and the current MH code will + # not flag these entries as being incorrect or requiring an update my $control_code = ($is_controller) ? '40' : '41'; # flags should be 'a2' for responder and 'e2' for controller my $flags = ($is_controller) ? 'E2' : 'A2'; From 7e123b58e4ecc24e35da922eb9018851a4f9548c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 15:19:21 -0700 Subject: [PATCH 269/330] Insteon: Sync Half Links for Deaf or Unhealthy Devices Will add the responder/controller link to the other device even if the other device is deaf or unhealthy. --- lib/Insteon.pm | 20 ++++---------------- lib/Insteon/BaseInsteon.pm | 31 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 84fabc82b..b976387e4 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -510,22 +510,10 @@ sub sync_all_links # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links for my $obj (&Insteon::find_members('Insteon::BaseController')) { - if (!$obj->isa('Insteon::InterfaceController') && $obj->is_deaf) - { - &main::print_log("[Sync all links] Ignoring links from 'deaf' device: " . $obj->get_object_name); - } - elsif(!($obj->isa('Insteon::InterfaceController')) && ($obj->_aldb->health eq 'unknown')) - { - &main::print_log("[Sync all links] Skipping links from 'unreachable' device: " - . $obj->get_object_name . ". Consider rescanning the link table of this device"); - } - else - { - my %sync_req = ('sync_object' => $obj, 'audit_mode' => ($audit_mode) ? 1 : 0); - &main::print_log("[Sync all links] Adding " . $obj->get_object_name - . " to sync queue"); - push @_sync_devices, \%sync_req - }; + my %sync_req = ('sync_object' => $obj, 'audit_mode' => ($audit_mode) ? 1 : 0); + &main::print_log("[Sync all links] Adding " . $obj->get_object_name + . " to sync queue"); + push @_sync_devices, \%sync_req } $_sync_cnt = scalar @_sync_devices; diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 2a06cdf5e..247a22477 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2845,17 +2845,32 @@ sub sync_links my $interface_object = Insteon::active_interface(); my $interface_name = $interface_object->get_object_name; if (!($self->isa('Insteon::InterfaceController'))) { - $insteon_object = &Insteon::get_object($self->device_id,'01'); - &main::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " + $insteon_object = $self->get_root; + ::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " . "Please double check your items.mht file.") if (!(defined($insteon_object))); } # Abort if $insteon_object doesn't exist $self->_process_sync_queue() unless $insteon_object; + # Warn if device is deaf or ALDB out of sync + if ((!$self->isa('Insteon::InterfaceController') && $insteon_object->is_deaf)) { + ::print_log("[Insteon::BaseController] WARN! $self_link_name is a deaf device, responder links will be added to devices " + ."controlled by this device, but no links will be added to $self_link_name."); + } + elsif (!$self->isa('Insteon::InterfaceController') && + ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty')){ + ::print_log("[Insteon::BaseController] WARN! The ALDB of $self_link_name is ".$insteon_object->_aldb->health + .", links will be added to devices " + ."linked to this device, but no links will be added to $self_link_name. Please rescan this device and attempt " + ."sync links again."); + } + # 1. Does a controller link exist for Device-> PLM if ((!$self->isa('Insteon::InterfaceController') && - !$insteon_object->has_link($self->interface,$self->group,1,$subaddress))) { + !$insteon_object->has_link($self->interface,$self->group,1,$subaddress) && + !$insteon_object->is_deaf && + ($insteon_object->_aldb->health eq 'good' || $insteon_object->_aldb->health eq 'empty'))) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", @@ -2904,7 +2919,8 @@ sub sync_links $member->group); # 3. Does the responder link exist - if (!$member_root->has_link($insteon_object, $self->group, 0, $member->group)){ + if (!$member_root->has_link($insteon_object, $self->group, 0, $member->group) && + ($member_root->_aldb->health eq 'good' || $member_root->_aldb->health eq 'empty')){ my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, group => $self->group, is_controller => 0, on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, @@ -2951,7 +2967,8 @@ sub sync_links $cause .= "Ramp rate "; } } - if ($requires_update) { + if ($requires_update && + ($member_root->_aldb->health eq 'good' || $member_root->_aldb->health eq 'empty')) { my %link_req = ( member => $member, cmd => 'update', object => $insteon_object, group => $self->group, is_controller => 0, on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, @@ -2963,7 +2980,9 @@ sub sync_links } # 5. Does the controller link on this device exist - if (!($insteon_object->has_link($member, $self->group, 1, $subaddress))) { + if (!($insteon_object->has_link($member, $self->group, 1, $subaddress)) && + !$insteon_object->is_deaf && + ($insteon_object->_aldb->health eq 'healthy' || $insteon_object->_aldb->health eq 'empty')) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", From 0b4f76d44892e2bffcc31d5d73a76cbb2617c247 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 15:47:43 -0700 Subject: [PATCH 270/330] Insteon: Simplify Process Delete Queue in PLM --- lib/Insteon/AllLinkDatabase.pm | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 641db4914..2f24e92b7 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2549,19 +2549,12 @@ sub _process_delete_queue { } else { - if ($delete_req{linkdevice} eq $self) - { - &::print_log("[Insteon::ALDB_PLM] now deleting orphaned link w/ details: " - . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") - . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name - : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") - if $main::Debug{insteon}; - $self->delete_link(%delete_req); - } - elsif ($delete_req{linkdevice}) - { - $delete_req{linkdevice}->delete_link(%delete_req); - } + &::print_log("[Insteon::ALDB_PLM] now deleting orphaned link w/ details: " + . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") + . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name + : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") + if $main::Debug{insteon}; + $self->delete_link(%delete_req); } } else From ea8b40f15714a69ed133cfced8da370d8fcf996d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 16:15:53 -0700 Subject: [PATCH 271/330] Insteon: Make PLM Log Links Output Easier to Read --- lib/Insteon/AllLinkDatabase.pm | 48 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 2f24e92b7..acc815f09 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2424,29 +2424,39 @@ sub log_alllink_table my ($self) = @_; &::print_log("[Insteon::ALDB_PLM] Link table health: " . $self->health); foreach my $linkkey (sort(keys(%{$$self{aldb}}))) { - my $data3 = $$self{aldb}{$linkkey}{data3}; my $is_controller = $$self{aldb}{$linkkey}{is_controller}; - my $group = ($is_controller) ? $data3 : $$self{aldb}{$linkkey}{group}; + my $group = $$self{aldb}{$linkkey}{group}; $group = '01' if $group eq '00'; - my $deviceid = $$self{aldb}{$linkkey}{deviceid}; - my $device = &Insteon::get_object($deviceid,$group); - my $object_name = ''; - if ($device) - { - $object_name = $device->get_object_name; - } - else - { - $object_name = uc substr($deviceid,0,2) . '.' . - uc substr($deviceid,2,2) . '.' . - uc substr($deviceid,4,2); - } + my $deviceid = $$self{aldb}{$linkkey}{deviceid}; + my $linked_subgroup = '01'; + my $controller_device; + my $controller_name; + if (!$is_controller){ + $linked_subgroup = $group; + } + elsif ($group ne '00' && $group ne '01') { + $controller_device = Insteon::get_object('000000',$group); + $controller_name = $controller_device->get_object_name . " ($group)"; + } + else { + $controller_name = $group; + } + my $linked_object = Insteon::get_object($deviceid,$linked_subgroup); + my $linked_name = ''; + if ($linked_object) { + $linked_name = $linked_object->get_object_name; + } + else { + $linked_name = uc substr($deviceid,0,2) . '.' . + uc substr($deviceid,2,2) . '.' . + uc substr($deviceid,4,2); + } &::print_log("[Insteon::ALDB_PLM] " . - (($is_controller) ? "cntlr($$self{aldb}{$linkkey}{group}) record to " - . $object_name - : "responder record to " . $object_name . "($$self{aldb}{$linkkey}{group})") + (($is_controller) ? "cntlr($controller_name) record to " + . $linked_name + : "responder record to " . $linked_name . "($$self{aldb}{$linkkey}{group})") . " (d1=$$self{aldb}{$linkkey}{data1}, d2=$$self{aldb}{$linkkey}{data2}, " - . "d3=$data3)"); + . "d3=$$self{aldb}{$linkkey}{data3})"); } } From 7902e5708e843411cdad7b51a10d18e28a10b7a3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 17:42:43 -0700 Subject: [PATCH 272/330] Insteon: Place Skip Messages into Delete_Req This way the print in the proper order with everything else. --- lib/Insteon/AllLinkDatabase.pm | 89 ++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index acc815f09..6c14e711b 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -440,9 +440,9 @@ sub delete_orphan_links my $selfname = $$self{device}->get_object_name; # first, make sure that the health of ALDB is ok - if ($self->health ne 'good') { - if ($$self{device}->is_deaf) { - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not scan deaf device: $selfname"); + if ($self->health ne 'good' || (!$$self{device}->isa('Insteon_PLM') && $$self{device}->is_deaf)) { + if (!$$self{device}->isa('Insteon_PLM') && $$self{device}->is_deaf) { + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not delete links on deaf device: $selfname"); } elsif ($self->health eq 'empty'){ ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Skipping $selfname, because it has no links"); @@ -463,13 +463,14 @@ sub delete_orphan_links # Skip empty addresses next LINKKEY if ($linkkey eq 'empty'); + # Define delete request + my %delete_req = (callback => "$selfname->_aldb->_process_delete_queue()",); + # Delete duplicate entries if ($linkkey eq 'duplicates') { push my @duplicate_addresses, @{$$self{aldb}{duplicates}}; foreach (@duplicate_addresses) { - my %delete_req = (address => $_, - callback => "$selfname->_aldb->_process_delete_queue()", - cause => "it is a duplicate record"); + %delete_req = (address => $_, cause => "it is a duplicate record"); push @{$$self{delete_queue}}, \%delete_req; } next LINKKEY; @@ -477,28 +478,22 @@ sub delete_orphan_links # Initialize Variables my ($linked_device, $plm_scene, $controller_object, $link_defined, - $controller_id, $responder_id, $link_data3, - $recip_data3, $group_object, $interface_id, - $data3_object); + $controller_id, $responder_id, $link_data3, $recip_data3, + $group_object, $data3_object); my $group = lc $$self{aldb}{$linkkey}{group}; my $is_controller = $$self{aldb}{$linkkey}{is_controller}; my $data3 = lc $$self{aldb}{$linkkey}{data3}; my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; my $self_id = lc $$self{device}->device_id; - if ($$self{device}->isa('Insteon_PLM')){ - $interface_id = $self_id; - } - else { - $interface_id = lc $$self{device}->interface->device_id; - } + my $interface_id = $self_id; + $interface_id = lc $$self{device}->interface->device_id if (!$$self{device}->isa('Insteon_PLM')); $linked_device = Insteon::get_object($deviceid,'01'); $linked_device = Insteon::active_interface() if ($deviceid eq $interface_id); $group_object = ($is_controller) ? Insteon::get_object($self_id, $group) : Insteon::get_object($deviceid, $group); $data3_object = Insteon::get_object($self_id, $data3); - my %delete_req = (deviceid => $deviceid, + %delete_req = (deviceid => $deviceid, group => $group, is_controller => $is_controller, - callback => "$selfname->_aldb->_process_delete_queue()", data3 => $data3); # Is the linked device defined in MH? @@ -506,14 +501,14 @@ sub delete_orphan_links $delete_req{cause} = "no device with deviceid: $deviceid could be found"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; - } + } - # IF link is a PLM Scene, is the PLM Scene defined in MH? + # If link is a PLM Scene, is the PLM Scene defined in MH? if (($linked_device->isa("Insteon_PLM") and !$is_controller)|| ($$self{device}->isa("Insteon_PLM") and $is_controller)) { - $plm_scene = &Insteon::get_object('000000', $group); + $plm_scene = Insteon::get_object('000000', $group); if (!ref $plm_scene && $group ne '01' && $group ne '00') { - $delete_req{cause} = "no plm scene for this group could be found"; + $delete_req{cause} = "no plm scene for group $group could be found"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; } @@ -552,12 +547,11 @@ sub delete_orphan_links } # Second, is this a PLM->Device, Device->PLM link, these are not members - if ($$self{device}->isa("Insteon_PLM") && ($data3 eq '00' || $data3 eq '01')){ + if ($$self{device}->isa("Insteon_PLM")){ if ($is_controller && ($group eq '00' || $group eq '01')){ #Valid Controller for PLM->Device link - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " - ."Skipping reciprocal link check for group 00 or 01 link from " - ."$selfname to " . $linked_device->get_object_name); + $delete_req{skip} = "$selfname -- Skipping reciprocal link check for controller group 00 or 01 link to " + . $linked_device->get_object_name; next LINKKEY; } elsif (!$is_controller && ref $group_object){ @@ -582,49 +576,63 @@ sub delete_orphan_links # Third, delete link if not defined if (!$link_defined){ - $delete_req{cause} = "link is not defined in MisterHouse $linkkey"; + $delete_req{cause} = "link is not defined in MisterHouse"; push @{$$self{delete_queue}}, \%delete_req; next LINKKEY; } # Do not delete links to deaf devices if (!$linked_device->isa('Insteon_PLM') && $linked_device->is_deaf) { - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: " - ."ignoring link from $selfname to 'deaf' device: " . $linked_device->get_object_name); + $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on deaf device " . $linked_device->get_object_name; next LINKKEY; } # Do not delete links to unhealthy devices if ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping check for reciprocal links from " + $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on " . $linked_device->get_object_name . " because aldb health of that device is " - . $linked_device->_aldb->health . ". Please rescan this device!!"); + . $linked_device->_aldb->health . ". Please rescan this device."; next LINKKEY; } # Do not delete responder links from the PLM (prevents locking i2CS devices) if ($linked_device->isa("Insteon_PLM") and !$is_controller && ($group eq '00' || $group eq '01')) { - ::print_log("[Insteon::AllLinkDatabase] Skipping check for reciprocal link on PLM for responder " - ."link on $selfname for group 00 or 01."); + $delete_req{skip} = "$selfname -- Skipping check for reciprocal controller link on PLM for group 00 or 01."; next LINKKEY; } # Does a reciprocal link exist? if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $recip_data3)) { - $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name . " recip_data3 $recip_data3 linkkey $linkkey"; + $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name; push @{$$self{delete_queue}}, \%delete_req; } } # /LINKKEY Loop + my $index = 0; foreach (@{$$self{delete_queue}}){ my %delete_req = %{$_}; my $audit_text = "(AUDIT)" if ($audit_mode); - my $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; - $log_text .= $delete_req{cause} . "\n"; - PRINT: for (keys %delete_req) { - next PRINT if ($_ eq 'cause'); - next PRINT if ($_ eq 'callback'); - $log_text .= "$_ = $delete_req{$_}; "; + my $log_text; + if ($delete_req{skip}){ + $log_text = "[Insteon::AllLinkDatabase] $audit_text " . $delete_req{skip}; + splice @{$$self{delete_queue}}, $index, 1; + } else { + $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; + $log_text .= $delete_req{cause} . "\n"; + PRINT: for (keys %delete_req) { + next PRINT if ($_ eq 'cause'); + next PRINT if ($_ eq 'callback'); + $log_text .= "$_ = $delete_req{$_}; "; + } + if ($delete_req{deviceid}){ + my $reciprocal_object = Insteon::get_object($delete_req{deviceid}, '01'); + if (!$delete_req{is_controller}){ + $reciprocal_object = Insteon::get_object($delete_req{deviceid}, $delete_req{group}); + } + my $reciprocal_name = $reciprocal_object->get_object_name; + $log_text .= "linked device name= " . $reciprocal_name; + } + $index ++; } ::print_log($log_text); } @@ -2525,11 +2533,8 @@ sub delete_orphan_links &::print_log("[Insteon::ALDB_PLM] #### NOW BEGINNING DELETE ORPHAN LINKS ####"); - @{$$self{delete_queue}} = (); # reset the work queue $self->SUPER::delete_orphan_links($audit_mode); - $$self{delete_queue_processed} = 0; # reset the counter - # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links for my $obj (&Insteon::find_members('Insteon::BaseDevice')) { From a58f5ae0683f2c4bac0ca214a47ae7a09066ab12 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 17:44:09 -0700 Subject: [PATCH 273/330] Insteon: Fix Return from i2CS to Link to Interface Was in the wrong location, which skipped adding the controller record on the device for device->PLM --- lib/Insteon/BaseInsteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7a575d43d..edf517301 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1482,7 +1482,7 @@ sub link_to_interface_i2cs case (2) { #Scan device to get an accurate link table #return to normal link_to_interface routine if successful $success_callback_prefix = $self->get_object_name."->link_to_interface('$p_group','$p_data3',"; - $success_callback = $success_callback_prefix . "'2')"; + $success_callback = $success_callback_prefix . "'1')"; $self->scan_link_table($success_callback, $failure_callback); } } From 9c81b59e1b4b9dc26b7b897bb3e162de83568ce0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 18:17:46 -0700 Subject: [PATCH 274/330] Insteon: Prevent Success Callback from Running on Get Engine NACK Both the success callback and the failure callback were being called on an i2CS device when using the command link to interface. --- lib/Insteon/BaseInsteon.pm | 2 ++ lib/Insteon/Message.pm | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f465feef7..ee1de5a9a 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1886,6 +1886,8 @@ sub _get_engine_version_failure ."linked; Please use 'link to interface' voice command"); $self->engine_version('I2CS'); } + #Clear success callback, otherwise it will run when message is cleared + $self->interface->active_message->success_callback('0'); } =item C diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index fea11d116..fbdb13a34 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -113,7 +113,7 @@ Data will be evaluated after the receipt of an ACK from the device for this comm sub success_callback { my ($self, $callback) = @_; - if ($callback) + if (defined $callback) { $$self{success_callback} = $callback; } From 038387d3e72c199e6b247df3145d4fbf080c072d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 19 Oct 2013 19:35:58 -0700 Subject: [PATCH 275/330] Insteon: Fix Merge Conflict Bug --- lib/Insteon/BaseInsteon.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 188c85f0f..5946f27d8 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -3441,6 +3441,7 @@ Returns the root object of a device, in this case the interface. sub get_root { my ($self) = @_; return $self->interface; +} sub is_responder { return 1; From 10f0dc5bf8fbc7e851fd7c85e0eae96c9fae986f Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 20 Oct 2013 01:31:41 -0400 Subject: [PATCH 276/330] Simplify pa type iteration, switch to arrow notation for referenced hash values. --- code/common/pa_control.pl | 14 ++-- lib/PAobj.pm | 162 +++++++++++--------------------------- 2 files changed, 51 insertions(+), 125 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index 3418fb753..bb36db5ab 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -100,11 +100,11 @@ sub pa_parms_stub { } else { #MH is already speaking, and other PA zones are already active. Delay speech. if ($main::Debug{voice}) { - $$parms{clash_retry}=0 unless $$parms{clash_retry}; - &print_log("PA SPEECH CLASH($$parms{clash_retry}): Delaying speech call for " . $$parms{text} . "\n") unless $$parms{clash_retry} lt 1; - $$parms{clash_retry}++; #To track how many loops are made + $parms->{clash_retry}=0 unless $parms->{clash_retry}; + &print_log("PA SPEECH CLASH($parms->{clash_retry}): Delaying speech call for " . $parms->{text} . "\n") unless $parms->{clash_retry} lt 1; + $parms->{clash_retry}++; #To track how many loops are made } - $$parms{nolog}=1; #To stop MH from logging the speech again + $parms->{nolog}=1; #To stop MH from logging the speech again my $parmstxt; my ($pkey,$pval); @@ -112,13 +112,13 @@ sub pa_parms_stub { $parmstxt.=', ' if $parmstxt; $parmstxt .= "$pkey => q($pval)"; } - &print_log("PA SPEECH CLASH Parameters: $parmstxt") if $main::Debug{voice} && $$parms{clash_retry} eq 0; + &print_log("PA SPEECH CLASH Parameters: $parmstxt") if $main::Debug{voice} && $parms->{clash_retry} eq 0; &run_after_delay($pa_clash_delay, "speak($parmstxt)"); - $$parms{no_speak}=1; #To stop MH from speaking this time around + $parms->{no_speak}=1; #To stop MH from speaking this time around return; } - if ($$parms{clash_retry}) { + if ($parms->{clash_retry}) { &print_log("PA SPEECH CLASH: Resolved, continuing speech."); } } diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 8091f851c..4a7f1949a 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -49,7 +49,7 @@ sub new bless $self,$class; - $$self{pa_delay} = 0.5; + $self->{pa_delay} = 0.5; return $self; } @@ -66,66 +66,26 @@ sub init { $self->active(0); my @speakers = $self->get_speakers('allspeakers'); - my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey,@speakers_aviosys); + my %speakertype; for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); my $type = $ref->get_type(); &::print_log("PAobj: init: room=$room, zonetype=$type") if $main::Debug{pa}; $pa_zone_types{$type}++ unless $pa_zone_types{$type}; - - if($type eq 'wdio') { - push(@speakers_wdio,$room); - } - if($type eq 'x10') { - push(@speakers_x10,$room); - } - if($type eq 'xap') { - push(@speakers_xap,$room); - } - if($type eq 'xpl') { - push(@speakers_xpl,$room); - } - if($type eq 'object') { - push(@speakers_obj,$room); - } - if($type eq 'audrey') { - push(@speakers_audrey,$room); - } - if($type eq 'aviosys') { - push(@speakers_aviosys,$room); - } + push(@{$speakertype{$type}}, $room); } - &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa};# || $#speakers_wdio gt -1; - &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa};# || $#speakers_x10 gt -1; - &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa};# || $#speakers_xap gt -1; - &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa};# || $#speakers_xpl gt -1; - &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa};# || $#speakers_audrey gt -1; - &::print_log("PAobj: speakers_aviosys: $#speakers_aviosys") if $main::Debug{pa};# || $#speakers_aviosys gt -1; - &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa};# || $#speakers_obj gt -1; - - - $pa_zones{all}{wdio}=join(',',@speakers_wdio); - $pa_zones{all}{x10}=join(',',@speakers_x10); - $pa_zones{all}{xap}=join(',',@speakers_xap); - $pa_zones{all}{xpl}=join(',',@speakers_xpl); - $pa_zones{all}{audrey}=join(',',@speakers_audrey); - $pa_zones{all}{aviosys}=join(',',@speakers_aviosys); - $pa_zones{all}{obj}=join(',',@speakers_obj); - - if ($#speakers_wdio > -1) { - $self->init_weeder(@speakers_wdio); - } - if ($pa_zone_types{'x10'}) { - &::print_log("PAobj: x10 PA type initialized...") if $main::Debug{pa}; - } - if ($pa_zone_types{'xap'}) { - &::print_log("PAobj: xAP PA type initialized...") if $main::Debug{pa}; - } - if ($pa_zone_types{'xpl'}) { - &::print_log("PAobj: xPL PA type initialized...") if $main::Debug{pa}; - } + foreach my $type (keys(%speakertype)) { + my @thespeakers = \@{$speakertype{$type}}; + &::print_log("PAobj: speakers_$type: $#thespeakers") if $main::Debug{pa}; + $pa_zones{all}{$type}=join(',',@thespeakers); + if ($#thespeakers > -1) { + &::print_log("PAobj: $type PA type initialized...") if $main::Debug{pa}; + $self->init_weeder(@thespeakers) if $type eq 'wdio'; + } + } + return 1; } @@ -162,10 +122,10 @@ sub active &::print_log("PAobj: setactive: active: $active / set: $setactive\n") if $main::Debug{pa} >=4; return $active unless defined $setactive; if($active && $setactive) { - &::print_log("PAobj: Cannot make active, already active\n") if $main::Debug{pa} >=3; + &::print_log("PAobj: Cannot make active, already active\n") if $main::Debug{pa}; return 0; } - &::print_log("PAobj: setting active to: ".$setactive."\n") if $main::Debug{pa} >=3; + &::print_log("PAobj: setting active to: ".$setactive."\n") if $main::Debug{pa} >=2; $active=$setactive; return 1; } @@ -184,59 +144,24 @@ sub prep_parms $parms->{pa_zones} = join(',', @speakers); - my (@speakers_wdio,@speakers_x10,@speakers_obj,@speakers_xap,@speakers_xpl,@speakers_audrey,@speakers_aviosys); + my %speakertype; for my $room (@speakers) { my $ref = &::get_object_by_name("pa_$room"); - my $type = lc $ref->get_type(); - if($type eq 'wdio' || $type eq 'wdio_old') { - &::print_log("PAobj: speakers_wdio: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_wdio,$room); - } - if($type eq 'x10') { - &::print_log("PAobj: speakers_x10: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_x10,$room); - } - if($type eq 'xap') { - &::print_log("PAobj: speakers_xap: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_xap,$room); - } - if($type eq 'xpl') { - &::print_log("PAobj: speakers_xpl: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_xpl,$room); - } - if($type eq 'audrey') { - &::print_log("PAobj: speakers_audrey: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_audrey,$room); - } - if($type eq 'aviosys') { - &::print_log("PAobj: speakers_aviosys: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_aviosys,$room); - } - if($type eq 'object') { - &::print_log("PAobj: speakers_object: Adding $room") if $main::Debug{pa} >=3; - push(@speakers_obj,$room); - } + my $type = $ref->get_type(); + &::print_log("PAobj: speakers_$type: Adding $room") if $main::Debug{pa} >=3; + $pa_zone_types{$type}++ unless $pa_zone_types{$type}; + push(@{$speakertype{$type}}, $room); } - &::print_log("PAobj: speakers_wdio: $#speakers_wdio") if $main::Debug{pa} >=2 || $#speakers_wdio gt -1; - &::print_log("PAobj: speakers_x10: $#speakers_x10") if $main::Debug{pa} >=2 || $#speakers_x10 gt -1; - &::print_log("PAobj: speakers_xap: $#speakers_xap") if $main::Debug{pa} >=2 || $#speakers_xap gt -1; - &::print_log("PAobj: speakers_xpl: $#speakers_xpl") if $main::Debug{pa} >=2 || $#speakers_xpl gt -1; - &::print_log("PAobj: speakers_audrey: $#speakers_audrey") if $main::Debug{pa} >=2 || $#speakers_audrey gt -1; - &::print_log("PAobj: speakers_aviosys: $#speakers_aviosys") if $main::Debug{pa} >=2 || $#speakers_aviosys gt -1; - &::print_log("PAobj: speakers_obj: $#speakers_obj") if $main::Debug{pa} >=2 || $#speakers_obj gt -1; - - - $pa_zones{active}{wdio}=join(',',@speakers_wdio); - $pa_zones{active}{x10}=join(',',@speakers_x10); - $pa_zones{active}{xap}=join(',',@speakers_xap); - $pa_zones{active}{xpl}=join(',',@speakers_xpl); - $pa_zones{active}{audrey}=join(',',@speakers_audrey); - $pa_zones{active}{aviosys}=join(',',@speakers_aviosys); - $pa_zones{active}{obj}=join(',',@speakers_obj); - - $parms->{web_file}="web_file" if $#speakers_audrey gt -1; + foreach my $type (keys(%speakertype)) { + my @thespeakers = @{$speakertype{$type}}; + &::print_log("PAobj: speakers_$type: $#thespeakers: " . join(',',@thespeakers)) if $main::Debug{pa}; + $pa_zones{active}{$type}=join(',',@thespeakers); + if ($#thespeakers > -1) { + $parms->{web_file}="web_file" if $type eq 'audrey'; + } + } if( 1 @@ -248,7 +173,7 @@ sub prep_parms && $pa_zones{active}{obj} eq '' ) { - $$parms{to_file}='/dev/null'; + $parms->{to_file}='/dev/null'; } return 1; @@ -297,7 +222,7 @@ sub audio_hook $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; &::print_log("PAobj: set results: $results") if $main::Debug{pa}; - select undef, undef, undef, $$self{pa_delay} if $results; + select undef, undef, undef, $self->{pa_delay} if $results; $results=0; if( @@ -469,7 +394,8 @@ sub get_weeder_string for $bit ('A' .. $pa_weeder_max_port{$card}) { $id = $card . 'L' . $bit; - $id = "D$id" if $$self{pa_type} eq 'wdio_old'; #TODO: Find way to implement this with new code + #TODO: Find way to implement this with new code + #$id = "D$id" if $self->{pa_type} eq 'wdio_old'; my $ref = &Device_Item::item_by_id($id); if ($ref) { $state = $ref->{state}; @@ -495,7 +421,7 @@ sub get_weeder_string $weeder_code = $decimal_to_hex{$byte_code} . $weeder_code; } - if ($$self{pa_type} eq 'wdio_old') { #TODO: Find way to implement this with new code + if ($self->{pa_type} eq 'wdio_old') { #TODO: Find way to implement this with new code $card = "D$card"; $weeder_code = 'h' . $weeder_code; } @@ -613,7 +539,7 @@ sub get_pa_zones sub set_delay { my ($self,$delay) = @_; - $$self{pa_delay} = $delay; + $self->{pa_delay} = $delay; } sub print_speaker_states @@ -648,11 +574,11 @@ sub new bless $self,$class; - $$self{name} = $paz_name; - $$self{address} = $paz_address; - $$self{groups} = $paz_groups; - $$self{serial} = $paz_serial; - $$self{other} = $paz_other; + $self->{name} = $paz_name; + $self->{address} = $paz_address; + $self->{groups} = $paz_groups; + $self->{serial} = $paz_serial; + $self->{other} = $paz_other; return $self; } @@ -665,31 +591,31 @@ sub init sub get_address { my ($self) = @_; - return $$self{address}; + return $self->{address}; } sub get_name { my ($self) = @_; - return $$self{name}; + return $self->{name}; } sub get_groups { my ($self) = @_; - return $$self{groups}; + return $self->{groups}; } sub get_serial { my ($self) = @_; - return $$self{serial}; + return $self->{serial}; } sub get_type { my ($self) = @_; - return $$self{other}; + return $self->{other}; } 1; From 6cb48b78d9fb8648986338c6fecf6e3762ceaef9 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 20 Oct 2013 01:49:18 -0400 Subject: [PATCH 277/330] Fix array reference for wdio. --- lib/PAobj.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/PAobj.pm b/lib/PAobj.pm index 4a7f1949a..abee38043 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -77,7 +77,7 @@ sub init { } foreach my $type (keys(%speakertype)) { - my @thespeakers = \@{$speakertype{$type}}; + my @thespeakers = @{$speakertype{$type}}; &::print_log("PAobj: speakers_$type: $#thespeakers") if $main::Debug{pa}; $pa_zones{all}{$type}=join(',',@thespeakers); if ($#thespeakers > -1) { From 10e62b596bc02447eec0bdf9c043781dc06dd8cc Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 20 Oct 2013 02:00:39 -0400 Subject: [PATCH 278/330] Cause print_log statements to display the actual number of array elements, rather than the index of the last element. (index 0 = element 1) --- lib/PAobj.pm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/PAobj.pm b/lib/PAobj.pm index abee38043..a106a067b 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -78,7 +78,7 @@ sub init { foreach my $type (keys(%speakertype)) { my @thespeakers = @{$speakertype{$type}}; - &::print_log("PAobj: speakers_$type: $#thespeakers") if $main::Debug{pa}; + &::print_log("PAobj: speakers_$type: ".($#thespeakers+1)) if $main::Debug{pa}; $pa_zones{all}{$type}=join(',',@thespeakers); if ($#thespeakers > -1) { &::print_log("PAobj: $type PA type initialized...") if $main::Debug{pa}; @@ -156,7 +156,7 @@ sub prep_parms foreach my $type (keys(%speakertype)) { my @thespeakers = @{$speakertype{$type}}; - &::print_log("PAobj: speakers_$type: $#thespeakers: " . join(',',@thespeakers)) if $main::Debug{pa}; + &::print_log("PAobj: speakers_$type: ".($#thespeakers+1).": " . join(',',@thespeakers)) if $main::Debug{pa}; $pa_zones{active}{$type}=join(',',@thespeakers); if ($#thespeakers > -1) { $parms->{web_file}="web_file" if $type eq 'audrey'; @@ -271,7 +271,7 @@ sub set_audrey { my ($self,$speakFile,@speakers) = @_; &::print_log("PAobj: set_audrey: file: " . $speakFile) if $main::Debug{pa} >=4; - &::print_log("PAobj: set_audrey: count: " . $#speakers) if $main::Debug{pa} >=4; + &::print_log("PAobj: set_audrey: count: " . ($#speakers+1)) if $main::Debug{pa} >=4; for my $room (@speakers) { #my $ref = &::get_object_by_name('pa_'.$room); @@ -496,7 +496,7 @@ sub check_group my $ref = &::get_object_by_name("pa_$group"); if (!$ref) {&::print_log("PAobj: check group: Error! Group does not exist: $group"); return;} my @list = $ref->list; - &::print_log("PAobj: check group=$group, list=$#list") if $main::Debug{pa} >=2; + &::print_log("PAobj: check group=$group, list=".($#list+1)) if $main::Debug{pa} >=2; if ($#list == -1) { &::print_log("PAobj: check populating group: $group!") if $main::Debug{pa}; for my $room ($self->get_speakers('allspeakers')) { @@ -522,7 +522,7 @@ sub get_speakers_speakable my $gss_mode = $ref->{mode}; if ($gss_mode ne 'sleeping' && ($gss_mode eq 'normal' || $mode eq 'unmuted')) { push(@pazones,$room); - &::print_log("PAobj: speakable: Pushing $room into pazones array:$#pazones") if $main::Debug{pa} >=2; + &::print_log("PAobj: speakable: Pushed $room into pazones array. New count:".($#pazones+1)) if $main::Debug{pa} >=2; } } } From 242f9890e3b202b5a9874e72b71f5903524587a4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 09:57:53 -0700 Subject: [PATCH 279/330] Insteon: Don't Try and Name Unknown Objects in Delete Orphans --- lib/Insteon/AllLinkDatabase.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 6c14e711b..0b11aa306 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -629,8 +629,9 @@ sub delete_orphan_links if (!$delete_req{is_controller}){ $reciprocal_object = Insteon::get_object($delete_req{deviceid}, $delete_req{group}); } - my $reciprocal_name = $reciprocal_object->get_object_name; - $log_text .= "linked device name= " . $reciprocal_name; + if (ref $reciprocal_object) { + $log_text .= "linked device name= " . $reciprocal_object->get_object_name; + } } $index ++; } From 2e69fea9886464256f97fd16707dda2bafd8a5d5 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 10:27:27 -0700 Subject: [PATCH 280/330] Insteon: Move Device Parameter Flags in BaseObject add to BaseInterface This includes, is_deaf, is_responder, is_controller parameters. No need to continue calling both isa('Insteon::BaseDevice') and these functions. --- lib/Insteon/BaseInsteon.pm | 134 +++++++++++++++++++---------------- lib/Insteon/BaseInterface.pm | 34 +++++++++ 2 files changed, 108 insertions(+), 60 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 68ba714e8..876491412 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -127,6 +127,7 @@ sub new $$self{is_responder} = 1; $$self{default_hop_count} = 0; $$self{timeout_factor} = 1.0; + $$self{is_deaf} = 0; &Insteon::add($self); return $self; @@ -1124,6 +1125,69 @@ sub get_voice_cmds return \%voice_cmds; } +sub _aldb +{ + my ($self) = @_; + my $root_obj = $self->get_root(); + return $$root_obj{aldb}; +} + +=item C + +Returns true if the device must be awake in order to respond to messages. Most +devices are not deaf, currently devices that are deaf are battery operated +devices such as the Motion Sensor, RemoteLinc and TriggerLinc. + +At the BaseObject level all devices are defined as not deaf. Objects which +inherit BaseObject should redefine is_deaf as necessary. + +=cut + +sub is_deaf +{ + my ($self) = @_; + return $$self{is_deaf}; +} + +=item C + +Returns true if the device is a controller. + +=cut + +sub is_controller +{ + my ($self) = @_; + return $$self{is_controller}; +} + +=item C + +Stores and returns whether a device is a responder. + +=cut + +sub is_responder +{ + my ($self,$is_responder) = @_; + $$self{is_responder} = $is_responder if defined $is_responder; + if ($self->is_root) { + return $$self{is_responder}; + } + else + { + my $root_obj = $self->get_root(); + if (ref $root_obj) + { + return $$root_obj{is_responder}; + } + else + { + return 0; + } + } +} + =back =head2 INI PARAMETERS @@ -1277,7 +1341,6 @@ sub new $$self{max_queue_time} = 10 unless $$self{max_queue_time}; # 10 seconds is max time allowed in command stack @{$$self{command_stack}} = (); $$self{_onlevel} = undef; - $$self{is_responder} = 1; $$self{retry_count_log} = 0; $$self{fail_count_log} = 0; $$self{outgoing_count_log} = 0; @@ -1287,7 +1350,7 @@ sub new $$self{hops_left_count} = 0; $$self{max_hops_count} = 0; $$self{outgoing_hop_count} = 0; - $$self{is_deaf} = 0; + return $self; } @@ -1322,62 +1385,6 @@ sub rate return $$self{rate}; } -=item C - -Returns true if the device must be awake in order to respond to messages. Most -devices are not deaf, currently devices that are deaf are battery operated -devices such as the Motion Sensor, RemoteLinc and TriggerLinc. - -At the BaseObject level all devices are defined as not deaf. Objects which -inherit BaseObject should redefine is_deaf as necessary. - -=cut - -sub is_deaf -{ - my ($self) = @_; - return $$self{is_deaf}; -} - -=item C - -Returns true if the device is a controller. - -=cut - -sub is_controller -{ - my ($self) = @_; - return $$self{is_controller}; -} - -=item C - -Stores and returns whether a device is a responder. - -=cut - -sub is_responder -{ - my ($self,$is_responder) = @_; - $$self{is_responder} = $is_responder if defined $is_responder; - if ($self->is_root) { - return $$self{is_responder}; - } - else - { - my $root_obj = $self->get_root(); - if (ref $root_obj) - { - return $$root_obj{is_responder}; - } - else - { - return 0; - } - } -} - =item C If a controller link from the device to the interface does not exist, this will @@ -3471,8 +3478,15 @@ sub is_root return 0; } -sub is_responder { - return 1; +=item C + +Returns the root object of a device, in this case the interface. + +=cut + +sub get_root { + my ($self) = @_; + return $self->interface; } # For IFaceControllers, need to call set_linked_devices diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 5d0faf805..0b87c4806 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -926,6 +926,40 @@ sub get_voice_cmds return \%voice_cmds; } +=item C + +Returns false. + +=cut + +sub is_deaf +{ + return 0; +} + +=item C + +Returns true. + +=cut + +sub is_controller +{ + return 1; +} + +=item C + +Returns true. + +=cut + +sub is_responder +{ + return 1; +} + + =back =head2 INI PARAMETERS From 9cebf7d227e00103ae99a6054f97c4aa4c17cfc2 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 11:20:30 -0700 Subject: [PATCH 281/330] Insteon: Remove Extra ISA Calls from Sync Links --- lib/Insteon/BaseInsteon.pm | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index e34451c3f..2801650e5 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2857,33 +2857,32 @@ sub sync_links my $insteon_object = $self->interface; my $interface_object = Insteon::active_interface(); my $interface_name = $interface_object->get_object_name; - if (!($self->isa('Insteon::InterfaceController'))) { - $insteon_object = $self->get_root; - ::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " - . "Please double check your items.mht file.") if (!(defined($insteon_object))); - } + $insteon_object = $self->get_root; + ::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " + . "Please double check your items.mht file.") if (!(defined($insteon_object))); # Abort if $insteon_object doesn't exist $self->_process_sync_queue() unless $insteon_object; # Warn if device is deaf or ALDB out of sync - if ((!$self->isa('Insteon::InterfaceController') && $insteon_object->is_deaf)) { + my $insteon_object_is_syncable = 1; + if ($insteon_object->is_deaf) { ::print_log("[Insteon::BaseController] WARN! $self_link_name is a deaf device, responder links will be added to devices " ."controlled by this device, but no links will be added to $self_link_name."); + $insteon_object_is_syncable = 0; } - elsif (!$self->isa('Insteon::InterfaceController') && - ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty')){ + elsif ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty'){ ::print_log("[Insteon::BaseController] WARN! The ALDB of $self_link_name is ".$insteon_object->_aldb->health .", links will be added to devices " ."linked to this device, but no links will be added to $self_link_name. Please rescan this device and attempt " ."sync links again."); + $insteon_object_is_syncable = 0; } # 1. Does a controller link exist for Device-> PLM - if ((!$self->isa('Insteon::InterfaceController') && + if (!$insteon_object->isa('Insteon_PLM') && !$insteon_object->has_link($self->interface,$self->group,1,$subaddress) && - !$insteon_object->is_deaf && - ($insteon_object->_aldb->health eq 'good' || $insteon_object->_aldb->health eq 'empty'))) { + $insteon_object_is_syncable) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", @@ -2894,7 +2893,7 @@ sub sync_links } # 2. Does a responder link exist on the PLM - if ((!$self->isa('Insteon::InterfaceController') && + if ((!$insteon_object->isa('Insteon_PLM') && !$self->interface->has_link($insteon_object,$self->group,0,'00'))) { my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, group => $self->group, is_controller => 0, @@ -2994,8 +2993,7 @@ sub sync_links # 5. Does the controller link on this device exist if (!($insteon_object->has_link($member, $self->group, 1, $subaddress)) && - !$insteon_object->is_deaf && - ($insteon_object->_aldb->health eq 'healthy' || $insteon_object->_aldb->health eq 'empty')) { + $insteon_object_is_syncable) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", From 3de22c1161ef3c76fb5960d8771036f778a7323c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 12:00:20 -0700 Subject: [PATCH 282/330] Insteon: Do Not Get Root Obj for IFaceController in Is_Responder IFace Controller are responders, but root object PLM is not. --- lib/Insteon/BaseInsteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 876491412..c44c8bcdd 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1171,7 +1171,7 @@ sub is_responder { my ($self,$is_responder) = @_; $$self{is_responder} = $is_responder if defined $is_responder; - if ($self->is_root) { + if ($self->is_root || $self->isa('Insteon::InterfaceController')) { return $$self{is_responder}; } else From 6330e56bdd6a2197e2a93c6f5d990a8aba16d596 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 12:27:05 -0700 Subject: [PATCH 283/330] Insteon: KeyPadLinc Subgroups Derive Link State is Not Dimmable The subgroups are not dimmable --- lib/Insteon/BaseInsteon.pm | 7 ++++--- lib/Insteon/Lighting.pm | 12 ++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 68ba714e8..dba1a97f7 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -443,6 +443,7 @@ sub set_receive my ($self, $p_state, $p_setby, $p_response) = @_; my $curr_milli = sprintf('%.0f', &main::get_tickcount); my $window = 1000; + $p_state = $self->derive_link_state($p_state); if (($p_state eq $self->state || $p_state eq $self->state_final) && ($curr_milli - $$self{set_milliseconds} < $window)){ ::print_log("[Insteon::BaseObject] Ignoring duplicate set " . $p_state . @@ -3135,8 +3136,8 @@ sub set_linked_devices # remember the current state to support resume $$self{members}{$member_ref}{resume_state} = $light->state; $member->manual($light, $ramp_rate); - if ($light->can('derive_link_state') && lc $link_state ne 'on'){ - $local_state = $light->derive_link_state($link_state); + if (lc $link_state ne 'on'){ + $local_state = $light->$link_state; } $light->set_receive($local_state,$self); } @@ -3153,7 +3154,7 @@ sub set_linked_devices # if they are Insteon_Device objects, then simply set_receive their state to # the member on level if (lc $link_state ne 'on'){ - $local_state = $member->derive_link_state($link_state); + $local_state = $link_state; } $member->set_receive($local_state,$self); } diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index a4e23830a..4b786516f 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -986,6 +986,18 @@ sub get_voice_cmds return \%voice_cmds; } +# The subgroup items are not dimmable, so call BaseInsteon for them + +sub derive_link_state +{ + my ($self, $p_state) = @_; + if ($self->group eq '01'){ + return $self->SUPER::derive_link_state($p_state); + } else { + return $self->Insteon::BaseObject::derive_link_state($p_state); + } +} + =back =head2 AUTHOR From 1e6d29195c1e63028796dd9dba610490f6862f11 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 12:42:19 -0700 Subject: [PATCH 284/330] Insteon: Insert Delete_Req Hash Into Itself so as not to Delete Callback --- lib/Insteon/AllLinkDatabase.pm | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 0b11aa306..97b01c0f0 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -470,7 +470,7 @@ sub delete_orphan_links if ($linkkey eq 'duplicates') { push my @duplicate_addresses, @{$$self{aldb}{duplicates}}; foreach (@duplicate_addresses) { - %delete_req = (address => $_, cause => "it is a duplicate record"); + %delete_req = (%delete_req, address => $_, cause => "it is a duplicate record"); push @{$$self{delete_queue}}, \%delete_req; } next LINKKEY; @@ -491,10 +491,8 @@ sub delete_orphan_links $linked_device = Insteon::active_interface() if ($deviceid eq $interface_id); $group_object = ($is_controller) ? Insteon::get_object($self_id, $group) : Insteon::get_object($deviceid, $group); $data3_object = Insteon::get_object($self_id, $data3); - %delete_req = (deviceid => $deviceid, - group => $group, - is_controller => $is_controller, - data3 => $data3); + %delete_req = (%delete_req, deviceid => $deviceid, group => $group, + is_controller => $is_controller, data3 => $data3); # Is the linked device defined in MH? if (! ref $linked_device) { From ce6d858c8fe9871a8b34e7c7c293b754a99ef073 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 13:12:56 -0700 Subject: [PATCH 285/330] Insteon: Add Inline Debug Msg When Skipping a Link Due to ALDB Health in Sync Links --- lib/Insteon/BaseInsteon.pm | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index d779d0023..f5e196f9e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2868,8 +2868,8 @@ sub sync_links # Warn if device is deaf or ALDB out of sync my $insteon_object_is_syncable = 1; if ($insteon_object->is_deaf) { - ::print_log("[Insteon::BaseController] WARN! $self_link_name is a deaf device, responder links will be added to devices " - ."controlled by this device, but no links will be added to $self_link_name."); + ::print_log("[Insteon::BaseController] $self_link_name is deaf, only responder links will be added to devices " + ."controlled by this device."); $insteon_object_is_syncable = 0; } elsif ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty'){ @@ -2942,6 +2942,17 @@ sub sync_links $link_req{cause} = "Adding responder record to $member_name from $self_link_name"; push @{$$self{sync_queue}}, \%link_req; $has_link = 0; + } + elsif ($member_root->_aldb->health ne 'good' && $member_root->_aldb->health ne 'empty'){ + my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, + group => $self->group, is_controller => 0, + on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, + callback => "$self_link_name->_process_sync_queue()", + data3 => $member->group); + $link_req{skip} = "Unable to add the following responder record to $member_name " + ."from $self_link_name because the aldb of $member_name is " + . $member_root->_aldb->health; + push @{$$self{sync_queue}}, \%link_req; } # 4. Is the responder link accurate @@ -3005,6 +3016,7 @@ sub sync_links } my $num_sync_queue = @{$$self{sync_queue}}; + my $index = 0; if (!($num_sync_queue)) { &::print_log("[Insteon::BaseController] Nothing to do when syncing links for " . $self->get_object_name) @@ -3014,12 +3026,19 @@ sub sync_links my %sync_req = %{$_}; my $audit_text = "(AUDIT)" if ($audit_mode); my $log_text = "[Insteon::BaseController] $audit_text "; - $log_text .= $sync_req{cause} . "\n"; + if ($sync_req{skip}){ + $log_text .= $sync_req{skip} ."\n"; + splice @{$$self{sync_queue}}, $index, 1; + } else { + $index++; + $log_text .= $sync_req{cause} . "\n"; + } PRINT: for (keys %sync_req) { next PRINT if ($_ eq 'cause'); next PRINT if ($_ eq 'callback'); next PRINT if ($_ eq 'member'); next PRINT if ($_ eq 'object'); + next PRINT if ($_ eq 'skip'); $log_text .= "$_ = $sync_req{$_}; "; } ::print_log($log_text); From 3861dce8f9799e14cb1be157f58a31f92e95727d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 13:18:35 -0700 Subject: [PATCH 286/330] Insteon: Capture Number of Delete PLM Links --- lib/Insteon/AllLinkDatabase.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 97b01c0f0..54e1eacb4 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -2569,6 +2569,7 @@ sub _process_delete_queue { : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") if $main::Debug{insteon}; $self->delete_link(%delete_req); + $$self{delete_queue_processed}++; } } else From e89b5474852c9883cd15f5af0903b66c682d11b8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 20 Oct 2013 14:02:33 -0700 Subject: [PATCH 287/330] Insteon: Fix Log Link Entry for New Data3 Setup --- lib/Insteon/AllLinkDatabase.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 54e1eacb4..6f0dc2d79 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1134,7 +1134,7 @@ sub log_alllink_table } $log_msg .= $is_controller ? "contlr($aldb_entry->{group}) " - . "record to $object_name ($rspndr_group), " + . "record to $object_name, " . "(d1:$aldb_entry->{data1}, " . "d2:$aldb_entry->{data2}, " . "d3:$aldb_entry->{data3})" From 3b597eda8d93029036ca60c25de0fd4e93a120fe Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 20 Oct 2013 18:43:40 -0400 Subject: [PATCH 288/330] Change from every minute, to hook into weather_commons update routine. --- code/common/weather_summary.pl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/code/common/weather_summary.pl b/code/common/weather_summary.pl index b72178f66..9e3f12f0f 100644 --- a/code/common/weather_summary.pl +++ b/code/common/weather_summary.pl @@ -13,8 +13,14 @@ ## Mark Radke ## +if($Startup || $Reload){ + &Weather_Common::weather_add_hook(\&weather_summary_update); +} + +$v_weather_summary_update = new Voice_Cmd("Update weather summary"); +if(said $v_weather_summary_update) {&weather_summary_update;} -if ($New_Minute) { +sub weather_summary_update { ## ## '°' puts in a degree symbol ## From 26976c267afbf7155788a5e8af1414796dfc6b30 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 21 Oct 2013 21:55:31 -0400 Subject: [PATCH 289/330] Added Alsa support through amixer to enable left and right audio and multiple speaker out channels to be used as separate PA rooms. --- code/common/pa_control.pl | 12 ++++++++- lib/PAobj.pm | 51 ++++++++++++++++++++++++++++++++++----- lib/read_table_A.pl | 4 ++- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/code/common/pa_control.pl b/code/common/pa_control.pl index bb36db5ab..5ce3d7b51 100644 --- a/code/common/pa_control.pl +++ b/code/common/pa_control.pl @@ -175,6 +175,11 @@ sub pa_web_hook { PA, 192.168.0.1,family, all|mainfloor, , xap PA, 192.168.0.2,dining, all|mainfloor, , xpl PA, 192.168.0.3,table, all|mainfloor, , audrey +# +PA, Headphone:0:L, mix1l, all, , amixer +PA, Headphone:0:R, mix1r, all, , amixer +PA, Headphone:1, mix2, all, , amixer +PA, Headphone:1:R, mix2r, all, , amixer Type: "PA", constant. This must be there. @@ -185,6 +190,11 @@ sub pa_web_hook { if the command to turn on the pin you want is: BHC, then the Address is: BC For X10, the X10 address of the (likely) relay device. For xAP, xPL and audrey, use the IP address or hostname of the target device. + For amixer (Linux Only), use the alsa mixer name. My laptop has "Headphone" and + "Headphone 1". This is really "Headphone,0" and "Headphone,1". They are also both + stereo. Use : as a separator, and then add L or R to control the left or right + channel. Omitting this causes BOTH channels to be turned on. There's several examples + above. For "object", use the name of the object (without the $). You may use anything that responds ON and OFF set commands. Tested with and Insteon device. @@ -203,7 +213,7 @@ sub pa_web_hook { The default is "weeder". Note that this can be changed with an INI parm. Other: Optional. Sets the type of PA control. Defaults to 'wdio'. Available options are: - wdio,wdio_old,X10,xpl,xap,audrey,object + wdio,wdio_old,X10,xpl,xap,audrey,amixer,object @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =begin Audrey Config diff --git a/lib/PAobj.pm b/lib/PAobj.pm index a106a067b..ae3f8b346 100644 --- a/lib/PAobj.pm +++ b/lib/PAobj.pm @@ -170,6 +170,7 @@ sub prep_parms && $pa_zones{active}{xap} eq '' && $pa_zones{active}{xpl} eq '' && $pa_zones{active}{aviosys} eq '' + && $pa_zones{active}{amixer} eq '' && $pa_zones{active}{obj} eq '' ) { @@ -189,6 +190,7 @@ sub audio_hook my @speakers_x10=split(',',$pa_zones{active}{x10}); my @speakers_xap=split(',',$pa_zones{active}{xap}); my @speakers_xpl=split(',',$pa_zones{active}{xpl}); + my @speakers_amixer=split(',',$pa_zones{active}{amixer}); my @speakers_obj=split(',',$pa_zones{active}{obj}); #TODO: Properly handle $results across multiple types @@ -200,7 +202,7 @@ sub audio_hook for my $room (split(',',$pa_zones{active}{aviosys})) { my $ref = &::get_object_by_name('pa_'.$room); - my $serial=$ref->get_serial(); + my $serial=$ref->get_serial(); &::print_log("PAobj: aviosys serial: " . $room . " / " . $serial) if $main::Debug{pa} >=3; push(@{$speakers_aviosys{$serial}},$room); } @@ -210,7 +212,7 @@ sub audio_hook } for my $room (split(',',$pa_zones{active}{wdio})) { my $ref = &::get_object_by_name('pa_'.$room); - my $serial=$ref->get_serial(); + my $serial=$ref->get_serial(); &::print_log("PAobj: wdio serial: " . $room . " / " . $serial) if $main::Debug{pa} >=3; push(@{$speakers_wdio{$serial}},$room); } @@ -219,6 +221,7 @@ sub audio_hook $results = $self->set_weeder($state,$serial,@{$speakers_wdio{$serial}}); } + $results = $self->set_amixer($state,@speakers_amixer) if $#speakers_amixer > -1; $results = $self->set_obj($state,@speakers_obj) if $#speakers_obj > -1; &::print_log("PAobj: set results: $results") if $main::Debug{pa}; @@ -453,6 +456,34 @@ sub set_aviosys return 1; } +sub set_amixer +{ + my ($self,$state,@speakers) = @_; + my %amixerref; + my $mixpercent; + $mixpercent = '0%' if lc $state eq 'off'; + $mixpercent = '100%' if lc $state eq 'on'; + for my $room (@speakers) { + my $ref = &::get_object_by_name('pa_'.$room); + &::print_log("PAobj: set_amixer: " . $room . " / " . $state . " / " . $ref->{mixer} . " / " . $ref->{mixerchan}) if $main::Debug{pa} >=3; + + if(defined($ref->{mixerchan})) { + $amixerref{$ref->{mixer}}{'l'}='0%' unless $amixerref{$ref->{mixer}}{'l'}; + $amixerref{$ref->{mixer}}{'r'}='0%' unless $amixerref{$ref->{mixer}}{'r'}; + $amixerref{$ref->{mixer}}{$ref->{mixerchan}}=$mixpercent if $mixpercent; + } else { + $amixerref{$ref->{mixer}}{'l'}=$mixpercent if $mixpercent; + $amixerref{$ref->{mixer}}{'r'}=$mixpercent if $mixpercent; + } + } + foreach my $mixer (keys(%amixerref)) { + my $mixcmd = "amixer -q set $mixer ".$amixerref{$mixer}{'l'}.','.$amixerref{$mixer}{'r'}; + &main::print_log("PAobj: set_amixer: CMD: $mixcmd") if $main::Debug{pa} >=2; + my $r = system $mixcmd; + &main::print_log("PAobj: set_amixer: ERROR running command: $mixcmd") if $r != 0; + } +} + sub get_speakers { my ($self,$rooms) = @_; @@ -566,10 +597,10 @@ sub last_char return((sort @chars)[-1]); } -#Type Address Name Groups Serial Other +#Type Address Name Groups Serial Type sub new { - my ($class,$paz_address,$paz_name,$paz_groups,$paz_serial,$paz_other) = @_; + my ($class,$paz_address,$paz_name,$paz_groups,$paz_serial,$paz_type) = @_; my $self={}; bless $self,$class; @@ -578,7 +609,15 @@ sub new $self->{address} = $paz_address; $self->{groups} = $paz_groups; $self->{serial} = $paz_serial; - $self->{other} = $paz_other; + $self->{type} = $paz_type; + + if(lc $paz_type eq 'amixer') { + #Headphone:0:L + my ($mixer,$mixernum,$channel) = split(':',$self->{address}); + &main::print_log("$mixer / $mixernum / $channel"); + $self->{mixer} = "$mixer,$mixernum"; + $self->{mixerchan} = lc $channel if $channel; + } return $self; } @@ -615,7 +654,7 @@ sub get_serial sub get_type { my ($self) = @_; - return $self->{other}; + return $self->{type}; } 1; diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index f162ed2a8..26a28e39e 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -528,7 +528,7 @@ sub read_table_A { my ($pa_type, $serial); ($address, $name, $grouplist, $serial, $pa_type, @other) = @item_info; $pa_type = 'wdio' unless $pa_type; - + if( ! $packages{PAobj}++ ) { # first time for this object type? $code .= "my (%pa_weeder_max_port,%pa_zone_types,%pa_zone_type_by_zone);\n"; } @@ -585,6 +585,8 @@ sub read_table_A { my $aviosysref = {'on' => {'1' => '!','2' => '#','3' => '%','4' => '&','5' => '(','6' => '_','7' => '{','8' => '}' },'off' => {'1' => '@','2' => '$','3' => '^','4' => '*','5' => ')','6' => '-','7' => '[','8' => ']'}}; $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n",$name.'_obj',$aviosysref->{'on'}{$address},$serial; $code .= sprintf "\$%-35s -> add ('%s','off');\n",$name.'_obj',$aviosysref->{'off'}{$address}; + } elsif (lc $pa_type eq 'amixer') { + #Nothing needed here, except to avoid the "else" statement. } else { print "\nUnrecognized .mht entry for PA: $record\n"; return; From b0d962222d816bc01e344ac2fbe286f7ffb1b1bd Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 22 Oct 2013 17:27:00 -0700 Subject: [PATCH 290/330] Insteon: Remove Unnecessary ISA Calls in Delete_Orphans --- lib/Insteon/AllLinkDatabase.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 6f0dc2d79..4fcba2914 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -440,8 +440,8 @@ sub delete_orphan_links my $selfname = $$self{device}->get_object_name; # first, make sure that the health of ALDB is ok - if ($self->health ne 'good' || (!$$self{device}->isa('Insteon_PLM') && $$self{device}->is_deaf)) { - if (!$$self{device}->isa('Insteon_PLM') && $$self{device}->is_deaf) { + if ($self->health ne 'good' || $$self{device}->is_deaf) { + if ($$self{device}->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not delete links on deaf device: $selfname"); } elsif ($self->health eq 'empty'){ @@ -580,7 +580,7 @@ sub delete_orphan_links } # Do not delete links to deaf devices - if (!$linked_device->isa('Insteon_PLM') && $linked_device->is_deaf) { + if ($linked_device->is_deaf) { $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on deaf device " . $linked_device->get_object_name; next LINKKEY; } From a57834e74f72a8da0dff5a0d9eac5aabdee39ae4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 22 Oct 2013 17:28:00 -0700 Subject: [PATCH 291/330] Insteon: Catch, Report and Recover from Errors in Sync All Links This enables the same feature that has been available in the scan_all_links function. If an error occurs while attempting to sync_all_links, MH now recovers from the error, and continues to process the remainin objects in Sync_All. At the conclusion of the Sync_All routine a message is displayed identifying the failed objects. Partial Fix #73 --- lib/Insteon/AllLinkDatabase.pm | 4 ++-- lib/Insteon/BaseInsteon.pm | 18 +++++++++++------- lib/Insteon/BaseInterface.pm | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 4fcba2914..140a59d87 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -851,6 +851,8 @@ sub add_link # check whether the link already exists my $key = $self->get_linkkey($device_id, $group, $is_controller, $data3); + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ ## Check whether ALDB is in sync $self->{callback_parms} = \%link_parms; @@ -896,8 +898,6 @@ sub add_link $ramp_rate = '0.1' unless $ramp_rate; # 0.1s is the default # get the first available memory location my $address = $self->get_first_empty_address(); - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; if ($address) { &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record to " . $$self{device}->get_object_name diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index f5e196f9e..eb6cbfb3c 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2887,7 +2887,8 @@ sub sync_links my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", - data3 => $subaddress); + failure_callback => $failure_callback, + data3 => $self->group); $link_req{cause} = "Adding controller record to $self_link_name for $interface_name"; $link_req{data3} = $self->group; push @{$$self{sync_queue}}, \%link_req; @@ -2899,6 +2900,7 @@ sub sync_links my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, group => $self->group, is_controller => 0, callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, data3 => '00'); $link_req{cause} = "Adding responder record to $interface_name from $self_link_name"; push @{$$self{sync_queue}}, \%link_req; @@ -2938,6 +2940,7 @@ sub sync_links group => $self->group, is_controller => 0, on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, data3 => $member->group); $link_req{cause} = "Adding responder record to $member_name from $self_link_name"; push @{$$self{sync_queue}}, \%link_req; @@ -2948,6 +2951,7 @@ sub sync_links group => $self->group, is_controller => 0, on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, data3 => $member->group); $link_req{skip} = "Unable to add the following responder record to $member_name " ."from $self_link_name because the aldb of $member_name is " @@ -2997,6 +3001,7 @@ sub sync_links group => $self->group, is_controller => 0, on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, data3 => $member->group); $link_req{cause} = "Updating responder record on $member_name " . "to fix $cause"; @@ -3009,7 +3014,8 @@ sub sync_links my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, group => $self->group, is_controller => 1, callback => "$self_link_name->_process_sync_queue()", - data3 => $subaddress); + failure_callback => $failure_callback, + data3 => $self->group); $link_req{cause} = "Adding controller record to $self_link_name for $member_name"; push @{$$self{sync_queue}}, \%link_req; } @@ -3034,11 +3040,9 @@ sub sync_links $log_text .= $sync_req{cause} . "\n"; } PRINT: for (keys %sync_req) { - next PRINT if ($_ eq 'cause'); - next PRINT if ($_ eq 'callback'); - next PRINT if ($_ eq 'member'); - next PRINT if ($_ eq 'object'); - next PRINT if ($_ eq 'skip'); + next PRINT if (($_ eq 'cause') || ($_ eq 'callback') || + ($_ eq 'member') || ($_ eq 'object') || + ($_ eq 'skip') || ($_ eq 'failure_callback')); $log_text .= "$_ = $sync_req{$_}; "; } ::print_log($log_text); diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 0b87c4806..a010c0fed 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -369,8 +369,6 @@ sub process_queue &main::print_log("[Insteon::BaseInterface] WARN! Unable to clear acknowledge for " . ((defined($failed_message->setby)) ? $failed_message->setby->get_object_name : "undefined")); } - # clear active message - $self->clear_active_message(); # may instead want a "failure" callback separate from success callback if ($failed_message->failure_callback) { @@ -383,6 +381,8 @@ sub process_queue &::print_log("[Insteon::BaseInterface] problem w/ retry callback: $@") if $@; package Insteon::BaseInterface; } + # clear active message + $self->clear_active_message(); $self->process_queue(); } } From 5ade47aed386da2391cb005f37e2819eee8059a3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 22 Oct 2013 17:28:00 -0700 Subject: [PATCH 292/330] Insteon: Remove Unnecessary SubAddress Variable from Sync Links Routine --- lib/Insteon/BaseInsteon.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index eb6cbfb3c..b44e1742a 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2853,7 +2853,6 @@ sub sync_links # Intialize Variables @{$$self{sync_queue}} = (); # reset the work queue $$self{sync_queue_callback} = ($callback) ? $callback : undef; - my $subaddress = $self->group; my $self_link_name = $self->get_object_name; my $insteon_object = $self->interface; my $interface_object = Insteon::active_interface(); @@ -2882,7 +2881,7 @@ sub sync_links # 1. Does a controller link exist for Device-> PLM if (!$insteon_object->isa('Insteon_PLM') && - !$insteon_object->has_link($self->interface,$self->group,1,$subaddress) && + !$insteon_object->has_link($self->interface,$self->group,1,$self->group) && $insteon_object_is_syncable) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, group => $self->group, is_controller => 1, @@ -2890,7 +2889,6 @@ sub sync_links failure_callback => $failure_callback, data3 => $self->group); $link_req{cause} = "Adding controller record to $self_link_name for $interface_name"; - $link_req{data3} = $self->group; push @{$$self{sync_queue}}, \%link_req; } @@ -3009,7 +3007,7 @@ sub sync_links } # 5. Does the controller link on this device exist - if (!($insteon_object->has_link($member, $self->group, 1, $subaddress)) && + if (!($insteon_object->has_link($member, $self->group, 1, $self->group)) && $insteon_object_is_syncable) { my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, group => $self->group, is_controller => 1, From b3bd3ad36c45075d934810ed9dd43895a90ed1b7 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 22 Oct 2013 17:29:00 -0700 Subject: [PATCH 293/330] Insteon: Extend Failure Callback to Update_Links --- lib/Insteon/AllLinkDatabase.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 140a59d87..d6089582e 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -975,6 +975,9 @@ sub update_link my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; + my $deviceid = $insteon_object->device_id; my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ @@ -997,8 +1000,6 @@ sub update_link elsif (defined $$self{aldb}{$key} && $link_parms{aldb_check} eq "ok"){ my $address = $$self{aldb}{$key}{address}; $$self{_mem_activity} = 'update'; - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; $self->_write_link($address, $deviceid, $group, $is_controller, $data1, $data2, $data3); } else { &::print_log("[Insteon::AllLinkDatabase] ERROR: updating link record failed because " From e361a69461febb567d5a7b232415cdac12d3dc1e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 22 Oct 2013 20:18:43 -0700 Subject: [PATCH 294/330] Insteon: Track, Recover, and Report from Failed Delete Orphans Adds a similar functionality to what has been in scan all links. If a failure occurs on a device while running delte orphans, MH now recovers and continues to process the rest of the delete queue. At the conclusion of the queue, the failed object is reported. Fixes #82 #73 --- lib/Insteon/AllLinkDatabase.pm | 52 +++++++++++++++++++++++----------- lib/Insteon/BaseInsteon.pm | 4 +-- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index d6089582e..42fab13a2 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -434,7 +434,7 @@ scanned and processed. sub delete_orphan_links { - my ($self, $audit_mode) = @_; + my ($self, $audit_mode, $failure_callback) = @_; @{$$self{delete_queue}} = (); # reset the work queue $$self{delete_queue_processed} = 0; my $selfname = $$self{device}->get_object_name; @@ -464,7 +464,8 @@ sub delete_orphan_links next LINKKEY if ($linkkey eq 'empty'); # Define delete request - my %delete_req = (callback => "$selfname->_aldb->_process_delete_queue()",); + my %delete_req = (callback => "$selfname->_aldb->_process_delete_queue()", + failure_callback => $failure_callback); # Delete duplicate entries if ($linkkey eq 'duplicates') { @@ -618,8 +619,8 @@ sub delete_orphan_links $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; $log_text .= $delete_req{cause} . "\n"; PRINT: for (keys %delete_req) { - next PRINT if ($_ eq 'cause'); - next PRINT if ($_ eq 'callback'); + next PRINT if (($_ eq 'cause') || ($_ eq 'callback') || + ($_ eq 'failure_callback')); $log_text .= "$_ = $delete_req{$_}; "; } if ($delete_req{deviceid}){ @@ -2557,27 +2558,48 @@ sub _process_delete_queue { { my $delete_req_ptr = shift(@{$$self{delete_queue}}); my %delete_req = %$delete_req_ptr; + my $failure_callback = $$self{device}->get_object_name . + "->_aldb->_process_delete_queue_failure"; # distinguish between deleting PLM links and processing delete orphans for a root item if ($delete_req{'root_object'}) { - $delete_req{'root_object'}->delete_orphan_links($delete_req{'audit_mode'}); + $$self{current_delete_device} = $delete_req{'root_object'}->get_object_name; + $delete_req{'root_object'}->delete_orphan_links(($delete_req{'audit_mode'}) ? 1 : 0, $failure_callback); } else { + $$self{current_delete_device} = $$self{device}->get_object_name; &::print_log("[Insteon::ALDB_PLM] now deleting orphaned link w/ details: " . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") if $main::Debug{insteon}; + $delete_req{failure_callback} = $failure_callback; $self->delete_link(%delete_req); $$self{delete_queue_processed}++; } } - else - { - &::print_log("[Insteon::ALDB_PLM] A total of $$self{delete_queue_processed} orphaned link records were deleted."); - &::print_log("[Insteon::ALDB_PLM] #### END DELETE ORPHAN LINKS ####"); + else { + ::print_log("[Insteon::ALDB_PLM] Delete All Links has Completed."); + my $_delete_failure_cnt = scalar $$self{_delete_device_failures}; + if ($_delete_failure_cnt) { + &main::print_log("[Insteon::ALDB_PLM] However, some failures were noted with the following devices:"); + for my $failed_obj (@{$$self{_delete_device_failures}}) { + ::print_log("[Insteon::ALDB_PLM] Failure on: " + . $failed_obj); + } + } + ::print_log("[Insteon::ALDB_PLM] A total of $$self{delete_queue_processed} orphaned link records were deleted."); + ::print_log("[Insteon::ALDB_PLM] #### END DELETE ORPHAN LINKS ####"); } +} + +sub _process_delete_queue_failure { + my ($self) = @_; + push @{$$self{_delete_device_failures}}, $$self{current_delete_device}; + ::print_log("[Insteon::ALDB_PLM] WARN: failure occurred when deleting orphan links from: " + . $$self{current_delete_device} . ". Moving on..."); + $self->_process_delete_queue; } @@ -2620,10 +2642,8 @@ sub delete_link delete $$self{aldb}{$linkkey}; $num_deleted = 1; my $message = new Insteon::InsteonMessage('all_link_manage_rec', $$self{device}); - if ($link_parms{callback}) - { - $$self{_success_callback} = $link_parms{callback}; - } + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; $message->interface_data($cmd); $$self{device}->queue_message($message); } @@ -2729,10 +2749,8 @@ sub add_link $self->health('good') if($self->health() eq 'empty'); my $message = new Insteon::InsteonMessage('all_link_manage_rec', $$self{device}); $message->interface_data($cmd); - if ($link_parms{callback}) - { - $$self{_success_callback} = $link_parms{callback}; - } + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; $message->interface_data($cmd); $$self{device}->queue_message($message); } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index b44e1742a..a64e8744c 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2177,8 +2177,8 @@ does nothing. sub delete_orphan_links { - my ($self, $audit_mode) = @_; - return $self->_aldb->delete_orphan_links($audit_mode) if $self->_aldb; + my ($self, $audit_mode, $failure_callback) = @_; + return $self->_aldb->delete_orphan_links($audit_mode, $failure_callback) if $self->_aldb; } sub _process_delete_queue { From b5ba05100a09fb0215baf07baeaaf8bc5b957afd Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 23 Oct 2013 17:29:00 -0700 Subject: [PATCH 295/330] Insteon: Re-enable off_fast and on_fast in dimmable lights Accidentally dropped this feature when the new set routine was merged. --- lib/Insteon/Lighting.pm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 4b786516f..930421c2b 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -187,9 +187,8 @@ sub derive_link_state } my $link_state = 'on'; - if ($p_state eq 'off' or $p_state eq 'off_fast') - { - $link_state = 'off'; + if (grep(/$p_state/i, @{['on_fast', 'off', 'off_fast']})) { + $link_state = $p_state; } elsif ($p_state =~ /\d+%?/) { From a70563b522707ece28cdbce87fd23e600f546e7d Mon Sep 17 00:00:00 2001 From: CityDweller Date: Fri, 25 Oct 2013 10:15:35 -0400 Subject: [PATCH 296/330] array error/deprecations from perl --- lib/Musica.pm | 84 +++++++++++++++++++++++++-------------------------- lib/UPBPIM.pm | 2 +- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/lib/Musica.pm b/lib/Musica.pm index ada131467..e4965cc86 100644 --- a/lib/Musica.pm +++ b/lib/Musica.pm @@ -7,7 +7,7 @@ Important Note: Because the new FM-Tuner keypads (and possibly other newer keypads?) take *forever (minutes) to respond to the StatVer command, their version is stored in the persistant Misterhouse %Save hash. This means that if you - change the physical keypad, you'll need to stop Misterhouse, edit + change the physical keypad, you'll need to stop Misterhouse, edit data_dir/mh_temp.saved_states.persistent, and search for 'Musica' and remove or modify the entry for the zone you chaged. Then start Misterhouse back up. If you simply remove a keypad, just take the entry for that zone @@ -136,7 +136,7 @@ Controlling either all zones or one zone: $zone1_obj->set_source('MP3'); $Musica->set_source($source1_obj); - set_volume(level): Sets the volume of one or all zones, where the level + set_volume(level): Sets the volume of one or all zones, where the level specified must range from 0 to 35. Can also be specified as a percentage where '0%' is off and '100%' is full volume. @@ -200,8 +200,8 @@ Controlling either all zones or one zone: Controlling the Musica system object: The following functions allow you to make changes to the Musica system - as a whole from Misterhouse. - + as a whole from Misterhouse. + all_off(): Turn off all zones. Retrieving data from the Musica system object: @@ -223,9 +223,9 @@ Controlling the Musica zone objects using functions: The following functions allow you to make changes to a specific Musica zone from Misterhouse. - + set_source(source): Sets the zone to the specified source, where 'source' - is a number between 1 and 4 or E for the local expansion source. Turns + is a number between 1 and 4 or E for the local expansion source. Turns the zone ON if it is not already on. turn_off(): Turns off the zone. @@ -294,15 +294,15 @@ Monitoring the Musica zone objects: source_changed: The zone is already on and a user changed the source. changed_label_source_X: This zone keypad was used to change the label of source X (where X is a number from 1 to 4). - volume_changed: Volume of the zone was changed. - bass_changed: Bass level of the zone was changed. + volume_changed: Volume of the zone was changed. + bass_changed: Bass level of the zone was changed. treble_changed: Treble level of the zone was changed. balance_changed: Balance level of the zone was changed. loudness_on: Loudness was enabled in the zone loudness_off: Loudness was disabled in the zone mute_on: This zone was muted. mute_off: This zone was unmuted. - color_amber: The backlight color in this zone was changed to amber + color_amber: The backlight color in this zone was changed to amber (or blue on applicable keypads). color_green: The backlight color in this zone was changed to green (or white on applicable keypads). @@ -321,8 +321,8 @@ Monitoring the Musica zone objects: button_pressed_*: A button was pressed (button names can be found after this comment section or by calling get_button_labels() on a zone object). - button_held_*: A button was pressed and held (button names can be found - after this comment section or by calling get_button_labels() on a + button_held_*: A button was pressed and held (button names can be found + after this comment section or by calling get_button_labels() on a zone object). Controlling the Musica source object: @@ -332,18 +332,18 @@ Controlling the Musica source object: set_label(label): Sets the global label for this particular source. The label can either be given as a number between 1 and 30 or as a string. - Only certain strings are allowed, and these strings can be determined + Only certain strings are allowed, and these strings can be determined by calling get_source_labels() on a source object. Retrieving data from a Musica source object: - get_musica_obj(): Returns the main musica object. - get_source_num(): Returns the numerical source number associated with + get_musica_obj(): Returns the main musica object. + get_source_num(): Returns the numerical source number associated with this source object. - get_label(): Returns the label for the source as a string. + get_label(): Returns the label for the source as a string. get_source_labels(): Returns a list of valid source labels. get_zones(): Returns array of zones currently listening to this source. - get_usage_count(): Returns the number of zones currently listening to + get_usage_count(): Returns the number of zones currently listening to this source. Monitoring the Musica source objects: @@ -354,14 +354,14 @@ Monitoring the Musica source objects: error: An error has occurred, call get_last_error() for details. label_changed: somebody changed the label for this source from a keypad (call get_set_by() to find the object name of the keypad used to make - the change). + the change). first_listener: the source now has a listener and it did not before. no_listeners: the only listener stopped listening and now nobody is using this particular source. listener_zone_X: zone X just selected this source. button_pressed_*: A button was pressed (button names can be found after this comment section or by calling get_button_labels()). - button_held_*: A button was pressed and held (button names can be found + button_held_*: A button was pressed and held (button names can be found after this comment section or by calling get_button_labels()). Usage Examples: @@ -377,10 +377,10 @@ Usage Examples: } } - Then I create an array of all of the Musica::Source objects (which + Then I create an array of all of the Musica::Source objects (which are already defined in my .mht file): - my @musica_sources = ( $music_source1, $music_source2, $music_source3, + my @musica_sources = ( $music_source1, $music_source2, $music_source3, $music_source4 ); The 'first_listener' and 'no_listeners' states are handy to start/stop @@ -424,7 +424,7 @@ MUSICA SYSTEM BUGS: - Never sends back a copy of the ChangeAmp command nor does it send an EventData message back. - Does not respond to various commands like ChangeVol, ChangeBass, etc. - Instead, sends an EventData message showing the changes (bug or protocol + Instead, sends an EventData message showing the changes (bug or protocol change?) [ADC Version M30419/Keypad Version R30419] @@ -451,7 +451,7 @@ TODO: - Implement IR_Dn/IR_Up? - ExeMenu* is not implemented as the ADC does not respond which makes it not like every other command plus I don't know why you would use it as it - just allows you to navigate the menu system on a keypad but you can + just allows you to navigate the menu system on a keypad but you can change everything directly through other messages anyways. But, for the record: ExeMenu/Zone/Command where Command is: @@ -506,8 +506,8 @@ package Musica; @Musica::ISA = ('Serial_Item'); -use vars qw( %source_name_to_number @source_number_to_name - %source_name_to_number_30419 @source_number_to_name_30419 +use vars qw( %source_name_to_number @source_number_to_name + %source_name_to_number_30419 @source_number_to_name_30419 %button_name_to_number @button_number_to_name %button_name_to_number_40822 @button_number_to_name_40822 ); @@ -574,7 +574,7 @@ use vars qw( %source_name_to_number @source_number_to_name 'R&B' => 23, 'RAP' => 24, 'RADIO' => 25, - 'HD RADIO' => 25, + 'HD RADIO' => 25, 'ROCK' => 26, 'SAT' => 27, 'SAT2' => 28, @@ -636,7 +636,7 @@ use vars qw( %source_name_to_number @source_number_to_name 'SAT' , 'SAT2' , 'SOUL' , - 'WESTERN' + 'WESTERN' ); @source_number_to_name_30419 = ( @@ -681,22 +681,22 @@ use vars qw( %source_name_to_number @source_number_to_name 'DSS' , 'M SERVER' , 'DISH' , - '' + '' ); %button_name_to_number = ( - 'pause' => 1, - 'stop' => 2, - 'play' => 3, - 'rewind' => 4, - 'up' => 5, - 'forward' => 6, - 'left' => 7, - 'down' => 8, - 'right' => 9, - 'previous' => 10, + 'pause' => 1, + 'stop' => 2, + 'play' => 3, + 'rewind' => 4, + 'up' => 5, + 'forward' => 6, + 'left' => 7, + 'down' => 8, + 'right' => 9, + 'previous' => 10, 'power' => 11, - 'next' => 12, + 'next' => 12, ); %button_name_to_number_40822 = ( @@ -1168,14 +1168,14 @@ sub _parse_data { } } } elsif ($cmd eq 'EventData') { - my ($zone, $volume, $bass, $treble, $balance, $loudness, $mute, $blcolor, + my ($zone, $volume, $bass, $treble, $balance, $loudness, $mute, $blcolor, $brightness, $audioport, $amp, $locked, $overheat) = split /\//, $value; if ($$self{'zones'}[$zone]) { if ($self->_check_first_cmd($zone, "ChangeVol/$zone/$volume")) { $self->_store_zone_data($zone, 'volume', $volume, 'volume_changed'); } elsif ($$self{'zones'}[$zone]->{'volume'} != $volume) { if (defined($$self{'zones'}[$zone]->{'volume'}) and (($$self{'zones'}[$zone]->{'on_time'} + IGNORE_AFTER_ON) < $::Time)) { - $$self{'zones'}[$zone]->set_receive('volume_changed', 'keypad') + $$self{'zones'}[$zone]->set_receive('volume_changed', 'keypad') } $$self{'zones'}[$zone]->{'volume'} = $volume; } @@ -1302,7 +1302,7 @@ sub _parse_data { sub _process_keypad_version { my ($self, $zone_num) = @_; if ($$self{'zones'}[$zone_num]->{'keypad_version'} < 40822) { - # Use older source names if any keypad is older... + # Use older source names if any keypad is older... %source_name_to_number = %source_name_to_number_30419; @source_number_to_name = @source_number_to_name_30419; } @@ -1343,7 +1343,7 @@ sub _send_next_cmd { if ($$self{'zones'}[$zone]->{'just_turned_on'}) { my $moved = ''; # See if we can find a command for another zone meanwhile - for (my $i = 0; $i <= $#{@{$$self{'queue'}}}; $i++) { + for (my $i = 0; $i <= {@{$$self{'queue'}}}; $i++) { if ($$self{'queue'}->[$i] =~ /^[^\/]+\/(\d+)/) { if ($$self{'zones'}[$1] and not $$self{'zones'}[$zone]->{'just_turned_on'}) { $moved = $$self{'queue'}->[$i]; diff --git a/lib/UPBPIM.pm b/lib/UPBPIM.pm index 4730a1a1d..f6d1ade59 100644 --- a/lib/UPBPIM.pm +++ b/lib/UPBPIM.pm @@ -181,7 +181,7 @@ sub get_register my $response; for (my $index=$start;$index<$start+$end;$index++) { - $response.=sprintf("%02X",@{$$self{'registers'}}->[$index]); + $response.=sprintf("%02X",$$self{'registers'}->[$index]); } return $response; } From 7842ee0365ac39f4d85990ef18ff50b9b9b56635 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 25 Oct 2013 13:24:56 -0700 Subject: [PATCH 297/330] Scene_Builder: Dereference Hashes for Backwards Compatibility in Perl http://stackoverflow.com/questions/10979486/perl-incompatibility-issue-with-each-in-a-hash-of-hashes-5-14-5-8-8 as discovered by @CityDweller --- lib/read_table_A.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index f3240d200..38cb9aed1 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1085,10 +1085,10 @@ sub read_table_finish_A { #Loop through the controller hash if (exists $scene_build_controllers{$scene}){ - foreach my $scene_controller (keys $scene_build_controllers{$scene}) { + foreach my $scene_controller (keys %{$scene_build_controllers{$scene}}) { if ($objects{$scene_controller}) { #Make a link to each responder in the responder hash - while (my ($scene_responder, $responder_data) = each($scene_build_responders{$scene})) { + while (my ($scene_responder, $responder_data) = each(%{$scene_build_responders{$scene}})) { my ($on_level, $ramp_rate) = split(',', $responder_data); if (($objects{$scene_responder}) and ($scene_responder ne $scene_controller)) { From 7981001f6aad2df016702187a86958bc9cfd77b6 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 25 Oct 2013 17:29:00 -0700 Subject: [PATCH 298/330] Insteon: Add Sync_All_Links Routine for Multigroup items; Add MultigroupDevice Package Added a package called MutigroupDevice in BaseInsteon.pm. The purpose of this package is to contain routines specific to all multigroup devices. Added Sync_All_links routines to MultigroupDevice. The routine will sync all links between any group on the device. Useful for KeyPadLincs and RemoteLincs and such. Fixes #85 --- lib/Insteon/BaseInsteon.pm | 147 +++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index b1b7259dc..82dacfa90 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2745,6 +2745,153 @@ You should have received a copy of the GNU General Public License along with thi =cut +#################################### +### ############### +### MultigroupDevice ############### +### ############### +#################################### + +=head1 B + +=head2 DESCRIPTION + +Contains functions which are unique to insteon devices which have more than +one group. This includes, KeyPadLincs, RemoteLincs, FanLincs, Thermostats +(i2 versions). + +=head2 INHERITS + +Nothing. + +This package is meant to provide supplemental support and should only be added +as a secondary inheritance to an object. + +=head2 METHODS + +=over + +=cut + +package Insteon::MultigroupDevice; + +=item C + +Syncs all links on the object, including all subgroups such as additional +buttons. + +Paramter B - Causes sync to walk through but not actually +send any commands to the devices. Useful with the insteon:3 debug setting for +troubleshooting. + +=cut + +sub sync_all_links +{ + my ($self, $audit_mode) = @_; + $self = $self->get_root(); + @{$$self{_sync_devices}} = (); + @{$$self{_sync_device_failures}} = (); + my $device_id = $self->device_id(); + my ($subgroup_object, $group, $dec_group); + + ::print_log("[Insteon::MultigroupDevice] Sync All Links on device " + .$self->get_object_name . " starting ..."); + # Find all subgroup items check groups from 02 - FF; + for ($dec_group = 02; $dec_group <= 255; $dec_group++) { + $group = sprintf("%02X", $dec_group); + $subgroup_object = Insteon::get_object($device_id, $group); + if (ref $subgroup_object){ + my %sync_req = ('sync_object' => $subgroup_object, + 'audit_mode' => ($audit_mode) ? 1 : 0); + ::print_log("[Insteon::MultigroupDevice] " + ."Adding " . $subgroup_object->get_object_name + ." to sync queue."); + push @{$$self{_sync_devices}}, \%sync_req + } + } + $$self{_sync_cnt} = scalar @{$$self{_sync_devices}}; + $self->_get_next_linksync(); +} + +=item C<_get_next_linksync()> + +Calls the sync_links() function for each device identified by sync_all_link. +This function will be called recursively since the callback passed to sync_links() +is this function again. Will also ask sync_links() to call +_get_next_linksync_failure() if sync_links() fails. + +=cut + +sub _get_next_linksync +{ + my ($self) = @_; + $self = $self->get_root(); + my $sync_req_ptr = shift(@{$$self{_sync_devices}}); + my %sync_req = ($sync_req_ptr) ? %$sync_req_ptr : undef; + my $current_sync_device; + if (%sync_req) { + $current_sync_device = $sync_req{'sync_object'}; + } + else { + $current_sync_device = undef; + } + + if ($current_sync_device) { + ::print_log("[Insteon::MultigroupDevice] Now syncing: " + . $current_sync_device->get_object_name . " (" + . ($$self{_sync_cnt} - scalar @{$$self{_sync_devices}}) + . " of ".$$self{_sync_cnt}.")"); + $current_sync_device->sync_links($sync_req{'audit_mode'}, + $self->get_object_name . '->_get_next_linksync()', + $self->get_object_name . '->_get_next_linksync_failure('.$current_sync_device->get_object_name.')'); + } + else { + ::print_log("[Insteon::MultigroupDevice] All links have completed syncing " + . "on device " . $self->get_object_name); + my $_sync_failure_cnt = scalar @{$$self{_sync_device_failures}}; + if ($_sync_failure_cnt) { + ::print_log("[Insteon::MultigroupDevice] However, some failures were noted:"); + for my $failed_obj (@{$$self{_sync_device_failures}}) { + ::print_log("[Insteon::MultigroupDevice] WARN: failure occurred when syncing " + . $failed_obj->get_object_name); + } + } + } +} + +=item C<_get_next_linksync()> + +Called by the failure callback in a device's sync_links() function. Will add +the failed device to the module global variable @_sync_device_failures. + +=cut + +sub _get_next_linksync_failure +{ + my ($self, $current_sync_device) = @_; + $self = $self->get_root(); + push @{$$self{_sync_device_failures}}, $current_sync_device; + ::print_log("[Insteon::MultigroupDevice] WARN: failure occurred when syncing " + . $current_sync_device->get_object_name . ". Moving on..."); + $self->_get_next_linksync(); +} + +=back + +=head2 AUTHOR + +Kevin Robert Keegan + +=head2 LICENSE + +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. + +=cut + #################################### ### ################# ### BaseController ################# From ed4ecba51c7277effbc16530ace6ff273224f26a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 25 Oct 2013 17:39:00 -0700 Subject: [PATCH 299/330] Insteon: Add Sync All Links Voice Command to Multigroup Devices --- lib/Insteon/Controller.pm | 33 ++++++++++++++++++++++++++++- lib/Insteon/Lighting.pm | 44 ++++++++++++++++++++++++++++++++++----- lib/Insteon/Thermostat.pm | 13 +++++++++--- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 4c1c67e30..93568a86f 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -42,6 +42,7 @@ must first be put into "awake mode." L, L, +L =head2 METHODS @@ -54,7 +55,7 @@ package Insteon::RemoteLinc; use strict; use Insteon::BaseInsteon; -@Insteon::RemoteLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); +@Insteon::RemoteLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController', 'Insteon::MultigroupDevice'); my %message_types = ( %Insteon::BaseDevice::message_types, @@ -243,6 +244,36 @@ sub _process_message { return $clear_message; } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" + ); + } + return \%voice_cmds; +} + =back =head2 AUTHOR diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 4b786516f..bfa473296 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -733,7 +733,8 @@ Provides support for the Insteon KeypadLinc Relay. =head2 INHERITS L, -L +L, +L =head2 METHODS @@ -746,7 +747,7 @@ package Insteon::KeyPadLincRelay; use strict; use Insteon::BaseInsteon; -@Insteon::KeyPadLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); +@Insteon::KeyPadLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController', 'Insteon::MultigroupDevice'); our %operating_flags = ( 'program_lock_on' => '00', @@ -876,7 +877,9 @@ sub get_voice_cmds 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", - 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")" + 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")", + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" ); } return \%voice_cmds; @@ -1036,7 +1039,8 @@ Provides support for the Insteon FanLinc. =head2 INHERITS L, -L +L, +L =head2 METHODS @@ -1049,7 +1053,7 @@ package Insteon::FanLinc; use strict; use Insteon::BaseInsteon; -@Insteon::FanLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController'); +@Insteon::FanLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController', 'Insteon::MultigroupDevice'); =item C @@ -1178,6 +1182,36 @@ sub is_acknowledged } } +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" + ); + } + return \%voice_cmds; +} + =back =head2 AUTHOR diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index f7a6eef45..b66a7e14a 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -569,7 +569,7 @@ sub simple_message { package Insteon::Thermo_i2CS; use strict; -@Insteon::Thermo_i2CS::ISA = ('Insteon::Thermostat'); +@Insteon::Thermo_i2CS::ISA = ('Insteon::Thermostat', 'Insteon::MultigroupDevice'); our %message_types = ( %Insteon::Thermostat::message_types, @@ -1186,9 +1186,16 @@ sub get_voice_cmds my ($self) = @_; my $object_name = $self->get_object_name; my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds}, - 'sync time' => "$object_name->sync_time()" + %{$self->SUPER::get_voice_cmds} ); + if ($self->is_root){ + %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'sync time' => "$object_name->sync_time()", + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" + ); + } return \%voice_cmds; } From 65b63b0c1a80128d925da34478f61472f0842d6a Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 25 Oct 2013 17:39:00 -0700 Subject: [PATCH 300/330] Insteon: Multigroup Sync All Links to Scan Root Object Not sure why I omitted this before. --- lib/Insteon/BaseInsteon.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 82dacfa90..daa008757 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2797,7 +2797,7 @@ sub sync_all_links ::print_log("[Insteon::MultigroupDevice] Sync All Links on device " .$self->get_object_name . " starting ..."); # Find all subgroup items check groups from 02 - FF; - for ($dec_group = 02; $dec_group <= 255; $dec_group++) { + for ($dec_group = 01; $dec_group <= 255; $dec_group++) { $group = sprintf("%02X", $dec_group); $subgroup_object = Insteon::get_object($device_id, $group); if (ref $subgroup_object){ From 097d46320e3a3c93334b3379f59302516d9527b4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sun, 27 Oct 2013 12:39:00 -0700 Subject: [PATCH 301/330] Insteon: Set Failure Callback for ALDB Query This way if device is unreachable, we still continue on with the scan --- lib/Insteon.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index b976387e4..a0341b3f2 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -458,6 +458,7 @@ sub _get_next_linkscan ## check if aldb_delta has changed; $current_scan_device->_aldb->{_aldb_unchanged_callback} = '&Insteon::_get_next_linkscan('.$skip_unchanged.')'; $current_scan_device->_aldb->{_aldb_changed_callback} = '&Insteon::_get_next_linkscan('.$skip_unchanged.', '.$current_scan_device->get_object_name.')'; + $current_scan_device->_aldb->{_failure_callback} = '&Insteon::_get_next_linkscan_failure('.$skip_unchanged.')'; $current_scan_device->_aldb->query_aldb_delta("check"); $checking = 1; } From ec5311c14433ebac25bcf56c2b886d8dceee43c2 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 4 Nov 2013 17:39:00 -0800 Subject: [PATCH 302/330] Display Git Build Number and Date for Unstable Branch If running unstable, Mh will now try and determine a build number and modification date by calling git commands. --- bin/mh | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/bin/mh b/bin/mh index e85103685..404bf1028 100755 --- a/bin/mh +++ b/bin/mh @@ -70,6 +70,14 @@ BEGIN { $Version = $autover || 'unknown'; $Version .= " (compiler: $Info{Perl_compiled})" if $Info{Perl_compiled}; + #Create a build number if this is unstable version + if (lc $Version eq 'unstable'){ + my $build = lc `git describe --long`; + $build =~ /(\S+)-(\d+)-g([0-9a-f]+)/; + $Version = "$1 Build $2 ($3)" if ($1 && $2 && $3); + } + + # $Pgm_Path = '.'; $usage = <<"eof"; @@ -213,9 +221,23 @@ BEGIN { sub print_version { - $Version_date = localtime((stat $0)[9]); + $Version_date = localtime((stat $0)[9]); #this script as default $Version_date = localtime((stat "$Pgm_Path/bin/$0.exe")[9]) if $PerlApp::BUILD; $Version_date = localtime((stat $ENV{sourceExe})[9]) if $ENV{sourceExe}; # Older 3.x perl2exe + $Version_date = localtime((stat '../VERSION')[9]) if (-e '../VERSION'); #if VERSION file exists use that + + #Get version date for unstable branch + my $autover; + if (-e '../VERSION') { + open (VERSION, '../VERSION'); + $autover = ; + chomp $autover; + close (VERSION); + } + if (lc $autover eq 'unstable'){ + my $build_date = `git show -s --format="%ci"`; + $Version_date = $build_date if ($build_date=~/^\d\d\d\d-\d\d-\d\d/); + } $Pgm_PathU = '.'; # Since we now chdir, this is obsolete, but still used in some user code :( $Pgm_Root = './..'; @@ -249,7 +271,7 @@ BEGIN { print "\nCommand: $Pgm_Name @ARGV\n"; print "Pgm path : $Pgm_Path\n"; - print "Pgm version: $Version Last updated: $Version_date\n"; + print "Pgm version: $Version \nLast updated: $Version_date\n"; $Info{Perl_version} = $]; # Use eval to avoid problems with earlier version (e.g. build 502) $Info{Perl_version} .= " Build " . eval "&Win32::BuildNumber()" if $OS_win; From 9c8f29298af218a5b67a936683d8c92d4ae5e80e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Tue, 5 Nov 2013 17:39:00 -0800 Subject: [PATCH 303/330] Remove Switch/Case Statement, Incompatible with Perl 5.8 For some reason it appears that Swtich.pm version 2.10 which was distributed in perl 5.8.x has some egregious bugs. In order to enable backwards compatibility, these have been converted to if/elsif/else statements --- lib/Insteon/BaseInsteon.pm | 223 ++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 115 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index b1b7259dc..c25350b06 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -25,7 +25,6 @@ L package Insteon::BaseObject; -use Switch; use strict; use Insteon::AllLinkDatabase; @@ -1412,62 +1411,60 @@ sub link_to_interface my $failure_callback = '::print_log("[Insteon::BaseInsteon] Error: The Link_To_Interface '. 'routine failed for device: '.$self->get_object_name.'")'; $step = 0 if ($step eq ''); - switch ($step){ - case (0) { #If NAK on get_engine, then this is an I2CS device - $success_callback = $success_callback_prefix . "\"1\")"; - $failure_callback = $self->get_object_name."->link_to_interface_i2cs(\"$p_group\",\"$p_data3\")"; - $self->get_engine_version($success_callback, $failure_callback); - } - case (1) { #Add Link from object->PLM - $success_callback = $success_callback_prefix . "\"2\")"; - my %link_info = ( object => $self->interface, group => $p_group, is_controller => 1, - callback => "$success_callback", failure_callback=> "$failure_callback"); - $link_info{data3} = $p_data3 if $p_data3; - if ($self->_aldb) { - $self->_aldb->add_link(%link_info); - } - else - { - &main::print_log("[Insteon::BaseInsteon] Error: This item, " . $self->get_object_name . - ", does not have an ALDB object. Linking is not permitted."); - } - } - case (2){ #Add Link from PLM->object - $success_callback = $success_callback_prefix . "\"3\")"; - my $link_info = "deviceid=" . lc $self->device_id . " group=$p_group is_controller=0 " . - "callback=$success_callback failure_callback=$failure_callback"; - $self->interface->add_link($link_info); - } - case (3){ #Add surrogate link on device if surrogate exists - if (ref $$self{surrogate}){ - $success_callback = $success_callback_prefix . "\"4\")"; - my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( object => $self->interface, - group => $surrogate_group, is_controller => 0, - callback => "$success_callback", - failure_callback=> "$failure_callback", - data3 => $p_group); - $self->_aldb->add_link(%link_info); - } else { - ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. - ' for device ' .$self->get_object_name); - } - } - case (4){ #Add surrogate link on PLM if surrogate exists - $success_callback = $success_callback_prefix . "\"5\")"; + if ($step == 0) { #If NAK on get_engine, then this is an I2CS device + $success_callback = $success_callback_prefix . "\"1\")"; + $failure_callback = $self->get_object_name."->link_to_interface_i2cs(\"$p_group\",\"$p_data3\")"; + $self->get_engine_version($success_callback, $failure_callback); + } + elsif ($step == 1) { #Add Link from object->PLM + $success_callback = $success_callback_prefix . "\"2\")"; + my %link_info = ( object => $self->interface, group => $p_group, is_controller => 1, + callback => "$success_callback", failure_callback=> "$failure_callback"); + $link_info{data3} = $p_data3 if $p_data3; + if ($self->_aldb) { + $self->_aldb->add_link(%link_info); + } + else + { + &main::print_log("[Insteon::BaseInsteon] Error: This item, " . $self->get_object_name . + ", does not have an ALDB object. Linking is not permitted."); + } + } + elsif ($step == 2){ #Add Link from PLM->object + $success_callback = $success_callback_prefix . "\"3\")"; + my $link_info = "deviceid=" . lc $self->device_id . " group=$p_group is_controller=0 " . + "callback=$success_callback failure_callback=$failure_callback"; + $self->interface->add_link($link_info); + } + elsif ($step == 3){ #Add surrogate link on device if surrogate exists + if (ref $$self{surrogate}){ + $success_callback = $success_callback_prefix . "\"4\")"; my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( deviceid=> lc $self->device_id, - group => $surrogate_group, is_controller => 1, + my %link_info = ( object => $self->interface, + group => $surrogate_group, is_controller => 0, callback => "$success_callback", failure_callback=> "$failure_callback", - data3 => $surrogate_group); - $self->interface->add_link(%link_info); - } - case (5){ + data3 => $p_group); + $self->_aldb->add_link(%link_info); + } else { ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. - ' for device ' .$self->get_object_name); + ' for device ' .$self->get_object_name); } } + elsif ($step == 4){ #Add surrogate link on PLM if surrogate exists + $success_callback = $success_callback_prefix . "\"5\")"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( deviceid=> lc $self->device_id, + group => $surrogate_group, is_controller => 1, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $surrogate_group); + $self->interface->add_link(%link_info); + } + elsif ($step == 5){ + ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. + ' for device ' .$self->get_object_name); + } } =item C @@ -1488,21 +1485,19 @@ sub link_to_interface_i2cs my $failure_callback = "::print_log('[Insteon::BaseInsteon] Error Link_To_Interface_I2CS ". "routine failed for device: ".$self->get_object_name."')"; $step = 0 if ($step eq ''); - switch ($step){ - case (0) { #Put PLM into initiate linking mode - $success_callback = $success_callback_prefix . "'1')"; - $self->interface()->initiate_linking_as_controller('00', $success_callback, $failure_callback); - } - case (1) { #Ask device to respond to link request - $success_callback = $success_callback_prefix . "'2')"; - $self->enter_linking_mode($p_group, $success_callback, $failure_callback); - } - case (2) { #Scan device to get an accurate link table - #return to normal link_to_interface routine if successful - $success_callback_prefix = $self->get_object_name."->link_to_interface('$p_group','$p_data3',"; - $success_callback = $success_callback_prefix . "'1')"; - $self->scan_link_table($success_callback, $failure_callback); - } + if ($step == 0) { #Put PLM into initiate linking mode + $success_callback = $success_callback_prefix . "'1')"; + $self->interface()->initiate_linking_as_controller('00', $success_callback, $failure_callback); + } + elsif ($step == 1) { #Ask device to respond to link request + $success_callback = $success_callback_prefix . "'2')"; + $self->enter_linking_mode($p_group, $success_callback, $failure_callback); + } + elsif ($step == 2) { #Scan device to get an accurate link table + #return to normal link_to_interface routine if successful + $success_callback_prefix = $self->get_object_name."->link_to_interface('$p_group','$p_data3',"; + $success_callback = $success_callback_prefix . "'1')"; + $self->scan_link_table($success_callback, $failure_callback); } } @@ -1529,62 +1524,60 @@ sub unlink_to_interface my $failure_callback = "::print_log('[Insteon::BaseInsteon] ERROR: Unlink_To_Interface ". "failed for device: ".$self->get_object_name."')"; $step = 0 if ($step eq ''); - switch ($step){ - case (0) { #Delete link on the device - if ($self->_aldb) { - $success_callback = $success_callback_prefix . "'1')"; - $self->_aldb->delete_link(object => $self->interface, - group => $p_group, - data3=> $p_group, is_controller => 1, - callback => $success_callback, - failure_callback=> $failure_callback); - } - else - { - &main::print_log("[BaseInsteon] This item " . $self->get_object_name . - " does not have an ALDB object. Unlinking is not permitted."); - } - } - case (1) { #Delete link on the PLM - $success_callback = $success_callback_prefix . "'2')"; - $self->interface->delete_link( - deviceid => lc $self->device_id, - group=> $p_group, is_controller=>0, - callback=>$success_callback, - failure_callback=>$failure_callback); - } - case (2){ #Delete surrogate link on device if surrogate exists - if (ref $$self{surrogate}){ - $success_callback = $success_callback_prefix . "'3')"; - my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( object => $self->interface, - group => $surrogate_group, is_controller => 0, - callback => "$success_callback", - failure_callback=> "$failure_callback", - data3 => $p_group); - $self->_aldb->delete_link(%link_info); - } else { - ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". - " successfully completed for device " - . $self->get_object_name); - } - } - case (3){ #Delete surrogate link on PLM if surrogate exists - $success_callback = $success_callback_prefix . "'4')"; + if ($step == 0) { #Delete link on the device + if ($self->_aldb) { + $success_callback = $success_callback_prefix . "'1')"; + $self->_aldb->delete_link(object => $self->interface, + group => $p_group, + data3=> $p_group, is_controller => 1, + callback => $success_callback, + failure_callback=> $failure_callback); + } + else + { + &main::print_log("[BaseInsteon] This item " . $self->get_object_name . + " does not have an ALDB object. Unlinking is not permitted."); + } + } + elsif ($step == 1) { #Delete link on the PLM + $success_callback = $success_callback_prefix . "'2')"; + $self->interface->delete_link( + deviceid => lc $self->device_id, + group=> $p_group, is_controller=>0, + callback=>$success_callback, + failure_callback=>$failure_callback); + } + elsif ($step == 2){ #Delete surrogate link on device if surrogate exists + if (ref $$self{surrogate}){ + $success_callback = $success_callback_prefix . "'3')"; my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( deviceid=> lc $self->device_id, - group => $surrogate_group, is_controller => 1, + my %link_info = ( object => $self->interface, + group => $surrogate_group, is_controller => 0, callback => "$success_callback", failure_callback=> "$failure_callback", - data3 => $surrogate_group); - $self->interface->delete_link(%link_info); - } - case (4) { + data3 => $p_group); + $self->_aldb->delete_link(%link_info); + } else { ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". " successfully completed for device " - . $self->get_object_name); + . $self->get_object_name); } } + elsif ($step == 3){ #Delete surrogate link on PLM if surrogate exists + $success_callback = $success_callback_prefix . "'4')"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( deviceid=> lc $self->device_id, + group => $surrogate_group, is_controller => 1, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $surrogate_group); + $self->interface->delete_link(%link_info); + } + elsif ($step == 4) { + ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". + " successfully completed for device " + . $self->get_object_name); + } } =item C From 2771d11dbf3cdfa7c23d7cc03b033291c6bfb54f Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Wed, 6 Nov 2013 13:21:36 -0800 Subject: [PATCH 304/330] Allow a Build Number of 0 Check if build number is defined, not if true, as in extremely rare case build number may be 0. --- bin/mh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mh b/bin/mh index 404bf1028..3eb52899e 100755 --- a/bin/mh +++ b/bin/mh @@ -74,7 +74,7 @@ BEGIN { if (lc $Version eq 'unstable'){ my $build = lc `git describe --long`; $build =~ /(\S+)-(\d+)-g([0-9a-f]+)/; - $Version = "$1 Build $2 ($3)" if ($1 && $2 && $3); + $Version = "$1 Build $2 ($3)" if ($1 && defined($2) && $3); } From 762b9adb412678ba2a73442a43c9a6354714d7b9 Mon Sep 17 00:00:00 2001 From: JaredF Date: Wed, 6 Nov 2013 16:08:02 -0800 Subject: [PATCH 305/330] Adds INSTEON_TRIGGERLINC Device to 'Edit Items' Page in Web Interface --- web/bin/items.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/bin/items.pl b/web/bin/items.pl index 8fbf3c409..cc224b435 100644 --- a/web/bin/items.pl +++ b/web/bin/items.pl @@ -67,7 +67,7 @@ sub web_items_list { 'EIB Value (EIB5)', 'EIB Drive (EIB7)', 'GENERIC', 'GROUP', 'IBUTTON', 'INSTEON_PLM','INSTEON_LAMPLINC','INSTEON_APPLIANCELINC', 'INSTEON_SWITCHLINC','INSTEON_SWITCHLINCRELAY','INSTEON_KEYPADLINC','INSTEON_KEYPADLINCRELAY', - 'INSTEON_REMOTELINC','INSTEON_MOTIONSENSOR','INSTEON_ICONTROLLER', + 'INSTEON_REMOTELINC','INSTEON_MOTIONSENSOR','INSTEON_TRIGGERLINC','INSTEON_ICONTROLLER', 'MP3PLAYER', 'One-Wire xAP Connector (OWX)', 'RF', 'SERIAL', 'SG485LCD', 'SG485RCSTHRM', 'STARGATEDIN', 'STARGATEVAR', 'STARGATEFLAG', 'STARGATERELAY', 'STARGATETHERM', 'STARGATEPHONE', @@ -148,6 +148,7 @@ sub web_items_list { INSTEON_KEYPADLINCRELAY => [qw(Address Name Groups)], INSTEON_REMOTELINC => [qw(Address Name Groups)], INSTEON_MOTIONSENSOR => [qw(Address Name Groups)], + INSTEON_TRIGGERLINC => [qw(Address Name Groups)], INSTEON_ICONTROLLER => [qw(Address Name Groups)], SCENE_MEMBER => [qw(MemberName LinkName OnLevel RampRate)], default => [qw(Address Name Groups Other)] From 1345a22873edd32e5d6009392130e3be4576488b Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 7 Nov 2013 17:25:00 -0800 Subject: [PATCH 306/330] Insteon: Remove extraneous commented code from Insteon.pm If we don't remove the code now, we will likely never do it. --- lib/Insteon.pm | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index a2000bd70..2da44a1ed 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -917,11 +917,7 @@ sub debuglevel my ($object, $debug_level) = @_; $debug_level = 1 unless $debug_level; my $objname; - #try { $objname = lc $object->get_object_name if defined $object; - #} catch { - # &::print_log("$object doesn't have a get_object_name function.") if $main::Debug{insteon} >= 2; - #} &::print_log("Insteon::debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; return 1 if $main::Debug{insteon} >= $debug_level; return 1 if defined $objname && $main::Debug{$objname} >= $debug_level; @@ -1132,7 +1128,6 @@ sub check_thermo_versions bless $thermo_device, 'Insteon::Thermo_i1'; } } - #main::print_log("[Insteon] DEBUG4 Checking thermostat version of all devices completed") if ($self->debuglevel(4)); } =back From 52e97ab547b960c86fbdbdbb4eb87644e50bcb14 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 7 Nov 2013 17:35:00 -0800 Subject: [PATCH 307/330] Insteon: Convert DebugLevel Entries to Parent Device ALDB objects have no real name, and as such, a user would not be able to add them to a debug line in the ini. Moreover, if a user puts a device in the debug line, they likely expect to get all relevant messages, including ALDB messages for this device. Removed debuglevel routine as it is unnecessary. --- lib/Insteon/AllLinkDatabase.pm | 121 +++++++++++++++------------------ 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 9ab9d79f8..3e3396096 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -145,7 +145,7 @@ sub query_aldb_delta $self->{_aldb_changed_callback} = undef; eval ($callback); &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDatabase; } } elsif ($action eq "check" && ((&main::get_tickcount - $self->scandatetime()) <= 2000)){ @@ -160,7 +160,7 @@ sub query_aldb_delta $self->{_aldb_unchanged_callback} = undef; eval ($callback); &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDatabase; } } else { @@ -209,7 +209,6 @@ sub restore_string } $aldb .= $record; } -# &::print_log("[AllLinkDataBase] aldb restore string: $aldb") if $self->debuglevel(); if (defined $self->scandatetime) { $restore_string .= $$self{device}->get_object_name . "->_aldb->scandatetime(q~" . $self->scandatetime . "~) if " @@ -337,7 +336,7 @@ sub delete_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDatabase; } } elsif ($link_parms{address} && $link_parms{aldb_check} eq "ok") @@ -395,7 +394,7 @@ sub delete_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] error encountered during delete_link callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDataBase; } } @@ -561,7 +560,7 @@ sub delete_orphan_links if ($group eq '01' || $group eq '00') { #ignore manual responder link to PLM group 01 or 00 required for I2CS devices main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01 or 00") if $self->debuglevel(2); + . $selfname . " to PLM for group 01 or 00") if $self->{device}->debuglevel(2); } elsif ($audit_mode) { @@ -857,7 +856,7 @@ sub _process_delete_queue { else { &::print_log("[Insteon::AllLinkDatabase] Nothing else to do for " . $$self{device}->get_object_name . " after deleting " - . $$self{delete_queue_processed} . " links") if $self->debuglevel(); + . $$self{delete_queue_processed} . " links") if $self->{device}->debuglevel(); $$self{device}->interface->_aldb->_process_delete_queue($$self{delete_queue_processed}); } } @@ -978,10 +977,10 @@ sub get_first_empty_address } $first_address = ($high_address > 0) ? sprintf('%04x', $high_address - 8) : 0; main::print_log("[Insteon::AllLinkDatabase] DEBUG4: No empty link entries; using next lowest link address [" - .$first_address."]") if $self->debuglevel(4); + .$first_address."]") if $self->{device}->debuglevel(4); } else { main::print_log("[Insteon::AllLinkDatabase] DEBUG4: Found empty address [" - .$first_address."] in empty array") if $self->debuglevel(4); + .$first_address."] in empty array") if $self->{device}->debuglevel(4); } return $first_address; @@ -1047,7 +1046,7 @@ sub add_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDatabase; } } @@ -1063,7 +1062,7 @@ sub add_link eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDatabase; } } @@ -1086,7 +1085,7 @@ sub add_link &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") - if $self->debuglevel(2); + if $self->{device}->debuglevel(2); my ($data1, $data2); if($link_parms{is_controller}) { $data1 = '03'; #application retries == 3 @@ -1106,14 +1105,14 @@ sub add_link . $$self{device}->get_object_name . " does not have a record of the first empty ALDB record." . " Please rescan this device's link table") - if $self->debuglevel(); + if $self->{device}->debuglevel(); if ($$self{_success_callback}) { package main; eval ($$self{_success_callback}); &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->{device}->debuglevel(1); package Insteon::AllLinkDatabase; } } @@ -1145,7 +1144,7 @@ sub update_link my $ramp_rate = $link_parms{ramp_rate}; $ramp_rate =~ s/(\d)s?/$1/; &::print_log("[Insteon::AllLinkDatabase] updating " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name - . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->debuglevel(); + . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->{device}->debuglevel(); my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; @@ -1171,7 +1170,7 @@ sub update_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon::AllLinkDatabase; } } @@ -1185,14 +1184,14 @@ sub update_link &::print_log("[Insteon::AllLinkDatabase] ERROR: updating link record failed because " . $$self{device}->get_object_name . " does not have an existing ALDB entry key=$key") - if $self->debuglevel(); + if $self->{device}->debuglevel(); if ($$self{_success_callback}) { package main; eval ($$self{_success_callback}); &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->{device}->debuglevel(1); package Insteon::AllLinkDatabase; } } @@ -1362,18 +1361,6 @@ sub has_link return (defined $$self{aldb}{$key}); } -=item C - -Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. - -=cut - -sub debuglevel -{ - my ($self, $debug_level) = @_; - return Insteon::debuglevel(undef, $debug_level); -} - =back =head2 INI PARAMETERS @@ -1592,7 +1579,7 @@ sub _on_peek my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); if ($msg{is_extended}) { &::print_log("[Insteon::ALDB_i1]: extended peek for " . $$self{device}->{object_name} - . " is " . $msg{extra}) if $self->debuglevel(); + . " is " . $msg{extra}) if $self->{device}->debuglevel(); } else { @@ -1644,7 +1631,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); my $flag = hex($msg{extra}); $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; @@ -1674,7 +1661,7 @@ sub _on_peek } &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " completed link memory scan") - if $self->debuglevel(); + if $self->{device}->debuglevel(); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -1724,7 +1711,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{pending_aldb}{group} = lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devhi'; @@ -1744,7 +1731,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{pending_aldb}{deviceid} = lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devmid'; @@ -1765,7 +1752,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{pending_aldb}{deviceid} .= lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devlo'; @@ -1786,7 +1773,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{pending_aldb}{deviceid} .= lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_data1'; @@ -1809,7 +1796,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{_mem_action} = 'aldb_data2'; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{pending_aldb}{data1} = $msg{extra}; @@ -1832,7 +1819,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{pending_aldb}{data2} = $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_data3'; @@ -1855,7 +1842,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{pending_aldb}{data3} = $msg{extra}; if ($$self{_stress_test_act}){ @@ -1937,7 +1924,7 @@ sub _on_peek else { ::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) - if $self->debuglevel(); + if $self->{device}->debuglevel(); } } } @@ -2020,7 +2007,7 @@ sub _write_link if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) { &::print_log("[Insteon::ALDB_i1] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $self->debuglevel(); + if $self->{device}->debuglevel(); $data3 = $$self{device}->group; } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; @@ -2036,7 +2023,7 @@ sub _write_link package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i1] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->{device}->debuglevel(1); package Insteon::ALDB_i1; } } @@ -2144,7 +2131,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for _mem_activity=".$$self{_mem_activity} - ." _mem_action=". $$self{_mem_action}) if $self->debuglevel(3); + ." _mem_action=". $$self{_mem_action}) if $self->{device}->debuglevel(3); if ($$self{_mem_action} eq 'aldb_i2read') { @@ -2156,12 +2143,12 @@ sub on_read_write_aldb $$self{_mem_action} = 'aldb_i2readack'; &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received ack") - if $self->debuglevel(3); + if $self->{device}->debuglevel(3); } else { #otherwise just ignore the message because it is out of sequence &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] ack not received. " - . "ignoring message") if $self->debuglevel(3); + . "ignoring message") if $self->{device}->debuglevel(3); } } @@ -2170,14 +2157,14 @@ sub on_read_write_aldb if($msg{is_ack}) { &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received duplicate ack. Ignoring.") - if $self->debuglevel(3); + if $self->{device}->debuglevel(3); $clear_message = 0; } elsif(length($msg{extra})<30) { &::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #can't clear message, if valid message doesn't arrive #resend logic will kick in @@ -2188,7 +2175,7 @@ sub on_read_write_aldb . " address received did not match address requested: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #can't clear message, if valid message doesn't arrive #resend logic will kick in @@ -2206,7 +2193,7 @@ sub on_read_write_aldb if (lc $$self{_mem_msb} eq '00' and lc $$self{_mem_lsb} eq '00') { main::print_log("[Insteon::ALDB_i2] DEBUG4: Start of scan; initializing aldb structure") - if $self->debuglevel(4); + if $self->{device}->debuglevel(4); # reinit the aldb hash as there will be a new one $$self{aldb} = undef; # reinit the empty address list @@ -2248,10 +2235,10 @@ sub on_read_write_aldb . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) - if(($$self{pending_aldb}{inuse}) and $self->debuglevel(3)); + if(($$self{pending_aldb}{inuse}) and $self->{device}->debuglevel(3)); main::print_log("[Insteon::ALDB_i2] DEBUG4: scan done; adding last address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $self->debuglevel(4); + if $self->{device}->debuglevel(4); $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); # scan done; clear out state flags $$self{_mem_action} = undef; @@ -2268,7 +2255,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " completed link memory scan: status: " . $self->health()) - if $self->debuglevel(); + if $self->{device}->debuglevel(); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -2279,7 +2266,7 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] DEBUG4: inuse flag == false; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $self->debuglevel(4); + if $self->{device}->debuglevel(4); $self->add_empty_address($$self{pending_aldb}{address}); } else @@ -2305,14 +2292,14 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] DEBUG4: duplicate link found; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to duplicates array") - if $self->debuglevel(4); + if $self->{device}->debuglevel(4); $self->add_duplicate_link_address($$self{pending_aldb}{address}); } else { main::print_log("[Insteon::ALDB_i2] DEBUG4: active link found; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to aldb") - if $self->debuglevel(4); + if $self->{device}->debuglevel(4); %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; } } @@ -2352,7 +2339,7 @@ sub on_read_write_aldb $$self{pending_aldb} = undef; main::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " link write completed for [".$$self{aldb}{$aldbkey}{address}."]") - if $self->debuglevel(3); + if $self->{device}->debuglevel(3); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -2385,7 +2372,7 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) - if $self->debuglevel(); + if $self->{device}->debuglevel(); $clear_message = 0; } return $clear_message; @@ -2432,7 +2419,7 @@ sub _write_link if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) { &::print_log("[Insteon::ALDB_i2] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $self->debuglevel(); + if $self->{device}->debuglevel(); $data3 = $$self{device}->group; } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; @@ -2453,7 +2440,7 @@ sub _write_link package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->{device}->debuglevel(1); package Insteon::ALDB_i2; } } @@ -2503,7 +2490,7 @@ sub _write_delete package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->{device}->debuglevel(1); package Insteon::ALDB_i2; } } @@ -2783,7 +2770,7 @@ sub delete_orphan_links &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan Link to non-existant deviceid: " . $deviceid . "; group:$group; " . (($is_controller) ? "controller; data:$data3" : "responder")) - if $self->debuglevel(); + if $self->{device}->debuglevel(); } else { @@ -2808,13 +2795,13 @@ sub delete_orphan_links if ($group eq '01' || $group eq '00') { #ignore manual controller link from PLM group 01 or 00 to device required for I2CS devices main::print_log("[Insteon::ALDB_PLM] DEBUG2 Ignoring orphan PLM controller(01 or 00) link to " - . $device->get_object_name() ) if $self->debuglevel(2); + . $device->get_object_name() ) if $self->{device}->debuglevel(2); } elsif ($audit_mode) { &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan PLM controller link ($group) to: " . $device->get_object_name() . "($data3)") - if $self->debuglevel(); + if $self->{device}->debuglevel(); } else { @@ -2938,7 +2925,7 @@ sub _process_delete_queue { . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") - if $self->debuglevel(); + if $self->{device}->debuglevel(); $self->delete_link(%delete_req); } elsif ($delete_req{linkdevice}) @@ -3008,7 +2995,7 @@ sub delete_link package main; eval ($link_parms{callback}); &::print_log("[Insteon_PLM] error in add link callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon_PLM; } } @@ -3059,7 +3046,7 @@ sub add_link package main; eval ($link_parms{callback}); &::print_log("[Insteon::ALDB_PLM] error in add link callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->{device}->debuglevel(); package Insteon_PLM; } } From b1241bed7520b1e7ab2bc06ee9ad0ddc9de34f4d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Nov 2013 17:20:00 -0800 Subject: [PATCH 308/330] Insteon: Removed Debuglevel routine from Insteon.pm Neither the Insteon Manager nor the non-object oriented Insteon.pm have user friendly names that can be put into the debug parameter. As a result adding a debuglevel routine which really just resulted in returning the global insteon debug level only complicated things. --- lib/Insteon.pm | 38 ++++---------------------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 2da44a1ed..2345dc989 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -906,24 +906,6 @@ sub init { } -=item C - -Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. - -=cut - -sub debuglevel -{ - my ($object, $debug_level) = @_; - $debug_level = 1 unless $debug_level; - my $objname; - $objname = lc $object->get_object_name if defined $object; - &::print_log("Insteon::debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; - return 1 if $main::Debug{insteon} >= $debug_level; - return 1 if defined $objname && $main::Debug{$objname} >= $debug_level; - return 0; -} - =item C Generates and sets the voice commands for all Insteon devices. @@ -1079,7 +1061,7 @@ Walks through every Insteon device and checks the aldb object version for I1 vs. sub check_all_aldb_versions { - main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices") if Insteon::debuglevel(undef,4); + main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices") if ($main::Debug{insteon} >= 4); my @ALDB_devices = (); push @ALDB_devices, Insteon::find_members("Insteon::BaseDevice"); @@ -1102,12 +1084,12 @@ sub check_all_aldb_versions if ($ALDB_device->debuglevel(4)); } } - main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if Insteon::debuglevel(undef,4); + main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); } sub check_thermo_versions { - main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if Insteon::debuglevel(undef,4); + main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if ($main::Debug{insteon} >= 4); my @thermo_devices = (); push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); @@ -1203,7 +1185,7 @@ sub _active_interface my ($self, $interface) = @_; # setup hooks the first time that an interface is made active if (!($$self{active_interface}) and $interface) { - &main::print_log("[Insteon] Setting up initialization hooks") if $self->debuglevel(); + &main::print_log("[Insteon] Setting up initialization hooks") if $main::Debug{insteon}; &main::MainLoop_pre_add_hook(\&Insteon::BaseInterface::check_for_data, 1); &main::Reload_post_add_hook(\&Insteon::check_all_aldb_versions, 1); &main::Reload_post_add_hook(\&Insteon::BaseInterface::poll_all, 1); @@ -1216,18 +1198,6 @@ sub _active_interface return $$self{active_interface}; } -=item C - -Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. - -=cut - -sub debuglevel -{ - my ($self, $debug_level) = @_; - return Insteon::debuglevel(undef, $debug_level); -} - =item C Adds a list of objects to be tracked. From 48e7f075f5a5cafc3243fd0ef0772669497f62c3 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Nov 2013 17:28:00 -0800 Subject: [PATCH 309/330] Insteon: Move Debuglevel into BaseObject, Re-Assign Debuglevel in BaseInterface Trying to attach the debug message to the most logical object. Whenever possible, trying to avoid having the PLM be the "catch-all" object. --- lib/Insteon/BaseInsteon.pm | 10 ++++++++-- lib/Insteon/BaseInterface.pm | 24 ++++++++++++++---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index faeb92099..1adc93bd4 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -214,8 +214,14 @@ Returns 1 if insteon or this device is at least debug level 'level', otherwise r sub debuglevel { - my ($self, $debug_level) = @_; - return Insteon::debuglevel($self, $debug_level); + my ($object, $debug_level) = @_; + $debug_level = 1 unless $debug_level; + my $objname; + $objname = lc $object->get_object_name if defined $object; + ::print_log("[Insteon] debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; + return 1 if $main::Debug{insteon} >= $debug_level; + return 1 if defined $objname && $main::Debug{$objname} >= $debug_level; + return 0; } =item C diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 85ab3b813..1707fa2f9 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -129,7 +129,7 @@ Returns 1 if Insteon or this device is at least debug level 'level', otherwise r sub debuglevel { my ($self, $debug_level) = @_; - return Insteon::debuglevel($self, $debug_level); + return Insteon::BaseObject::debuglevel($self, $debug_level); } =item C<_is_duplicate(cmd)> @@ -576,7 +576,7 @@ sub on_standard_insteon_received if($object->_process_message($self, %msg)) { if ($self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $self->debuglevel(4); + . $self->active_message->success_callback) if $object->debuglevel(4); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; @@ -606,11 +606,11 @@ sub on_standard_insteon_received if (($msg{extra} == $self->active_message->setby->group)){ &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received for scene " . $object->get_object_name . " from " . $setby_object->get_object_name) - if $self->debuglevel(3); + if $object->debuglevel(3); } elsif ($self->active_message->command_type eq 'all_link_direct_cleanup' && lc($self->active_message->setby->device_id) eq $msg{source}) { - &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $self->debuglevel(2); + &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $object->debuglevel(2); $self->clear_active_message(); } else { @@ -619,7 +619,7 @@ sub on_standard_insteon_received . $object->get_object_name . ", but group in recent message " . $msg{extra}. " did not match group in " . "prior sent message group " . $self->active_message->setby->group) - if $self->debuglevel(3); + if $object->debuglevel(3); } # If ACK or NACK received then PLM is still working on the ALL Link Command # Increase the command timeout to wait for next one @@ -655,9 +655,13 @@ sub on_standard_insteon_received # in the Insteon_PLM handler for cleanup messages. # however, if the virtual handler was not invoked due to receipt of the broadcast message # then, the above cleanup handler would be run + my $plm_group_obj = Insteon::get_object('000000', $msg{extra}); + my $group_name = $msg{extra}; + $group_name = $plm_group_obj->get_object_name if (ref $plm_group_obj); + $plm_group_obj = $self if (!ref $plm_group_obj); &main::print_log("[Insteon::BaseInterface] DEBUG3: received cleanup message responding to " - . "PLM controller group: $msg{extra}. Ignoring as this has already been processed") - if $self->debuglevel(3); + . "PLM controller group: $group_name. Ignoring as this has already been processed") + if $plm_group_obj->debuglevel(3); } else { @@ -728,11 +732,11 @@ sub on_extended_insteon_received if( (!($msg{is_ack} or $msg{is_nack}) and $self->debuglevel()) or $self->debuglevel(3)); } - &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $self->debuglevel(3); + &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $object->debuglevel(3); if($object->_process_message($self, %msg)) { if (ref $self->active_message && $self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $self->debuglevel(4); + . $self->active_message->success_callback) if $object->debuglevel(4); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; @@ -891,7 +895,7 @@ sub _is_duplicate_received { $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); }; ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " - . $message_data . ", from $source.") if $self->debuglevel(); + . $message_data . ", from ". $object->get_object_name) if $object->debuglevel(); } else { #Message was not in hash, so add it $$self{received_commands}{$key} = $curr_milli + $delay; From cc2eb5eb8cedf2dde65b6021b1fe9fdc29068a4d Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Nov 2013 17:30:00 -0800 Subject: [PATCH 310/330] Insteon: Edit Debuglevel Calls in Message.pm Remove debuglevel routine, messages have no human readable names. All calls should be to the relevant object. Calls to X10 items are tricky, x10 items are defined outside of BaseObject and so do not inherit the necessary debuglevel routine (unless it is expanded to Generic_Item at some future date) --- lib/Insteon/Message.pm | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index eb95975fd..cf65bb0a7 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -136,18 +136,6 @@ sub send_attempts return $$self{send_attempts}; } -=item C - -Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. - -=cut - -sub debuglevel -{ - my ($self, $debug_level) = @_; - return Insteon::debuglevel(undef, $debug_level); -} - =item C Stores and retrieves what the source of this message was. @@ -245,7 +233,7 @@ sub send { &::print_log("[Insteon::BaseMessage] WARN: now resending " . $self->to_string() . " after " . $self->send_attempts - . " attempts.") if $self->debuglevel(); + . " attempts.") if $self->setby->debuglevel(); # revise default hop count to reflect retries if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') && !defined($$self{no_hop_increase})) @@ -260,7 +248,7 @@ sub send && $self->setby->isa('Insteon::BaseObject')){ &main::print_log("[Insteon::BaseMessage] Hop count not increased for " . $self->setby->get_object_name . " because no_hop_increase flag was set.") - if $self->debuglevel(); + if $self->setby->debuglevel(); $$self{no_hop_increase} = undef; } } @@ -1098,7 +1086,7 @@ sub generate_commands } if ($uc eq undef) { - &main::print_log("[Insteon::Message] Message is for entire HC") if Insteon::debuglevel(undef); + &main::print_log("[Insteon::Message] Message is for entire HC") if Insteon::BaseObject::debuglevel($p_setby,); } else { @@ -1107,7 +1095,7 @@ sub generate_commands $msg.= substr(unpack("H*",pack("C",$x10_unit_codes{substr($id,2,1)})),1,1); $msg.= "00"; &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $uc) . " as insteon msg: " - . $msg) if Insteon::debuglevel(undef); + . $msg) if Insteon::BaseObject::debuglevel($p_setby,); push @data, $msg; } @@ -1135,7 +1123,7 @@ sub generate_commands $msg.= "80"; &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $x10_arg) . " as insteon msg: " - . $msg) if Insteon::debuglevel(undef); + . $msg) if Insteon::BaseObject::debuglevel($p_setby,); push @data, $msg; From f746ba23c29bb62dd16745120992d1bbb777e8ac Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Nov 2013 17:41:00 -0800 Subject: [PATCH 311/330] Insteon: Rearrange Debuglevel Association in Insteon_PLM Attempt to remove as many messages from the PLM as possible. Avoid using it like a "catch-all". This is particularly difficult for incoming messages. --- lib/Insteon_PLM.pm | 54 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 019451663..3bc1060c5 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -390,7 +390,7 @@ sub _send_cmd { my $incurred_delay_time = $message->seconds_delayed; &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " . sprintf('%.2f',$incurred_delay_time) . " seconds; starting hop-count: " - . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $self->debuglevel(2); + . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $message->setby->debuglevel(2); if ($message->isa('Insteon::X10Message')) { # is x10; so, be slow $command = $prefix{x10_send} . $command; @@ -412,8 +412,8 @@ sub _send_cmd { } else { - &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $self->debuglevel(3); - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $message->setby->debuglevel(3); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $message->setby->debuglevel(4); my $data = pack("H*",$command); $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; @@ -492,7 +492,7 @@ sub _parse_data { $entered_ack_loop = 1; if ($parsed_data =~ /^($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($prefix{all_link_first_rec}15)|($prefix{all_link_next_rec}15)|($badcmd)$/) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->active_message->setby->debuglevel(4); my $ret_code = substr($parsed_data,length($parsed_data)-2,2); my $record_type = substr($parsed_data,0,4); my $message_data = substr($parsed_data,4,length($parsed_data)-4); @@ -515,7 +515,7 @@ sub _parse_data { package main; eval ($self->active_message->success_callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->active_message->setby->debuglevel(1); package Insteon_PLM; } # clear the active message because we're done @@ -524,7 +524,7 @@ sub _parse_data { else { &::print_log("[Insteon_PLM] DEBUG3: Received PLM acknowledge: " - . $pending_message->to_string) if $self->debuglevel(3); + . $pending_message->to_string) if $self->active_message->setby->debuglevel(3); } # X10 messages don't ACK back on the powerline, so clear them if the PLM acknowledges @@ -552,7 +552,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->active_message->setby->debuglevel(1); package Insteon_PLM; } } @@ -665,7 +665,6 @@ sub _parse_data { # is $parsed_data an accidental anomoly? (there are other cases; but, this is a good start) if ($parsed_data =~ /^($prefix{insteon_send}\w{12}06)|($prefix{insteon_send}\w{12}15)$/) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); # first, parse the content to confirm that it could be a legitimate ACK my $unknown_deviceid = substr($parsed_data,4,6); my $unknown_msg_flags = substr($parsed_data,10,2); @@ -674,6 +673,7 @@ sub _parse_data { my $unknown_obj = &Insteon::get_object($unknown_deviceid, '01'); if ($unknown_obj) { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $unknown_obj->debuglevel(4); &::print_log("[Insteon_PLM] WARN: encountered '$parsed_data' " . "from " . $unknown_obj->get_object_name() . " with command: $unknown_command, but expected '$ackcmd'."); @@ -681,6 +681,7 @@ sub _parse_data { } else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); &::print_log("[Insteon_PLM] ERROR: encountered '$parsed_data' " . "that does not match any known device ID (expected '$ackcmd')." . " Discarding received data."); @@ -710,7 +711,7 @@ sub _parse_data { { #ignore blanks.. the split does odd things next if $parsed_data eq ''; - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + if ($previous_parsed_data eq $parsed_data){ # guard against repeats ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $self->debuglevel(3); @@ -727,14 +728,29 @@ sub _parse_data { if ($parsed_prefix eq $prefix{insteon_received} and ($message_length == 22)) { #Insteon Standard Received + my $find_obj = Insteon::get_object(substr($parsed_data,4,6)); + if (ref $find_obj) { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4); + } + else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + } $self->on_standard_insteon_received($message_data); } elsif ($parsed_prefix eq $prefix{insteon_ext_received} and ($message_length == 50)) { #Insteon Extended Received + my $find_obj = Insteon::get_object(substr($parsed_data,4,6)); + if (ref $find_obj) { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4); + } + else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + } $self->on_extended_insteon_received($message_data); } elsif($parsed_prefix eq $prefix{x10_received} and ($message_length == 8)) { #X10 Received + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); my $x10_message = new Insteon::X10Message($parsed_data); my $x10_data = $x10_message->get_formatted_data(); &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $self->debuglevel(3); @@ -742,6 +758,7 @@ sub _parse_data { } elsif ($parsed_prefix eq $prefix{all_link_complete} and ($message_length == 20)) { #ALL-Linking Completed + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); my $link_address = substr($message_data,4,6); &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $self->debuglevel(2); if ($self->active_message->success_callback){ @@ -763,21 +780,23 @@ sub _parse_data { # bytes 0-1 - group; 2-7 device address my $failure_group = substr($message_data,0,2); my $failure_device = substr($message_data,2,6); - - &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure from device: " - . "$failure_device and group: $failure_group") if $self->debuglevel(2); - my $failed_object = &Insteon::get_object($failure_device,'01'); if (ref $failed_object){ + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $failed_object->debuglevel(4); + &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure from " . $failed_object->get_object_name + . " for all link group: $failure_group. Trying a direct cleanup.") if $failed_object->debuglevel(2); my $message = new Insteon::InsteonMessage('all_link_direct_cleanup', $failed_object, $self->active_message->command, $failure_group); push(@{$$failed_object{command_stack}}, $message); $failed_object->_process_command_stack(); } else { - &::print_log("[Insteon_PLM] WARN: Device ID: $failure_device does not exist. You may " + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log("[Insteon_PLM] Received all-link cleanup failure from an unkown device id: " + . "$failure_device and for all link group: $failure_group. You may " . "want to run delete orphans to remove this link from your PLM"); } } else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure." . " But there is no pending message.") if $self->debuglevel(2); } @@ -785,6 +804,7 @@ sub _parse_data { } elsif ($parsed_prefix eq $prefix{all_link_record} and ($message_length == 20)) { #ALL-Link Record Response + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $self->debuglevel(2); $self->_aldb->parse_alllink($message_data); # before doing the next, make sure that the pending command @@ -795,16 +815,18 @@ sub _parse_data { } elsif ($parsed_prefix eq $prefix{plm_user_reset} and ($message_length == 4)) { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); main::print_log("[Insteon_PLM] Detected PLM user reset to factory defaults"); } elsif ($parsed_prefix eq $prefix{all_link_clean_status} and ($message_length == 6)) { #ALL-Link Cleanup Status Report + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); my $cleanup_ack = substr($message_data,0,2); if ($cleanup_ack eq '15') { &::print_log("[Insteon_PLM] WARN1: All-link cleanup failure for scene: " . $self->active_message->setby->get_object_name . ". Retrying in 1 second.") - if $self->debuglevel(1); + if $self->active_message->setby->debuglevel(1); $self->retry_active_message(); # except that we should cause a bit of a delay to let things settle out $self->_set_timeout('xmit', 1000); @@ -814,7 +836,7 @@ sub _parse_data { { my $message_to_string = ($self->active_message) ? $self->active_message->to_string() : ""; &::print_log("[Insteon_PLM] Received all-link cleanup success: $message_to_string") - if $self->debuglevel(); + if $self->active_message->setby->debuglevel(); if (ref $self->active_message && ref $self->active_message->setby){ my $object = $self->active_message->setby; $object->is_acknowledged(1); From 23c0049f2a9424c6afaca85a19943cb684a9ccb4 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Nov 2013 17:41:00 -0800 Subject: [PATCH 312/330] Insteon: Fix Errors in Insteon_PLM, Get_Object Requires Group --- lib/Insteon_PLM.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 3bc1060c5..7089896ec 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -728,7 +728,7 @@ sub _parse_data { if ($parsed_prefix eq $prefix{insteon_received} and ($message_length == 22)) { #Insteon Standard Received - my $find_obj = Insteon::get_object(substr($parsed_data,4,6)); + my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); if (ref $find_obj) { &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4); } @@ -739,7 +739,7 @@ sub _parse_data { } elsif ($parsed_prefix eq $prefix{insteon_ext_received} and ($message_length == 50)) { #Insteon Extended Received - my $find_obj = Insteon::get_object(substr($parsed_data,4,6)); + my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); if (ref $find_obj) { &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4); } From 02d768059d2c51abd53373a879684863629a73eb Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 8 Nov 2013 17:55:00 -0800 Subject: [PATCH 313/330] Insteon: Move Debuglevel Routine to Generic_Item, Fix Bugs in Insteon_PLM Moving to Generic_Item so that it will work properly with X10 items, also allows for expansion into other object types. --- lib/Generic_Item.pm | 18 ++++++ lib/Insteon.pm | 10 +-- lib/Insteon/AllLinkDatabase.pm | 108 ++++++++++++++++----------------- lib/Insteon/BaseInsteon.pm | 94 ++++++++++++---------------- lib/Insteon/BaseInterface.pm | 40 ++++++------ lib/Insteon/Controller.pm | 6 +- lib/Insteon/IOLinc.pm | 18 +++--- lib/Insteon/Irrigation.pm | 8 +-- lib/Insteon/Lighting.pm | 4 +- lib/Insteon/Message.pm | 10 +-- lib/Insteon/Security.pm | 12 ++-- lib/Insteon/Thermostat.pm | 48 +++++++-------- lib/Insteon_PLM.pm | 94 +++++++++++++++------------- 13 files changed, 239 insertions(+), 231 deletions(-) diff --git a/lib/Generic_Item.pm b/lib/Generic_Item.pm index acf58f291..0399bf523 100644 --- a/lib/Generic_Item.pm +++ b/lib/Generic_Item.pm @@ -1340,6 +1340,24 @@ sub user_data { return \%{$$self{user_data}}; } +=item C + +Returns 1 if debug_group or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($object, $debug_level, $debug_group) = @_; + $debug_level = 1 unless $debug_level; + my $objname; + $objname = lc $object->get_object_name if defined $object; + ::print_log("[Generic_Item] debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{$debug_group} >= 5; + return 1 if $main::Debug{$debug_group} >= $debug_level; + return 1 if defined $objname && $main::Debug{$objname} >= $debug_level; + return 0; +} + =back =head2 PACKAGE FUNCTIONS diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 2345dc989..9665ef1f6 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -406,7 +406,7 @@ sub scan_all_linktables push @_scan_devices, $candidate_object; &main::print_log("[Scan all linktables] INFO1: " . $candidate_object->get_object_name - . " will be scanned.") if $candidate_object->debuglevel(1); + . " will be scanned.") if $candidate_object->debuglevel(1, 'insteon'); } else { @@ -1075,13 +1075,13 @@ sub check_all_aldb_versions { main::print_log("[Insteon] DEBUG4 Checking aldb version for " . $ALDB_device->get_object_name() - . " ($count of $ALDB_cnt)") if ($ALDB_device->debuglevel(4)); + . " ($count of $ALDB_cnt)") if ($ALDB_device->debuglevel(4, 'insteon')); $ALDB_device->check_aldb_version(); } else { main::print_log("[Insteon] DEBUG4 " . $ALDB_device->get_object_name . " does not have its own aldb ($count of $ALDB_cnt)") - if ($ALDB_device->debuglevel(4)); + if ($ALDB_device->debuglevel(4, 'insteon')); } } main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); @@ -1099,14 +1099,14 @@ sub check_thermo_versions $thermo_device->get_root()->engine_version eq "I2CS"){ main::print_log("[Insteon] DEBUG4 Setting thermostat " . $thermo_device->get_object_name() . " to i2CS") - if ($thermo_device->debuglevel(4)); + if ($thermo_device->debuglevel(4, 'insteon')); bless $thermo_device, 'Insteon::Thermo_i2CS'; $thermo_device->init(); } else { main::print_log("[Insteon] DEBUG4 Setting thermostat " . $thermo_device->get_object_name() . " to i1") - if ($thermo_device->debuglevel(4)); + if ($thermo_device->debuglevel(4, 'insteon')); bless $thermo_device, 'Insteon::Thermo_i1'; } } diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 3e3396096..594186190 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -145,7 +145,7 @@ sub query_aldb_delta $self->{_aldb_changed_callback} = undef; eval ($callback); &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } elsif ($action eq "check" && ((&main::get_tickcount - $self->scandatetime()) <= 2000)){ @@ -160,7 +160,7 @@ sub query_aldb_delta $self->{_aldb_unchanged_callback} = undef; eval ($callback); &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } else { @@ -336,7 +336,7 @@ sub delete_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } elsif ($link_parms{address} && $link_parms{aldb_check} eq "ok") @@ -394,7 +394,7 @@ sub delete_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] error encountered during delete_link callback: " . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDataBase; } } @@ -560,7 +560,7 @@ sub delete_orphan_links if ($group eq '01' || $group eq '00') { #ignore manual responder link to PLM group 01 or 00 required for I2CS devices main::print_log("[Insteon::AllLinkDatabase] DEBUG2 Ignoring orphan responder link from " - . $selfname . " to PLM for group 01 or 00") if $self->{device}->debuglevel(2); + . $selfname . " to PLM for group 01 or 00") if $self->{device}->debuglevel(2, 'insteon'); } elsif ($audit_mode) { @@ -856,7 +856,7 @@ sub _process_delete_queue { else { &::print_log("[Insteon::AllLinkDatabase] Nothing else to do for " . $$self{device}->get_object_name . " after deleting " - . $$self{delete_queue_processed} . " links") if $self->{device}->debuglevel(); + . $$self{delete_queue_processed} . " links") if $self->{device}->debuglevel(1, 'insteon'); $$self{device}->interface->_aldb->_process_delete_queue($$self{delete_queue_processed}); } } @@ -977,10 +977,10 @@ sub get_first_empty_address } $first_address = ($high_address > 0) ? sprintf('%04x', $high_address - 8) : 0; main::print_log("[Insteon::AllLinkDatabase] DEBUG4: No empty link entries; using next lowest link address [" - .$first_address."]") if $self->{device}->debuglevel(4); + .$first_address."]") if $self->{device}->debuglevel(4, 'insteon'); } else { main::print_log("[Insteon::AllLinkDatabase] DEBUG4: Found empty address [" - .$first_address."] in empty array") if $self->{device}->debuglevel(4); + .$first_address."] in empty array") if $self->{device}->debuglevel(4, 'insteon'); } return $first_address; @@ -1046,7 +1046,7 @@ sub add_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } @@ -1062,7 +1062,7 @@ sub add_link eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } @@ -1085,7 +1085,7 @@ sub add_link &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") - if $self->{device}->debuglevel(2); + if $self->{device}->debuglevel(2, 'insteon'); my ($data1, $data2); if($link_parms{is_controller}) { $data1 = '03'; #application retries == 3 @@ -1105,14 +1105,14 @@ sub add_link . $$self{device}->get_object_name . " does not have a record of the first empty ALDB record." . " Please rescan this device's link table") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); if ($$self{_success_callback}) { package main; eval ($$self{_success_callback}); &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } @@ -1144,7 +1144,7 @@ sub update_link my $ramp_rate = $link_parms{ramp_rate}; $ramp_rate =~ s/(\d)s?/$1/; &::print_log("[Insteon::AllLinkDatabase] updating " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name - . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->{device}->debuglevel(); + . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->{device}->debuglevel(1, 'insteon'); my $data1 = &Insteon::DimmableLight::convert_level($on_level); my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; @@ -1170,7 +1170,7 @@ sub update_link package main; eval($link_parms{callback}); &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } @@ -1184,14 +1184,14 @@ sub update_link &::print_log("[Insteon::AllLinkDatabase] ERROR: updating link record failed because " . $$self{device}->get_object_name . " does not have an existing ALDB entry key=$key") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); if ($$self{_success_callback}) { package main; eval ($$self{_success_callback}); &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; } } @@ -1579,7 +1579,7 @@ sub _on_peek my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); if ($msg{is_extended}) { &::print_log("[Insteon::ALDB_i1]: extended peek for " . $$self{device}->{object_name} - . " is " . $msg{extra}) if $self->{device}->debuglevel(); + . " is " . $msg{extra}) if $self->{device}->debuglevel(1, 'insteon'); } else { @@ -1631,7 +1631,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); my $flag = hex($msg{extra}); $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; @@ -1661,7 +1661,7 @@ sub _on_peek } &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " completed link memory scan") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -1711,7 +1711,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{pending_aldb}{group} = lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devhi'; @@ -1731,7 +1731,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{pending_aldb}{deviceid} = lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devmid'; @@ -1752,7 +1752,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{pending_aldb}{deviceid} .= lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_devlo'; @@ -1773,7 +1773,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{pending_aldb}{deviceid} .= lc $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_data1'; @@ -1796,7 +1796,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{_mem_action} = 'aldb_data2'; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{pending_aldb}{data1} = $msg{extra}; @@ -1819,7 +1819,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{pending_aldb}{data2} = $msg{extra}; $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); $$self{_mem_action} = 'aldb_data3'; @@ -1842,7 +1842,7 @@ sub _on_peek { &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{pending_aldb}{data3} = $msg{extra}; if ($$self{_stress_test_act}){ @@ -1924,7 +1924,7 @@ sub _on_peek else { ::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); } } } @@ -2007,7 +2007,7 @@ sub _write_link if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) { &::print_log("[Insteon::ALDB_i1] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); $data3 = $$self{device}->group; } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; @@ -2023,7 +2023,7 @@ sub _write_link package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i1] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::ALDB_i1; } } @@ -2131,7 +2131,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for _mem_activity=".$$self{_mem_activity} - ." _mem_action=". $$self{_mem_action}) if $self->{device}->debuglevel(3); + ." _mem_action=". $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); if ($$self{_mem_action} eq 'aldb_i2read') { @@ -2143,12 +2143,12 @@ sub on_read_write_aldb $$self{_mem_action} = 'aldb_i2readack'; &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received ack") - if $self->{device}->debuglevel(3); + if $self->{device}->debuglevel(3, 'insteon'); } else { #otherwise just ignore the message because it is out of sequence &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] ack not received. " - . "ignoring message") if $self->{device}->debuglevel(3); + . "ignoring message") if $self->{device}->debuglevel(3, 'insteon'); } } @@ -2157,14 +2157,14 @@ sub on_read_write_aldb if($msg{is_ack}) { &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received duplicate ack. Ignoring.") - if $self->{device}->debuglevel(3); + if $self->{device}->debuglevel(3, 'insteon'); $clear_message = 0; } elsif(length($msg{extra})<30) { &::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #can't clear message, if valid message doesn't arrive #resend logic will kick in @@ -2175,7 +2175,7 @@ sub on_read_write_aldb . " address received did not match address requested: " . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3); + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); #can't clear message, if valid message doesn't arrive #resend logic will kick in @@ -2193,7 +2193,7 @@ sub on_read_write_aldb if (lc $$self{_mem_msb} eq '00' and lc $$self{_mem_lsb} eq '00') { main::print_log("[Insteon::ALDB_i2] DEBUG4: Start of scan; initializing aldb structure") - if $self->{device}->debuglevel(4); + if $self->{device}->debuglevel(4, 'insteon'); # reinit the aldb hash as there will be a new one $$self{aldb} = undef; # reinit the empty address list @@ -2235,10 +2235,10 @@ sub on_read_write_aldb . $$self{device}->get_object_name . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " . lc $msg{extra} . " for " . $$self{_mem_action}) - if(($$self{pending_aldb}{inuse}) and $self->{device}->debuglevel(3)); + if(($$self{pending_aldb}{inuse}) and $self->{device}->debuglevel(3, 'insteon')); main::print_log("[Insteon::ALDB_i2] DEBUG4: scan done; adding last address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $self->{device}->debuglevel(4); + if $self->{device}->debuglevel(4, 'insteon'); $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); # scan done; clear out state flags $$self{_mem_action} = undef; @@ -2255,7 +2255,7 @@ sub on_read_write_aldb &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " completed link memory scan: status: " . $self->health()) - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -2266,7 +2266,7 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] DEBUG4: inuse flag == false; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $self->{device}->debuglevel(4); + if $self->{device}->debuglevel(4, 'insteon'); $self->add_empty_address($$self{pending_aldb}{address}); } else @@ -2292,14 +2292,14 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] DEBUG4: duplicate link found; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to duplicates array") - if $self->{device}->debuglevel(4); + if $self->{device}->debuglevel(4, 'insteon'); $self->add_duplicate_link_address($$self{pending_aldb}{address}); } else { main::print_log("[Insteon::ALDB_i2] DEBUG4: active link found; adding address [" . $$self{_mem_msb} . $$self{_mem_lsb} ."] to aldb") - if $self->{device}->debuglevel(4); + if $self->{device}->debuglevel(4, 'insteon'); %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; } } @@ -2339,7 +2339,7 @@ sub on_read_write_aldb $$self{pending_aldb} = undef; main::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name . " link write completed for [".$$self{aldb}{$aldbkey}{address}."]") - if $self->{device}->debuglevel(3); + if $self->{device}->debuglevel(3, 'insteon'); $self->health("good"); # Put the new ALDB Delta into memory $self->query_aldb_delta('set'); @@ -2372,7 +2372,7 @@ sub on_read_write_aldb { main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . ": unhandled _mem_action=".$$self{_mem_action}) - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); $clear_message = 0; } return $clear_message; @@ -2419,7 +2419,7 @@ sub _write_link if (($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) and ($data3 eq '00')) { &::print_log("[Insteon::ALDB_i2] setting data3 to " . $$self{device}->group . " for this keypadlinc") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); $data3 = $$self{device}->group; } $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; @@ -2440,7 +2440,7 @@ sub _write_link package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::ALDB_i2; } } @@ -2490,7 +2490,7 @@ sub _write_delete package main; eval ($$self{_success_callback}); &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::ALDB_i2; } } @@ -2770,7 +2770,7 @@ sub delete_orphan_links &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan Link to non-existant deviceid: " . $deviceid . "; group:$group; " . (($is_controller) ? "controller; data:$data3" : "responder")) - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); } else { @@ -2795,13 +2795,13 @@ sub delete_orphan_links if ($group eq '01' || $group eq '00') { #ignore manual controller link from PLM group 01 or 00 to device required for I2CS devices main::print_log("[Insteon::ALDB_PLM] DEBUG2 Ignoring orphan PLM controller(01 or 00) link to " - . $device->get_object_name() ) if $self->{device}->debuglevel(2); + . $device->get_object_name() ) if $self->{device}->debuglevel(2, 'insteon'); } elsif ($audit_mode) { &::print_log("[Insteon::ALDB_PLM] (AUDIT) Delete Orphan PLM controller link ($group) to: " . $device->get_object_name() . "($data3)") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); } else { @@ -2925,7 +2925,7 @@ sub _process_delete_queue { . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") - if $self->{device}->debuglevel(); + if $self->{device}->debuglevel(1, 'insteon'); $self->delete_link(%delete_req); } elsif ($delete_req{linkdevice}) @@ -2995,7 +2995,7 @@ sub delete_link package main; eval ($link_parms{callback}); &::print_log("[Insteon_PLM] error in add link callback: " . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon_PLM; } } @@ -3046,7 +3046,7 @@ sub add_link package main; eval ($link_parms{callback}); &::print_log("[Insteon::ALDB_PLM] error in add link callback: " . $@) - if $@ and $self->{device}->debuglevel(); + if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon_PLM; } } diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 1adc93bd4..b569c1df1 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -206,24 +206,6 @@ sub group return $$self{m_group}; } -=item C - -Returns 1 if insteon or this device is at least debug level 'level', otherwise returns 0. - -=cut - -sub debuglevel -{ - my ($object, $debug_level) = @_; - $debug_level = 1 unless $debug_level; - my $objname; - $objname = lc $object->get_object_name if defined $object; - ::print_log("[Insteon] debuglevel: Processing debug for object $objname ... " . $main::Debug{$objname}) if $main::Debug{insteon} >= 5; - return 1 if $main::Debug{insteon} >= $debug_level; - return 1 if defined $objname && $main::Debug{$objname} >= $debug_level; - return 0; -} - =item C Changes the amount of time MH will wait to receive a response from a device before @@ -291,7 +273,7 @@ sub default_hop_count my ($self, $hop_count) = @_; if (defined($hop_count)){ ::print_log("[Insteon::BaseObject] DEBUG3: Adding hop count of " . $hop_count . " to hop_array of " - . $self->get_object_name) if $self->debuglevel(3); + . $self->get_object_name) if $self->debuglevel(3, 'insteon'); if (!defined(@{$$self{hop_array}})) { unshift(@{$$self{hop_array}}, $$self{default_hop_count}); $$self{hop_sum} = $$self{default_hop_count}; @@ -305,7 +287,7 @@ sub default_hop_count ::print_log("[Insteon::BaseObject] DEBUG4: ".$self->get_object_name ."->default_hop_count()=".$$self{default_hop_count} ." :: hop_array[]=". join("",@{$$self{hop_array}})) - if $self->debuglevel(4); + if $self->debuglevel(4, 'insteon'); } #Allow for per-device settings @@ -386,14 +368,14 @@ sub set { #If set by device, update MH state, my $derived_state = $self->derive_link_state($p_state); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . "::set_receive($derived_state, $setby_name)") if $self->debuglevel(); + . "::set_receive($derived_state, $setby_name)") if $self->debuglevel(1, 'insteon'); $self->set_receive($derived_state,$p_setby,$p_response); $self->set_linked_devices($p_state); } elsif (ref $p_setby and $p_setby eq $self->interface) { #If set by interface, this was a manual status_request response &::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name)") if $self->debuglevel(); + . "::set_receive($p_state, $setby_name)") if $self->debuglevel(1, 'insteon'); $self->set_receive($p_state,$p_setby,$p_response); } else { # Not called by device, send set command @@ -401,7 +383,7 @@ sub set my $message = $self->derive_message($p_state); $self->_send_cmd($message); &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); $self->is_acknowledged(0); $$self{pending_state} = $p_state; $$self{pending_setby} = $p_setby; @@ -467,7 +449,7 @@ sub set_receive && ($curr_milli - $$self{set_milliseconds} < $window)){ ::print_log("[Insteon::BaseObject] Ignoring duplicate set " . $p_state . " state command for " . $self->get_object_name . " received in " . - "less than $window milliseconds") if $self->debuglevel(); + "less than $window milliseconds") if $self->debuglevel(1, 'insteon'); } else { $$self{set_milliseconds} = $curr_milli; $self->level($p_state) if $self->can('level'); # update the level value @@ -591,7 +573,7 @@ sub derive_message # confirm that the resulting $msg is legitimate if (!(defined($self->message_type_code($command)))) { - &::print_log("[Insteon::BaseInsteon] invalid state=$command") if $self->debuglevel(); + &::print_log("[Insteon::BaseInsteon] invalid state=$command") if $self->debuglevel(1, 'insteon'); return undef; } @@ -675,7 +657,7 @@ sub _is_info_request my $ack_on_level = sprintf("%d", int((hex($msg{extra}) * 100 / 255)+.5)); &::print_log("[Insteon::BaseObject] received status for " . $self->{object_name} . " with on-level: $ack_on_level%, " - . "hops left: $msg{hopsleft}") if $self->debuglevel(); + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); $self->level($ack_on_level) if $self->can('level'); # update the level value if ($ack_on_level == 0) { $self->set('off', $ack_setby); @@ -733,7 +715,7 @@ sub _is_info_request package main; eval ($callback); &::print_log("[Insteon::BaseObject] " . $self->get_object_name . ": error during scan callback $@") - if $@ and $self->debuglevel(); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon::BaseObject; } } @@ -744,7 +726,7 @@ sub _is_info_request $self->engine_version($version); &::print_log("[Insteon::BaseObject] received engine version for " . $self->{object_name} . " of $version. " - . "hops left: $msg{hopsleft}") if $self->debuglevel(); + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); } return $is_info_request; } @@ -761,7 +743,7 @@ sub _process_message # by Insteon_Link. main::print_log("[Insteon::BaseObject] WARN: Message has invalid checksum") - if ($self->debuglevel() && !($msg{crc_valid}) + if ($self->debuglevel(1, 'insteon') && !($msg{crc_valid}) && $msg{is_extended} && $self->engine_version() eq 'I2CS'); my $clear_message = 0; @@ -827,7 +809,7 @@ sub _process_message if (!$corrupt_cmd){ $self->_process_command_stack(%msg); &::print_log("[Insteon::BaseObject] received ping acknowledgement from " . $self->{object_name}) - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); $self->ping(); $clear_message = 1; } @@ -836,7 +818,7 @@ sub _process_message $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); if (!$corrupt_cmd){ &::print_log("[Insteon::BaseObject] received linking mode ACK from " . $self->{object_name}) - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); $self->interface->_set_timeout('xmit', 2000); $clear_message = 0; } @@ -853,7 +835,7 @@ sub _process_message # signal receipt of message to the command stack in case commands are queued $self->_process_command_stack(%msg); &::print_log("[Insteon::BaseObject] received command/state (awaiting) acknowledge from " . $self->{object_name} - . ": $pending_cmd and data: $msg{extra}") if $self->debuglevel(); + . ": $pending_cmd and data: $msg{extra}") if $self->debuglevel(1, 'insteon'); } } else @@ -865,7 +847,7 @@ sub _process_message $self->_process_command_stack(%msg); &::print_log("[Insteon::BaseObject] received command/state acknowledge from " . $self->{object_name} . ": " . (($msg{command}) ? $msg{command} : "(unknown)") - . " and data: $msg{extra}") if $self->debuglevel(); + . " and data: $msg{extra}") if $self->debuglevel(1, 'insteon'); } if ($corrupt_cmd) { main::print_log("[Insteon::BaseObject] WARN: received a message from " @@ -885,7 +867,7 @@ sub _process_message . $self->get_nack_msg_for( $msg{extra} ) .") for " . $self->{object_name} . ". It may be unplugged, have a burned out bulb, or this may be a new I2CS " . "type device that must first be manually linked to the PLM using the set button.") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); } else { @@ -899,7 +881,7 @@ sub _process_message if($p_setby->active_message->failure_callback) { main::print_log("[Insteon::BaseObject] WARN: Now calling message failure callback: " - . $p_setby->active_message->failure_callback) if $self->debuglevel(); + . $p_setby->active_message->failure_callback) if $self->debuglevel(1, 'insteon'); $self->failure_reason('NAK'); package main; eval $p_setby->active_message->failure_callback; @@ -943,7 +925,7 @@ sub _process_message if ($msg{command} eq 'link_cleanup_report'){ if ($msg{extra} == 0){ ::print_log("[Insteon::BaseObject] DEBUG Received AllLink Cleanup Success for " - . $self->{object_name}) if $self->debuglevel(1); + . $self->{object_name}) if $self->debuglevel(1, 'insteon'); } else { ::print_log("[Insteon::BaseObject] WARN " . $msg{extra} . " Device(s) failed to " . "acknowledge the command from " . $self->{object_name}); @@ -959,7 +941,7 @@ sub _process_message my $timeout = (scalar(@links)+1) * 300; ::print_log("[Insteon::BaseObject] DEBUG3 Delaying any outgoing messages ". "by $timeout milliseconds to avoid collision with subsequent cleanup ". - "messages from " . $self->get_object_name) if ($self->debuglevel(3)); + "messages from " . $self->get_object_name) if ($self->debuglevel(3, 'insteon')); $self->interface->_set_timeout('xmit', $timeout); } } @@ -968,14 +950,14 @@ sub _process_message if (($self->state eq $p_state or $self->state_final eq $p_state) and $$self{_pending_cleanup}){ ::print_log("[Insteon::BaseObject] Ignoring Received Direct AllLink Cleanup Message for " - . $self->{object_name} . " since AllLink Broadcast Message was Received.") if $self->debuglevel(); + . $self->{object_name} . " since AllLink Broadcast Message was Received.") if $self->debuglevel(1, 'insteon'); } else { $self->set($p_state, $self); } $$self{_pending_cleanup} = 0; } else { main::print_log("[Insteon::BaseObject] Ignoring unsupported command from " - . $self->{object_name}) if $self->debuglevel(); + . $self->{object_name}) if $self->debuglevel(1, 'insteon'); $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); } } @@ -1038,11 +1020,11 @@ sub _process_command_stack package main; eval ($callback); &::print_log("[Insteon::BaseObject] error in queue timer callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon::BaseObject; } } else { -# &::print_log("[Insteon_Device] " . $self->get_object_name . " command queued but not yet sent; awaiting ack from prior command") if $self->debuglevel(); +# &::print_log("[Insteon_Device] " . $self->get_object_name . " command queued but not yet sent; awaiting ack from prior command") if $self->debuglevel(1, 'insteon'); } } @@ -1986,7 +1968,7 @@ sub _get_engine_version_failure my $failure_reason = $self->failure_reason(); main::print_log("[Insteon::BaseDevice::_get_engine_version_failure] DEBUG4: " - ."failure reason: $failure_reason") if $self->debuglevel(4); + ."failure reason: $failure_reason") if $self->debuglevel(4, 'insteon'); if($failure_reason eq 'NAK') { @@ -2052,7 +2034,7 @@ sub ping package main; eval ($complete_callback); &::print_log("[Insteon::BaseDevice] error in ping callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon::BaseDevice; delete $$self{ping_callback}; } @@ -2358,7 +2340,7 @@ sub stress_test package main; eval ($complete_callback); &::print_log("[Insteon::BaseDevice] error in stress_test callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon::BaseDevice; delete $$self{stress_test_callback}; } @@ -2673,7 +2655,7 @@ sub check_aldb_version if ($new_version) { main::print_log("[Insteon::BaseDevice] DEBUG4: aldb_version is " .$self->_aldb->aldb_version()." but device is ".$engine_version. - ". Remapping aldb version to $new_version") if $self->debuglevel(4); + ". Remapping aldb version to $new_version") if $self->debuglevel(4, 'insteon'); my $restore_string = ''; if ($self->_aldb) { $restore_string = $self->_aldb->restore_string(); @@ -2690,7 +2672,7 @@ sub check_aldb_version package main; eval ($restore_string); &::print_log("[Insteon::BaseDevice] error in eval creating ALDB object: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon::BaseDevice; } } @@ -2942,7 +2924,7 @@ sub sync_links $requires_update = 1; &::print_log("[Insteon::BaseController] DEBUG: flagging " . $self->get_object_name . " for update because existing ramp rate ($raw_ramp_rate) != target ($raw_tgt_ramp_rate)") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); } elsif (($link_on_level > $tgt_on_level + 1) or ($link_on_level < $tgt_on_level -1)) @@ -2950,7 +2932,7 @@ sub sync_links $requires_update = 1; &::print_log("[Insteon::BaseController] DEBUG: flagging " . $self->get_object_name . " for update because existing on level ($link_on_level) != target ($tgt_on_level)") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); } } if ($requires_update) @@ -2977,7 +2959,7 @@ sub sync_links . $member->get_object_name . " for " . $insteon_object->get_object_name . " with group:" . $self->group . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate") - if $self->debuglevel(4); + if $self->debuglevel(4, 'insteon'); push @{$$self{sync_queue}}, \%link_req; } } @@ -3006,7 +2988,7 @@ sub sync_links . $member->get_object_name . " for " . $insteon_object->get_object_name . " with group:" . $self->group . "; on_level:$tgt_on_level; ramp_rate:$tgt_ramp_rate") - if $self->debuglevel(4); + if $self->debuglevel(4, 'insteon'); push @{$$self{sync_queue}}, \%link_req; } } @@ -3030,7 +3012,7 @@ sub sync_links } main::print_log("[Insteon::BaseController] DEBUG4: queuing add for controller record to " . $insteon_object->get_object_name . " for " . $member->get_object_name - . " with group:" . $self->group) if $self->debuglevel(4); + . " with group:" . $self->group) if $self->debuglevel(4, 'insteon'); push @{$$self{sync_queue}}, \%link_req; } } @@ -3058,7 +3040,7 @@ sub sync_links main::print_log("[Insteon::BaseController] DEBUG4: queuing add for controller record to " . $insteon_object->get_object_name . " for " . $self->interface->get_object_name . " with group:" . $self->group) - if $self->debuglevel(4); + if $self->debuglevel(4, 'insteon'); push @{$$self{sync_queue}}, \%link_req; } } @@ -3079,7 +3061,7 @@ sub sync_links main::print_log("[Insteon::BaseController] DEBUG4: queuing add for responder record to " . $self->interface->get_object_name . " for " . $insteon_object->get_object_name . " with group:" . $self->group) - if $self->debuglevel(4); + if $self->debuglevel(4, 'insteon'); push @{$$self{sync_queue}}, \%link_req; } } @@ -3088,7 +3070,7 @@ sub sync_links if (!($num_sync_queue)) { &::print_log("[Insteon::BaseController] Nothing to do when syncing links for " . $self->get_object_name) - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); } $self->_process_sync_queue(); @@ -3115,7 +3097,7 @@ sub _process_sync_queue { package main; eval ($$self{sync_queue_callback}); &::print_log("[Insteon::BaseController] error in sync links callback: " . $@) - if $@ and $self->debuglevel(); + if $@ and $self->debuglevel(1, 'insteon'); $$self{sync_queue_callback} = undef; package Insteon::BaseController; } else { @@ -3249,7 +3231,7 @@ sub update_members my %current_record = $device->get_link_record($self->device_id . $self->group); if (%current_record) { &::print_log("[Insteon::BaseController] remote record: $current_record{data1}") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); } } } diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 1707fa2f9..d1b70eccb 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -128,8 +128,8 @@ Returns 1 if Insteon or this device is at least debug level 'level', otherwise r sub debuglevel { - my ($self, $debug_level) = @_; - return Insteon::BaseObject::debuglevel($self, $debug_level); + my ($self, $debug_level, $debug_group) = @_; + return Generic_Item::debuglevel($self, $debug_level, $debug_group); } =item C<_is_duplicate(cmd)> @@ -308,7 +308,7 @@ sub queue_message my $setby = $message->setby; if ($self->_is_duplicate($message->interface_data) && !($message->isa('Insteon::X10Message'))) { - &main::print_log("[Insteon::BaseInterface] Attempt to queue command already in queue; skipping ...") if $self->debuglevel(); + &main::print_log("[Insteon::BaseInterface] Attempt to queue command already in queue; skipping ...") if $self->debuglevel(1, 'insteon'); } else { @@ -366,7 +366,7 @@ sub process_queue &::print_log("[Insteon::BaseInterface] WARN: number of retries (" . $self->active_message->send_attempts . ") for " . $self->active_message->to_string() - . " exceeds limit. Now moving on...") if $self->debuglevel(); + . " exceeds limit. Now moving on...") if $self->debuglevel(1, 'insteon'); # !!!!!!!!! TO-DO - handle failure timeout ??? my $failed_message = $self->active_message; # make sure to let the sending object know!!! @@ -387,7 +387,7 @@ sub process_queue if ($failed_message->failure_callback) { &::print_log("[Insteon::BaseInterface] WARN: Message Timeout: Now calling callback: " . - $failed_message->failure_callback) if $self->debuglevel(); + $failed_message->failure_callback) if $self->debuglevel(1, 'insteon'); $failed_message->setby->failure_reason('timeout') if (defined($failed_message->setby) and $failed_message->setby->can('failure_reason')); package main; @@ -500,7 +500,7 @@ sub on_interface_info_received my ($self) = @_; &::print_log("[Insteon_PLM] PLM id: " . $self->device_id . " firmware: " . $self->firmware) - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); $self->clear_active_message(); } @@ -533,7 +533,7 @@ sub on_standard_insteon_received #time has been required. Extra 50 millis helps prevent dupes $wait_time = ($wait_time * 100) + 50; $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; - ::print_log($wait_message) if ($self->debuglevel(3) && $wait_time > 50); + ::print_log($wait_message) if ($self->debuglevel(3, 'insteon') && $wait_time > 50); $self->_set_timeout('xmit', $wait_time); # get the matching object @@ -548,13 +548,13 @@ sub on_standard_insteon_received $msg{command} = $object->message_type($msg{cmd_code}); &::print_log("[Insteon::BaseInterface] Received message from: ". $object->get_object_name ."; command: $msg{command}; type: $msg{type}; group: $msg{group}") - if (!($msg{is_ack} or $msg{is_nack})) and $self->debuglevel(); + if (!($msg{is_ack} or $msg{is_nack})) and $self->debuglevel(1, 'insteon'); } if ($msg{is_ack} or $msg{is_nack}) { main::print_log("[Insteon::BaseInterface] DEBUG3: PLM command:insteon_received; " . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") - if $self->debuglevel(3); + if $self->debuglevel(3, 'insteon'); # need to confirm that this message corresponds to the current active one before clearing it # TO-DO!!! This is a brute force and poor compare technique; needs to be replaced by full compare if ($self->active_message && ref $self->active_message->setby) @@ -576,7 +576,7 @@ sub on_standard_insteon_received if($object->_process_message($self, %msg)) { if ($self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $object->debuglevel(4); + . $self->active_message->success_callback) if $object->debuglevel(4, 'insteon'); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; @@ -606,11 +606,11 @@ sub on_standard_insteon_received if (($msg{extra} == $self->active_message->setby->group)){ &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received for scene " . $object->get_object_name . " from " . $setby_object->get_object_name) - if $object->debuglevel(3); + if $object->debuglevel(3, 'insteon'); } elsif ($self->active_message->command_type eq 'all_link_direct_cleanup' && lc($self->active_message->setby->device_id) eq $msg{source}) { - &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $object->debuglevel(2); + &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $object->debuglevel(2, 'insteon'); $self->clear_active_message(); } else { @@ -619,7 +619,7 @@ sub on_standard_insteon_received . $object->get_object_name . ", but group in recent message " . $msg{extra}. " did not match group in " . "prior sent message group " . $self->active_message->setby->group) - if $object->debuglevel(3); + if $object->debuglevel(3, 'insteon'); } # If ACK or NACK received then PLM is still working on the ALL Link Command # Increase the command timeout to wait for next one @@ -661,7 +661,7 @@ sub on_standard_insteon_received $plm_group_obj = $self if (!ref $plm_group_obj); &main::print_log("[Insteon::BaseInterface] DEBUG3: received cleanup message responding to " . "PLM controller group: $group_name. Ignoring as this has already been processed") - if $plm_group_obj->debuglevel(3); + if $plm_group_obj->debuglevel(3, 'insteon'); } else { @@ -714,7 +714,7 @@ sub on_extended_insteon_received #time has been required. Extra 50 millis helps prevent dupes $wait_time = ($wait_time * 200) + 50; $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; - ::print_log($wait_message) if ($self->debuglevel(3) && $wait_time > 50); + ::print_log($wait_message) if ($self->debuglevel(3, 'insteon') && $wait_time > 50); $self->_set_timeout('xmit', $wait_time); # get the matching object @@ -729,14 +729,14 @@ sub on_extended_insteon_received $msg{command} = $object->message_type($msg{cmd_code}); main::print_log("[Insteon::BaseInterface] DEBUG: PLM command:insteon_ext_received; " . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") - if( (!($msg{is_ack} or $msg{is_nack}) and $self->debuglevel()) - or $self->debuglevel(3)); + if( (!($msg{is_ack} or $msg{is_nack}) and $self->debuglevel(1, 'insteon')) + or $self->debuglevel(3, 'insteon')); } - &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $object->debuglevel(3); + &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $object->debuglevel(3, 'insteon'); if($object->_process_message($self, %msg)) { if (ref $self->active_message && $self->active_message->success_callback){ main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $object->debuglevel(4); + . $self->active_message->success_callback) if $object->debuglevel(4, 'insteon'); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; @@ -895,7 +895,7 @@ sub _is_duplicate_received { $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); }; ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " - . $message_data . ", from ". $object->get_object_name) if $object->debuglevel(); + . $message_data . ", from ". $object->get_object_name) if $object->debuglevel(1, 'insteon'); } else { #Message was not in hash, so add it $$self{received_commands}{$key} = $curr_milli + $delay; diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 6a9958855..89f58e777 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -209,9 +209,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::RemoteLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); + main::print_log("[Insteon::RemoteLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::RemoteLinc] Clearing active message") if $self->debuglevel(); + main::print_log("[Insteon::RemoteLinc] Clearing active message") if $self->debuglevel(1, 'insteon'); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -234,7 +234,7 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::RemoteLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); } } else { diff --git a/lib/Insteon/IOLinc.pm b/lib/Insteon/IOLinc.pm index 92b94876b..0349011c1 100755 --- a/lib/Insteon/IOLinc.pm +++ b/lib/Insteon/IOLinc.pm @@ -158,11 +158,11 @@ sub set if ($p_state eq $$self{child_state} && ($curr_milli - $$self{child_set_milliseconds} < $window)) { ::print_log("[Insteon::IOLinc] Received duplicate ". $self->get_object_name - . " sensor " . $p_state . " message, ignoring.") if $self->debuglevel(); + . " sensor " . $p_state . " message, ignoring.") if $self->debuglevel(1, 'insteon'); } else { ::print_log("[Insteon::IOLinc] Received ". $self->get_object_name - . " sensor " . $p_state . " message.") if $self->debuglevel(); + . " sensor " . $p_state . " message.") if $self->debuglevel(1, 'insteon'); $$self{child_state} = $p_state; $$self{child_set_milliseconds} = $curr_milli; if (ref $$self{child_sensor}){ @@ -213,7 +213,7 @@ sub _is_info_request my $child_state = &Insteon::BaseObject::derive_link_state(hex($msg{extra})); &::print_log("[Insteon::IOLinc] received status for " . $self->get_object_name . "sensor of: $child_state " - . "hops left: $msg{hopsleft}") if $self->debuglevel(); + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); $ack_setby = $$self{child_sensor} if ref $$self{child_sensor}; if (ref $$self{child_sensor}){ $$self{child_sensor}->set_receive($child_state, $ack_setby); @@ -265,9 +265,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::IOLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); + main::print_log("[Insteon::IOLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::IOLinc] Clearing active message") if $self->debuglevel(); + main::print_log("[Insteon::IOLinc] Clearing active message") if $self->debuglevel(1, 'insteon'); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -284,12 +284,12 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::IOLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); } } elsif ($msg{command} eq "set_operating_flags" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - main::print_log("[Insteon::IOLinc] Acknowledged flag set for " . $self->get_object_name) if $self->debuglevel(); + main::print_log("[Insteon::IOLinc] Acknowledged flag set for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); $clear_message = 1; $self->_process_command_stack(%msg); } @@ -314,12 +314,12 @@ sub set_momentary_time my $root = $self->get_root(); if ($momentary_time == 0){ ::print_log("[Insteon::IOLinc] Setting " . $self->get_object_name . - " to Latching Relay Mode." ) if $self->debuglevel(); + " to Latching Relay Mode." ) if $self->debuglevel(1, 'insteon'); } elsif ($momentary_time <= 255) { $momentary_time = 2 if $momentary_time == 1; #Can't set to 1 ::print_log("[Insteon::IOLinc] Setting Momentary Time to $momentary_time " . - "tenths of a second for " . $self->get_object_name) if $self->debuglevel(); + "tenths of a second for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); } else { ::print_log("[Insteon::IOLinc] WARN Invalid Momentary Time of $momentary_time " . diff --git a/lib/Insteon/Irrigation.pm b/lib/Insteon/Irrigation.pm index f56b0a957..686586780 100755 --- a/lib/Insteon/Irrigation.pm +++ b/lib/Insteon/Irrigation.pm @@ -128,7 +128,7 @@ sub set_valve { } unless ($cmd and $subcmd) { &::print_log("Insteon::Irrigation] ERROR: You must specify a valve number and a valid state (ON or OFF)") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); return; } my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); @@ -154,7 +154,7 @@ sub set_program { } unless ($cmd and $subcmd) { &::print_log("Insteon::Irrigation] ERROR: You must specify a program number and a valid state (ON or OFF)") - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); return; } my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); @@ -257,7 +257,7 @@ sub _is_info_request { or $cmd eq 'sprinkler_program_off') { $is_info_request = 1; my $val = hex($msg{extra}); - &::print_log("[Insteon::Irrigation] Processing data for $cmd with value: $val") if $self->debuglevel(); + &::print_log("[Insteon::Irrigation] Processing data for $cmd with value: $val") if $self->debuglevel(1, 'insteon'); $$self{'active_valve_id'} = ($val & 7) + 1; $$self{'active_program_number'} = (($val >> 3) & 3) + 1; $$self{'program_is_running'} = ($val >> 5) & 1; @@ -265,7 +265,7 @@ sub _is_info_request { $$self{'valve_is_running'} = ($val >> 7) & 1; &::print_log("[Insteon::Irrigation] active_valve_id: $$self{'active_valve_id'}," . " valve_is_running: $$self{'valve_is_running'}, active_program: $$self{'active_program_number'}," - . " program_is_running: $$self{'program_is_running'}, pump_enabled: $$self{'pump_enabled'}") if $self->debuglevel(); + . " program_is_running: $$self{'program_is_running'}, pump_enabled: $$self{'pump_enabled'}") if $self->debuglevel(1, 'insteon'); } else { #Check if this was a generic info_request diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index cd12c6b91..070f7a383 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -1140,7 +1140,7 @@ sub _is_info_request my $child_state = $child_obj->derive_link_state(hex($msg{extra})); &::print_log("[Insteon::FanLinc] received status for " . $child_obj->{object_name} . " of: $child_state " - . "hops left: $msg{hopsleft}") if $self->debuglevel(); + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); $ack_setby = $$child_obj{m_status_request_pending} if ref $$child_obj{m_status_request_pending}; $child_obj->SUPER::set($child_state, $ack_setby); delete($$parent{child_status_request_pending}); @@ -1171,7 +1171,7 @@ sub is_acknowledged $$child_obj{pending_setby} = undef; $$child_obj{pending_response} = undef; $$parent{child_pending_state} = undef; - &::print_log("[Insteon::FanLinc] received command/state acknowledge from " . $child_obj->{object_name}) if $self->debuglevel(); + &::print_log("[Insteon::FanLinc] received command/state acknowledge from " . $child_obj->{object_name}) if $self->debuglevel(1, 'insteon'); return $$self{is_acknowledged}; } else { return $self->SUPER::is_acknowledged($p_ack); diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index cf65bb0a7..843547980 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -233,7 +233,7 @@ sub send { &::print_log("[Insteon::BaseMessage] WARN: now resending " . $self->to_string() . " after " . $self->send_attempts - . " attempts.") if $self->setby->debuglevel(); + . " attempts.") if $self->setby->debuglevel(1, 'insteon'); # revise default hop count to reflect retries if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') && !defined($$self{no_hop_increase})) @@ -248,7 +248,7 @@ sub send && $self->setby->isa('Insteon::BaseObject')){ &main::print_log("[Insteon::BaseMessage] Hop count not increased for " . $self->setby->get_object_name . " because no_hop_increase flag was set.") - if $self->setby->debuglevel(); + if $self->setby->debuglevel(1, 'insteon'); $$self{no_hop_increase} = undef; } } @@ -1086,7 +1086,7 @@ sub generate_commands } if ($uc eq undef) { - &main::print_log("[Insteon::Message] Message is for entire HC") if Insteon::BaseObject::debuglevel($p_setby,); + &main::print_log("[Insteon::Message] Message is for entire HC") if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); } else { @@ -1095,7 +1095,7 @@ sub generate_commands $msg.= substr(unpack("H*",pack("C",$x10_unit_codes{substr($id,2,1)})),1,1); $msg.= "00"; &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $uc) . " as insteon msg: " - . $msg) if Insteon::BaseObject::debuglevel($p_setby,); + . $msg) if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); push @data, $msg; } @@ -1123,7 +1123,7 @@ sub generate_commands $msg.= "80"; &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $x10_arg) . " as insteon msg: " - . $msg) if Insteon::BaseObject::debuglevel($p_setby,); + . $msg) if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); push @data, $msg; diff --git a/lib/Insteon/Security.pm b/lib/Insteon/Security.pm index cfdcdfa43..cdd325efd 100644 --- a/lib/Insteon/Security.pm +++ b/lib/Insteon/Security.pm @@ -392,7 +392,7 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::MotionSensor] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); + main::print_log("[Insteon::MotionSensor] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); if ($$self{_ext_set_get_action} eq 'set'){ if (defined($$root{_set_bit_action})){ ::print_log("[Insteon::MotionSensor] Set of ". @@ -400,7 +400,7 @@ sub _process_message { $root->get_object_name); $$root{_set_bit_action} = undef; } else { - main::print_log("[Insteon::MotionSensor] Clearing active message") if $self->debuglevel(); + main::print_log("[Insteon::MotionSensor] Clearing active message") if $self->debuglevel(1, 'insteon'); } $clear_message = 1; $$self{_ext_set_get_action} = undef; @@ -455,7 +455,7 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::MotionSensor] WARN: Unknown Extended " - ."Set/Get Data Message Received for ". $self->get_object_name) if $self->debuglevel(); + ."Set/Get Data Message Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); } } else { @@ -817,9 +817,9 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::TriggerLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); + main::print_log("[Insteon::TriggerLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::TriggerLinc] Clearing active message") if $self->debuglevel(); + main::print_log("[Insteon::TriggerLinc] Clearing active message") if $self->debuglevel(1, 'insteon'); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); @@ -837,7 +837,7 @@ sub _process_message { $self->_process_command_stack(%msg); } else { main::print_log("[Insteon::TriggerLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); } } else { diff --git a/lib/Insteon/Thermostat.pm b/lib/Insteon/Thermostat.pm index a8f88fb82..e9eea1a35 100755 --- a/lib/Insteon/Thermostat.pm +++ b/lib/Insteon/Thermostat.pm @@ -215,7 +215,7 @@ Sets fan to 'on' or 'auto' sub fan{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon::Thermostat] Fan $state") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Fan $state") if $self->debuglevel(1, 'insteon'); my $fan; if (($state eq 'on') or ($state eq 'fan_on')) { $fan = '07'; @@ -236,7 +236,7 @@ Sets a new cool setpoint. =cut sub cool_setpoint{ my ($self, $temp) = @_; - main::print_log("[Insteon::Thermostat] Cool setpoint -> $temp") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Cool setpoint -> $temp") if $self->debuglevel(1, 'insteon'); if($temp !~ /^\d+$/){ main::print_log("[Insteon::Thermostat] ERROR: cool_setpoint $temp not numeric"); return; @@ -250,7 +250,7 @@ Sets a new heat setpoint. =cut sub heat_setpoint{ my ($self, $temp) = @_; - main::print_log("[Insteon::Thermostat] Heat setpoint -> $temp") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Heat setpoint -> $temp") if $self->debuglevel(1, 'insteon'); if($temp !~ /^\d+$/){ main::print_log("[Insteon::Thermostat] ERROR: heat_setpoint $temp not numeric"); return; @@ -374,7 +374,7 @@ sub _is_info_request { my $is_info_request = ($cmd eq 'thermostat_get_zone_info') ? 1 : 0; if ($is_info_request) { my $val = $msg{extra}; - main::print_log("[Insteon::Thermostat] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(1, 'insteon'); if ($$self{_zone_action} eq "temp") { $val = (hex $val) / 2; # returned value is twice the real value if (exists $$self{'temp'} and ($$self{'temp'} != $val)) { @@ -420,21 +420,21 @@ sub _process_message elsif ($msg{command} eq "thermostat_setpoint_cool" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of cool setpoint ". - "for ". $self->get_object_name) if $self->debuglevel(); + "for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->_cool_sp((hex($msg{extra})/2)); $clear_message = 1; } elsif ($msg{command} eq "thermostat_setpoint_heat" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermostat] Received ACK of heat setpoint ". - "for ". $self->get_object_name) if $self->debuglevel(); + "for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->_heat_sp((hex($msg{extra})/2)); $clear_message = 1; } elsif ($$self{_zone_action} eq 'setpoint' && $$self{m_pending_setpoint}) { $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); # we got our cool setpoint in auto mode - main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Processing data for $msg{command} with value: $msg{extra}") if $self->debuglevel(1, 'insteon'); my $val = (hex $msg{extra})/2; $self->_cool_sp($val); $$self{m_setpoint_pending} = 0; @@ -498,7 +498,7 @@ Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', sub mode{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon::Thermostat] Mode $state") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Mode $state") if $self->debuglevel(1, 'insteon'); my $mode; if ($state eq 'off') { $mode = "09"; @@ -527,7 +527,7 @@ sub _is_info_request { my $is_info_request; if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { my $val = $msg{extra}; - main::print_log("[Insteon::Thermo_i1] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(); + main::print_log("[Insteon::Thermo_i1] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(1, 'insteon'); if ($val eq '00') { $self->_mode('off'); } elsif ($val eq '01') { @@ -719,16 +719,16 @@ sub _process_message { elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); #If this was a get request don't clear until data packet received - main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(); + main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::Thermo_i2CS] Clearing active message") if $self->debuglevel(); + main::print_log("[Insteon::Thermo_i2CS] Clearing active message") if $self->debuglevel(1, 'insteon'); $clear_message = 1; $$self{_ext_set_get_action} = undef; $self->_process_command_stack(%msg); } elsif ($$self{_ext_set_get_action} eq 'set_high_humid'){ main::print_log("[Insteon::Thermostat] Received ACK of high humid setpoint ". - "for ". $self->get_object_name) if $self->debuglevel(); + "for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->_high_humid_sp($$self{_high_humid_pending}); $clear_message = 1; $$self{_ext_set_get_action} = undef; @@ -737,7 +737,7 @@ sub _process_message { } elsif ($$self{_ext_set_get_action} eq 'set_low_humid'){ main::print_log("[Insteon::Thermostat] Received ACK of low humid setpoint ". - "for ". $self->get_object_name) if $self->debuglevel(); + "for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->_low_humid_sp($$self{_low_humid_pending}); $clear_message = 1; $$self{_ext_set_get_action} = undef; @@ -749,7 +749,7 @@ sub _process_message { if (substr($msg{extra},0,4) eq "0201") { $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Extended Set/Get Data ". - "Received for ". $self->get_object_name) if $self->debuglevel(); + "Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); #0 = 2 #14 = Cool SP #2 = 1 #16 = humidity #3 = day #18 = temp in Celsius High byte @@ -821,37 +821,37 @@ sub _process_message { } else { main::print_log("[Insteon::Thermo_i2CS] WARN: Unknown Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(); + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); } } elsif ($msg{command} eq "status_temp" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Temp Change Message ". - "from ". $self->get_object_name) if $self->debuglevel(); + "from ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->hex_short_temp($msg{extra}); } elsif ($msg{command} eq "status_mode" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Mode Change Message ". - "from ". $self->get_object_name) if $self->debuglevel(); + "from ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->status_mode($msg{extra}); } elsif ($msg{command} eq "status_cool" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Cool Setpoint Change Message ". - "from ". $self->get_object_name) if $self->debuglevel(); + "from ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->hex_cool($msg{extra}); } elsif ($msg{command} eq "status_humid" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Humidity Change Message ". - "from ". $self->get_object_name) if $self->debuglevel(); + "from ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->hex_humid($msg{extra}); } elsif ($msg{command} eq "status_heat" && !$msg{is_ack}){ $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); main::print_log("[Insteon::Thermo_i2CS] Received Heat Setpoint Change Message ". - "from ". $self->get_object_name) if $self->debuglevel(); + "from ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); $self->hex_heat($msg{extra}); } else { @@ -865,7 +865,7 @@ sub _is_info_request { my $is_info_request; if ($cmd eq 'thermostat_control' && $$self{_control_action} eq "mode") { my $val = $msg{extra}; - main::print_log("[Insteon::Thermo_i2CS] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(); + main::print_log("[Insteon::Thermo_i2CS] Processing is_info_request for $cmd with value: $val") if $self->debuglevel(1, 'insteon'); if ($val eq '09') { $self->_mode('Off'); } elsif ($val eq '04') { @@ -1062,7 +1062,7 @@ Sets system mode to argument: 'off', 'heat', 'cool', 'auto', 'program_heat', sub mode{ my ($self, $state) = @_; $state = lc($state); - main::print_log("[Insteon::Thermostat] Mode $state") if $self->debuglevel(); + main::print_log("[Insteon::Thermostat] Mode $state") if $self->debuglevel(1, 'insteon'); my $mode; if ($state eq 'off') { $mode = "09"; @@ -1110,7 +1110,7 @@ Sets the high humidity setpoint. =cut sub high_humid_setpoint { my ($self, $value) = @_; - main::print_log("[Insteon::Thermo_i2CS] Setting high humid setpoint -> $value") if $self->debuglevel(); + main::print_log("[Insteon::Thermo_i2CS] Setting high humid setpoint -> $value") if $self->debuglevel(1, 'insteon'); if($value !~ /^\d+$/){ main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint $value not numeric"); return; @@ -1134,7 +1134,7 @@ Sets the low humidity setpoint. =cut sub low_humid_setpoint { my ($self, $value) = @_; - main::print_log("[Insteon::Thermo_i2CS] Setting low humid setpoint -> $value") if $self->debuglevel(); + main::print_log("[Insteon::Thermo_i2CS] Setting low humid setpoint -> $value") if $self->debuglevel(1, 'insteon'); if($value !~ /^\d+$/){ main::print_log("[Insteon::Thermo_i2CS] ERROR: Setpoint $value not numeric"); return; diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 7089896ec..2b91d37f2 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -218,7 +218,7 @@ sub check_for_data { } else { - &::print_log("[Insteon_PLM] DEBUG2: PLM command timer expired but no transmission in place. Moving on...") if $self->debuglevel(2); + &::print_log("[Insteon_PLM] DEBUG2: PLM command timer expired but no transmission in place. Moving on...") if $self->debuglevel(2, 'insteon'); $self->clear_active_message(); $self->process_queue(); } @@ -388,16 +388,18 @@ sub _send_cmd { # determine the delay from the point that the message was created to # the point that it is queued my $incurred_delay_time = $message->seconds_delayed; - &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " - . sprintf('%.2f',$incurred_delay_time) . " seconds; starting hop-count: " - . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $message->setby->debuglevel(2); if ($message->isa('Insteon::X10Message')) { # is x10; so, be slow + &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " + . sprintf('%.2f',$incurred_delay_time) . " seconds") if $self->debuglevel(2, 'insteon'); $command = $prefix{x10_send} . $command; $delay = $$self{xmit_x10_delay}; # clear command timeout so that we don't wait for an insteon ack before sending the next command } else { my $command_type = $message->command_type; + &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " + . sprintf('%.2f',$incurred_delay_time) . " seconds; starting hop-count: " + . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $message->setby->debuglevel(2, 'insteon'); $command = $prefix{$command_type} . $command; if ($command_type eq 'all_link_send' or $command_type eq 'insteon_send' or $command_type eq 'insteon_ext_send' or $command_type eq 'all_link_direct_cleanup') { @@ -412,8 +414,10 @@ sub _send_cmd { } else { - &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $message->setby->debuglevel(3); - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $message->setby->debuglevel(4); + my $debug_obj = $self; + $debug_obj = $message->setby if ($message->can('setby') && ref $message->setby); + &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $debug_obj->debuglevel(3, 'insteon'); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $debug_obj->debuglevel(4, 'insteon'); my $data = pack("H*",$command); $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; @@ -442,7 +446,7 @@ sub _parse_data { # it is possible that a fragment exists from a previous attempt; so, if it exists, prepend it if ($$self{_data_fragment}) { - &::print_log("[Insteon_PLM] DEBUG3: Prepending prior data fragment: $$self{_data_fragment}") if $self->debuglevel(3); + &::print_log("[Insteon_PLM] DEBUG3: Prepending prior data fragment: $$self{_data_fragment}") if $self->debuglevel(3, 'insteon'); # maintain a copy of the parsed data fragment $$self{_prior_data_fragment} = $$self{_data_fragment}; # append if not a repeat @@ -456,7 +460,7 @@ sub _parse_data { $$self{_prior_data_fragment} = ''; } - &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $self->debuglevel(3); + &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $self->debuglevel(3, 'insteon'); # begin by pulling out any PLM ack/nacks my $prev_cmd = ''; @@ -492,7 +496,9 @@ sub _parse_data { $entered_ack_loop = 1; if ($parsed_data =~ /^($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($prefix{all_link_first_rec}15)|($prefix{all_link_next_rec}15)|($badcmd)$/) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->active_message->setby->debuglevel(4); + my $debug_obj = $self; + $debug_obj = $self->active_message->setby if ($self->active_message->can('setby') && ref $self->active_message->setby); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $debug_obj->debuglevel(4, 'insteon'); my $ret_code = substr($parsed_data,length($parsed_data)-2,2); my $record_type = substr($parsed_data,0,4); my $message_data = substr($parsed_data,4,length($parsed_data)-4); @@ -515,7 +521,7 @@ sub _parse_data { package main; eval ($self->active_message->success_callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->active_message->setby->debuglevel(1); + if $@ and $self->active_message->setby->debuglevel(1, 'insteon'); package Insteon_PLM; } # clear the active message because we're done @@ -523,8 +529,10 @@ sub _parse_data { } else { + my $debug_obj = $self; + $debug_obj = $self->active_message->setby if ($self->active_message->can('setby') && ref $self->active_message->setby); &::print_log("[Insteon_PLM] DEBUG3: Received PLM acknowledge: " - . $pending_message->to_string) if $self->active_message->setby->debuglevel(3); + . $pending_message->to_string) if $debug_obj->debuglevel(3, 'insteon'); } # X10 messages don't ACK back on the powerline, so clear them if the PLM acknowledges @@ -552,7 +560,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->active_message->setby->debuglevel(1); + if $@ and $self->active_message->setby->debuglevel(1, 'insteon'); package Insteon_PLM; } } @@ -579,7 +587,7 @@ sub _parse_data { $self->_aldb->scandatetime(&main::get_tickcount); &::print_log("[Insteon_PLM] " . $self->get_object_name . " completed link memory scan: status: " . $self->_aldb->health()) - if $self->debuglevel(); + if $self->debuglevel(1, 'insteon'); if ($$self{_mem_callback}) { my $callback = $$self{_mem_callback}; @@ -587,7 +595,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during nack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon_PLM; } } @@ -637,7 +645,7 @@ sub _parse_data { package main; eval ($callback); &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1); + if $@ and $self->debuglevel(1, 'insteon'); package Insteon_PLM; } # clear the active message because we're done @@ -673,7 +681,7 @@ sub _parse_data { my $unknown_obj = &Insteon::get_object($unknown_deviceid, '01'); if ($unknown_obj) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $unknown_obj->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $unknown_obj->debuglevel(4, 'insteon'); &::print_log("[Insteon_PLM] WARN: encountered '$parsed_data' " . "from " . $unknown_obj->get_object_name() . " with command: $unknown_command, but expected '$ackcmd'."); @@ -681,7 +689,7 @@ sub _parse_data { } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); &::print_log("[Insteon_PLM] ERROR: encountered '$parsed_data' " . "that does not match any known device ID (expected '$ackcmd')." . " Discarding received data."); @@ -714,7 +722,7 @@ sub _parse_data { if ($previous_parsed_data eq $parsed_data){ # guard against repeats - ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $self->debuglevel(3); + ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $self->debuglevel(3, 'insteon'); next; } $previous_parsed_data = $parsed_data; # and, now reinitialize @@ -730,10 +738,10 @@ sub _parse_data { { #Insteon Standard Received my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); if (ref $find_obj) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4, 'insteon'); } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); } $self->on_standard_insteon_received($message_data); } @@ -741,29 +749,29 @@ sub _parse_data { { #Insteon Extended Received my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); if (ref $find_obj) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4, 'insteon'); } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); } $self->on_extended_insteon_received($message_data); } elsif($parsed_prefix eq $prefix{x10_received} and ($message_length == 8)) { #X10 Received - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); my $x10_message = new Insteon::X10Message($parsed_data); my $x10_data = $x10_message->get_formatted_data(); - &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $self->debuglevel(3); + &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $self->debuglevel(3, 'insteon'); &::process_serial_data($x10_data,undef,$self); } elsif ($parsed_prefix eq $prefix{all_link_complete} and ($message_length == 20)) { #ALL-Linking Completed - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); my $link_address = substr($message_data,4,6); - &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $self->debuglevel(2); + &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $self->debuglevel(2, 'insteon'); if ($self->active_message->success_callback){ main::print_log("[Insteon::Insteon_PLM] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $self->debuglevel(4); + . $self->active_message->success_callback) if $self->debuglevel(4, 'insteon'); package main; eval $self->active_message->success_callback; ::print_log("[Insteon::Insteon_PLM] problem w/ success callback: $@") if $@; @@ -782,30 +790,30 @@ sub _parse_data { my $failure_device = substr($message_data,2,6); my $failed_object = &Insteon::get_object($failure_device,'01'); if (ref $failed_object){ - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $failed_object->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $failed_object->debuglevel(4, 'insteon'); &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure from " . $failed_object->get_object_name - . " for all link group: $failure_group. Trying a direct cleanup.") if $failed_object->debuglevel(2); + . " for all link group: $failure_group. Trying a direct cleanup.") if $failed_object->debuglevel(2, 'insteon'); my $message = new Insteon::InsteonMessage('all_link_direct_cleanup', $failed_object, $self->active_message->command, $failure_group); push(@{$$failed_object{command_stack}}, $message); $failed_object->_process_command_stack(); } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); &::print_log("[Insteon_PLM] Received all-link cleanup failure from an unkown device id: " . "$failure_device and for all link group: $failure_group. You may " . "want to run delete orphans to remove this link from your PLM"); } } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure." - . " But there is no pending message.") if $self->debuglevel(2); + . " But there is no pending message.") if $self->debuglevel(2, 'insteon'); } } elsif ($parsed_prefix eq $prefix{all_link_record} and ($message_length == 20)) { #ALL-Link Record Response - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); - &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $self->debuglevel(2); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $self->debuglevel(2, 'insteon'); $self->_aldb->parse_alllink($message_data); # before doing the next, make sure that the pending command # (if it sitll exists) is pulled from the queue @@ -815,18 +823,18 @@ sub _parse_data { } elsif ($parsed_prefix eq $prefix{plm_user_reset} and ($message_length == 4)) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); main::print_log("[Insteon_PLM] Detected PLM user reset to factory defaults"); } elsif ($parsed_prefix eq $prefix{all_link_clean_status} and ($message_length == 6)) { #ALL-Link Cleanup Status Report - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); my $cleanup_ack = substr($message_data,0,2); if ($cleanup_ack eq '15') { &::print_log("[Insteon_PLM] WARN1: All-link cleanup failure for scene: " . $self->active_message->setby->get_object_name . ". Retrying in 1 second.") - if $self->active_message->setby->debuglevel(1); + if $self->active_message->setby->debuglevel(1, 'insteon'); $self->retry_active_message(); # except that we should cause a bit of a delay to let things settle out $self->_set_timeout('xmit', 1000); @@ -836,7 +844,7 @@ sub _parse_data { { my $message_to_string = ($self->active_message) ? $self->active_message->to_string() : ""; &::print_log("[Insteon_PLM] Received all-link cleanup success: $message_to_string") - if $self->active_message->setby->debuglevel(); + if $self->active_message->setby->debuglevel(1, 'insteon'); if (ref $self->active_message && ref $self->active_message->setby){ my $object = $self->active_message->setby; $object->is_acknowledged(1); @@ -853,14 +861,14 @@ sub _parse_data { if ($self->active_message){ my $nack_delay = ($::config_parms{Insteon_PLM_disable_throttling}) ? 0.3 : 1.0; &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy. Resending command" - . " after delaying for $nack_delay second") if $self->debuglevel(3); + . " after delaying for $nack_delay second") if $self->debuglevel(3, 'insteon'); $self->_set_timeout('xmit',$nack_delay * 1000); $self->active_message->no_hop_increase(1); $self->retry_active_message(); $process_next_command = 0; } else { &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy." - . " No message to resend.") if $self->debuglevel(3); + . " No message to resend.") if $self->debuglevel(3, 'insteon'); } $nack_count++; } @@ -869,7 +877,7 @@ sub _parse_data { if ($parsed_data ne ''){ $$self{_data_fragment} .= $parsed_data; ::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " - . $parsed_data) if( $self->debuglevel(3)); + . $parsed_data) if( $self->debuglevel(3, 'insteon')); } } else @@ -879,7 +887,7 @@ sub _parse_data { unless (($parsed_data eq $$self{_prior_data_fragment}) or ($parsed_data eq $$self{_data_fragment})) { $$self{_data_fragment} .= $parsed_data; main::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " - . $parsed_data) if( $self->debuglevel(3)); + . $parsed_data) if( $self->debuglevel(3, 'insteon')); } } } @@ -887,7 +895,7 @@ sub _parse_data { unless( $entered_rcv_loop or $$self{_data_fragment}) { $$self{_data_fragment} = $residue_data; main::print_log("[Insteon_PLM] DEBUG3: Saving residue data fragment: " - . $residue_data) if( $residue_data and $self->debuglevel(3)); + . $residue_data) if( $residue_data and $self->debuglevel(3, 'insteon')); } if ($process_next_command) { From 937006b9df5718a189b4b859cf8e028313d4b33c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 11 Nov 2013 17:20:00 -0800 Subject: [PATCH 314/330] Insteon: Fix Bug Preventing Delete Orphans Due to Is_Deaf Error Closes #310 --- lib/Insteon/AllLinkDatabase.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 003db8636..dd24473b4 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -421,7 +421,7 @@ sub delete_orphan_links # first, make sure that the health of ALDB is ok if ($self->health ne 'good') { - if ($self->is_deaf) + if ($$self{device}->is_deaf) { &::print_log("[Insteon::AllLinkDatabase] Delete orphan links: ignoring link from deaf device: $selfname"); From 4e06fb4de9bc2b5bf1910ef76ed1d4413d53a363 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 11 Nov 2013 17:25:00 -0800 Subject: [PATCH 315/330] Insteon: Fix Bug Which Converted Object Names to Lowercase It looks like the routine lc is greedy and will convert multiple concatenated strings. Closed #309 --- lib/Insteon/BaseInsteon.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c25350b06..7328b1d66 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1432,7 +1432,7 @@ sub link_to_interface } elsif ($step == 2){ #Add Link from PLM->object $success_callback = $success_callback_prefix . "\"3\")"; - my $link_info = "deviceid=" . lc $self->device_id . " group=$p_group is_controller=0 " . + my $link_info = "deviceid=" . lc ($self->device_id) . " group=$p_group is_controller=0 " . "callback=$success_callback failure_callback=$failure_callback"; $self->interface->add_link($link_info); } @@ -1454,7 +1454,7 @@ sub link_to_interface elsif ($step == 4){ #Add surrogate link on PLM if surrogate exists $success_callback = $success_callback_prefix . "\"5\")"; my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( deviceid=> lc $self->device_id, + my %link_info = ( deviceid=> lc($self->device_id), group => $surrogate_group, is_controller => 1, callback => "$success_callback", failure_callback=> "$failure_callback", @@ -1542,7 +1542,7 @@ sub unlink_to_interface elsif ($step == 1) { #Delete link on the PLM $success_callback = $success_callback_prefix . "'2')"; $self->interface->delete_link( - deviceid => lc $self->device_id, + deviceid => lc($self->device_id), group=> $p_group, is_controller=>0, callback=>$success_callback, failure_callback=>$failure_callback); @@ -1566,7 +1566,7 @@ sub unlink_to_interface elsif ($step == 3){ #Delete surrogate link on PLM if surrogate exists $success_callback = $success_callback_prefix . "'4')"; my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( deviceid=> lc $self->device_id, + my %link_info = ( deviceid=> lc($self->device_id), group => $surrogate_group, is_controller => 1, callback => "$success_callback", failure_callback=> "$failure_callback", From a159c571281730fef7162e7e671d8bd2840f7189 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 11 Nov 2013 17:32:00 -0800 Subject: [PATCH 316/330] Insteon: Check if Hop_Array is Defined Before Printing Closes #311 --- lib/Insteon/BaseInsteon.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index c25350b06..7f15cc94e 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1877,7 +1877,9 @@ sub log_aldb_status { my ($self) = @_; main::print_log( " Device ID: ".$self->device_id()); - main::print_log( " Hop Count: ".$self->default_hop_count()." :: [". join("",@{$$self{hop_array}})."]"); + my $hop_array; + $hop_array = join("",@{$$self{hop_array}}) if (defined($$self{hop_array})); + main::print_log( " Hop Count: ".$self->default_hop_count()." :: [$hop_array]"); main::print_log( "Engine Version: ".$self->engine_version()); my $aldb = $self->get_root()->_aldb; if ($aldb) From fd47e540eefcb2c9346114e9abca249f7ee9edd8 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Nov 2013 17:25:00 -0800 Subject: [PATCH 317/330] Insteon: Clear Active Message Before Calling Retry Failure Callback If active message is not cleared, subsequent messages which are cued by the Retry Failure Callback will continue to call the Callback over and over. --- lib/Insteon/BaseInterface.pm | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 9e97f5135..0de64a645 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -381,20 +381,26 @@ sub process_queue &main::print_log("[Insteon::BaseInterface] WARN! Unable to clear acknowledge for " . ((defined($failed_message->setby)) ? $failed_message->setby->get_object_name : "undefined")); } - # may instead want a "failure" callback separate from success callback - if ($failed_message->failure_callback) + + # Get failed message details before clearing + my $callback = $failed_message->failure_callback; + my $setby = $failed_message->setby; + # clear active message + $self->clear_active_message(); + + if ($callback) { &::print_log("[Insteon::BaseInterface] WARN: Message Timeout: Now calling callback: " . - $failed_message->failure_callback) if $self->debuglevel(1, 'insteon'); - $failed_message->setby->failure_reason('timeout') - if (defined($failed_message->setby) and $failed_message->setby->can('failure_reason')); + $callback) if $self->debuglevel(1, 'insteon'); + $setby->failure_reason('timeout') + if (defined($setby) and $setby->can('failure_reason')); package main; - eval $failed_message->failure_callback; + eval $callback; &::print_log("[Insteon::BaseInterface] problem w/ retry callback: $@") if $@; package Insteon::BaseInterface; } - # clear active message - $self->clear_active_message(); + + #Any other outgoing messages pending in the queue? $self->process_queue(); } } From 694aa8ffde58f234d861f75c3a9c8ebb038db72e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Nov 2013 17:32:00 -0800 Subject: [PATCH 318/330] Insteon: Report Skipped Devices in List of Failed Objects When syncing an object if a device is skipped because the device is out of sync, the object will be reported in the failed objects list at the end of the sync process to help identify potentially out-of-sync devices. --- lib/Insteon.pm | 8 ++++---- lib/Insteon/BaseInsteon.pm | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 6451cf387..fafb42396 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -561,11 +561,11 @@ sub _get_next_linksync my $_sync_failure_cnt = scalar @_sync_device_failures; if ($_sync_failure_cnt) { - &main::print_log("[Sync all links] However, some failures were noted:"); + &main::print_log("[Sync all links] WARN! Failures occured, " + ."some links for the following objects remain out-of-sync:"); for my $failed_obj (@_sync_device_failures) { - &main::print_log("[Sync all links] WARN: failure occurred when syncing " - . $failed_obj->get_object_name); + &main::print_log("[Sync all links] " . $failed_obj->get_object_name); } } @@ -583,7 +583,7 @@ the failed device to the module global variable @_sync_device_failures. sub _get_next_linksync_failure { push @_sync_device_failures, $current_sync_device; - &main::print_log("[Sync all links] WARN: failure occurred when scanning " + &main::print_log("[Sync all links] WARN: failure occurred when syncing links for " . $current_sync_device->get_object_name . ". Moving on..."); &_get_next_linksync(); diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index d5ce42cec..7d1e85e3c 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2995,6 +2995,8 @@ sub sync_links # Intialize Variables @{$$self{sync_queue}} = (); # reset the work queue $$self{sync_queue_callback} = ($callback) ? $callback : undef; + $$self{sync_queue_failure_callback} = ($failure_callback) ? $failure_callback : undef; + $$self{sync_queue_failure} = undef; my $self_link_name = $self->get_object_name; my $insteon_object = $self->interface; my $interface_object = Insteon::active_interface(); @@ -3019,6 +3021,7 @@ sub sync_links ."linked to this device, but no links will be added to $self_link_name. Please rescan this device and attempt " ."sync links again."); $insteon_object_is_syncable = 0; + $$self{sync_queue_failure} = 1; } # 1. Does a controller link exist for Device-> PLM @@ -3097,6 +3100,7 @@ sub sync_links ."from $self_link_name because the aldb of $member_name is " . $member_root->_aldb->health; push @{$$self{sync_queue}}, \%link_req; + $$self{sync_queue_failure} = 1; } # 4. Is the responder link accurate @@ -3209,11 +3213,14 @@ sub _process_sync_queue { $link_member->add_link(%link_req); } } elsif ($$self{sync_queue_callback}) { + my $callback = $$self{sync_queue_callback}; + if ($$self{sync_queue_failure}){ + $callback = $$self{sync_queue_failure_callback}; + } package main; - eval ($$self{sync_queue_callback}); + eval ($callback); &::print_log("[Insteon::BaseController] error in sync links callback: " . $@) if $@ and $self->debuglevel(1, 'insteon'); - $$self{sync_queue_callback} = undef; package Insteon::BaseController; } else { main::print_log($self->get_object_name." completed sync links"); From d27ec930242134f9dac9db1f592d71d6fc10f6ef Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Nov 2013 17:34:00 -0800 Subject: [PATCH 319/330] Insteon: Resume Sync Queue After Failure of a Device Previously, if a single failure occured in a sync_queue, the whole queue was abandoned. This attempts to resume where the queue left off. --- lib/Insteon.pm | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index fafb42396..5df9206ca 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -562,7 +562,7 @@ sub _get_next_linksync if ($_sync_failure_cnt) { &main::print_log("[Sync all links] WARN! Failures occured, " - ."some links for the following objects remain out-of-sync:"); + ."some links involving the following objects remain out-of-sync:"); for my $failed_obj (@_sync_device_failures) { &main::print_log("[Sync all links] " . $failed_obj->get_object_name); @@ -582,10 +582,17 @@ the failed device to the module global variable @_sync_device_failures. sub _get_next_linksync_failure { - push @_sync_device_failures, $current_sync_device; + push @_sync_device_failures, $current_sync_device + unless (grep{$current_sync_device == $_} @_sync_device_failures); &main::print_log("[Sync all links] WARN: failure occurred when syncing links for " - . $current_sync_device->get_object_name . ". Moving on..."); + . $current_sync_device->get_object_name . ". Resuming sync queue if it exists."); + my $num_sync_queue = @{$$current_sync_device{sync_queue}}; + if ($num_sync_queue){ + $current_sync_device->_process_sync_queue(); + } + else { #No other pending links in the queue &_get_next_linksync(); + } } From d1a3ec8dd24d5362b638c6f4c98edbb28ad71924 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Nov 2013 17:38:00 -0800 Subject: [PATCH 320/330] Insteon: Report Out-of-Sync Devices in Failed Delete Orphans Log If a device is skipped because it was out of sync, it will now appear in the final summary listing the devices for which delete orphans failed. --- lib/Insteon/AllLinkDatabase.pm | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index 878f4057f..e6f37738e 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -440,6 +440,7 @@ sub delete_orphan_links # first, make sure that the health of ALDB is ok if ($self->health ne 'good' || $$self{device}->is_deaf) { + my $sent_to_failure = 0; if ($$self{device}->is_deaf) { ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not delete links on deaf device: $selfname"); } @@ -450,8 +451,17 @@ sub delete_orphan_links ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " . $self->health . ". Please rescan the link table of this device and rerun delete " . "orphans if necessary"); + #log the failure + $sent_to_failure = 1; + if ($failure_callback) { + package main; + eval ($failure_callback); + &::print_log("[Insteon::AllLinkDatabase] error in delete orphans failure callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } } - if (!$$self{device}->isa('Insteon_PLM')){ + if (!$$self{device}->isa('Insteon_PLM') && !$sent_to_failure){ $self->_process_delete_queue(); } return; From ec6cda4a4defa06b4fb4497f774f4f53aed7c22e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 14 Nov 2013 17:40:00 -0800 Subject: [PATCH 321/330] Insteon: Make Failed Device Reports Easier to Read Condense list of failed devices into a single line in the print log. --- lib/Insteon.pm | 42 +++++++++++++++------------------- lib/Insteon/AllLinkDatabase.pm | 11 +++++---- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 5df9206ca..e9984e30d 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -477,16 +477,14 @@ sub _get_next_linkscan { &main::print_log("[Scan all link tables] All tables have completed scanning"); my $_scan_failure_cnt = scalar @_scan_device_failures; - if ($_scan_failure_cnt) - { - &main::print_log("[Scan all link tables] However, some failures were noted:"); - for my $failed_obj (@_scan_device_failures) - { - &main::print_log("[Scan all link tables] WARN: failure occurred when scanning " - . $failed_obj->get_object_name); - } - } - + if ($_scan_failure_cnt){ + my $obj_list; + for my $failed_obj (@_scan_device_failures){ + $obj_list .= $failed_obj->get_object_name .", "; + } + ::print_log("[Scan all link tables] However, some failures " + ."were noted with the following devices: $obj_list"); + } } } @@ -559,16 +557,14 @@ sub _get_next_linksync { &main::print_log("[Sync all links] All links have completed syncing"); my $_sync_failure_cnt = scalar @_sync_device_failures; - if ($_sync_failure_cnt) - { - &main::print_log("[Sync all links] WARN! Failures occured, " - ."some links involving the following objects remain out-of-sync:"); - for my $failed_obj (@_sync_device_failures) - { - &main::print_log("[Sync all links] " . $failed_obj->get_object_name); - } - } - + if ($_sync_failure_cnt){ + my $obj_list; + for my $failed_obj (@_sync_device_failures){ + $obj_list .= $failed_obj->get_object_name .", "; + } + ::print_log("[Sync all links] WARN! Failures occured, " + ."some links involving the following objects remain out-of-sync: $obj_list"); + } } } @@ -591,7 +587,7 @@ sub _get_next_linksync_failure $current_sync_device->_process_sync_queue(); } else { #No other pending links in the queue - &_get_next_linksync(); + &_get_next_linksync(); } } @@ -901,7 +897,7 @@ sub init { @_insteon_link = (); } - + =item C Generates and sets the voice commands for all Insteon devices. @@ -1193,7 +1189,7 @@ sub _active_interface $$self{active_interface} = $interface if $interface; return $$self{active_interface}; } - + =item C Adds a list of objects to be tracked. diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index e6f37738e..eec478977 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -459,7 +459,7 @@ sub delete_orphan_links &::print_log("[Insteon::AllLinkDatabase] error in delete orphans failure callback: " . $@) if $@ and $self->{device}->debuglevel(1, 'insteon'); package Insteon::AllLinkDatabase; - } + } } if (!$$self{device}->isa('Insteon_PLM') && !$sent_to_failure){ $self->_process_delete_queue(); @@ -2592,11 +2592,12 @@ sub _process_delete_queue { ::print_log("[Insteon::ALDB_PLM] Delete All Links has Completed."); my $_delete_failure_cnt = scalar $$self{_delete_device_failures}; if ($_delete_failure_cnt) { - &main::print_log("[Insteon::ALDB_PLM] However, some failures were noted with the following devices:"); - for my $failed_obj (@{$$self{_delete_device_failures}}) { - ::print_log("[Insteon::ALDB_PLM] Failure on: " - . $failed_obj); + my $obj_list; + for my $failed_obj (@{$$self{_delete_device_failures}}){ + $obj_list .= $failed_obj .", "; } + ::print_log("[Insteon::ALDB_PLM] However, some failures were ". + "noted with the following devices: $obj_list"); } ::print_log("[Insteon::ALDB_PLM] A total of $$self{delete_queue_processed} orphaned link records were deleted."); ::print_log("[Insteon::ALDB_PLM] #### END DELETE ORPHAN LINKS ####"); From 20b514b7291f52053fe7d8896707cc04266c5624 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 15 Nov 2013 17:19:00 -0800 Subject: [PATCH 322/330] Insteon: Don't Call Debuglevel if Object Doesn't Exist --- lib/Insteon/BaseInterface.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 0de64a645..93d00e589 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -899,9 +899,13 @@ sub _is_duplicate_received { #This message still provides a data point on how many hops it is #taking for messages to arrive. $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); - }; ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " . $message_data . ", from ". $object->get_object_name) if $object->debuglevel(1, 'insteon'); + } + else { + ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " + . " from an unknown device. Id: $msg{source} Grp: $msg{group}") if $main::Debug{'insteon'}; + } } else { #Message was not in hash, so add it $$self{received_commands}{$key} = $curr_milli + $delay; From baa8de6d9cbad302a5d24ec7eeaee0458a45d346 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Fri, 15 Nov 2013 17:27:00 -0800 Subject: [PATCH 323/330] Insteon: Permit Syncing of Deaf Devices if Called for Individual Device --- lib/Insteon.pm | 3 ++- lib/Insteon/BaseInsteon.pm | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Insteon.pm b/lib/Insteon.pm index e9984e30d..58afb690f 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -550,8 +550,9 @@ sub _get_next_linksync . $current_sync_device->get_object_name . " (" . ($_sync_cnt - scalar @_sync_devices) . " of $_sync_cnt)"); + my $skip_deaf = 1; # pass first the success callback followed by the failure callback - $current_sync_device->sync_links($sync_req{'audit_mode'}, '&Insteon::_get_next_linksync()','&Insteon::_get_next_linksync_failure()'); + $current_sync_device->sync_links($sync_req{'audit_mode'}, '&Insteon::_get_next_linksync()','&Insteon::_get_next_linksync_failure()', $skip_deaf); } else { diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 7d1e85e3c..efe8ab1a7 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -2990,7 +2990,7 @@ it then loops through all of the links defined for a device and checks: sub sync_links { - my ($self, $audit_mode, $callback, $failure_callback) = @_; + my ($self, $audit_mode, $callback, $failure_callback, $skip_deaf) = @_; # Intialize Variables @{$$self{sync_queue}} = (); # reset the work queue @@ -3010,9 +3010,10 @@ sub sync_links # Warn if device is deaf or ALDB out of sync my $insteon_object_is_syncable = 1; - if ($insteon_object->is_deaf) { + if ($insteon_object->is_deaf && $skip_deaf) { ::print_log("[Insteon::BaseController] $self_link_name is deaf, only responder links will be added to devices " - ."controlled by this device."); + ."controlled by this device. To sync links on this device, put it in awake mode and run the 'Sync Links' " + ."command on this specific device."); $insteon_object_is_syncable = 0; } elsif ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty'){ From 7ec54651c3228af82c341fb5667b641137d72e35 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Sat, 16 Nov 2013 17:34:27 -0800 Subject: [PATCH 324/330] Normalize Line Endings in Repository to LF --- .gitattributes | 1 + bin/dir_to_file.bat | 6 +- bin/get_earthquakes.bat | 2 +- bin/holical.bat | 4 +- bin/ical2vsdb.bat | 4 +- bin/xAP-speech.pl | 492 +- code/common/cm11_control.pl | 74 +- code/common/cm11_monitor.pl | 196 +- code/common/dvd_player.pl | 154 +- code/common/email_marketplace_sales.pl | 442 +- code/common/froggyrita.pl | 142 +- code/common/holiday_ical.pl | 88 +- code/common/mp3_dj.pl | 574 +- .../SOAP_examples/vb.net/AssemblyInfo.vb | 64 +- code/examples/SOAP_examples/vb.net/Form1.resx | 420 +- code/examples/SOAP_examples/vb.net/Form1.vb | 464 +- .../Web References/MySoapRef/Reference.map | 10 +- .../Web References/MySoapRef/Reference.vb | 226 +- .../Web References/MySoapRef/mhsoap.wsdl | 198 +- .../vb.net/WebServiceTest.vbproj | 288 +- .../vb.net/WebServiceTest.vbproj.user | 96 +- code/examples/iButton_weather_station.pl | 462 +- code/examples/network_items_web.pl | 26 +- code/examples/test_weather_item.pl | 26 +- code/public/Brian/flagreminders.pl | 152 +- code/public/Brian/grlevel3.lst | 12 +- code/public/Brian/grlevel3.pl | 248 +- code/public/Brian/klier.mht | 248 +- .../old stuff i don't use anymore/calllog.pl | 516 +- .../old stuff i don't use anymore/callog.pl | 464 +- .../old stuff i don't use anymore/radar.pl | 160 +- .../tk_frames.pl | 158 +- .../tk_widgets.pl | 76 +- .../old stuff i don't use anymore/tracking.pl | 2788 ++--- code/public/Brian/phonestuff.pl | 68 +- code/public/Brian/radar-wx.pl | 386 +- code/public/Brian/stormwarning.pl | 86 +- code/public/Brian/xpl.pl | 112 +- code/public/Esensor_EM01.pl | 144 +- code/public/Ricardo/bin/get_tv_com | 494 +- code/public/Ricardo/bin/get_tv_com.bat | 2 +- code/public/Ricardo/bin/get_weather_ca | 716 +- code/public/Ricardo/code/tv_com.pl | 248 +- code/public/hvac_upb_thermostat.pl | 106 +- data/event_sounds.txt | 64 +- data/infrared/devicelib/Apple MAC.dvc | 32 +- data/infrared/devicelib/Motorola qip6200.dvc | 142 +- .../devicelib/Samsung HLT Series TV.dvc | 268 +- docs/faq.pod | 3578 +++--- docs/usenet_post.txt | 206 +- lib/Insteon.pm | 2740 ++-- lib/Insteon/AllLinkDatabase.pm | 5610 ++++----- lib/Insteon/BaseInsteon.pm | 7338 +++++------ lib/Insteon/BaseInterface.pm | 2040 +-- lib/Insteon/Controller.pm | 750 +- lib/Insteon/Irrigation.pm | 622 +- lib/Insteon/Lighting.pm | 2460 ++-- lib/Insteon/Message.pm | 2302 ++-- lib/Insteon/MessageDecoder.pm | 1906 +-- lib/Insteon/MessageDecoder_test.pl | 176 +- lib/Insteon/PLMTerminal.pl | 686 +- lib/Insteon_PLM.pm | 1980 +-- lib/Weather_davisvantageproii.pm | 1158 +- lib/X10_RF_rfxsensor.pm | 320 +- lib/ajax.pm | 432 +- lib/site/ControlX10/CM11.pm.old | 1678 +-- lib/site/MSN.pm | 1670 +-- lib/site/Tie/Hash.pm.original | 486 +- lib/site/Tk/CursorControl.pm | 1828 +-- lib/site/Tk/ToolBar.pm | 2192 ++-- lib/site/Tk/ToolBar/tkIcons | 390 +- lib/site/Tk/trans_cur.mask | 8 +- lib/site/Tk/trans_cur.xbm | 12 +- lib/site/Win32/TieRegistry.pm | 7602 +++++------ lib/site_win50/Win32/Registry.pm | 1094 +- lib/site_win50/Win32API/Registry.pm | 3584 +++--- lib/site_win50/Win32API/Registry/cRegistry.pc | 174 +- lib/site_win56/Win32/Registry.pm | 1094 +- lib/site_win56/Win32API/Registry.pm | 3584 +++--- lib/site_win56/Win32API/Registry/cRegistry.pc | 174 +- lib/site_win58/Win32/Registry.pm | 1094 +- lib/site_win58/Win32API/Registry.pm | 3584 +++--- lib/site_win58/Win32API/Registry/cRegistry.pc | 174 +- lib/xPL_Items.pm | 2732 ++-- web/bin/mhsoap.wsdl | 28 +- web/comics/dailystrips/strips.def.old | 9254 +++++++------- web/comics/dailystrips/strips.def.old2 | 10432 ++++++++-------- web/ia5/security/webcam.shtml | 28 +- web/iphone/icons/LICENSE.TXT | 14 +- web/lib/android.xsl | 22 +- web/lib/default.xsl | 22 +- web/lib/pod.css | 16 +- web/misc/actiontec_traffic.html | 224 +- web/newclock/GlobeClock.html | 192 +- web/newclock/MapClock.html | 190 +- web/newclock/OriginalLED.html | 184 +- web/newclock/alarm.pl | 120 +- web/newclock/calendar.js | 350 +- web/newclock/index.html | 408 +- web/newclock/index.pl | 340 +- web/robots.txt | 8 +- web/test/ajax_example1.shtml | 222 +- web/test/ajax_example2.shtml | 230 +- web/test/ajax_example3.shtml | 154 +- web/test/index.shtml | 24 +- 105 files changed, 51035 insertions(+), 51034 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/bin/dir_to_file.bat b/bin/dir_to_file.bat index 65e9dc5c3..a869a1876 100644 --- a/bin/dir_to_file.bat +++ b/bin/dir_to_file.bat @@ -1,3 +1,3 @@ -@echo off -dir %1:\ > .\%2 - +@echo off +dir %1:\ > .\%2 + diff --git a/bin/get_earthquakes.bat b/bin/get_earthquakes.bat index 2281275ce..e01ea40c1 100755 --- a/bin/get_earthquakes.bat +++ b/bin/get_earthquakes.bat @@ -1 +1 @@ -@mh -run get_earthquakes %1 %2 %3 %4 %5 %6 %7 %8 %9 +@mh -run get_earthquakes %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/bin/holical.bat b/bin/holical.bat index 8a3f1ffd9..bc4a38c21 100755 --- a/bin/holical.bat +++ b/bin/holical.bat @@ -1,2 +1,2 @@ -@mh -run holical %1 %2 %3 -@rem perl -S holical %1 %2 %3 +@mh -run holical %1 %2 %3 +@rem perl -S holical %1 %2 %3 diff --git a/bin/ical2vsdb.bat b/bin/ical2vsdb.bat index 0e7df62d5..68dadf2b4 100755 --- a/bin/ical2vsdb.bat +++ b/bin/ical2vsdb.bat @@ -1,2 +1,2 @@ -@mh -run ical2vsdb %1 %2 %3 -@rem perl -S ical2vsdb %1 %2 %3 +@mh -run ical2vsdb %1 %2 %3 +@rem perl -S ical2vsdb %1 %2 %3 diff --git a/bin/xAP-speech.pl b/bin/xAP-speech.pl index 544b1a012..7369534d4 100755 --- a/bin/xAP-speech.pl +++ b/bin/xAP-speech.pl @@ -1,247 +1,247 @@ -#!/usr/bin/perl - -=begin comment - -This program provides a simple xAP listener for the Festival text-to-speech synthesizer. - -Written by Chris Barrett, based on code provided by Bruce Winter. - -version 0.1 - 27 March 2005 -version 0.2 - 28 April 2005 - -Modified by Tim Sailer to support festival, flite and swift TTS systems. -11/07 - -=cut - -my $debug = 0; - -use strict; -use IO::Socket::INET; -use Getopt::Long; - # Setup constants -my $FORCE_LOCAL_HUB = 0; # set to 0 if no hub or default operation -my $XAP_PORT = 3639; -my $XAP_GUID = 'FFBA8300'; -my $XAP_ME = 'MHOUSE'; -# Use festival, flite, or swift for source -my $XAP_SOURCE = 'swift'; -my $XAP_INSTANCE; -my $MAXLEN = 1500; # Max size of a UDP packet -my $HBEAT_INTERVAL = 120; # Send every 2 minutes -my $fqfn; -my $room; -my $target; -my $xap_listen; - -my $help; -GetOptions( - 'room=s', \$room, - 'debug', \$debug, - 'help|h|?', \$help - ); -if ($help) - { - print "usage: $0 [--room=room] [--debug] [--help|h|?]\n"; - exit; - } - -if ($^O =~ /MSWin/i) - { - print "Sorry, but this only works on *nix systems\n"; - exit; - } - -if ($room eq "") - { - my $hostname = `hostname`; - chomp $hostname; - $hostname =~ s/^(.*?)\.(.*)$/$1/; - print "A room was not specified using --room= so I'm using the hostname [$hostname]\n" if $debug; - $room = $hostname; - } -$XAP_INSTANCE = $room; - -if ($XAP_SOURCE eq "festival") - { - $fqfn = `which festival`; - chomp $fqfn; - if ($fqfn =~ /no festival in/) - { - print "Could not find festival in the PATH\n"; - exit; - } - } -elsif ($XAP_SOURCE eq "flite") - { - $fqfn = `which flite`; - chomp $fqfn; - if (($fqfn =~ /no flite in/) || ($fqfn eq "")) - { - print "Could not find flite in the PATH\n"; - exit; - } - } -elsif ($XAP_SOURCE eq "swift") - { - $fqfn = `which swift`; - chomp $fqfn; - if (($fqfn =~ /no swift in/)||($fqfn eq "")) - { - if ( -e "/opt/swift/bin/swift") - { - $fqfn="/opt/swift/bin/swift"; - } - else - { - print "Could not find swift binary"; - exit; - } - } - } - - -print "Using $fqfn for speech\n" if $debug; - -my $my_address = lc("$XAP_ME.$XAP_SOURCE.$XAP_INSTANCE"); -print "My address is $my_address\n" if $debug; - - # Create a broadcast socket for sending data -my $xap_send = new IO::Socket::INET - ->new(PeerPort => $XAP_PORT, Proto => 'udp', PeerAddr => inet_ntoa(INADDR_BROADCAST), Broadcast => 1 ) or - die "Could not create xap sender\n"; - -if ($FORCE_LOCAL_HUB) -{ - for my $p (49152 .. 65535) { - $XAP_PORT = $p; - last if $xap_listen = new IO::Socket::INET - ->new(LocalAddr => 'localhost', LocalPort => $p, Proto => 'udp'); - } -} -else -{ - - $xap_listen = new IO::Socket::INET - ->new(LocalAddr=>inet_ntoa(INADDR_ANY), LocalPort => $XAP_PORT, Proto => 'udp', Broadcast => 1 ); - - # If a hub is not active, bind directly for listening - if ($xap_listen) - { - print "No hub active. Listening on broadcast socket ", $xap_listen->sockport(), "\n" if $debug; - } -else -{ - # Hub is active. Loop until we find an available port - print "Hub is active, search for free relay port\n" if $debug; - for my $p (49152 .. 65535) { - $XAP_PORT = $p; - last if $xap_listen = new IO::Socket::INET - ->new(LocalAddr => 'localhost', LocalPort => $p, Proto => 'udp'); - } - die "Could not create xap listener\n" unless $xap_listen; - print "Listening on relay socket ", $xap_listen->sockport(), "\n" if $debug; - } -} - -&send_heartbeat; - # Do a loop -while (1) - { - select undef, undef, undef, 1.0; # Sleep a bit - my $time = time; - &send_heartbeat if !($time % $HBEAT_INTERVAL); - - # Check for incoming xap traffic - my $rin = ''; - vec($rin, $xap_listen->fileno(), 1) = 1; - if (select($rin, undef, undef, 0)) - { - my $xap_rx_msg; - recv($xap_listen, $xap_rx_msg, $MAXLEN, 0) or die "recv: $!"; - print "\n------------- Incoming message -------------\n$xap_rx_msg\n" if $debug; - - (my $header) = $xap_rx_msg =~ /xap-header\n\{(.*?)\}/is; - if ($header =~ /\nclass\=tts.speak\n/i) - { - print "header = [$header]\n" if $debug; - ($target) = $header =~ /target=(.*?)\n/is; - $target = lc $target; - print "target=[$target]\n" if $debug; - if ( ($target eq "") || ($target eq "*") || ($target eq "*.".$XAP_SOURCE.".*") || ($target eq $my_address)) - { - &handle_tts_speak($xap_rx_msg); - } - else - { - print "Not for me\n" if $debug; - } - } - } - } - -sub send_heartbeat - { - print "Sending heartbeat on port ", $xap_send->peerport, "\n" if ($debug); - print $xap_send "xap-hbeat\n{\nv=12\nhop=1\nuid=$XAP_GUID\nclass=xap-hbeat.alive\n" . - "source=$XAP_ME.$XAP_SOURCE.$XAP_INSTANCE\ninterval=$HBEAT_INTERVAL\nport=$XAP_PORT\npid=$$\n}\n"; - } - -sub handle_tts_speak - { - my $xap_rx_msg = shift; - - (my $block) = $xap_rx_msg =~ /tts.speak\n\{(.*?)\}/is; - print "block = [$block]\n" if $debug; - - (my $present, my $say) = $block =~ /(say)=(.*?)\n/is; - print "say=[$say]\n" if ($present && $debug); - - my $volume = 50; # The SABLE spec says that the default is "medium" - (my $present, $volume) = $block =~ /(volume)=(.*?)\n/is; - print "volume=[$volume]\n" if ($present && $debug); - - # The SABLE spec says that the default is the "default gender for the engine"; - (my $present, my $voice) = $block =~ /(voice)=(.*?)\n/is; - print "voice=[$voice]\n" if ($present && $debug); - - (my $present, my $priority) = $block =~ /(priority)=(.*?)\n/is; - print "priority=[$priority]\n" if ($present && $debug); - - (my $present, my $rooms) = $block =~ /(rooms)=(.*?)\n/is; - print "rooms=[$rooms]\n" if ($present && $debug); - $rooms = lc($rooms); - - if ( ($rooms eq "") || ($rooms eq "all") || ($rooms eq $room) ) { - &speak($say,$volume,$voice); - } - } - -sub speak - { - my $text = shift; - my $volume = shift; - my $voice = shift; - my $cmd; - $volume = $volume / 100; # SABLE uses a floating point-number between zero and 1 to represent volume. - - # The SABLE tags are currently not working properly - it's reading them out - # $text = "".$text."" if ($volume != 0.5); - # $text = "".$text."" if ($voice ne ""); - - if ($XAP_SOURCE eq "festival") -{ - $cmd = "echo \"$text\" | $fqfn --tts"; -} -elsif ($XAP_SOURCE eq "flite") -{ - $cmd = "$fqfn -t \"$text\""; -} -elsif ($XAP_SOURCE eq "swift") -{ - $cmd = "$fqfn \"$text\""; -} - print "cmd=[$cmd]\n" if $debug; - print "cmd=[$cmd]\n"; - system $cmd; +#!/usr/bin/perl + +=begin comment + +This program provides a simple xAP listener for the Festival text-to-speech synthesizer. + +Written by Chris Barrett, based on code provided by Bruce Winter. + +version 0.1 - 27 March 2005 +version 0.2 - 28 April 2005 + +Modified by Tim Sailer to support festival, flite and swift TTS systems. +11/07 + +=cut + +my $debug = 0; + +use strict; +use IO::Socket::INET; +use Getopt::Long; + # Setup constants +my $FORCE_LOCAL_HUB = 0; # set to 0 if no hub or default operation +my $XAP_PORT = 3639; +my $XAP_GUID = 'FFBA8300'; +my $XAP_ME = 'MHOUSE'; +# Use festival, flite, or swift for source +my $XAP_SOURCE = 'swift'; +my $XAP_INSTANCE; +my $MAXLEN = 1500; # Max size of a UDP packet +my $HBEAT_INTERVAL = 120; # Send every 2 minutes +my $fqfn; +my $room; +my $target; +my $xap_listen; + +my $help; +GetOptions( + 'room=s', \$room, + 'debug', \$debug, + 'help|h|?', \$help + ); +if ($help) + { + print "usage: $0 [--room=room] [--debug] [--help|h|?]\n"; + exit; + } + +if ($^O =~ /MSWin/i) + { + print "Sorry, but this only works on *nix systems\n"; + exit; + } + +if ($room eq "") + { + my $hostname = `hostname`; + chomp $hostname; + $hostname =~ s/^(.*?)\.(.*)$/$1/; + print "A room was not specified using --room= so I'm using the hostname [$hostname]\n" if $debug; + $room = $hostname; + } +$XAP_INSTANCE = $room; + +if ($XAP_SOURCE eq "festival") + { + $fqfn = `which festival`; + chomp $fqfn; + if ($fqfn =~ /no festival in/) + { + print "Could not find festival in the PATH\n"; + exit; + } + } +elsif ($XAP_SOURCE eq "flite") + { + $fqfn = `which flite`; + chomp $fqfn; + if (($fqfn =~ /no flite in/) || ($fqfn eq "")) + { + print "Could not find flite in the PATH\n"; + exit; + } + } +elsif ($XAP_SOURCE eq "swift") + { + $fqfn = `which swift`; + chomp $fqfn; + if (($fqfn =~ /no swift in/)||($fqfn eq "")) + { + if ( -e "/opt/swift/bin/swift") + { + $fqfn="/opt/swift/bin/swift"; + } + else + { + print "Could not find swift binary"; + exit; + } + } + } + + +print "Using $fqfn for speech\n" if $debug; + +my $my_address = lc("$XAP_ME.$XAP_SOURCE.$XAP_INSTANCE"); +print "My address is $my_address\n" if $debug; + + # Create a broadcast socket for sending data +my $xap_send = new IO::Socket::INET + ->new(PeerPort => $XAP_PORT, Proto => 'udp', PeerAddr => inet_ntoa(INADDR_BROADCAST), Broadcast => 1 ) or + die "Could not create xap sender\n"; + +if ($FORCE_LOCAL_HUB) +{ + for my $p (49152 .. 65535) { + $XAP_PORT = $p; + last if $xap_listen = new IO::Socket::INET + ->new(LocalAddr => 'localhost', LocalPort => $p, Proto => 'udp'); + } +} +else +{ + + $xap_listen = new IO::Socket::INET + ->new(LocalAddr=>inet_ntoa(INADDR_ANY), LocalPort => $XAP_PORT, Proto => 'udp', Broadcast => 1 ); + + # If a hub is not active, bind directly for listening + if ($xap_listen) + { + print "No hub active. Listening on broadcast socket ", $xap_listen->sockport(), "\n" if $debug; + } +else +{ + # Hub is active. Loop until we find an available port + print "Hub is active, search for free relay port\n" if $debug; + for my $p (49152 .. 65535) { + $XAP_PORT = $p; + last if $xap_listen = new IO::Socket::INET + ->new(LocalAddr => 'localhost', LocalPort => $p, Proto => 'udp'); + } + die "Could not create xap listener\n" unless $xap_listen; + print "Listening on relay socket ", $xap_listen->sockport(), "\n" if $debug; + } +} + +&send_heartbeat; + # Do a loop +while (1) + { + select undef, undef, undef, 1.0; # Sleep a bit + my $time = time; + &send_heartbeat if !($time % $HBEAT_INTERVAL); + + # Check for incoming xap traffic + my $rin = ''; + vec($rin, $xap_listen->fileno(), 1) = 1; + if (select($rin, undef, undef, 0)) + { + my $xap_rx_msg; + recv($xap_listen, $xap_rx_msg, $MAXLEN, 0) or die "recv: $!"; + print "\n------------- Incoming message -------------\n$xap_rx_msg\n" if $debug; + + (my $header) = $xap_rx_msg =~ /xap-header\n\{(.*?)\}/is; + if ($header =~ /\nclass\=tts.speak\n/i) + { + print "header = [$header]\n" if $debug; + ($target) = $header =~ /target=(.*?)\n/is; + $target = lc $target; + print "target=[$target]\n" if $debug; + if ( ($target eq "") || ($target eq "*") || ($target eq "*.".$XAP_SOURCE.".*") || ($target eq $my_address)) + { + &handle_tts_speak($xap_rx_msg); + } + else + { + print "Not for me\n" if $debug; + } + } + } + } + +sub send_heartbeat + { + print "Sending heartbeat on port ", $xap_send->peerport, "\n" if ($debug); + print $xap_send "xap-hbeat\n{\nv=12\nhop=1\nuid=$XAP_GUID\nclass=xap-hbeat.alive\n" . + "source=$XAP_ME.$XAP_SOURCE.$XAP_INSTANCE\ninterval=$HBEAT_INTERVAL\nport=$XAP_PORT\npid=$$\n}\n"; + } + +sub handle_tts_speak + { + my $xap_rx_msg = shift; + + (my $block) = $xap_rx_msg =~ /tts.speak\n\{(.*?)\}/is; + print "block = [$block]\n" if $debug; + + (my $present, my $say) = $block =~ /(say)=(.*?)\n/is; + print "say=[$say]\n" if ($present && $debug); + + my $volume = 50; # The SABLE spec says that the default is "medium" + (my $present, $volume) = $block =~ /(volume)=(.*?)\n/is; + print "volume=[$volume]\n" if ($present && $debug); + + # The SABLE spec says that the default is the "default gender for the engine"; + (my $present, my $voice) = $block =~ /(voice)=(.*?)\n/is; + print "voice=[$voice]\n" if ($present && $debug); + + (my $present, my $priority) = $block =~ /(priority)=(.*?)\n/is; + print "priority=[$priority]\n" if ($present && $debug); + + (my $present, my $rooms) = $block =~ /(rooms)=(.*?)\n/is; + print "rooms=[$rooms]\n" if ($present && $debug); + $rooms = lc($rooms); + + if ( ($rooms eq "") || ($rooms eq "all") || ($rooms eq $room) ) { + &speak($say,$volume,$voice); + } + } + +sub speak + { + my $text = shift; + my $volume = shift; + my $voice = shift; + my $cmd; + $volume = $volume / 100; # SABLE uses a floating point-number between zero and 1 to represent volume. + + # The SABLE tags are currently not working properly - it's reading them out + # $text = "".$text."" if ($volume != 0.5); + # $text = "".$text."" if ($voice ne ""); + + if ($XAP_SOURCE eq "festival") +{ + $cmd = "echo \"$text\" | $fqfn --tts"; +} +elsif ($XAP_SOURCE eq "flite") +{ + $cmd = "$fqfn -t \"$text\""; +} +elsif ($XAP_SOURCE eq "swift") +{ + $cmd = "$fqfn \"$text\""; +} + print "cmd=[$cmd]\n" if $debug; + print "cmd=[$cmd]\n"; + system $cmd; } \ No newline at end of file diff --git a/code/common/cm11_control.pl b/code/common/cm11_control.pl index 3f3b3a3c5..c555a9c48 100644 --- a/code/common/cm11_control.pl +++ b/code/common/cm11_control.pl @@ -1,37 +1,37 @@ -# Category=X10 -#@ Stops and starts the cm11 automation controller -#execute unstick if ack/done cycle fails - -$v_cm11_control1 = new Voice_Cmd "[Start,Stop,Reset] the CM11 port"; -if (said $v_cm11_control1) { - my $state = $v_cm11_control1->{state}; - - if (defined $main::Serial_Ports{'cm11'}{object}) { - - if ($state eq 'Stop' or $state eq 'Reset') { - if ($main::Serial_Ports{'cm11'}{object}->close) { - print "CM11 port was closed\n"; - delete $Serial_Ports{object_by_port}{$Serial_Ports{'cm11'}{port}}; - } - else { - print "CM11 port failed to close\n"; - } - } - - if ($state eq 'Start' or $state eq 'Reset') { - if (&main::serial_port_open('cm11')) { - print "CM11 port was re-opened\n"; - } - else { - print "CM11 port failed to re-open\n"; - } - } - - $v_cm11_control1->respond("app=cm11 CM11 port has been set to $state."); - - } - else { - $v_cm11_control1->respond("app=error CM11 port does not exist."); - } -} - +# Category=X10 +#@ Stops and starts the cm11 automation controller +#execute unstick if ack/done cycle fails + +$v_cm11_control1 = new Voice_Cmd "[Start,Stop,Reset] the CM11 port"; +if (said $v_cm11_control1) { + my $state = $v_cm11_control1->{state}; + + if (defined $main::Serial_Ports{'cm11'}{object}) { + + if ($state eq 'Stop' or $state eq 'Reset') { + if ($main::Serial_Ports{'cm11'}{object}->close) { + print "CM11 port was closed\n"; + delete $Serial_Ports{object_by_port}{$Serial_Ports{'cm11'}{port}}; + } + else { + print "CM11 port failed to close\n"; + } + } + + if ($state eq 'Start' or $state eq 'Reset') { + if (&main::serial_port_open('cm11')) { + print "CM11 port was re-opened\n"; + } + else { + print "CM11 port failed to re-open\n"; + } + } + + $v_cm11_control1->respond("app=cm11 CM11 port has been set to $state."); + + } + else { + $v_cm11_control1->respond("app=error CM11 port does not exist."); + } +} + diff --git a/code/common/cm11_monitor.pl b/code/common/cm11_monitor.pl index c416f8a92..2739bfda1 100644 --- a/code/common/cm11_monitor.pl +++ b/code/common/cm11_monitor.pl @@ -1,99 +1,99 @@ - -=begin comment - -#@ Monitors the cm11 controller and resets if down (optionally "tickles" via a cm17 to ensure that the cm11 comes back up immediately.) Timeout is set with cm11_timeout parameter. cm11_tickle_address is the address sent by the cm17 on reset. To disable automated resets, reset the cm11_auto_reset parameter (set it to 0.) - -=cut - -# Category = X10 - -$cm11_monitor = new Generic_Item(); -$timer_x10_inactivity = new Timer(); -$v_cm11_control = new Voice_Cmd "[Start,Stop,Reset] the CM11 port"; -$v_cm11_control->set_info('Controls the CM11 X10 controller. Reset if it sticks.'); -if ($Reload) { - Serial_data_add_hook(\&cm11_monitor); - my $timeout = $config_parms{cm11_timeout}; - $timeout = 1800 unless $timeout; - set $timer_x10_inactivity $timeout, \&cm11_unstick; -} - -sub cm11_monitor { - my $state = shift; - my $current_unit; - return unless $state =~ /^X/; - - # *** These signals could come from a different controller (like a Lynx or proxy, etc.) - # Need way to do this (inside of cm11 object makes most sense.) - - $current_unit = $1 if ($state =~ /^X([A-P][1-9A-G])/); - - set $cm11_monitor 'up' if ($cm11_monitor->{state} ne 'up'); - - print_log "Reset cm11 inactivity timer $current_unit $state" if $config_parms{x10_errata} >= 3; - set $timer_x10_inactivity 1800, \&cm11_unstick; #every half hour without activity triggers a restart. Otherwise cm11 remains lost in space." -} - -sub cm11_unstick { - set $cm11_monitor 'down'; - if (!defined $config_parms{'cm11_auto_reset'} or $config_parms{'cm11_auto_reset'}) { - speak "app=system Restarting automation controller after inactivity."; - # default is to reset the port after inactivity (can be turned off with 0) - &cm11_control('Reset'); - set $cm11_monitor 'reset'; - } - my $timeout = $config_parms{cm11_timeout}; - $timeout = 1800 unless $timeout; - set $timer_x10_inactivity $timeout, \&cm11_unstick; -} - -sub cm11_control { - my $state = shift; - - if (defined $main::Serial_Ports{'cm11'}{object}) { - - if ($state eq 'Stop' or $state eq 'Reset') { - if ($main::Serial_Ports{'cm11'}{object}->close) { - print "CM11 port was closed\n" if $Debug{cm11}; - delete $Serial_Ports{object_by_port}{$Serial_Ports{'cm11'}{port}}; - } - else { - print "CM11 port failed to close\n" if $Debug{cm11}; - } - } - - if ($state eq 'Start' or $state eq 'Reset') { - if (&main::serial_port_open('cm11')) { - print "CM11 port was re-opened\n" if $Debug{cm11}; - } - else { - print "CM11 port failed to re-open\n" if $Debug{cm11}; - } - } - - # "Tickle" controller w/ X10 signal via CM17 (if exists) - - if ($state eq 'Reset') { - if (&main::serial_port_open('cm17')) { - print "Sending X10 signal via CM17\n" if $Debug{cm11}; - -ControlX10::CM17::send(($config_parms{'cm11_tickle_address'}?$config_parms{'cm11_tickle_address'}:'A1') . 'K'); # send an off command to the dummy tickle address - - } - } - - - $v_cm11_control->respond("app=cm11 CM11 port has been set to $state."); - - } - else { - $v_cm11_control->respond("app=error CM11 port does not exist."); - } - - - -} - -if (said $v_cm11_control) { - &cm11_control($v_cm11_control->{state}); + +=begin comment + +#@ Monitors the cm11 controller and resets if down (optionally "tickles" via a cm17 to ensure that the cm11 comes back up immediately.) Timeout is set with cm11_timeout parameter. cm11_tickle_address is the address sent by the cm17 on reset. To disable automated resets, reset the cm11_auto_reset parameter (set it to 0.) + +=cut + +# Category = X10 + +$cm11_monitor = new Generic_Item(); +$timer_x10_inactivity = new Timer(); +$v_cm11_control = new Voice_Cmd "[Start,Stop,Reset] the CM11 port"; +$v_cm11_control->set_info('Controls the CM11 X10 controller. Reset if it sticks.'); +if ($Reload) { + Serial_data_add_hook(\&cm11_monitor); + my $timeout = $config_parms{cm11_timeout}; + $timeout = 1800 unless $timeout; + set $timer_x10_inactivity $timeout, \&cm11_unstick; +} + +sub cm11_monitor { + my $state = shift; + my $current_unit; + return unless $state =~ /^X/; + + # *** These signals could come from a different controller (like a Lynx or proxy, etc.) + # Need way to do this (inside of cm11 object makes most sense.) + + $current_unit = $1 if ($state =~ /^X([A-P][1-9A-G])/); + + set $cm11_monitor 'up' if ($cm11_monitor->{state} ne 'up'); + + print_log "Reset cm11 inactivity timer $current_unit $state" if $config_parms{x10_errata} >= 3; + set $timer_x10_inactivity 1800, \&cm11_unstick; #every half hour without activity triggers a restart. Otherwise cm11 remains lost in space." +} + +sub cm11_unstick { + set $cm11_monitor 'down'; + if (!defined $config_parms{'cm11_auto_reset'} or $config_parms{'cm11_auto_reset'}) { + speak "app=system Restarting automation controller after inactivity."; + # default is to reset the port after inactivity (can be turned off with 0) + &cm11_control('Reset'); + set $cm11_monitor 'reset'; + } + my $timeout = $config_parms{cm11_timeout}; + $timeout = 1800 unless $timeout; + set $timer_x10_inactivity $timeout, \&cm11_unstick; +} + +sub cm11_control { + my $state = shift; + + if (defined $main::Serial_Ports{'cm11'}{object}) { + + if ($state eq 'Stop' or $state eq 'Reset') { + if ($main::Serial_Ports{'cm11'}{object}->close) { + print "CM11 port was closed\n" if $Debug{cm11}; + delete $Serial_Ports{object_by_port}{$Serial_Ports{'cm11'}{port}}; + } + else { + print "CM11 port failed to close\n" if $Debug{cm11}; + } + } + + if ($state eq 'Start' or $state eq 'Reset') { + if (&main::serial_port_open('cm11')) { + print "CM11 port was re-opened\n" if $Debug{cm11}; + } + else { + print "CM11 port failed to re-open\n" if $Debug{cm11}; + } + } + + # "Tickle" controller w/ X10 signal via CM17 (if exists) + + if ($state eq 'Reset') { + if (&main::serial_port_open('cm17')) { + print "Sending X10 signal via CM17\n" if $Debug{cm11}; + +ControlX10::CM17::send(($config_parms{'cm11_tickle_address'}?$config_parms{'cm11_tickle_address'}:'A1') . 'K'); # send an off command to the dummy tickle address + + } + } + + + $v_cm11_control->respond("app=cm11 CM11 port has been set to $state."); + + } + else { + $v_cm11_control->respond("app=error CM11 port does not exist."); + } + + + +} + +if (said $v_cm11_control) { + &cm11_control($v_cm11_control->{state}); } \ No newline at end of file diff --git a/code/common/dvd_player.pl b/code/common/dvd_player.pl index 1e17a3edd..8b03ffa92 100644 --- a/code/common/dvd_player.pl +++ b/code/common/dvd_player.pl @@ -1,78 +1,78 @@ -# Category=Entertainment - -#@ Set dvd_program to DVD player program path. For example: -#@ dvd_program=C:\program files\intervideo\dvd6\windvd.exe # NOTE spaces are allowed -#@ Set dvd_archives_folder to folder with ripped DVD's. -#@ dvd_archives_folder=F:\archives\dvds -#@ Set dvd_favorites to list of favorite DVD's. -#@ dvd_favorites=Hulk,Indiana Jones and the Last Crusade,King Kong - -# WinDVD only for now (tested with v6.) - -use DVDPlayer; -#***Don't even create these objects if no WinDVD (look for above by default) Do nothing on Linux -$dvd_player = new DVDPlayer(); -$dvd_marquee = new Generic_Item; -#&tk_entry('DVD', $dvd_marquee); -#noloop=start -my %dvd_states = (play=>'play',stop=>'stop',pause=>'pause',rewind=>'rewind','fast forward'=>'fast forward',step=>'step','skip forward'=>'skip forward','instant replay'=>'instant replay','root menu'=>'root menu','title menu'=>'title menu','volume up'=>'vol +','volume down'=>'vol -',mute=>'mute',unzoom=>'unzoom',pan=>'pan',angle=>'angle',subtitle=>'subtitle','previous chapter'=>'previous chapter','next chapter'=>'next chapter','eject'=>'eject','brightness up'=>'brightness up','brightness down'=>'brightness down',on=>'on',off=>'off','full screen'=>'full screen'); -my $dvd_states; -$dvd_states = join(',',keys %dvd_states); -#***Point voice commands to error messages if objects not created! -#***Set info, icons? Use info in help -$v_dvd_control = new Voice_Cmd("DVD movie [$dvd_states]", 0); -$v_dvd_movie = new Voice_Cmd("Show DVD movie [$config_parms{dvd_favorites}]", 0); -$v_dvd_attractions = new Voice_Cmd("What is showing on DVD",0); -$v_dvd_help = new Voice_Cmd("DVD movie help",0); -$f_dvd = new File_Item("$Pgm_Path/dvd.txt"); -$p_dvd = new Process_Item("dir_to_file $config_parms{dvd_drive} dvd.txt"); -#noloop=stop - -sub refresh_marquee { - start $p_dvd; -} - -# *** trigger - -&refresh_marquee() if $Reload; - -if (done_now $p_dvd) { - - my $dir = read_all $f_dvd; - - my ($title) = $dir =~ /volume in drive.*is (\S*)/i; - - $title =~ s/_/\x20/g; - $title = ucfirst(lc($title)); - - set $dvd_marquee $title; - unlink $f_dvd->{name}; -} - - -if ($state = said $v_dvd_help) { - my @commands = join(', ', sort keys %dvd_states); - &respond("app=movie Commands are: @commands"); -} -if ($state = said $v_dvd_attractions) { - &refresh_marquee(); - &respond("app=movie mode=rotates $config_parms{dvd_favorites}" . (($dvd_marquee->{state})?" plus $dvd_marquee->{state}":'')); -} -if ($state = said $v_dvd_control) { - &refresh_marquee() if $state eq 'play'; - &respond("app=dvd " . ucfirst($state)); - &dvd_control($state); -} -if ($state = said $v_dvd_movie) { - &respond("app=movie $state Now Showing"); - &dvd_movie($state); -} -sub dvd_control { - my ($command) = @_; - set $dvd_player $dvd_states{$command}; -} -sub dvd_movie { - my ($movie) = @_; - set $dvd_marquee $movie; - set $dvd_player "play \"$movie\""; +# Category=Entertainment + +#@ Set dvd_program to DVD player program path. For example: +#@ dvd_program=C:\program files\intervideo\dvd6\windvd.exe # NOTE spaces are allowed +#@ Set dvd_archives_folder to folder with ripped DVD's. +#@ dvd_archives_folder=F:\archives\dvds +#@ Set dvd_favorites to list of favorite DVD's. +#@ dvd_favorites=Hulk,Indiana Jones and the Last Crusade,King Kong + +# WinDVD only for now (tested with v6.) + +use DVDPlayer; +#***Don't even create these objects if no WinDVD (look for above by default) Do nothing on Linux +$dvd_player = new DVDPlayer(); +$dvd_marquee = new Generic_Item; +#&tk_entry('DVD', $dvd_marquee); +#noloop=start +my %dvd_states = (play=>'play',stop=>'stop',pause=>'pause',rewind=>'rewind','fast forward'=>'fast forward',step=>'step','skip forward'=>'skip forward','instant replay'=>'instant replay','root menu'=>'root menu','title menu'=>'title menu','volume up'=>'vol +','volume down'=>'vol -',mute=>'mute',unzoom=>'unzoom',pan=>'pan',angle=>'angle',subtitle=>'subtitle','previous chapter'=>'previous chapter','next chapter'=>'next chapter','eject'=>'eject','brightness up'=>'brightness up','brightness down'=>'brightness down',on=>'on',off=>'off','full screen'=>'full screen'); +my $dvd_states; +$dvd_states = join(',',keys %dvd_states); +#***Point voice commands to error messages if objects not created! +#***Set info, icons? Use info in help +$v_dvd_control = new Voice_Cmd("DVD movie [$dvd_states]", 0); +$v_dvd_movie = new Voice_Cmd("Show DVD movie [$config_parms{dvd_favorites}]", 0); +$v_dvd_attractions = new Voice_Cmd("What is showing on DVD",0); +$v_dvd_help = new Voice_Cmd("DVD movie help",0); +$f_dvd = new File_Item("$Pgm_Path/dvd.txt"); +$p_dvd = new Process_Item("dir_to_file $config_parms{dvd_drive} dvd.txt"); +#noloop=stop + +sub refresh_marquee { + start $p_dvd; +} + +# *** trigger + +&refresh_marquee() if $Reload; + +if (done_now $p_dvd) { + + my $dir = read_all $f_dvd; + + my ($title) = $dir =~ /volume in drive.*is (\S*)/i; + + $title =~ s/_/\x20/g; + $title = ucfirst(lc($title)); + + set $dvd_marquee $title; + unlink $f_dvd->{name}; +} + + +if ($state = said $v_dvd_help) { + my @commands = join(', ', sort keys %dvd_states); + &respond("app=movie Commands are: @commands"); +} +if ($state = said $v_dvd_attractions) { + &refresh_marquee(); + &respond("app=movie mode=rotates $config_parms{dvd_favorites}" . (($dvd_marquee->{state})?" plus $dvd_marquee->{state}":'')); +} +if ($state = said $v_dvd_control) { + &refresh_marquee() if $state eq 'play'; + &respond("app=dvd " . ucfirst($state)); + &dvd_control($state); +} +if ($state = said $v_dvd_movie) { + &respond("app=movie $state Now Showing"); + &dvd_movie($state); +} +sub dvd_control { + my ($command) = @_; + set $dvd_player $dvd_states{$command}; +} +sub dvd_movie { + my ($movie) = @_; + set $dvd_marquee $movie; + set $dvd_player "play \"$movie\""; } \ No newline at end of file diff --git a/code/common/email_marketplace_sales.pl b/code/common/email_marketplace_sales.pl index 89ebc1122..9e7379096 100644 --- a/code/common/email_marketplace_sales.pl +++ b/code/common/email_marketplace_sales.pl @@ -1,221 +1,221 @@ - -# Category = eCommerce - -# $Date$ -# $Revision$ - -#@ Check incoming email for sales. Venues supported are: Amazon, Amazon Canada, Alibris and eBay -#@ NOTE: Does nothing with multiple-item sales (names are not on the subject line.) -#@ Results are announced and logged with the $cash_register object. -#@ Requires internet_mail.pl - -#noloop=start - -$cash_register = new Generic_Item; -$v_email_marketplace = new Voice_Cmd 'Process email sales'; -$v_email_sales = new Voice_Cmd 'How many sales [today,this week,this month]'; -$v_email_questions = new Voice_Cmd 'How many questions [today,this week,this month]'; -my $daily_sales = 0; -my $daily_questions = 0; -my $weekly_sales = 0; -my $weekly_questions = 0; -my $monthly_sales = 0; -my $monthly_questions = 0; -&load_sales(); -#noloop=stop - -if ($MW and $Reload) { - &tk_label_new(3, \$Save{marketplace_sales_day}); - &tk_label_new(3, \$Save{marketplace_sales_week}); - &tk_label_new(3, \$Save{marketplace_sales_month}); -} - - -if (said $v_email_sales) { - my $state = $v_email_sales->{state}; - my $message; - - if ($state eq 'today') { - $message = "$daily_sales today."; - } - elsif ($state eq 'this week') { - $message = "$weekly_sales this week."; - } - else { - $message = "$monthly_sales this month."; - } - $v_email_sales->respond("app=cashier $message"); -} - -if (said $v_email_questions) { - my $state = $v_email_questions->{state}; - my $message; - - if ($state eq 'today') { - $message = "$daily_questions today."; - } - elsif ($state eq 'this week') { - $message = "$weekly_questions this week."; - } - else { - $message = "$monthly_questions this month."; - } - $v_email_questions->respond("app=cashier $message"); -} - - - - # get_email_scan_file and $p_get_email are created by internet_mail.pl -if (done_now $p_get_email and -e $get_email_scan_file or said $v_email_marketplace) { - print "marketplace: checking $get_email_scan_file\n" if $Debug{email}; - my @msgs; - my $total_sales = 0; - my $total_questions = 0; - for my $line (file_read $get_email_scan_file) { - print "marketplace: mail =$line\n" if $Debug{email}; - my ($msg, $from, $to, $subject, $body) = $line =~ /Msg: (\d+) From:(.+?) To:(.+?) Subject:(.+?) Body:(.+)/; - - my $message = ''; - - if ($subject =~ /^ *Sold, Ship Now\. (\d{5}) (.*)/i or $subject =~ /^ Sold -- Ship Now! (\d{5}) (.*)/i) { - $message = "app=cashier $2 just sold on Amazon."; - set $cash_register $2; - print "marketplace: Found Amazon email: $subject\n" if $Debug{email}; - $total_sales++; - } - elsif ($subject =~ /^ *eBay Store Inventory Sold: (.*) \(\d+\)/) { - $message = "$1 just sold in eBay Store."; - set $cash_register $1; - print "marketplace: Found eBay store email: $subject\n" if $Debug{email}; - $total_sales++; - } - elsif ($subject =~ /^ *eBay Item Sold: (.*) \(\d+\)/) { - $message = "$1 just sold on eBay."; - set $cash_register $1; - print "marketplace: Found eBay auction email: $subject\n" if $Debug{email}; - $total_sales++; - } - elsif ($subject =~ /^ *Alibris Purchase Notification # (\d+-\d+) - (.*)/) { - $message = "$2 just sold on Alibris."; - set $cash_register $2; - print "marketplace: Found Alibris email: $subject\n" if $Debug{email}; - $total_sales++; - } - elsif ($subject =~ /^ *You've made a sale - Please ship your item/i) { - $message = "Item just sold on Half.com."; - set $cash_register "Half.com item"; - print "marketplace: Found Half email: $subject\n" if $Debug{email}; - $total_sales++; - } - elsif ($subject =~ /^ *Sold -- ship now! \(\d+ listings\/(\d+) items\)/i or $subject =~ /^ *Sold -- ship now! \(\d+ listings\/(\d+) items\)/i) { - $message = "$1 item(s) just sold on Amazon."; - set $cash_register "$1 Amazon item(s)"; - print "marketplace: Found Amazon email: $subject\n" if $Debug{email}; - $total_sales += int($1); - } - elsif ($subject =~ /^ *Message from eBay Member/i) { - $message = "Message received from eBay member."; - print "marketplace: Found eBay message email: $subject\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Question for item #(\d+) - (.*)/i) { - $message = "Question received from eBay member about $2."; - print "marketplace: Found eBay question email: $subject\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Alibris Inquiry: (.*?); Rec #(\d+)/i) { - $message = "Question received from Alibris customer about SKU $2."; - print "marketplace: Found Alibris question email: $subject\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Please send me total amount for eBay item #(\d+), (.*)/i) { - $message = "Customer would like an invoice for $2."; - print "marketplace: Found eBay request for invoice email: $subject\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Re: Order information from Amazon seller/i) { - $message = "Customer replied to Amazon communique."; - print "marketplace: Found Amazon customer reply email: $subject from $from\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Product details inquiry from Amazon customer (.*)/i) { - $message = "$1 wants to know more about an Amazon listing..."; - print "marketplace: Found Amazon customer inquiry email: $subject from $from\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *RE: Your Amazon Marketplace Purchase/i) { - $message = "$1 has a complaint about their purchase..."; - print "marketplace: Found Amazon customer complaint email: $subject from $from\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Product Details/i) { - $message = "$1 wants to know more about an amazon listing..."; - print "marketplace: Found Amazon customer inquiry email: $subject from $from\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Question\/Comment regarding Half.com Transaction #: (\d+)/i) { - $message = "Customer wants to know more about a Half.com transaction..."; - print "marketplace: Found Half.com order inquiry email: $subject from $from\n" if $Debug{email}; - $total_questions++; - } - elsif ($subject =~ /^ *Re: Question\/Comment regarding Half.com Transaction #: (\d+)/i) { - $message = "Half.com customer replied to your answer."; - print "marketplace: Found Half.com answer reply email: $subject from $from\n" if $Debug{email}; - $total_questions++; - } - - # *** Need to un-associate the chime from this app (pass explicitly if $total_sales) - - my $chime = ($total_sales == 0)?'sound_nature/*.wav':'cash_register'; - - speak "app=cashier chime=$chime $message" if ($message); - } - - if ($total_sales) { - $daily_sales += $total_sales; - $daily_questions += $total_questions; - $weekly_sales += $total_sales; - $monthly_sales += $total_sales; - speak "app=cashier no_chime=1 $total_sales new sale(s). That's $daily_sales on the day, $weekly_sales for the week and $monthly_sales this month."; - speak "app=cashier no_chime=1 $daily_questions questions received today." if $daily_questions; - &persist_sales(); - } - -} - -sub persist_sales { - $Save{marketplace_sales_day} = $daily_sales; - $Save{marketplace_questions_day} = $daily_questions; - $Save{marketplace_sales_week} = $weekly_sales; - $Save{marketplace_questions_week} = $weekly_questions; - $Save{marketplace_sales_month} = $monthly_sales; - $Save{marketplace_questions_month} = $monthly_questions; -} - -sub load_sales { - $daily_sales = $Save{marketplace_sales_day} if $Save{marketplace_sales_day}; - $daily_questions = $Save{marketplace_questions_day} if $Save{marketplace_questions_day}; - $weekly_sales = $Save{marketplace_sales_week} if $Save{marketplace_sales_week}; - $monthly_sales = $Save{marketplace_sales_month} if $Save{marketplace_sales_month}; -} - - -if ($New_Day) { - $daily_sales = 0; - $daily_questions = 0; - &persist_sales(); -} - -if ($New_Week) { - $weekly_sales = 0; - $weekly_questions = 0; - &persist_sales(); -} - -if ($New_Month) { - $monthly_sales = 0; - $monthly_questions = 0; - &persist_sales(); -} - - + +# Category = eCommerce + +# $Date$ +# $Revision$ + +#@ Check incoming email for sales. Venues supported are: Amazon, Amazon Canada, Alibris and eBay +#@ NOTE: Does nothing with multiple-item sales (names are not on the subject line.) +#@ Results are announced and logged with the $cash_register object. +#@ Requires internet_mail.pl + +#noloop=start + +$cash_register = new Generic_Item; +$v_email_marketplace = new Voice_Cmd 'Process email sales'; +$v_email_sales = new Voice_Cmd 'How many sales [today,this week,this month]'; +$v_email_questions = new Voice_Cmd 'How many questions [today,this week,this month]'; +my $daily_sales = 0; +my $daily_questions = 0; +my $weekly_sales = 0; +my $weekly_questions = 0; +my $monthly_sales = 0; +my $monthly_questions = 0; +&load_sales(); +#noloop=stop + +if ($MW and $Reload) { + &tk_label_new(3, \$Save{marketplace_sales_day}); + &tk_label_new(3, \$Save{marketplace_sales_week}); + &tk_label_new(3, \$Save{marketplace_sales_month}); +} + + +if (said $v_email_sales) { + my $state = $v_email_sales->{state}; + my $message; + + if ($state eq 'today') { + $message = "$daily_sales today."; + } + elsif ($state eq 'this week') { + $message = "$weekly_sales this week."; + } + else { + $message = "$monthly_sales this month."; + } + $v_email_sales->respond("app=cashier $message"); +} + +if (said $v_email_questions) { + my $state = $v_email_questions->{state}; + my $message; + + if ($state eq 'today') { + $message = "$daily_questions today."; + } + elsif ($state eq 'this week') { + $message = "$weekly_questions this week."; + } + else { + $message = "$monthly_questions this month."; + } + $v_email_questions->respond("app=cashier $message"); +} + + + + # get_email_scan_file and $p_get_email are created by internet_mail.pl +if (done_now $p_get_email and -e $get_email_scan_file or said $v_email_marketplace) { + print "marketplace: checking $get_email_scan_file\n" if $Debug{email}; + my @msgs; + my $total_sales = 0; + my $total_questions = 0; + for my $line (file_read $get_email_scan_file) { + print "marketplace: mail =$line\n" if $Debug{email}; + my ($msg, $from, $to, $subject, $body) = $line =~ /Msg: (\d+) From:(.+?) To:(.+?) Subject:(.+?) Body:(.+)/; + + my $message = ''; + + if ($subject =~ /^ *Sold, Ship Now\. (\d{5}) (.*)/i or $subject =~ /^ Sold -- Ship Now! (\d{5}) (.*)/i) { + $message = "app=cashier $2 just sold on Amazon."; + set $cash_register $2; + print "marketplace: Found Amazon email: $subject\n" if $Debug{email}; + $total_sales++; + } + elsif ($subject =~ /^ *eBay Store Inventory Sold: (.*) \(\d+\)/) { + $message = "$1 just sold in eBay Store."; + set $cash_register $1; + print "marketplace: Found eBay store email: $subject\n" if $Debug{email}; + $total_sales++; + } + elsif ($subject =~ /^ *eBay Item Sold: (.*) \(\d+\)/) { + $message = "$1 just sold on eBay."; + set $cash_register $1; + print "marketplace: Found eBay auction email: $subject\n" if $Debug{email}; + $total_sales++; + } + elsif ($subject =~ /^ *Alibris Purchase Notification # (\d+-\d+) - (.*)/) { + $message = "$2 just sold on Alibris."; + set $cash_register $2; + print "marketplace: Found Alibris email: $subject\n" if $Debug{email}; + $total_sales++; + } + elsif ($subject =~ /^ *You've made a sale - Please ship your item/i) { + $message = "Item just sold on Half.com."; + set $cash_register "Half.com item"; + print "marketplace: Found Half email: $subject\n" if $Debug{email}; + $total_sales++; + } + elsif ($subject =~ /^ *Sold -- ship now! \(\d+ listings\/(\d+) items\)/i or $subject =~ /^ *Sold -- ship now! \(\d+ listings\/(\d+) items\)/i) { + $message = "$1 item(s) just sold on Amazon."; + set $cash_register "$1 Amazon item(s)"; + print "marketplace: Found Amazon email: $subject\n" if $Debug{email}; + $total_sales += int($1); + } + elsif ($subject =~ /^ *Message from eBay Member/i) { + $message = "Message received from eBay member."; + print "marketplace: Found eBay message email: $subject\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Question for item #(\d+) - (.*)/i) { + $message = "Question received from eBay member about $2."; + print "marketplace: Found eBay question email: $subject\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Alibris Inquiry: (.*?); Rec #(\d+)/i) { + $message = "Question received from Alibris customer about SKU $2."; + print "marketplace: Found Alibris question email: $subject\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Please send me total amount for eBay item #(\d+), (.*)/i) { + $message = "Customer would like an invoice for $2."; + print "marketplace: Found eBay request for invoice email: $subject\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Re: Order information from Amazon seller/i) { + $message = "Customer replied to Amazon communique."; + print "marketplace: Found Amazon customer reply email: $subject from $from\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Product details inquiry from Amazon customer (.*)/i) { + $message = "$1 wants to know more about an Amazon listing..."; + print "marketplace: Found Amazon customer inquiry email: $subject from $from\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *RE: Your Amazon Marketplace Purchase/i) { + $message = "$1 has a complaint about their purchase..."; + print "marketplace: Found Amazon customer complaint email: $subject from $from\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Product Details/i) { + $message = "$1 wants to know more about an amazon listing..."; + print "marketplace: Found Amazon customer inquiry email: $subject from $from\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Question\/Comment regarding Half.com Transaction #: (\d+)/i) { + $message = "Customer wants to know more about a Half.com transaction..."; + print "marketplace: Found Half.com order inquiry email: $subject from $from\n" if $Debug{email}; + $total_questions++; + } + elsif ($subject =~ /^ *Re: Question\/Comment regarding Half.com Transaction #: (\d+)/i) { + $message = "Half.com customer replied to your answer."; + print "marketplace: Found Half.com answer reply email: $subject from $from\n" if $Debug{email}; + $total_questions++; + } + + # *** Need to un-associate the chime from this app (pass explicitly if $total_sales) + + my $chime = ($total_sales == 0)?'sound_nature/*.wav':'cash_register'; + + speak "app=cashier chime=$chime $message" if ($message); + } + + if ($total_sales) { + $daily_sales += $total_sales; + $daily_questions += $total_questions; + $weekly_sales += $total_sales; + $monthly_sales += $total_sales; + speak "app=cashier no_chime=1 $total_sales new sale(s). That's $daily_sales on the day, $weekly_sales for the week and $monthly_sales this month."; + speak "app=cashier no_chime=1 $daily_questions questions received today." if $daily_questions; + &persist_sales(); + } + +} + +sub persist_sales { + $Save{marketplace_sales_day} = $daily_sales; + $Save{marketplace_questions_day} = $daily_questions; + $Save{marketplace_sales_week} = $weekly_sales; + $Save{marketplace_questions_week} = $weekly_questions; + $Save{marketplace_sales_month} = $monthly_sales; + $Save{marketplace_questions_month} = $monthly_questions; +} + +sub load_sales { + $daily_sales = $Save{marketplace_sales_day} if $Save{marketplace_sales_day}; + $daily_questions = $Save{marketplace_questions_day} if $Save{marketplace_questions_day}; + $weekly_sales = $Save{marketplace_sales_week} if $Save{marketplace_sales_week}; + $monthly_sales = $Save{marketplace_sales_month} if $Save{marketplace_sales_month}; +} + + +if ($New_Day) { + $daily_sales = 0; + $daily_questions = 0; + &persist_sales(); +} + +if ($New_Week) { + $weekly_sales = 0; + $weekly_questions = 0; + &persist_sales(); +} + +if ($New_Month) { + $monthly_sales = 0; + $monthly_questions = 0; + &persist_sales(); +} + + diff --git a/code/common/froggyrita.pl b/code/common/froggyrita.pl index 780d99236..b303bfddd 100644 --- a/code/common/froggyrita.pl +++ b/code/common/froggyrita.pl @@ -1,71 +1,71 @@ -# Category = HVAC - -use strict; -use FroggyRita; - -my $command_waiting; #noloop - -$Froggy = new FroggyRita; -$v_froggy_indoor_temperature = new Voice_Cmd('[What is] the indoor temperature',0); -$v_froggy_indoor_humidity = new Voice_Cmd('[What is] the indoor humidity',0); -$v_froggy_indoor_check = new Voice_Cmd('Check indoor conditions',0); - -if (said $v_froggy_indoor_check) { - respond "app=frog Checking indoor conditions..."; - $command_waiting = $v_froggy_indoor_check; - set $Froggy 'status', $v_froggy_indoor_check; -} - - -if (said $v_froggy_indoor_temperature) { - - if (defined $Weather{TempIndoor}) { - respond "app=frog It is $Weather{TempIndoor} degrees fahrenheit indoors."; - } - else { - respond "app=frog I don't know the temperature at the moment. Try again in a few minutes..."; - } - -} - -if (my $state = said $v_froggy_indoor_humidity) { - if (defined $Froggy->humidity()) { - respond "app=frog It is " . $Froggy->humidity() . "% indoors."; - } - else { - respond "app=frog I do not know at the moment. Try again in a few minutes..."; - } - -} - -if (my $state = state_now $Froggy) { - my $temperature = $Froggy->temperature(); - - $temperature = ($config_parms{weather_uom_temp} eq 'C' ? (int($temperature * 100 + .5)/100):int((($temperature * 180) + 3200) + .5)/100 if defined $temperature; - - - if ($command_waiting and $state ne 'status') { - if (defined $temperature) { - $command_waiting->respond('app=frog connected=0 Indoor temperature is ' . $temperature . ' degrees fahrenheit. Humidity is ' . $Froggy->humidity() . '%'); - } - else { - $command_waiting->respond('app=frog connected=0 I do not know at the moment. Try again in a few minutes...'); - - } - $command_waiting = undef; - } - $Weather{TempIndoor} = $temperature if defined $temperature; - $Weather{HumidIndoor} = $Froggy->humidity() if defined $Froggy->humidity(); -} - -sub get_froggy_status { - set $Froggy 'status', 'time'; -} - - # trigger - -if ($Reload) { - &trigger_set("new_minute 5", - "&get_froggy_status()", 'NoExpire', 'get frog status') - unless &trigger_get('get frog status'); -} +# Category = HVAC + +use strict; +use FroggyRita; + +my $command_waiting; #noloop + +$Froggy = new FroggyRita; +$v_froggy_indoor_temperature = new Voice_Cmd('[What is] the indoor temperature',0); +$v_froggy_indoor_humidity = new Voice_Cmd('[What is] the indoor humidity',0); +$v_froggy_indoor_check = new Voice_Cmd('Check indoor conditions',0); + +if (said $v_froggy_indoor_check) { + respond "app=frog Checking indoor conditions..."; + $command_waiting = $v_froggy_indoor_check; + set $Froggy 'status', $v_froggy_indoor_check; +} + + +if (said $v_froggy_indoor_temperature) { + + if (defined $Weather{TempIndoor}) { + respond "app=frog It is $Weather{TempIndoor} degrees fahrenheit indoors."; + } + else { + respond "app=frog I don't know the temperature at the moment. Try again in a few minutes..."; + } + +} + +if (my $state = said $v_froggy_indoor_humidity) { + if (defined $Froggy->humidity()) { + respond "app=frog It is " . $Froggy->humidity() . "% indoors."; + } + else { + respond "app=frog I do not know at the moment. Try again in a few minutes..."; + } + +} + +if (my $state = state_now $Froggy) { + my $temperature = $Froggy->temperature(); + + $temperature = ($config_parms{weather_uom_temp} eq 'C' ? (int($temperature * 100 + .5)/100):int((($temperature * 180) + 3200) + .5)/100 if defined $temperature; + + + if ($command_waiting and $state ne 'status') { + if (defined $temperature) { + $command_waiting->respond('app=frog connected=0 Indoor temperature is ' . $temperature . ' degrees fahrenheit. Humidity is ' . $Froggy->humidity() . '%'); + } + else { + $command_waiting->respond('app=frog connected=0 I do not know at the moment. Try again in a few minutes...'); + + } + $command_waiting = undef; + } + $Weather{TempIndoor} = $temperature if defined $temperature; + $Weather{HumidIndoor} = $Froggy->humidity() if defined $Froggy->humidity(); +} + +sub get_froggy_status { + set $Froggy 'status', 'time'; +} + + # trigger + +if ($Reload) { + &trigger_set("new_minute 5", + "&get_froggy_status()", 'NoExpire', 'get frog status') + unless &trigger_get('get frog status'); +} diff --git a/code/common/holiday_ical.pl b/code/common/holiday_ical.pl index fd93d590c..dd2dc43e4 100755 --- a/code/common/holiday_ical.pl +++ b/code/common/holiday_ical.pl @@ -1,52 +1,52 @@ -# Category = Time - -#@ Generate ical compatible holiday information every year -#@ Can be used with ical2vsdb to reimport holiday information back into MH - -=begin comment - -mh.ini parameters required - -holiday_definition_file = path to filename that has holiday definitions +# Category = Time + +#@ Generate ical compatible holiday information every year +#@ Can be used with ical2vsdb to reimport holiday information back into MH + +=begin comment + +mh.ini parameters required + +holiday_definition_file = path to filename that has holiday definitions ie holiday_definition_file = $Pgm_Root/data/holidays.ca_ab for Alberta, Canada - -optional - - holiday_ical_filename = filename of generated holiday information (defaults to $Pgm_Root/web/holical.ics) - holiday_no_stats = 1 to not generate stat holidays (next business day if the holiday is on a weekend) + +optional + + holiday_ical_filename = filename of generated holiday information (defaults to $Pgm_Root/web/holical.ics) + holiday_no_stats = 1 to not generate stat holidays (next business day if the holiday is on a weekend) ------------------------------------------------------------------------------------- Note: $Pgm_Root/bin/holical (ical creator helper program) has several dependencies: Date::Easter, Date::Manip, Date::ICal, Data::ICal, Data::ICal::Entry::Event ------------------------------------------------------------------------------------- - -=cut - - -$p_holical = new Process_Item(); - -$v_generate_holidays = new Voice_Cmd('Generate holiday information for [1,2,3,4] year(s)'); -$v_generate_holidays->set_info('Generate ical compatible holiday information'); -$v_generate_holidays->set_authority('anyone'); -my $holiday_output_filename = $config_parms{holiday_ical_filename}; -$holiday_output_filename = "$Pgm_Root/web/holical.ics" if !$holiday_output_filename; - - + +=cut + + +$p_holical = new Process_Item(); + +$v_generate_holidays = new Voice_Cmd('Generate holiday information for [1,2,3,4] year(s)'); +$v_generate_holidays->set_info('Generate ical compatible holiday information'); +$v_generate_holidays->set_authority('anyone'); +my $holiday_output_filename = $config_parms{holiday_ical_filename}; +$holiday_output_filename = "$Pgm_Root/web/holical.ics" if !$holiday_output_filename; + + if ((said $v_generate_holidays) or ($New_Year) or - (($Reload) and !(-e $holiday_output_filename))) { - - print_log "MH_Holidays: Starting holiday generation process..."; - my $options; - $options = "-n" if $config_parms{holiday_no_stats}; - my $years; - $years = said $v_generate_holidays; - $years = "0" if !$years; - - if (-e $config_parms{holiday_definition_file}) { - $p_holical->set("holical -y +$years $options -f $holiday_output_filename $config_parms{holiday_definition_file} "); - start $p_holical; - } else { - print_log "MH_Holidays: Error, cannot find holiday definition file!"; - } -} + (($Reload) and !(-e $holiday_output_filename))) { + + print_log "MH_Holidays: Starting holiday generation process..."; + my $options; + $options = "-n" if $config_parms{holiday_no_stats}; + my $years; + $years = said $v_generate_holidays; + $years = "0" if !$years; + + if (-e $config_parms{holiday_definition_file}) { + $p_holical->set("holical -y +$years $options -f $holiday_output_filename $config_parms{holiday_definition_file} "); + start $p_holical; + } else { + print_log "MH_Holidays: Error, cannot find holiday definition file!"; + } +} diff --git a/code/common/mp3_dj.pl b/code/common/mp3_dj.pl index 7f190905e..f8f2e71e8 100644 --- a/code/common/mp3_dj.pl +++ b/code/common/mp3_dj.pl @@ -1,287 +1,287 @@ -# Category=Music - -# $Date$ -# $Revision$ - -#@ -#@ Jukebox DJ-tested with Winamp/httpq, but should work with all that can respond with now playing info (including elapsed time of course!) Also lowers and restores player volume during speech. -#@ Requires: mp3 and a player module (ex. mp3_winamp) - -#noloop=start -my $trivia_question_asked = 1; -my $trivia_question_answered = 1; -my $dj_flag = ($config_parms{dj})?$config_parms{dj}:1; -my $speech_lowered_volume = 0; -#noloop=stop - -$timer_voice_over = new Timer; - -$v_dj = new Voice_Cmd('[Start,Stop] the DJ'); -$v_dj->set_info('Starts or stops the virtual disc jockey'); - -if (said $v_dj) { - my $state = $v_dj->{state}; - $v_dj->respond("app=dj $state" . 'ing the disc jockey...'); - $dj_flag = ($state eq 'Start'); -} - -sub voice_over { - return if !$dj_flag; - my $now_playing_formatted; - my $last_track = shift; - my $now_playing = &dj_now_playing(); - my $speech; - - my $mptimestr = &mp3_get_output_timestr(); - - my ( $mpelapse, $mprest ) = split ( /\//,$mptimestr ); - my ( $mpmin, $mpsec ) = split ( /:/,$mpelapse ); - $mpelapse = ( $mpmin * 60 ) + $mpsec ; - - print "DJ Voice over: $last_track-$now_playing-$mpelapse\n" if $Debug{dj}; - - - if ($now_playing ne $last_track and &mp3_playing() and $mpelapse < 7) { - my ($voice, $pitch); - my $time_slot; - $now_playing_formatted = format_track($now_playing); - - if (time_greater_than("12:00 AM") and time_less_than("6:00 AM")) { - $time_slot = 'early morning'; - - $voice = 'male'; - } - if (time_greater_than("6:00 AM") and time_less_than("12:00 PM")) { - $time_slot = 'morning'; - $voice = 'random'; # Morning zoo - } - elsif (time_greater_than("5:30 AM") and time_less_than("06:00 PM")) { - $time_slot = 'afternoon'; - $voice = 'female'; - } - elsif (time_greater_than("6:00 PM") and time_less_than("11:59 PM")) { - $time_slot = 'evening'; - $voice = 'female'; - } - - $voice = $config_parms{"dj_voice_$time_slot"} if $config_parms{"dj_voice_$time_slot"}; - - - - - - - if ($now_playing_formatted) { - if (not $trivia_question_asked) { - my $f_trivia_question = new File_Item("$config_parms{data_dir}/trivia_question.txt"); - my $trivia_question = read_all $f_trivia_question; - $speech = "Time for today's trivia question. $trivia_question"; - $trivia_question_asked = 1; - } - elsif ($trivia_question_asked and not $trivia_question_answered) { - my $f_trivia_answer = new File_Item("$config_parms{data_dir}/trivia_answer.txt"); - my $trivia_answer = read_all $f_trivia_answer; - $speech = "And now the answer to today's trivia question. $trivia_answer"; - $trivia_question_answered = 1; - } - elsif (rand(10) > 7) { - if (rand(10) > 4) { - if ($time_slot eq 'morning') { - play(app => 'dj', file => "fun/*.wav"); - $speech = "Good morning. "; - } - elsif ($time_slot eq 'afternoon') { - $speech = "Good afternoon. "; - } - elsif ($time_slot eq 'evening') { - $speech = "Good evening. "; - } - else { - $speech = "Still up? So are we! "; - } - if (rand(10) > 4 and $speech) { - $speech = " Hey, " . lcfirst($speech); - } - $speech .= "The outdoor temperature is " . $Weather{TempOutdoor} . '. ' if (defined $Weather{TempOutdoor}); - $speech .= "Inside it is " . $Weather{TempIndoor} . " degrees. " if ($Weather{TempIndoor}); - $speech .= $Weather{chance_of_rain} . ' ' if ($Weather{chance_of_rain} and rand(10) > 4); - $speech .= "There is mail in the mailbox. " if ($Save{mail_delivered} eq "1" and $Save{mail_retrieved} eq ""); - - if (defined $Weather{ChanceOfRainPercent} and $Weather{ChanceOfRainPercent} > 60) { - $speech .= "Looks like rain. "; - } - else { - $speech .= "It is raining. " if defined $Weather{IsRaining} and $Weather{IsRaining}; - } - $speech .= "Tonight is a full moon. " if ($Moon{phase} eq 'Full'); - - - } - - $speech .= "Now here's " . $now_playing_formatted; - if (rand(10) > 5) { - $speech .= ' on W M H--the voice of Misterhouse.'; - if (rand(10) > 6) { - $speech .= ' Keep it right here.'; - } - else { - $speech .= " I like this one."; - } - } - else { - $speech .= '. Misterhouse... Rocks!'; - } - } - else { - if (rand(10) > 4) { - $speech = "That was " . format_track($last_track); - } - elsif (rand(10) > 7) { - - my $conditions; - $conditions = $Weather{Summary_Short}; - - $speech = "It is $Time_Now"; - $speech .= ". $conditions" if (rand(10) > 6 and $conditions); - $speech .= ". In the stock market " . $Save{stock_results} if (rand(10) > 8 and $Save{stock_results}); - } - else { - - my @forecast_days; - my $forecast; - @forecast_days = split /\|/, $Weather{"Forecast Days"}; - - my $forecast_day = $forecast_days[($forecast_days[0] =~ /warning/)?1:0]; - $forecast = $Weather{"Forecast $forecast_day"}; - - $speech = "How are you? It is $Time_Now"; - $speech .= " and time for the weather forecast. " . $forecast if ($forecast and rand(10) > 6); - - - } - my ($previous_artist) = $last_track =~ /(.+)\s+-\s+(.+)/; - my ($artist,$title) = $now_playing =~ /(.+)\s+-\s+(.+)/; - if ($artist eq $previous_artist) { - $speech .= ". Now here's another by " . $artist; - } - else { - $speech .= ". Now it's " . $artist; - } - if (rand(10) > 3) { - $speech .= ' on W M H.'; - $speech .= ' Keep it right here.' if (rand(10) > 6) ; - } - else { - $speech .= '. Stay tuned...'; - } - } - - &speak("app=dj no_chime=1 voice=$voice " . ((defined $pitch)?" pitch=$pitch":'') . $speech); - } - } -} - -sub dj_now_playing { - my $ref = &mp3_get_playlist(); - my $track; - - if ($ref) { - my $pos = &mp3_get_playlist_pos(); - if ($pos >= 0) { - $track = ${$ref}[$pos] if $ref; - } else { - $track = &mp3_get_curr_song(); - } - } - return $track; -} - -sub dj { - my $mptimestr = &mp3_get_output_timestr(); - - my $last_track = &dj_now_playing(); - - my ( $mpelapse, $mprest ) = split ( /\//,$mptimestr ); - my ( $mpmin, $mpsec ) = split ( /:/,$mpelapse ); - $mpelapse = ( $mpmin * 60 ) + $mpsec ; - - my $mpisrun = &mp3_playing(); - - if ($mpisrun) { - - my ( $mptime, $mpperct ) = split ( / /,$mprest ) ; - ( $mpmin, $mpsec ) = split ( /:/,$mptime ); - $mptime = ( $mpmin * 60 ) + $mpsec ; - - - if ($mptime - $mpelapse > 5) { - $timer_voice_over->stop() unless inactive $timer_voice_over; - set $timer_voice_over $mptime - $mpelapse + 1, "voice_over(" . '"' . "$last_track" . '")'; - } - } -} - -sub format_track { - my $playing = shift; - my ($artist,$title) = $playing =~ /(.+)\s+-\s+(.+)/; - - if ($artist) { - return $title . ' by ' . $artist; - } -} - -&dj() if (new_minute (($config_parms{dj_freq})?$config_parms{dj_freq}:7) or $Reload) and $dj_flag; - -# *** This is stupid (should use a time stamp for last time question asked!) - -if (new_hour 8) { - $trivia_question_asked = 0; - $trivia_question_answered = 0; -} - -&Speak_pre_add_hook(\&dj_speech_hook) if $Reload; - -sub dj_speech_hook { - my %parms = &parse_func_parms(@_); - - my $mode = $parms{mode}; - my $app = $parms{app}; - - #lower volume if speech won't be muted - - if (&mp3_playing() and !$speech_lowered_volume and !$parms{to_file} and $mode ne 'mute' and (($mode_mh->{state} ne 'mute' and $mode_mh->{state} ne 'offline') or $mode eq 'unmuted')) { - $speech_lowered_volume = 1; - -&mp3_control('volume down'); -&mp3_control('volume down'); - -&mp3_control('volume down'); -&mp3_control('volume down'); - - - } -} - -if (state_now $mh_speakers eq OFF) { - - # *** Need isspeaking check here (for speeches with chimes, volume is raised after sound file ends, not the speech!) - - if ($speech_lowered_volume) { - for my $i (1..$speech_lowered_volume) { - -&mp3_control('volume up'); -&mp3_control('volume up'); - -&mp3_control('volume up'); -&mp3_control('volume up'); - - } - } - $speech_lowered_volume = 0; -} - - - - - - +# Category=Music + +# $Date$ +# $Revision$ + +#@ +#@ Jukebox DJ-tested with Winamp/httpq, but should work with all that can respond with now playing info (including elapsed time of course!) Also lowers and restores player volume during speech. +#@ Requires: mp3 and a player module (ex. mp3_winamp) + +#noloop=start +my $trivia_question_asked = 1; +my $trivia_question_answered = 1; +my $dj_flag = ($config_parms{dj})?$config_parms{dj}:1; +my $speech_lowered_volume = 0; +#noloop=stop + +$timer_voice_over = new Timer; + +$v_dj = new Voice_Cmd('[Start,Stop] the DJ'); +$v_dj->set_info('Starts or stops the virtual disc jockey'); + +if (said $v_dj) { + my $state = $v_dj->{state}; + $v_dj->respond("app=dj $state" . 'ing the disc jockey...'); + $dj_flag = ($state eq 'Start'); +} + +sub voice_over { + return if !$dj_flag; + my $now_playing_formatted; + my $last_track = shift; + my $now_playing = &dj_now_playing(); + my $speech; + + my $mptimestr = &mp3_get_output_timestr(); + + my ( $mpelapse, $mprest ) = split ( /\//,$mptimestr ); + my ( $mpmin, $mpsec ) = split ( /:/,$mpelapse ); + $mpelapse = ( $mpmin * 60 ) + $mpsec ; + + print "DJ Voice over: $last_track-$now_playing-$mpelapse\n" if $Debug{dj}; + + + if ($now_playing ne $last_track and &mp3_playing() and $mpelapse < 7) { + my ($voice, $pitch); + my $time_slot; + $now_playing_formatted = format_track($now_playing); + + if (time_greater_than("12:00 AM") and time_less_than("6:00 AM")) { + $time_slot = 'early morning'; + + $voice = 'male'; + } + if (time_greater_than("6:00 AM") and time_less_than("12:00 PM")) { + $time_slot = 'morning'; + $voice = 'random'; # Morning zoo + } + elsif (time_greater_than("5:30 AM") and time_less_than("06:00 PM")) { + $time_slot = 'afternoon'; + $voice = 'female'; + } + elsif (time_greater_than("6:00 PM") and time_less_than("11:59 PM")) { + $time_slot = 'evening'; + $voice = 'female'; + } + + $voice = $config_parms{"dj_voice_$time_slot"} if $config_parms{"dj_voice_$time_slot"}; + + + + + + + if ($now_playing_formatted) { + if (not $trivia_question_asked) { + my $f_trivia_question = new File_Item("$config_parms{data_dir}/trivia_question.txt"); + my $trivia_question = read_all $f_trivia_question; + $speech = "Time for today's trivia question. $trivia_question"; + $trivia_question_asked = 1; + } + elsif ($trivia_question_asked and not $trivia_question_answered) { + my $f_trivia_answer = new File_Item("$config_parms{data_dir}/trivia_answer.txt"); + my $trivia_answer = read_all $f_trivia_answer; + $speech = "And now the answer to today's trivia question. $trivia_answer"; + $trivia_question_answered = 1; + } + elsif (rand(10) > 7) { + if (rand(10) > 4) { + if ($time_slot eq 'morning') { + play(app => 'dj', file => "fun/*.wav"); + $speech = "Good morning. "; + } + elsif ($time_slot eq 'afternoon') { + $speech = "Good afternoon. "; + } + elsif ($time_slot eq 'evening') { + $speech = "Good evening. "; + } + else { + $speech = "Still up? So are we! "; + } + if (rand(10) > 4 and $speech) { + $speech = " Hey, " . lcfirst($speech); + } + $speech .= "The outdoor temperature is " . $Weather{TempOutdoor} . '. ' if (defined $Weather{TempOutdoor}); + $speech .= "Inside it is " . $Weather{TempIndoor} . " degrees. " if ($Weather{TempIndoor}); + $speech .= $Weather{chance_of_rain} . ' ' if ($Weather{chance_of_rain} and rand(10) > 4); + $speech .= "There is mail in the mailbox. " if ($Save{mail_delivered} eq "1" and $Save{mail_retrieved} eq ""); + + if (defined $Weather{ChanceOfRainPercent} and $Weather{ChanceOfRainPercent} > 60) { + $speech .= "Looks like rain. "; + } + else { + $speech .= "It is raining. " if defined $Weather{IsRaining} and $Weather{IsRaining}; + } + $speech .= "Tonight is a full moon. " if ($Moon{phase} eq 'Full'); + + + } + + $speech .= "Now here's " . $now_playing_formatted; + if (rand(10) > 5) { + $speech .= ' on W M H--the voice of Misterhouse.'; + if (rand(10) > 6) { + $speech .= ' Keep it right here.'; + } + else { + $speech .= " I like this one."; + } + } + else { + $speech .= '. Misterhouse... Rocks!'; + } + } + else { + if (rand(10) > 4) { + $speech = "That was " . format_track($last_track); + } + elsif (rand(10) > 7) { + + my $conditions; + $conditions = $Weather{Summary_Short}; + + $speech = "It is $Time_Now"; + $speech .= ". $conditions" if (rand(10) > 6 and $conditions); + $speech .= ". In the stock market " . $Save{stock_results} if (rand(10) > 8 and $Save{stock_results}); + } + else { + + my @forecast_days; + my $forecast; + @forecast_days = split /\|/, $Weather{"Forecast Days"}; + + my $forecast_day = $forecast_days[($forecast_days[0] =~ /warning/)?1:0]; + $forecast = $Weather{"Forecast $forecast_day"}; + + $speech = "How are you? It is $Time_Now"; + $speech .= " and time for the weather forecast. " . $forecast if ($forecast and rand(10) > 6); + + + } + my ($previous_artist) = $last_track =~ /(.+)\s+-\s+(.+)/; + my ($artist,$title) = $now_playing =~ /(.+)\s+-\s+(.+)/; + if ($artist eq $previous_artist) { + $speech .= ". Now here's another by " . $artist; + } + else { + $speech .= ". Now it's " . $artist; + } + if (rand(10) > 3) { + $speech .= ' on W M H.'; + $speech .= ' Keep it right here.' if (rand(10) > 6) ; + } + else { + $speech .= '. Stay tuned...'; + } + } + + &speak("app=dj no_chime=1 voice=$voice " . ((defined $pitch)?" pitch=$pitch":'') . $speech); + } + } +} + +sub dj_now_playing { + my $ref = &mp3_get_playlist(); + my $track; + + if ($ref) { + my $pos = &mp3_get_playlist_pos(); + if ($pos >= 0) { + $track = ${$ref}[$pos] if $ref; + } else { + $track = &mp3_get_curr_song(); + } + } + return $track; +} + +sub dj { + my $mptimestr = &mp3_get_output_timestr(); + + my $last_track = &dj_now_playing(); + + my ( $mpelapse, $mprest ) = split ( /\//,$mptimestr ); + my ( $mpmin, $mpsec ) = split ( /:/,$mpelapse ); + $mpelapse = ( $mpmin * 60 ) + $mpsec ; + + my $mpisrun = &mp3_playing(); + + if ($mpisrun) { + + my ( $mptime, $mpperct ) = split ( / /,$mprest ) ; + ( $mpmin, $mpsec ) = split ( /:/,$mptime ); + $mptime = ( $mpmin * 60 ) + $mpsec ; + + + if ($mptime - $mpelapse > 5) { + $timer_voice_over->stop() unless inactive $timer_voice_over; + set $timer_voice_over $mptime - $mpelapse + 1, "voice_over(" . '"' . "$last_track" . '")'; + } + } +} + +sub format_track { + my $playing = shift; + my ($artist,$title) = $playing =~ /(.+)\s+-\s+(.+)/; + + if ($artist) { + return $title . ' by ' . $artist; + } +} + +&dj() if (new_minute (($config_parms{dj_freq})?$config_parms{dj_freq}:7) or $Reload) and $dj_flag; + +# *** This is stupid (should use a time stamp for last time question asked!) + +if (new_hour 8) { + $trivia_question_asked = 0; + $trivia_question_answered = 0; +} + +&Speak_pre_add_hook(\&dj_speech_hook) if $Reload; + +sub dj_speech_hook { + my %parms = &parse_func_parms(@_); + + my $mode = $parms{mode}; + my $app = $parms{app}; + + #lower volume if speech won't be muted + + if (&mp3_playing() and !$speech_lowered_volume and !$parms{to_file} and $mode ne 'mute' and (($mode_mh->{state} ne 'mute' and $mode_mh->{state} ne 'offline') or $mode eq 'unmuted')) { + $speech_lowered_volume = 1; + +&mp3_control('volume down'); +&mp3_control('volume down'); + +&mp3_control('volume down'); +&mp3_control('volume down'); + + + } +} + +if (state_now $mh_speakers eq OFF) { + + # *** Need isspeaking check here (for speeches with chimes, volume is raised after sound file ends, not the speech!) + + if ($speech_lowered_volume) { + for my $i (1..$speech_lowered_volume) { + +&mp3_control('volume up'); +&mp3_control('volume up'); + +&mp3_control('volume up'); +&mp3_control('volume up'); + + } + } + $speech_lowered_volume = 0; +} + + + + + + diff --git a/code/examples/SOAP_examples/vb.net/AssemblyInfo.vb b/code/examples/SOAP_examples/vb.net/AssemblyInfo.vb index 06899e48f..ad055b9b3 100755 --- a/code/examples/SOAP_examples/vb.net/AssemblyInfo.vb +++ b/code/examples/SOAP_examples/vb.net/AssemblyInfo.vb @@ -1,32 +1,32 @@ -Imports System -Imports System.Reflection -Imports System.Runtime.InteropServices - -' General Information about an assembly is controlled through the following -' set of attributes. Change these attribute values to modify the information -' associated with an assembly. - -' Review the values of the assembly attributes - - - - - - - - - -'The following GUID is for the ID of the typelib if this project is exposed to COM - - -' Version information for an assembly consists of the following four values: -' -' Major Version -' Minor Version -' Build Number -' Revision -' -' You can specify all the values or you can default the Build and Revision Numbers -' by using the '*' as shown below: - - +Imports System +Imports System.Reflection +Imports System.Runtime.InteropServices + +' General Information about an assembly is controlled through the following +' set of attributes. Change these attribute values to modify the information +' associated with an assembly. + +' Review the values of the assembly attributes + + + + + + + + + +'The following GUID is for the ID of the typelib if this project is exposed to COM + + +' Version information for an assembly consists of the following four values: +' +' Major Version +' Minor Version +' Build Number +' Revision +' +' You can specify all the values or you can default the Build and Revision Numbers +' by using the '*' as shown below: + + diff --git a/code/examples/SOAP_examples/vb.net/Form1.resx b/code/examples/SOAP_examples/vb.net/Form1.resx index 91df5cc7d..d62ac83b0 100755 --- a/code/examples/SOAP_examples/vb.net/Form1.resx +++ b/code/examples/SOAP_examples/vb.net/Form1.resx @@ -1,211 +1,211 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Assembly - - - False - - - Assembly - - - Assembly - - - False - - - Assembly - - - Assembly - - - False - - - Assembly - - - False - - - Assembly - - - Assembly - - - False - - - Assembly - - - Assembly - - - False - - - Assembly - - - Assembly - - - False - - - Assembly - - - Assembly - - - Assembly - - - False - - - Assembly - - - False - - - Assembly - - - Assembly - - - False - - - (Default) - - - False - - - False - - - 8, 8 - - - True - - - Form1 - - - 80 - - - True - - - Assembly - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Assembly + + + False + + + Assembly + + + Assembly + + + False + + + Assembly + + + Assembly + + + False + + + Assembly + + + False + + + Assembly + + + Assembly + + + False + + + Assembly + + + Assembly + + + False + + + Assembly + + + Assembly + + + False + + + Assembly + + + Assembly + + + Assembly + + + False + + + Assembly + + + False + + + Assembly + + + Assembly + + + False + + + (Default) + + + False + + + False + + + 8, 8 + + + True + + + Form1 + + + 80 + + + True + + + Assembly + \ No newline at end of file diff --git a/code/examples/SOAP_examples/vb.net/Form1.vb b/code/examples/SOAP_examples/vb.net/Form1.vb index aa573d063..151ab5e54 100755 --- a/code/examples/SOAP_examples/vb.net/Form1.vb +++ b/code/examples/SOAP_examples/vb.net/Form1.vb @@ -1,232 +1,232 @@ -Public Class Form1 - Inherits System.Windows.Forms.Form - - Private ws As MySoapRef.mhsoap - -#Region " Windows Form Designer generated code " - - Public Sub New() - MyBase.New() - - 'This call is required by the Windows Form Designer. - InitializeComponent() - - 'Add any initialization after the InitializeComponent() call - - End Sub - - 'Form overrides dispose to clean up the component list. - Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) - If disposing Then - If Not (components Is Nothing) Then - components.Dispose() - End If - End If - MyBase.Dispose(disposing) - End Sub - - 'Required by the Windows Form Designer - Private components As System.ComponentModel.IContainer - - 'NOTE: The following procedure is required by the Windows Form Designer - 'It can be modified using the Windows Form Designer. - 'Do not modify it using the code editor. - Friend WithEvents ComboBox1 As System.Windows.Forms.ComboBox - Friend WithEvents ListBox1 As System.Windows.Forms.ListBox - Friend WithEvents Label1 As System.Windows.Forms.Label - Friend WithEvents cmdConnect As System.Windows.Forms.Button - Friend WithEvents Label2 As System.Windows.Forms.Label - Friend WithEvents txtNewState As System.Windows.Forms.TextBox - Friend WithEvents cmdSetState As System.Windows.Forms.Button - Friend WithEvents txtURL As System.Windows.Forms.TextBox - Friend WithEvents lblState As System.Windows.Forms.Label - Private Sub InitializeComponent() - Me.ComboBox1 = New System.Windows.Forms.ComboBox - Me.ListBox1 = New System.Windows.Forms.ListBox - Me.txtURL = New System.Windows.Forms.TextBox - Me.Label1 = New System.Windows.Forms.Label - Me.cmdConnect = New System.Windows.Forms.Button - Me.Label2 = New System.Windows.Forms.Label - Me.lblState = New System.Windows.Forms.Label - Me.txtNewState = New System.Windows.Forms.TextBox - Me.cmdSetState = New System.Windows.Forms.Button - Me.SuspendLayout() - ' - 'ComboBox1 - ' - Me.ComboBox1.Location = New System.Drawing.Point(8, 72) - Me.ComboBox1.Name = "ComboBox1" - Me.ComboBox1.Size = New System.Drawing.Size(272, 21) - Me.ComboBox1.TabIndex = 0 - Me.ComboBox1.Text = "Press Connect to fill me" - ' - 'ListBox1 - ' - Me.ListBox1.Location = New System.Drawing.Point(8, 104) - Me.ListBox1.Name = "ListBox1" - Me.ListBox1.Size = New System.Drawing.Size(272, 199) - Me.ListBox1.TabIndex = 1 - ' - 'txtURL - ' - Me.txtURL.Location = New System.Drawing.Point(136, 24) - Me.txtURL.Name = "txtURL" - Me.txtURL.Size = New System.Drawing.Size(136, 20) - Me.txtURL.TabIndex = 2 - Me.txtURL.Text = "http://misterhouse:8080" - ' - 'Label1 - ' - Me.Label1.Location = New System.Drawing.Point(16, 24) - Me.Label1.Name = "Label1" - Me.Label1.Size = New System.Drawing.Size(112, 16) - Me.Label1.TabIndex = 3 - Me.Label1.Text = "Misterhouse URL" - ' - 'cmdConnect - ' - Me.cmdConnect.Location = New System.Drawing.Point(336, 24) - Me.cmdConnect.Name = "cmdConnect" - Me.cmdConnect.Size = New System.Drawing.Size(104, 24) - Me.cmdConnect.TabIndex = 4 - Me.cmdConnect.Text = "Connect" - ' - 'Label2 - ' - Me.Label2.Location = New System.Drawing.Point(296, 112) - Me.Label2.Name = "Label2" - Me.Label2.Size = New System.Drawing.Size(40, 16) - Me.Label2.TabIndex = 5 - Me.Label2.Text = "State :" - ' - 'lblState - ' - Me.lblState.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D - Me.lblState.Location = New System.Drawing.Point(360, 112) - Me.lblState.Name = "lblState" - Me.lblState.Size = New System.Drawing.Size(80, 16) - Me.lblState.TabIndex = 6 - Me.lblState.Text = "Unknown" - ' - 'txtNewState - ' - Me.txtNewState.Location = New System.Drawing.Point(360, 136) - Me.txtNewState.Name = "txtNewState" - Me.txtNewState.Size = New System.Drawing.Size(80, 20) - Me.txtNewState.TabIndex = 7 - Me.txtNewState.Text = "" - ' - 'cmdSetState - ' - Me.cmdSetState.Location = New System.Drawing.Point(296, 136) - Me.cmdSetState.Name = "cmdSetState" - Me.cmdSetState.Size = New System.Drawing.Size(48, 24) - Me.cmdSetState.TabIndex = 8 - Me.cmdSetState.Text = "Set" - ' - 'Form1 - ' - Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13) - Me.ClientSize = New System.Drawing.Size(472, 318) - Me.Controls.Add(Me.cmdSetState) - Me.Controls.Add(Me.txtNewState) - Me.Controls.Add(Me.lblState) - Me.Controls.Add(Me.Label2) - Me.Controls.Add(Me.cmdConnect) - Me.Controls.Add(Me.Label1) - Me.Controls.Add(Me.txtURL) - Me.Controls.Add(Me.ListBox1) - Me.Controls.Add(Me.ComboBox1) - Me.Name = "Form1" - Me.Text = "Misterhouse Test Client" - Me.ResumeLayout(False) - - End Sub - -#End Region - - - Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load - System.Net.ServicePointManager.Expect100Continue = False - - ws = New MySoapRef.mhsoap - - End Sub - - - - Private Sub ComboBox1_SelectedValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ComboBox1.SelectedValueChanged - Dim results As String() - Dim i As Integer - - results = ws.ListObjectsByType(ComboBox1.Text) - - With ListBox1 - .BeginUpdate() - For i = .Items.Count - 1 To 0 Step -1 - .Items.Remove(.Items(0)) - Next i - - For i = 0 To UBound(results) - .Items.Add(results(i)) - Next i - .EndUpdate() - End With - End Sub - - Private Sub cmdConnect_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdConnect.Click - Dim i As Integer - Dim results As String() - - ws.Url = txtURL.Text & "/bin/soapcgi.pl" - - results = ws.ListObjectTypes() - - With ComboBox1 - .Text = "Choose Type" - For i = 0 To UBound(results) - .Items.Add(results(i)) - Next - End With - End Sub - - Private Sub ListBox1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ListBox1.SelectedIndexChanged - - End Sub - - Private Sub ListBox1_SelectedValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ListBox1.SelectedValueChanged - Dim stState As String - - Dim stItem As String - - stItem = ListBox1.SelectedItem - - If Mid(stItem, 1, 1) = "$" Then - stItem = stItem.Remove(0, 1) - End If - - stState = ws.GetItemState(stItem) - - lblState.Text = stState - - End Sub - - Private Sub cmdSetState_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdSetState.Click - - Dim iReturn As Integer - Dim stItem As String - Dim stState As String - - stItem = ListBox1.SelectedItem - - If Mid(stItem, 1, 1) = "$" Then - stItem = stItem.Remove(0, 1) - End If - - stState = txtNewState.Text - iReturn = ws.SetItemState(stItem, stState) - - lblState.Text = txtNewState.Text - - End Sub -End Class +Public Class Form1 + Inherits System.Windows.Forms.Form + + Private ws As MySoapRef.mhsoap + +#Region " Windows Form Designer generated code " + + Public Sub New() + MyBase.New() + + 'This call is required by the Windows Form Designer. + InitializeComponent() + + 'Add any initialization after the InitializeComponent() call + + End Sub + + 'Form overrides dispose to clean up the component list. + Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) + If disposing Then + If Not (components Is Nothing) Then + components.Dispose() + End If + End If + MyBase.Dispose(disposing) + End Sub + + 'Required by the Windows Form Designer + Private components As System.ComponentModel.IContainer + + 'NOTE: The following procedure is required by the Windows Form Designer + 'It can be modified using the Windows Form Designer. + 'Do not modify it using the code editor. + Friend WithEvents ComboBox1 As System.Windows.Forms.ComboBox + Friend WithEvents ListBox1 As System.Windows.Forms.ListBox + Friend WithEvents Label1 As System.Windows.Forms.Label + Friend WithEvents cmdConnect As System.Windows.Forms.Button + Friend WithEvents Label2 As System.Windows.Forms.Label + Friend WithEvents txtNewState As System.Windows.Forms.TextBox + Friend WithEvents cmdSetState As System.Windows.Forms.Button + Friend WithEvents txtURL As System.Windows.Forms.TextBox + Friend WithEvents lblState As System.Windows.Forms.Label + Private Sub InitializeComponent() + Me.ComboBox1 = New System.Windows.Forms.ComboBox + Me.ListBox1 = New System.Windows.Forms.ListBox + Me.txtURL = New System.Windows.Forms.TextBox + Me.Label1 = New System.Windows.Forms.Label + Me.cmdConnect = New System.Windows.Forms.Button + Me.Label2 = New System.Windows.Forms.Label + Me.lblState = New System.Windows.Forms.Label + Me.txtNewState = New System.Windows.Forms.TextBox + Me.cmdSetState = New System.Windows.Forms.Button + Me.SuspendLayout() + ' + 'ComboBox1 + ' + Me.ComboBox1.Location = New System.Drawing.Point(8, 72) + Me.ComboBox1.Name = "ComboBox1" + Me.ComboBox1.Size = New System.Drawing.Size(272, 21) + Me.ComboBox1.TabIndex = 0 + Me.ComboBox1.Text = "Press Connect to fill me" + ' + 'ListBox1 + ' + Me.ListBox1.Location = New System.Drawing.Point(8, 104) + Me.ListBox1.Name = "ListBox1" + Me.ListBox1.Size = New System.Drawing.Size(272, 199) + Me.ListBox1.TabIndex = 1 + ' + 'txtURL + ' + Me.txtURL.Location = New System.Drawing.Point(136, 24) + Me.txtURL.Name = "txtURL" + Me.txtURL.Size = New System.Drawing.Size(136, 20) + Me.txtURL.TabIndex = 2 + Me.txtURL.Text = "http://misterhouse:8080" + ' + 'Label1 + ' + Me.Label1.Location = New System.Drawing.Point(16, 24) + Me.Label1.Name = "Label1" + Me.Label1.Size = New System.Drawing.Size(112, 16) + Me.Label1.TabIndex = 3 + Me.Label1.Text = "Misterhouse URL" + ' + 'cmdConnect + ' + Me.cmdConnect.Location = New System.Drawing.Point(336, 24) + Me.cmdConnect.Name = "cmdConnect" + Me.cmdConnect.Size = New System.Drawing.Size(104, 24) + Me.cmdConnect.TabIndex = 4 + Me.cmdConnect.Text = "Connect" + ' + 'Label2 + ' + Me.Label2.Location = New System.Drawing.Point(296, 112) + Me.Label2.Name = "Label2" + Me.Label2.Size = New System.Drawing.Size(40, 16) + Me.Label2.TabIndex = 5 + Me.Label2.Text = "State :" + ' + 'lblState + ' + Me.lblState.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D + Me.lblState.Location = New System.Drawing.Point(360, 112) + Me.lblState.Name = "lblState" + Me.lblState.Size = New System.Drawing.Size(80, 16) + Me.lblState.TabIndex = 6 + Me.lblState.Text = "Unknown" + ' + 'txtNewState + ' + Me.txtNewState.Location = New System.Drawing.Point(360, 136) + Me.txtNewState.Name = "txtNewState" + Me.txtNewState.Size = New System.Drawing.Size(80, 20) + Me.txtNewState.TabIndex = 7 + Me.txtNewState.Text = "" + ' + 'cmdSetState + ' + Me.cmdSetState.Location = New System.Drawing.Point(296, 136) + Me.cmdSetState.Name = "cmdSetState" + Me.cmdSetState.Size = New System.Drawing.Size(48, 24) + Me.cmdSetState.TabIndex = 8 + Me.cmdSetState.Text = "Set" + ' + 'Form1 + ' + Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13) + Me.ClientSize = New System.Drawing.Size(472, 318) + Me.Controls.Add(Me.cmdSetState) + Me.Controls.Add(Me.txtNewState) + Me.Controls.Add(Me.lblState) + Me.Controls.Add(Me.Label2) + Me.Controls.Add(Me.cmdConnect) + Me.Controls.Add(Me.Label1) + Me.Controls.Add(Me.txtURL) + Me.Controls.Add(Me.ListBox1) + Me.Controls.Add(Me.ComboBox1) + Me.Name = "Form1" + Me.Text = "Misterhouse Test Client" + Me.ResumeLayout(False) + + End Sub + +#End Region + + + Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load + System.Net.ServicePointManager.Expect100Continue = False + + ws = New MySoapRef.mhsoap + + End Sub + + + + Private Sub ComboBox1_SelectedValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ComboBox1.SelectedValueChanged + Dim results As String() + Dim i As Integer + + results = ws.ListObjectsByType(ComboBox1.Text) + + With ListBox1 + .BeginUpdate() + For i = .Items.Count - 1 To 0 Step -1 + .Items.Remove(.Items(0)) + Next i + + For i = 0 To UBound(results) + .Items.Add(results(i)) + Next i + .EndUpdate() + End With + End Sub + + Private Sub cmdConnect_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdConnect.Click + Dim i As Integer + Dim results As String() + + ws.Url = txtURL.Text & "/bin/soapcgi.pl" + + results = ws.ListObjectTypes() + + With ComboBox1 + .Text = "Choose Type" + For i = 0 To UBound(results) + .Items.Add(results(i)) + Next + End With + End Sub + + Private Sub ListBox1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ListBox1.SelectedIndexChanged + + End Sub + + Private Sub ListBox1_SelectedValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ListBox1.SelectedValueChanged + Dim stState As String + + Dim stItem As String + + stItem = ListBox1.SelectedItem + + If Mid(stItem, 1, 1) = "$" Then + stItem = stItem.Remove(0, 1) + End If + + stState = ws.GetItemState(stItem) + + lblState.Text = stState + + End Sub + + Private Sub cmdSetState_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdSetState.Click + + Dim iReturn As Integer + Dim stItem As String + Dim stState As String + + stItem = ListBox1.SelectedItem + + If Mid(stItem, 1, 1) = "$" Then + stItem = stItem.Remove(0, 1) + End If + + stState = txtNewState.Text + iReturn = ws.SetItemState(stItem, stState) + + lblState.Text = txtNewState.Text + + End Sub +End Class diff --git a/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.map b/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.map index 6c75b6170..b5c7d41f6 100755 --- a/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.map +++ b/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.map @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.vb b/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.vb index fc947a21b..ef4362036 100755 --- a/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.vb +++ b/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/Reference.vb @@ -1,113 +1,113 @@ -'------------------------------------------------------------------------------ -' -' This code was generated by a tool. -' Runtime Version: 1.1.4322.2032 -' -' Changes to this file may cause incorrect behavior and will be lost if -' the code is regenerated. -' -'------------------------------------------------------------------------------ - -Option Strict Off -Option Explicit On - -Imports System -Imports System.ComponentModel -Imports System.Diagnostics -Imports System.Web.Services -Imports System.Web.Services.Protocols -Imports System.Xml.Serialization - -' -'This source code was auto-generated by Microsoft.VSDesigner, Version 1.1.4322.2032. -' -Namespace MySoapRef - - ' - _ - Public Class mhsoap - Inherits System.Web.Services.Protocols.SoapHttpClientProtocol - - ' - Public Sub New() - MyBase.New - Me.Url = "http://misterhouse:8080/bin/soapcgi.pl" - End Sub - - ' - _ - Public Function ListObjectTypes() As String() - Dim results() As Object = Me.Invoke("ListObjectTypes", New Object(-1) {}) - Return CType(results(0),String()) - End Function - - ' - Public Function BeginListObjectTypes(ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult - Return Me.BeginInvoke("ListObjectTypes", New Object(-1) {}, callback, asyncState) - End Function - - ' - Public Function EndListObjectTypes(ByVal asyncResult As System.IAsyncResult) As String() - Dim results() As Object = Me.EndInvoke(asyncResult) - Return CType(results(0),String()) - End Function - - ' - _ - Public Function ListObjectsByType(ByVal object_type As String) As String() - Dim results() As Object = Me.Invoke("ListObjectsByType", New Object() {object_type}) - Return CType(results(0),String()) - End Function - - ' - Public Function BeginListObjectsByType(ByVal object_type As String, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult - Return Me.BeginInvoke("ListObjectsByType", New Object() {object_type}, callback, asyncState) - End Function - - ' - Public Function EndListObjectsByType(ByVal asyncResult As System.IAsyncResult) As String() - Dim results() As Object = Me.EndInvoke(asyncResult) - Return CType(results(0),String()) - End Function - - ' - _ - Public Function SetItemState(ByVal ItemToSet As String, ByRef State As String) As Integer - Dim results() As Object = Me.Invoke("SetItemState", New Object() {ItemToSet, State}) - State = CType(results(1),String) - Return CType(results(0),Integer) - End Function - - ' - Public Function BeginSetItemState(ByVal ItemToSet As String, ByVal State As String, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult - Return Me.BeginInvoke("SetItemState", New Object() {ItemToSet, State}, callback, asyncState) - End Function - - ' - Public Function EndSetItemState(ByVal asyncResult As System.IAsyncResult, ByRef State As String) As Integer - Dim results() As Object = Me.EndInvoke(asyncResult) - State = CType(results(1),String) - Return CType(results(0),Integer) - End Function - - ' - _ - Public Function GetItemState(ByVal ItemToGet As String) As String - Dim results() As Object = Me.Invoke("GetItemState", New Object() {ItemToGet}) - Return CType(results(0),String) - End Function - - ' - Public Function BeginGetItemState(ByVal ItemToGet As String, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult - Return Me.BeginInvoke("GetItemState", New Object() {ItemToGet}, callback, asyncState) - End Function - - ' - Public Function EndGetItemState(ByVal asyncResult As System.IAsyncResult) As String - Dim results() As Object = Me.EndInvoke(asyncResult) - Return CType(results(0),String) - End Function - End Class -End Namespace +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version: 1.1.4322.2032 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict Off +Option Explicit On + +Imports System +Imports System.ComponentModel +Imports System.Diagnostics +Imports System.Web.Services +Imports System.Web.Services.Protocols +Imports System.Xml.Serialization + +' +'This source code was auto-generated by Microsoft.VSDesigner, Version 1.1.4322.2032. +' +Namespace MySoapRef + + ' + _ + Public Class mhsoap + Inherits System.Web.Services.Protocols.SoapHttpClientProtocol + + ' + Public Sub New() + MyBase.New + Me.Url = "http://misterhouse:8080/bin/soapcgi.pl" + End Sub + + ' + _ + Public Function ListObjectTypes() As String() + Dim results() As Object = Me.Invoke("ListObjectTypes", New Object(-1) {}) + Return CType(results(0),String()) + End Function + + ' + Public Function BeginListObjectTypes(ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult + Return Me.BeginInvoke("ListObjectTypes", New Object(-1) {}, callback, asyncState) + End Function + + ' + Public Function EndListObjectTypes(ByVal asyncResult As System.IAsyncResult) As String() + Dim results() As Object = Me.EndInvoke(asyncResult) + Return CType(results(0),String()) + End Function + + ' + _ + Public Function ListObjectsByType(ByVal object_type As String) As String() + Dim results() As Object = Me.Invoke("ListObjectsByType", New Object() {object_type}) + Return CType(results(0),String()) + End Function + + ' + Public Function BeginListObjectsByType(ByVal object_type As String, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult + Return Me.BeginInvoke("ListObjectsByType", New Object() {object_type}, callback, asyncState) + End Function + + ' + Public Function EndListObjectsByType(ByVal asyncResult As System.IAsyncResult) As String() + Dim results() As Object = Me.EndInvoke(asyncResult) + Return CType(results(0),String()) + End Function + + ' + _ + Public Function SetItemState(ByVal ItemToSet As String, ByRef State As String) As Integer + Dim results() As Object = Me.Invoke("SetItemState", New Object() {ItemToSet, State}) + State = CType(results(1),String) + Return CType(results(0),Integer) + End Function + + ' + Public Function BeginSetItemState(ByVal ItemToSet As String, ByVal State As String, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult + Return Me.BeginInvoke("SetItemState", New Object() {ItemToSet, State}, callback, asyncState) + End Function + + ' + Public Function EndSetItemState(ByVal asyncResult As System.IAsyncResult, ByRef State As String) As Integer + Dim results() As Object = Me.EndInvoke(asyncResult) + State = CType(results(1),String) + Return CType(results(0),Integer) + End Function + + ' + _ + Public Function GetItemState(ByVal ItemToGet As String) As String + Dim results() As Object = Me.Invoke("GetItemState", New Object() {ItemToGet}) + Return CType(results(0),String) + End Function + + ' + Public Function BeginGetItemState(ByVal ItemToGet As String, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult + Return Me.BeginInvoke("GetItemState", New Object() {ItemToGet}, callback, asyncState) + End Function + + ' + Public Function EndGetItemState(ByVal asyncResult As System.IAsyncResult) As String + Dim results() As Object = Me.EndInvoke(asyncResult) + Return CType(results(0),String) + End Function + End Class +End Namespace diff --git a/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/mhsoap.wsdl b/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/mhsoap.wsdl index a22f1af8b..173074943 100755 --- a/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/mhsoap.wsdl +++ b/code/examples/SOAP_examples/vb.net/Web References/MySoapRef/mhsoap.wsdl @@ -1,100 +1,100 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj b/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj index 1cdf2bfae..4e0be1650 100755 --- a/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj +++ b/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj @@ -1,144 +1,144 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj.user b/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj.user index cebfc896c..c0fd212a9 100755 --- a/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj.user +++ b/code/examples/SOAP_examples/vb.net/WebServiceTest.vbproj.user @@ -1,48 +1,48 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/code/examples/iButton_weather_station.pl b/code/examples/iButton_weather_station.pl index c092f9967..e3a71bf30 100755 --- a/code/examples/iButton_weather_station.pl +++ b/code/examples/iButton_weather_station.pl @@ -1,231 +1,231 @@ -# Category=Weather -# -# Interface to DS2450 based 1-Wire weather stations. -# Max Lock 12/08. - -use iButton; -use Weather_Common; - -$ib_wind_dir = new iButton '2000000000F521'; -$ib_wind_speed = new iButton '1D0000000151A3'; -$ib_temp_outside = new iButton '100008000948F7'; - -use vars '%weather'; -my $delta_i; -my %delta; -my %iButton_data_avg_data; -my $wind_time2; -my $last_wind_count; -my @wind_speed; -my @wind_cos; -my @wind_sin; - -&read_iButton_temp($ib_temp_outside) if $New_Second and ($Second == 45 or $Second == 15); -&iButton_wind_read if ($New_Second and !($Second % 15)); - -&setup_ds2450($ib_wind_dir) if $Startup; - -#if (time_cron('* * * * *') || $Startup) { -if (time_cron('* * * * *')) { - # - # Define the source variables - $Weather{DewOutdoor}; # used in status line - $Weather{TempOutdoor} = state $ib_temp_outside; # used in status line - $Weather{WindDir} = state $ib_wind_dir; - $Weather{WindSpeed} = state $ib_wind_speed; - # - # Define the derived variables - $Weather{WindGustDir} = $Weather{WindDir}; # Whats the relation between Dir and GustDir? - $Weather{WindAvgDir} = &average_wind_dir($Weather{WindDir}); - $Weather{WindGustSpeed} = $Weather{WindSpeed} if $Weather{WindSpeed} > $Weather{WindGustSpeed}; - $Weather{WindAvgSpeed} = &average_wind_speed($Weather{WindSpeed}); - $Weather{WindChill} = &windchill($Weather{TempOutdoor}, $Weather{WindAvgSpeed}); - # - # Log variables - $Weather{SummaryWind} = sprintf("cur/avg %s(%3.1f)/%s(%3.1f) %3.1f%s/%3.1f%s",(&convert_wind_dir_to_abbr($Weather{WindDir})),$Weather{WindDir},(&convert_wind_dir_to_abbr($Weather{WindAvgDir})),$Weather{WindAvgDir},$Weather{WindSpeed},$main::config_parms{weather_uom_wind},$Weather{WindAvgSpeed},$main::config_parms{weather_uom_wind}); - $Weather{SummaryTemp} = sprintf("%3.1f%s",$Weather{TempOutdoor},$main::config_parms{weather_uom_temp}); - # - print_log "Wind $Weather{SummaryWind}, Temperature $Weather{SummaryTemp}"; - &Weather_Common::weather_updated; -} - -### Only subroutines below this point ### - -sub deg_to_rad { ($_[0]/180) * (4 * atan2(1,1)) } # convert degrees to radians - -sub rad_to_deg { ($_[0] / (4 * atan2(1,1))) * 180 } # convert radians to degrees - -sub average_wind_dir { - my $readings = 10; - my $sum_wind_sin; - my $sum_wind_cos; - push (@wind_sin, sin(°_to_rad($_[0]))); - push (@wind_cos, cos(°_to_rad($_[0]))); - if ((scalar(@wind_cos)) == ($readings + 1)){shift @wind_cos}; - if ((scalar(@wind_sin)) == ($readings + 1)){shift @wind_sin}; - foreach (@wind_sin) {$sum_wind_sin += $_}; - foreach (@wind_cos) {$sum_wind_cos += $_}; - my $avg_wind_dir = &rad_to_deg(atan2($sum_wind_sin,$sum_wind_cos)); - if($avg_wind_dir < 0){$avg_wind_dir += 359; }; - $avg_wind_dir = sprintf("%.0f",$avg_wind_dir); - return $avg_wind_dir; -} - -sub average_wind_speed { - my $readings = 10; - my $avg_wind_speed; - push (@wind_speed, $_[0]); - if ((scalar(@wind_speed)) == ($readings + 1)){shift @wind_speed}; - foreach (@wind_speed) {$avg_wind_speed += $_}; - $avg_wind_speed = $avg_wind_speed / scalar(@wind_speed); - return $avg_wind_speed; -} - -sub convert_wind_dir_to_abbr { - my ($dir)=@_; - return 'unknown' if $dir !~ /^[\d \.]+$/; - if ($dir >= 0 and $dir <= 360) { - return qw{North NNE NE ENE East ESE SE SSE South SSW SW WSW West WNW NW NNW North}[(($dir+11.25)/22.5)%16]; - } - return 'unknown'; -} - -sub windchill { - my $temp = shift; - my $wind = shift; - my $chill; - - if (($wind<5) || ($wind>100) || ($temp<-50) || ($temp>5)) { - $chill = ''; - } - else { - $chill=(13.12 + 0.6215*$temp - 11.37*($wind**0.16) + 0.3965*$temp*($wind**0.16)); - $chill=int($chill+0.5); - print "temp $temp wind $wind chill $chill\n"; - } - return $chill; -} - -sub iButton_wind_read { - # - #wind speed (mph) - # - my $wind_time1 = &get_tickcount; - my $count = $ib_wind_speed->Hardware::iButton::Device::DS2423::read_counter(); - # - if ($wind_time2) { - my $revolution_sec = (($count - $last_wind_count) * 1000) / ($wind_time1 - $wind_time2) / 2.0; - set $ib_wind_speed sprintf("%3.2f",$revolution_sec * 2.453); - } - # - $last_wind_count = $count; - $wind_time2 = $wind_time1; - $Save{WindGustMax} = 0 if $New_Day; - # - # wind direction (c) - # - my %position_lookup=(); - $position_lookup{'HHLH'}=1; - $position_lookup{'HMMH'}=2; - $position_lookup{'HLHH'}=3; - $position_lookup{'MMHH'}=4; - $position_lookup{'LHHH'}=5; - $position_lookup{'LHHZ'}=6; - $position_lookup{'HHHZ'}=7; - $position_lookup{'HHZZ'}=8; - $position_lookup{'HHZH'}=9; - $position_lookup{'HZZH'}=10; - $position_lookup{'HZHH'}=11; - $position_lookup{'ZZHH'}=12; - $position_lookup{'ZHHH'}=13; - $position_lookup{'ZHHL'}=14; - $position_lookup{'HHHL'}=15; - $position_lookup{'HHMM'}=16; - # - my $wind_adc_states = &read_ds2450($ib_wind_dir); - my $wind_position = $position_lookup{$wind_adc_states}; - $Weather{WindDir} = sprintf("%3.1f",($wind_position * 22.5)); - set $ib_wind_dir $Weather{WindDir}; -} - -sub read_iButton_temp { - my @ib_list = @_; - for my $ib (@ib_list) { - my $temp = $ib->read_temperature_hires(); - next if $temp < -20 or $temp > 120; - my $temp_c = sprintf("%3.2f", $temp); - my $temp_f = sprintf("%3.2f", $temp*9/5 +32); - my $serial = $ib->serial(); - - # Average the last 5 entries - if (defined @{$iButton_data_avg_data{$serial}}) { - unshift(@{$iButton_data_avg_data{$serial}}, $temp_c); - pop(@{$iButton_data_avg_data{$serial}}); - } - else { - @{$iButton_data_avg_data{$serial}} = ($temp_c) x 5; - } - - my $iButton_data_avg = 0; - grep($iButton_data_avg += $_, @{$iButton_data_avg_data{$serial}}); - $iButton_data_avg /= 5; - $ib->{state} = sprintf("%3.1f", $iButton_data_avg); - } -} - -sub read_ds2450 { - # Read ds2450 adc's and return a 4 character string representing their states - # H=high,M=medium,L=low,Z=zero - # - $ib_wind_dir->Hardware::iButton::Device::DS2450::convert('all'); - my ($A,$B,$C,$D) = $ib_wind_dir->Hardware::iButton::Device::DS2450::readAD('all'); - my $channel_A_state = &volts_to_state($A); - my $channel_B_state = &volts_to_state($B); - my $channel_C_state = &volts_to_state($C); - my $channel_D_state = &volts_to_state($D); - return $channel_A_state.$channel_B_state.$channel_C_state.$channel_D_state; -} - -sub volts_to_state { - # Calculate state from voltage - my $volts = $_[0]; - my $state = 'Z'; - if ($volts >= 2){$state = 'L'}; - if ($volts >= 3){$state = 'M'}; - if ($volts >= 4){$state = 'H'}; - return $state; -} - -sub setup_ds2450 { - my $ibutton = $_[0]; - my $VCC = 0; - my %A; - my %B; - my %C; - my %D; - my $setupds2450 = 0; - until ($setupds2450 == 1) { - # - $A{type} = "AD"; - $A{resolution} = 4; - $A{range} = 5.12; - $B{type} = "AD"; - $B{resolution} = 4; - $B{range} = 5.12; - $C{type} = "AD"; - $C{resolution} = 4; - $C{range} = 5.12; - $D{type} = "AD"; - $D{resolution} = 4; - $D{range} = 5.12; - # - if ($ib_wind_dir->Hardware::iButton::Device::DS2450::setup($VCC,\%A,\%B,\%C,\%D)){$setupds2450 = 1}; - if ($setupds2450 = 1) { - print_log "Initialised weather station DS2450"; - }else{ - print_log "Failed to initialise weather station DS2450, retrying..."; - sleep 1; - } - } -} - +# Category=Weather +# +# Interface to DS2450 based 1-Wire weather stations. +# Max Lock 12/08. + +use iButton; +use Weather_Common; + +$ib_wind_dir = new iButton '2000000000F521'; +$ib_wind_speed = new iButton '1D0000000151A3'; +$ib_temp_outside = new iButton '100008000948F7'; + +use vars '%weather'; +my $delta_i; +my %delta; +my %iButton_data_avg_data; +my $wind_time2; +my $last_wind_count; +my @wind_speed; +my @wind_cos; +my @wind_sin; + +&read_iButton_temp($ib_temp_outside) if $New_Second and ($Second == 45 or $Second == 15); +&iButton_wind_read if ($New_Second and !($Second % 15)); + +&setup_ds2450($ib_wind_dir) if $Startup; + +#if (time_cron('* * * * *') || $Startup) { +if (time_cron('* * * * *')) { + # + # Define the source variables + $Weather{DewOutdoor}; # used in status line + $Weather{TempOutdoor} = state $ib_temp_outside; # used in status line + $Weather{WindDir} = state $ib_wind_dir; + $Weather{WindSpeed} = state $ib_wind_speed; + # + # Define the derived variables + $Weather{WindGustDir} = $Weather{WindDir}; # Whats the relation between Dir and GustDir? + $Weather{WindAvgDir} = &average_wind_dir($Weather{WindDir}); + $Weather{WindGustSpeed} = $Weather{WindSpeed} if $Weather{WindSpeed} > $Weather{WindGustSpeed}; + $Weather{WindAvgSpeed} = &average_wind_speed($Weather{WindSpeed}); + $Weather{WindChill} = &windchill($Weather{TempOutdoor}, $Weather{WindAvgSpeed}); + # + # Log variables + $Weather{SummaryWind} = sprintf("cur/avg %s(%3.1f)/%s(%3.1f) %3.1f%s/%3.1f%s",(&convert_wind_dir_to_abbr($Weather{WindDir})),$Weather{WindDir},(&convert_wind_dir_to_abbr($Weather{WindAvgDir})),$Weather{WindAvgDir},$Weather{WindSpeed},$main::config_parms{weather_uom_wind},$Weather{WindAvgSpeed},$main::config_parms{weather_uom_wind}); + $Weather{SummaryTemp} = sprintf("%3.1f%s",$Weather{TempOutdoor},$main::config_parms{weather_uom_temp}); + # + print_log "Wind $Weather{SummaryWind}, Temperature $Weather{SummaryTemp}"; + &Weather_Common::weather_updated; +} + +### Only subroutines below this point ### + +sub deg_to_rad { ($_[0]/180) * (4 * atan2(1,1)) } # convert degrees to radians + +sub rad_to_deg { ($_[0] / (4 * atan2(1,1))) * 180 } # convert radians to degrees + +sub average_wind_dir { + my $readings = 10; + my $sum_wind_sin; + my $sum_wind_cos; + push (@wind_sin, sin(°_to_rad($_[0]))); + push (@wind_cos, cos(°_to_rad($_[0]))); + if ((scalar(@wind_cos)) == ($readings + 1)){shift @wind_cos}; + if ((scalar(@wind_sin)) == ($readings + 1)){shift @wind_sin}; + foreach (@wind_sin) {$sum_wind_sin += $_}; + foreach (@wind_cos) {$sum_wind_cos += $_}; + my $avg_wind_dir = &rad_to_deg(atan2($sum_wind_sin,$sum_wind_cos)); + if($avg_wind_dir < 0){$avg_wind_dir += 359; }; + $avg_wind_dir = sprintf("%.0f",$avg_wind_dir); + return $avg_wind_dir; +} + +sub average_wind_speed { + my $readings = 10; + my $avg_wind_speed; + push (@wind_speed, $_[0]); + if ((scalar(@wind_speed)) == ($readings + 1)){shift @wind_speed}; + foreach (@wind_speed) {$avg_wind_speed += $_}; + $avg_wind_speed = $avg_wind_speed / scalar(@wind_speed); + return $avg_wind_speed; +} + +sub convert_wind_dir_to_abbr { + my ($dir)=@_; + return 'unknown' if $dir !~ /^[\d \.]+$/; + if ($dir >= 0 and $dir <= 360) { + return qw{North NNE NE ENE East ESE SE SSE South SSW SW WSW West WNW NW NNW North}[(($dir+11.25)/22.5)%16]; + } + return 'unknown'; +} + +sub windchill { + my $temp = shift; + my $wind = shift; + my $chill; + + if (($wind<5) || ($wind>100) || ($temp<-50) || ($temp>5)) { + $chill = ''; + } + else { + $chill=(13.12 + 0.6215*$temp - 11.37*($wind**0.16) + 0.3965*$temp*($wind**0.16)); + $chill=int($chill+0.5); + print "temp $temp wind $wind chill $chill\n"; + } + return $chill; +} + +sub iButton_wind_read { + # + #wind speed (mph) + # + my $wind_time1 = &get_tickcount; + my $count = $ib_wind_speed->Hardware::iButton::Device::DS2423::read_counter(); + # + if ($wind_time2) { + my $revolution_sec = (($count - $last_wind_count) * 1000) / ($wind_time1 - $wind_time2) / 2.0; + set $ib_wind_speed sprintf("%3.2f",$revolution_sec * 2.453); + } + # + $last_wind_count = $count; + $wind_time2 = $wind_time1; + $Save{WindGustMax} = 0 if $New_Day; + # + # wind direction (c) + # + my %position_lookup=(); + $position_lookup{'HHLH'}=1; + $position_lookup{'HMMH'}=2; + $position_lookup{'HLHH'}=3; + $position_lookup{'MMHH'}=4; + $position_lookup{'LHHH'}=5; + $position_lookup{'LHHZ'}=6; + $position_lookup{'HHHZ'}=7; + $position_lookup{'HHZZ'}=8; + $position_lookup{'HHZH'}=9; + $position_lookup{'HZZH'}=10; + $position_lookup{'HZHH'}=11; + $position_lookup{'ZZHH'}=12; + $position_lookup{'ZHHH'}=13; + $position_lookup{'ZHHL'}=14; + $position_lookup{'HHHL'}=15; + $position_lookup{'HHMM'}=16; + # + my $wind_adc_states = &read_ds2450($ib_wind_dir); + my $wind_position = $position_lookup{$wind_adc_states}; + $Weather{WindDir} = sprintf("%3.1f",($wind_position * 22.5)); + set $ib_wind_dir $Weather{WindDir}; +} + +sub read_iButton_temp { + my @ib_list = @_; + for my $ib (@ib_list) { + my $temp = $ib->read_temperature_hires(); + next if $temp < -20 or $temp > 120; + my $temp_c = sprintf("%3.2f", $temp); + my $temp_f = sprintf("%3.2f", $temp*9/5 +32); + my $serial = $ib->serial(); + + # Average the last 5 entries + if (defined @{$iButton_data_avg_data{$serial}}) { + unshift(@{$iButton_data_avg_data{$serial}}, $temp_c); + pop(@{$iButton_data_avg_data{$serial}}); + } + else { + @{$iButton_data_avg_data{$serial}} = ($temp_c) x 5; + } + + my $iButton_data_avg = 0; + grep($iButton_data_avg += $_, @{$iButton_data_avg_data{$serial}}); + $iButton_data_avg /= 5; + $ib->{state} = sprintf("%3.1f", $iButton_data_avg); + } +} + +sub read_ds2450 { + # Read ds2450 adc's and return a 4 character string representing their states + # H=high,M=medium,L=low,Z=zero + # + $ib_wind_dir->Hardware::iButton::Device::DS2450::convert('all'); + my ($A,$B,$C,$D) = $ib_wind_dir->Hardware::iButton::Device::DS2450::readAD('all'); + my $channel_A_state = &volts_to_state($A); + my $channel_B_state = &volts_to_state($B); + my $channel_C_state = &volts_to_state($C); + my $channel_D_state = &volts_to_state($D); + return $channel_A_state.$channel_B_state.$channel_C_state.$channel_D_state; +} + +sub volts_to_state { + # Calculate state from voltage + my $volts = $_[0]; + my $state = 'Z'; + if ($volts >= 2){$state = 'L'}; + if ($volts >= 3){$state = 'M'}; + if ($volts >= 4){$state = 'H'}; + return $state; +} + +sub setup_ds2450 { + my $ibutton = $_[0]; + my $VCC = 0; + my %A; + my %B; + my %C; + my %D; + my $setupds2450 = 0; + until ($setupds2450 == 1) { + # + $A{type} = "AD"; + $A{resolution} = 4; + $A{range} = 5.12; + $B{type} = "AD"; + $B{resolution} = 4; + $B{range} = 5.12; + $C{type} = "AD"; + $C{resolution} = 4; + $C{range} = 5.12; + $D{type} = "AD"; + $D{resolution} = 4; + $D{range} = 5.12; + # + if ($ib_wind_dir->Hardware::iButton::Device::DS2450::setup($VCC,\%A,\%B,\%C,\%D)){$setupds2450 = 1}; + if ($setupds2450 = 1) { + print_log "Initialised weather station DS2450"; + }else{ + print_log "Failed to initialise weather station DS2450, retrying..."; + sleep 1; + } + } +} + diff --git a/code/examples/network_items_web.pl b/code/examples/network_items_web.pl index 63280aada..495ff6621 100644 --- a/code/examples/network_items_web.pl +++ b/code/examples/network_items_web.pl @@ -43,23 +43,23 @@ sub web_networkitems { $html_hdr = &html_header ("Network Devices: $nd_up on and $nd_down off "); $html_hdr .= "\n"; - my $html = " - - - -"; - + my $html = " + + + +"; + $html .= $html_hdr . $html_data; $html .= ""; - - - my $html_page = &html_page('', $html); - return &html_page('', $html); + + + my $html_page = &html_page('', $html); + return &html_page('', $html); } diff --git a/code/examples/test_weather_item.pl b/code/examples/test_weather_item.pl index 760795580..f1db22fd3 100644 --- a/code/examples/test_weather_item.pl +++ b/code/examples/test_weather_item.pl @@ -1,13 +1,13 @@ - -$Weather{temp_test}++ if new_second 5; -$Weather{temp_test} = 0 if new_second 30; - -$w_test1 = new Weather_Item 'temp_test'; -$w_test2 = new Weather_Item 'temp_test > 4'; -$w_test3 = new Weather_Item 'temp_test == 0'; - -print "Weather change 1: $temp\n" if defined ($temp = state_now $w_test1); -print "Weather change 2: $Weather{test}\n" if state_now $w_test2; -print "Weather change 3: $Weather{test}\n" if state_now $w_test3; - - + +$Weather{temp_test}++ if new_second 5; +$Weather{temp_test} = 0 if new_second 30; + +$w_test1 = new Weather_Item 'temp_test'; +$w_test2 = new Weather_Item 'temp_test > 4'; +$w_test3 = new Weather_Item 'temp_test == 0'; + +print "Weather change 1: $temp\n" if defined ($temp = state_now $w_test1); +print "Weather change 2: $Weather{test}\n" if state_now $w_test2; +print "Weather change 3: $Weather{test}\n" if state_now $w_test3; + + diff --git a/code/public/Brian/flagreminders.pl b/code/public/Brian/flagreminders.pl index 4a867f02b..6600c6515 100644 --- a/code/public/Brian/flagreminders.pl +++ b/code/public/Brian/flagreminders.pl @@ -1,77 +1,77 @@ -########################## -# Klier Home Automation # -########################## - -####################>>> Timed Events -# TIME_CRON EVENTS - 1st digit = Minute(s) separated by commas -# 2nd digit = Hour(s) separated by commas -# 3rd digit = Day(s) separated by commas -# 4th digit = Month(s) separated by commas -# 5th digit = Day of week(s) 0=Sun 1=Mon 2=Tue, etc. -# * = Ignore this field - -if (time_cron('20 8 1 1 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 17 1 1 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 12 2 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 17 12 2 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 22 2 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 17 22 2 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 15 5 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 15 5 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 15 5 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 15 5 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 14 6 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 14 6 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 4 7 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 4 7 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 27 7 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 27 7 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 11 9 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 11 9 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 17 9 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 20 17 9 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 27 10 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 18 27 10 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 11 11 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 17 11 11 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 6 7 12 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 17 7 12 *')) {speak "Notice: Please take the flag in."}; -if (time_cron('20 8 25 12 *')) {speak "Notice: Please put the flag out."}; -if (time_cron('20 17 25 12 *')) {speak "Notice: Please take the flag in."}; - -# Week 1 > 0 < 7 -# Week 2 > 7 < 14 -# Week 3 > 14 < 21 -# Week 4 > 21 < 32 - -# First Week of the month Events -if ($Day > 0 and $Day < 7) { - if (time_cron('20 6 * 9 1')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 20 * 9 1')) {speak "Notice: Please take the flag in."}; - if (time_cron('20 6 * 11 2')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 17 * 11 2')) {speak "Notice: Please take the flag in."}; -} - -# 2nd Week of the month Events -if ($Day > 7 and $Day < 14) { - if (time_cron('20 6 * 5 0')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 20 * 5 0')) {speak "Notice: Please take the flag in."}; - if (time_cron('20 6 * 10 1')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 17 * 10 1')) {speak "Notice: Please take the flag in."}; -} - -# 3rd Week of the month Events -if ($Day > 14 and $Day < 21) { - if (time_cron('20 6 * 4 1')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 20 * 4 1')) {speak "Notice: Please take the flag in."}; - if (time_cron('20 6 * 5 6')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 20 * 5 6')) {speak "Notice: Please take the flag in."}; - if (time_cron('20 6 * 6 0')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 20 * 6 0')) {speak "Notice: Please take the flag in."}; -} - -# 4th Week of the month Events -if ($Day > 21 and $Day < 28) { - if (time_cron('20 6 * 11 4')) {speak "Notice: Please put the flag out."}; - if (time_cron('20 17 * 11 4')) {speak "Notice: Please take the flag in."}; +########################## +# Klier Home Automation # +########################## + +####################>>> Timed Events +# TIME_CRON EVENTS - 1st digit = Minute(s) separated by commas +# 2nd digit = Hour(s) separated by commas +# 3rd digit = Day(s) separated by commas +# 4th digit = Month(s) separated by commas +# 5th digit = Day of week(s) 0=Sun 1=Mon 2=Tue, etc. +# * = Ignore this field + +if (time_cron('20 8 1 1 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 17 1 1 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 12 2 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 17 12 2 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 22 2 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 17 22 2 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 15 5 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 15 5 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 15 5 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 15 5 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 14 6 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 14 6 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 4 7 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 4 7 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 27 7 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 27 7 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 11 9 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 11 9 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 17 9 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 20 17 9 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 27 10 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 18 27 10 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 11 11 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 17 11 11 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 6 7 12 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 17 7 12 *')) {speak "Notice: Please take the flag in."}; +if (time_cron('20 8 25 12 *')) {speak "Notice: Please put the flag out."}; +if (time_cron('20 17 25 12 *')) {speak "Notice: Please take the flag in."}; + +# Week 1 > 0 < 7 +# Week 2 > 7 < 14 +# Week 3 > 14 < 21 +# Week 4 > 21 < 32 + +# First Week of the month Events +if ($Day > 0 and $Day < 7) { + if (time_cron('20 6 * 9 1')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 20 * 9 1')) {speak "Notice: Please take the flag in."}; + if (time_cron('20 6 * 11 2')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 17 * 11 2')) {speak "Notice: Please take the flag in."}; +} + +# 2nd Week of the month Events +if ($Day > 7 and $Day < 14) { + if (time_cron('20 6 * 5 0')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 20 * 5 0')) {speak "Notice: Please take the flag in."}; + if (time_cron('20 6 * 10 1')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 17 * 10 1')) {speak "Notice: Please take the flag in."}; +} + +# 3rd Week of the month Events +if ($Day > 14 and $Day < 21) { + if (time_cron('20 6 * 4 1')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 20 * 4 1')) {speak "Notice: Please take the flag in."}; + if (time_cron('20 6 * 5 6')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 20 * 5 6')) {speak "Notice: Please take the flag in."}; + if (time_cron('20 6 * 6 0')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 20 * 6 0')) {speak "Notice: Please take the flag in."}; +} + +# 4th Week of the month Events +if ($Day > 21 and $Day < 28) { + if (time_cron('20 6 * 11 4')) {speak "Notice: Please put the flag out."}; + if (time_cron('20 17 * 11 4')) {speak "Notice: Please take the flag in."}; } \ No newline at end of file diff --git a/code/public/Brian/grlevel3.lst b/code/public/Brian/grlevel3.lst index 155cfd93e..3400387a1 100644 --- a/code/public/Brian/grlevel3.lst +++ b/code/public/Brian/grlevel3.lst @@ -1,6 +1,6 @@ -n0qvc-9 -wa0ssn-9 -n0pcd-9 -kc0ouz-9 -kc0uuv-9 -n0pqk-5 +n0qvc-9 +wa0ssn-9 +n0pcd-9 +kc0ouz-9 +kc0uuv-9 +n0pqk-5 diff --git a/code/public/Brian/grlevel3.pl b/code/public/Brian/grlevel3.pl index a041395c5..6a6ea6b8c 100644 --- a/code/public/Brian/grlevel3.pl +++ b/code/public/Brian/grlevel3.pl @@ -1,124 +1,124 @@ -#################################################### -# Dynamic Placefile generation from Findu # -# For Selected Stations # -# Version 1.0 # -# By: Brian Klier, N0QVC # -# brian@kliernetwork.net, http://kliernetwork.net # -# You are free to modify and redistribute this # -# script as long as this box remains on the top of # -# the code. Feel free to add your own name as # -# well. # -#################################################### - -# Category = Vehicles - -my ($ItsRunning, @callretlines, $Temp, $get_url_string, @QVCArray, $StripInfo, $QVCArrayBit); -my ($ValidReport, $TimeStamp, $GRLatitude, $GRLongitude, $GRDirection, $GRSpeed, $GRTest, $FinalOutput); -my ($ShortCallsign, $ShortCallsignLength, $OtherStuff); - -$p_get_track_data = new Process_Item; -$v_create_placefile = new Voice_Cmd('Create GRLEVEL3 Place File'); - -if (time_cron('3,8,13,18,23,28,33,38,43,48,53,58 * * * *')) {run_voice_cmd "Create GRLEVEL3 Place File"}; -if ($ItsRunning eq '' and said $v_create_placefile) { - $ItsRunning = "1"; - $p_get_track_data = new Process_Item; - open(APRSLOG, ">c:/mh/data/web/grlevel3.txt"); # Log it - print APRSLOG "Refresh: 1\n"; - print APRSLOG "Color: 255 255 255\n"; - print APRSLOG 'IconFile: 1, 16, 16, 8, 8, "http://kliers.net/skywarn/APRS.png"'; - print APRSLOG "\n"; - print APRSLOG 'Font: 1, 11, 1, "Courier New"'; - print APRSLOG "\n"; - #remmed out 2/19/06 - #print APRSLOG 'Font: 2, 8, 1, "Arial"'; - print APRSLOG 'Font: 2, 8, 0, "Arial"'; - print APRSLOG "\n"; - close APRSLOG; - - # Load List of callsigns to retrieve - open(CALLRET, "$config_parms{code_dir}/grlevel3.lst"); - @callretlines = ; - close CALLRET; - - foreach $Temp (@callretlines) { - chomp $Temp; # get rid of CR/LF - $get_url_string = "get_url http://www.findu.com/cgi-bin/posit.cgi?call="; - $get_url_string .= $Temp; - $get_url_string .= "\&time=1\&comma=1 c:/mh/data/web/"; - $get_url_string .= $Temp; - $get_url_string .= ".txt"; - - add $p_get_track_data $get_url_string; - start $p_get_track_data; - } -} - -if (done_now $p_get_track_data) { - - foreach $Temp (@callretlines) { - $ValidReport = 1; # assume valid report - chomp $Temp; # get rid of CR/LF - $get_url_string = "c:/mh/data/web/"; - $get_url_string .= $Temp; - $get_url_string .= ".txt"; - - $StripInfo = file_read($get_url_string); - my $text = &html_to_text($StripInfo); - $text =~ s/\240/ /g; - if ($text =~ /osition/) { - $ValidReport = 0; - } - file_write($get_url_string, $text); - - $StripInfo = file_read($get_url_string); - open(QVCINFO, $get_url_string); - @QVCArray = ; - close QVCINFO; - - foreach $QVCArrayBit (@QVCArray) { - ($TimeStamp, $GRLatitude, $GRLongitude, $GRDirection, $GRSpeed) = (split(',', $QVCArrayBit))[0, 1, 2, 3, 4]; - } - - if ($ValidReport eq '1') { - ($ShortCallsign, $OtherStuff) = (split('-', $Temp))[0, 1]; - $ShortCallsignLength = (length($ShortCallsign)); - $ShortCallsign = substr($ShortCallsign, ($ShortCallsignLength-3), 3); - - open(APRSLOG, ">>c:/mh/data/web/grlevel3.txt"); - print APRSLOG "Object: "; - print APRSLOG $GRLatitude; - print APRSLOG ","; - print APRSLOG $GRLongitude; - print APRSLOG "\n"; - print APRSLOG "Threshold: 999"; - print APRSLOG "\n"; - print APRSLOG "Icon: 0,0,0,1,181,"; - print APRSLOG '"'; - print APRSLOG uc($Temp); - print APRSLOG ": Heading "; - print APRSLOG $GRDirection; - print APRSLOG " at $GRSpeed"; - print APRSLOG '"'; - print APRSLOG "\n"; - print APRSLOG "Threshold: 150"; - print APRSLOG "\n"; - print APRSLOG "Text: 6, 6, 2, "; - print APRSLOG '"'; - #print APRSLOG uc($Temp); - print APRSLOG uc($ShortCallsign); - print APRSLOG '"'; - print APRSLOG "\n"; - print APRSLOG "End:"; - print APRSLOG "\n"; - close APRSLOG; - } - - } - -net_ftp( - file => 'c:/mh/data/web/grlevel3.txt', file_remote => '/public_html/skywarn/grlevel3.txt', - passive => '1', command => 'put', server => 'kliers.net', - user => 'myusername', password => 'mypassword'); -$ItsRunning = ''; -} +#################################################### +# Dynamic Placefile generation from Findu # +# For Selected Stations # +# Version 1.0 # +# By: Brian Klier, N0QVC # +# brian@kliernetwork.net, http://kliernetwork.net # +# You are free to modify and redistribute this # +# script as long as this box remains on the top of # +# the code. Feel free to add your own name as # +# well. # +#################################################### + +# Category = Vehicles + +my ($ItsRunning, @callretlines, $Temp, $get_url_string, @QVCArray, $StripInfo, $QVCArrayBit); +my ($ValidReport, $TimeStamp, $GRLatitude, $GRLongitude, $GRDirection, $GRSpeed, $GRTest, $FinalOutput); +my ($ShortCallsign, $ShortCallsignLength, $OtherStuff); + +$p_get_track_data = new Process_Item; +$v_create_placefile = new Voice_Cmd('Create GRLEVEL3 Place File'); + +if (time_cron('3,8,13,18,23,28,33,38,43,48,53,58 * * * *')) {run_voice_cmd "Create GRLEVEL3 Place File"}; +if ($ItsRunning eq '' and said $v_create_placefile) { + $ItsRunning = "1"; + $p_get_track_data = new Process_Item; + open(APRSLOG, ">c:/mh/data/web/grlevel3.txt"); # Log it + print APRSLOG "Refresh: 1\n"; + print APRSLOG "Color: 255 255 255\n"; + print APRSLOG 'IconFile: 1, 16, 16, 8, 8, "http://kliers.net/skywarn/APRS.png"'; + print APRSLOG "\n"; + print APRSLOG 'Font: 1, 11, 1, "Courier New"'; + print APRSLOG "\n"; + #remmed out 2/19/06 + #print APRSLOG 'Font: 2, 8, 1, "Arial"'; + print APRSLOG 'Font: 2, 8, 0, "Arial"'; + print APRSLOG "\n"; + close APRSLOG; + + # Load List of callsigns to retrieve + open(CALLRET, "$config_parms{code_dir}/grlevel3.lst"); + @callretlines = ; + close CALLRET; + + foreach $Temp (@callretlines) { + chomp $Temp; # get rid of CR/LF + $get_url_string = "get_url http://www.findu.com/cgi-bin/posit.cgi?call="; + $get_url_string .= $Temp; + $get_url_string .= "\&time=1\&comma=1 c:/mh/data/web/"; + $get_url_string .= $Temp; + $get_url_string .= ".txt"; + + add $p_get_track_data $get_url_string; + start $p_get_track_data; + } +} + +if (done_now $p_get_track_data) { + + foreach $Temp (@callretlines) { + $ValidReport = 1; # assume valid report + chomp $Temp; # get rid of CR/LF + $get_url_string = "c:/mh/data/web/"; + $get_url_string .= $Temp; + $get_url_string .= ".txt"; + + $StripInfo = file_read($get_url_string); + my $text = &html_to_text($StripInfo); + $text =~ s/\240/ /g; + if ($text =~ /osition/) { + $ValidReport = 0; + } + file_write($get_url_string, $text); + + $StripInfo = file_read($get_url_string); + open(QVCINFO, $get_url_string); + @QVCArray = ; + close QVCINFO; + + foreach $QVCArrayBit (@QVCArray) { + ($TimeStamp, $GRLatitude, $GRLongitude, $GRDirection, $GRSpeed) = (split(',', $QVCArrayBit))[0, 1, 2, 3, 4]; + } + + if ($ValidReport eq '1') { + ($ShortCallsign, $OtherStuff) = (split('-', $Temp))[0, 1]; + $ShortCallsignLength = (length($ShortCallsign)); + $ShortCallsign = substr($ShortCallsign, ($ShortCallsignLength-3), 3); + + open(APRSLOG, ">>c:/mh/data/web/grlevel3.txt"); + print APRSLOG "Object: "; + print APRSLOG $GRLatitude; + print APRSLOG ","; + print APRSLOG $GRLongitude; + print APRSLOG "\n"; + print APRSLOG "Threshold: 999"; + print APRSLOG "\n"; + print APRSLOG "Icon: 0,0,0,1,181,"; + print APRSLOG '"'; + print APRSLOG uc($Temp); + print APRSLOG ": Heading "; + print APRSLOG $GRDirection; + print APRSLOG " at $GRSpeed"; + print APRSLOG '"'; + print APRSLOG "\n"; + print APRSLOG "Threshold: 150"; + print APRSLOG "\n"; + print APRSLOG "Text: 6, 6, 2, "; + print APRSLOG '"'; + #print APRSLOG uc($Temp); + print APRSLOG uc($ShortCallsign); + print APRSLOG '"'; + print APRSLOG "\n"; + print APRSLOG "End:"; + print APRSLOG "\n"; + close APRSLOG; + } + + } + +net_ftp( + file => 'c:/mh/data/web/grlevel3.txt', file_remote => '/public_html/skywarn/grlevel3.txt', + passive => '1', command => 'put', server => 'kliers.net', + user => 'myusername', password => 'mypassword'); +$ItsRunning = ''; +} diff --git a/code/public/Brian/klier.mht b/code/public/Brian/klier.mht index 7d7ca063d..4c582e07c 100644 --- a/code/public/Brian/klier.mht +++ b/code/public/Brian/klier.mht @@ -1,124 +1,124 @@ -Format = A -# -# See mh/lib/read_table_A.pl for definition of Format=A items -# -# -# Type Address Name Groups Other Info -# -GROUP, Garage, Property(0;0;20;20) -GROUP, Living_Room, Property(20;0;30;20) -GROUP, Bedroom, Property(50;0;20;20) -GROUP, Backyard, Property(0;20;70;15) -GROUP, Computer_Room, Property(0;40;70;15) -GROUP, Kitchen, Property(0;60;70;15) - -###OCCUPANCY, om, - -VOICE, All_Lights, All Lights [on,off] - -##### A house code - North Motion Detectors (+ setback) -X10A, A1, unused_xcvr, Appliances -VOICE, unused_xcvr, Unused Transceiver [on,off] -X10MS, A2, motion_detector_backdoor, Sensors|Kitchen, MS13 -MOTION, motion_detector_backdoor, motion_backdoor, Kitchen -X10MS, A5, motion_detector_kitchen, Sensors|Kitchen, MS13 -MOTION, motion_detector_kitchen, motion_kitchen, Kitchen -#SERIAL, XA6BJ, low_light_kitchen, ,on -#SERIAL, XA6BK, low_light_kitchen, ,off -PHOTOCELL, motion_detector_kitchen, low_light_kitchen, Sensors|Kitchen -# A11 -X10MS, AB, motion_detector_trailer, Sensors|Backyard, MS13 -MOTION, motion_detector_trailer, motion_trailer, Backyard -# A16 -X10A, AG, thermostat_setback, Appliances|HVAC|Living_Room(5;12) -VOICE, thermostat_setback, Thermostat Setback [on,off] - -##### B house code - Lighting and HVAC Control -X10I, B1, living_room_light, All_Lights|ambient_lights|Living_Room -VOICE, living_room_light, Living Room Light [on,off] -X10I, B2, bedroom_light, All_Lights|ambient_lights|Bedroom -VOICE, bedroom_light, Bedroom Light [on,off] -X10I, B3, computer_room_light, All_Lights|ambient_lights|Computer_Room -VOICE, computer_room_light, Computer Room Light [on,off] -SERIAL, XB4BJ, request_time_stuff, HIDDEN,on -SERIAL, XB4BK, request_time_stuff, HIDDEN,off -X10A, B5, boombox_bedroom, HIDDEN|Appliances|Bedroom -VOICE, boombox_bedroom, Boombox [on,off] -SERIAL, XB6BJ, morning_alarm_buttons, HIDDEN,on -SERIAL, XB6BK, morning_alarm_buttons, HIDDEN,off -SERIAL, XB7BJ, request_music_stuff, HIDDEN, on -SERIAL, XB7BK, request_music_stuff, HIDDEN, off -SERIAL, XB8BJ, request_wx_stuff, HIDDEN,on -SERIAL, XB8BK, request_wx_stuff, HIDDEN,off -X10A, B9, circ_fan, Appliances|Bedroom -VOICE, circ_fan, Circulation Fan [on,off] -# B10 -X10I, BA, kitchen_light, All_Lights|Kitchen -VOICE, kitchen_light, Kitchen Light [on,off] -# B11 -X10A, BB, bed_heater, Appliances|HVAC|Bedroom -VOICE, bed_heater, Bed Heater [on,off] -# B12 -SERIAL, XBCBJ, whats_on_tv, HIDDEN,on -SERIAL, XBCBK, whats_on_tv, HIDDEN,off -# B13 -X10A, BD, projector, HIDDEN|Appliances|Living_Room -VOICE, projector, Projector [on,off] -# B14 -X10A, BE, air_cond_fan, Appliances|HVAC|Living_Room -VOICE, air_cond_fan, Air Conditioner Fan [on,off] -# B15 -X10I, BF, back_porch_light, All_Lights|Backyard -VOICE, back_porch_light, Back Porch Light [on,off] -# B16 -SERIAL, XBGBJ, come_home_stuff, HIDDEN, on -SERIAL, XBGBK, come_home_stuff, HIDDEN, off - -##### C house code - Garage -X10I, C1, garage_light, All_Lights|Garage -VOICE, garage_light, Garage Light [on,off] -X10MS, C2, motion_detector_garage, Sensors|Garage, MS13 -MOTION, motion_detector_garage, motion_garage, Garage -# C3 -#SERIAL, XC3BJ, low_light_garage, ,on -#SERIAL, XC3BK, low_light_garage, ,off -PHOTOCELL, motion_detector_garage, low_light_garage, Sensors|Garage - -##### D house code - Security Cameras -X10A, D1, security_camera_backdoor, HIDDEN|Security|security_cameras -X10A, D2, security_camera_garage, HIDDEN|Security|security_cameras -X10A, D3, security_camera_frontdoor, HIDDEN|Security|security_cameras -X10A, D4, security_camera_driveway, HIDDEN|Security|security_cameras - -##### E house code - Christmas Lights -X10I, E1, christmas_lights, Christmas|Backyard -VOICE, christmas_lights, Christmas Lights [on,off] -X10I, E2, christmas_tree, Christmas|Living_Room -VOICE, christmas_tree, Christmas Tree [on,off] - -##### F house code - South Motion Detectors -X10MS, F2, motion_detector_living_room, Sensors|Living_Room, MS13 -MOTION, motion_detector_living_room, motion_living_room, Living_Room -#SERIAL, XF3BJ, low_light_living_room, ,on -#SERIAL, XF3BK, low_light_living_room, ,off -PHOTOCELL, motion_detector_living_room, low_light_living_room, Sensors|Living_Room -X10MS, F4, motion_detector_frontdoor, Sensors|Computer_Room, MS13 -MOTION, motion_detector_frontdoor, motion_frontdoor, Computer_Room -#SERIAL, XF5BJ, low_light_frontdoor, ,on -#SERIAL, XF5BK, low_light_frontdoor, ,off -PHOTOCELL, motion_detector_frontdoor, low_light_frontdoor, Sensors|Computer_Room - -##### O house code - Alarm System -SERIAL, XO1BJ, alarm_lights, HIDDEN,on -SERIAL, XO1BK, alarm_lights, HIDDEN,off -SERIAL, XO2BJ, alarm_detected, HIDDEN,on -SERIAL, XO2BK, alarm_detected, HIDDEN,off -X10A, O5, cctv_record_alarm, HIDDEN|Security - -#LIGHT, kitchen_light, x10_kitchen_light, Kitchen -#MOTION, motion_detector_kitchen, x10_motion_detector_kitchen, Kitchen -#PRESENCE, x10_motion_detector_kitchen, om, presence_kitchen - -#LIGHT, back_porch_light, x10_back_porch_light, Backyard -#MOTION, motion_detector_backdoor, x10_motion_detector_backdoor, Backyard -#PRESENCE, x10_motion_detector_backdoor, om, presence_backdoor +Format = A +# +# See mh/lib/read_table_A.pl for definition of Format=A items +# +# +# Type Address Name Groups Other Info +# +GROUP, Garage, Property(0;0;20;20) +GROUP, Living_Room, Property(20;0;30;20) +GROUP, Bedroom, Property(50;0;20;20) +GROUP, Backyard, Property(0;20;70;15) +GROUP, Computer_Room, Property(0;40;70;15) +GROUP, Kitchen, Property(0;60;70;15) + +###OCCUPANCY, om, + +VOICE, All_Lights, All Lights [on,off] + +##### A house code - North Motion Detectors (+ setback) +X10A, A1, unused_xcvr, Appliances +VOICE, unused_xcvr, Unused Transceiver [on,off] +X10MS, A2, motion_detector_backdoor, Sensors|Kitchen, MS13 +MOTION, motion_detector_backdoor, motion_backdoor, Kitchen +X10MS, A5, motion_detector_kitchen, Sensors|Kitchen, MS13 +MOTION, motion_detector_kitchen, motion_kitchen, Kitchen +#SERIAL, XA6BJ, low_light_kitchen, ,on +#SERIAL, XA6BK, low_light_kitchen, ,off +PHOTOCELL, motion_detector_kitchen, low_light_kitchen, Sensors|Kitchen +# A11 +X10MS, AB, motion_detector_trailer, Sensors|Backyard, MS13 +MOTION, motion_detector_trailer, motion_trailer, Backyard +# A16 +X10A, AG, thermostat_setback, Appliances|HVAC|Living_Room(5;12) +VOICE, thermostat_setback, Thermostat Setback [on,off] + +##### B house code - Lighting and HVAC Control +X10I, B1, living_room_light, All_Lights|ambient_lights|Living_Room +VOICE, living_room_light, Living Room Light [on,off] +X10I, B2, bedroom_light, All_Lights|ambient_lights|Bedroom +VOICE, bedroom_light, Bedroom Light [on,off] +X10I, B3, computer_room_light, All_Lights|ambient_lights|Computer_Room +VOICE, computer_room_light, Computer Room Light [on,off] +SERIAL, XB4BJ, request_time_stuff, HIDDEN,on +SERIAL, XB4BK, request_time_stuff, HIDDEN,off +X10A, B5, boombox_bedroom, HIDDEN|Appliances|Bedroom +VOICE, boombox_bedroom, Boombox [on,off] +SERIAL, XB6BJ, morning_alarm_buttons, HIDDEN,on +SERIAL, XB6BK, morning_alarm_buttons, HIDDEN,off +SERIAL, XB7BJ, request_music_stuff, HIDDEN, on +SERIAL, XB7BK, request_music_stuff, HIDDEN, off +SERIAL, XB8BJ, request_wx_stuff, HIDDEN,on +SERIAL, XB8BK, request_wx_stuff, HIDDEN,off +X10A, B9, circ_fan, Appliances|Bedroom +VOICE, circ_fan, Circulation Fan [on,off] +# B10 +X10I, BA, kitchen_light, All_Lights|Kitchen +VOICE, kitchen_light, Kitchen Light [on,off] +# B11 +X10A, BB, bed_heater, Appliances|HVAC|Bedroom +VOICE, bed_heater, Bed Heater [on,off] +# B12 +SERIAL, XBCBJ, whats_on_tv, HIDDEN,on +SERIAL, XBCBK, whats_on_tv, HIDDEN,off +# B13 +X10A, BD, projector, HIDDEN|Appliances|Living_Room +VOICE, projector, Projector [on,off] +# B14 +X10A, BE, air_cond_fan, Appliances|HVAC|Living_Room +VOICE, air_cond_fan, Air Conditioner Fan [on,off] +# B15 +X10I, BF, back_porch_light, All_Lights|Backyard +VOICE, back_porch_light, Back Porch Light [on,off] +# B16 +SERIAL, XBGBJ, come_home_stuff, HIDDEN, on +SERIAL, XBGBK, come_home_stuff, HIDDEN, off + +##### C house code - Garage +X10I, C1, garage_light, All_Lights|Garage +VOICE, garage_light, Garage Light [on,off] +X10MS, C2, motion_detector_garage, Sensors|Garage, MS13 +MOTION, motion_detector_garage, motion_garage, Garage +# C3 +#SERIAL, XC3BJ, low_light_garage, ,on +#SERIAL, XC3BK, low_light_garage, ,off +PHOTOCELL, motion_detector_garage, low_light_garage, Sensors|Garage + +##### D house code - Security Cameras +X10A, D1, security_camera_backdoor, HIDDEN|Security|security_cameras +X10A, D2, security_camera_garage, HIDDEN|Security|security_cameras +X10A, D3, security_camera_frontdoor, HIDDEN|Security|security_cameras +X10A, D4, security_camera_driveway, HIDDEN|Security|security_cameras + +##### E house code - Christmas Lights +X10I, E1, christmas_lights, Christmas|Backyard +VOICE, christmas_lights, Christmas Lights [on,off] +X10I, E2, christmas_tree, Christmas|Living_Room +VOICE, christmas_tree, Christmas Tree [on,off] + +##### F house code - South Motion Detectors +X10MS, F2, motion_detector_living_room, Sensors|Living_Room, MS13 +MOTION, motion_detector_living_room, motion_living_room, Living_Room +#SERIAL, XF3BJ, low_light_living_room, ,on +#SERIAL, XF3BK, low_light_living_room, ,off +PHOTOCELL, motion_detector_living_room, low_light_living_room, Sensors|Living_Room +X10MS, F4, motion_detector_frontdoor, Sensors|Computer_Room, MS13 +MOTION, motion_detector_frontdoor, motion_frontdoor, Computer_Room +#SERIAL, XF5BJ, low_light_frontdoor, ,on +#SERIAL, XF5BK, low_light_frontdoor, ,off +PHOTOCELL, motion_detector_frontdoor, low_light_frontdoor, Sensors|Computer_Room + +##### O house code - Alarm System +SERIAL, XO1BJ, alarm_lights, HIDDEN,on +SERIAL, XO1BK, alarm_lights, HIDDEN,off +SERIAL, XO2BJ, alarm_detected, HIDDEN,on +SERIAL, XO2BK, alarm_detected, HIDDEN,off +X10A, O5, cctv_record_alarm, HIDDEN|Security + +#LIGHT, kitchen_light, x10_kitchen_light, Kitchen +#MOTION, motion_detector_kitchen, x10_motion_detector_kitchen, Kitchen +#PRESENCE, x10_motion_detector_kitchen, om, presence_kitchen + +#LIGHT, back_porch_light, x10_back_porch_light, Backyard +#MOTION, motion_detector_backdoor, x10_motion_detector_backdoor, Backyard +#PRESENCE, x10_motion_detector_backdoor, om, presence_backdoor diff --git a/code/public/Brian/old stuff i don't use anymore/calllog.pl b/code/public/Brian/old stuff i don't use anymore/calllog.pl index 4960a863d..1264e3d62 100644 --- a/code/public/Brian/old stuff i don't use anymore/calllog.pl +++ b/code/public/Brian/old stuff i don't use anymore/calllog.pl @@ -1,258 +1,258 @@ -############################################################ -# Klier Home Automation - Caller ID Module for Rockwell # -# Version 2.2 Release # -# By: Brian J. Klier, N0QVC # -# Thanks for the mucho help from: Bruce Winter # -# E-Mail: klier@lakes.com # -# Webpage: http://www.faribault.k12.mn.us/brian # -############################################################ - -# Category=Phone - -# New in Version 2.2: -# - Added ability to recite phone numbers of callers with no name given. - -# Modem Caller ID Information looks like the following -# DATE = 990305 -# TIME = 1351 -# NMBR = 5073336399 -# NAME = KLIER BRIAN J - -# Declare Variables - -use vars qw($PhoneName $PhoneNumber $PhoneTime $PhoneDate); - -my ($PhoneModemString, $NameDone, $NumberDone, $i, $j); -my (@rejloglines, $NumofCalls); -my (@callloglines, $CallLogTempLine, $RejLogTempLine); -my ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog); -my ($last, $first, $middle, $areacode, $local_number, $caller); - -$phone_modem = new Serial_Item ('AT#CID=1','init','serial2'); -$timer_hangup = new Timer; - -#-----> Provide PalmPad Controller Access for some Items -$request_phone_stuff = new X10_Item('B6'); - -# Web Interface Commands - -$v_phone_lastcaller = new Voice_Cmd('Show Recent Call Log'); -if ((said $v_phone_lastcaller) || (state_now $request_phone_stuff eq 'on')) { - open(CALLLOG, "$config_parms{code_dir}/calllog.log"); # Open for input - @callloglines = ; # Open array and - # read in data - close CALLLOG; # Close the file - - print_log "Announced Recent Callers."; - - $NumofCalls = 0; - - foreach $CallLogTempLine (@callloglines) { - $NumofCalls = $NumofCalls + 1; - ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog) = (split('`', $CallLogTempLine))[0, 1, 2, 3]; - if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog ne $Date_Now - and $PhoneNumberLog eq '') { - speak "At $PhoneTimeLog on $PhoneDateLog, an unidentified party called."; - } - if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog eq $Date_Now - and $PhoneNumberLog eq '') { - speak "At $PhoneTimeLog, an unidentified party called."; - } - if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog ne $Date_Now - and $PhoneNumberLog ne '') { - speak "At $PhoneTimeLog on $PhoneDateLog, an unidentified party called. Call back at $PhoneNumberLog."; - } - if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog eq $Date_Now - and $PhoneNumberLog ne '') { - speak "At $PhoneTimeLog, an unidentified party called. Call back at $PhoneNumberLog."; - } - if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog ne $Date_Now) { - speak "At $PhoneTimeLog on $PhoneDateLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; - } - if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog eq $Date_Now) { - speak "At $PhoneTimeLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; - } - } - speak "$NumofCalls total calls."; -} - -$v_phone_clearlog = new Voice_Cmd('Clear Recent Call Log'); -if ((said $v_phone_clearlog) || (state_now $request_phone_stuff eq 'off')) { - open(CALLLOG, ">$config_parms{code_dir}/calllog.log"); # CLEAR Log - close CALLLOG; - print_log "Call Log Cleared."; - speak "Call Log Cleared."; -} - -#$v_phone_log = new Voice_Cmd('Run Display Callers'); -#if (said $v_phone_log) { -# print_log "Running Display Callers..."; -# undef @ARGV; -# do "$Pgm_Path/display_callers"; -# print_log "Done."; -#} - -# Set MODEM Init Strings on Startup - -if ($Startup or $Reload) { - set $phone_modem 'init'; # Initialize MODEM - - open(REJLOG, "$config_parms{code_dir}/rejlog.log"); # Open for input - @rejloglines = ; # Open array and - # read in data - close REJLOG; # Close the file - - print_msg "Caller ID Interface has been Initialized..."; - print_log "Caller ID Interface has been Initialized..."; -} - -# Timer Information for Phone Hangup (Reject List) - -if (expired $timer_hangup) { - set $phone_modem 'ATH'; - set $timer_hangup 0; -} - -# Display Incoming Serial Data - -if ($PhoneModemString = said $phone_modem) { - print_msg "PHONE: $PhoneModemString"; - - if (substr($PhoneModemString, 0, 4) eq 'RING') { - #run_voice_cmd "Stop Music"; - #set $boombox_bedroom 'off'; - } - - if (substr($PhoneModemString, 0, 4) eq 'NAME') { - $NameDone = "yes"; - $PhoneName = (substr($PhoneModemString, 7, 15)); - - # Switch name strings so first last, not last first. - # Use only the 1st two blank delimited fields, as the 3rd, 4th are usually just initials or incomplete - # Last First M - # Last M First - - ($last, $first, $middle) = (split(' ', $PhoneName))[0, 1, 2]; - $first = ucfirst(lc($first)); - $first = ucfirst(lc($middle)) if length($first) == 1; # Last M First format - $last = ucfirst(lc($last)); - $caller = "$first $last"; - } - - if (substr($PhoneModemString, 0, 4) eq 'NMBR') { - $NumberDone = "yes"; - $PhoneNumber = (substr($PhoneModemString, 7, 10)); - $areacode = (substr($PhoneNumber, 0, 3)); - $local_number = (substr($PhoneNumber, 3, 7)); - - if ($PhoneNumber eq "O") { - $PhoneNumber = ""; - $areacode = ""; - $local_number = ""; - } - } - - if (substr($PhoneModemString, 0, 13) eq 'MESG = 08014F') { - $NameDone = "yes"; - $NumberDone = "yes"; - $PhoneName = "Out of the Area"; - $caller = "Out of the Area"; - } - - if ($NumberDone eq "yes" and $NameDone eq "yes") { - $NumberDone = 0; - $NameDone = 0; - $PhoneDate = $Date_Now; - $PhoneTime = $Time_Now; - - # Log the data for use by display_callers - - logit("$Pgm_Path/../data/phone/logs/callerid.$Year_Month_Now.log", "$PhoneNumber $PhoneName"); - logit_dbm("$Pgm_Path/../data/phone/callerid.dbm", $PhoneNumber, "$Time_Now $Date_Now $Year name=$PhoneName"); - - # Check to see if callers phone number is in reject table. If so, - # let them have it. - - foreach $RejLogTempLine (@rejloglines) { - if (substr($RejLogTempLine, 0, 10) eq $PhoneNumber) { - print_log "$PhoneName is calling, and is in reject list!"; - speak "$PhoneName is calling, and is in reject list!"; - set $phone_modem 'ATA'; - set $timer_hangup 5; - } - } - - # If the incoming area code is the same, drop it from being spoken. - - if ($areacode eq $config_parms{local_area_code}) { - $PhoneNumber = $local_number; - } - - # Put pauses in between area code, exchange, and number for - # announce reasons - - if ((length($PhoneNumber) == 7)) {$PhoneNumber = substr($PhoneNumber, 0, 3) . "." . substr($PhoneNumber, 3, 4)}; - if ((length($PhoneNumber) == 10)) {$PhoneNumber = substr($PhoneNumber, 0, 3) . "." . substr($PhoneNumber, 3, 3) . "." . substr($PhoneNumber, 6, 4)}; - - # Put Spaces in the Phone Number for Announce Reasons - - $j = ''; - for ($i = 0; $i != (length($PhoneNumber)); ++$i) { - $j = $j . substr($PhoneNumber, $i, 1); - $j = $j . " "; - } - - $PhoneNumber = $j; - - # Log the data in a special file to announce from Palmpad - - open(CALLLOG, ">>$config_parms{code_dir}/calllog.log"); # Log it - print CALLLOG "$PhoneDate`$PhoneTime`$caller`$PhoneNumber\n"; - close CALLLOG; - - if ($PhoneName eq "Out of the Area" and $PhoneNumber eq '') { - print_msg "PHONE: Caller's Identification not available."; - speak "Caller's Identification not available."; - } - elsif ($PhoneName eq "Out of the Area" and $PhoneNumber ne '') { - print_msg "PHONE: Caller's Identification not available."; - speak "Unknown caller calling. Number is $PhoneNumber."; - } - else { - print_msg "PHONE: $caller is calling. Number is $PhoneNumber."; - speak "$caller is calling. Number is $PhoneNumber."; - } - } -} - -# Monthly Phone Log Backup - -if ($New_Month) { - my $dbm_file = "$Pgm_Path\\..\\data\\phone\\callerid.dbm"; - print_log "Backing up Phone Log to logs\\$dbm_file.$Year_Month_Now"; - - copy("$dbm_file.dir", "$dbm_file.$Year_Month_Now.dir") or print_log "Error in phone dbm copy 1: $!"; - copy("$dbm_file.pag", "$dbm_file.$Year_Month_Now.pag") or print_log "Error in phone dbm copy 2: $!"; - - # dbm_copy will delete any bad entries (those with binary characters) from the file. - - system("dbm_copy $dbm_file"); - copy("$dbm_file.backup.dir", "$dbm_file.dir") or print_log "Error in phone dbm copy 3: $!"; - copy("$dbm_file.backup.pag", "$dbm_file.pag") or print_log "Error in phone dbm copy 4: $!"; -} - -# Example on how to start and stop a serial port -$v_port_control1 = new Voice_Cmd("[Start,Stop] Serial Port Monitoring"); -if ($state = said $v_port_control1) { - print_log "Serial Port now in $state position."; - ($state eq 'start') ? start $phone_modem : stop $phone_modem; -} - -# Re-start the port, if it is not in use -if ($New_Minute and is_stopped $phone_modem and is_available $phone_modem) { - start $phone_modem; - set $phone_modem 'init'; - print_msg "MODEM Reinitialized..."; - print_log "MODEM Reinitialized..."; -} - +############################################################ +# Klier Home Automation - Caller ID Module for Rockwell # +# Version 2.2 Release # +# By: Brian J. Klier, N0QVC # +# Thanks for the mucho help from: Bruce Winter # +# E-Mail: klier@lakes.com # +# Webpage: http://www.faribault.k12.mn.us/brian # +############################################################ + +# Category=Phone + +# New in Version 2.2: +# - Added ability to recite phone numbers of callers with no name given. + +# Modem Caller ID Information looks like the following +# DATE = 990305 +# TIME = 1351 +# NMBR = 5073336399 +# NAME = KLIER BRIAN J + +# Declare Variables + +use vars qw($PhoneName $PhoneNumber $PhoneTime $PhoneDate); + +my ($PhoneModemString, $NameDone, $NumberDone, $i, $j); +my (@rejloglines, $NumofCalls); +my (@callloglines, $CallLogTempLine, $RejLogTempLine); +my ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog); +my ($last, $first, $middle, $areacode, $local_number, $caller); + +$phone_modem = new Serial_Item ('AT#CID=1','init','serial2'); +$timer_hangup = new Timer; + +#-----> Provide PalmPad Controller Access for some Items +$request_phone_stuff = new X10_Item('B6'); + +# Web Interface Commands + +$v_phone_lastcaller = new Voice_Cmd('Show Recent Call Log'); +if ((said $v_phone_lastcaller) || (state_now $request_phone_stuff eq 'on')) { + open(CALLLOG, "$config_parms{code_dir}/calllog.log"); # Open for input + @callloglines = ; # Open array and + # read in data + close CALLLOG; # Close the file + + print_log "Announced Recent Callers."; + + $NumofCalls = 0; + + foreach $CallLogTempLine (@callloglines) { + $NumofCalls = $NumofCalls + 1; + ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog) = (split('`', $CallLogTempLine))[0, 1, 2, 3]; + if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog ne $Date_Now + and $PhoneNumberLog eq '') { + speak "At $PhoneTimeLog on $PhoneDateLog, an unidentified party called."; + } + if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog eq $Date_Now + and $PhoneNumberLog eq '') { + speak "At $PhoneTimeLog, an unidentified party called."; + } + if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog ne $Date_Now + and $PhoneNumberLog ne '') { + speak "At $PhoneTimeLog on $PhoneDateLog, an unidentified party called. Call back at $PhoneNumberLog."; + } + if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog eq $Date_Now + and $PhoneNumberLog ne '') { + speak "At $PhoneTimeLog, an unidentified party called. Call back at $PhoneNumberLog."; + } + if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog ne $Date_Now) { + speak "At $PhoneTimeLog on $PhoneDateLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; + } + if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog eq $Date_Now) { + speak "At $PhoneTimeLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; + } + } + speak "$NumofCalls total calls."; +} + +$v_phone_clearlog = new Voice_Cmd('Clear Recent Call Log'); +if ((said $v_phone_clearlog) || (state_now $request_phone_stuff eq 'off')) { + open(CALLLOG, ">$config_parms{code_dir}/calllog.log"); # CLEAR Log + close CALLLOG; + print_log "Call Log Cleared."; + speak "Call Log Cleared."; +} + +#$v_phone_log = new Voice_Cmd('Run Display Callers'); +#if (said $v_phone_log) { +# print_log "Running Display Callers..."; +# undef @ARGV; +# do "$Pgm_Path/display_callers"; +# print_log "Done."; +#} + +# Set MODEM Init Strings on Startup + +if ($Startup or $Reload) { + set $phone_modem 'init'; # Initialize MODEM + + open(REJLOG, "$config_parms{code_dir}/rejlog.log"); # Open for input + @rejloglines = ; # Open array and + # read in data + close REJLOG; # Close the file + + print_msg "Caller ID Interface has been Initialized..."; + print_log "Caller ID Interface has been Initialized..."; +} + +# Timer Information for Phone Hangup (Reject List) + +if (expired $timer_hangup) { + set $phone_modem 'ATH'; + set $timer_hangup 0; +} + +# Display Incoming Serial Data + +if ($PhoneModemString = said $phone_modem) { + print_msg "PHONE: $PhoneModemString"; + + if (substr($PhoneModemString, 0, 4) eq 'RING') { + #run_voice_cmd "Stop Music"; + #set $boombox_bedroom 'off'; + } + + if (substr($PhoneModemString, 0, 4) eq 'NAME') { + $NameDone = "yes"; + $PhoneName = (substr($PhoneModemString, 7, 15)); + + # Switch name strings so first last, not last first. + # Use only the 1st two blank delimited fields, as the 3rd, 4th are usually just initials or incomplete + # Last First M + # Last M First + + ($last, $first, $middle) = (split(' ', $PhoneName))[0, 1, 2]; + $first = ucfirst(lc($first)); + $first = ucfirst(lc($middle)) if length($first) == 1; # Last M First format + $last = ucfirst(lc($last)); + $caller = "$first $last"; + } + + if (substr($PhoneModemString, 0, 4) eq 'NMBR') { + $NumberDone = "yes"; + $PhoneNumber = (substr($PhoneModemString, 7, 10)); + $areacode = (substr($PhoneNumber, 0, 3)); + $local_number = (substr($PhoneNumber, 3, 7)); + + if ($PhoneNumber eq "O") { + $PhoneNumber = ""; + $areacode = ""; + $local_number = ""; + } + } + + if (substr($PhoneModemString, 0, 13) eq 'MESG = 08014F') { + $NameDone = "yes"; + $NumberDone = "yes"; + $PhoneName = "Out of the Area"; + $caller = "Out of the Area"; + } + + if ($NumberDone eq "yes" and $NameDone eq "yes") { + $NumberDone = 0; + $NameDone = 0; + $PhoneDate = $Date_Now; + $PhoneTime = $Time_Now; + + # Log the data for use by display_callers + + logit("$Pgm_Path/../data/phone/logs/callerid.$Year_Month_Now.log", "$PhoneNumber $PhoneName"); + logit_dbm("$Pgm_Path/../data/phone/callerid.dbm", $PhoneNumber, "$Time_Now $Date_Now $Year name=$PhoneName"); + + # Check to see if callers phone number is in reject table. If so, + # let them have it. + + foreach $RejLogTempLine (@rejloglines) { + if (substr($RejLogTempLine, 0, 10) eq $PhoneNumber) { + print_log "$PhoneName is calling, and is in reject list!"; + speak "$PhoneName is calling, and is in reject list!"; + set $phone_modem 'ATA'; + set $timer_hangup 5; + } + } + + # If the incoming area code is the same, drop it from being spoken. + + if ($areacode eq $config_parms{local_area_code}) { + $PhoneNumber = $local_number; + } + + # Put pauses in between area code, exchange, and number for + # announce reasons + + if ((length($PhoneNumber) == 7)) {$PhoneNumber = substr($PhoneNumber, 0, 3) . "." . substr($PhoneNumber, 3, 4)}; + if ((length($PhoneNumber) == 10)) {$PhoneNumber = substr($PhoneNumber, 0, 3) . "." . substr($PhoneNumber, 3, 3) . "." . substr($PhoneNumber, 6, 4)}; + + # Put Spaces in the Phone Number for Announce Reasons + + $j = ''; + for ($i = 0; $i != (length($PhoneNumber)); ++$i) { + $j = $j . substr($PhoneNumber, $i, 1); + $j = $j . " "; + } + + $PhoneNumber = $j; + + # Log the data in a special file to announce from Palmpad + + open(CALLLOG, ">>$config_parms{code_dir}/calllog.log"); # Log it + print CALLLOG "$PhoneDate`$PhoneTime`$caller`$PhoneNumber\n"; + close CALLLOG; + + if ($PhoneName eq "Out of the Area" and $PhoneNumber eq '') { + print_msg "PHONE: Caller's Identification not available."; + speak "Caller's Identification not available."; + } + elsif ($PhoneName eq "Out of the Area" and $PhoneNumber ne '') { + print_msg "PHONE: Caller's Identification not available."; + speak "Unknown caller calling. Number is $PhoneNumber."; + } + else { + print_msg "PHONE: $caller is calling. Number is $PhoneNumber."; + speak "$caller is calling. Number is $PhoneNumber."; + } + } +} + +# Monthly Phone Log Backup + +if ($New_Month) { + my $dbm_file = "$Pgm_Path\\..\\data\\phone\\callerid.dbm"; + print_log "Backing up Phone Log to logs\\$dbm_file.$Year_Month_Now"; + + copy("$dbm_file.dir", "$dbm_file.$Year_Month_Now.dir") or print_log "Error in phone dbm copy 1: $!"; + copy("$dbm_file.pag", "$dbm_file.$Year_Month_Now.pag") or print_log "Error in phone dbm copy 2: $!"; + + # dbm_copy will delete any bad entries (those with binary characters) from the file. + + system("dbm_copy $dbm_file"); + copy("$dbm_file.backup.dir", "$dbm_file.dir") or print_log "Error in phone dbm copy 3: $!"; + copy("$dbm_file.backup.pag", "$dbm_file.pag") or print_log "Error in phone dbm copy 4: $!"; +} + +# Example on how to start and stop a serial port +$v_port_control1 = new Voice_Cmd("[Start,Stop] Serial Port Monitoring"); +if ($state = said $v_port_control1) { + print_log "Serial Port now in $state position."; + ($state eq 'start') ? start $phone_modem : stop $phone_modem; +} + +# Re-start the port, if it is not in use +if ($New_Minute and is_stopped $phone_modem and is_available $phone_modem) { + start $phone_modem; + set $phone_modem 'init'; + print_msg "MODEM Reinitialized..."; + print_log "MODEM Reinitialized..."; +} + diff --git a/code/public/Brian/old stuff i don't use anymore/callog.pl b/code/public/Brian/old stuff i don't use anymore/callog.pl index dd00a3c2d..2bbb16f92 100644 --- a/code/public/Brian/old stuff i don't use anymore/callog.pl +++ b/code/public/Brian/old stuff i don't use anymore/callog.pl @@ -1,232 +1,232 @@ -########################################################### -# Klier Home Automation - Caller ID Module for Rockwell # -# Version 2.1a Release # -# By: Brian J. Klier, N0QVC # -# Thanks for the mucho help from: Bruce Winter # -# E-Mail: klier@lakes.com # -# Webpage: http://www.faribault.k12.mn.us/brian # -########################################################### - -# Category=Phone - -# Modem Caller ID Information looks like the following -# DATE = 990305 -# TIME = 1351 -# NMBR = 5073336399 -# NAME = KLIER BRIAN J - -# Declare Variables - -my ($PhoneModemString, $PhoneNumber, $PhoneName, $NameDone, $NumberDone, $i, $j); -my (@rejloglines, $NumofCalls); -my ($PhoneDate, $PhoneTime, @callloglines, $CallLogTempLine, $RejLogTempLine); -my ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog); -my ($last, $first, $middle, $areacode, $local_number, $caller); - -$phone_modem = new Serial_Item ('AT#CID=1','init','serial2'); -$timer_hangup = new Timer; - -#-----> Provide PalmPad Controller Access for some Items -$request_phone_stuff = new X10_Item('B6'); - -# Web Interface Commands - -$v_phone_lastcaller = new Voice_Cmd('Show Recent Call Log'); -if ((said $v_phone_lastcaller) || (state_now $request_phone_stuff eq 'on')) { - open(CALLLOG, "$config_parms{code_dir}/calllog.log"); # Open for input - @callloglines = ; # Open array and - # read in data - close CALLLOG; # Close the file - - print_log "Announced Recent Callers."; - - $NumofCalls = 0; - - foreach $CallLogTempLine (@callloglines) { - $NumofCalls = $NumofCalls + 1; - ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog) = (split('`', $CallLogTempLine))[0, 1, 2, 3]; - if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog ne $Date_Now) { - speak "At $PhoneTimeLog on $PhoneDateLog, a person from out of the area called."; - } - if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog eq $Date_Now) { - speak "At $PhoneTimeLog, a person from out of the area called."; - } - if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog ne $Date_Now) { - speak "At $PhoneTimeLog on $PhoneDateLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; - } - if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog eq $Date_Now) { - speak "At $PhoneTimeLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; - } - } - speak "$NumofCalls total calls."; -} - -$v_phone_clearlog = new Voice_Cmd('Clear Recent Call Log'); -if ((said $v_phone_clearlog) || (state_now $request_phone_stuff eq 'off')) { - open(CALLLOG, ">$config_parms{code_dir}/calllog.log"); # CLEAR Log - close CALLLOG; - print_log "Call Log Cleared."; - speak "Call Log Cleared."; -} - -#$v_phone_log = new Voice_Cmd('Run Display Callers'); -#if (said $v_phone_log) { -# print_log "Running Display Callers..."; -# undef @ARGV; -# do "$Pgm_Path/display_callers"; -# print_log "Done."; -#} - -# Set MODEM Init Strings on Startup - -if ($Startup or $Reload) { - set $phone_modem 'init'; # Initialize MODEM - - open(REJLOG, "$config_parms{code_dir}/rejlog.log"); # Open for input - @rejloglines = ; # Open array and - # read in data - close REJLOG; # Close the file - - print_msg "Caller ID Interface has been Initialized..."; - print_log "Caller ID Interface has been Initialized..."; -} - -# Timer Information for Phone Hangup (Reject List) - -if (expired $timer_hangup) { - set $phone_modem 'ATH'; - set $timer_hangup 0; -} - -# Display Incoming Serial Data - -if ($PhoneModemString = said $phone_modem) { - print_msg "PHONE: $PhoneModemString"; - - if (substr($PhoneModemString, 0, 4) eq 'RING') { - #run_voice_cmd "Stop Music"; - #set $boombox_bedroom 'off'; - } - - if (substr($PhoneModemString, 0, 4) eq 'NAME') { - $NameDone = "yes"; - $PhoneName = (substr($PhoneModemString, 7, 15)); - - # Switch name strings so first last, not last first. - # Use only the 1st two blank delimited fields, as the 3rd, 4th are usually just initials or incomplete - # Last First M - # Last M First - - ($last, $first, $middle) = (split(' ', $PhoneName))[0, 1, 2]; - $first = ucfirst(lc($first)); - $first = ucfirst(lc($middle)) if length($first) == 1; # Last M First format - $last = ucfirst(lc($last)); - $caller = "$first $last"; - } - - if (substr($PhoneModemString, 0, 4) eq 'NMBR') { - $NumberDone = "yes"; - $PhoneNumber = (substr($PhoneModemString, 7, 10)); - $areacode = (substr($PhoneNumber, 0, 3)); - $local_number = (substr($PhoneNumber, 3, 7)); - - if ($PhoneNumber eq "O") { - $PhoneNumber = ""; - $areacode = ""; - $local_number = ""; - } - } - - if (substr($PhoneModemString, 0, 13) eq 'MESG = 08014F') { - $NameDone = "yes"; - $NumberDone = "yes"; - $PhoneName = "Out of the Area"; - $caller = "Out of the Area"; - } - - if ($NumberDone eq "yes" and $NameDone eq "yes") { - $NumberDone = 0; - $NameDone = 0; - $PhoneDate = $Date_Now; - $PhoneTime = $Time_Now; - - # Log the data for use by display_callers - - logit("$Pgm_Path/../data/phone/logs/callerid.$Year_Month_Now.log", "$PhoneNumber $PhoneName"); - logit_dbm("$Pgm_Path/../data/phone/callerid.dbm", $PhoneNumber, "$Time_Now $Date_Now $Year name=$PhoneName"); - - # Check to see if callers phone number is in reject table. If so, - # let them have it. - - foreach $RejLogTempLine (@rejloglines) { - if (substr($RejLogTempLine, 0, 10) eq $PhoneNumber) { - print_log "$PhoneName is calling, and is in reject list!"; - speak "$PhoneName is calling, and is in reject list!"; - set $phone_modem 'ATA'; - set $timer_hangup 5; - } - } - - # If the incoming area code is the same, drop it from being spoken. - - if ($areacode eq $config_parms{local_area_code}) { - $PhoneNumber = $local_number; - } - - # Put Spaces in the Phone Number for Announce Reasons - - $j = ''; - for ($i = 0; $i != (length($PhoneNumber)); ++$i) { - $j = $j . substr($PhoneNumber, $i, 1); - $j = $j . " "; - } - - $PhoneNumber = $j; - - # Log the data in a special file to announce from Palmpad - - open(CALLLOG, ">>$config_parms{code_dir}/calllog.log"); # Log it - print CALLLOG "$PhoneDate`$PhoneTime`$caller`$PhoneNumber\n"; - close CALLLOG; - - if ($PhoneName eq "Out of the Area") { - print_msg "PHONE: Out of area phone call."; - speak "Out of area phone call."; - } - else { - print_msg "PHONE: $caller is calling. Number is $PhoneNumber."; - speak "$caller is calling. Number is $PhoneNumber."; - } - } -} - -# Monthly Phone Log Backup - -if ($New_Month) { - my $dbm_file = "$Pgm_Path\\..\\data\\phone\\callerid.dbm"; - print_log "Backing up Phone Log to logs\\$dbm_file.$Year_Month_Now"; - - copy("$dbm_file.dir", "$dbm_file.$Year_Month_Now.dir") or print_log "Error in phone dbm copy 1: $!"; - copy("$dbm_file.pag", "$dbm_file.$Year_Month_Now.pag") or print_log "Error in phone dbm copy 2: $!"; - - # dbm_copy will delete any bad entries (those with binary characters) from the file. - - system("dbm_copy $dbm_file"); - copy("$dbm_file.backup.dir", "$dbm_file.dir") or print_log "Error in phone dbm copy 3: $!"; - copy("$dbm_file.backup.pag", "$dbm_file.pag") or print_log "Error in phone dbm copy 4: $!"; -} - -# Example on how to start and stop a serial port -$v_port_control1 = new Voice_Cmd("[Start,Stop] Serial Port Monitoring"); -if ($state = said $v_port_control1) { - print_log "Serial Port now in $state position."; - ($state eq 'start') ? start $phone_modem : stop $phone_modem; -} - -# Re-start the port, if it is not in use -if ($New_Minute and is_stopped $phone_modem and is_available $phone_modem) { - start $phone_modem; - set $phone_modem 'init'; - print_msg "MODEM Reinitialized..."; - print_log "MODEM Reinitialized..."; -} +########################################################### +# Klier Home Automation - Caller ID Module for Rockwell # +# Version 2.1a Release # +# By: Brian J. Klier, N0QVC # +# Thanks for the mucho help from: Bruce Winter # +# E-Mail: klier@lakes.com # +# Webpage: http://www.faribault.k12.mn.us/brian # +########################################################### + +# Category=Phone + +# Modem Caller ID Information looks like the following +# DATE = 990305 +# TIME = 1351 +# NMBR = 5073336399 +# NAME = KLIER BRIAN J + +# Declare Variables + +my ($PhoneModemString, $PhoneNumber, $PhoneName, $NameDone, $NumberDone, $i, $j); +my (@rejloglines, $NumofCalls); +my ($PhoneDate, $PhoneTime, @callloglines, $CallLogTempLine, $RejLogTempLine); +my ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog); +my ($last, $first, $middle, $areacode, $local_number, $caller); + +$phone_modem = new Serial_Item ('AT#CID=1','init','serial2'); +$timer_hangup = new Timer; + +#-----> Provide PalmPad Controller Access for some Items +$request_phone_stuff = new X10_Item('B6'); + +# Web Interface Commands + +$v_phone_lastcaller = new Voice_Cmd('Show Recent Call Log'); +if ((said $v_phone_lastcaller) || (state_now $request_phone_stuff eq 'on')) { + open(CALLLOG, "$config_parms{code_dir}/calllog.log"); # Open for input + @callloglines = ; # Open array and + # read in data + close CALLLOG; # Close the file + + print_log "Announced Recent Callers."; + + $NumofCalls = 0; + + foreach $CallLogTempLine (@callloglines) { + $NumofCalls = $NumofCalls + 1; + ($PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog) = (split('`', $CallLogTempLine))[0, 1, 2, 3]; + if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog ne $Date_Now) { + speak "At $PhoneTimeLog on $PhoneDateLog, a person from out of the area called."; + } + if ($PhoneNameLog eq 'Out of the Area' and $PhoneDateLog eq $Date_Now) { + speak "At $PhoneTimeLog, a person from out of the area called."; + } + if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog ne $Date_Now) { + speak "At $PhoneTimeLog on $PhoneDateLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; + } + if ($PhoneNameLog ne 'Out of the Area' and $PhoneDateLog eq $Date_Now) { + speak "At $PhoneTimeLog, $PhoneNameLog called. Call back at $PhoneNumberLog."; + } + } + speak "$NumofCalls total calls."; +} + +$v_phone_clearlog = new Voice_Cmd('Clear Recent Call Log'); +if ((said $v_phone_clearlog) || (state_now $request_phone_stuff eq 'off')) { + open(CALLLOG, ">$config_parms{code_dir}/calllog.log"); # CLEAR Log + close CALLLOG; + print_log "Call Log Cleared."; + speak "Call Log Cleared."; +} + +#$v_phone_log = new Voice_Cmd('Run Display Callers'); +#if (said $v_phone_log) { +# print_log "Running Display Callers..."; +# undef @ARGV; +# do "$Pgm_Path/display_callers"; +# print_log "Done."; +#} + +# Set MODEM Init Strings on Startup + +if ($Startup or $Reload) { + set $phone_modem 'init'; # Initialize MODEM + + open(REJLOG, "$config_parms{code_dir}/rejlog.log"); # Open for input + @rejloglines = ; # Open array and + # read in data + close REJLOG; # Close the file + + print_msg "Caller ID Interface has been Initialized..."; + print_log "Caller ID Interface has been Initialized..."; +} + +# Timer Information for Phone Hangup (Reject List) + +if (expired $timer_hangup) { + set $phone_modem 'ATH'; + set $timer_hangup 0; +} + +# Display Incoming Serial Data + +if ($PhoneModemString = said $phone_modem) { + print_msg "PHONE: $PhoneModemString"; + + if (substr($PhoneModemString, 0, 4) eq 'RING') { + #run_voice_cmd "Stop Music"; + #set $boombox_bedroom 'off'; + } + + if (substr($PhoneModemString, 0, 4) eq 'NAME') { + $NameDone = "yes"; + $PhoneName = (substr($PhoneModemString, 7, 15)); + + # Switch name strings so first last, not last first. + # Use only the 1st two blank delimited fields, as the 3rd, 4th are usually just initials or incomplete + # Last First M + # Last M First + + ($last, $first, $middle) = (split(' ', $PhoneName))[0, 1, 2]; + $first = ucfirst(lc($first)); + $first = ucfirst(lc($middle)) if length($first) == 1; # Last M First format + $last = ucfirst(lc($last)); + $caller = "$first $last"; + } + + if (substr($PhoneModemString, 0, 4) eq 'NMBR') { + $NumberDone = "yes"; + $PhoneNumber = (substr($PhoneModemString, 7, 10)); + $areacode = (substr($PhoneNumber, 0, 3)); + $local_number = (substr($PhoneNumber, 3, 7)); + + if ($PhoneNumber eq "O") { + $PhoneNumber = ""; + $areacode = ""; + $local_number = ""; + } + } + + if (substr($PhoneModemString, 0, 13) eq 'MESG = 08014F') { + $NameDone = "yes"; + $NumberDone = "yes"; + $PhoneName = "Out of the Area"; + $caller = "Out of the Area"; + } + + if ($NumberDone eq "yes" and $NameDone eq "yes") { + $NumberDone = 0; + $NameDone = 0; + $PhoneDate = $Date_Now; + $PhoneTime = $Time_Now; + + # Log the data for use by display_callers + + logit("$Pgm_Path/../data/phone/logs/callerid.$Year_Month_Now.log", "$PhoneNumber $PhoneName"); + logit_dbm("$Pgm_Path/../data/phone/callerid.dbm", $PhoneNumber, "$Time_Now $Date_Now $Year name=$PhoneName"); + + # Check to see if callers phone number is in reject table. If so, + # let them have it. + + foreach $RejLogTempLine (@rejloglines) { + if (substr($RejLogTempLine, 0, 10) eq $PhoneNumber) { + print_log "$PhoneName is calling, and is in reject list!"; + speak "$PhoneName is calling, and is in reject list!"; + set $phone_modem 'ATA'; + set $timer_hangup 5; + } + } + + # If the incoming area code is the same, drop it from being spoken. + + if ($areacode eq $config_parms{local_area_code}) { + $PhoneNumber = $local_number; + } + + # Put Spaces in the Phone Number for Announce Reasons + + $j = ''; + for ($i = 0; $i != (length($PhoneNumber)); ++$i) { + $j = $j . substr($PhoneNumber, $i, 1); + $j = $j . " "; + } + + $PhoneNumber = $j; + + # Log the data in a special file to announce from Palmpad + + open(CALLLOG, ">>$config_parms{code_dir}/calllog.log"); # Log it + print CALLLOG "$PhoneDate`$PhoneTime`$caller`$PhoneNumber\n"; + close CALLLOG; + + if ($PhoneName eq "Out of the Area") { + print_msg "PHONE: Out of area phone call."; + speak "Out of area phone call."; + } + else { + print_msg "PHONE: $caller is calling. Number is $PhoneNumber."; + speak "$caller is calling. Number is $PhoneNumber."; + } + } +} + +# Monthly Phone Log Backup + +if ($New_Month) { + my $dbm_file = "$Pgm_Path\\..\\data\\phone\\callerid.dbm"; + print_log "Backing up Phone Log to logs\\$dbm_file.$Year_Month_Now"; + + copy("$dbm_file.dir", "$dbm_file.$Year_Month_Now.dir") or print_log "Error in phone dbm copy 1: $!"; + copy("$dbm_file.pag", "$dbm_file.$Year_Month_Now.pag") or print_log "Error in phone dbm copy 2: $!"; + + # dbm_copy will delete any bad entries (those with binary characters) from the file. + + system("dbm_copy $dbm_file"); + copy("$dbm_file.backup.dir", "$dbm_file.dir") or print_log "Error in phone dbm copy 3: $!"; + copy("$dbm_file.backup.pag", "$dbm_file.pag") or print_log "Error in phone dbm copy 4: $!"; +} + +# Example on how to start and stop a serial port +$v_port_control1 = new Voice_Cmd("[Start,Stop] Serial Port Monitoring"); +if ($state = said $v_port_control1) { + print_log "Serial Port now in $state position."; + ($state eq 'start') ? start $phone_modem : stop $phone_modem; +} + +# Re-start the port, if it is not in use +if ($New_Minute and is_stopped $phone_modem and is_available $phone_modem) { + start $phone_modem; + set $phone_modem 'init'; + print_msg "MODEM Reinitialized..."; + print_log "MODEM Reinitialized..."; +} diff --git a/code/public/Brian/old stuff i don't use anymore/radar.pl b/code/public/Brian/old stuff i don't use anymore/radar.pl index c2e72d4f0..19336d156 100644 --- a/code/public/Brian/old stuff i don't use anymore/radar.pl +++ b/code/public/Brian/old stuff i don't use anymore/radar.pl @@ -1,80 +1,80 @@ -# Category = Vehicles - -$radar_active = new Generic_Item; - -$timer_aftercon = new Timer; -$timer_beforedis = new Timer; - -my ($ManualRadarFlag); - -if ($Startup or $Reload) { - set $radar_active 'no'; -} - -$p_get_radar = new Process_Item("get_url http://www.findu.com/cgi-bin/radar-find.cgi?call=n0qvc-1&nocall=1&offsetx=40&offsety=35 $config_parms{data_dir}/web/radar.png"); -#$p_get_radar2 = new Process_Item("get_url http://www.swiftwx.com/warnings/warnings.aspx $config_parms{data_dir}/web/warnings.asp"); -#$p_get_radar3 = new Process_Item("get_url http://www.swiftwx.com/warnings/watches.aspx $config_parms{data_dir}/web/watches.asp"); - -$v_get_radar = new Voice_Cmd('Retrieve Chicklet Radar'); -if (said $v_get_radar) { - $ManualRadarFlag = 'yes'; - start $p_get_radar; - #start $p_get_radar2; - #start $p_get_radar3; - speak "Retrieving RADAR"; -} - -# Main Loop - -#if ((substr($APRSString, 0, 7) eq '*** CON') || # If a packet comes in, -# (substr($APRSString, 0, 11) eq 'cmd:*** CON')) { # and we are connected, -# speak "We're connected!"; -# set $tnc_output "$HamCall Radar Retrieval System"; -# set $radar_active 'yes'; -# set $enable_transmit 'no'; -# start $p_get_radar; -# #start $p_get_radar2; -# #start $p_get_radar3; -# speak "Retrieving RADAR"; -#} - -# If the TNC shows we are disconnected, force a disconnect and re-enter -# converse mode. -if ((substr($APRSString, 0, 7) eq '*** DIS') || - (substr($APRSString, 0, 11) eq 'cmd:*** DIS') || - (substr($APRSString, 0, 15) eq 'cmd:cmd:*** DIS')) { - set $timer_beforedis 1; - speak "Disconnected!"; - set $enable_transmit 'yes'; - set $radar_active 'no'; - set $tnc_output pack('C',3); # Send Control-C - set $tnc_output 'CONV'; # Enter Converse Mode -} - -# Force a disconnect -if (expired $timer_beforedis) { - set $timer_beforedis 0; - set $enable_transmit 'yes'; - set $radar_active 'no'; - set $tnc_output pack('C',3); # Send Control-C - set $tnc_output 'D'; # Go to Disconnect - set $tnc_output 'CONV'; # Enter Converse Mode -} - -# After we retrieve the radar, pause for 15 seconds to allow uuencode to -# do it's thing. -if (done_now $p_get_radar) { - speak "Done Retrieving Radar"; - if ($ManualRadarFlag ne 'yes') {set $timer_aftercon 15}; - $ManualRadarFlag = ''; - run qq[uueradar]; -} - -# After the 15 seconds is up, go ahead and send the data to the remote -# station. -if (expired $timer_aftercon) { - set $timer_aftercon 0; - my $RadarBase64 = file_read("$config_parms{data_dir}/web/radar.001"); - set $tnc_output $RadarBase64; - set $timer_beforedis 360; -} +# Category = Vehicles + +$radar_active = new Generic_Item; + +$timer_aftercon = new Timer; +$timer_beforedis = new Timer; + +my ($ManualRadarFlag); + +if ($Startup or $Reload) { + set $radar_active 'no'; +} + +$p_get_radar = new Process_Item("get_url http://www.findu.com/cgi-bin/radar-find.cgi?call=n0qvc-1&nocall=1&offsetx=40&offsety=35 $config_parms{data_dir}/web/radar.png"); +#$p_get_radar2 = new Process_Item("get_url http://www.swiftwx.com/warnings/warnings.aspx $config_parms{data_dir}/web/warnings.asp"); +#$p_get_radar3 = new Process_Item("get_url http://www.swiftwx.com/warnings/watches.aspx $config_parms{data_dir}/web/watches.asp"); + +$v_get_radar = new Voice_Cmd('Retrieve Chicklet Radar'); +if (said $v_get_radar) { + $ManualRadarFlag = 'yes'; + start $p_get_radar; + #start $p_get_radar2; + #start $p_get_radar3; + speak "Retrieving RADAR"; +} + +# Main Loop + +#if ((substr($APRSString, 0, 7) eq '*** CON') || # If a packet comes in, +# (substr($APRSString, 0, 11) eq 'cmd:*** CON')) { # and we are connected, +# speak "We're connected!"; +# set $tnc_output "$HamCall Radar Retrieval System"; +# set $radar_active 'yes'; +# set $enable_transmit 'no'; +# start $p_get_radar; +# #start $p_get_radar2; +# #start $p_get_radar3; +# speak "Retrieving RADAR"; +#} + +# If the TNC shows we are disconnected, force a disconnect and re-enter +# converse mode. +if ((substr($APRSString, 0, 7) eq '*** DIS') || + (substr($APRSString, 0, 11) eq 'cmd:*** DIS') || + (substr($APRSString, 0, 15) eq 'cmd:cmd:*** DIS')) { + set $timer_beforedis 1; + speak "Disconnected!"; + set $enable_transmit 'yes'; + set $radar_active 'no'; + set $tnc_output pack('C',3); # Send Control-C + set $tnc_output 'CONV'; # Enter Converse Mode +} + +# Force a disconnect +if (expired $timer_beforedis) { + set $timer_beforedis 0; + set $enable_transmit 'yes'; + set $radar_active 'no'; + set $tnc_output pack('C',3); # Send Control-C + set $tnc_output 'D'; # Go to Disconnect + set $tnc_output 'CONV'; # Enter Converse Mode +} + +# After we retrieve the radar, pause for 15 seconds to allow uuencode to +# do it's thing. +if (done_now $p_get_radar) { + speak "Done Retrieving Radar"; + if ($ManualRadarFlag ne 'yes') {set $timer_aftercon 15}; + $ManualRadarFlag = ''; + run qq[uueradar]; +} + +# After the 15 seconds is up, go ahead and send the data to the remote +# station. +if (expired $timer_aftercon) { + set $timer_aftercon 0; + my $RadarBase64 = file_read("$config_parms{data_dir}/web/radar.001"); + set $tnc_output $RadarBase64; + set $timer_beforedis 360; +} diff --git a/code/public/Brian/old stuff i don't use anymore/tk_frames.pl b/code/public/Brian/old stuff i don't use anymore/tk_frames.pl index 736d199cc..41e8aa912 100644 --- a/code/public/Brian/old stuff i don't use anymore/tk_frames.pl +++ b/code/public/Brian/old stuff i don't use anymore/tk_frames.pl @@ -1,79 +1,79 @@ - -# Position=1 Load before any tk_widget code - -# This file determines the layout of the mh Tk window - - # Re-create tk widgets on startup or if this file has changed on code reload -if ($MW and $Reload) { - - # If this file has not changed, only re-create the tk widget grid - if (!$Startup and !file_change("$config_parms{code_dir}/tk_frames.pl")) { - print "Deleteing old grid frame\n"; - $Tk_objects{grid}->destroy; - $Tk_objects{grid} = $Tk_objects{ft}->Frame->pack(qw/-side right -anchor n/); - } - - # This file changed, so re-create all frames - else { - file_change("$config_parms{code_dir}/tk_frames.pl") if $Startup; # Set file change time stamp - - unless ($Startup) { - print "Deleteing old tk frames\n"; - $Tk_objects{ft}->destroy; - $Tk_objects{fb}->destroy; - } - print "Creating new tk frames\n"; - - # Create top and bottom frames - $Tk_objects{ft} = $MW->Frame->pack(qw/-side top -fill both -expand 1/); - $Tk_objects{fb} = $MW->Frame->pack(qw/-side top -fill both -expand 1/); - - # Create top left and tk grid frames - $Tk_objects{ftl} = $Tk_objects{ft}->Frame->pack(qw/-side left -fill both -expand 1/); - $Tk_objects{grid} = $Tk_objects{ft}->Frame->pack(qw/-side right -anchor n/); - - # Add command list and msg windows to top left frame - $Tk_objects{cmd_list} = &tk_command_list($Tk_objects{ftl}); - $Tk_objects{cmd_list}->pack(qw/-side top -expand 1 -fill both/); - - $Tk_objects{cmd_list}->insert(0, ' ', ' ', ' ', &list_voice_cmds_match('')) if $Startup; # Init with all commands - - - $Tk_objects{msg_window} = $Tk_objects{ftl}-> - Scrolled('Text', -height=> 5, -width => 30, -bg => 'white', -wrap => 'none', -scrollbars => 'se')-> - pack(qw/-side top -expand 1 -fill both/); - &print_msg("Started"); - - # Add speak and log windows to bottom frame - $Tk_objects{speak_window} = $Tk_objects{fb}-> - Scrolled('Text', -height => 7, -width => 40, -bg => 'cyan', -wrap => 'none', -scrollbars => 'se', - -setgrid => 'true', -font => 'Courier* 10 bold')-> - pack(qw/-side top -expand 1 -fill both/); - - $Tk_objects{log_window} = $Tk_objects{fb}-> - Scrolled('Text', -height => 7, -width => 40, -bg => 'cyan', -wrap => 'none', -scrollbars => 'se', - -setgrid => 'true', -font => 'Courier* 10 bold')-> - pack(qw/-side top -expand 1 -fill both/); - - - # Allow geometry resizing on reload, but only if it has changed, - # so we don't mess up manual changes. - if ($config_parms{tk_geometry} and - ($Startup or - $config_parms{tk_geometry} ne $config_parms{tk_geometry_startup})) { - $MW->geometry($config_parms{tk_geometry}); - $config_parms{tk_geometry_startup} = $config_parms{tk_geometry}; - } - } - - # Show the window (it is hidden during statup) - if ($Startup) { - $MW->deiconify; - $MW->raise; - $MW->focusForce; -# $MW->focus("-force"); -# $MW->grabGlobal; -# $MW->grab("-global"); - } - -} + +# Position=1 Load before any tk_widget code + +# This file determines the layout of the mh Tk window + + # Re-create tk widgets on startup or if this file has changed on code reload +if ($MW and $Reload) { + + # If this file has not changed, only re-create the tk widget grid + if (!$Startup and !file_change("$config_parms{code_dir}/tk_frames.pl")) { + print "Deleteing old grid frame\n"; + $Tk_objects{grid}->destroy; + $Tk_objects{grid} = $Tk_objects{ft}->Frame->pack(qw/-side right -anchor n/); + } + + # This file changed, so re-create all frames + else { + file_change("$config_parms{code_dir}/tk_frames.pl") if $Startup; # Set file change time stamp + + unless ($Startup) { + print "Deleteing old tk frames\n"; + $Tk_objects{ft}->destroy; + $Tk_objects{fb}->destroy; + } + print "Creating new tk frames\n"; + + # Create top and bottom frames + $Tk_objects{ft} = $MW->Frame->pack(qw/-side top -fill both -expand 1/); + $Tk_objects{fb} = $MW->Frame->pack(qw/-side top -fill both -expand 1/); + + # Create top left and tk grid frames + $Tk_objects{ftl} = $Tk_objects{ft}->Frame->pack(qw/-side left -fill both -expand 1/); + $Tk_objects{grid} = $Tk_objects{ft}->Frame->pack(qw/-side right -anchor n/); + + # Add command list and msg windows to top left frame + $Tk_objects{cmd_list} = &tk_command_list($Tk_objects{ftl}); + $Tk_objects{cmd_list}->pack(qw/-side top -expand 1 -fill both/); + + $Tk_objects{cmd_list}->insert(0, ' ', ' ', ' ', &list_voice_cmds_match('')) if $Startup; # Init with all commands + + + $Tk_objects{msg_window} = $Tk_objects{ftl}-> + Scrolled('Text', -height=> 5, -width => 30, -bg => 'white', -wrap => 'none', -scrollbars => 'se')-> + pack(qw/-side top -expand 1 -fill both/); + &print_msg("Started"); + + # Add speak and log windows to bottom frame + $Tk_objects{speak_window} = $Tk_objects{fb}-> + Scrolled('Text', -height => 7, -width => 40, -bg => 'cyan', -wrap => 'none', -scrollbars => 'se', + -setgrid => 'true', -font => 'Courier* 10 bold')-> + pack(qw/-side top -expand 1 -fill both/); + + $Tk_objects{log_window} = $Tk_objects{fb}-> + Scrolled('Text', -height => 7, -width => 40, -bg => 'cyan', -wrap => 'none', -scrollbars => 'se', + -setgrid => 'true', -font => 'Courier* 10 bold')-> + pack(qw/-side top -expand 1 -fill both/); + + + # Allow geometry resizing on reload, but only if it has changed, + # so we don't mess up manual changes. + if ($config_parms{tk_geometry} and + ($Startup or + $config_parms{tk_geometry} ne $config_parms{tk_geometry_startup})) { + $MW->geometry($config_parms{tk_geometry}); + $config_parms{tk_geometry_startup} = $config_parms{tk_geometry}; + } + } + + # Show the window (it is hidden during statup) + if ($Startup) { + $MW->deiconify; + $MW->raise; + $MW->focusForce; +# $MW->focus("-force"); +# $MW->grabGlobal; +# $MW->grab("-global"); + } + +} diff --git a/code/public/Brian/old stuff i don't use anymore/tk_widgets.pl b/code/public/Brian/old stuff i don't use anymore/tk_widgets.pl index 14c3df12f..5827d0d06 100644 --- a/code/public/Brian/old stuff i don't use anymore/tk_widgets.pl +++ b/code/public/Brian/old stuff i don't use anymore/tk_widgets.pl @@ -1,38 +1,38 @@ - -# Position=2 Load after tk_frames - -# This file adds to the tk widget grid frame - - # Re-create tk widgets on reload -if ($MW and $Reload) { - - &tk_mbutton('Help', \&help); - - &tk_button('Reload(F1)', \&read_code, 'Pause (F2)', \&pause, ' Exit (F3) ', \&sig_handler, - 'Debug(F4)', \&toggle_debug, 'Log(F5)', \&toggle_log); - $MW->bind('' => \&read_code); - $MW->bind('' => \&pause); - $MW->bind('' => \&sig_handler); - $MW->bind('' => \&toggle_debug); - $MW->bind('' => \&toggle_log); - - &tk_label(\$Tk_objects{label_time}); - &tk_label(\$Tk_objects{label_uptime_cpu}); - &tk_label(\$Tk_objects{label_uptime_mh}); - &tk_label(\$Tk_objects{label_cpu_used}); - &tk_label(\$Tk_objects{label_cpu_loops}); - -# &tk_entry("Sleep time:", \$Loop_Sleep_Time); -# &tk_entry("Tk passes:", \$Loop_Tk_Passes); - -# &tk_entry("Test data:", \$Save{test_data}); - - &tk_radiobutton('Mode', \$Save{mode}, ['normal', 'mute', 'offline'], ['Normal', 'Mute', 'Offline']); - -# &tk_radiobutton('Debug', \$config_parms{debug}, [1, 0], ['On', 'Off']); -# &tk_checkbutton('Debug on', \$config_parms{debug}); - - &tk_radiobutton('Tracking', \$config_parms{tracking_speakflag}, [0,1,2,3], ['None', 'GPS', 'WX', 'All']); - - &tk_radiobutton('Weekday Callout', \$Save{Autocall}, ['on', 'off'], ['On', 'Off']); -} + +# Position=2 Load after tk_frames + +# This file adds to the tk widget grid frame + + # Re-create tk widgets on reload +if ($MW and $Reload) { + + &tk_mbutton('Help', \&help); + + &tk_button('Reload(F1)', \&read_code, 'Pause (F2)', \&pause, ' Exit (F3) ', \&sig_handler, + 'Debug(F4)', \&toggle_debug, 'Log(F5)', \&toggle_log); + $MW->bind('' => \&read_code); + $MW->bind('' => \&pause); + $MW->bind('' => \&sig_handler); + $MW->bind('' => \&toggle_debug); + $MW->bind('' => \&toggle_log); + + &tk_label(\$Tk_objects{label_time}); + &tk_label(\$Tk_objects{label_uptime_cpu}); + &tk_label(\$Tk_objects{label_uptime_mh}); + &tk_label(\$Tk_objects{label_cpu_used}); + &tk_label(\$Tk_objects{label_cpu_loops}); + +# &tk_entry("Sleep time:", \$Loop_Sleep_Time); +# &tk_entry("Tk passes:", \$Loop_Tk_Passes); + +# &tk_entry("Test data:", \$Save{test_data}); + + &tk_radiobutton('Mode', \$Save{mode}, ['normal', 'mute', 'offline'], ['Normal', 'Mute', 'Offline']); + +# &tk_radiobutton('Debug', \$config_parms{debug}, [1, 0], ['On', 'Off']); +# &tk_checkbutton('Debug on', \$config_parms{debug}); + + &tk_radiobutton('Tracking', \$config_parms{tracking_speakflag}, [0,1,2,3], ['None', 'GPS', 'WX', 'All']); + + &tk_radiobutton('Weekday Callout', \$Save{Autocall}, ['on', 'off'], ['On', 'Off']); +} diff --git a/code/public/Brian/old stuff i don't use anymore/tracking.pl b/code/public/Brian/old stuff i don't use anymore/tracking.pl index 960d5708e..447e61f98 100644 --- a/code/public/Brian/old stuff i don't use anymore/tracking.pl +++ b/code/public/Brian/old stuff i don't use anymore/tracking.pl @@ -1,1394 +1,1394 @@ - -###################################################### -# Klier Home Automation - Tracking Module # -# Version 4.92 (release for MH 2.??) # -# By: Brian J. Klier, N0QVC # -# June 16, 2001 # -# E-Mail: brian@kliernetwork.net # -# Webpage: http://www.kliernetwork.net # -###################################################### - -=begin comment - -mh.ini parms: - -tracking_trackself=1 # This parameter should equal "1" if - # GPS Speaking is off and you still want - # to hear tracking from your own mobile -tracking_shortannounce=1 # 0 = When Speaking Tracking - # Information, this will - # announce both distance from - # this station and distance - # from waypoint. - # 1 = Only Distance from waypoint. -tracking_withname=1 # 0 = If tracking.nam available, - # announce callsign instead - # of given name. - # 1 = Announce Given name instead - # of callsign. - # 2 = Announce given name AND - # callsign. - -=cut - -# For more information on hardware needed for this system to function: -# - Check out http://www.kliernetwork.net/aprs and -# http://www.kliernetwork.net/aprs/mine -# -# New in Version 4.92: -# - Added Roger Bille's "Longitude Hundreds" Fix -# -# New in Version 4.91: -# - Fixed bugs in "EMAIL2" Procedure -# - Added ALPHA Procedure to take data from Telnet Port and transmit -# on the air (for WinAPRS Connectivity) -# -# New in Version 4.9: -# - Fixed bugs in the HTML Based Logging System. -# -# New in Version 4.8 and 4.8a: -# - Added HTML Based Logging (from Bruce's tracking_bruce.pl) for GPS's. -# -# New in Version 4.7: -# - Added Speaking of POSFILE Positions for Weather Stations -# -# New in Version 4.62: -# - Fixed problem with tracking_withname and speaking EVERY position -# instead of checking to see if it was the same as the last one. -# - Added ability to respond to ?WX? queries with current temperature. -# -# New in Version 4.6: -# - 4.61 ALPHA - Continued work on proper implementation. -# - Added tracking_shortannounce and tracking_withname variables. -# - NOTE: I STILL need to implement tracking_withname=2!!! -# -# New in Version 4.5: -# - 4.52 ALPHA - MHATS now ignores "acks" that are sent - does not talk -# to them. Also added msg # to "msg received" messages. -# - 4.51 ALPHA - Fix Small Bug in X-10 Message Response... -# - ALPHA - Working on Temperature Graphs -# - Changed "EMAIL" gateway to "EMAIL2" so it doesn't interfere with the -# main APRS IGATES. -# -# New in Version 4.4: -# - Fixed all the messaging/X10 Packet Remote Control stuff. -# - Bulletin feature still needs to be tested for operation. -# -# Next Version Wishlist: -# - Check for duplicate messages in a row (like when they pass through -# a digipeater to make sure an X10 command isn't sent twice) -# - System to execute X-10 commands when a station is a certain distance from home - -# Declare Variables - -use vars '$GPSSpeakString', '$GPSSpeakString2', '$WXSpeakString', '$WXSpeakString2', '$CurrentTemp', '$CurrentChill', '$WXWindDirVoice', '$WXWindSpeed', '$WXHrPrecip'; - -my ($APRSFoundAPRS, $APRSPacketDigi, $GPSTime, $APRSStatus, $MsgLine); -my ($GPSLatitudeDegrees, $GPSLatitudeMinutes, $GPSLongitudeDegrees, $GPSCallsign); -my ($GPSLongitudeMinutes, $GPSLatitude, $GPSLongitude, $GPSDistance, $GPSLongitudeMinutes100); -my ($GPSLstBr, $GPSLstBrLat, $GPSLstBrLon); - -my (@gpscomplines, $GPSTempCompPlace, $GPSTempCompDist, $GPSTempCompLine); -my ($GPSTempCompLat, $GPSTempCompLong); -my ($GPSCompPlace, $GPSCompLat, $GPSCompLong, $GPSCompDist); -my ($GPSCompLstBr, $GPSCompLstBrLat, $GPSCompLstBrLon); - -# Added 4.7 -my ($WXTempCompPlace, $WXTempCompDist, $WXTempCompLine); -my ($WXTempCompLat, $WXTempCompLong); -my ($WXCompPlace, $WXCompLat, $WXCompLong, $WXCompDist); -my ($WXCompLstBr, $WXCompLstBrLat, $WXCompLstBrLon); - -my (@namelines, $TempName, $TempNameCall, $TempNameName); - -my (@wxgraphinlines, $WXTempGraphLine, $WXTempGraphTime); -my ($WXTempGraphDOW, $WXTempGraphDaytime, $WXTempGraphDate, $WXTempGraphTemp); -my ($WXTempGraphWindDir, $WXTempGraphWindSpeed, $WXTempGraphHrPrecip); -my ($WXTempGraph24HrPrecip, $WXTempGraphHour, $WXTempGraphMin); -my ($WXTempGraphAMPM); - -my ($i, $j, $k, $CallsignPart, $PacketPart, $GPSSpeed, $GPSCourse, $GPSCourseVoice); -my ($ToCallsignPart, $LastGPSCallsign, $LastGPSDistance, $LastGPSLstBr); -my ($APRSCallsign, $APRSStringLength, $APRSString, $MessageX10Command); -my ($MessageX10Action); -my ($GPSSpeakString3, $MessageAck, $APRSCallsignVoice, $HamCall, $HamName); -my ($WXTime, $WXLatitudeDegrees, $WXLatitudeMinutes, $WXLongitudeDegrees); -my ($WXLongitudeMinutes, $WXLatitude, $WXLongitude, $WXDistance, $WXTemp); -my ($WXLstBr, $WXLstBrLat, $WXLstBrLon); -my ($WXCallsign, $WXWindDir, $WX24HrPrecip, $RealAPRSCallsign); -my ($CurrentTempDist, $WXWindChill); -my ($LastWXCallsign, $LastWXTemp, $LastWXDistance, $LastWXWindDir); -my ($LastWXWindSpeed, $LastWXWindChill, $APRSCallsignNoSSID); -my ($LastWXHrPrecip, $LastWX24HrPrecip, $CurrentHrPrecip, $Current24HrPrecip); - -# Setup TELNET Server 2 (port 14439) to output what the TNC hears - -$server2 = new Socket_Item('#Welcome to MisterHouse APRS Tracking!', 'APRSWelcome', 'server2'); -# Send Welcome Message out port 2 if connected. -set $server2 'APRSWelcome' if active_now $server2; -set $server2 'APRSERVE>APRS:javaTITLE:N0QVC MisterHouse - tracking.pl - Brian Klier, N0QVC' if active_now $server2; - -my $socket_speak_loop; -if (my $telnetdata = said $server2) { - print_log "Data Transmitted from Telnet Port: $telnetdata"; - set $tnc_output $telnetdata; -} - -# TNC Output Lines - -$tnc_output = new Serial_Item ('CONV','converse','serial1'); -$tnc_output -> add ('?WX?','wxquery','serial1'); -$tnc_output -> add (sprintf("=%2d%05.02fN/0%2d%05.02fW- *** %s MisterHouse Tracking System - ICQ#659962 ***", - int($config_parms{latitude}), - abs ($config_parms{latitude} - int($config_parms{latitude}))*60, - int($config_parms{longitude}), - abs ($config_parms{longitude} -int($config_parms{longitude}))*60, - $config_parms{tracking_callsign}), - ,'position','serial1'); - -# Set TNC to Converse and send position on Startup - -if ($Reload) { - $HamCall = $config_parms{tracking_callsign}; # Feed in my Tracking Callsign - open(GPSCOMP, "$config_parms{code_dir}/tracking.pos"); # Open for input - @gpscomplines = ; # Open array and - # read in data - close GPSCOMP; # Close the file - - open(GPSNAME, "$config_parms{code_dir}/tracking.nam"); # Open for input - @namelines = ; # Open array and - # read in data - close GPSNAME; # Close the file -} - -if ($Startup) { - mkdir "$Pgm_Root/web/javAPRS",777 unless -d "$Pgm_Root/web/javAPRS"; - - open(APRSLOG, ">$Pgm_Root/web/javAPRS/aprs.tnc"); # CLEAR Log - close APRSLOG; - -# set $tnc_output pack('C',3); -# set $tnc_output 'MRPT OFF'; # Do NOT Show Digipeater Path -# set $tnc_output 'HEADERLN OFF'; # Keep Header and data on the same line - set $tnc_output 'converse'; - set $tnc_output 'position'; - print_msg "Tracking Interface has been Initialized...Callsign $HamCall"; - print_log "Tracking Interface has been Initialized...Callsign $HamCall"; -} - -# Voice Responses -$v_send_position = new Voice_Cmd("Send my Position"); - -if ($state = said $v_send_position) { - set $tnc_output 'position'; - print_log "Position Sent."; - speak "Position Sent."; -} - -$v_send_status = new Voice_Cmd("Send my Status Report"); - -if ($state = said $v_send_status) { - $APRSStatus = ">Frnt Move $motion_detector_frontdoor->{state}-Bck Move $motion_detector_backdoor->{state}-Kitc Move $motion_detector_kitchen->{state}-Garg Move $motion_detector_garage->{state}-Temp: $CurrentTemp"; - set $tnc_output $APRSStatus; - print_log "Status Sent."; - speak "Status Sent."; -} - -$v_wx_query = new Voice_Cmd("Weather Query"); - -if ($state = said $v_wx_query) { - set $tnc_output 'wxquery'; - print_log "Weather Query Requested."; - speak "Weather Query Requested."; -} - -$v_last_callsign = new Voice_Cmd("Last Callsign"); - -if ($state = said $v_last_callsign and $APRSCallsign ne '') { - print_log "Callsign is $APRSCallsign."; - speak "Call sign is $APRSCallsign."; -} -elsif ($state = said $v_last_callsign and $APRSCallsign eq '') { - print_log "No packets received."; - speak "No packets received."; -} - -$v_last_mobile = new Voice_Cmd("Last Mobile Report"); - -if ($state = said $v_last_mobile and $GPSCallsign ne '') { - print_log "$GPSSpeakString"; - speak $GPSSpeakString; -} -elsif ($state = said $v_last_mobile and $GPSCallsign eq '') { - print_log "No mobile packets received."; - speak "No mobile packets received."; -} - -$v_last_wxrpt = new Voice_Cmd("Last Weather Report"); - -if ($state = said $v_last_wxrpt) { - if ($WXTemp ne '') { - print_log "$WXSpeakString"; - speak $WXSpeakString; - } - elsif ($WXTemp eq '') { - print_log "No weather packets received."; - speak "No weather packets received."; - } -} - -$v_curr_cond = new Voice_Cmd("Current Conditions"); - -if ($state = said $v_curr_cond) { - if ($CurrentTemp ne '') { - if ($CurrentTemp eq $CurrentChill) { - print_log "Temperature is $CurrentTemp degrees."; - speak "Temperature is $CurrentTemp degrees."; - } - if ($CurrentTemp ne $CurrentChill) { - print_log "Temperature is $CurrentTemp degrees. Wind Chill is $CurrentChill degrees."; - speak "Temperature is $CurrentTemp degrees. Winnd Chill is $CurrentChill degrees."; - } - if ($CurrentHrPrecip != 0) { - print_log "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; - speak "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; - } - } - elsif ($CurrentTemp eq '') { - print_log "No weather packets received."; - speak "No weather packets received."; - } -} - -$v_curr_precip = new Voice_Cmd("Current Precipitation"); - -if ($state = said $v_curr_precip) { - if ($CurrentHrPrecip != 0) { - print_log "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; - speak "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; - } - elsif ($CurrentTemp eq '') { - print_log "No weather packets received."; - speak "No weather packets received."; - } - else { - print_log "No rain to report."; - speak "No rain to report."; - } -} - -$v_send_test_email = new Voice_Cmd("Send test email to myself"); - -if ($state = said $v_send_test_email) { - $i = ":EMAIL :$config_parms{net_mail_user}\@$config_parms{net_mail_server} Test E-Mail - " . $CurrentTemp . "deg.{0"; - set $tnc_output $i; -} - -$v_send_test_icq = new Voice_Cmd("Send test ICQ msg to myself"); - -if ($state = said $v_send_test_icq) { - $i = ":ICQSERVE :659962 Test Message - " . $CurrentTemp . " degrees.{1"; - set $tnc_output $i; -} - -# Added 4.7 -$v_register_icqserve = new Voice_Cmd("Register on ICQServe (Do Once)"); - -if ($state = said $v_register_icqserve) { - $i = ":ICQSERVE :REGISTER 659962 {1"; - set $tnc_output $i; -} - -# Procedure to Log Temperature and Stats every 10 minutes - -if (time_cron('0,10,20,30,40,50 * * * *') or $Startup) { - logit("$Pgm_Path/../web/mh/weather.html", "Temp: $CurrentTemp    Wind Chill: $CurrentChill    Wind: $LastWXWindDir/$LastWXWindSpeed    Precipitation: $WXHrPrecip/$WX24HrPrecip
    "); - logit("$Pgm_Path/../data/logs/weather.log", ",$CurrentTemp,$LastWXWindDir,$LastWXWindSpeed,$WXHrPrecip,$WX24HrPrecip"); -} - -# Daily Weather Log Backup - -if (time_cron('0 0 * * *')) { - print_log "Backing up Weather Log."; - open(WXGRAPHIN, ">$Pgm_Path/../data/logs/weather.log"); # CLEAR Log - close WXGRAPHIN; - open(WXGRAPHIN, ">$Pgm_Path/../web/mh/weather.html"); # CLEAR Log - close WXGRAPHIN; - #copy("$Pgm_Path/../data/logs/weather.log", "$Pgm_Path/../data/logs/weather.bak.log") or print_log "Error in copying: $!"; -} - -# Procedure to Make a series of Graphs with Temperature and Stats - -$v_make_graph = new Voice_Cmd("Make Weather Graph Now"); -if (time_cron('15 * * * *') or $Startup or $Reload or $state = said $v_make_graph) { - open(WXGRAPHOUT, ">$Pgm_Path/../web/mh/wxgraph.html"); # Log it - print WXGRAPHOUT "\nWeather Graphs\n"; - print WXGRAPHOUT "\n"; - print WXGRAPHOUT "\n"; - print WXGRAPHOUT "; # Open array and - # read in data - close WXGRAPHIN; # Close the file - - foreach $WXTempGraphLine (@wxgraphinlines) { - ($WXTempGraphDOW, $WXTempGraphDaytime, $WXTempGraphTemp, $WXTempGraphWindDir, $WXTempGraphWindSpeed, $WXTempGraphHrPrecip, $WXTempGraph24HrPrecip) = (split(',', $WXTempGraphLine))[0, 1, 2, 3, 4, 5, 6]; - $WXTempGraphDate = substr($WXTempGraphDaytime, 5, 2); # Day - $WXTempGraphHour = substr($WXTempGraphDaytime, 8, 2); # Hour - $WXTempGraphMin = substr($WXTempGraphDaytime, 11, 2); # Minute - $WXTempGraphAMPM = substr($WXTempGraphDaytime, 14, 1); # A/P - - if ($WXTempGraphAMPM eq 'A' and $WXTempGraphHour eq '12') {$WXTempGraphHour = '0'}; - if ($WXTempGraphAMPM eq 'P' and $WXTempGraphHour ne '12') {$WXTempGraphHour = $WXTempGraphHour + 12}; - $WXTempGraphMin = $WXTempGraphMin / 60; - $WXTempGraphTime = $WXTempGraphHour + $WXTempGraphMin; - - if ($WXTempGraphTemp ne '') { # If the Temp isn't blank, - print WXGRAPHOUT "$WXTempGraphTime,$WXTempGraphTemp "; - } - } - - print WXGRAPHOUT "\">\n\n"; - print WXGRAPHOUT "\n"; - print WXGRAPHOUT "\n"; - print WXGRAPHOUT "; # Open array and - # read in data - close WXGRAPHIN; # Close the file - - foreach $WXTempGraphLine (@wxgraphinlines) { - ($WXTempGraphDOW, $WXTempGraphDaytime, $WXTempGraphTemp, $WXTempGraphWindDir, $WXTempGraphWindSpeed, $WXTempGraphHrPrecip, $WXTempGraph24HrPrecip) = (split(',', $WXTempGraphLine))[0, 1, 2, 3, 4, 5, 6]; - $WXTempGraphDate = substr($WXTempGraphDaytime, 5, 2); # Day - $WXTempGraphHour = substr($WXTempGraphDaytime, 8, 2); # Hour - $WXTempGraphMin = substr($WXTempGraphDaytime, 11, 2); # Minute - $WXTempGraphAMPM = substr($WXTempGraphDaytime, 14, 1); # A/P - - if ($WXTempGraphAMPM eq 'A' and $WXTempGraphHour eq '12') {$WXTempGraphHour = '0'}; - if ($WXTempGraphAMPM eq 'P' and $WXTempGraphHour ne '12') {$WXTempGraphHour = $WXTempGraphHour + 12}; - $WXTempGraphMin = $WXTempGraphMin / 60; - $WXTempGraphTime = $WXTempGraphHour + $WXTempGraphMin; - - if ($WXTempGraphWindSpeed ne '') { # If the Speed isn't blank, - print WXGRAPHOUT "$WXTempGraphTime,$WXTempGraphWindSpeed "; - } - } - - print WXGRAPHOUT "\">\n\n"; - close WXGRAPHOUT; -} - -# Procedure to occasionally send out APRS Position Report and Status String - -if (time_cron('0,30 * * * *')) { - set $tnc_output 'converse'; - set $tnc_output 'position'; - $APRSStatus = ">Frnt Move $motion_detector_frontdoor->{state}-Bck Move $motion_detector_backdoor->{state}-Kitc Move $motion_detector_kitchen->{state}-Garg Move $motion_detector_garage->{state}-Temp: $CurrentTemp"; - set $tnc_output $APRSStatus; -} - -# Main TNC Parse Procedure - -if ($APRSString = said $tnc_output) { - - open(APRSLOG, ">>$Pgm_Root/web/javAPRS/aprs.tnc"); # Log it - print APRSLOG "$APRSString\n"; - close APRSLOG; - - print_msg "TRACK: $APRSString"; # Monitor to Msg Window - - $APRSFoundAPRS = ""; # Reset Found Flag - # $APRSPacketDigi = ""; - $APRSStringLength = (length($APRSString)); # Save Length of Ser - - # Send the packet out TELNET if connected... - set $server2 $APRSString if active $server2; - - # Decode the Callsign and different parts from the Packet - - ($CallsignPart, $MsgLine) = (split('::', $APRSString))[0, 1]; #New Line for Msging - ($CallsignPart, $PacketPart) = (split(':', $APRSString))[0, 1]; - ($APRSCallsign, $ToCallsignPart) = (split('>', $CallsignPart))[0, 1]; - ($APRSCallsignNoSSID, $j) = (split('-', $APRSCallsign))[0, 1]; - - # Save the APRS Callsign for Message Reading Procedure (non-spaced) - - $RealAPRSCallsign = $APRSCallsign; - - # Make APRS Callsign so it is spoken properly - - $j = ''; - for ($i = 0; $i != (length($APRSCallsign)); ++$i) { - $j = $j . substr($APRSCallsign, $i, 1); - $j = $j . " "; - } - $APRSCallsign = $j; - - # MAIN LOOP - - if ($APRSFoundAPRS != 1) { - # If it's a $GPRMC, $GPGGA, or Mic-E statement for GPS, - if ((substr($PacketPart, 0, 6) eq '$GPRMC') || - (substr($PacketPart, 0, 6) eq '$GPGGA') || - (substr($PacketPart, 0, 1) eq '`') || - (substr($PacketPart, 0, 1) eq "'")) { - $APRSFoundAPRS = 1; # Found an APRS String - $GPSCallsign = $APRSCallsign; # Reset Variables - $GPSTime = ""; - $GPSLatitudeDegrees = ""; - $GPSLatitudeMinutes = ""; - $GPSLongitudeDegrees = ""; - $GPSLongitudeMinutes = ""; - $GPSLatitude = ""; - $GPSLongitude = ""; - $GPSDistance = ""; - $GPSSpeed = ""; - $GPSCourse = ""; - $GPSCourseVoice = ""; - $GPSLstBr = ""; - $GPSLstBrLat = ""; - $GPSLstBrLon = ""; - $GPSCompPlace = ""; - $GPSCompLat = ""; - $GPSCompLong = ""; - $GPSCompDist = "9999"; - $GPSSpeakString2 = ""; - $GPSTempCompPlace = ""; - $GPSTempCompDist = ""; - - # Find out the user defined "name" for this callsign. - - $j = '0'; - - foreach $TempName (@namelines) { - if ($j eq '0') { - ($TempNameCall, $TempNameName) = (split(',', $TempName))[0, 1]; - if ($TempNameCall eq $APRSCallsignNoSSID) { - $HamName = $TempNameName; - chomp $HamName; - $j = '1'; - } - } - } - - # If there is NO defined name, make $HamName equal to the callsign - - if ($j eq '0') { - $HamName = $APRSCallsign; - print_msg "$APRSString -> No callsign Found\n"; - } - - - if (substr($PacketPart, 0, 6) eq '$GPRMC') { - ($GPSTime, $GPSLatitude, $GPSLongitude, $GPSSpeed, $GPSCourse) = (split(',', $PacketPart))[1, 3, 5, 7, 8]; - } - - if (substr($PacketPart, 0, 6) eq '$GPGGA') { - ($GPSTime, $GPSLatitude, $GPSLongitude) = (split(',', $PacketPart))[1, 2, 4]; - } - - if ((substr($PacketPart, 0, 6) eq '$GPRMC') || - (substr($PacketPart, 0, 6) eq '$GPGGA')) { - - $GPSTime = (substr($GPSTime, 0, 6)); # Get the GPS Time - $GPSLatitudeDegrees = (substr($GPSLatitude, 0, 2)); - $GPSLatitudeMinutes = (substr($GPSLatitude, 2, 8)); - $GPSLatitude = ($GPSLatitudeDegrees + ($GPSLatitudeMinutes / 60)); - $GPSLongitudeDegrees = (substr($GPSLongitude, 0, 3)); - $GPSLongitudeMinutes = (substr($GPSLongitude, 3, 8)); - $GPSLongitude = ($GPSLongitudeDegrees + ($GPSLongitudeMinutes / 60)); - - # Convert the GPS Speed to MPH - $GPSSpeed = ($GPSSpeed * 1.853248) / 1.609344; - } - - if ((substr($PacketPart, 0, 1) eq '`') || # If a Mic-E, - (substr($PacketPart, 0, 1) eq "'")) { - - $GPSLongitudeDegrees = (substr($PacketPart, 1, 1)); - $GPSLongitudeDegrees = (unpack('C', $GPSLongitudeDegrees)) - 28; - if ($GPSLongitudeDegrees >= 180 and $GPSLongitudeDegrees <= 189) {$GPSLongitudeDegrees = $GPSLongitudeDegrees - 80}; - if ($GPSLongitudeDegrees >= 190 and $GPSLongitudeDegrees <= 199) {$GPSLongitudeDegrees = $GPSLongitudeDegrees - 190}; - - $GPSLongitudeMinutes = (substr($PacketPart, 2, 1)); - $GPSLongitudeMinutes = (unpack('C', $GPSLongitudeMinutes)) - 28; - if ($GPSLongitudeMinutes > 60) {$GPSLongitudeMinutes = $GPSLongitudeMinutes - 60}; - - # Added Lines from Roger - $GPSLongitudeMinutes100 = (substr($PacketPart, 3, 1)); - $GPSLongitudeMinutes100 = (unpack('C', $GPSLongitudeMinutes100)) - 28; - # - - $GPSLongitude = ($GPSLongitudeDegrees + ($GPSLongitudeMinutes / 60)+ ($GPSLongitudeMinutes100 / 6000)); - # old -> $GPSLongitude = ($GPSLongitudeDegrees + ($GPSLongitudeMinutes / 60)); - - $GPSSpeed = (substr($PacketPart, 4, 1)); - $GPSSpeed = ((unpack('C', $GPSSpeed)) - 28) * 10; - # $CallsignPart simply used as a temp variable - $CallsignPart = (substr($PacketPart, 5, 1)); - $GPSSpeed = (((unpack('C', $CallsignPart)) - 28) / 10) + $GPSSpeed; - - $GPSCourse = ((unpack('C', $CallsignPart)) - 28) % 10; - # $CallsignPart simply used as a temp variable - $CallsignPart = (substr($PacketPart, 6, 1)); - $GPSCourse = ((unpack('C', $CallsignPart)) - 28) + $GPSCourse; - - # Last minute course and speed adjustments per specs - if ($GPSSpeed >= 800) {$GPSSpeed = $GPSSpeed - 800}; - if ($GPSCourse >= 400) {$GPSCourse = $GPSCourse - 400}; - - # Convert the GPS Speed to MPH - $GPSSpeed = ($GPSSpeed * 1.853248) / 1.609344; - - # Truncate Course to max of 3 numbers for parsing below - $GPSCourse = substr($GPSCourse, 0, 3); - - # Round the Speed to the nearest integer - $GPSSpeed = round($GPSSpeed); - - # Load the tens digit of Degrees Latitude - $GPSLatitudeDegrees = (substr($ToCallsignPart, 0, 1)); - $GPSLatitudeDegrees = (unpack('C', $GPSLatitudeDegrees)) - 32; - $GPSLatitudeDegrees = ($GPSLatitudeDegrees & 15) * 10; - - # Load the ones digit of Degrees Latitude (temp variable used) - $GPSDistance = (substr($ToCallsignPart, 1, 1)); - $GPSDistance = (unpack('C', $GPSDistance)) - 32; - $GPSDistance = ($GPSDistance & 15); - - # Here's our Degrees Latitude - $GPSLatitudeDegrees = $GPSLatitudeDegrees + $GPSDistance; - - # Load the tens digit of Minutes Latitude - $GPSLatitudeMinutes = (substr($ToCallsignPart, 2, 1)); - $GPSLatitudeMinutes = (unpack('C', $GPSLatitudeMinutes)) - 32; - $GPSLatitudeMinutes = ($GPSLatitudeMinutes & 15) * 10; - - # Load the ones digit of Minutes Latitude (temp variable used) - $GPSDistance = (substr($ToCallsignPart, 3, 1)); - $GPSDistance = (unpack('C', $GPSDistance)) - 32; - $GPSDistance = ($GPSDistance & 15); - - # Here's our Minutes Latitude - $GPSLatitudeMinutes = $GPSLatitudeMinutes + $GPSDistance; - - # Load the tens digit of hundreds of Minutes Latitude - # $CallsignPart simply used as a temp variable - $CallsignPart = (substr($ToCallsignPart, 4, 1)); - $CallsignPart = (unpack('C', $CallsignPart)) - 32; - $CallsignPart = ($CallsignPart & 15) * 10; - - # Load the ones digit of hundreds of Minutes Latitude - # (temp variable used) - $GPSDistance = (substr($ToCallsignPart, 5, 1)); - $GPSDistance = (unpack('C', $GPSDistance)) - 32; - $GPSDistance = ($GPSDistance & 15); - - # Here's our hundreds of Minutes Latitude - $CallsignPart = $CallsignPart + $GPSDistance; - - $GPSLatitude = ($GPSLatitudeDegrees + ($GPSLatitudeMinutes / 60) + ($CallsignPart / 6000)); - } - - # --- Do the following for all received GPS Strings - - # Calculate distance station is away - $GPSDistance = &great_circle_distance($GPSLatitude, $GPSLongitude, $config_parms{latitude}, $config_parms{longitude}); - #$GPSDistance = (sin $GPSLatitude) * (sin $config_parms{latitude}) + (cos $GPSLatitude) * (cos $config_parms{latitude}) * (cos ($config_parms{longitude}-$GPSLongitude)); - #$GPSDistance = 1.852 * 60 * atan2(sqrt(1 - $GPSDistance * $GPSDistance), $GPSDistance); - #$GPSDistance = $GPSDistance / 1.6093440; - $GPSDistance = round($GPSDistance, 1); - - # Calculate bearing from the Position file - foreach $GPSTempCompLine (@gpscomplines) { - ($GPSTempCompPlace, $GPSTempCompLat, $GPSTempCompLong) = (split(',', $GPSTempCompLine))[0, 1, 2]; - - # Calculate distance station is away from pos file - $GPSTempCompDist = &great_circle_distance($GPSLatitude, $GPSLongitude, $GPSTempCompLat, $GPSTempCompLong); - #$GPSTempCompDist = (sin $GPSLatitude) * (sin $GPSTempCompLat) + (cos $GPSLatitude) * (cos $GPSTempCompLat) * (cos ($GPSTempCompLong-$GPSLongitude)); - #$GPSTempCompDist = 1.852 * 60 * atan2(sqrt(1 - $GPSTempCompDist * $GPSTempCompDist), $GPSTempCompDist); - #$GPSTempCompDist = $GPSTempCompDist / 1.6093440; - $GPSTempCompDist = round($GPSTempCompDist, 1); - - if ($GPSTempCompDist < 15 and $GPSTempCompDist < $GPSCompDist) { - $GPSCompPlace = $GPSTempCompPlace; - $GPSCompDist = $GPSTempCompDist; - $GPSCompLat = $GPSTempCompLat; - $GPSCompLong = $GPSTempCompLong; - } - } - - # Calculate if station is north/west/east/south of POSFILE - $GPSCompLstBrLat = ($GPSCompLat - $GPSLatitude); - $GPSCompLstBrLon = ($GPSCompLong - $GPSLongitude); - if ($GPSCompLstBrLat < 0 and $GPSCompLstBrLon < 0) {$GPSCompLstBr = 'northwest'}; - if ($GPSCompLstBrLat > 0 and $GPSCompLstBrLon < 0) {$GPSCompLstBr = 'southwest'}; - if ($GPSCompLstBrLat < 0 and $GPSCompLstBrLon > 0) {$GPSCompLstBr = 'northeast'}; - if ($GPSCompLstBrLat > 0 and $GPSCompLstBrLon > 0) {$GPSCompLstBr = 'southeast'}; - if ($GPSCompLstBrLat <= 0 and (abs($GPSCompLstBrLon) * 2) < abs($GPSCompLstBrLat)) {$GPSCompLstBr = 'north'}; - if ($GPSCompLstBrLat >= 0 and (abs($GPSCompLstBrLon) * 2) < abs($GPSCompLstBrLat)) {$GPSCompLstBr = 'south'}; - if ($GPSCompLstBrLon <= 0 and (abs($GPSCompLstBrLat) * 2) < abs($GPSCompLstBrLon)) {$GPSCompLstBr = 'west'}; - if ($GPSCompLstBrLon >= 0 and (abs($GPSCompLstBrLat) * 2) < abs($GPSCompLstBrLon)) {$GPSCompLstBr = 'east'}; - - # Calculate if station is north/west/east/south of ours - $GPSLstBrLat = ($config_parms{latitude} - $GPSLatitude); - $GPSLstBrLon = ($config_parms{longitude} - $GPSLongitude); - if ($GPSLstBrLat < 0 and $GPSLstBrLon < 0) {$GPSLstBr = 'northwest'}; - if ($GPSLstBrLat > 0 and $GPSLstBrLon < 0) {$GPSLstBr = 'southwest'}; - if ($GPSLstBrLat < 0 and $GPSLstBrLon > 0) {$GPSLstBr = 'northeast'}; - if ($GPSLstBrLat > 0 and $GPSLstBrLon > 0) {$GPSLstBr = 'southeast'}; - if ($GPSLstBrLat <= 0 and (abs($GPSLstBrLon) * 2) < abs($GPSLstBrLat)) {$GPSLstBr = 'north'}; - if ($GPSLstBrLat >= 0 and (abs($GPSLstBrLon) * 2) < abs($GPSLstBrLat)) {$GPSLstBr = 'south'}; - if ($GPSLstBrLon <= 0 and (abs($GPSLstBrLat) * 2) < abs($GPSLstBrLon)) {$GPSLstBr = 'west'}; - if ($GPSLstBrLon >= 0 and (abs($GPSLstBrLat) * 2) < abs($GPSLstBrLon)) {$GPSLstBr = 'east'}; - - # Add bearing from station in position file IF it's a new position report - if ((($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) and ($GPSCompDist ne '9999')) { - $GPSSpeakString2 = "Currently $GPSCompDist miles $GPSCompLstBr of $GPSCompPlace."; - # and form a special speak string just for on the air - $GPSSpeakString3 = ">$RealAPRSCallsign $GPSCompDist mi $GPSCompLstBr of $GPSCompPlace."; - -### # Added in 4.9 - if ($GPSDistance <= 0.2) { - $GPSSpeakString2 = "Currently near $GPSCompPlace."; - $GPSSpeakString3 = ">$RealAPRSCallsign near $GPSCompPlace."; - } - if ((substr($PacketPart, 0, 6) eq '$GPRMC') and - ($GPSDistance <= 0.1) and - ($GPSSpeed <= 1)) { - $GPSSpeakString2 = "Currently parked at $GPSCompPlace."; - $GPSSpeakString3 = ">$RealAPRSCallsign parked at $GPSCompPlace."; - } - if ((substr($PacketPart, 0, 6) eq '$GPGGA') and - ($GPSDistance <= 0.1)) { - $GPSSpeakString2 = "Currently at $GPSCompPlace."; - $GPSSpeakString3 = ">$RealAPRSCallsign at $GPSCompPlace."; - } -## - - set $tnc_output $GPSSpeakString3; - } - - # If It's a $GPRMC or Mic-E String, - - if ((substr($PacketPart, 0, 6) eq '$GPRMC') || - (substr($PacketPart, 0, 1) eq '`')) { - - # Only Calculate Course & Speed if it's a $GPRMC string, - if (substr($PacketPart, 0, 6) eq '$GPRMC') { - - # Truncate Course to max of 3 numbers for parsing below - $GPSCourse = substr($GPSCourse, 0, 3); - - # Round the Speed to the nearest integer - $GPSSpeed = round($GPSSpeed); - } - - $GPSCourseVoice = "north" if ($GPSCourse >= 0 and $GPSCourse <= 11); - $GPSCourseVoice = "north-northeast" if ($GPSCourse >= 12 and $GPSCourse <= 33); - $GPSCourseVoice = "northeast" if ($GPSCourse >= 34 and $GPSCourse <= 55); - $GPSCourseVoice = "east-northeast" if ($GPSCourse >= 56 and $GPSCourse <= 77); - $GPSCourseVoice = "east" if ($GPSCourse >= 78 and $GPSCourse <= 99); - $GPSCourseVoice = "east-southeast" if ($GPSCourse >= 100 and $GPSCourse <= 121); - $GPSCourseVoice = "southeast" if ($GPSCourse >= 122 and $GPSCourse <= 143); - $GPSCourseVoice = "south-southeast" if ($GPSCourse >= 144 and $GPSCourse <= 165); - $GPSCourseVoice = "south" if ($GPSCourse >= 166 and $GPSCourse <= 187); - $GPSCourseVoice = "south-southwest" if ($GPSCourse >= 188 and $GPSCourse <= 209); - $GPSCourseVoice = "southwest" if ($GPSCourse >= 210 and $GPSCourse <= 231); - $GPSCourseVoice = "west-southwest" if ($GPSCourse >= 232 and $GPSCourse <= 253); - $GPSCourseVoice = "west" if ($GPSCourse >= 254 and $GPSCourse <= 275); - $GPSCourseVoice = "west-northwest" if ($GPSCourse >= 276 and $GPSCourse <= 297); - $GPSCourseVoice = "northwest" if ($GPSCourse >= 298 and $GPSCourse <= 319); - $GPSCourseVoice = "north-northwest" if ($GPSCourse >= 320 and $GPSCourse <= 341); - $GPSCourseVoice = "north" if ($GPSCourse >= 342 and $GPSCourse <= 360); - - if ($config_parms{tracking_withname} == 1) - {$GPSCallsign = $HamName}; - - # If It's not the same as the last report, say it. - - if ((($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) and ($GPSSpeed ne '0')) { - - if ($config_parms{tracking_shortannounce} == 0) - {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us, heading $GPSCourseVoice at $GPSSpeed miles an hour. $GPSSpeakString2"}; - - if ($config_parms{tracking_shortannounce} == 1) { - $GPSSpeakString = "$GPSCallsign is $GPSSpeakString2"; - if ($GPSSpeakString2 eq '') {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us, heading $GPSCourseVoice at $GPSSpeed miles an hour."}; - } - - print_log "$GPSSpeakString"; - - if (($config_parms{tracking_speakflag} == 1) || - ($config_parms{tracking_speakflag} == 3)) - {speak $GPSSpeakString}; - - if ($config_parms{tracking_trackself} == 1 and - $APRSCallsignNoSSID eq $HamCall) - {speak $GPSSpeakString}; - } - - # If the GPS is Stationary, say the following. - - if ((($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) and ($GPSSpeed eq '0')) { - - if ($config_parms{tracking_shortannounce} == 0) - {$GPSSpeakString = "$GPSCallsign is parked $GPSDistance miles $GPSLstBr of us. $GPSSpeakString2"}; - - if ($config_parms{tracking_shortannounce} == 1) { - $GPSSpeakString = "$GPSCallsign is $GPSSpeakString2"; - if ($GPSSpeakString2 eq '') {$GPSSpeakString = "$GPSCallsign is parked $GPSDistance miles $GPSLstBr of us."}; - } - - print_log "$GPSSpeakString"; - - if (($config_parms{tracking_speakflag} == 1) || - ($config_parms{tracking_speakflag} == 3)) - {speak $GPSSpeakString}; - - if ($config_parms{tracking_trackself} == 1 and - $APRSCallsignNoSSID eq $HamCall) - {speak $GPSSpeakString}; - } - } - - # If It's a $GPGGA String, - - if (substr($PacketPart, 0, 6) eq '$GPGGA') { - - if ($config_parms{tracking_withname} == 1) - {$GPSCallsign = $HamName}; - - if (($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) { - - if ($config_parms{tracking_shortannounce} == 0) - {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us. $GPSSpeakString2"}; - - if ($config_parms{tracking_shortannounce} == 1) { - $GPSSpeakString = "$GPSCallsign is $GPSSpeakString2"; - if ($GPSSpeakString2 eq '') {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us."}; - } - - print_log "$GPSSpeakString"; - - if (($config_parms{tracking_speakflag} == 1) || - ($config_parms{tracking_speakflag} == 3)) - {speak $GPSSpeakString}; - - if ($config_parms{tracking_trackself} == 1 and - $APRSCallsignNoSSID eq $HamCall) - {speak $GPSSpeakString}; - } - } - - $LastGPSCallsign = $GPSCallsign; # Save last GPS rpt - $LastGPSDistance = $GPSDistance; - $LastGPSLstBr = $GPSLstBr; - - # NEW IN 4.8 - # Prototype Log File Procedure for Tracking - - $i = -$GPSLongitude; - $j = $GPSLatitude; - - my $html = qq[
    \n]; - $html .= qq[\n|; - $html .= qq[\n\n]; - - #$k = qq[
  • $Date_Now $Time_Now: ]; - #$k .= qq[\n$GPSSpeakString\n\n]; - logit "$config_parms{html_dir}/mh/tracking/today.html", $html, 0; - logit "$config_parms{html_dir}/mh/tracking/week1.html", $html, 0; - - if ($New_Day) { - open(NEWDAY, ">$config_parms{html_dir}/mh/tracking/today.html"); - close NEWDAY; - my $html = qq[\n
  • Network DeviceStatusControl ONControl OFF$Date_Now $Time_Now$GPSSpeakString$GPSSpeakString2\n]; - $i = -$i; # For logging form data in .pos - $html .= qq[
    \n]; - $html .= qq[\n]; - logit "$config_parms{html_dir}/mh/tracking/today.html", $html, 0, 1; - logit "$config_parms{html_dir}/mh/tracking/week1.html", "
    \n", 0, 1; - } - - if ($New_Week) { - open(NEWWEEK, ">$config_parms{html_dir}/mh/tracking/week1.html"); - close NEWWEEK; - #file_cat "$config_parms{html_dir}/mh/tracking/week2.html", "$config_parms{html_dir}/mh/tracking/old/${Year_Month_Now}.html"; - #rename "$config_parms{html_dir}/mh/tracking/week1.html", "$config_parms{html_dir}/mh/tracking/week2.html" or print_log "Error in aprs rename 2: $!"; - my $html = qq[\n
    Date TimeVehicle Heading and SpeedLocationNew Location
    \n]; - $html .= qq[\n]; - logit "$config_parms{html_dir}/mh/tracking/week1.html", $html, 1; - } - - # Add an index entry for the new months entry in aprs/old - - #if ($New_Month) { - # my $html = qq[
  • $Year_Month_Now.html\n]; - # logit "$config_parms{html_dir}/mh/tracking/old/index.html", $html, 1; - #} - } # **END** GPS Parse - - # Send E-Mail from APRS messages with "EMAIL2" - - if (substr($MsgLine, 0, 6) eq 'EMAIL2') { - $APRSFoundAPRS = 1; - - ($MsgLine, $MessageAck) = (split('{', $MsgLine))[0, 1]; - ($CallsignPart, $PacketPart) = (split(':', $MsgLine))[0, 1]; - ($CallsignPart, $PacketPart) = (split(' ', $PacketPart))[0, 1]; - - # Let $i equals the number of spaces to put before :ack - $i = (9 - length($RealAPRSCallsign)); - $k = ' '; - $k = ($k x $i); - - print_log "Email gateway: Callsign=$RealAPRSCallsign, to=$CallsignPart data=$PacketPart\n"; - - # Send the mail!! - #if (&net_connect_check) { - $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; - set $tnc_output $i; - &net_mail_send(to => $CallsignPart, subject => "APRS Gateway", - text => "From $HamCall APRS Gateway\n$PacketPart"); - $i = ":" . $RealAPRSCallsign . $k . ":Your E-Mail Message has been sent.{7"; - set $tnc_output $i; - #} - #else { - # $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; - # set $tnc_output $i; - # $i = ":" . $RealAPRSCallsign . $k . ":Sorry, Gateway is currently closed.{8"; - # set $tnc_output $i; - #} - } - - # Speak any incoming APRS Bulletins - - if (substr($MsgLine, 0, 3) eq 'BLN') { - $APRSFoundAPRS = 1; - -# ($CallsignPart, $PacketPart) = (split(':', $MsgLine))[0, 1]; - ($CallsignPart, $CallsignPart, $PacketPart) = (split(':', $APRSString))[0, 1, 2]; - print_log "Incoming Bulletin from $APRSCallsign: $PacketPart"; - ## REMMED THIS NEXT STATEMENT OUT FOR SANITY - #speak "Incoming Bulletin from $APRSCallsign. $PacketPart"; - } - - # If It's an APRS Message, either say it or process the voice command: - - if (substr($MsgLine, 0, length($HamCall)) eq $HamCall) { - $APRSFoundAPRS = 1; - - ($MsgLine, $MessageAck) = (split('{', $MsgLine))[0, 1]; - ($CallsignPart, $PacketPart) = (split(':', $MsgLine))[0, 1]; - - # Let $i equals the number of spaces to put before :ack - $i = (9 - length($RealAPRSCallsign)); - $k = ' '; - $k = ($k x $i); - - # Check to see if it is a voice command to process from our CALLSIGN: - - if (substr($PacketPart, 0, 4) eq 'X10-' and - substr($RealAPRSCallsign, 0, length($HamCall)) eq $HamCall) { - - # Split the line so $PacketPart is actually the message received to process. - ($CallsignPart, $PacketPart) = (split('-', $PacketPart))[0, 1]; - - run_voice_cmd $PacketPart; - print_log "X10 received from APRS: $PacketPart"; - speak "X10 received from A P R S: $PacketPart"; - - $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; - set $tnc_output $i; - $i = ":" . $RealAPRSCallsign . $k . ":X-10 Message Received.{9"; - set $tnc_output $i; - $MsgLine = ""; - } - - # NEW in 4.52 - Check to see if its an ack. If so, don't speak it. - - elsif (substr($PacketPart, 0, 3) eq 'ack') { - print_log "Acknowledgement received from $RealAPRSCallsign"; - } - - # NEW in 4.62 - Respond to ?WX? requests with the temperature. - - elsif (substr($PacketPart, 0, 4) eq '?WX?') { - $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; - set $tnc_output $i; - $i = ":" . $RealAPRSCallsign . $k . ": Current Temperature: $CurrentTemp.{4"; - set $tnc_output $i; - $MsgLine = ""; - } - - # NEW in 4.62 - Respond to ?PHONE? requests with last call. - - elsif (substr($PacketPart, 0, 7) eq '?PHONE?') { - $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; - set $tnc_output $i; - $i = ":" . $RealAPRSCallsign . $k . ": Last Call: $PhoneName ($DisplayPhoneNumber){6"; - set $tnc_output $i; - $MsgLine = ""; - } - - # If it's not a voice command, than assume it's a standard message: - - else { - print_log "Incoming Message from $APRSCallsign: $PacketPart"; - #speak "Incoming Message from $APRSCallsign. $PacketPart"; - # THIS IS A STATUS PAGE EVENT - #if (time_greater_than("22:00") and time_less_than("15:00")) { - #$page_icq = "$PacketPart"; - #} - - $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; - set $tnc_output $i; - set $tnc_output $i; - #$i = ":" . $RealAPRSCallsign . $k . ":Message Received.{2"; - #set $tnc_output $i; - - $MsgLine = ""; - } - } - - # If it's a U2k or UII Weather Station, - # AA0SM>APRSW,N0EST,WIDE*,WIDE:_02050122c168s005g010t011r000p000P000h91b10224wU2K - - if ((substr($APRSString, ($APRSStringLength - 4), 2) eq 'dU') - || (substr($APRSString, ($APRSStringLength - 6), 6) eq 'dU2kFM') - || (substr($APRSString, ($APRSStringLength - 4), 2) eq 'wU')) { - $APRSFoundAPRS = 1; - $WXCallsign = $APRSCallsign; # Reset Variables - $WXTime = ""; - $WXLatitudeDegrees = ""; - $WXLatitudeMinutes = ""; - $WXLongitudeDegrees = ""; - $WXLongitudeMinutes = ""; - $WXLatitude = ""; - $WXLongitude = ""; - $WXDistance = ""; - $WXTemp = ""; - $WXWindDir = ""; - $WXWindSpeed = ""; - $WXWindChill = ""; - $WXHrPrecip = ""; - $WX24HrPrecip = ""; - # Added in 4.7 - $WXLstBr = ""; - $WXLstBrLat = ""; - $WXLstBrLon = ""; - $WXCompPlace = ""; - $WXCompLat = ""; - $WXCompLong = ""; - $WXCompDist = "9999"; - $WXSpeakString2 = ""; - $WXTempCompPlace = ""; - $WXTempCompDist = ""; - - # If It's a DOS Weather String, - - if ((substr($APRSString, ($APRSStringLength - 4), 2) eq 'dU') - || (substr($APRSString, ($APRSStringLength - 6), 6) eq 'dU2kFM')) { - - $WXTime = (substr($PacketPart, 3, 4)); # Time of WX Report - $WXWindDir = (substr($PacketPart, 27, 3)); # Wind Direction - $WXWindSpeed = (substr($PacketPart, 31, 3)); # Wind Speed - - # Get rid of those damn 0's in the Speed - if (substr($WXWindSpeed, 0, 2) eq '00') {$WXWindSpeed = (substr($WXWindSpeed, 2, 1))}; - if ($WXWindSpeed ne '0') { # Except if wind speed IS 0 - if (substr($WXWindSpeed, 0, 1) eq '0') {$WXWindSpeed = (substr($WXWindSpeed, 1, 2))}; - } - - $WXLatitudeDegrees = (substr($PacketPart, 8, 2)); - $WXLatitudeMinutes = (substr($PacketPart, 10, 5)); - $WXLatitude = ($WXLatitudeDegrees + ($WXLatitudeMinutes / 60)); - $WXLongitudeDegrees = (substr($PacketPart, 17, 3)); - $WXLongitudeMinutes = (substr($PacketPart, 20, 5)); - $WXLongitude = ($WXLongitudeDegrees + ($WXLongitudeMinutes / 60)); - - $WXDistance = &great_circle_distance($WXLatitude, $WXLongitude, $config_parms{latitude}, $config_parms{longitude}); - #$WXDistance = (sin $WXLatitude) * (sin $config_parms{latitude}) + (cos $WXLatitude) * (cos $config_parms{latitude}) * (cos ($config_parms{longitude}-$WXLongitude)); - #$WXDistance = 1.852 * 60 * atan2(sqrt(1 - $WXDistance * $WXDistance), $WXDistance); - #$WXDistance = $WXDistance / 1.6093440; - $WXDistance = round($WXDistance, 1); - - # Calculate if station is north/west/east/south of ours - $WXLstBrLat = ($config_parms{latitude} - $WXLatitude); - $WXLstBrLon = ($config_parms{longitude} - $WXLongitude); - if ($WXLstBrLat < 0 and $WXLstBrLon < 0) {$WXLstBr = 'northwest'}; - if ($WXLstBrLat > 0 and $WXLstBrLon < 0) {$WXLstBr = 'southwest'}; - if ($WXLstBrLat < 0 and $WXLstBrLon > 0) {$WXLstBr = 'northeast'}; - if ($WXLstBrLat > 0 and $WXLstBrLon > 0) {$WXLstBr = 'southeast'}; - if ($WXLstBrLat <= 0 and (abs($WXLstBrLon) * 2) < abs($WXLstBrLat)) {$WXLstBr = 'north'}; - if ($WXLstBrLat >= 0 and (abs($WXLstBrLon) * 2) < abs($WXLstBrLat)) {$WXLstBr = 'south'}; - if ($WXLstBrLon <= 0 and (abs($WXLstBrLat) * 2) < abs($WXLstBrLon)) {$WXLstBr = 'west'}; - if ($WXLstBrLon >= 0 and (abs($WXLstBrLat) * 2) < abs($WXLstBrLon)) {$WXLstBr = 'east'}; - } - - # For New Windows UII/U2000 String Only - - if (substr($APRSString, ($APRSStringLength - 4), 2) eq 'wU') { - $WXTime = (substr($PacketPart, 5, 4)); # Time of WX Report - $WXWindDir = (substr($PacketPart, 10, 3)); # Wind Direction - $WXWindSpeed = (substr($PacketPart, 14, 3)); # Wind Speed - - # Get rid of those damn 0's in the Speed - if (substr($WXWindSpeed, 0, 2) eq '00') {$WXWindSpeed = (substr($WXWindSpeed, 2, 1))}; - if ($WXWindSpeed ne '0') { # Except if wind speed IS 0 - if (substr($WXWindSpeed, 0, 1) eq '0') {$WXWindSpeed = (substr($WXWindSpeed, 1, 2))}; - } - } - - # All Weather Stations Process the Following - - $WXWindDirVoice = "north" if ($WXWindDir >= 0 and $WXWindDir <= 11); - $WXWindDirVoice = "north-northeast" if ($WXWindDir >= 12 and $WXWindDir <= 33); - $WXWindDirVoice = "northeast" if ($WXWindDir >= 34 and $WXWindDir <= 55); - $WXWindDirVoice = "east-northeast" if ($WXWindDir >= 56 and $WXWindDir <= 77); - $WXWindDirVoice = "east" if ($WXWindDir >= 78 and $WXWindDir <= 99); - $WXWindDirVoice = "east-southeast" if ($WXWindDir >= 100 and $WXWindDir <= 121); - $WXWindDirVoice = "southeast" if ($WXWindDir >= 122 and $WXWindDir <= 143); - $WXWindDirVoice = "south-southeast" if ($WXWindDir >= 144 and $WXWindDir <= 165); - $WXWindDirVoice = "south" if ($WXWindDir >= 166 and $WXWindDir <= 187); - $WXWindDirVoice = "south-southwest" if ($WXWindDir >= 188 and $WXWindDir <= 209); - $WXWindDirVoice = "southwest" if ($WXWindDir >= 210 and $WXWindDir <= 231); - $WXWindDirVoice = "west-southwest" if ($WXWindDir >= 232 and $WXWindDir <= 253); - $WXWindDirVoice = "west" if ($WXWindDir >= 254 and $WXWindDir <= 275); - $WXWindDirVoice = "west-northwest" if ($WXWindDir >= 276 and $WXWindDir <= 297); - $WXWindDirVoice = "northwest" if ($WXWindDir >= 298 and $WXWindDir <= 319); - $WXWindDirVoice = "north-northwest" if ($WXWindDir >= 320 and $WXWindDir <= 341); - $WXWindDirVoice = "north" if ($WXWindDir >= 342 and $WXWindDir <= 360); - -####### -# Added 4.7 - - # Calculate bearing from the Position file - foreach $WXTempCompLine (@gpscomplines) { - ($WXTempCompPlace, $WXTempCompLat, $WXTempCompLong) = (split(',', $WXTempCompLine))[0, 1, 2]; - - # Calculate distance station is away from pos file - $WXTempCompDist = &great_circle_distance($WXLatitude, $WXLongitude, $WXTempCompLat, $WXTempCompLong); - #$WXTempCompDist = (sin $WXLatitude) * (sin $WXTempCompLat) + (cos $WXLatitude) * (cos $WXTempCompLat) * (cos ($WXTempCompLong-$WXLongitude)); - #$WXTempCompDist = 1.852 * 60 * atan2(sqrt(1 - $WXTempCompDist * $WXTempCompDist), $WXTempCompDist); - #$WXTempCompDist = $WXTempCompDist / 1.6093440; - $WXTempCompDist = round($WXTempCompDist, 1); - - if ($WXTempCompDist < 150 and $WXTempCompDist < $WXCompDist) { - $WXCompPlace = $WXTempCompPlace; - $WXCompDist = $WXTempCompDist; - $WXCompLat = $WXTempCompLat; - $WXCompLong = $WXTempCompLong; - } - } - - # Calculate if station is north/west/east/south of POSFILE - $WXCompLstBrLat = ($WXCompLat - $WXLatitude); - $WXCompLstBrLon = ($WXCompLong - $WXLongitude); - if ($WXCompLstBrLat < 0 and $WXCompLstBrLon < 0) {$WXCompLstBr = 'northwest'}; - if ($WXCompLstBrLat > 0 and $WXCompLstBrLon < 0) {$WXCompLstBr = 'southwest'}; - if ($WXCompLstBrLat < 0 and $WXCompLstBrLon > 0) {$WXCompLstBr = 'northeast'}; - if ($WXCompLstBrLat > 0 and $WXCompLstBrLon > 0) {$WXCompLstBr = 'southeast'}; - if ($WXCompLstBrLat <= 0 and (abs($WXCompLstBrLon) * 2) < abs($WXCompLstBrLat)) {$WXCompLstBr = 'north'}; - if ($WXCompLstBrLat >= 0 and (abs($WXCompLstBrLon) * 2) < abs($WXCompLstBrLat)) {$WXCompLstBr = 'south'}; - if ($WXCompLstBrLon <= 0 and (abs($WXCompLstBrLat) * 2) < abs($WXCompLstBrLon)) {$WXCompLstBr = 'west'}; - if ($WXCompLstBrLon >= 0 and (abs($WXCompLstBrLat) * 2) < abs($WXCompLstBrLon)) {$WXCompLstBr = 'east'}; - - # Add bearing from station in position file IF it's a new position report - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance)) and ($GPSCompDist ne '9999')) { - $WXSpeakString2 = "Station is located $WXCompDist miles $WXCompLstBr of $WXCompPlace."; - } - -####### - - if (substr($PacketPart, 35, 1) eq 'T') { # If a traditional, - $WXTemp = (substr($PacketPart, 36, 3)); - # Get rid of those damn 0's in the Temperature - if (substr($WXTemp, 0, 2) eq '00') {$WXTemp = (substr($WXTemp, 2, 1))}; - if ($WXTemp ne '0') { # Except if temp IS 0 - if (substr($WXTemp, 0, 1) eq '0') {$WXTemp = (substr($WXTemp, 1, 2))}; - } - - # Calculate Wind Chill if temp is less than 45 degrees - if ($WXTemp <= 45) { - $WXWindChill = .0817 * (3.71 * sqrt($WXWindSpeed) + 5.81 - .25 * $WXWindSpeed) * ($WXTemp - 91.4) + 91.4; - $WXWindChill = round($WXWindChill, 0); - if ($WXWindSpeed <= 5) {$WXWindChill = $WXTemp}; - } - - # If Temperature is above freezing, make it equal. - if ($WXTemp >= 46) { - $WXWindChill = $WXTemp; - } - - # Add Readings for Last Hour Precip and Last 24 Hour Precip - $WXHrPrecip = (substr($PacketPart, 41, 3)); - # Get rid of those damn 0's in the Precip - if (substr($WXHrPrecip, 0, 2) eq '00') {$WXHrPrecip = (substr($WXHrPrecip, 2, 1))}; - if ($WXHrPrecip ne '0') { # Except if precip IS 0 - if (substr($WXHrPrecip, 0, 1) eq '0') {$WXHrPrecip = (substr($WXHrPrecip, 1, 2))}; - } - # Divide Precip by 10 - $WXHrPrecip = $WXHrPrecip / 10; - $WX24HrPrecip = (substr($PacketPart, 45, 3)); - # Get rid of those damn 0's in the 24 hr Precip - if (substr($WX24HrPrecip, 0, 2) eq '00') {$WX24HrPrecip = (substr($WX24HrPrecip, 2, 1))}; - if ($WX24HrPrecip ne '0') { # Except if precip IS 0 - if (substr($WX24HrPrecip, 0, 1) eq '0') {$WX24HrPrecip = (substr($WX24HrPrecip, 1, 2))}; - } - # Divide Precip by 10 - $WX24HrPrecip = $WX24HrPrecip / 10; - - # If It's not the same as the last report, say it. - - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed ne '0')) { - $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is out of the $WXWindDirVoice at $WXWindSpeed miles an hour."; - if ($WXTemp <= 45 and $WXTemp ne $WXWindChill) {$WXSpeakString = $WXSpeakString . " The winnd chill is $WXWindChill."}; - if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; - # NEW in 4.7 - Add bearing from POSFILE - $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; - - print_log "$WXSpeakString"; - - if (($config_parms{tracking_speakflag} == 2) || - ($config_parms{tracking_speakflag} == 3)) - {speak $WXSpeakString}; - } - - # If the wind is calm, say the following. - - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed eq '0')) { - $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is calm."; - if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; - # NEW in 4.7 - Add bearing from POSFILE - $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; - - print_log "$WXSpeakString"; - - if (($config_parms{tracking_speakflag} == 2) || - ($config_parms{tracking_speakflag} == 3)) - {speak $WXSpeakString}; - } - - $LastWXCallsign = $WXCallsign; # Save last WX rpt information - $LastWXDistance = $WXDistance; - $LastWXTemp = $WXTemp; - $LastWXWindDir = $WXWindDir; - $LastWXWindSpeed = $WXWindSpeed; - $LastWXWindChill = $WXWindChill; - $LastWXHrPrecip = $WXHrPrecip; - $LastWX24HrPrecip = $WX24HrPrecip; - - } - - elsif (substr($PacketPart, 38, 1) eq 't') { # If a new format, - $WXTemp = (substr($PacketPart, 39, 3)); - # Get rid of those damn 0's in the Temperature - if (substr($WXTemp, 0, 2) eq '00') {$WXTemp = (substr($WXTemp, 2, 1))}; - if ($WXTemp ne '0') { # Except if temp IS 0 - if (substr($WXTemp, 0, 1) eq '0') {$WXTemp = (substr($WXTemp, 1, 2))}; - } - - # Calculate Wind Chill if temp is less than 45 degrees - if ($WXTemp <= 45) { - $WXWindChill = .0817 * (3.71 * sqrt($WXWindSpeed) + 5.81 - .25 * $WXWindSpeed) * ($WXTemp - 91.4) + 91.4; - $WXWindChill = round($WXWindChill, 0); - if ($WXWindSpeed <= 5) {$WXWindChill = $WXTemp}; - } - - # If Temperature is above freezing, make it equal. - if ($WXTemp >= 46) { - $WXWindChill = $WXTemp; - } - - # Add Readings for Last Hour Precip and Last 24 Hour Precip - $WXHrPrecip = (substr($PacketPart, 43, 3)); - # Get rid of those damn 0's in the Precip - if (substr($WXHrPrecip, 0, 2) eq '00') {$WXHrPrecip = (substr($WXHrPrecip, 2, 1))}; - if ($WXHrPrecip ne '0') { # Except if precip IS 0 - if (substr($WXHrPrecip, 0, 1) eq '0') {$WXHrPrecip = (substr($WXHrPrecip, 1, 2))}; - } - # Divide Precip by 10 - $WXHrPrecip = $WXHrPrecip / 10; - $WX24HrPrecip = (substr($PacketPart, 47, 3)); - # Get rid of those damn 0's in the 24 hr Precip - if (substr($WX24HrPrecip, 0, 2) eq '00') {$WX24HrPrecip = (substr($WX24HrPrecip, 2, 1))}; - if ($WX24HrPrecip ne '0') { # Except if precip IS 0 - if (substr($WX24HrPrecip, 0, 1) eq '0') {$WX24HrPrecip = (substr($WX24HrPrecip, 1, 2))}; - } - # Divide Precip by 10 - $WX24HrPrecip = $WX24HrPrecip / 10; - - # If It's not the same as the last report, say it. - - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed ne '0')) { - $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is out of the $WXWindDirVoice at $WXWindSpeed miles an hour."; - if ($WXTemp <= 45 and $WXTemp ne $WXWindChill) {$WXSpeakString = $WXSpeakString . " The winnd chill is $WXWindChill."}; - if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; - # NEW in 4.7 - Add bearing from POSFILE - $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; - - print_log "$WXSpeakString"; - - if (($config_parms{tracking_speakflag} == 2) || - ($config_parms{tracking_speakflag} == 3)) - {speak $WXSpeakString}; - } - - # If the wind is calm, say the following. - - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed eq '0')) { - $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is calm."; - if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; - # NEW in 4.7 - Add bearing from POSFILE - $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; - - print_log "$WXSpeakString"; - - if (($config_parms{tracking_speakflag} == 2) || - ($config_parms{tracking_speakflag} == 3)) - {speak $WXSpeakString}; - } - - $LastWXCallsign = $WXCallsign; # Save last WX rpt information - $LastWXDistance = $WXDistance; - $LastWXTemp = $WXTemp; - $LastWXWindDir = $WXWindDir; - $LastWXWindSpeed = $WXWindSpeed; - $LastWXWindChill = $WXWindChill; - $LastWXHrPrecip = $WXHrPrecip; - $LastWX24HrPrecip = $WX24HrPrecip; - - } - - elsif (substr($PacketPart, 21, 1) eq 't') { # If WINDOWS format, - $WXDistance = 999.9; # Because we don't - # have the position - $WXTemp = (substr($PacketPart, 22, 3)); - # Get rid of those damn 0's in the Temperature - if (substr($WXTemp, 0, 2) eq '00') {$WXTemp = (substr($WXTemp, 2, 1))}; - if ($WXTemp ne '0') { # Except if temp IS 0 - if (substr($WXTemp, 0, 1) eq '0') {$WXTemp = (substr($WXTemp, 1, 2))}; - } - - # Calculate Wind Chill if temp is less than 45 degrees - if ($WXTemp <= 45) { - $WXWindChill = .0817 * (3.71 * sqrt($WXWindSpeed) + 5.81 - .25 * $WXWindSpeed) * ($WXTemp - 91.4) + 91.4; - $WXWindChill = round($WXWindChill, 0); - if ($WXWindSpeed <= 5) {$WXWindChill = $WXTemp}; - } - - # If Temperature is above freezing, make it equal. - if ($WXTemp >= 45) { - $WXWindChill = $WXTemp; - } - - # Add Readings for Last Hour Precip and Last 24 Hour Precip - $WXHrPrecip = (substr($PacketPart, 26, 3)); - # Get rid of those damn 0's in the Precip - if (substr($WXHrPrecip, 0, 2) eq '00') {$WXHrPrecip = (substr($WXHrPrecip, 2, 1))}; - if ($WXHrPrecip ne '0') { # Except if precip IS 0 - if (substr($WXHrPrecip, 0, 1) eq '0') {$WXHrPrecip = (substr($WXHrPrecip, 1, 2))}; - } - # Divide Precip by 10 - $WXHrPrecip = $WXHrPrecip / 10; - $WX24HrPrecip = (substr($PacketPart, 30, 3)); - # Get rid of those damn 0's in the 24 hr Precip - if (substr($WX24HrPrecip, 0, 2) eq '00') {$WX24HrPrecip = (substr($WX24HrPrecip, 2, 1))}; - if ($WX24HrPrecip ne '0') { # Except if precip IS 0 - if (substr($WX24HrPrecip, 0, 1) eq '0') {$WX24HrPrecip = (substr($WX24HrPrecip, 1, 2))}; - } - # Divide Precip by 10 - $WX24HrPrecip = $WX24HrPrecip / 10; - - # If It's not the same as the last report, say it. - - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed ne '0')) { - $WXSpeakString = "$APRSCallsign reports temperature of $WXTemp degrees. Winnd is out of the $WXWindDirVoice at $WXWindSpeed miles an hour."; - if ($WXTemp <= 45 and $WXTemp ne $WXWindChill) {$WXSpeakString = $WXSpeakString . " The winnd chill is $WXWindChill."}; - if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; - - print_log "$WXSpeakString"; - - if (($config_parms{tracking_speakflag} == 2) || - ($config_parms{tracking_speakflag} == 3)) - {speak $WXSpeakString}; - } - - # If the wind is calm, say the following. - - if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed eq '0')) { - $WXSpeakString = "$APRSCallsign reports temperature of $WXTemp degrees. Winnd is calm."; - if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; - - print_log "$WXSpeakString"; - - if (($config_parms{tracking_speakflag} == 2) || - ($config_parms{tracking_speakflag} == 3)) - {speak $WXSpeakString}; - } - - $LastWXCallsign = $WXCallsign; # Save last WX rpt information - $LastWXDistance = $WXDistance; - $LastWXTemp = $WXTemp; - $LastWXWindDir = $WXWindDir; - $LastWXWindSpeed = $WXWindSpeed; - $LastWXWindChill = $WXWindChill; - $LastWXHrPrecip = $WXHrPrecip; - $LastWX24HrPrecip = $WX24HrPrecip; - } - - else - { - print_log "A funny weather station."; - } - - if ($CurrentTempDist >= $WXDistance) { # If a closer rpt, - $CurrentTempDist = $WXDistance; # change the current - $CurrentTemp = $WXTemp; # temp variable. - $CurrentChill = $WXWindChill; - $CurrentHrPrecip = $WXHrPrecip; - $Current24HrPrecip = $WX24HrPrecip; - } - - if ($CurrentTempDist eq '') { # If 1st report - $CurrentTempDist = $WXDistance; # of the day, change - $CurrentTemp = $WXTemp; # the temp variable. - $CurrentChill = $WXWindChill; - $CurrentHrPrecip = $WXHrPrecip; - $Current24HrPrecip = $WX24HrPrecip; - } - } - - #elsif (substr($APRSString, $i, 1) eq '*') { # Else if we find a "*", - # $APRSPacketDigi = 1; # It got digipeated - # print_log "Packet was Digipeated."; - #} - #else - #{ - #} - - } -} - -sub great_circle_distance { - my ($lat1, $lon1, $lat2, $lon2) = map {°rees_to_radians($_)} @_; -# my $radius = 6367; # km - my $radius = 3956; # miles - my $d = (sin(($lat2 - $lat1) / 2)) ** 2 + cos($lat1) * cos($lat2) * -(sin(($lon2 - $lon1) / 2)) ** 2; - $d = $radius * 2 * atan2(sqrt($d), sqrt(1 - $d)); -# print "db d=$d l=$lat1,$lon1,$lat2,$lon2\n"; - return round($d, 1); -} - -#EGIN { $::pi = 4 * atan2(1,1); } -sub degrees_to_radians { - return $_[0] * 3.14159265 / 180.0; -} + +###################################################### +# Klier Home Automation - Tracking Module # +# Version 4.92 (release for MH 2.??) # +# By: Brian J. Klier, N0QVC # +# June 16, 2001 # +# E-Mail: brian@kliernetwork.net # +# Webpage: http://www.kliernetwork.net # +###################################################### + +=begin comment + +mh.ini parms: + +tracking_trackself=1 # This parameter should equal "1" if + # GPS Speaking is off and you still want + # to hear tracking from your own mobile +tracking_shortannounce=1 # 0 = When Speaking Tracking + # Information, this will + # announce both distance from + # this station and distance + # from waypoint. + # 1 = Only Distance from waypoint. +tracking_withname=1 # 0 = If tracking.nam available, + # announce callsign instead + # of given name. + # 1 = Announce Given name instead + # of callsign. + # 2 = Announce given name AND + # callsign. + +=cut + +# For more information on hardware needed for this system to function: +# - Check out http://www.kliernetwork.net/aprs and +# http://www.kliernetwork.net/aprs/mine +# +# New in Version 4.92: +# - Added Roger Bille's "Longitude Hundreds" Fix +# +# New in Version 4.91: +# - Fixed bugs in "EMAIL2" Procedure +# - Added ALPHA Procedure to take data from Telnet Port and transmit +# on the air (for WinAPRS Connectivity) +# +# New in Version 4.9: +# - Fixed bugs in the HTML Based Logging System. +# +# New in Version 4.8 and 4.8a: +# - Added HTML Based Logging (from Bruce's tracking_bruce.pl) for GPS's. +# +# New in Version 4.7: +# - Added Speaking of POSFILE Positions for Weather Stations +# +# New in Version 4.62: +# - Fixed problem with tracking_withname and speaking EVERY position +# instead of checking to see if it was the same as the last one. +# - Added ability to respond to ?WX? queries with current temperature. +# +# New in Version 4.6: +# - 4.61 ALPHA - Continued work on proper implementation. +# - Added tracking_shortannounce and tracking_withname variables. +# - NOTE: I STILL need to implement tracking_withname=2!!! +# +# New in Version 4.5: +# - 4.52 ALPHA - MHATS now ignores "acks" that are sent - does not talk +# to them. Also added msg # to "msg received" messages. +# - 4.51 ALPHA - Fix Small Bug in X-10 Message Response... +# - ALPHA - Working on Temperature Graphs +# - Changed "EMAIL" gateway to "EMAIL2" so it doesn't interfere with the +# main APRS IGATES. +# +# New in Version 4.4: +# - Fixed all the messaging/X10 Packet Remote Control stuff. +# - Bulletin feature still needs to be tested for operation. +# +# Next Version Wishlist: +# - Check for duplicate messages in a row (like when they pass through +# a digipeater to make sure an X10 command isn't sent twice) +# - System to execute X-10 commands when a station is a certain distance from home + +# Declare Variables + +use vars '$GPSSpeakString', '$GPSSpeakString2', '$WXSpeakString', '$WXSpeakString2', '$CurrentTemp', '$CurrentChill', '$WXWindDirVoice', '$WXWindSpeed', '$WXHrPrecip'; + +my ($APRSFoundAPRS, $APRSPacketDigi, $GPSTime, $APRSStatus, $MsgLine); +my ($GPSLatitudeDegrees, $GPSLatitudeMinutes, $GPSLongitudeDegrees, $GPSCallsign); +my ($GPSLongitudeMinutes, $GPSLatitude, $GPSLongitude, $GPSDistance, $GPSLongitudeMinutes100); +my ($GPSLstBr, $GPSLstBrLat, $GPSLstBrLon); + +my (@gpscomplines, $GPSTempCompPlace, $GPSTempCompDist, $GPSTempCompLine); +my ($GPSTempCompLat, $GPSTempCompLong); +my ($GPSCompPlace, $GPSCompLat, $GPSCompLong, $GPSCompDist); +my ($GPSCompLstBr, $GPSCompLstBrLat, $GPSCompLstBrLon); + +# Added 4.7 +my ($WXTempCompPlace, $WXTempCompDist, $WXTempCompLine); +my ($WXTempCompLat, $WXTempCompLong); +my ($WXCompPlace, $WXCompLat, $WXCompLong, $WXCompDist); +my ($WXCompLstBr, $WXCompLstBrLat, $WXCompLstBrLon); + +my (@namelines, $TempName, $TempNameCall, $TempNameName); + +my (@wxgraphinlines, $WXTempGraphLine, $WXTempGraphTime); +my ($WXTempGraphDOW, $WXTempGraphDaytime, $WXTempGraphDate, $WXTempGraphTemp); +my ($WXTempGraphWindDir, $WXTempGraphWindSpeed, $WXTempGraphHrPrecip); +my ($WXTempGraph24HrPrecip, $WXTempGraphHour, $WXTempGraphMin); +my ($WXTempGraphAMPM); + +my ($i, $j, $k, $CallsignPart, $PacketPart, $GPSSpeed, $GPSCourse, $GPSCourseVoice); +my ($ToCallsignPart, $LastGPSCallsign, $LastGPSDistance, $LastGPSLstBr); +my ($APRSCallsign, $APRSStringLength, $APRSString, $MessageX10Command); +my ($MessageX10Action); +my ($GPSSpeakString3, $MessageAck, $APRSCallsignVoice, $HamCall, $HamName); +my ($WXTime, $WXLatitudeDegrees, $WXLatitudeMinutes, $WXLongitudeDegrees); +my ($WXLongitudeMinutes, $WXLatitude, $WXLongitude, $WXDistance, $WXTemp); +my ($WXLstBr, $WXLstBrLat, $WXLstBrLon); +my ($WXCallsign, $WXWindDir, $WX24HrPrecip, $RealAPRSCallsign); +my ($CurrentTempDist, $WXWindChill); +my ($LastWXCallsign, $LastWXTemp, $LastWXDistance, $LastWXWindDir); +my ($LastWXWindSpeed, $LastWXWindChill, $APRSCallsignNoSSID); +my ($LastWXHrPrecip, $LastWX24HrPrecip, $CurrentHrPrecip, $Current24HrPrecip); + +# Setup TELNET Server 2 (port 14439) to output what the TNC hears + +$server2 = new Socket_Item('#Welcome to MisterHouse APRS Tracking!', 'APRSWelcome', 'server2'); +# Send Welcome Message out port 2 if connected. +set $server2 'APRSWelcome' if active_now $server2; +set $server2 'APRSERVE>APRS:javaTITLE:N0QVC MisterHouse - tracking.pl - Brian Klier, N0QVC' if active_now $server2; + +my $socket_speak_loop; +if (my $telnetdata = said $server2) { + print_log "Data Transmitted from Telnet Port: $telnetdata"; + set $tnc_output $telnetdata; +} + +# TNC Output Lines + +$tnc_output = new Serial_Item ('CONV','converse','serial1'); +$tnc_output -> add ('?WX?','wxquery','serial1'); +$tnc_output -> add (sprintf("=%2d%05.02fN/0%2d%05.02fW- *** %s MisterHouse Tracking System - ICQ#659962 ***", + int($config_parms{latitude}), + abs ($config_parms{latitude} - int($config_parms{latitude}))*60, + int($config_parms{longitude}), + abs ($config_parms{longitude} -int($config_parms{longitude}))*60, + $config_parms{tracking_callsign}), + ,'position','serial1'); + +# Set TNC to Converse and send position on Startup + +if ($Reload) { + $HamCall = $config_parms{tracking_callsign}; # Feed in my Tracking Callsign + open(GPSCOMP, "$config_parms{code_dir}/tracking.pos"); # Open for input + @gpscomplines = ; # Open array and + # read in data + close GPSCOMP; # Close the file + + open(GPSNAME, "$config_parms{code_dir}/tracking.nam"); # Open for input + @namelines = ; # Open array and + # read in data + close GPSNAME; # Close the file +} + +if ($Startup) { + mkdir "$Pgm_Root/web/javAPRS",777 unless -d "$Pgm_Root/web/javAPRS"; + + open(APRSLOG, ">$Pgm_Root/web/javAPRS/aprs.tnc"); # CLEAR Log + close APRSLOG; + +# set $tnc_output pack('C',3); +# set $tnc_output 'MRPT OFF'; # Do NOT Show Digipeater Path +# set $tnc_output 'HEADERLN OFF'; # Keep Header and data on the same line + set $tnc_output 'converse'; + set $tnc_output 'position'; + print_msg "Tracking Interface has been Initialized...Callsign $HamCall"; + print_log "Tracking Interface has been Initialized...Callsign $HamCall"; +} + +# Voice Responses +$v_send_position = new Voice_Cmd("Send my Position"); + +if ($state = said $v_send_position) { + set $tnc_output 'position'; + print_log "Position Sent."; + speak "Position Sent."; +} + +$v_send_status = new Voice_Cmd("Send my Status Report"); + +if ($state = said $v_send_status) { + $APRSStatus = ">Frnt Move $motion_detector_frontdoor->{state}-Bck Move $motion_detector_backdoor->{state}-Kitc Move $motion_detector_kitchen->{state}-Garg Move $motion_detector_garage->{state}-Temp: $CurrentTemp"; + set $tnc_output $APRSStatus; + print_log "Status Sent."; + speak "Status Sent."; +} + +$v_wx_query = new Voice_Cmd("Weather Query"); + +if ($state = said $v_wx_query) { + set $tnc_output 'wxquery'; + print_log "Weather Query Requested."; + speak "Weather Query Requested."; +} + +$v_last_callsign = new Voice_Cmd("Last Callsign"); + +if ($state = said $v_last_callsign and $APRSCallsign ne '') { + print_log "Callsign is $APRSCallsign."; + speak "Call sign is $APRSCallsign."; +} +elsif ($state = said $v_last_callsign and $APRSCallsign eq '') { + print_log "No packets received."; + speak "No packets received."; +} + +$v_last_mobile = new Voice_Cmd("Last Mobile Report"); + +if ($state = said $v_last_mobile and $GPSCallsign ne '') { + print_log "$GPSSpeakString"; + speak $GPSSpeakString; +} +elsif ($state = said $v_last_mobile and $GPSCallsign eq '') { + print_log "No mobile packets received."; + speak "No mobile packets received."; +} + +$v_last_wxrpt = new Voice_Cmd("Last Weather Report"); + +if ($state = said $v_last_wxrpt) { + if ($WXTemp ne '') { + print_log "$WXSpeakString"; + speak $WXSpeakString; + } + elsif ($WXTemp eq '') { + print_log "No weather packets received."; + speak "No weather packets received."; + } +} + +$v_curr_cond = new Voice_Cmd("Current Conditions"); + +if ($state = said $v_curr_cond) { + if ($CurrentTemp ne '') { + if ($CurrentTemp eq $CurrentChill) { + print_log "Temperature is $CurrentTemp degrees."; + speak "Temperature is $CurrentTemp degrees."; + } + if ($CurrentTemp ne $CurrentChill) { + print_log "Temperature is $CurrentTemp degrees. Wind Chill is $CurrentChill degrees."; + speak "Temperature is $CurrentTemp degrees. Winnd Chill is $CurrentChill degrees."; + } + if ($CurrentHrPrecip != 0) { + print_log "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; + speak "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; + } + } + elsif ($CurrentTemp eq '') { + print_log "No weather packets received."; + speak "No weather packets received."; + } +} + +$v_curr_precip = new Voice_Cmd("Current Precipitation"); + +if ($state = said $v_curr_precip) { + if ($CurrentHrPrecip != 0) { + print_log "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; + speak "$CurrentHrPrecip inches of rain in the last hour. $Current24HrPrecip total inches of rain today."; + } + elsif ($CurrentTemp eq '') { + print_log "No weather packets received."; + speak "No weather packets received."; + } + else { + print_log "No rain to report."; + speak "No rain to report."; + } +} + +$v_send_test_email = new Voice_Cmd("Send test email to myself"); + +if ($state = said $v_send_test_email) { + $i = ":EMAIL :$config_parms{net_mail_user}\@$config_parms{net_mail_server} Test E-Mail - " . $CurrentTemp . "deg.{0"; + set $tnc_output $i; +} + +$v_send_test_icq = new Voice_Cmd("Send test ICQ msg to myself"); + +if ($state = said $v_send_test_icq) { + $i = ":ICQSERVE :659962 Test Message - " . $CurrentTemp . " degrees.{1"; + set $tnc_output $i; +} + +# Added 4.7 +$v_register_icqserve = new Voice_Cmd("Register on ICQServe (Do Once)"); + +if ($state = said $v_register_icqserve) { + $i = ":ICQSERVE :REGISTER 659962 {1"; + set $tnc_output $i; +} + +# Procedure to Log Temperature and Stats every 10 minutes + +if (time_cron('0,10,20,30,40,50 * * * *') or $Startup) { + logit("$Pgm_Path/../web/mh/weather.html", "Temp: $CurrentTemp    Wind Chill: $CurrentChill    Wind: $LastWXWindDir/$LastWXWindSpeed    Precipitation: $WXHrPrecip/$WX24HrPrecip
    "); + logit("$Pgm_Path/../data/logs/weather.log", ",$CurrentTemp,$LastWXWindDir,$LastWXWindSpeed,$WXHrPrecip,$WX24HrPrecip"); +} + +# Daily Weather Log Backup + +if (time_cron('0 0 * * *')) { + print_log "Backing up Weather Log."; + open(WXGRAPHIN, ">$Pgm_Path/../data/logs/weather.log"); # CLEAR Log + close WXGRAPHIN; + open(WXGRAPHIN, ">$Pgm_Path/../web/mh/weather.html"); # CLEAR Log + close WXGRAPHIN; + #copy("$Pgm_Path/../data/logs/weather.log", "$Pgm_Path/../data/logs/weather.bak.log") or print_log "Error in copying: $!"; +} + +# Procedure to Make a series of Graphs with Temperature and Stats + +$v_make_graph = new Voice_Cmd("Make Weather Graph Now"); +if (time_cron('15 * * * *') or $Startup or $Reload or $state = said $v_make_graph) { + open(WXGRAPHOUT, ">$Pgm_Path/../web/mh/wxgraph.html"); # Log it + print WXGRAPHOUT "\nWeather Graphs\n"; + print WXGRAPHOUT "\n"; + print WXGRAPHOUT "\n"; + print WXGRAPHOUT "; # Open array and + # read in data + close WXGRAPHIN; # Close the file + + foreach $WXTempGraphLine (@wxgraphinlines) { + ($WXTempGraphDOW, $WXTempGraphDaytime, $WXTempGraphTemp, $WXTempGraphWindDir, $WXTempGraphWindSpeed, $WXTempGraphHrPrecip, $WXTempGraph24HrPrecip) = (split(',', $WXTempGraphLine))[0, 1, 2, 3, 4, 5, 6]; + $WXTempGraphDate = substr($WXTempGraphDaytime, 5, 2); # Day + $WXTempGraphHour = substr($WXTempGraphDaytime, 8, 2); # Hour + $WXTempGraphMin = substr($WXTempGraphDaytime, 11, 2); # Minute + $WXTempGraphAMPM = substr($WXTempGraphDaytime, 14, 1); # A/P + + if ($WXTempGraphAMPM eq 'A' and $WXTempGraphHour eq '12') {$WXTempGraphHour = '0'}; + if ($WXTempGraphAMPM eq 'P' and $WXTempGraphHour ne '12') {$WXTempGraphHour = $WXTempGraphHour + 12}; + $WXTempGraphMin = $WXTempGraphMin / 60; + $WXTempGraphTime = $WXTempGraphHour + $WXTempGraphMin; + + if ($WXTempGraphTemp ne '') { # If the Temp isn't blank, + print WXGRAPHOUT "$WXTempGraphTime,$WXTempGraphTemp "; + } + } + + print WXGRAPHOUT "\">\n\n"; + print WXGRAPHOUT "\n"; + print WXGRAPHOUT "\n"; + print WXGRAPHOUT "; # Open array and + # read in data + close WXGRAPHIN; # Close the file + + foreach $WXTempGraphLine (@wxgraphinlines) { + ($WXTempGraphDOW, $WXTempGraphDaytime, $WXTempGraphTemp, $WXTempGraphWindDir, $WXTempGraphWindSpeed, $WXTempGraphHrPrecip, $WXTempGraph24HrPrecip) = (split(',', $WXTempGraphLine))[0, 1, 2, 3, 4, 5, 6]; + $WXTempGraphDate = substr($WXTempGraphDaytime, 5, 2); # Day + $WXTempGraphHour = substr($WXTempGraphDaytime, 8, 2); # Hour + $WXTempGraphMin = substr($WXTempGraphDaytime, 11, 2); # Minute + $WXTempGraphAMPM = substr($WXTempGraphDaytime, 14, 1); # A/P + + if ($WXTempGraphAMPM eq 'A' and $WXTempGraphHour eq '12') {$WXTempGraphHour = '0'}; + if ($WXTempGraphAMPM eq 'P' and $WXTempGraphHour ne '12') {$WXTempGraphHour = $WXTempGraphHour + 12}; + $WXTempGraphMin = $WXTempGraphMin / 60; + $WXTempGraphTime = $WXTempGraphHour + $WXTempGraphMin; + + if ($WXTempGraphWindSpeed ne '') { # If the Speed isn't blank, + print WXGRAPHOUT "$WXTempGraphTime,$WXTempGraphWindSpeed "; + } + } + + print WXGRAPHOUT "\">\n\n"; + close WXGRAPHOUT; +} + +# Procedure to occasionally send out APRS Position Report and Status String + +if (time_cron('0,30 * * * *')) { + set $tnc_output 'converse'; + set $tnc_output 'position'; + $APRSStatus = ">Frnt Move $motion_detector_frontdoor->{state}-Bck Move $motion_detector_backdoor->{state}-Kitc Move $motion_detector_kitchen->{state}-Garg Move $motion_detector_garage->{state}-Temp: $CurrentTemp"; + set $tnc_output $APRSStatus; +} + +# Main TNC Parse Procedure + +if ($APRSString = said $tnc_output) { + + open(APRSLOG, ">>$Pgm_Root/web/javAPRS/aprs.tnc"); # Log it + print APRSLOG "$APRSString\n"; + close APRSLOG; + + print_msg "TRACK: $APRSString"; # Monitor to Msg Window + + $APRSFoundAPRS = ""; # Reset Found Flag + # $APRSPacketDigi = ""; + $APRSStringLength = (length($APRSString)); # Save Length of Ser + + # Send the packet out TELNET if connected... + set $server2 $APRSString if active $server2; + + # Decode the Callsign and different parts from the Packet + + ($CallsignPart, $MsgLine) = (split('::', $APRSString))[0, 1]; #New Line for Msging + ($CallsignPart, $PacketPart) = (split(':', $APRSString))[0, 1]; + ($APRSCallsign, $ToCallsignPart) = (split('>', $CallsignPart))[0, 1]; + ($APRSCallsignNoSSID, $j) = (split('-', $APRSCallsign))[0, 1]; + + # Save the APRS Callsign for Message Reading Procedure (non-spaced) + + $RealAPRSCallsign = $APRSCallsign; + + # Make APRS Callsign so it is spoken properly + + $j = ''; + for ($i = 0; $i != (length($APRSCallsign)); ++$i) { + $j = $j . substr($APRSCallsign, $i, 1); + $j = $j . " "; + } + $APRSCallsign = $j; + + # MAIN LOOP + + if ($APRSFoundAPRS != 1) { + # If it's a $GPRMC, $GPGGA, or Mic-E statement for GPS, + if ((substr($PacketPart, 0, 6) eq '$GPRMC') || + (substr($PacketPart, 0, 6) eq '$GPGGA') || + (substr($PacketPart, 0, 1) eq '`') || + (substr($PacketPart, 0, 1) eq "'")) { + $APRSFoundAPRS = 1; # Found an APRS String + $GPSCallsign = $APRSCallsign; # Reset Variables + $GPSTime = ""; + $GPSLatitudeDegrees = ""; + $GPSLatitudeMinutes = ""; + $GPSLongitudeDegrees = ""; + $GPSLongitudeMinutes = ""; + $GPSLatitude = ""; + $GPSLongitude = ""; + $GPSDistance = ""; + $GPSSpeed = ""; + $GPSCourse = ""; + $GPSCourseVoice = ""; + $GPSLstBr = ""; + $GPSLstBrLat = ""; + $GPSLstBrLon = ""; + $GPSCompPlace = ""; + $GPSCompLat = ""; + $GPSCompLong = ""; + $GPSCompDist = "9999"; + $GPSSpeakString2 = ""; + $GPSTempCompPlace = ""; + $GPSTempCompDist = ""; + + # Find out the user defined "name" for this callsign. + + $j = '0'; + + foreach $TempName (@namelines) { + if ($j eq '0') { + ($TempNameCall, $TempNameName) = (split(',', $TempName))[0, 1]; + if ($TempNameCall eq $APRSCallsignNoSSID) { + $HamName = $TempNameName; + chomp $HamName; + $j = '1'; + } + } + } + + # If there is NO defined name, make $HamName equal to the callsign + + if ($j eq '0') { + $HamName = $APRSCallsign; + print_msg "$APRSString -> No callsign Found\n"; + } + + + if (substr($PacketPart, 0, 6) eq '$GPRMC') { + ($GPSTime, $GPSLatitude, $GPSLongitude, $GPSSpeed, $GPSCourse) = (split(',', $PacketPart))[1, 3, 5, 7, 8]; + } + + if (substr($PacketPart, 0, 6) eq '$GPGGA') { + ($GPSTime, $GPSLatitude, $GPSLongitude) = (split(',', $PacketPart))[1, 2, 4]; + } + + if ((substr($PacketPart, 0, 6) eq '$GPRMC') || + (substr($PacketPart, 0, 6) eq '$GPGGA')) { + + $GPSTime = (substr($GPSTime, 0, 6)); # Get the GPS Time + $GPSLatitudeDegrees = (substr($GPSLatitude, 0, 2)); + $GPSLatitudeMinutes = (substr($GPSLatitude, 2, 8)); + $GPSLatitude = ($GPSLatitudeDegrees + ($GPSLatitudeMinutes / 60)); + $GPSLongitudeDegrees = (substr($GPSLongitude, 0, 3)); + $GPSLongitudeMinutes = (substr($GPSLongitude, 3, 8)); + $GPSLongitude = ($GPSLongitudeDegrees + ($GPSLongitudeMinutes / 60)); + + # Convert the GPS Speed to MPH + $GPSSpeed = ($GPSSpeed * 1.853248) / 1.609344; + } + + if ((substr($PacketPart, 0, 1) eq '`') || # If a Mic-E, + (substr($PacketPart, 0, 1) eq "'")) { + + $GPSLongitudeDegrees = (substr($PacketPart, 1, 1)); + $GPSLongitudeDegrees = (unpack('C', $GPSLongitudeDegrees)) - 28; + if ($GPSLongitudeDegrees >= 180 and $GPSLongitudeDegrees <= 189) {$GPSLongitudeDegrees = $GPSLongitudeDegrees - 80}; + if ($GPSLongitudeDegrees >= 190 and $GPSLongitudeDegrees <= 199) {$GPSLongitudeDegrees = $GPSLongitudeDegrees - 190}; + + $GPSLongitudeMinutes = (substr($PacketPart, 2, 1)); + $GPSLongitudeMinutes = (unpack('C', $GPSLongitudeMinutes)) - 28; + if ($GPSLongitudeMinutes > 60) {$GPSLongitudeMinutes = $GPSLongitudeMinutes - 60}; + + # Added Lines from Roger + $GPSLongitudeMinutes100 = (substr($PacketPart, 3, 1)); + $GPSLongitudeMinutes100 = (unpack('C', $GPSLongitudeMinutes100)) - 28; + # + + $GPSLongitude = ($GPSLongitudeDegrees + ($GPSLongitudeMinutes / 60)+ ($GPSLongitudeMinutes100 / 6000)); + # old -> $GPSLongitude = ($GPSLongitudeDegrees + ($GPSLongitudeMinutes / 60)); + + $GPSSpeed = (substr($PacketPart, 4, 1)); + $GPSSpeed = ((unpack('C', $GPSSpeed)) - 28) * 10; + # $CallsignPart simply used as a temp variable + $CallsignPart = (substr($PacketPart, 5, 1)); + $GPSSpeed = (((unpack('C', $CallsignPart)) - 28) / 10) + $GPSSpeed; + + $GPSCourse = ((unpack('C', $CallsignPart)) - 28) % 10; + # $CallsignPart simply used as a temp variable + $CallsignPart = (substr($PacketPart, 6, 1)); + $GPSCourse = ((unpack('C', $CallsignPart)) - 28) + $GPSCourse; + + # Last minute course and speed adjustments per specs + if ($GPSSpeed >= 800) {$GPSSpeed = $GPSSpeed - 800}; + if ($GPSCourse >= 400) {$GPSCourse = $GPSCourse - 400}; + + # Convert the GPS Speed to MPH + $GPSSpeed = ($GPSSpeed * 1.853248) / 1.609344; + + # Truncate Course to max of 3 numbers for parsing below + $GPSCourse = substr($GPSCourse, 0, 3); + + # Round the Speed to the nearest integer + $GPSSpeed = round($GPSSpeed); + + # Load the tens digit of Degrees Latitude + $GPSLatitudeDegrees = (substr($ToCallsignPart, 0, 1)); + $GPSLatitudeDegrees = (unpack('C', $GPSLatitudeDegrees)) - 32; + $GPSLatitudeDegrees = ($GPSLatitudeDegrees & 15) * 10; + + # Load the ones digit of Degrees Latitude (temp variable used) + $GPSDistance = (substr($ToCallsignPart, 1, 1)); + $GPSDistance = (unpack('C', $GPSDistance)) - 32; + $GPSDistance = ($GPSDistance & 15); + + # Here's our Degrees Latitude + $GPSLatitudeDegrees = $GPSLatitudeDegrees + $GPSDistance; + + # Load the tens digit of Minutes Latitude + $GPSLatitudeMinutes = (substr($ToCallsignPart, 2, 1)); + $GPSLatitudeMinutes = (unpack('C', $GPSLatitudeMinutes)) - 32; + $GPSLatitudeMinutes = ($GPSLatitudeMinutes & 15) * 10; + + # Load the ones digit of Minutes Latitude (temp variable used) + $GPSDistance = (substr($ToCallsignPart, 3, 1)); + $GPSDistance = (unpack('C', $GPSDistance)) - 32; + $GPSDistance = ($GPSDistance & 15); + + # Here's our Minutes Latitude + $GPSLatitudeMinutes = $GPSLatitudeMinutes + $GPSDistance; + + # Load the tens digit of hundreds of Minutes Latitude + # $CallsignPart simply used as a temp variable + $CallsignPart = (substr($ToCallsignPart, 4, 1)); + $CallsignPart = (unpack('C', $CallsignPart)) - 32; + $CallsignPart = ($CallsignPart & 15) * 10; + + # Load the ones digit of hundreds of Minutes Latitude + # (temp variable used) + $GPSDistance = (substr($ToCallsignPart, 5, 1)); + $GPSDistance = (unpack('C', $GPSDistance)) - 32; + $GPSDistance = ($GPSDistance & 15); + + # Here's our hundreds of Minutes Latitude + $CallsignPart = $CallsignPart + $GPSDistance; + + $GPSLatitude = ($GPSLatitudeDegrees + ($GPSLatitudeMinutes / 60) + ($CallsignPart / 6000)); + } + + # --- Do the following for all received GPS Strings + + # Calculate distance station is away + $GPSDistance = &great_circle_distance($GPSLatitude, $GPSLongitude, $config_parms{latitude}, $config_parms{longitude}); + #$GPSDistance = (sin $GPSLatitude) * (sin $config_parms{latitude}) + (cos $GPSLatitude) * (cos $config_parms{latitude}) * (cos ($config_parms{longitude}-$GPSLongitude)); + #$GPSDistance = 1.852 * 60 * atan2(sqrt(1 - $GPSDistance * $GPSDistance), $GPSDistance); + #$GPSDistance = $GPSDistance / 1.6093440; + $GPSDistance = round($GPSDistance, 1); + + # Calculate bearing from the Position file + foreach $GPSTempCompLine (@gpscomplines) { + ($GPSTempCompPlace, $GPSTempCompLat, $GPSTempCompLong) = (split(',', $GPSTempCompLine))[0, 1, 2]; + + # Calculate distance station is away from pos file + $GPSTempCompDist = &great_circle_distance($GPSLatitude, $GPSLongitude, $GPSTempCompLat, $GPSTempCompLong); + #$GPSTempCompDist = (sin $GPSLatitude) * (sin $GPSTempCompLat) + (cos $GPSLatitude) * (cos $GPSTempCompLat) * (cos ($GPSTempCompLong-$GPSLongitude)); + #$GPSTempCompDist = 1.852 * 60 * atan2(sqrt(1 - $GPSTempCompDist * $GPSTempCompDist), $GPSTempCompDist); + #$GPSTempCompDist = $GPSTempCompDist / 1.6093440; + $GPSTempCompDist = round($GPSTempCompDist, 1); + + if ($GPSTempCompDist < 15 and $GPSTempCompDist < $GPSCompDist) { + $GPSCompPlace = $GPSTempCompPlace; + $GPSCompDist = $GPSTempCompDist; + $GPSCompLat = $GPSTempCompLat; + $GPSCompLong = $GPSTempCompLong; + } + } + + # Calculate if station is north/west/east/south of POSFILE + $GPSCompLstBrLat = ($GPSCompLat - $GPSLatitude); + $GPSCompLstBrLon = ($GPSCompLong - $GPSLongitude); + if ($GPSCompLstBrLat < 0 and $GPSCompLstBrLon < 0) {$GPSCompLstBr = 'northwest'}; + if ($GPSCompLstBrLat > 0 and $GPSCompLstBrLon < 0) {$GPSCompLstBr = 'southwest'}; + if ($GPSCompLstBrLat < 0 and $GPSCompLstBrLon > 0) {$GPSCompLstBr = 'northeast'}; + if ($GPSCompLstBrLat > 0 and $GPSCompLstBrLon > 0) {$GPSCompLstBr = 'southeast'}; + if ($GPSCompLstBrLat <= 0 and (abs($GPSCompLstBrLon) * 2) < abs($GPSCompLstBrLat)) {$GPSCompLstBr = 'north'}; + if ($GPSCompLstBrLat >= 0 and (abs($GPSCompLstBrLon) * 2) < abs($GPSCompLstBrLat)) {$GPSCompLstBr = 'south'}; + if ($GPSCompLstBrLon <= 0 and (abs($GPSCompLstBrLat) * 2) < abs($GPSCompLstBrLon)) {$GPSCompLstBr = 'west'}; + if ($GPSCompLstBrLon >= 0 and (abs($GPSCompLstBrLat) * 2) < abs($GPSCompLstBrLon)) {$GPSCompLstBr = 'east'}; + + # Calculate if station is north/west/east/south of ours + $GPSLstBrLat = ($config_parms{latitude} - $GPSLatitude); + $GPSLstBrLon = ($config_parms{longitude} - $GPSLongitude); + if ($GPSLstBrLat < 0 and $GPSLstBrLon < 0) {$GPSLstBr = 'northwest'}; + if ($GPSLstBrLat > 0 and $GPSLstBrLon < 0) {$GPSLstBr = 'southwest'}; + if ($GPSLstBrLat < 0 and $GPSLstBrLon > 0) {$GPSLstBr = 'northeast'}; + if ($GPSLstBrLat > 0 and $GPSLstBrLon > 0) {$GPSLstBr = 'southeast'}; + if ($GPSLstBrLat <= 0 and (abs($GPSLstBrLon) * 2) < abs($GPSLstBrLat)) {$GPSLstBr = 'north'}; + if ($GPSLstBrLat >= 0 and (abs($GPSLstBrLon) * 2) < abs($GPSLstBrLat)) {$GPSLstBr = 'south'}; + if ($GPSLstBrLon <= 0 and (abs($GPSLstBrLat) * 2) < abs($GPSLstBrLon)) {$GPSLstBr = 'west'}; + if ($GPSLstBrLon >= 0 and (abs($GPSLstBrLat) * 2) < abs($GPSLstBrLon)) {$GPSLstBr = 'east'}; + + # Add bearing from station in position file IF it's a new position report + if ((($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) and ($GPSCompDist ne '9999')) { + $GPSSpeakString2 = "Currently $GPSCompDist miles $GPSCompLstBr of $GPSCompPlace."; + # and form a special speak string just for on the air + $GPSSpeakString3 = ">$RealAPRSCallsign $GPSCompDist mi $GPSCompLstBr of $GPSCompPlace."; + +### # Added in 4.9 + if ($GPSDistance <= 0.2) { + $GPSSpeakString2 = "Currently near $GPSCompPlace."; + $GPSSpeakString3 = ">$RealAPRSCallsign near $GPSCompPlace."; + } + if ((substr($PacketPart, 0, 6) eq '$GPRMC') and + ($GPSDistance <= 0.1) and + ($GPSSpeed <= 1)) { + $GPSSpeakString2 = "Currently parked at $GPSCompPlace."; + $GPSSpeakString3 = ">$RealAPRSCallsign parked at $GPSCompPlace."; + } + if ((substr($PacketPart, 0, 6) eq '$GPGGA') and + ($GPSDistance <= 0.1)) { + $GPSSpeakString2 = "Currently at $GPSCompPlace."; + $GPSSpeakString3 = ">$RealAPRSCallsign at $GPSCompPlace."; + } +## + + set $tnc_output $GPSSpeakString3; + } + + # If It's a $GPRMC or Mic-E String, + + if ((substr($PacketPart, 0, 6) eq '$GPRMC') || + (substr($PacketPart, 0, 1) eq '`')) { + + # Only Calculate Course & Speed if it's a $GPRMC string, + if (substr($PacketPart, 0, 6) eq '$GPRMC') { + + # Truncate Course to max of 3 numbers for parsing below + $GPSCourse = substr($GPSCourse, 0, 3); + + # Round the Speed to the nearest integer + $GPSSpeed = round($GPSSpeed); + } + + $GPSCourseVoice = "north" if ($GPSCourse >= 0 and $GPSCourse <= 11); + $GPSCourseVoice = "north-northeast" if ($GPSCourse >= 12 and $GPSCourse <= 33); + $GPSCourseVoice = "northeast" if ($GPSCourse >= 34 and $GPSCourse <= 55); + $GPSCourseVoice = "east-northeast" if ($GPSCourse >= 56 and $GPSCourse <= 77); + $GPSCourseVoice = "east" if ($GPSCourse >= 78 and $GPSCourse <= 99); + $GPSCourseVoice = "east-southeast" if ($GPSCourse >= 100 and $GPSCourse <= 121); + $GPSCourseVoice = "southeast" if ($GPSCourse >= 122 and $GPSCourse <= 143); + $GPSCourseVoice = "south-southeast" if ($GPSCourse >= 144 and $GPSCourse <= 165); + $GPSCourseVoice = "south" if ($GPSCourse >= 166 and $GPSCourse <= 187); + $GPSCourseVoice = "south-southwest" if ($GPSCourse >= 188 and $GPSCourse <= 209); + $GPSCourseVoice = "southwest" if ($GPSCourse >= 210 and $GPSCourse <= 231); + $GPSCourseVoice = "west-southwest" if ($GPSCourse >= 232 and $GPSCourse <= 253); + $GPSCourseVoice = "west" if ($GPSCourse >= 254 and $GPSCourse <= 275); + $GPSCourseVoice = "west-northwest" if ($GPSCourse >= 276 and $GPSCourse <= 297); + $GPSCourseVoice = "northwest" if ($GPSCourse >= 298 and $GPSCourse <= 319); + $GPSCourseVoice = "north-northwest" if ($GPSCourse >= 320 and $GPSCourse <= 341); + $GPSCourseVoice = "north" if ($GPSCourse >= 342 and $GPSCourse <= 360); + + if ($config_parms{tracking_withname} == 1) + {$GPSCallsign = $HamName}; + + # If It's not the same as the last report, say it. + + if ((($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) and ($GPSSpeed ne '0')) { + + if ($config_parms{tracking_shortannounce} == 0) + {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us, heading $GPSCourseVoice at $GPSSpeed miles an hour. $GPSSpeakString2"}; + + if ($config_parms{tracking_shortannounce} == 1) { + $GPSSpeakString = "$GPSCallsign is $GPSSpeakString2"; + if ($GPSSpeakString2 eq '') {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us, heading $GPSCourseVoice at $GPSSpeed miles an hour."}; + } + + print_log "$GPSSpeakString"; + + if (($config_parms{tracking_speakflag} == 1) || + ($config_parms{tracking_speakflag} == 3)) + {speak $GPSSpeakString}; + + if ($config_parms{tracking_trackself} == 1 and + $APRSCallsignNoSSID eq $HamCall) + {speak $GPSSpeakString}; + } + + # If the GPS is Stationary, say the following. + + if ((($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) and ($GPSSpeed eq '0')) { + + if ($config_parms{tracking_shortannounce} == 0) + {$GPSSpeakString = "$GPSCallsign is parked $GPSDistance miles $GPSLstBr of us. $GPSSpeakString2"}; + + if ($config_parms{tracking_shortannounce} == 1) { + $GPSSpeakString = "$GPSCallsign is $GPSSpeakString2"; + if ($GPSSpeakString2 eq '') {$GPSSpeakString = "$GPSCallsign is parked $GPSDistance miles $GPSLstBr of us."}; + } + + print_log "$GPSSpeakString"; + + if (($config_parms{tracking_speakflag} == 1) || + ($config_parms{tracking_speakflag} == 3)) + {speak $GPSSpeakString}; + + if ($config_parms{tracking_trackself} == 1 and + $APRSCallsignNoSSID eq $HamCall) + {speak $GPSSpeakString}; + } + } + + # If It's a $GPGGA String, + + if (substr($PacketPart, 0, 6) eq '$GPGGA') { + + if ($config_parms{tracking_withname} == 1) + {$GPSCallsign = $HamName}; + + if (($GPSCallsign ne $LastGPSCallsign) || ($GPSDistance ne $LastGPSDistance)) { + + if ($config_parms{tracking_shortannounce} == 0) + {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us. $GPSSpeakString2"}; + + if ($config_parms{tracking_shortannounce} == 1) { + $GPSSpeakString = "$GPSCallsign is $GPSSpeakString2"; + if ($GPSSpeakString2 eq '') {$GPSSpeakString = "$GPSCallsign is $GPSDistance miles $GPSLstBr of us."}; + } + + print_log "$GPSSpeakString"; + + if (($config_parms{tracking_speakflag} == 1) || + ($config_parms{tracking_speakflag} == 3)) + {speak $GPSSpeakString}; + + if ($config_parms{tracking_trackself} == 1 and + $APRSCallsignNoSSID eq $HamCall) + {speak $GPSSpeakString}; + } + } + + $LastGPSCallsign = $GPSCallsign; # Save last GPS rpt + $LastGPSDistance = $GPSDistance; + $LastGPSLstBr = $GPSLstBr; + + # NEW IN 4.8 + # Prototype Log File Procedure for Tracking + + $i = -$GPSLongitude; + $j = $GPSLatitude; + + my $html = qq[
  • \n]; + $html .= qq[\n|; + $html .= qq[\n\n]; + + #$k = qq[
  • $Date_Now $Time_Now: ]; + #$k .= qq[\n$GPSSpeakString\n\n]; + logit "$config_parms{html_dir}/mh/tracking/today.html", $html, 0; + logit "$config_parms{html_dir}/mh/tracking/week1.html", $html, 0; + + if ($New_Day) { + open(NEWDAY, ">$config_parms{html_dir}/mh/tracking/today.html"); + close NEWDAY; + my $html = qq[\n
  • Date TimeVehicle Heading and SpeedLocationNew Location
    $Date_Now $Time_Now$GPSSpeakString$GPSSpeakString2\n]; + $i = -$i; # For logging form data in .pos + $html .= qq[
    \n]; + $html .= qq[\n]; + logit "$config_parms{html_dir}/mh/tracking/today.html", $html, 0, 1; + logit "$config_parms{html_dir}/mh/tracking/week1.html", "
    \n", 0, 1; + } + + if ($New_Week) { + open(NEWWEEK, ">$config_parms{html_dir}/mh/tracking/week1.html"); + close NEWWEEK; + #file_cat "$config_parms{html_dir}/mh/tracking/week2.html", "$config_parms{html_dir}/mh/tracking/old/${Year_Month_Now}.html"; + #rename "$config_parms{html_dir}/mh/tracking/week1.html", "$config_parms{html_dir}/mh/tracking/week2.html" or print_log "Error in aprs rename 2: $!"; + my $html = qq[\n
    Date TimeVehicle Heading and SpeedLocationNew Location
    \n]; + $html .= qq[\n]; + logit "$config_parms{html_dir}/mh/tracking/week1.html", $html, 1; + } + + # Add an index entry for the new months entry in aprs/old + + #if ($New_Month) { + # my $html = qq[
  • $Year_Month_Now.html\n]; + # logit "$config_parms{html_dir}/mh/tracking/old/index.html", $html, 1; + #} + } # **END** GPS Parse + + # Send E-Mail from APRS messages with "EMAIL2" + + if (substr($MsgLine, 0, 6) eq 'EMAIL2') { + $APRSFoundAPRS = 1; + + ($MsgLine, $MessageAck) = (split('{', $MsgLine))[0, 1]; + ($CallsignPart, $PacketPart) = (split(':', $MsgLine))[0, 1]; + ($CallsignPart, $PacketPart) = (split(' ', $PacketPart))[0, 1]; + + # Let $i equals the number of spaces to put before :ack + $i = (9 - length($RealAPRSCallsign)); + $k = ' '; + $k = ($k x $i); + + print_log "Email gateway: Callsign=$RealAPRSCallsign, to=$CallsignPart data=$PacketPart\n"; + + # Send the mail!! + #if (&net_connect_check) { + $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; + set $tnc_output $i; + &net_mail_send(to => $CallsignPart, subject => "APRS Gateway", + text => "From $HamCall APRS Gateway\n$PacketPart"); + $i = ":" . $RealAPRSCallsign . $k . ":Your E-Mail Message has been sent.{7"; + set $tnc_output $i; + #} + #else { + # $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; + # set $tnc_output $i; + # $i = ":" . $RealAPRSCallsign . $k . ":Sorry, Gateway is currently closed.{8"; + # set $tnc_output $i; + #} + } + + # Speak any incoming APRS Bulletins + + if (substr($MsgLine, 0, 3) eq 'BLN') { + $APRSFoundAPRS = 1; + +# ($CallsignPart, $PacketPart) = (split(':', $MsgLine))[0, 1]; + ($CallsignPart, $CallsignPart, $PacketPart) = (split(':', $APRSString))[0, 1, 2]; + print_log "Incoming Bulletin from $APRSCallsign: $PacketPart"; + ## REMMED THIS NEXT STATEMENT OUT FOR SANITY + #speak "Incoming Bulletin from $APRSCallsign. $PacketPart"; + } + + # If It's an APRS Message, either say it or process the voice command: + + if (substr($MsgLine, 0, length($HamCall)) eq $HamCall) { + $APRSFoundAPRS = 1; + + ($MsgLine, $MessageAck) = (split('{', $MsgLine))[0, 1]; + ($CallsignPart, $PacketPart) = (split(':', $MsgLine))[0, 1]; + + # Let $i equals the number of spaces to put before :ack + $i = (9 - length($RealAPRSCallsign)); + $k = ' '; + $k = ($k x $i); + + # Check to see if it is a voice command to process from our CALLSIGN: + + if (substr($PacketPart, 0, 4) eq 'X10-' and + substr($RealAPRSCallsign, 0, length($HamCall)) eq $HamCall) { + + # Split the line so $PacketPart is actually the message received to process. + ($CallsignPart, $PacketPart) = (split('-', $PacketPart))[0, 1]; + + run_voice_cmd $PacketPart; + print_log "X10 received from APRS: $PacketPart"; + speak "X10 received from A P R S: $PacketPart"; + + $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; + set $tnc_output $i; + $i = ":" . $RealAPRSCallsign . $k . ":X-10 Message Received.{9"; + set $tnc_output $i; + $MsgLine = ""; + } + + # NEW in 4.52 - Check to see if its an ack. If so, don't speak it. + + elsif (substr($PacketPart, 0, 3) eq 'ack') { + print_log "Acknowledgement received from $RealAPRSCallsign"; + } + + # NEW in 4.62 - Respond to ?WX? requests with the temperature. + + elsif (substr($PacketPart, 0, 4) eq '?WX?') { + $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; + set $tnc_output $i; + $i = ":" . $RealAPRSCallsign . $k . ": Current Temperature: $CurrentTemp.{4"; + set $tnc_output $i; + $MsgLine = ""; + } + + # NEW in 4.62 - Respond to ?PHONE? requests with last call. + + elsif (substr($PacketPart, 0, 7) eq '?PHONE?') { + $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; + set $tnc_output $i; + $i = ":" . $RealAPRSCallsign . $k . ": Last Call: $PhoneName ($DisplayPhoneNumber){6"; + set $tnc_output $i; + $MsgLine = ""; + } + + # If it's not a voice command, than assume it's a standard message: + + else { + print_log "Incoming Message from $APRSCallsign: $PacketPart"; + #speak "Incoming Message from $APRSCallsign. $PacketPart"; + # THIS IS A STATUS PAGE EVENT + #if (time_greater_than("22:00") and time_less_than("15:00")) { + #$page_icq = "$PacketPart"; + #} + + $i = $RealAPRSCallsign . $k . ":ack" . $MessageAck; + set $tnc_output $i; + set $tnc_output $i; + #$i = ":" . $RealAPRSCallsign . $k . ":Message Received.{2"; + #set $tnc_output $i; + + $MsgLine = ""; + } + } + + # If it's a U2k or UII Weather Station, + # AA0SM>APRSW,N0EST,WIDE*,WIDE:_02050122c168s005g010t011r000p000P000h91b10224wU2K + + if ((substr($APRSString, ($APRSStringLength - 4), 2) eq 'dU') + || (substr($APRSString, ($APRSStringLength - 6), 6) eq 'dU2kFM') + || (substr($APRSString, ($APRSStringLength - 4), 2) eq 'wU')) { + $APRSFoundAPRS = 1; + $WXCallsign = $APRSCallsign; # Reset Variables + $WXTime = ""; + $WXLatitudeDegrees = ""; + $WXLatitudeMinutes = ""; + $WXLongitudeDegrees = ""; + $WXLongitudeMinutes = ""; + $WXLatitude = ""; + $WXLongitude = ""; + $WXDistance = ""; + $WXTemp = ""; + $WXWindDir = ""; + $WXWindSpeed = ""; + $WXWindChill = ""; + $WXHrPrecip = ""; + $WX24HrPrecip = ""; + # Added in 4.7 + $WXLstBr = ""; + $WXLstBrLat = ""; + $WXLstBrLon = ""; + $WXCompPlace = ""; + $WXCompLat = ""; + $WXCompLong = ""; + $WXCompDist = "9999"; + $WXSpeakString2 = ""; + $WXTempCompPlace = ""; + $WXTempCompDist = ""; + + # If It's a DOS Weather String, + + if ((substr($APRSString, ($APRSStringLength - 4), 2) eq 'dU') + || (substr($APRSString, ($APRSStringLength - 6), 6) eq 'dU2kFM')) { + + $WXTime = (substr($PacketPart, 3, 4)); # Time of WX Report + $WXWindDir = (substr($PacketPart, 27, 3)); # Wind Direction + $WXWindSpeed = (substr($PacketPart, 31, 3)); # Wind Speed + + # Get rid of those damn 0's in the Speed + if (substr($WXWindSpeed, 0, 2) eq '00') {$WXWindSpeed = (substr($WXWindSpeed, 2, 1))}; + if ($WXWindSpeed ne '0') { # Except if wind speed IS 0 + if (substr($WXWindSpeed, 0, 1) eq '0') {$WXWindSpeed = (substr($WXWindSpeed, 1, 2))}; + } + + $WXLatitudeDegrees = (substr($PacketPart, 8, 2)); + $WXLatitudeMinutes = (substr($PacketPart, 10, 5)); + $WXLatitude = ($WXLatitudeDegrees + ($WXLatitudeMinutes / 60)); + $WXLongitudeDegrees = (substr($PacketPart, 17, 3)); + $WXLongitudeMinutes = (substr($PacketPart, 20, 5)); + $WXLongitude = ($WXLongitudeDegrees + ($WXLongitudeMinutes / 60)); + + $WXDistance = &great_circle_distance($WXLatitude, $WXLongitude, $config_parms{latitude}, $config_parms{longitude}); + #$WXDistance = (sin $WXLatitude) * (sin $config_parms{latitude}) + (cos $WXLatitude) * (cos $config_parms{latitude}) * (cos ($config_parms{longitude}-$WXLongitude)); + #$WXDistance = 1.852 * 60 * atan2(sqrt(1 - $WXDistance * $WXDistance), $WXDistance); + #$WXDistance = $WXDistance / 1.6093440; + $WXDistance = round($WXDistance, 1); + + # Calculate if station is north/west/east/south of ours + $WXLstBrLat = ($config_parms{latitude} - $WXLatitude); + $WXLstBrLon = ($config_parms{longitude} - $WXLongitude); + if ($WXLstBrLat < 0 and $WXLstBrLon < 0) {$WXLstBr = 'northwest'}; + if ($WXLstBrLat > 0 and $WXLstBrLon < 0) {$WXLstBr = 'southwest'}; + if ($WXLstBrLat < 0 and $WXLstBrLon > 0) {$WXLstBr = 'northeast'}; + if ($WXLstBrLat > 0 and $WXLstBrLon > 0) {$WXLstBr = 'southeast'}; + if ($WXLstBrLat <= 0 and (abs($WXLstBrLon) * 2) < abs($WXLstBrLat)) {$WXLstBr = 'north'}; + if ($WXLstBrLat >= 0 and (abs($WXLstBrLon) * 2) < abs($WXLstBrLat)) {$WXLstBr = 'south'}; + if ($WXLstBrLon <= 0 and (abs($WXLstBrLat) * 2) < abs($WXLstBrLon)) {$WXLstBr = 'west'}; + if ($WXLstBrLon >= 0 and (abs($WXLstBrLat) * 2) < abs($WXLstBrLon)) {$WXLstBr = 'east'}; + } + + # For New Windows UII/U2000 String Only + + if (substr($APRSString, ($APRSStringLength - 4), 2) eq 'wU') { + $WXTime = (substr($PacketPart, 5, 4)); # Time of WX Report + $WXWindDir = (substr($PacketPart, 10, 3)); # Wind Direction + $WXWindSpeed = (substr($PacketPart, 14, 3)); # Wind Speed + + # Get rid of those damn 0's in the Speed + if (substr($WXWindSpeed, 0, 2) eq '00') {$WXWindSpeed = (substr($WXWindSpeed, 2, 1))}; + if ($WXWindSpeed ne '0') { # Except if wind speed IS 0 + if (substr($WXWindSpeed, 0, 1) eq '0') {$WXWindSpeed = (substr($WXWindSpeed, 1, 2))}; + } + } + + # All Weather Stations Process the Following + + $WXWindDirVoice = "north" if ($WXWindDir >= 0 and $WXWindDir <= 11); + $WXWindDirVoice = "north-northeast" if ($WXWindDir >= 12 and $WXWindDir <= 33); + $WXWindDirVoice = "northeast" if ($WXWindDir >= 34 and $WXWindDir <= 55); + $WXWindDirVoice = "east-northeast" if ($WXWindDir >= 56 and $WXWindDir <= 77); + $WXWindDirVoice = "east" if ($WXWindDir >= 78 and $WXWindDir <= 99); + $WXWindDirVoice = "east-southeast" if ($WXWindDir >= 100 and $WXWindDir <= 121); + $WXWindDirVoice = "southeast" if ($WXWindDir >= 122 and $WXWindDir <= 143); + $WXWindDirVoice = "south-southeast" if ($WXWindDir >= 144 and $WXWindDir <= 165); + $WXWindDirVoice = "south" if ($WXWindDir >= 166 and $WXWindDir <= 187); + $WXWindDirVoice = "south-southwest" if ($WXWindDir >= 188 and $WXWindDir <= 209); + $WXWindDirVoice = "southwest" if ($WXWindDir >= 210 and $WXWindDir <= 231); + $WXWindDirVoice = "west-southwest" if ($WXWindDir >= 232 and $WXWindDir <= 253); + $WXWindDirVoice = "west" if ($WXWindDir >= 254 and $WXWindDir <= 275); + $WXWindDirVoice = "west-northwest" if ($WXWindDir >= 276 and $WXWindDir <= 297); + $WXWindDirVoice = "northwest" if ($WXWindDir >= 298 and $WXWindDir <= 319); + $WXWindDirVoice = "north-northwest" if ($WXWindDir >= 320 and $WXWindDir <= 341); + $WXWindDirVoice = "north" if ($WXWindDir >= 342 and $WXWindDir <= 360); + +####### +# Added 4.7 + + # Calculate bearing from the Position file + foreach $WXTempCompLine (@gpscomplines) { + ($WXTempCompPlace, $WXTempCompLat, $WXTempCompLong) = (split(',', $WXTempCompLine))[0, 1, 2]; + + # Calculate distance station is away from pos file + $WXTempCompDist = &great_circle_distance($WXLatitude, $WXLongitude, $WXTempCompLat, $WXTempCompLong); + #$WXTempCompDist = (sin $WXLatitude) * (sin $WXTempCompLat) + (cos $WXLatitude) * (cos $WXTempCompLat) * (cos ($WXTempCompLong-$WXLongitude)); + #$WXTempCompDist = 1.852 * 60 * atan2(sqrt(1 - $WXTempCompDist * $WXTempCompDist), $WXTempCompDist); + #$WXTempCompDist = $WXTempCompDist / 1.6093440; + $WXTempCompDist = round($WXTempCompDist, 1); + + if ($WXTempCompDist < 150 and $WXTempCompDist < $WXCompDist) { + $WXCompPlace = $WXTempCompPlace; + $WXCompDist = $WXTempCompDist; + $WXCompLat = $WXTempCompLat; + $WXCompLong = $WXTempCompLong; + } + } + + # Calculate if station is north/west/east/south of POSFILE + $WXCompLstBrLat = ($WXCompLat - $WXLatitude); + $WXCompLstBrLon = ($WXCompLong - $WXLongitude); + if ($WXCompLstBrLat < 0 and $WXCompLstBrLon < 0) {$WXCompLstBr = 'northwest'}; + if ($WXCompLstBrLat > 0 and $WXCompLstBrLon < 0) {$WXCompLstBr = 'southwest'}; + if ($WXCompLstBrLat < 0 and $WXCompLstBrLon > 0) {$WXCompLstBr = 'northeast'}; + if ($WXCompLstBrLat > 0 and $WXCompLstBrLon > 0) {$WXCompLstBr = 'southeast'}; + if ($WXCompLstBrLat <= 0 and (abs($WXCompLstBrLon) * 2) < abs($WXCompLstBrLat)) {$WXCompLstBr = 'north'}; + if ($WXCompLstBrLat >= 0 and (abs($WXCompLstBrLon) * 2) < abs($WXCompLstBrLat)) {$WXCompLstBr = 'south'}; + if ($WXCompLstBrLon <= 0 and (abs($WXCompLstBrLat) * 2) < abs($WXCompLstBrLon)) {$WXCompLstBr = 'west'}; + if ($WXCompLstBrLon >= 0 and (abs($WXCompLstBrLat) * 2) < abs($WXCompLstBrLon)) {$WXCompLstBr = 'east'}; + + # Add bearing from station in position file IF it's a new position report + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance)) and ($GPSCompDist ne '9999')) { + $WXSpeakString2 = "Station is located $WXCompDist miles $WXCompLstBr of $WXCompPlace."; + } + +####### + + if (substr($PacketPart, 35, 1) eq 'T') { # If a traditional, + $WXTemp = (substr($PacketPart, 36, 3)); + # Get rid of those damn 0's in the Temperature + if (substr($WXTemp, 0, 2) eq '00') {$WXTemp = (substr($WXTemp, 2, 1))}; + if ($WXTemp ne '0') { # Except if temp IS 0 + if (substr($WXTemp, 0, 1) eq '0') {$WXTemp = (substr($WXTemp, 1, 2))}; + } + + # Calculate Wind Chill if temp is less than 45 degrees + if ($WXTemp <= 45) { + $WXWindChill = .0817 * (3.71 * sqrt($WXWindSpeed) + 5.81 - .25 * $WXWindSpeed) * ($WXTemp - 91.4) + 91.4; + $WXWindChill = round($WXWindChill, 0); + if ($WXWindSpeed <= 5) {$WXWindChill = $WXTemp}; + } + + # If Temperature is above freezing, make it equal. + if ($WXTemp >= 46) { + $WXWindChill = $WXTemp; + } + + # Add Readings for Last Hour Precip and Last 24 Hour Precip + $WXHrPrecip = (substr($PacketPart, 41, 3)); + # Get rid of those damn 0's in the Precip + if (substr($WXHrPrecip, 0, 2) eq '00') {$WXHrPrecip = (substr($WXHrPrecip, 2, 1))}; + if ($WXHrPrecip ne '0') { # Except if precip IS 0 + if (substr($WXHrPrecip, 0, 1) eq '0') {$WXHrPrecip = (substr($WXHrPrecip, 1, 2))}; + } + # Divide Precip by 10 + $WXHrPrecip = $WXHrPrecip / 10; + $WX24HrPrecip = (substr($PacketPart, 45, 3)); + # Get rid of those damn 0's in the 24 hr Precip + if (substr($WX24HrPrecip, 0, 2) eq '00') {$WX24HrPrecip = (substr($WX24HrPrecip, 2, 1))}; + if ($WX24HrPrecip ne '0') { # Except if precip IS 0 + if (substr($WX24HrPrecip, 0, 1) eq '0') {$WX24HrPrecip = (substr($WX24HrPrecip, 1, 2))}; + } + # Divide Precip by 10 + $WX24HrPrecip = $WX24HrPrecip / 10; + + # If It's not the same as the last report, say it. + + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed ne '0')) { + $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is out of the $WXWindDirVoice at $WXWindSpeed miles an hour."; + if ($WXTemp <= 45 and $WXTemp ne $WXWindChill) {$WXSpeakString = $WXSpeakString . " The winnd chill is $WXWindChill."}; + if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; + # NEW in 4.7 - Add bearing from POSFILE + $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; + + print_log "$WXSpeakString"; + + if (($config_parms{tracking_speakflag} == 2) || + ($config_parms{tracking_speakflag} == 3)) + {speak $WXSpeakString}; + } + + # If the wind is calm, say the following. + + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed eq '0')) { + $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is calm."; + if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; + # NEW in 4.7 - Add bearing from POSFILE + $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; + + print_log "$WXSpeakString"; + + if (($config_parms{tracking_speakflag} == 2) || + ($config_parms{tracking_speakflag} == 3)) + {speak $WXSpeakString}; + } + + $LastWXCallsign = $WXCallsign; # Save last WX rpt information + $LastWXDistance = $WXDistance; + $LastWXTemp = $WXTemp; + $LastWXWindDir = $WXWindDir; + $LastWXWindSpeed = $WXWindSpeed; + $LastWXWindChill = $WXWindChill; + $LastWXHrPrecip = $WXHrPrecip; + $LastWX24HrPrecip = $WX24HrPrecip; + + } + + elsif (substr($PacketPart, 38, 1) eq 't') { # If a new format, + $WXTemp = (substr($PacketPart, 39, 3)); + # Get rid of those damn 0's in the Temperature + if (substr($WXTemp, 0, 2) eq '00') {$WXTemp = (substr($WXTemp, 2, 1))}; + if ($WXTemp ne '0') { # Except if temp IS 0 + if (substr($WXTemp, 0, 1) eq '0') {$WXTemp = (substr($WXTemp, 1, 2))}; + } + + # Calculate Wind Chill if temp is less than 45 degrees + if ($WXTemp <= 45) { + $WXWindChill = .0817 * (3.71 * sqrt($WXWindSpeed) + 5.81 - .25 * $WXWindSpeed) * ($WXTemp - 91.4) + 91.4; + $WXWindChill = round($WXWindChill, 0); + if ($WXWindSpeed <= 5) {$WXWindChill = $WXTemp}; + } + + # If Temperature is above freezing, make it equal. + if ($WXTemp >= 46) { + $WXWindChill = $WXTemp; + } + + # Add Readings for Last Hour Precip and Last 24 Hour Precip + $WXHrPrecip = (substr($PacketPart, 43, 3)); + # Get rid of those damn 0's in the Precip + if (substr($WXHrPrecip, 0, 2) eq '00') {$WXHrPrecip = (substr($WXHrPrecip, 2, 1))}; + if ($WXHrPrecip ne '0') { # Except if precip IS 0 + if (substr($WXHrPrecip, 0, 1) eq '0') {$WXHrPrecip = (substr($WXHrPrecip, 1, 2))}; + } + # Divide Precip by 10 + $WXHrPrecip = $WXHrPrecip / 10; + $WX24HrPrecip = (substr($PacketPart, 47, 3)); + # Get rid of those damn 0's in the 24 hr Precip + if (substr($WX24HrPrecip, 0, 2) eq '00') {$WX24HrPrecip = (substr($WX24HrPrecip, 2, 1))}; + if ($WX24HrPrecip ne '0') { # Except if precip IS 0 + if (substr($WX24HrPrecip, 0, 1) eq '0') {$WX24HrPrecip = (substr($WX24HrPrecip, 1, 2))}; + } + # Divide Precip by 10 + $WX24HrPrecip = $WX24HrPrecip / 10; + + # If It's not the same as the last report, say it. + + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed ne '0')) { + $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is out of the $WXWindDirVoice at $WXWindSpeed miles an hour."; + if ($WXTemp <= 45 and $WXTemp ne $WXWindChill) {$WXSpeakString = $WXSpeakString . " The winnd chill is $WXWindChill."}; + if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; + # NEW in 4.7 - Add bearing from POSFILE + $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; + + print_log "$WXSpeakString"; + + if (($config_parms{tracking_speakflag} == 2) || + ($config_parms{tracking_speakflag} == 3)) + {speak $WXSpeakString}; + } + + # If the wind is calm, say the following. + + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed eq '0')) { + $WXSpeakString = "At $WXDistance miles $WXLstBr of us, temperature is $WXTemp degrees. Winnd is calm."; + if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; + # NEW in 4.7 - Add bearing from POSFILE + $WXSpeakString = $WXSpeakString . " $WXSpeakString2"; + + print_log "$WXSpeakString"; + + if (($config_parms{tracking_speakflag} == 2) || + ($config_parms{tracking_speakflag} == 3)) + {speak $WXSpeakString}; + } + + $LastWXCallsign = $WXCallsign; # Save last WX rpt information + $LastWXDistance = $WXDistance; + $LastWXTemp = $WXTemp; + $LastWXWindDir = $WXWindDir; + $LastWXWindSpeed = $WXWindSpeed; + $LastWXWindChill = $WXWindChill; + $LastWXHrPrecip = $WXHrPrecip; + $LastWX24HrPrecip = $WX24HrPrecip; + + } + + elsif (substr($PacketPart, 21, 1) eq 't') { # If WINDOWS format, + $WXDistance = 999.9; # Because we don't + # have the position + $WXTemp = (substr($PacketPart, 22, 3)); + # Get rid of those damn 0's in the Temperature + if (substr($WXTemp, 0, 2) eq '00') {$WXTemp = (substr($WXTemp, 2, 1))}; + if ($WXTemp ne '0') { # Except if temp IS 0 + if (substr($WXTemp, 0, 1) eq '0') {$WXTemp = (substr($WXTemp, 1, 2))}; + } + + # Calculate Wind Chill if temp is less than 45 degrees + if ($WXTemp <= 45) { + $WXWindChill = .0817 * (3.71 * sqrt($WXWindSpeed) + 5.81 - .25 * $WXWindSpeed) * ($WXTemp - 91.4) + 91.4; + $WXWindChill = round($WXWindChill, 0); + if ($WXWindSpeed <= 5) {$WXWindChill = $WXTemp}; + } + + # If Temperature is above freezing, make it equal. + if ($WXTemp >= 45) { + $WXWindChill = $WXTemp; + } + + # Add Readings for Last Hour Precip and Last 24 Hour Precip + $WXHrPrecip = (substr($PacketPart, 26, 3)); + # Get rid of those damn 0's in the Precip + if (substr($WXHrPrecip, 0, 2) eq '00') {$WXHrPrecip = (substr($WXHrPrecip, 2, 1))}; + if ($WXHrPrecip ne '0') { # Except if precip IS 0 + if (substr($WXHrPrecip, 0, 1) eq '0') {$WXHrPrecip = (substr($WXHrPrecip, 1, 2))}; + } + # Divide Precip by 10 + $WXHrPrecip = $WXHrPrecip / 10; + $WX24HrPrecip = (substr($PacketPart, 30, 3)); + # Get rid of those damn 0's in the 24 hr Precip + if (substr($WX24HrPrecip, 0, 2) eq '00') {$WX24HrPrecip = (substr($WX24HrPrecip, 2, 1))}; + if ($WX24HrPrecip ne '0') { # Except if precip IS 0 + if (substr($WX24HrPrecip, 0, 1) eq '0') {$WX24HrPrecip = (substr($WX24HrPrecip, 1, 2))}; + } + # Divide Precip by 10 + $WX24HrPrecip = $WX24HrPrecip / 10; + + # If It's not the same as the last report, say it. + + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed ne '0')) { + $WXSpeakString = "$APRSCallsign reports temperature of $WXTemp degrees. Winnd is out of the $WXWindDirVoice at $WXWindSpeed miles an hour."; + if ($WXTemp <= 45 and $WXTemp ne $WXWindChill) {$WXSpeakString = $WXSpeakString . " The winnd chill is $WXWindChill."}; + if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; + + print_log "$WXSpeakString"; + + if (($config_parms{tracking_speakflag} == 2) || + ($config_parms{tracking_speakflag} == 3)) + {speak $WXSpeakString}; + } + + # If the wind is calm, say the following. + + if ((($WXCallsign ne $LastWXCallsign) || ($WXDistance ne $LastWXDistance) || ($WXTemp ne $LastWXTemp)) and ($WXWindSpeed eq '0')) { + $WXSpeakString = "$APRSCallsign reports temperature of $WXTemp degrees. Winnd is calm."; + if ($WXHrPrecip != 0) {$WXSpeakString = $WXSpeakString . " $WXHrPrecip inches of rain in the last hour."}; + + print_log "$WXSpeakString"; + + if (($config_parms{tracking_speakflag} == 2) || + ($config_parms{tracking_speakflag} == 3)) + {speak $WXSpeakString}; + } + + $LastWXCallsign = $WXCallsign; # Save last WX rpt information + $LastWXDistance = $WXDistance; + $LastWXTemp = $WXTemp; + $LastWXWindDir = $WXWindDir; + $LastWXWindSpeed = $WXWindSpeed; + $LastWXWindChill = $WXWindChill; + $LastWXHrPrecip = $WXHrPrecip; + $LastWX24HrPrecip = $WX24HrPrecip; + } + + else + { + print_log "A funny weather station."; + } + + if ($CurrentTempDist >= $WXDistance) { # If a closer rpt, + $CurrentTempDist = $WXDistance; # change the current + $CurrentTemp = $WXTemp; # temp variable. + $CurrentChill = $WXWindChill; + $CurrentHrPrecip = $WXHrPrecip; + $Current24HrPrecip = $WX24HrPrecip; + } + + if ($CurrentTempDist eq '') { # If 1st report + $CurrentTempDist = $WXDistance; # of the day, change + $CurrentTemp = $WXTemp; # the temp variable. + $CurrentChill = $WXWindChill; + $CurrentHrPrecip = $WXHrPrecip; + $Current24HrPrecip = $WX24HrPrecip; + } + } + + #elsif (substr($APRSString, $i, 1) eq '*') { # Else if we find a "*", + # $APRSPacketDigi = 1; # It got digipeated + # print_log "Packet was Digipeated."; + #} + #else + #{ + #} + + } +} + +sub great_circle_distance { + my ($lat1, $lon1, $lat2, $lon2) = map {°rees_to_radians($_)} @_; +# my $radius = 6367; # km + my $radius = 3956; # miles + my $d = (sin(($lat2 - $lat1) / 2)) ** 2 + cos($lat1) * cos($lat2) * +(sin(($lon2 - $lon1) / 2)) ** 2; + $d = $radius * 2 * atan2(sqrt($d), sqrt(1 - $d)); +# print "db d=$d l=$lat1,$lon1,$lat2,$lon2\n"; + return round($d, 1); +} + +#EGIN { $::pi = 4 * atan2(1,1); } +sub degrees_to_radians { + return $_[0] * 3.14159265 / 180.0; +} diff --git a/code/public/Brian/phonestuff.pl b/code/public/Brian/phonestuff.pl index 1133f653e..6257c8f9c 100644 --- a/code/public/Brian/phonestuff.pl +++ b/code/public/Brian/phonestuff.pl @@ -1,34 +1,34 @@ -############################################################ -# Klier Home Automation - Caller ID Module for Rockwell # -# Version 2.2 Release # -# By: Brian J. Klier, N0QVC # -# Thanks for the mucho help from: Bruce Winter # -# E-Mail: klier@lakes.com # -# Webpage: http://www.faribault.k12.mn.us/brian # -############################################################ - -# Category=Phone - -# Declare Variables - -use vars qw($CompPhoneNumber $PhoneName $PhoneNumber $PhoneTime $PhoneDate $PhoneNumberSpoken); - -# Set Variables used by my custom code to match the callerid.pl - -$PhoneName = $cid_item->name(); -$PhoneNumber = $cid_item->number(); -$PhoneTime = localtime($cid_item->{set_time}); - -# IF Phone Rings, play a wave file. - -if (state_now $cid_interface1 eq 'ring') { - play('file' => 'c:\mh\sounds\st-ring230.wav'); - #set $TV 'mute'; -} - -# Page my phone if there's a new call - -if ($CompPhoneNumber ne $PhoneNumber) { - if (state $current_away_mode eq 'away') {$page_email = "Call from $PhoneName - $PhoneNumber - $PhoneTime"}; - $CompPhoneNumber = $PhoneNumber; -} +############################################################ +# Klier Home Automation - Caller ID Module for Rockwell # +# Version 2.2 Release # +# By: Brian J. Klier, N0QVC # +# Thanks for the mucho help from: Bruce Winter # +# E-Mail: klier@lakes.com # +# Webpage: http://www.faribault.k12.mn.us/brian # +############################################################ + +# Category=Phone + +# Declare Variables + +use vars qw($CompPhoneNumber $PhoneName $PhoneNumber $PhoneTime $PhoneDate $PhoneNumberSpoken); + +# Set Variables used by my custom code to match the callerid.pl + +$PhoneName = $cid_item->name(); +$PhoneNumber = $cid_item->number(); +$PhoneTime = localtime($cid_item->{set_time}); + +# IF Phone Rings, play a wave file. + +if (state_now $cid_interface1 eq 'ring') { + play('file' => 'c:\mh\sounds\st-ring230.wav'); + #set $TV 'mute'; +} + +# Page my phone if there's a new call + +if ($CompPhoneNumber ne $PhoneNumber) { + if (state $current_away_mode eq 'away') {$page_email = "Call from $PhoneName - $PhoneNumber - $PhoneTime"}; + $CompPhoneNumber = $PhoneNumber; +} diff --git a/code/public/Brian/radar-wx.pl b/code/public/Brian/radar-wx.pl index 10b8fd23f..e789572be 100644 --- a/code/public/Brian/radar-wx.pl +++ b/code/public/Brian/radar-wx.pl @@ -1,193 +1,193 @@ -# This program retrieves current condition information from the Airport 10 minutes past each -# hour, and then sends an APRS packet with this information every hour. - -# Category=Vehicles - -my ($DownloadedWindDirection, $DownloadedBarometer, $DownloadedWindSpeed, $DownloadedWindGust, $DownloadedTemp, $DownloadedHumidity, $tempwxsend); -my ($DownloadedRainTotal, $DownloadedRainRate); -my ($DownDay, $DownHour, $DownMinute); - -if (state $enable_transmit eq 'yes' and time_cron('6,15,21,36,45,51 * * * *')) {run_voice_cmd "Send Faribault Airport Weather"}; - -$v_send_remotewx = new Voice_Cmd("Send Faribault Airport Weather"); - -if ($state = said $v_send_remotewx) { - if (substr($Weather{Wind}, 0, 3) eq 'NNE') { - $DownloadedWindDirection = "023"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'ENE') { - $DownloadedWindDirection = "067"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'ESE') { - $DownloadedWindDirection = "112"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'SSE') { - $DownloadedWindDirection = "157"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'SSW') { - $DownloadedWindDirection = "202"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'WSW') { - $DownloadedWindDirection = "257"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'WNW') { - $DownloadedWindDirection = "292"; - } - elsif (substr($Weather{Wind}, 0, 3) eq 'NNW') { - $DownloadedWindDirection = "337"; - } - elsif (substr($Weather{Wind}, 0, 2) eq 'NE') { - $DownloadedWindDirection = "045"; - } - elsif (substr($Weather{Wind}, 0, 2) eq 'SE') { - $DownloadedWindDirection = "135"; - } - elsif (substr($Weather{Wind}, 0, 2) eq 'SW') { - $DownloadedWindDirection = "235"; - } - elsif (substr($Weather{Wind}, 0, 2) eq 'NW') { - $DownloadedWindDirection = "315"; - } - elsif (substr($Weather{Wind}, 0, 1) eq 'N') { - $DownloadedWindDirection = "360"; - } - elsif (substr($Weather{Wind}, 0, 1) eq 'E') { - $DownloadedWindDirection = "090"; - } - elsif (substr($Weather{Wind}, 0, 1) eq 'S') { - $DownloadedWindDirection = "180"; - } - elsif (substr($Weather{Wind}, 0, 1) eq 'W') { - $DownloadedWindDirection = "270"; - } - else { - $DownloadedWindDirection = "360"; - } - -# for ($i = 0; $i != (length($Weather{Wind})); ++$i) { -# if (substr($Weather{Wind}, $i, 3) eq 'mph') { -# $DownloadedWindSpeed = substr($Weather{Wind}, ($i-2), 2); -# } -# } - - $DownloadedWindSpeed = $Weather{WindAvgSpeed}; - $DownloadedWindSpeed = round($DownloadedWindSpeed); - - if (length(abs($DownloadedWindSpeed)) == 2) { - $DownloadedWindSpeed = "0" . $DownloadedWindSpeed; - } - elsif (length(abs($DownloadedWindSpeed)) == 1) { - $DownloadedWindSpeed = "00" . $DownloadedWindSpeed; - } - else { - $DownloadedWindSpeed = "000"; - } - - if ($DownloadedWindSpeed == 0) {$DownloadedWindSpeed = "000"}; - $DownloadedWindSpeed = substr($DownloadedWindSpeed, 0, 3); - -# for ($i = 0; $i != (length($Weather{Wind})); ++$i) { -# if (substr($Weather{Wind}, $i, 2) eq 'to') { -# $DownloadedWindGust = substr($Weather{Wind}, ($i+3), 2); -# } -# } - - $DownloadedWindGust = $Weather{WindGustSpeed}; - $DownloadedWindGust = round($DownloadedWindGust); - - if (length(abs($DownloadedWindGust)) == 2) { - $DownloadedWindGust = "0" . $DownloadedWindGust; - } - elsif (length(abs($DownloadedWindGust)) == 1) { - $DownloadedWindGust = "00" . $DownloadedWindGust; - } - else { - $DownloadedWindGust = "000"; - } - - if ($DownloadedWindGust == 0) {$DownloadedWindGust = "000"}; - $DownloadedWindGust = substr($DownloadedWindGust, 0, 3); - - $DownloadedTemp = $Weather{TempOutdoor}; - $DownloadedTemp = round($DownloadedTemp); - - if (length($DownloadedTemp) == 2) { - $DownloadedTemp = "0" . $DownloadedTemp; - } - elsif (length($DownloadedTemp) == 1) { - $DownloadedTemp = "00" . $DownloadedTemp; - } - else { - $DownloadedTemp = $DownloadedTemp; - } - - if (length($Mday) == 1) { - $DownDay = "0" . $Mday; - } - else { - $DownDay = $Mday; - } - - if (length($Hour) == 1) { - $DownHour = "0" . $Hour; - } - else { - $DownHour = $Hour; - } - - if (length($Minute) == 1) { - $DownMinute = "0" . $Minute; - } - else { - $DownMinute = $Minute; - } - - print_log "Rain: $Weather{RainTotal} --- Rain Rate: $Weather{RainRate}"; - $DownloadedBarometer = round(($Weather{BaromSea} / .03) * 10); - if (length($DownloadedBarometer) == 4) {$DownloadedBarometer = "0" . $DownloadedBarometer}; - - $DownloadedHumidity = $Weather{HumidOutdoor}; - $DownloadedHumidity = round($DownloadedHumidity); - if ($DownloadedHumidity eq '100') {$DownloadedHumidity = "00"}; - - $DownloadedRainTotal = $Weather{RainTotal} * 100; - $DownloadedRainRate = $Weather{RainRate} * 100; - - if (length(abs($DownloadedRainTotal)) == 2) { - $DownloadedRainTotal = "0" . $DownloadedRainTotal; - } - elsif (length(abs($DownloadedRainTotal)) == 1) { - $DownloadedRainTotal = "00" . $DownloadedRainTotal; - } - else { - $DownloadedRainTotal = "000"; - } - - if ($DownloadedRainTotal == 0) {$DownloadedRainTotal = "000"}; - - if (length(abs($DownloadedRainRate)) == 2) { - $DownloadedRainRate = "0" . $DownloadedRainRate; - } - elsif (length(abs($DownloadedRainRate)) == 1) { - $DownloadedRainRate = "00" . $DownloadedRainRate; - } - else { - $DownloadedRainRate = "000"; - } - - if ($DownloadedRainRate == 0) {$DownloadedRainRate = "000"}; - - if (($Minute eq '15') || ($Minute eq '45')) { - - #$tempwxsend = "$HamCall>APRSMH,TCPIP*:;FRBLT *" . $DownDay . $DownHour . $DownMinute . "/4416.83N/09317.13W_" . $DownloadedWindDirection . "/" . $DownloadedWindSpeed . "g" . $DownloadedWindGust . "t" . $DownloadedTemp . "r...p...P...h" . $DownloadedHumidity . "b" . $DownloadedBarometer . "dU2k"; - $tempwxsend = "$HamCall>APRSMH,TCPIP*:;FRBLT *" . $DownDay . $DownHour . $DownMinute . "/4416.83N/09317.13W_" . $DownloadedWindDirection . "/" . $DownloadedWindSpeed . "g" . $DownloadedWindGust . "t" . $DownloadedTemp . "r" . $DownloadedRainRate . "p" . $DownloadedRainTotal . "P...h" . $DownloadedHumidity . "b" . $DownloadedBarometer . "dU2k"; - } - else { - $tempwxsend = "$HamCall>APRSMH,TCPIP*:;KFBL *" . $DownDay . $DownHour . $DownMinute . "/4419.62N/09318.48W_" . $DownloadedWindDirection . "/" . $DownloadedWindSpeed . "g" . $DownloadedWindGust . "t" . $DownloadedTemp . "r...p...P...h" . $DownloadedHumidity . "b" . $DownloadedBarometer . "dU2k"; - } - - print_log $tempwxsend; - - if (state $enable_transmit eq 'yes') {set $tnc_output $tempwxsend}; - set $telnet $tempwxsend if active $telnet; -} +# This program retrieves current condition information from the Airport 10 minutes past each +# hour, and then sends an APRS packet with this information every hour. + +# Category=Vehicles + +my ($DownloadedWindDirection, $DownloadedBarometer, $DownloadedWindSpeed, $DownloadedWindGust, $DownloadedTemp, $DownloadedHumidity, $tempwxsend); +my ($DownloadedRainTotal, $DownloadedRainRate); +my ($DownDay, $DownHour, $DownMinute); + +if (state $enable_transmit eq 'yes' and time_cron('6,15,21,36,45,51 * * * *')) {run_voice_cmd "Send Faribault Airport Weather"}; + +$v_send_remotewx = new Voice_Cmd("Send Faribault Airport Weather"); + +if ($state = said $v_send_remotewx) { + if (substr($Weather{Wind}, 0, 3) eq 'NNE') { + $DownloadedWindDirection = "023"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'ENE') { + $DownloadedWindDirection = "067"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'ESE') { + $DownloadedWindDirection = "112"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'SSE') { + $DownloadedWindDirection = "157"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'SSW') { + $DownloadedWindDirection = "202"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'WSW') { + $DownloadedWindDirection = "257"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'WNW') { + $DownloadedWindDirection = "292"; + } + elsif (substr($Weather{Wind}, 0, 3) eq 'NNW') { + $DownloadedWindDirection = "337"; + } + elsif (substr($Weather{Wind}, 0, 2) eq 'NE') { + $DownloadedWindDirection = "045"; + } + elsif (substr($Weather{Wind}, 0, 2) eq 'SE') { + $DownloadedWindDirection = "135"; + } + elsif (substr($Weather{Wind}, 0, 2) eq 'SW') { + $DownloadedWindDirection = "235"; + } + elsif (substr($Weather{Wind}, 0, 2) eq 'NW') { + $DownloadedWindDirection = "315"; + } + elsif (substr($Weather{Wind}, 0, 1) eq 'N') { + $DownloadedWindDirection = "360"; + } + elsif (substr($Weather{Wind}, 0, 1) eq 'E') { + $DownloadedWindDirection = "090"; + } + elsif (substr($Weather{Wind}, 0, 1) eq 'S') { + $DownloadedWindDirection = "180"; + } + elsif (substr($Weather{Wind}, 0, 1) eq 'W') { + $DownloadedWindDirection = "270"; + } + else { + $DownloadedWindDirection = "360"; + } + +# for ($i = 0; $i != (length($Weather{Wind})); ++$i) { +# if (substr($Weather{Wind}, $i, 3) eq 'mph') { +# $DownloadedWindSpeed = substr($Weather{Wind}, ($i-2), 2); +# } +# } + + $DownloadedWindSpeed = $Weather{WindAvgSpeed}; + $DownloadedWindSpeed = round($DownloadedWindSpeed); + + if (length(abs($DownloadedWindSpeed)) == 2) { + $DownloadedWindSpeed = "0" . $DownloadedWindSpeed; + } + elsif (length(abs($DownloadedWindSpeed)) == 1) { + $DownloadedWindSpeed = "00" . $DownloadedWindSpeed; + } + else { + $DownloadedWindSpeed = "000"; + } + + if ($DownloadedWindSpeed == 0) {$DownloadedWindSpeed = "000"}; + $DownloadedWindSpeed = substr($DownloadedWindSpeed, 0, 3); + +# for ($i = 0; $i != (length($Weather{Wind})); ++$i) { +# if (substr($Weather{Wind}, $i, 2) eq 'to') { +# $DownloadedWindGust = substr($Weather{Wind}, ($i+3), 2); +# } +# } + + $DownloadedWindGust = $Weather{WindGustSpeed}; + $DownloadedWindGust = round($DownloadedWindGust); + + if (length(abs($DownloadedWindGust)) == 2) { + $DownloadedWindGust = "0" . $DownloadedWindGust; + } + elsif (length(abs($DownloadedWindGust)) == 1) { + $DownloadedWindGust = "00" . $DownloadedWindGust; + } + else { + $DownloadedWindGust = "000"; + } + + if ($DownloadedWindGust == 0) {$DownloadedWindGust = "000"}; + $DownloadedWindGust = substr($DownloadedWindGust, 0, 3); + + $DownloadedTemp = $Weather{TempOutdoor}; + $DownloadedTemp = round($DownloadedTemp); + + if (length($DownloadedTemp) == 2) { + $DownloadedTemp = "0" . $DownloadedTemp; + } + elsif (length($DownloadedTemp) == 1) { + $DownloadedTemp = "00" . $DownloadedTemp; + } + else { + $DownloadedTemp = $DownloadedTemp; + } + + if (length($Mday) == 1) { + $DownDay = "0" . $Mday; + } + else { + $DownDay = $Mday; + } + + if (length($Hour) == 1) { + $DownHour = "0" . $Hour; + } + else { + $DownHour = $Hour; + } + + if (length($Minute) == 1) { + $DownMinute = "0" . $Minute; + } + else { + $DownMinute = $Minute; + } + + print_log "Rain: $Weather{RainTotal} --- Rain Rate: $Weather{RainRate}"; + $DownloadedBarometer = round(($Weather{BaromSea} / .03) * 10); + if (length($DownloadedBarometer) == 4) {$DownloadedBarometer = "0" . $DownloadedBarometer}; + + $DownloadedHumidity = $Weather{HumidOutdoor}; + $DownloadedHumidity = round($DownloadedHumidity); + if ($DownloadedHumidity eq '100') {$DownloadedHumidity = "00"}; + + $DownloadedRainTotal = $Weather{RainTotal} * 100; + $DownloadedRainRate = $Weather{RainRate} * 100; + + if (length(abs($DownloadedRainTotal)) == 2) { + $DownloadedRainTotal = "0" . $DownloadedRainTotal; + } + elsif (length(abs($DownloadedRainTotal)) == 1) { + $DownloadedRainTotal = "00" . $DownloadedRainTotal; + } + else { + $DownloadedRainTotal = "000"; + } + + if ($DownloadedRainTotal == 0) {$DownloadedRainTotal = "000"}; + + if (length(abs($DownloadedRainRate)) == 2) { + $DownloadedRainRate = "0" . $DownloadedRainRate; + } + elsif (length(abs($DownloadedRainRate)) == 1) { + $DownloadedRainRate = "00" . $DownloadedRainRate; + } + else { + $DownloadedRainRate = "000"; + } + + if ($DownloadedRainRate == 0) {$DownloadedRainRate = "000"}; + + if (($Minute eq '15') || ($Minute eq '45')) { + + #$tempwxsend = "$HamCall>APRSMH,TCPIP*:;FRBLT *" . $DownDay . $DownHour . $DownMinute . "/4416.83N/09317.13W_" . $DownloadedWindDirection . "/" . $DownloadedWindSpeed . "g" . $DownloadedWindGust . "t" . $DownloadedTemp . "r...p...P...h" . $DownloadedHumidity . "b" . $DownloadedBarometer . "dU2k"; + $tempwxsend = "$HamCall>APRSMH,TCPIP*:;FRBLT *" . $DownDay . $DownHour . $DownMinute . "/4416.83N/09317.13W_" . $DownloadedWindDirection . "/" . $DownloadedWindSpeed . "g" . $DownloadedWindGust . "t" . $DownloadedTemp . "r" . $DownloadedRainRate . "p" . $DownloadedRainTotal . "P...h" . $DownloadedHumidity . "b" . $DownloadedBarometer . "dU2k"; + } + else { + $tempwxsend = "$HamCall>APRSMH,TCPIP*:;KFBL *" . $DownDay . $DownHour . $DownMinute . "/4419.62N/09318.48W_" . $DownloadedWindDirection . "/" . $DownloadedWindSpeed . "g" . $DownloadedWindGust . "t" . $DownloadedTemp . "r...p...P...h" . $DownloadedHumidity . "b" . $DownloadedBarometer . "dU2k"; + } + + print_log $tempwxsend; + + if (state $enable_transmit eq 'yes') {set $tnc_output $tempwxsend}; + set $telnet $tempwxsend if active $telnet; +} diff --git a/code/public/Brian/stormwarning.pl b/code/public/Brian/stormwarning.pl index afc4f04f7..634fbfc46 100644 --- a/code/public/Brian/stormwarning.pl +++ b/code/public/Brian/stormwarning.pl @@ -1,43 +1,43 @@ -# Category = Weather - -my $wx_warnings = "$config_parms{data_dir}/web/wx_warnings"; -$p_wx_warnings = new Process_Item "get_url http://www.weather.gov/alerts/mn.cap $wx_warnings.xml"; -$v_wx_warnings = new Voice_Cmd '[Get,Show,Display,Read,Parse] the latest NWS Announcements'; - -$state = said $v_wx_warnings; -if ($state eq 'Get' or time_cron('1,11,21,31,41,51 10,11,12,13,14,15,16,17,18,19,20 * * *')) { - print_log "Getting NWS Alerts"; - start $p_wx_warnings; -} -display "$wx_warnings.txt" if $state eq 'Show' or $state eq 'Display'; -speak "$wx_warnings.txt" if $state eq 'Read'; - -if (done_now $p_wx_warnings or $state eq 'Parse') { - my ($finalsummarytx, $finalsummary, $summary, $tempcode, $localwarnflag, $i); - $localwarnflag = 0; - for (file_read "$wx_warnings.xml") { - $summary = ""; - $summary .= "The national weather service has issued a $1" if /(.+)<\/cap:event>/; - $summary .= " .. " if /(.+)<\/cap:expires>/; - #$summary .= " until $1 for Rice County." if /(.+)<\/cap:expires>/; - $summary .= "$1." if /(.+)<\/cap:description>/; - $tempcode = "$1" if /(.+)<\/cap:geocode>/; - if ($tempcode eq '027131') { - $localwarnflag = 1; - $finalsummary .= $summary; - } - } - if ($localwarnflag == 1) { - speak $finalsummary; - play('file' => 'C:\MH\SOUNDS\MSG.WAV'); - if ($finalsummary =~ m/WARNING/i) {play('file' => 'C:\MH\SOUNDS\STALLHRN2.WAV');} - - $finalsummarytx = "$HamCall>APRSMH,TCPIP*:BLN9RICE :"; - #$finalsummarytx = ":BLN9RICE :"; - $finalsummarytx .= $finalsummary; - print_log $finalsummarytx; - if (state $enable_transmit eq 'yes') {set $tnc_output $finalsummarytx}; - } - file_write "$wx_warnings.txt", $finalsummary; - display "$wx_warnings.txt"; -} +# Category = Weather + +my $wx_warnings = "$config_parms{data_dir}/web/wx_warnings"; +$p_wx_warnings = new Process_Item "get_url http://www.weather.gov/alerts/mn.cap $wx_warnings.xml"; +$v_wx_warnings = new Voice_Cmd '[Get,Show,Display,Read,Parse] the latest NWS Announcements'; + +$state = said $v_wx_warnings; +if ($state eq 'Get' or time_cron('1,11,21,31,41,51 10,11,12,13,14,15,16,17,18,19,20 * * *')) { + print_log "Getting NWS Alerts"; + start $p_wx_warnings; +} +display "$wx_warnings.txt" if $state eq 'Show' or $state eq 'Display'; +speak "$wx_warnings.txt" if $state eq 'Read'; + +if (done_now $p_wx_warnings or $state eq 'Parse') { + my ($finalsummarytx, $finalsummary, $summary, $tempcode, $localwarnflag, $i); + $localwarnflag = 0; + for (file_read "$wx_warnings.xml") { + $summary = ""; + $summary .= "The national weather service has issued a $1" if /(.+)<\/cap:event>/; + $summary .= " .. " if /(.+)<\/cap:expires>/; + #$summary .= " until $1 for Rice County." if /(.+)<\/cap:expires>/; + $summary .= "$1." if /(.+)<\/cap:description>/; + $tempcode = "$1" if /(.+)<\/cap:geocode>/; + if ($tempcode eq '027131') { + $localwarnflag = 1; + $finalsummary .= $summary; + } + } + if ($localwarnflag == 1) { + speak $finalsummary; + play('file' => 'C:\MH\SOUNDS\MSG.WAV'); + if ($finalsummary =~ m/WARNING/i) {play('file' => 'C:\MH\SOUNDS\STALLHRN2.WAV');} + + $finalsummarytx = "$HamCall>APRSMH,TCPIP*:BLN9RICE :"; + #$finalsummarytx = ":BLN9RICE :"; + $finalsummarytx .= $finalsummary; + print_log $finalsummarytx; + if (state $enable_transmit eq 'yes') {set $tnc_output $finalsummarytx}; + } + file_write "$wx_warnings.txt", $finalsummary; + display "$wx_warnings.txt"; +} diff --git a/code/public/Brian/xpl.pl b/code/public/Brian/xpl.pl index dbb16c3dd..8cc972792 100644 --- a/code/public/Brian/xpl.pl +++ b/code/public/Brian/xpl.pl @@ -1,56 +1,56 @@ -# Category = xPL - -my $xpllastcommand; -my $xpljabberdevice; - -$xpl_balloon = new xPL_Item('medusa-balloon.klierpc','log.basic' => {speech => '$state'}); -$xpl_jabber = new xPL_Item('doghouse-blabber.archeserver20'); -$xpl_jabber_resp = new xPL_Item('doghouse-blabber.archestar20','control.basic' => {device => 'brian.klier@gmail.com/BlackBerry4BF62086', current => '$state'}); - -if ($state = state_now $xpl_jabber) { - my $xpljabberdevice=$xpl_jabber->{'sensor.basic'}{'device'}; - #print_log "Jabberdevice: $xpljabberdevice"; - my $xpljabbertype=$xpl_jabber->{'sensor.basic'}{'type'}; - #print_log "Jabbertype: $xpljabbertype"; - my $xpljabbercommand=$xpl_jabber->{'sensor.basic'}{'current'}; - #print_log "Jabbercommand: $xpljabbercommand"; - my $xpljabberdevicecon = substr($xpljabberdevice, 0, 11); - #print_log "Jabberdeviceconcat: $xpljabberdevicecon"; - - if (($xpljabberdevicecon eq 'brian.klier') and ($xpljabbertype eq 'message') and ($xpljabbercommand ne $xpllastcommand)) { - $xpllastcommand = $xpljabbercommand; - run_voice_cmd $xpljabbercommand; - #my $test123 = "testmessage"; - #&xAP::send('xPL', 'doghouse-blabber.archestar20', 'control.basic' => $test123, 'message' => $test123); - #&xAP::send('xPL', 'doghouse-blabber.archestar20', 'sensor.basic'set $xpl_jabber - speak "Ran Jabber Command: $xpljabbercommand"; - $page_email = "Ack: $xpljabbercommand"; - set $xpl_jabber_resp 'Ack'; - } -} - - -if (state_now $bed_heater eq 'off') { - set $xpl_balloon "The Bed Heater is now off"; -} - -if (state_now $cid_interface1 eq 'ring') { - set $xpl_balloon "RING RING RING"; -} - -#set $xpl_balloon "$Time_Now" if $New_Minute; - -#$xpl_cid = new xPL_Item('ag-asterisk.asterisk1local'); -# -#if ($state = state_now $xpl_cid) { -# my $cidname=$xpl_cid->{'cid.asterisk'}{'cln'}; -# my $cidnumber=$xpl_cid->{'cid.asterisk'}{'phone'}; -# print_log "Incoming call from $cidname, number is $cidnumber"; -#} -# -# -# -#>04/12/05 08:06:35 AM xpl data: cid.asterisk : cln = 7856336488 | -#>cid.asterisk : calltype = inbound | cid.asterisk : ccs = ring | cid.asterisk -#>: phone = 7856336488 | xpl-trig : source = ag-asterisk.asterisk1local | -#>xpl-trig : target = * | xpl-trig : hop = 1 | +# Category = xPL + +my $xpllastcommand; +my $xpljabberdevice; + +$xpl_balloon = new xPL_Item('medusa-balloon.klierpc','log.basic' => {speech => '$state'}); +$xpl_jabber = new xPL_Item('doghouse-blabber.archeserver20'); +$xpl_jabber_resp = new xPL_Item('doghouse-blabber.archestar20','control.basic' => {device => 'brian.klier@gmail.com/BlackBerry4BF62086', current => '$state'}); + +if ($state = state_now $xpl_jabber) { + my $xpljabberdevice=$xpl_jabber->{'sensor.basic'}{'device'}; + #print_log "Jabberdevice: $xpljabberdevice"; + my $xpljabbertype=$xpl_jabber->{'sensor.basic'}{'type'}; + #print_log "Jabbertype: $xpljabbertype"; + my $xpljabbercommand=$xpl_jabber->{'sensor.basic'}{'current'}; + #print_log "Jabbercommand: $xpljabbercommand"; + my $xpljabberdevicecon = substr($xpljabberdevice, 0, 11); + #print_log "Jabberdeviceconcat: $xpljabberdevicecon"; + + if (($xpljabberdevicecon eq 'brian.klier') and ($xpljabbertype eq 'message') and ($xpljabbercommand ne $xpllastcommand)) { + $xpllastcommand = $xpljabbercommand; + run_voice_cmd $xpljabbercommand; + #my $test123 = "testmessage"; + #&xAP::send('xPL', 'doghouse-blabber.archestar20', 'control.basic' => $test123, 'message' => $test123); + #&xAP::send('xPL', 'doghouse-blabber.archestar20', 'sensor.basic'set $xpl_jabber + speak "Ran Jabber Command: $xpljabbercommand"; + $page_email = "Ack: $xpljabbercommand"; + set $xpl_jabber_resp 'Ack'; + } +} + + +if (state_now $bed_heater eq 'off') { + set $xpl_balloon "The Bed Heater is now off"; +} + +if (state_now $cid_interface1 eq 'ring') { + set $xpl_balloon "RING RING RING"; +} + +#set $xpl_balloon "$Time_Now" if $New_Minute; + +#$xpl_cid = new xPL_Item('ag-asterisk.asterisk1local'); +# +#if ($state = state_now $xpl_cid) { +# my $cidname=$xpl_cid->{'cid.asterisk'}{'cln'}; +# my $cidnumber=$xpl_cid->{'cid.asterisk'}{'phone'}; +# print_log "Incoming call from $cidname, number is $cidnumber"; +#} +# +# +# +#>04/12/05 08:06:35 AM xpl data: cid.asterisk : cln = 7856336488 | +#>cid.asterisk : calltype = inbound | cid.asterisk : ccs = ring | cid.asterisk +#>: phone = 7856336488 | xpl-trig : source = ag-asterisk.asterisk1local | +#>xpl-trig : target = * | xpl-trig : hop = 1 | diff --git a/code/public/Esensor_EM01.pl b/code/public/Esensor_EM01.pl index 5e02d7c07..da0d6b53e 100644 --- a/code/public/Esensor_EM01.pl +++ b/code/public/Esensor_EM01.pl @@ -1,72 +1,72 @@ - -=begin comment - -For sensor from: http://www.eEsensors.com - -I have the data posting to: -$TempSpare1, $HumidSpare1, $SunSensor, $E_sensor185 (Dry Contact) - -Let me know if you have any questions. I have 3 of them running in Misterhouse under "$xxxxSapre1, Spare2, and Spare3. - -Rick "TheBassman" Bassett -Salt Lake City USA -http://thebassman.is-a-geek.net - -=cut - - - -# Category = Weather - -# set $f_sensor184_url = IP address in mh.ini or mh.private.ini http://www.eEsensors.com -# Support added my Rick "TheBassman" Bassett http://thebassman.is-a-geek.net thebassmanis@gmail.com - -$TempSpare1 = new Weather_Item 'TempSpare1'; -$HumidSpare1 = new Weather_Item 'HumidSpare1'; -#$SunSensor = new Weather_Item 'sun_sensor'; #Could be used for the Lumination sensor. Uncomment throughout the file to use -#$Esensor184 = new Weather_Item 'Esensor184'; #Could be used for the contact feature. Uncomment throughout the file to use - -my $f_sensor184_text = "$config_parms{data_dir}/web/esensor184.txt"; -my $f_sensor184_html = "$config_parms{data_dir}/web/esensor184.html"; -my $f_sensor184_url=$config_parms{Esensor184_id}; - - -$p_check_sensor184 = new Process_Item qq[get_url "http://$f_sensor184_url/index.html" "$f_sensor184_html"]; -$v_check_sensor184 = new Voice_Cmd('[Get,Read,Check] sernsor184'); - -if (($New_Minute and (($Minute % 5) == 2))) { - if (&net_connect_check) { - $v_check_sensor184->respond("Getting sensor184 data..."); - start $p_check_sensor184; - } - else { - $v_check_sensor184->respond("Cannot retrieve data."); - } -} - -if (done_now $p_check_sensor184) { - - my $html = file_read $f_sensor184_html; - - my $text = &html_to_text($html); - - $text =~ /\D\D\d+TF\D+([\d.]+)\D*HU\D*([\d.]+)\D*IL\D*[\d.]+/; - file_write($f_sensor184_text, $text); - - if ($v_check_sensor184->{state} eq 'Check') { - $v_check_sensor184->respond("connected=0 important=1 $text"); - } - else { - $v_check_sensor184->respond("connected=0 data retrieved."); - } -} - my $text2 = file_read $f_sensor184_text; - - my ( $TempSpare1, $HumidSpare1) = $text2 =~ /\D\D\d+TF\D+([\d.]+)\D*HU\D*([\d.]+)\D*IL\D*[\d.]+/; -# my ($E_sensor185, $TempIndoor, $HumidIndoor, $SunSensor) = $text2 =~ /\D(\D)\d+TF\D+([\d.]+)\D*HU\D*([\d.]+)\D*IL\D*([\d.]+)/; - -$Weather{TempSpare1} = $TempSpare1; -$Weather{HumidSpare1} = $HumidSpare1; -#$Weather{sun_sensor} = $SunSensor; -#$Esensor184 = $E_sensor184; - + +=begin comment + +For sensor from: http://www.eEsensors.com + +I have the data posting to: +$TempSpare1, $HumidSpare1, $SunSensor, $E_sensor185 (Dry Contact) + +Let me know if you have any questions. I have 3 of them running in Misterhouse under "$xxxxSapre1, Spare2, and Spare3. + +Rick "TheBassman" Bassett +Salt Lake City USA +http://thebassman.is-a-geek.net + +=cut + + + +# Category = Weather + +# set $f_sensor184_url = IP address in mh.ini or mh.private.ini http://www.eEsensors.com +# Support added my Rick "TheBassman" Bassett http://thebassman.is-a-geek.net thebassmanis@gmail.com + +$TempSpare1 = new Weather_Item 'TempSpare1'; +$HumidSpare1 = new Weather_Item 'HumidSpare1'; +#$SunSensor = new Weather_Item 'sun_sensor'; #Could be used for the Lumination sensor. Uncomment throughout the file to use +#$Esensor184 = new Weather_Item 'Esensor184'; #Could be used for the contact feature. Uncomment throughout the file to use + +my $f_sensor184_text = "$config_parms{data_dir}/web/esensor184.txt"; +my $f_sensor184_html = "$config_parms{data_dir}/web/esensor184.html"; +my $f_sensor184_url=$config_parms{Esensor184_id}; + + +$p_check_sensor184 = new Process_Item qq[get_url "http://$f_sensor184_url/index.html" "$f_sensor184_html"]; +$v_check_sensor184 = new Voice_Cmd('[Get,Read,Check] sernsor184'); + +if (($New_Minute and (($Minute % 5) == 2))) { + if (&net_connect_check) { + $v_check_sensor184->respond("Getting sensor184 data..."); + start $p_check_sensor184; + } + else { + $v_check_sensor184->respond("Cannot retrieve data."); + } +} + +if (done_now $p_check_sensor184) { + + my $html = file_read $f_sensor184_html; + + my $text = &html_to_text($html); + + $text =~ /\D\D\d+TF\D+([\d.]+)\D*HU\D*([\d.]+)\D*IL\D*[\d.]+/; + file_write($f_sensor184_text, $text); + + if ($v_check_sensor184->{state} eq 'Check') { + $v_check_sensor184->respond("connected=0 important=1 $text"); + } + else { + $v_check_sensor184->respond("connected=0 data retrieved."); + } +} + my $text2 = file_read $f_sensor184_text; + + my ( $TempSpare1, $HumidSpare1) = $text2 =~ /\D\D\d+TF\D+([\d.]+)\D*HU\D*([\d.]+)\D*IL\D*[\d.]+/; +# my ($E_sensor185, $TempIndoor, $HumidIndoor, $SunSensor) = $text2 =~ /\D(\D)\d+TF\D+([\d.]+)\D*HU\D*([\d.]+)\D*IL\D*([\d.]+)/; + +$Weather{TempSpare1} = $TempSpare1; +$Weather{HumidSpare1} = $HumidSpare1; +#$Weather{sun_sensor} = $SunSensor; +#$Esensor184 = $E_sensor184; + diff --git a/code/public/Ricardo/bin/get_tv_com b/code/public/Ricardo/bin/get_tv_com index ddc11f167..ab825fa30 100644 --- a/code/public/Ricardo/bin/get_tv_com +++ b/code/public/Ricardo/bin/get_tv_com @@ -1,248 +1,248 @@ -#!/usr/bin/perl -# -*- Perl -*- - -#--------------------------------------------------------------------------- -# File: -# get_tv_com -# Description: -# A perl script that gets the -# Author: -# Mario Bonja based extensively on get_weather_ca, written by -# Bruce Winter bruce@misterhouse.net http://misterhouse.net -# 2006-04-20 Updated to the latest tv.com web page format. -# -# Copyright 2002 Bruce Winter -# -#--------------------------------------------------------------------------- -# -# $Id: get_tv_com,v 0.1 2006/01/14 Exp $ - -use strict; - -my ($Pgm_Path, $Pgm_Name); -BEGIN { - ($Pgm_Path, $Pgm_Name) = $0 =~ /(.*)[\\\/](.+)\.?/; - ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; -} - -my ($Version) = q$Revision: 0.1 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs - -#print "Command: $Pgm_Name @ARGV\n"; -#print "Version: $Version\n"; - -use Getopt::Long; -my %parms; -if (!&GetOptions(\%parms, "reget", "h", "help", "v", "debug", "showId:s", "file=s", "no_log") or - @ARGV or - ($parms{h} or $parms{help})) { - print< This help text - -help => This help text - -v => verbose - -debug => debug - - -reget => force HTML fetch - -showId => show index - - -no_log => Unless this option is used, the results also get filed - into the data_dir/web directory - - Example: - $Pgm_Name -showId 1 (1st show in TV_com_progs) - -eof - exit; - } - -my %config_parms; -my $caller = caller; -my $return_flag = ($caller and $caller ne 'main') ? 1 : 0; -my $ProgramName; -my $EpisodeName; -my $EpisodeDate; -my $EpisodeTime; -my $EpisodeDescription; - -#use my_lib "$Pgm_Path/../lib/site"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination -BEGIN { eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'" } # Use BEGIN eval to keep perl2exe happy - -require 'handy_utilities.pl'; # For read_mh_opts funcion -&main::read_mh_opts(\%config_parms, $Pgm_Path); - -use HTML::TableExtract; -use Date::Parse; -use Date::Format; - -$parms{showId} = 0 unless $parms{showId}; - -# Get the list of programs from the list file. -my $programs_file = $config_parms{code_dir}."/".$config_parms{TV_com_progs_file}; -open(TV_PROGRAMS, $programs_file) or print "Warning, could not open $programs_file!\n", return 1; -my(@TV_com_progs) = ; -close TV_PROGRAMS; - -my $TvURL; -my $showId; -$showId = $parms{showId}; -$TvURL = $TV_com_progs[$showId]; - -my $f_tv_html = "$config_parms{data_dir}/web/tv_com"; -$f_tv_html .= $showId . ".html"; -my $f_tv_data = "$config_parms{data_dir}/tv_data"; - -my $debug = 1 if ($parms{debug}); - -########## -# get TV # -########## - -my $tv_time = (stat($f_tv_html))[9]; -if ($parms{reget} or - (-s $f_tv_html < 10) ) -{ - get_url_ua($TvURL, $f_tv_html); -} - -############ -# parse TV # -############ - -print "parsing TV data from $f_tv_html\n" if $parms{v}; -&parse_tv_com($f_tv_html); - -########### -# save TV # -########### - -print "saving TV data to $f_tv_data\n" if $parms{v}; -&save_tv_com($f_tv_data); - -exit(0); - -############### -# subroutines # -############### - -# from get_url -sub get_url_ua { - my $url = shift; - my $file = shift; - - use LWP::UserAgent; - - my $ua = new LWP::UserAgent; - $config_parms{proxy} = $ENV{HTTP_PROXY} unless $config_parms{proxy}; - $ua -> proxy(['http', 'ftp'] => $config_parms{proxy}) if $config_parms{proxy}; - - $ua->timeout([120]); # Time out after 60 seconds - $ua->env_proxy(); - - my $request = new HTTP::Request('GET', $url); - my $response; - - print "Retrieving (with ua) $url into $file ...\n" unless $config_parms{quiet}; - if ($file eq '/dev/null') { - $response = $ua->simple_request($request); - } - else { - $response = $ua->simple_request($request, $file); - } - - if ($response->is_error()) { - printf "error: %s\n", $response->status_line; - } -} - - -# There is an HTML TV page on disk. Parse the TV data out of it and -# save the results in a file so that the parent MH process can read them back -# in. -# -sub parse_tv_com { - my $file = shift; - my $html = &file_read($file); - my $temp1; - my $temp2; - - # find the start of the actual data -#print STDERR $html if ($debug); - - # Get the program name - $html =~ m/\\s*([^<]*) TV Show/i; - $ProgramName = $1; - - $html =~ m/span class=\"f-C00\"\>\s*([\w]+)\s*([\w]+)\s*([\w]+),\s*([\w]+)/i; - $EpisodeDate = $1 . " " . $2 . " " . $3 . ", " . $4; - - # Get the program time - $html =~ m/Airs Next: \\s*([\w]+)\s+([\w]+)\s+([\w]+)\s+([\d:]+)\s+([\w]+)/i; - if ( $4 ne '' and $5 ne '' ) - { - $EpisodeTime = $4 . " " . $5; - } - else - { - $EpisodeTime = ""; - } - - # Get the next episode details. - $html =~ m/Next Episode:\s*\\s*([^\n]*)\<\/a\>\<\/span\>\\s*\n\s*([^\n]*)/i; - $EpisodeName = $2; - $EpisodeDescription = $3; - $EpisodeDescription =~ s/"//g; - $EpisodeDescription =~ s/\
    //g; - $EpisodeDescription =~ s/\n//g; - $EpisodeDescription =~ s/\r//g; - - print "Program: $ProgramName\nDate: $EpisodeDate\nTime: $EpisodeTime\nTitle: $EpisodeName\nDescription: $EpisodeDescription\n" if $parms{v}; -} - - -sub save_tv_com { - my $file = shift; - my $perl = ''; - my ($year, $month, $day); - - # input date format is "Thursday March 9, 2006" - $EpisodeDate =~ m/([\w]+) ([\w]+) ([\d]+), ([\d]+)/i; - $year = $4; - $month = monthStr_To_num($2); - $day = $3; - - # old format -# $perl .= '$TVProgramName[' . $showId . '] = "' . $ProgramName . '"' . ";\n"; -# $perl .= '$TVEpisodeName[' . $showId . '] = "' . $EpisodeName . '"' . ";\n"; -# $perl .= '$TVEpisodeDate[' . $showId . '] = "' . $EpisodeDate . '"' . ";\n"; -# $perl .= '$TVEpisodeDescription[' . $showId . '] = "' . $EpisodeDescription . '"' . ";\n"; - - # output format is "TV date no time Prog name Programmes TV Episode name & Desc" - if ( $year ) { - $perl .= "TV$showId\t$year.$month.$day\t$EpisodeTime\t$ProgramName\tProgrammes TV\t$EpisodeName: $EpisodeDescription\n"; - # append the data to the file - main::logit($file, $perl, 0, 0); - } -} - -sub monthStr_To_num { - my $monthStr = shift; - - return 1 if ( $monthStr =~ m/Jan/i ); - return 2 if ( $monthStr =~ m/Feb/i ); - return 3 if ( $monthStr =~ m/Mar/i ); - return 4 if ( $monthStr =~ m/Apr/i ); - return 5 if ( $monthStr =~ m/May/i ); - return 6 if ( $monthStr =~ m/Jun/i ); - return 7 if ( $monthStr =~ m/Jul/i ); - return 8 if ( $monthStr =~ m/Aug/i ); - return 9 if ( $monthStr =~ m/Sep/i ); - return 10 if ( $monthStr =~ m/Oct/i ); - return 11 if ( $monthStr =~ m/Nov/i ); - return 12 if ( $monthStr =~ m/Dec/i ); +#!/usr/bin/perl +# -*- Perl -*- + +#--------------------------------------------------------------------------- +# File: +# get_tv_com +# Description: +# A perl script that gets the +# Author: +# Mario Bonja based extensively on get_weather_ca, written by +# Bruce Winter bruce@misterhouse.net http://misterhouse.net +# 2006-04-20 Updated to the latest tv.com web page format. +# +# Copyright 2002 Bruce Winter +# +#--------------------------------------------------------------------------- +# +# $Id: get_tv_com,v 0.1 2006/01/14 Exp $ + +use strict; + +my ($Pgm_Path, $Pgm_Name); +BEGIN { + ($Pgm_Path, $Pgm_Name) = $0 =~ /(.*)[\\\/](.+)\.?/; + ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; +} + +my ($Version) = q$Revision: 0.1 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + +#print "Command: $Pgm_Name @ARGV\n"; +#print "Version: $Version\n"; + +use Getopt::Long; +my %parms; +if (!&GetOptions(\%parms, "reget", "h", "help", "v", "debug", "showId:s", "file=s", "no_log") or + @ARGV or + ($parms{h} or $parms{help})) { + print< This help text + -help => This help text + -v => verbose + -debug => debug + + -reget => force HTML fetch + -showId => show index + + -no_log => Unless this option is used, the results also get filed + into the data_dir/web directory + + Example: + $Pgm_Name -showId 1 (1st show in TV_com_progs) + +eof + exit; + } + +my %config_parms; +my $caller = caller; +my $return_flag = ($caller and $caller ne 'main') ? 1 : 0; +my $ProgramName; +my $EpisodeName; +my $EpisodeDate; +my $EpisodeTime; +my $EpisodeDescription; + +#use my_lib "$Pgm_Path/../lib/site"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination +BEGIN { eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'" } # Use BEGIN eval to keep perl2exe happy + +require 'handy_utilities.pl'; # For read_mh_opts funcion +&main::read_mh_opts(\%config_parms, $Pgm_Path); + +use HTML::TableExtract; +use Date::Parse; +use Date::Format; + +$parms{showId} = 0 unless $parms{showId}; + +# Get the list of programs from the list file. +my $programs_file = $config_parms{code_dir}."/".$config_parms{TV_com_progs_file}; +open(TV_PROGRAMS, $programs_file) or print "Warning, could not open $programs_file!\n", return 1; +my(@TV_com_progs) = ; +close TV_PROGRAMS; + +my $TvURL; +my $showId; +$showId = $parms{showId}; +$TvURL = $TV_com_progs[$showId]; + +my $f_tv_html = "$config_parms{data_dir}/web/tv_com"; +$f_tv_html .= $showId . ".html"; +my $f_tv_data = "$config_parms{data_dir}/tv_data"; + +my $debug = 1 if ($parms{debug}); + +########## +# get TV # +########## + +my $tv_time = (stat($f_tv_html))[9]; +if ($parms{reget} or + (-s $f_tv_html < 10) ) +{ + get_url_ua($TvURL, $f_tv_html); +} + +############ +# parse TV # +############ + +print "parsing TV data from $f_tv_html\n" if $parms{v}; +&parse_tv_com($f_tv_html); + +########### +# save TV # +########### + +print "saving TV data to $f_tv_data\n" if $parms{v}; +&save_tv_com($f_tv_data); + +exit(0); + +############### +# subroutines # +############### + +# from get_url +sub get_url_ua { + my $url = shift; + my $file = shift; + + use LWP::UserAgent; + + my $ua = new LWP::UserAgent; + $config_parms{proxy} = $ENV{HTTP_PROXY} unless $config_parms{proxy}; + $ua -> proxy(['http', 'ftp'] => $config_parms{proxy}) if $config_parms{proxy}; + + $ua->timeout([120]); # Time out after 60 seconds + $ua->env_proxy(); + + my $request = new HTTP::Request('GET', $url); + my $response; + + print "Retrieving (with ua) $url into $file ...\n" unless $config_parms{quiet}; + if ($file eq '/dev/null') { + $response = $ua->simple_request($request); + } + else { + $response = $ua->simple_request($request, $file); + } + + if ($response->is_error()) { + printf "error: %s\n", $response->status_line; + } +} + + +# There is an HTML TV page on disk. Parse the TV data out of it and +# save the results in a file so that the parent MH process can read them back +# in. +# +sub parse_tv_com { + my $file = shift; + my $html = &file_read($file); + my $temp1; + my $temp2; + + # find the start of the actual data +#print STDERR $html if ($debug); + + # Get the program name + $html =~ m/\\s*([^<]*) TV Show/i; + $ProgramName = $1; + + $html =~ m/span class=\"f-C00\"\>\s*([\w]+)\s*([\w]+)\s*([\w]+),\s*([\w]+)/i; + $EpisodeDate = $1 . " " . $2 . " " . $3 . ", " . $4; + + # Get the program time + $html =~ m/Airs Next: \\s*([\w]+)\s+([\w]+)\s+([\w]+)\s+([\d:]+)\s+([\w]+)/i; + if ( $4 ne '' and $5 ne '' ) + { + $EpisodeTime = $4 . " " . $5; + } + else + { + $EpisodeTime = ""; + } + + # Get the next episode details. + $html =~ m/Next Episode:\s*\\s*([^\n]*)\<\/a\>\<\/span\>\\s*\n\s*([^\n]*)/i; + $EpisodeName = $2; + $EpisodeDescription = $3; + $EpisodeDescription =~ s/"//g; + $EpisodeDescription =~ s/\
    //g; + $EpisodeDescription =~ s/\n//g; + $EpisodeDescription =~ s/\r//g; + + print "Program: $ProgramName\nDate: $EpisodeDate\nTime: $EpisodeTime\nTitle: $EpisodeName\nDescription: $EpisodeDescription\n" if $parms{v}; +} + + +sub save_tv_com { + my $file = shift; + my $perl = ''; + my ($year, $month, $day); + + # input date format is "Thursday March 9, 2006" + $EpisodeDate =~ m/([\w]+) ([\w]+) ([\d]+), ([\d]+)/i; + $year = $4; + $month = monthStr_To_num($2); + $day = $3; + + # old format +# $perl .= '$TVProgramName[' . $showId . '] = "' . $ProgramName . '"' . ";\n"; +# $perl .= '$TVEpisodeName[' . $showId . '] = "' . $EpisodeName . '"' . ";\n"; +# $perl .= '$TVEpisodeDate[' . $showId . '] = "' . $EpisodeDate . '"' . ";\n"; +# $perl .= '$TVEpisodeDescription[' . $showId . '] = "' . $EpisodeDescription . '"' . ";\n"; + + # output format is "TV date no time Prog name Programmes TV Episode name & Desc" + if ( $year ) { + $perl .= "TV$showId\t$year.$month.$day\t$EpisodeTime\t$ProgramName\tProgrammes TV\t$EpisodeName: $EpisodeDescription\n"; + # append the data to the file + main::logit($file, $perl, 0, 0); + } +} + +sub monthStr_To_num { + my $monthStr = shift; + + return 1 if ( $monthStr =~ m/Jan/i ); + return 2 if ( $monthStr =~ m/Feb/i ); + return 3 if ( $monthStr =~ m/Mar/i ); + return 4 if ( $monthStr =~ m/Apr/i ); + return 5 if ( $monthStr =~ m/May/i ); + return 6 if ( $monthStr =~ m/Jun/i ); + return 7 if ( $monthStr =~ m/Jul/i ); + return 8 if ( $monthStr =~ m/Aug/i ); + return 9 if ( $monthStr =~ m/Sep/i ); + return 10 if ( $monthStr =~ m/Oct/i ); + return 11 if ( $monthStr =~ m/Nov/i ); + return 12 if ( $monthStr =~ m/Dec/i ); } \ No newline at end of file diff --git a/code/public/Ricardo/bin/get_tv_com.bat b/code/public/Ricardo/bin/get_tv_com.bat index e02ec090e..92f244391 100644 --- a/code/public/Ricardo/bin/get_tv_com.bat +++ b/code/public/Ricardo/bin/get_tv_com.bat @@ -1 +1 @@ -@mh -run get_tv_com %1 %2 %3 %4 %5 %6 %7 %8 %9 +@mh -run get_tv_com %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/code/public/Ricardo/bin/get_weather_ca b/code/public/Ricardo/bin/get_weather_ca index 46040f7af..d91fb68db 100644 --- a/code/public/Ricardo/bin/get_weather_ca +++ b/code/public/Ricardo/bin/get_weather_ca @@ -1,358 +1,358 @@ -#!/usr/bin/perl -# -*- Perl -*- - -#--------------------------------------------------------------------------- -# File: -# get_weather_ca -# Description: -# A perl script that gets the weather forecast for Canada -# Author: -# Harald Koch chk@pobox.com -# based extensively on get_weather, written by -# Bruce Winter bruce@misterhouse.net http://misterhouse.net -# Latest version: -# http://misterhouse.net/mh/bin -# -# Copyright 2002 Bruce Winter -# -#--------------------------------------------------------------------------- -# -# $Id: get_weather_ca,v 1.4 2005/03/20 19:01:52 winter Exp $ - -use strict; - -my ($Pgm_Path, $Pgm_Name); -BEGIN { - ($Pgm_Path, $Pgm_Name) = $0 =~ /(.*)[\\\/](.+)\.?/; - ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; -} - -my ($Version) = q$Revision: 1.4 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs - -#print "Command: $Pgm_Name @ARGV\n"; -#print "Version: $Version\n"; - -use Getopt::Long; -my %parms; -if (!&GetOptions(\%parms, "reget", "h", "help", "v", "debug", "city:s", "file=s", "no_log") or - @ARGV or - ($parms{h} or $parms{help})) { - print< This help text - -help => This help text - -v => verbose - -debug => debug - - -reget => force HTML fetch - - -city xxx => xxx is the code for the city you want. - -file xxx => xxx is the output filename - - -no_log => Unless this option is used, the results also get filed - into the data_dir/web directory - - Example: - $Pgm_Name -city YYZ - -eof - exit; - } - -my ($conditions, $forecast, %data); -my %config_parms; - - -use vars qw(%Weather @Weather_Forecast); - -my $caller = caller; -my $return_flag = ($caller and $caller ne 'main') ? 1 : 0; - -#use my_lib "$Pgm_Path/../lib/site"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination -BEGIN { eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'" } # Use BEGIN eval to keep perl2exe happy - -require 'handy_utilities.pl'; # For read_mh_opts funcion -&main::read_mh_opts(\%config_parms, $Pgm_Path); - -use HTML::TableExtract; -use Date::Parse; -use Date::Format; - -$parms{city} = $config_parms{weather_city} unless $parms{city}; -#$parms{city} = 'yyz' unless $parms{city}; -$parms{city} = 'on-117' unless $parms{city}; - -my $WeatherURL; -$WeatherURL = sprintf 'http://text.weatheroffice.ec.gc.ca/forecast/city_e.html?%s&b_templatePrint=true', $parms{city}; - -my $f_weather_html = "$config_parms{data_dir}/web/weather_ca.html"; -my $f_weather_data = "$config_parms{data_dir}/weather_data"; - -my $debug = 1 if ($parms{debug}); - -############### -# get weather # -############### - -my $weather_time = (stat($f_weather_html))[9]; -if ($parms{reget} or - (-s $f_weather_html < 10) or - ((time - $weather_time) > 59*60) -) { - get_url_ua($WeatherURL, $f_weather_html); -} - -################# -# parse weather # -################# - -print "parsing weather data from $f_weather_html\n" if $parms{v}; -&parse_weather_ca($f_weather_html); - -################ -# save weather # -################ - -print "saving weather data to $f_weather_data\n" if $parms{v}; -&save_weather_ca($f_weather_data); - -exit(0); - -############### -# subroutines # -############### - -# from get_url -sub get_url_ua { - my $url = shift; - my $file = shift; - - use LWP::UserAgent; - - my $ua = new LWP::UserAgent; - $config_parms{proxy} = $ENV{HTTP_PROXY} unless $config_parms{proxy}; - $ua -> proxy(['http', 'ftp'] => $config_parms{proxy}) if $config_parms{proxy}; - - $ua->timeout([120]); # Time out after 60 seconds - $ua->env_proxy(); - - my $request = new HTTP::Request('GET', $url); - my $response; - - print "Retrieving (with ua) $url into $file ...\n" unless $config_parms{quiet}; - if ($file eq '/dev/null') { - $response = $ua->simple_request($request); - } - else { - $response = $ua->simple_request($request, $file); - } - - if ($response->is_error()) { - printf "error: %s\n", $response->status_line; - } -} - - -# There is an HTML weather page on disk. Parse the weather data out of it and -# save the results in a file so that the parent MH process can read them back -# in. -# -sub parse_weather_ca { - my $file = shift; - my $html = &file_read($file); - - %Weather = (); - @Weather_Forecast = (); - - # find the start of the actual data - $html =~ s/.*Currently\s*://m; - - $html =~ s///gs; - $html =~ s/<\/strong>//gs; - $html =~ s/\ / /gs; - -print STDERR $html if ($debug); - - $html =~ m/Observed at:*\s+([^<]*)/i; - # convert the strangely formatted UTC timestamp to local time. - my $tmp = $1; - - ($tmp =~ m/\s*([0-9]+)\s*([0-9|A-Z|a-z|:]+)\s*\s*([0-9|A-Z|a-z|:]+)\s*([0-9|A-Z|a-z|:]+)\s*([0-9|A-Z|a-z|:]+)/i) && -# ($tmp = $2 . " " . $1 . " " . $3 . " " . $4 . " " . $5); - ($tmp = $4 . " " . $5); - - $Weather{TimeObserved} = str2time($tmp); - - ($html =~ m/Condition\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && - ($Weather{Conditions} = $1); - - ($html =~ m/Temperature\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && - ($Weather{TempOutdoor} = $1); - - ($html =~ m/Pressure\s*\/\s*Tendency\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)\s*\//i) && - ($Weather{Barom} = $1); - - ($html =~ m/Visibility\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && - ($Weather{Visibility} = $1); - - ($html =~ m/Humidity\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && - ($Weather{HumidOutdoor} = $1); - - ($html =~ m/Dew Point\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && - ($Weather{DewpointOutdoor} = $1); - - if ($html =~ m/Wind Speed\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) { - my $wind = $1; - $wind =~ s/\s*$//; - - $Weather{Wind} = $wind; - $wind =~ /^\s*([A-Z]*)\s*(\d+)(
    )*\s*km\/h/; - $Weather{WindAvg} = $2; - $Weather{WindAvgDir} = convert_to_degrees($1); - - if ($wind =~ /gusting to (\d+)/) { - $Weather{WindGust} = $1; - $Weather{WindGustDir} = $Weather{WindAvgDir}; - } - else { - $Weather{WindGust} = 0; - $Weather{WindGustDir} = 0; - } - } - - - my $yesterday = ''; - if ($html =~ m/yesterday :(.*?)<\/ul>/si) { - $yesterday = $1; - } - # new format 20030310 - elsif ($html =~ m;Yesterday\s*(;si) { - $yesterday = $1; - } - print STDERR "Yesterday |$yesterday|\n" if ($debug); - - ($yesterday =~ m/max temp\.*\s*:\s*([^<]+)/i) && - ($Weather{TempMaxOutdoor} = $1); - ($yesterday =~ m/min temp\.*\s*:\s*([^<]+)/i) && - ($Weather{TempMinOutdoor} = $1); - ($yesterday =~ m/precip\. total\s*:\s*([^<]+)/i) && - ($Weather{RainTotal} = $1); - - my $normal = ''; - if ($html =~ m/normal :(.*?)<\/ul>/si) { - $normal = $1; - } - # new format 20030310 - elsif ($html =~ m;normal\s*(;si) { - $normal = $1; - } - print STDERR "Normal |$normal|\n" if ($debug); - - ($normal =~ m/max temp\.*\s*:\s*([^<]+)/i) && - ($Weather{TempMaxNormal} = $1); - ($normal =~ m/min temp\.*\s*:\s*([^<]+)/i) && - ($Weather{TempMinNormal} = $1); - ($normal =~ m/mean temp\s*:\s*([^<]+)/i) && - ($Weather{TempMeanNormal} = $1); - - my $today = ''; - if ($html =~ m/today :<\/h3>(.*?)<\/ul>/si) { - $today = $1; - } - # new format 20030310 - elsif ($html =~ m;today\s*(;si) { - $today = $1; - } - print STDERR "Today |$today|\n" if ($debug); - - ($today =~ m/moon\s*rise\s*:\s*([^<]+)/i) && - ($Weather{Moonrise} = $1); - ($today =~ m/moon\s*set\s*:\s*([^<]+)/i) && - ($Weather{Moonset} = $1); - -### GET FORECAST -# $html =~ m/]*>(.*?issued.*?)<\/dl>/si; - - my $forecast = ''; - # new format 20030310 - if ($html =~ m;forecast\s*]*>(.*issued.*?);si) { - $forecast = $1; - } - print STDERR "Forecast: |$forecast|\n" if ($debug); - - $forecast =~ s/\s*
    \s*\n//gs; - $forecast =~ s/\s*<\/dd>\s*/\n/gs; - $forecast =~ s/\s*
    \s*//sg; - $forecast =~ s/\s*<\/dt>\s*/\n/gs; - - my @forecast = split(/\n/, $forecast); - - @Weather_Forecast = @forecast; -} - - -sub save_weather_ca { - my $file = shift; - - # save the data for import by parent - my ($key, $perl, $line); - - $perl = '%Weather=(' . "\n"; - foreach $key (keys(%Weather)) { - # cleanup the text - my $data = $Weather{$key}; - $data =~ s/\s*(\xb0|°)\s*[C]*//g; - - $data =~ s/\s*%\s*//g; -# $data =~ s;\s*km/h\s*;;g; - $data =~ s/\s*kPa\s*//g; - - $perl .= ' ' . $key . ' => ' . "'" . $data . "',\n"; - } - $perl .= ');' . "\n"; - - $perl .= '@Weather_Forecast=('; - foreach $line (@Weather_Forecast) { - $perl .= "\n '" . $line . "',"; - } - $perl =~ s/,$//; # remove trailing comma - $perl .= "\n" . ');' . "\n"; - - main::file_write($file, $perl); -} - -# convert text wind direction to degrees. -sub convert_to_degrees { - my $text = shift; - my $dir; - - ($text eq 'N') && ($dir = 0); - ($text eq 'NNE') && ($dir = 22); - ($text eq 'NE') && ($dir = 45); - ($text eq 'ENE') && ($dir = 67); - - ($text eq 'E') && ($dir = 90); - ($text eq 'ESE') && ($dir = 112); - ($text eq 'SE') && ($dir = 135); - ($text eq 'SSE') && ($dir = 157); - - ($text eq 'S') && ($dir = 180); - ($text eq 'SSW') && ($dir = 202); - ($text eq 'SW') && ($dir = 225); - ($text eq 'WSW') && ($dir = 247); - - ($text eq 'W') && ($dir = 270); - ($text eq 'WNW') && ($dir = 292); - ($text eq 'NW') && ($dir = 315); - ($text eq 'NNW') && ($dir = 337); - - return $dir; -} - - +#!/usr/bin/perl +# -*- Perl -*- + +#--------------------------------------------------------------------------- +# File: +# get_weather_ca +# Description: +# A perl script that gets the weather forecast for Canada +# Author: +# Harald Koch chk@pobox.com +# based extensively on get_weather, written by +# Bruce Winter bruce@misterhouse.net http://misterhouse.net +# Latest version: +# http://misterhouse.net/mh/bin +# +# Copyright 2002 Bruce Winter +# +#--------------------------------------------------------------------------- +# +# $Id: get_weather_ca,v 1.4 2005/03/20 19:01:52 winter Exp $ + +use strict; + +my ($Pgm_Path, $Pgm_Name); +BEGIN { + ($Pgm_Path, $Pgm_Name) = $0 =~ /(.*)[\\\/](.+)\.?/; + ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; +} + +my ($Version) = q$Revision: 1.4 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + +#print "Command: $Pgm_Name @ARGV\n"; +#print "Version: $Version\n"; + +use Getopt::Long; +my %parms; +if (!&GetOptions(\%parms, "reget", "h", "help", "v", "debug", "city:s", "file=s", "no_log") or + @ARGV or + ($parms{h} or $parms{help})) { + print< This help text + -help => This help text + -v => verbose + -debug => debug + + -reget => force HTML fetch + + -city xxx => xxx is the code for the city you want. + -file xxx => xxx is the output filename + + -no_log => Unless this option is used, the results also get filed + into the data_dir/web directory + + Example: + $Pgm_Name -city YYZ + +eof + exit; + } + +my ($conditions, $forecast, %data); +my %config_parms; + + +use vars qw(%Weather @Weather_Forecast); + +my $caller = caller; +my $return_flag = ($caller and $caller ne 'main') ? 1 : 0; + +#use my_lib "$Pgm_Path/../lib/site"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination +BEGIN { eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'" } # Use BEGIN eval to keep perl2exe happy + +require 'handy_utilities.pl'; # For read_mh_opts funcion +&main::read_mh_opts(\%config_parms, $Pgm_Path); + +use HTML::TableExtract; +use Date::Parse; +use Date::Format; + +$parms{city} = $config_parms{weather_city} unless $parms{city}; +#$parms{city} = 'yyz' unless $parms{city}; +$parms{city} = 'on-117' unless $parms{city}; + +my $WeatherURL; +$WeatherURL = sprintf 'http://text.weatheroffice.ec.gc.ca/forecast/city_e.html?%s&b_templatePrint=true', $parms{city}; + +my $f_weather_html = "$config_parms{data_dir}/web/weather_ca.html"; +my $f_weather_data = "$config_parms{data_dir}/weather_data"; + +my $debug = 1 if ($parms{debug}); + +############### +# get weather # +############### + +my $weather_time = (stat($f_weather_html))[9]; +if ($parms{reget} or + (-s $f_weather_html < 10) or + ((time - $weather_time) > 59*60) +) { + get_url_ua($WeatherURL, $f_weather_html); +} + +################# +# parse weather # +################# + +print "parsing weather data from $f_weather_html\n" if $parms{v}; +&parse_weather_ca($f_weather_html); + +################ +# save weather # +################ + +print "saving weather data to $f_weather_data\n" if $parms{v}; +&save_weather_ca($f_weather_data); + +exit(0); + +############### +# subroutines # +############### + +# from get_url +sub get_url_ua { + my $url = shift; + my $file = shift; + + use LWP::UserAgent; + + my $ua = new LWP::UserAgent; + $config_parms{proxy} = $ENV{HTTP_PROXY} unless $config_parms{proxy}; + $ua -> proxy(['http', 'ftp'] => $config_parms{proxy}) if $config_parms{proxy}; + + $ua->timeout([120]); # Time out after 60 seconds + $ua->env_proxy(); + + my $request = new HTTP::Request('GET', $url); + my $response; + + print "Retrieving (with ua) $url into $file ...\n" unless $config_parms{quiet}; + if ($file eq '/dev/null') { + $response = $ua->simple_request($request); + } + else { + $response = $ua->simple_request($request, $file); + } + + if ($response->is_error()) { + printf "error: %s\n", $response->status_line; + } +} + + +# There is an HTML weather page on disk. Parse the weather data out of it and +# save the results in a file so that the parent MH process can read them back +# in. +# +sub parse_weather_ca { + my $file = shift; + my $html = &file_read($file); + + %Weather = (); + @Weather_Forecast = (); + + # find the start of the actual data + $html =~ s/.*Currently\s*://m; + + $html =~ s///gs; + $html =~ s/<\/strong>//gs; + $html =~ s/\ / /gs; + +print STDERR $html if ($debug); + + $html =~ m/Observed at:*\s+([^<]*)/i; + # convert the strangely formatted UTC timestamp to local time. + my $tmp = $1; + + ($tmp =~ m/\s*([0-9]+)\s*([0-9|A-Z|a-z|:]+)\s*\s*([0-9|A-Z|a-z|:]+)\s*([0-9|A-Z|a-z|:]+)\s*([0-9|A-Z|a-z|:]+)/i) && +# ($tmp = $2 . " " . $1 . " " . $3 . " " . $4 . " " . $5); + ($tmp = $4 . " " . $5); + + $Weather{TimeObserved} = str2time($tmp); + + ($html =~ m/Condition\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && + ($Weather{Conditions} = $1); + + ($html =~ m/Temperature\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && + ($Weather{TempOutdoor} = $1); + + ($html =~ m/Pressure\s*\/\s*Tendency\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)\s*\//i) && + ($Weather{Barom} = $1); + + ($html =~ m/Visibility\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && + ($Weather{Visibility} = $1); + + ($html =~ m/Humidity\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && + ($Weather{HumidOutdoor} = $1); + + ($html =~ m/Dew Point\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) && + ($Weather{DewpointOutdoor} = $1); + + if ($html =~ m/Wind Speed\s*:\s*\<\/dt\>\s*\n\s*\\s*([^<]+)/i) { + my $wind = $1; + $wind =~ s/\s*$//; + + $Weather{Wind} = $wind; + $wind =~ /^\s*([A-Z]*)\s*(\d+)(
    )*\s*km\/h/; + $Weather{WindAvg} = $2; + $Weather{WindAvgDir} = convert_to_degrees($1); + + if ($wind =~ /gusting to (\d+)/) { + $Weather{WindGust} = $1; + $Weather{WindGustDir} = $Weather{WindAvgDir}; + } + else { + $Weather{WindGust} = 0; + $Weather{WindGustDir} = 0; + } + } + + + my $yesterday = ''; + if ($html =~ m/yesterday :(.*?)<\/ul>/si) { + $yesterday = $1; + } + # new format 20030310 + elsif ($html =~ m;Yesterday\s*(;si) { + $yesterday = $1; + } + print STDERR "Yesterday |$yesterday|\n" if ($debug); + + ($yesterday =~ m/max temp\.*\s*:\s*([^<]+)/i) && + ($Weather{TempMaxOutdoor} = $1); + ($yesterday =~ m/min temp\.*\s*:\s*([^<]+)/i) && + ($Weather{TempMinOutdoor} = $1); + ($yesterday =~ m/precip\. total\s*:\s*([^<]+)/i) && + ($Weather{RainTotal} = $1); + + my $normal = ''; + if ($html =~ m/normal :(.*?)<\/ul>/si) { + $normal = $1; + } + # new format 20030310 + elsif ($html =~ m;normal\s*(;si) { + $normal = $1; + } + print STDERR "Normal |$normal|\n" if ($debug); + + ($normal =~ m/max temp\.*\s*:\s*([^<]+)/i) && + ($Weather{TempMaxNormal} = $1); + ($normal =~ m/min temp\.*\s*:\s*([^<]+)/i) && + ($Weather{TempMinNormal} = $1); + ($normal =~ m/mean temp\s*:\s*([^<]+)/i) && + ($Weather{TempMeanNormal} = $1); + + my $today = ''; + if ($html =~ m/today :<\/h3>(.*?)<\/ul>/si) { + $today = $1; + } + # new format 20030310 + elsif ($html =~ m;today\s*(;si) { + $today = $1; + } + print STDERR "Today |$today|\n" if ($debug); + + ($today =~ m/moon\s*rise\s*:\s*([^<]+)/i) && + ($Weather{Moonrise} = $1); + ($today =~ m/moon\s*set\s*:\s*([^<]+)/i) && + ($Weather{Moonset} = $1); + +### GET FORECAST +# $html =~ m/]*>(.*?issued.*?)<\/dl>/si; + + my $forecast = ''; + # new format 20030310 + if ($html =~ m;forecast\s*]*>(.*issued.*?);si) { + $forecast = $1; + } + print STDERR "Forecast: |$forecast|\n" if ($debug); + + $forecast =~ s/\s*
    \s*\n//gs; + $forecast =~ s/\s*<\/dd>\s*/\n/gs; + $forecast =~ s/\s*
    \s*//sg; + $forecast =~ s/\s*<\/dt>\s*/\n/gs; + + my @forecast = split(/\n/, $forecast); + + @Weather_Forecast = @forecast; +} + + +sub save_weather_ca { + my $file = shift; + + # save the data for import by parent + my ($key, $perl, $line); + + $perl = '%Weather=(' . "\n"; + foreach $key (keys(%Weather)) { + # cleanup the text + my $data = $Weather{$key}; + $data =~ s/\s*(\xb0|°)\s*[C]*//g; + + $data =~ s/\s*%\s*//g; +# $data =~ s;\s*km/h\s*;;g; + $data =~ s/\s*kPa\s*//g; + + $perl .= ' ' . $key . ' => ' . "'" . $data . "',\n"; + } + $perl .= ');' . "\n"; + + $perl .= '@Weather_Forecast=('; + foreach $line (@Weather_Forecast) { + $perl .= "\n '" . $line . "',"; + } + $perl =~ s/,$//; # remove trailing comma + $perl .= "\n" . ');' . "\n"; + + main::file_write($file, $perl); +} + +# convert text wind direction to degrees. +sub convert_to_degrees { + my $text = shift; + my $dir; + + ($text eq 'N') && ($dir = 0); + ($text eq 'NNE') && ($dir = 22); + ($text eq 'NE') && ($dir = 45); + ($text eq 'ENE') && ($dir = 67); + + ($text eq 'E') && ($dir = 90); + ($text eq 'ESE') && ($dir = 112); + ($text eq 'SE') && ($dir = 135); + ($text eq 'SSE') && ($dir = 157); + + ($text eq 'S') && ($dir = 180); + ($text eq 'SSW') && ($dir = 202); + ($text eq 'SW') && ($dir = 225); + ($text eq 'WSW') && ($dir = 247); + + ($text eq 'W') && ($dir = 270); + ($text eq 'WNW') && ($dir = 292); + ($text eq 'NW') && ($dir = 315); + ($text eq 'NNW') && ($dir = 337); + + return $dir; +} + + diff --git a/code/public/Ricardo/code/tv_com.pl b/code/public/Ricardo/code/tv_com.pl index 5fd730bb5..0c48b5524 100644 --- a/code/public/Ricardo/code/tv_com.pl +++ b/code/public/Ricardo/code/tv_com.pl @@ -1,124 +1,124 @@ -# Category=TV - -my $f_tv_data = "$config_parms{data_dir}/tv_data"; -$f_tv_file = new File_Item($f_tv_data); -my $calendar_data = "$config_parms{data_dir}/organizer/calendar.tab"; - -# The obligatory voice command. -$v_tv_page = new Voice_Cmd('[Reget,Get,Show] internet tv'); -$v_tv_page-> set_info("TV schedule"); -$v_tv_page-> set_authority('anyone'); - -respond &format_tv_com if said $v_tv_page eq 'Show'; - -# Here is the guts of the asynchronous processing. get_tv_com starts the -# fetch subprocess. When it is done, fetch_tv_com reads the resulting data -# back into the main misterhouse process. - -# Fetch the latest TV programs at 3:00 AM every day -&get_tv_com(0) if (said $v_tv_page eq 'Get'); -&get_tv_com(1) if ((said $v_tv_page eq 'Reget') or - time_cron("0 3 * * *")); - -&fetch_tv_com if (changed $f_tv_file); -&fetch_tv_com if ($Startup); - -# Other code modules (eg. internet_jabber.pl) can call these without having to -# worry about implementation changes. -sub read_tv_com { - respond &format_tv_com; -} -sub display_tv_com { - display &format_tv_com; -} - -# This subroutine formats the summary of the current TV programs and content. -sub format_tv_com { - my $data; - -# # simple summary for now. -# $temp = $TV_schedule{TempOutdoor}; -# $temp =~ s/C/degrees celsius/; -# $temp =~ s/F/degrees farenheit/; -# -# $data = "The weather is " . $Weather{Conditions} . ". "; -# $data .= "It is " . $temp . " degrees outside"; -# $data .= ", with a windchill of " . $Weather{WindChill} . " degrees" if ($Weather{WindChill}); -# $data .= ". "; -# -# if ($Weather{TempOutdoor} > 15) { -# $data .= "The humidity is " . $Weather{HumidOutdoor} . -# "%, with a humidex of " . $Weather{Humidex} . -# " and a dewpoint of " . $Weather{DewpointOutdoor} . ". "; -# } -# -# if ($Weather{WindAvg}) { -# $data .= "The wind is from the " . convert_direction($Weather{WindAvgDir}) . " at " . $Weather{WindAvg} . " kilometers per hour"; -# $data .= ", gusting to " . $Weather{WindGust} . " kilometers per hour" if ($Weather{WindGust}); -# $data .= ". "; -# } -# else { -# $data .= "There is no wind. "; -# } -# -# $data =~ s%km/h%kilometers per hour%; - - return $data; -} - - -# Fetch the raw HTML TV page from the tv.com webserver, and update the parsed data file. -sub get_tv_com { - my $programs_file = $config_parms{code_dir}."/".$config_parms{TV_com_progs_file}; - open(TV_PROGRAMS, $programs_file) or print_log "Warning, could not open $programs_file!\n"; - my @AllPrograms = ; - close TV_PROGRAMS; - my $force = shift; - my $line; - my $lineNo = 0; - my $pgm; - - # erase output file content - main::file_write($f_tv_data, ''); - - print_log "running $pgm multiple times"; - foreach $line (@AllPrograms) { - chomp $line; - $lineNo++; - $pgm = "get_tv_com"; - $pgm .= " -reget" if $force; - $pgm .= " -showId " . ($lineNo-1); - print_log "Getting TV $line $pgm"; - run 'inline', $pgm; - } - - set_watch $f_tv_file; - fetch_tv_com(); -} - -# this routine fetches the TV data already formatted for the organizer. -sub fetch_tv_com { - my $tempCalBuffer = ""; - - # need to strip all entries beginning with "TV" - open CALENDAR, $calendar_data; - while () { - if ( $_ =~ m/^TV/ ) { -# print "Stripped $_"; - } - else - { - $tempCalBuffer .= $_; - } - } - close CALENDAR; - - open TV_DATA, $f_tv_data; - while () { - $tempCalBuffer .= $_; - } - close TV_DATA; - - file_write($calendar_data, $tempCalBuffer); -} - +# Category=TV + +my $f_tv_data = "$config_parms{data_dir}/tv_data"; +$f_tv_file = new File_Item($f_tv_data); +my $calendar_data = "$config_parms{data_dir}/organizer/calendar.tab"; + +# The obligatory voice command. +$v_tv_page = new Voice_Cmd('[Reget,Get,Show] internet tv'); +$v_tv_page-> set_info("TV schedule"); +$v_tv_page-> set_authority('anyone'); + +respond &format_tv_com if said $v_tv_page eq 'Show'; + +# Here is the guts of the asynchronous processing. get_tv_com starts the +# fetch subprocess. When it is done, fetch_tv_com reads the resulting data +# back into the main misterhouse process. + +# Fetch the latest TV programs at 3:00 AM every day +&get_tv_com(0) if (said $v_tv_page eq 'Get'); +&get_tv_com(1) if ((said $v_tv_page eq 'Reget') or + time_cron("0 3 * * *")); + +&fetch_tv_com if (changed $f_tv_file); +&fetch_tv_com if ($Startup); + +# Other code modules (eg. internet_jabber.pl) can call these without having to +# worry about implementation changes. +sub read_tv_com { + respond &format_tv_com; +} +sub display_tv_com { + display &format_tv_com; +} + +# This subroutine formats the summary of the current TV programs and content. +sub format_tv_com { + my $data; + +# # simple summary for now. +# $temp = $TV_schedule{TempOutdoor}; +# $temp =~ s/C/degrees celsius/; +# $temp =~ s/F/degrees farenheit/; +# +# $data = "The weather is " . $Weather{Conditions} . ". "; +# $data .= "It is " . $temp . " degrees outside"; +# $data .= ", with a windchill of " . $Weather{WindChill} . " degrees" if ($Weather{WindChill}); +# $data .= ". "; +# +# if ($Weather{TempOutdoor} > 15) { +# $data .= "The humidity is " . $Weather{HumidOutdoor} . +# "%, with a humidex of " . $Weather{Humidex} . +# " and a dewpoint of " . $Weather{DewpointOutdoor} . ". "; +# } +# +# if ($Weather{WindAvg}) { +# $data .= "The wind is from the " . convert_direction($Weather{WindAvgDir}) . " at " . $Weather{WindAvg} . " kilometers per hour"; +# $data .= ", gusting to " . $Weather{WindGust} . " kilometers per hour" if ($Weather{WindGust}); +# $data .= ". "; +# } +# else { +# $data .= "There is no wind. "; +# } +# +# $data =~ s%km/h%kilometers per hour%; + + return $data; +} + + +# Fetch the raw HTML TV page from the tv.com webserver, and update the parsed data file. +sub get_tv_com { + my $programs_file = $config_parms{code_dir}."/".$config_parms{TV_com_progs_file}; + open(TV_PROGRAMS, $programs_file) or print_log "Warning, could not open $programs_file!\n"; + my @AllPrograms = ; + close TV_PROGRAMS; + my $force = shift; + my $line; + my $lineNo = 0; + my $pgm; + + # erase output file content + main::file_write($f_tv_data, ''); + + print_log "running $pgm multiple times"; + foreach $line (@AllPrograms) { + chomp $line; + $lineNo++; + $pgm = "get_tv_com"; + $pgm .= " -reget" if $force; + $pgm .= " -showId " . ($lineNo-1); + print_log "Getting TV $line $pgm"; + run 'inline', $pgm; + } + + set_watch $f_tv_file; + fetch_tv_com(); +} + +# this routine fetches the TV data already formatted for the organizer. +sub fetch_tv_com { + my $tempCalBuffer = ""; + + # need to strip all entries beginning with "TV" + open CALENDAR, $calendar_data; + while () { + if ( $_ =~ m/^TV/ ) { +# print "Stripped $_"; + } + else + { + $tempCalBuffer .= $_; + } + } + close CALENDAR; + + open TV_DATA, $f_tv_data; + while () { + $tempCalBuffer .= $_; + } + close TV_DATA; + + file_write($calendar_data, $tempCalBuffer); +} + diff --git a/code/public/hvac_upb_thermostat.pl b/code/public/hvac_upb_thermostat.pl index 56769f68a..98ddef767 100644 --- a/code/public/hvac_upb_thermostat.pl +++ b/code/public/hvac_upb_thermostat.pl @@ -1,53 +1,53 @@ -# category = HVAC - -if (my $current = $Livingroom_Thermostat->state_now) -{ - if ($current =~ /inside_temp: (\d+)/) - { - print "inside thermostat temp is: $1\n" - } - if ($current =~ /outside_temp: (\d+)/) - { - print "outside thermostat temp is: $1\n" - } - if ($current =~ /heat_sp_temp: (\d+)/) - { - print "heat setpoint temp is: $1\n" - } - if ($current =~ /cool_sp_temp: (\d+)/) - { - print "cool setpoint temp is: $1\n" - } - if ($current =~ /mode: (.*)/) - { - print "HVAC mode is: $1\n" - } - if ($current =~ /fan: (.*)/) - { - print "fan is: $1\n" - } - if ($current =~ /setback: (.*)/) - { - print "inside thermostat temp is: $1\n" - } - if ($current =~ /display_lockout: (.*)/) - { - print "display lockout status is: $1\n" - } - if ($current =~ /thermostat_status/) - { - # with this, you can call the different methods to get the current values - } - if ($current =~ /operating_mode_status: (.*)/) - { - print "thermostat operating mode is: $1\n" - } -} - -$v_Livingroom_Thermostat_mode = new Voice_Cmd("Set the Livingroom Thermostat mode to [off, heat, cool, auto]"); -$Livingroom_Thermostat->mode($v_Livingroom_Thermostat_mode->{state}) if (said $v_Livingroom_Thermostat_mode); - - -#$v_Master_Bedroom_Thermostat_mode = new Voice_Cmd("Set the Master Bedroom Thermostat mode to [off, heat, cool, auto]"); -#$Master_Bedroom_Thermostat->mode($v_Master_Bedroom_Thermostat_mode->{state}) if (said $v_Master_Bedroom_Thermostat_mode); - +# category = HVAC + +if (my $current = $Livingroom_Thermostat->state_now) +{ + if ($current =~ /inside_temp: (\d+)/) + { + print "inside thermostat temp is: $1\n" + } + if ($current =~ /outside_temp: (\d+)/) + { + print "outside thermostat temp is: $1\n" + } + if ($current =~ /heat_sp_temp: (\d+)/) + { + print "heat setpoint temp is: $1\n" + } + if ($current =~ /cool_sp_temp: (\d+)/) + { + print "cool setpoint temp is: $1\n" + } + if ($current =~ /mode: (.*)/) + { + print "HVAC mode is: $1\n" + } + if ($current =~ /fan: (.*)/) + { + print "fan is: $1\n" + } + if ($current =~ /setback: (.*)/) + { + print "inside thermostat temp is: $1\n" + } + if ($current =~ /display_lockout: (.*)/) + { + print "display lockout status is: $1\n" + } + if ($current =~ /thermostat_status/) + { + # with this, you can call the different methods to get the current values + } + if ($current =~ /operating_mode_status: (.*)/) + { + print "thermostat operating mode is: $1\n" + } +} + +$v_Livingroom_Thermostat_mode = new Voice_Cmd("Set the Livingroom Thermostat mode to [off, heat, cool, auto]"); +$Livingroom_Thermostat->mode($v_Livingroom_Thermostat_mode->{state}) if (said $v_Livingroom_Thermostat_mode); + + +#$v_Master_Bedroom_Thermostat_mode = new Voice_Cmd("Set the Master Bedroom Thermostat mode to [off, heat, cool, auto]"); +#$Master_Bedroom_Thermostat->mode($v_Master_Bedroom_Thermostat_mode->{state}) if (said $v_Master_Bedroom_Thermostat_mode); + diff --git a/data/event_sounds.txt b/data/event_sounds.txt index ffc202288..d674593af 100644 --- a/data/event_sounds.txt +++ b/data/event_sounds.txt @@ -1,33 +1,33 @@ -#event sounds -#one per line -# $Date$ -# $Revision$ - -alarm=>'sound_nature/gonge.wav', volume => 100, mode=>'unmuted', nolog=>1 -movement1 => 'sound_nature/02.wav', volume => 10 -movement2 => 'sound_nature/02.wav', volume => 100 -unauthorized => 'sound_nature/loon.wav', volume => 100 -3 => 'sound_nature/65.wav', volume => 20 -4 => 'sound_nature/aviary.wav', volume => 20 -barcode_scan => 'sound_beep1.wav', volume => 70 -mh_problem => 'sound_nature/bird1.wav', volume => 20 -mh_pause => 'none', volume => 20 -7 => 'sound_nature/bird2.wav', volume => 20 -wap => 'sound_nature/bird3.wav', volume => 100 -tell_me => 'sound_nature/birds.wav', volume => 20 -10 => 'sound_nature/chirp.wav', volume => 20 -router_hit => 'sound_nature/frog.wav', volume => 2 -12 => 'sound_nature/frog3.wav', volume => 20 -13 => 'sound_nature/h4560sh.wav', volume => 20 -14 => 'sound_nature/kirtland.wav', volume => 20 -router_new => 'sound_nature/loon.wav', volume => 20 -16 => 'sound_nature/octap95.wav', volume => 20 -17 => 'sound_nature/parakeet.wav', volume => 20 -18 => 'sound_nature/ribbit.wav', volume => 10 -19 => 'sound_nature/wren.wav', volume => 20 -timer => 'sound_nature/gonge.wav', volume => 100, rooms => 'all', time => 3 -announcement => 'announcement.wav', volume => 100, nolog=>1 -chat => 'sound_nature/loon.wav', volume => 100, nolog => 1 -frog => 'sound_nature/frog.wav', nolog => 1 -cash_register => 'cash_register.wav', nolog => 1 +#event sounds +#one per line +# $Date$ +# $Revision$ + +alarm=>'sound_nature/gonge.wav', volume => 100, mode=>'unmuted', nolog=>1 +movement1 => 'sound_nature/02.wav', volume => 10 +movement2 => 'sound_nature/02.wav', volume => 100 +unauthorized => 'sound_nature/loon.wav', volume => 100 +3 => 'sound_nature/65.wav', volume => 20 +4 => 'sound_nature/aviary.wav', volume => 20 +barcode_scan => 'sound_beep1.wav', volume => 70 +mh_problem => 'sound_nature/bird1.wav', volume => 20 +mh_pause => 'none', volume => 20 +7 => 'sound_nature/bird2.wav', volume => 20 +wap => 'sound_nature/bird3.wav', volume => 100 +tell_me => 'sound_nature/birds.wav', volume => 20 +10 => 'sound_nature/chirp.wav', volume => 20 +router_hit => 'sound_nature/frog.wav', volume => 2 +12 => 'sound_nature/frog3.wav', volume => 20 +13 => 'sound_nature/h4560sh.wav', volume => 20 +14 => 'sound_nature/kirtland.wav', volume => 20 +router_new => 'sound_nature/loon.wav', volume => 20 +16 => 'sound_nature/octap95.wav', volume => 20 +17 => 'sound_nature/parakeet.wav', volume => 20 +18 => 'sound_nature/ribbit.wav', volume => 10 +19 => 'sound_nature/wren.wav', volume => 20 +timer => 'sound_nature/gonge.wav', volume => 100, rooms => 'all', time => 3 +announcement => 'announcement.wav', volume => 100, nolog=>1 +chat => 'sound_nature/loon.wav', volume => 100, nolog => 1 +frog => 'sound_nature/frog.wav', nolog => 1 +cash_register => 'cash_register.wav', nolog => 1 about => 'sound_nature/loon.wav', nolog => 1 \ No newline at end of file diff --git a/data/infrared/devicelib/Apple MAC.dvc b/data/infrared/devicelib/Apple MAC.dvc index 1cb6225cf..f36b75de3 100755 --- a/data/infrared/devicelib/Apple MAC.dvc +++ b/data/infrared/devicelib/Apple MAC.dvc @@ -1,16 +1,16 @@ -Manufacturer=Apple -Model=MAC - -[Key Codes] - -DOWN = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 - -LEFT = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 - -MENU = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 - -RIGHT = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 - -SELECT = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 - -UP = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 +Manufacturer=Apple +Model=MAC + +[Key Codes] + +DOWN = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 + +LEFT = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 + +MENU = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 + +RIGHT = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 + +SELECT = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 + +UP = 0000 006D 0022 0002 0157 00AC 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 0157 0056 0015 0E94 diff --git a/data/infrared/devicelib/Motorola qip6200.dvc b/data/infrared/devicelib/Motorola qip6200.dvc index b017f0f33..269120633 100755 --- a/data/infrared/devicelib/Motorola qip6200.dvc +++ b/data/infrared/devicelib/Motorola qip6200.dvc @@ -1,71 +1,71 @@ -Manufacturer=Motorola -Model=QIP6200 - -[Key Codes] - -A = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 - -On Demand = 0000 006D 002E 0001 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0633 0154 0054 0013 0D07 0154 0055 0013 01D2 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0633 0154 0055 0013 0D06 0154 0055 0013 01D5 0154 00AA 0013 00AA 0013 0055 - -P- = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0435 0154 0055 0013 0D06 - -P+ = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0435 0154 0054 0013 0D07 - -Up Arrow = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0054 0013 0D07 - -Down Arrow = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 - -Left Arrow = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 03E0 0154 0055 0013 0D07 - -Right Arrow = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 03E0 0154 0055 0013 0D07 - -Info = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0435 0154 0055 0013 0D07 - -Guide = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 - -OK = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 - -Power Toggle = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 04DF 0154 0055 0013 0D07 - -Setup = 0000 0070 0000 0032 0080 0040 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0030 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0030 0010 0010 0010 0010 0010 0AC4 - -C = 0000 006D 0035 0000 0154 00AA 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0589 0154 0055 0013 0D06 0154 0054 0013 01DB 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 0154 0055 0013 01D8 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA - -Menu = 0000 006D 0018 0000 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 048A 0154 0055 0013 0D07 0154 0055 0013 0D07 0154 0055 0013 00AA - -Exit = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D08 - -Help = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 0435 0154 0054 0013 0D07 - -Last = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 - -Fav = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 048A 0154 0054 0013 0D07 - -Enter = 0000 006D 0018 0000 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 00AA 0013 048A 0154 0054 0013 0D07 0154 0055 0013 0D06 0154 0054 0013 00AA - -Input = 0000 006D 0016 0000 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 048A 0154 0054 0013 0D07 0154 0055 0013 00AA - -1 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 - -5 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 - -8 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0589 0154 0055 0013 0D06 - -7 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 - -6 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 04DF 0154 0055 0013 0D07 - -4 = 0000 006D 0018 0000 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0534 0154 0054 0013 0D07 0154 0055 0013 0D08 0154 0054 0013 00AA - -3 = 0000 006D 0018 0000 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0054 0013 0D07 0154 0054 0013 0D07 0154 0055 0013 00AA - -2 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 04E0 0154 0055 0013 0D07 - -9 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 048A 0154 0054 0013 0D07 - -0 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0633 0154 0055 0013 0D07 - -CH- = 0000 006D 0014 0000 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 048B 0154 0055 0013 00AA - -CH+ = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0534 0154 0055 0013 0D07 - +Manufacturer=Motorola +Model=QIP6200 + +[Key Codes] + +A = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 + +On Demand = 0000 006D 002E 0001 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0633 0154 0054 0013 0D07 0154 0055 0013 01D2 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0633 0154 0055 0013 0D06 0154 0055 0013 01D5 0154 00AA 0013 00AA 0013 0055 + +P- = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0435 0154 0055 0013 0D06 + +P+ = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0435 0154 0054 0013 0D07 + +Up Arrow = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0054 0013 0D07 + +Down Arrow = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 + +Left Arrow = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 03E0 0154 0055 0013 0D07 + +Right Arrow = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 03E0 0154 0055 0013 0D07 + +Info = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0435 0154 0055 0013 0D07 + +Guide = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 + +OK = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 + +Power Toggle = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 04DF 0154 0055 0013 0D07 + +Setup = 0000 0070 0000 0032 0080 0040 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0030 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0010 0010 0010 0010 0010 0010 0010 0010 0030 0010 0030 0010 0010 0010 0010 0010 0AC4 + +C = 0000 006D 0035 0000 0154 00AA 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0589 0154 0055 0013 0D06 0154 0054 0013 01DB 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 0154 0055 0013 01D8 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA + +Menu = 0000 006D 0018 0000 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 048A 0154 0055 0013 0D07 0154 0055 0013 0D07 0154 0055 0013 00AA + +Exit = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D08 + +Help = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 0435 0154 0054 0013 0D07 + +Last = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 + +Fav = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 048A 0154 0054 0013 0D07 + +Enter = 0000 006D 0018 0000 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 00AA 0013 048A 0154 0054 0013 0D07 0154 0055 0013 0D06 0154 0054 0013 00AA + +Input = 0000 006D 0016 0000 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 048A 0154 0054 0013 0D07 0154 0055 0013 00AA + +1 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 00AA 0013 048A 0154 0055 0013 0D07 + +5 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 + +8 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0589 0154 0055 0013 0D06 + +7 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 048A 0154 0055 0013 0D07 + +6 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 04DF 0154 0055 0013 0D07 + +4 = 0000 006D 0018 0000 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0534 0154 0054 0013 0D07 0154 0055 0013 0D08 0154 0054 0013 00AA + +3 = 0000 006D 0018 0000 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 00AA 0013 048A 0154 0054 0013 0D07 0154 0054 0013 0D07 0154 0055 0013 00AA + +2 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 04E0 0154 0055 0013 0D07 + +9 = 0000 006D 0012 0002 0154 00AA 0013 00AA 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 00AA 0013 0055 0013 048A 0154 0054 0013 0D07 + +0 = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0633 0154 0055 0013 0D07 + +CH- = 0000 006D 0014 0000 0154 00AA 0013 00AA 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 00AA 0013 0055 0013 048B 0154 0055 0013 00AA + +CH+ = 0000 006D 0012 0002 0154 00AA 0013 0055 0013 0055 0013 00AA 0013 00AA 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 0055 0013 00AA 0013 0055 0013 0534 0154 0055 0013 0D07 + diff --git a/data/infrared/devicelib/Samsung HLT Series TV.dvc b/data/infrared/devicelib/Samsung HLT Series TV.dvc index cf60c3ea1..a3bdc4088 100755 --- a/data/infrared/devicelib/Samsung HLT Series TV.dvc +++ b/data/infrared/devicelib/Samsung HLT Series TV.dvc @@ -1,134 +1,134 @@ -Manufacturer=Samsung -Model=HLT Series -Device=TV - -[Key Codes] - -ANT = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Vid1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 - -S-Vid2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 - -DVI = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 - -Vid2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 - -Comp1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 - -HDMI = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 - -HDMI2 = 0000 006C 0022 0000 00AD 00AD 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0015 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0015 0015 0728 - -HDMI3 = 0000 006C 0022 0000 00AD 00AD 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0041 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0728 - -USB = 0000 006C 0022 0000 00AD 00AD 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0728 - -S-Vid1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 - -Comp2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 - -TV/Video = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -PC = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Chan Up = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Prev-Ch = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Fav Ch = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0689 - -Chan Down = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Add/Del = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Fav Ch2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0689 - -Menu = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Exit = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -Down = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Left = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Info = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -Up = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Right = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Enter = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -MTS = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -Vol- = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -Sound Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -SRS = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Vol+ = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -Mute = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -MTS2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -PIP = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -PIP Ch DN = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0689 - -Swap = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -PIP Size = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -PIP Ch UP = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0689 - -1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -4 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -7 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -5 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -8 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -3 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -6 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -9 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -0 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 - -100+ = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -VCR Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 - -STB Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 - -Cable Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 - -TV Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 - -DVD Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 - -Pic Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 - -Pic Size = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0689 - -Still = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0689 - -DNIe = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 - -Power Toggle = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - -Power On = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0689 - -Power Off = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0689 - -Sleep = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 - +Manufacturer=Samsung +Model=HLT Series +Device=TV + +[Key Codes] + +ANT = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Vid1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 + +S-Vid2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 + +DVI = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 + +Vid2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0689 + +Comp1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 + +HDMI = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 + +HDMI2 = 0000 006C 0022 0000 00AD 00AD 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0015 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0015 0015 0728 + +HDMI3 = 0000 006C 0022 0000 00AD 00AD 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0041 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0728 + +USB = 0000 006C 0022 0000 00AD 00AD 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0015 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0015 0015 0041 0015 0041 0015 0041 0015 0015 0015 0728 + +S-Vid1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 + +Comp2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0689 + +TV/Video = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +PC = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Chan Up = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Prev-Ch = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Fav Ch = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0689 + +Chan Down = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Add/Del = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Fav Ch2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0689 + +Menu = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Exit = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +Down = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Left = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Info = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +Up = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Right = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Enter = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +MTS = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +Vol- = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +Sound Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +SRS = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Vol+ = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +Mute = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +MTS2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +PIP = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +PIP Ch DN = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0689 + +Swap = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +PIP Size = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +PIP Ch UP = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0689 + +1 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +4 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +7 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +2 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +5 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +8 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +3 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +6 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +9 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +0 = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0689 + +100+ = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +VCR Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 + +STB Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 + +Cable Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 + +TV Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 + +DVD Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0689 + +Pic Mode = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0689 + +Pic Size = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0689 + +Still = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0041 0015 0689 + +DNIe = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0689 + +Power Toggle = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + +Power On = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0689 + +Power Off = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0689 + +Sleep = 0000 006D 0000 0022 00AC 00AB 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0016 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0041 0015 0689 + diff --git a/docs/faq.pod b/docs/faq.pod index b7292385b..344bc036e 100644 --- a/docs/faq.pod +++ b/docs/faq.pod @@ -1,1789 +1,1789 @@ -=pod - -=head1 1: MisterHouse operational questions - -=head2 1.1: What OSes can it run on? - -It has been tested on Windows 95, 98, NT 4.0, 2K, XP, Vista (!) and 7. -On Unix, it has been run on Linux (Ubuntu, Debian, Fedora and others), AIX, and Sun. - -In theory, any OS that runs perl should be able to run mh. In practice, some platforms will likely have strange Serial Interfaces, so the Serial I/O related function would take some work. - -=head2 1.2: How can I make it run faster and use less memory? - -On my Celron 400 Mhz computer running Windows 98, using 60 code files with 6000 lines of code, mh takes 10% the cpu, running at 9 passes per second and using about 30 MB of memory. - -If you are happy with the Web interface and don't need the Tk interface, turn it off by editing the Tk mh.ini parm or run: - - mh -tk 0 - -If you want the tk window, but don't use the Command/Items/Group pull down menus, try: - - mh -tk_comands 0 -tk_items 0 -tk_groups 0 - -You can turn off the tk 'eye' that bounces back and forth by deleting the tk_eye.pl code file. - -You can speed up / slow down mh, using more/less cpu, by mh.ini sleep_time parm. - -Here are some memory stats: - - Linux - 10 Meg <- mh -tk 0 mh_control.pl (run just one code file) - 12 Meg <- mh -tk 0 (run all the default code) - 17 Meg <- mh (run all the default code with tk gui on) - - Windows - 18 Meg <- mh -tk 0 mh_control.pl (run just one code file) - 20 Meg <- mh -tk 0 (run all the default code) - 27 Meg <- mh (run all the default code with tk gui on) - - - -=head2 1.3: How can I easily upgrade to the latest version of mh? - -Point your mh.ini code_dir, data_dir, and html_dir parms to somplace other than your mh path. -Then you should be able to rename your old mh directory and unzip the new one in its place without loosing -anything. Also keep your mh.private.ini somewhere else (see next question). - -=head2 1.4: How does mh.ini work? - -All the entries in mh.ini are read on mh Startup and on a Reload. They are stored -in the %config_parms array. So, for example, if you added: - - myparm1=a b c - -Then, mh would set $config_parms{myparm1} = 'a b c'; - -These parms also defined what are legal startup values, so if you want to try -a different value, instead of editing your mh.ini, you can run: - - mh -myparm1 'd e f' - -Rather then edit the default mh.ini file, the best approach is to copy just the parms you want to change to your own .ini file (e.g. mh.private.ini), then set the environmental variable mh_parms to point where you keep that file (see the header of mh/bin/mh.ini for more detail). - -While the %config_parms array IS refreshed on a mh Reload, some parms are only used on mh startup (e.g. http_port), so will only be reloaded when you re-start mh. - -=head2 1.5: Can I run run mh in a 'fast debug mode'? - -Funny you should ask :) Check out the time_start, time_stop, and time_increment parms -in the mh.ini file. Here are some examples: - - mh -time_start 0 -time_stop 24 -tk 0 -voice_text 0 - mh -time_start "6 AM" -time_stop "11 PM" -time_increment 1 - mh -time_start "5/14 7:10" -time_stop "5/15 10 PM" -time_increment 300 - - -=head2 1.6: How are the X10 and Serial Items implemented, indpendent of the platform? - -The user works with an X10_Item which reflects physical hardware and the state of that hardware. The user "loop" code deals with the state and -conditions which alter it. This code is independent of OS and specific to mh. - -X10_Items talk to Serial_Items which implement a software protocol required by the model and type of the actual hardware. The Serial_Items -can use protocol translator modules such as CM11.pm, HomeBase.pm, and CM17.pm. But the output is platform-independent serial commands -to the next layer. The protocol translator modules are publicly available outside mh and may be used by code other than Serial_Items. These modules will eventually be on CPAN. The code is still independent of OS. Other Serial_Item types, including "generic" read/write ports, also exist. - -Win32::SerialPort.pm and Device::SerialPort.pm are CPAN modules which actually implement the serial command interface. Device::SerialPort is a -clone of its Win32 cousin. These modules handle high-level OS issues including device names, configuration files, and validation of port -settings. - -Each also calls a CPAN module to handle low-level interfacing to the OS and the serial driver: Win32API::CommPort.pm or POSIX.pm (you guessed -correctly ;-) - - -=head2 1.7: mh seems way too complicated. How can I run something simple? - -Copy mh\code\test\test_x10.pl into your own code dir (e.g. c:\mh_code). Then run: - - mh -code_dir c:\mh_code - -You can then use http://localhost:8080 for control. This will also run all the -code in mh/code/common. To run JUST a specific code member(s), specify -it on the command line. For example: - - mh test_x10.pl - - -=head2 1.8: Is there a way I can have direct X10 control with a simple perl script? - -If all you want to do is control X10 devices with a cron job or from your own cgi scripts, -running mh is probably overkill. You can send X10 commands using either the ActiveHome -interface (CM11, available from http://www.x10.com/automation/x10_ck11a_d.htm ) -or the cheaper Firecracker interface (CM17, available from http://www.x10.com/automation/firecracker_d.htm ) -using the modules and documentation found in mh/lib/site/ControlX10 or download them from CPAN. -Examples can be found in mh/lib/site/test_cm11.pl and test_cm17.pl. - - - -=head2 1.9: Can I send mh commands from other programs? - -You can send mh commands (Voice_Cmd text strings) from other programs in many ways: - -- Write commands to the file specified in the mh.ini xcmd_file parm. By default this -is mh/house_cmd.txt. See mh/bin/house and mh/bin/house.bat for examples. - -- Use mh/bin/mhsend to send commands via sockets (can be over the internet). If you have -run mh/bin/set_password, the mhsend -password parm is required. Some examples: - - mh/bin/mhsend -host ip.A -run "All Lights On" - mhsend -p password -run "turn living room light on" - -Where ip.A is computer A's ip address (or name if you have a HOST file setup -to alias a name to an ip address). mhsend is a stand alone perl script, -so you do not need mh running there (although it will not hurt). -The code that allows this is in mh/code/common/mhsend_server.pl. -An example of mh code to act as a client to different mh box is in mh/code/examples/test_server2.pl - -- You can also control mh remotely via email (see faq question 'Can do I send mh comands via email?'), -telnet sessions (if running mh/code/common/telent.pl), and via the built in web server. An example url -for the web server is: - - http://192.168.1.10:8080/RUN;last_response?turn_living_room_light_on - -- If you are runing IM client (if running code file internet_im.pl) -like AOL AIM, MSN, or Jabber, you can any voice command to -MisterHouse if you give Misterhouse its own account to logon with. - - - -=head2 1.11: How can I debug a problem? - -Lots of debug errata can be controled with the mh -debug switch. Most of this code is probably -meaningful only if you dig into the source code, so most people probably will not use -it unless you are generating debug to send to the author. To create a log file with debug in it, -run: - - mh -log debug.log -debug xyz - -Where xyz can be any of the following: - - x10 - serial - socket - startup - homebase - misc - http - port_name of socket port - -You can also stack debug flags, using ; as a separator. fro example: - - mh -debug "serial;x10" - - -=head2 1.12: How can I debug timed event problem? - -One way is to create a code member that contains just the event you are trying to debug, then run mh on just that one member. For example, create a member called \temp\test_time.pl with this code: - - print "min=$Minute\n" if $New_Minute; - print "hour=$Hour\n" if $New_Hour; - print "debug1 it is now $Time_Now\n" if time_now "11:59 PM"; - print "debug2 it is now $Time_Now\n" if time_now "23:59"; - print "debug3 it is now $Time_Now\n" if time_now "12:00 AM"; - print "debug4 it is now $Time_Now\n" if time_now "00:00"; - -Then run mh using these parameters: - - mh -code_dir \temp -tk 0 -time_start '12/15 11:58 pm' -time_start '12/16 12:01 am' - -Note, you can run this test mh quickly without stopping your normal mh. - -=head2 1.13: What are the advantages/disadvantages to running the compiled versions of mh? - -Advantages: - -=over - -=item * - -You don't need the latest version of perl installed - -=item * - -You don't have to install additional perl modules - -=back - -Disadvantages: - -=over - -=item * - -For some reason, starting mh with the compiled versions is slower. On my windows box, it is 10 seconds -vs- 20 seconds. - -=item * - -You can apply quick patches to mh, -vs- downloading a larger, less frequently updated mh.exe file. - -=back - -The memory and cpu used, once mh is started, is the same. - - -=head2 1.14: How does mh read and use the user code in code_dir - -Each time mh is started or a Reload is done, mh will re-read all the .pl code members -in the code_dir directory (this is a mh.ini parm) -and the file data_dir/mh_temp.user_code is created. -All of the objects and global variables (see FAQ question 2.4), are put at top -of this file, and everything else is put in seperate subroutines, one for each file read. -A &loop_code subroutine is also created (at the very bottom of mh_temp.user_code), that -calls each member subroutine. - -After creating this file, mh runs the perl eval function on its contents. This re-creates all the objects, -global variables, and code_dir member subroutines. Then mh goes back into its normal loop where -it queries various input data (e.g. serial, tcp/ip, voice, time), updates objects and variables, -evals the &loop_code code (it does an eval, rather then execute it directly, so we can trap errors and -not kill mh), sleeps for a bit, then repeats. - - -=head2 1.15 How can I control mh from a non-mh web server like Apache? - -You can think of the built in mh web server as 2 parts: - - - The part that auto-generates web pages that reflect your items and commands. - - - The part that responds to commands you give it via URLs. - -You don't really have to use the first part. -Some mh users custom generate their own mh pages, to be served by the mh server or -by a different server. This allows for full control of -what is on the web control page and how it looks. - -There is more (probably not so easy to read) documentation on how to do this -under 'Customizing the Web Interface' in mh/docs/mh.html - -You can also use something like mh/code/common/mhsend_server.pl -and then read/write to sockets directly from non-mh web server cgi scripts. -Or have the web server script simply call mh/bin/mhsend to run mh commands. - -=head2 1.16 Is there a front-end tool for defining/editing mh items/events? - -Not yet, but you can now define items/events in a table-like format. -This should allow us to enable a gui or web based front end tools for editing. - -Currently we have defined just one format defined, creativly named A, -but other formats (e.g. XML) should be easy to implement. - -To use, create one or more *.mht (mh table) files in your code directory, specifying the table format with Format=A -at the top of the table. Then mh will process (on starup and reload) each record with mh/lib/read_table_A.pl, to create -*.mhp (mh processed) files, which are then processed with all your other mh *.pl code files. - -Examples are in mh/code/bruce/*.mht and mh/code/test/*.mht. -If you want to create another table format, all you need to do is create a mh/lib/read_table_xyz.pl member, then set Format=xyz. - -=head2 1.17: How can I use Family Radios with MisterHouse? - -=begin html -There is seperate FAQ on the FRS topic in -mh/docs/faq_frs.html - -=end html - - -=head2 1.18: Is there a way to tell if MH is shutdown correctly or not? - -On windows, mh/bin/mh.bat checks errorlevel and on linux, mh/bin/mhl finds the return code via $?. -Both of these shells will restart mh unless they detect a normal exit (rc=1). - -This code in mh/code/common/mh_control.pl will detect an abnormal restart: - - if ($Startup and $Save{mh_exit} ne 'normal') { - display "MisterHouse auto restarted: $Save{mh_exit}", 0; - } - -If you want to detect a power outage and you have a CM11, -code in mh/code/common/mh_control.pl will set $Power_Supply, so you can do something like this: - - if (state_now $Power_Supply eq 'Restored') { - speak 'You should have bought a ups'; - } - - -=head2 1.19: How can I use an Internet Appliance with MisterHouse? - -=begin html -There is seperate FAQ on varous Internet Appliances in -mh/docs/faq_ia.html - -=end html - - -=head2 1.20: Where is perl program code located? - -From Harald Koch on 2/2002. - -There are (at least) three different locations for scripts: - -/mh/bin - standalone programs (such as get_weather, get_tv_grid); they are called from the main MH process via "run" or a Process_Item; - they do not get any environment or variables from the main MH program, and typically return results to MH by writing output files - which MH later reads. - - MH typically looks for external programs here, and also programs - here typically assume they're being run from this directory; they - try to find files in ../lib, for example. - -$config_parms{code_dir} - scripts in this directory are (after some parsing) loaded into the middle of the MH event loop, and the - commands in them run constantly. They run within the MH environment, - and can use all the variables available there. - -$config_parms{html_dir} - scripts in this directory are run by the HTTP - server, and are expected to return blobs of HTML. They too run - inside the MH process, and can access all variables, subroutines, - etc. - -Each script directory has a different purpose, and programs in one are -generally not terribly useful in another. My weather processing, for -example, has scripts in all three places; mh/bin/get_weather_ca is the -standalone that fetches/parses HTML, code/weather_ec.pl is the script -that creates and processes voice commands and timer events, and then I -have scripts in /web/weather/ that format the HTML for display. - - - - -=head1 2: MisterHouse coding questions - -=head2 2.1: How do I debug syntax errors in my code? - -If there are syntax errors in your code, most of them will be caught on startup or reload. -The error, and (hopefully) relevant code member and/or line number, will show up in -your console, and if running Tk, in a Tk window on reload. - -When you go to write new code, first start mh with old code that works, then edit or copy in -your new code and do a mh Reload. Fixing errors with code Reloads is much -quicker than trying to fix them on mh startup. You can add/delete the .pl suffix -on code members between reloads to enable/disable them. - -Some errors (e.g. too few/many {} around code) will point to a misleading line number. -In these cases, if you can not find the file with the error, -you can try a new option: mh -error_by_file 1 . -This will try loading one file at a time untill the error is found, but -it runs pretty slow and may not work with members that have # noloop directives. - -Some errors will only show up after mh triggers events. The first time this happens, a console/tk message -will be displayed, and a message spoken. Since these errors might happen on sequential passes of mh, subsequent errors -are only listed in the console window. If more than 10 occur, another window is displayed, -and the code member there error was in is disabled. - -All of these on-the-fly errors are logged in the file mh_temp.error_log in your code directory. -This is useful if you are not running tk and the error has scrolled out of your console window. - -You can enable the perl diagnostics module (same as perl -w) to get additional warnings on potential -coding errors. Either use mh -diagnostics 1 or set diagnostics=1 in your mh.ini. This takes -about 1 meg more of memory and causes mh to run about 10% slower, but the messages it displays -often point to valid problems. -To simplify coding, the 'uninitialized value' warnings disabled in the user code processing. - -=head2 2.2: How should I code events that should occur only at infrequently, or -only at Startup? - -You can use the $Startup or $Reload variables for events that should only -occur when mh is Started, or when code is Reloaded. Note that $Reload -is also true when $Startup is true. - -To run events infrequently, you can use the modulus operator (%), -$New_Second/Hour/Day/Month/Year variables, -or the Time_Now/Time_Cron functions to control events. - -The $New_xxx variables are 1 (true) for each pass that the xxx variable gets incremented, -and 0 for all other passes. For example, $New_Hour is true for one pass -when $Hour changes from one hour to another. - -Here are some examples: - - print_log "Only at startup" if $Startup; - - print_log "Every 5 minutes" if $New_Minute and !($Minute % 5); - - print_log "At 10:05 pm" if time_now '10:05 PM'; - - print_log "Every weekday at 2:15 am" time_cron '2 15 * * 1-5'; - - -=head2 2.3: How should I structure my Perl code? - -Pretty much anyway you want! You may want to put object definitions all in one -member (e.g. mh/code/Bruce/items.pl) so you can easily change device codes, -or you may want to define the objects in the file members you use them in. - -If the same object is defined in multiple members, it will still work ok, although -some object types (e.g. Voice_Item) will warn about duplicate names. - - -=head2 2.4: Can I create global variables that can be shared between code members? - -Yep. All objects definitions (e.g. $light = new X10_Item('C5')) are always shared. They are pulled out of the member loop by mh. - -All other variable declarations must be made using with 'my' or 'use vars': - - my ($my_var1, $my_var2); - use vars '$my_var1', '$my_var2'; - use vars qw($my_var1 $my_var2); - -The last example uses the qw function (Quote Words) to save you from the ' and , punctuation. - -If you code a 'my' or 'use var' record that starts in column 1, -mh will pull these records out of the member loop code (as it did with object definitions), -so that the variable can now be shared with other code members. - -If the 'my' or 'use var' record does not start in column 1, the variable declaration -is left in with the rest of the member code and is local to that member. - -If you want to use the variable with any mh/web/*.pl script when creating -mh web pages, you must use 'use vars', as 'my' variables can not be shared with the server. - - -=head2 2.5: How can I move code out of the loop code? - -Sometimes you may need to have mh run code out of the loop code. For example, if you -want to create a Voice_Cmd object that has states that are read from a file. Since -objects are moved out of the loop code by mh, we need to also move any other code used -to define that object. - -You can tell mh to do this using '# noloop=start/stop' comments. For example: - - # noloop=start - my $mp3names; - while ( my $mp3name = ) { - $mp3name =~ s#^.*/##; # remove path - $mp3name =~ s#\..*$##; # remove extension - $mp3names .= "," if $mp3names; - $mp3names .= $mp3name; - } - # noloop=stop - - $v_play_music = new Voice_Cmd("Play [$mp3names]"); - if ($state = said $v_play_music) { - speak "Playing song $state"; - run("winamp d:/songs/$state.mp3") - } - - -=head2 2.6: How can I add my own subroutines? - -You can add your own subroutines anywhere in any of your code files. These can then be -called by that member or any other member. Here is a couple of examples: - - print_log &mysub1(9990, 9); - - sub mysub1 { - my ($a, $b) = @_; - my $c = $a + $b; - return "The answer is: $a + $b = $c"; - } - - - $v_bedroom_curtain = new Voice_Cmd('[open,close] the bedroom curtains'); - &curtain_on('bedroom', $state) if $state = said $v_bedroom_curtain; - - sub curtain_on { - my($room, $action) = @_; - set $curtain_updown $action; - eval "set \$curtain_$room ON;"; - } - -=head2 2.7: What other special global variables are there? - -Here are a few: - - $Pgm_Path is the directory that that mh/bin/mh is found in. - $Pgm_Root is one directory above $Pgm_Path - $OS_win is true on windows, false otherwise. $^O will reflect the OS name. - -=head2 2.8: Can I use mh to control other windows - -On Windows (95,98, NT), you can use the SendKeys to send keystrokes to other windows. See mh.html for details. - -If anyone knows of an equivalent function for linux, let me know. Linux is usually clever enough to have -command line options, so there is less of a need here. - - - -=head2 2.9: What data is saved when mh is exited and restarted? - -The state of all objects (Timer Serial_Item X10_Item X10_Appliance File_Item Generic_Item) and all the $Save variables. - -Every 5 minutes while mh is running, and whenever mh is exited, the state of these variables are saved to your code directory in a member called mh_temp.saved_states. When you start mh, this member is run to restore the states. - -You can make up any members to the $Save array. For example, you can use $Save{sleeping_cat} to track whenever your cat is asleep. - - -=head2 2.10: How can I turn a bunch of lights on/off all at once? - -You can turn all the lights on a single house code on/off by specifying just the house code letter. For example: - - $v_bedroom_lights = new Voice_Cmd('Bedroom lights [on,off]'); - $bedroom_lights = new X10_Item('P'); - set $bedroom_lights $state if $state = said $v_bedroom_lights; - -You can also create a group of X10 items. Here is an example that would turn all lights on/off in several rooms. - - $all_lights_bed = new X10_Item('P'); - $all_lights_living = new X10_Item('O'); - $all_lights = new Group($all_lights_bed, $all_lights_living); - - $v_lights_all = new Voice_Cmd('All lights [on,off]'); - set $all_lights $state if $state = said $v_lights_all; - - -=head2 2.11: How do I get mh to stop telling the time each minute? - -Out of the box, mh defaults to running most of the code in the mh/code/common directory and all the code in mh/code/test. -Members test_tagline.pl and the hello_speak.pl members are the main sources of the periodic test speech. -Once you have played around with the example code, you will want to use -http://localhost:8080/bin/code_select.pl menu (avaliable on the web ia5 MrHouse menu 'Select Code' button) -and select only the code files you find useful. - -See the FAQ question 'How does mh.ini work' on how to modify the code_dir parm to point to your new code directory. - - -=head2 2.12: Can do I send mh comands via email? - -Set the mh.ini net_mail_command_code parm to a secret code, and copy mh/code/common/internet_mail.pl -into your code directory. Now you can send email with the following string either -in the subject or the body of your email: - - command:your command code:your_code - -The internet_mail.pl code periodicaly runs the get_email process and then scans for the above string. -If found, it will send a confirmation email saying either "command run", "command not found", -or "command not authorized". - - -=head2 2.13: How can I control Command Categories - -All Voice_Cmd objects are listed in the Tk Command pull down and Web Category list. -These lists are organize by Category, which is specified with a Category=value comment in the user code. -For example: - - # Category=Timers - -This can be specified multiple times in the same file, with the most recent specification appling -to all subsequent Voice_Cmd entries. - -If a file does not have a # Category record, the file name will be used. - - -=head2 2.14: How can I measure time between two nearly spaced events? - -If you are on Windows, or on Unix with the Time::HiRes module installed, -a call to get_tickcount will give sub-second resolution (returns milliseconds). -For example, to 'debounce' a doorbell button, (i.e. don't trigger it twice within a 250 millisecond time): - - my $doorbell_time; - if (state_now $doorbell) { - play 'doorbell' if get_tickcount - $doorbell_time > 250; - $doorbell_time = get_tickcount; - } - -If you are on unix without Time::HiRes, you can use $LoopCount and count number of passes through the loop code. - - -=head2 2.15: What is the format of MisterHouse X10 codes? - -At a low level MH sends and receives X10 data in character strings (called -Serial Items) that start with the letter X. It is usually easier to create an -X10 Item (or similar) for each X10 device you have and manipulate those -instead of using X10 strings. Most of the functionality descibed here is -available in various items in easy to use states. - -Here is format of Misterhouse X10 strings: X10 strings always begin with an -uppercase X (all letters in X10 strings must be uppercase.) The X is followed -by one or more token pairs that are either a housecode and unitcode, or a -housecode and command. - -A housecode is a letter in the range A through P. - -A unitcode is a number in the range 1 through 16. Unitcodes above 9 can be -specified as two digit numbers or their hex equivalent A through G. Okay, -there is no such thing as hex G, but X10 unitcodes start at 1, not 0. A note -to X10 interface module developers: the Serial_Item module converts unitcodes -above 9 to their hex equivalent before passing them to the interface module. - -The following four basic X10 commands are the most common and are supported by -all the interface modules. Each command is listed here followed by the -corresponding X10 command that is sent or received, plus any notes about its -use: - - J - ON - K - OFF - L - Brighten - M - Dim - - -These commands are less common and are not supported by all the interface modules: - - O - All Lights ON - P - All Units OFF - STATUS - accepted by some 2-way X10 devices to request status - PRESET_DIM1 - these are the original direct dim commands specified but never used by X10 - PRESET_DIM2 - still used by some X10 venders, including PCS, SwitchLinc and RCS - +# - increase brightness by # percent, not used for receive - -# - decrease brightness by # percent, not used for receive - Serial_Item rounds # to a multiple of 5 before it's passed to the interface module, - the interface module calculates the number of Bright/Dim commands to send - by multiplying # by 22/100, since there are 22 standard dim levels - &P# - send an extended direct dim command accepted by some X10 devices, - # is the brightness level in the range 0 through 63 - #% - same as &P command, but # is a percentage in the range 0 through 100, - Serial_Item converts this to a &P command before it's passed to the interface module, - neither &P nor % are currently used for receive - Z## - intended for sending and receiving EXTENDED_CODE commands with arbitrary extended - hex ## bytes appended, but appears only receive is implemented by the CM11 module, - no other tokens may follow this command - - -These commands are rarely used and only supported for completeness by a few interface modules: - - ALL_LIGHTS_OFF - reported as P on receive - HAIL_REQUEST - HAIL_ACK - EXTENDED_CODE - see Z above EXTENDED_DATA - STATUS_ON - STATUS_OFF - - -Here are examples of valid X10 strings and what they do: - - XM1MK - turn off M1 - XC1C2C3CJ - turn on C1, C2, and C3 simultaneously - XI6IJIKIJIK - flash I6 twice - - -For more examples, take a look at mh/lib/X10_Items.pm. - -Many of the X10 interface modules expect only a housecode/unitcode pair and a -housecode/command pair, like the first example above. This makes it -impossible to send more complicated strings like the other examples, and is -therefore discouraged. This can't be avoided with interfaces like the CM17 and -BX24 that transmit using the X10 wireless protocol, since it combines -housecode, unitcode, and command in one transmission. - - -=head2 2.16: How are states set for each pass though the user code loop? - -Misterhouse works on a multipass system where a state becomes 'new' for -one (and only) one pass thru the system. The actually timing of the -passes varies (based on the machine, the load, the code, etc) but you -generally can presume multiple passes will occur per second (I'm getting -about 19 per second with fairly light load on a 400mhz laptop). I ran -into an early problem where MH didn't handle multiple states being set -during one pass (each subsequent state would 'overwrite' the previous -one). As an example if my object read from a device and noticed that -both the volume and treble setting on my mixer changed, it would -generate a volume state and a treble state. The treble state when -overwrite the volume state. - -To fix this Bruce introduced a queue of states. So, when a module sets -a state, it gets set for the 'next' *available* pass thru the system. -In the example above (presuming no other states are outstanding) the -volume state would be 'new' during currentpass+1 and the treble state -would be 'new' during currentpass+2. - -The reason the 'current' pass is never effected is that you don't always -know where you are in the list of code looking at the time you make the -change. If your in the 'middle' and set a state, code that already ran -during that pass wouldn't have a chance to 'see' it before the pass -ended. By making states always start at the beginning of the next pass, -we can insure that all modules see a coherent state in the system. When -the states becomes valid, if effects the tied_items (that is when they -fire) as well as calls to 'state_now'. - - -=head2 2.17: Common Perl and mh coding errors - -=begin html -Clive Freedman sent in some useful tips on common coding errors in -mh/docs/faq_damnblast.html - -=end html - -=head2 2.18: What's the difference between 'on', 'ON' and ON? - -In MisterHouse, the state of many objects comes down to a question of whether -they are on or off. As this is a common situation, it is important to know -exactly how to ask whether something is on or off. - -First, it is important to know that case matters. PERL is a case sensitive -language, so 'On' does not equal 'on'. Second, by default, everytime that -the state of an item is set, mh converts that state to its lowercase -equivalent. Hence, setting a light 'ON' actually sets it 'on'. Third, the -correct operator to check for string equality is eq, not == or =. - -Now, MisterHouse adds a little twist to the situation. As 'on' and 'off' are -common states, MisterHouse defines the PERL constants ON and OFF. N.B. that -there are no dollar signs ($) at the beginning of the constants. Whenever -MisterHouse code encounters ON and OFF as bare words (not within strings), they -are replaced with 'on' and 'off' (both are lowercase). This means: - -if (($state=$myLight->state_now) eq 'ON') # always fails as 'ON' is uppercase - -if (($state=$myLight->state_now) eq 'on') # could be true if light just turned on - -if (($state=$myLight->state_now) eq ON) # could be true as ON is the same as 'on' - -=head1 3: Linux specific questions - -=head2 3.1: Problem: When I run Viavoice and festival at the same time I get "Can't open output file '/dev/dsp'" - -The problem is, festival or another sound program, is locking your dsp device. -The sound drivers that come stock in the Linux kernel do not allow more than one program to access the /dev/dsp at one time. -If you are using RedHat, you can use ESD to multiplex the soundcard usage. -The problem is, not all sound programs are esd aware. Festival and Viavoice do not directly support esd. -ESD does have a workaround that _sometimes_ works with non-esd aware programs. -Try starting your sound programs, festival and viavoice under esddsp. -For example: "esddsp festival --server &". -I had some success with this route but it doesn't always work because of sampling rates and such. - -The best fix would be to replace OSS with ALSA. http://www.alsa-project.org. -The ALSA drivers directly support multiplexing the dsp devices. -The only problem is they currently support fewer cards than the OSS drivers that come with the Linux kernel. -Check out the web page and see if your card is supported. If it is, the best avenue would be replacing OSS with ALSA. - - -=head2 3.1.1: Problem: The speech stutters and then stops half way through, and I end up with 'hung' vv_tts.pl processes that do not complete. - -Solution: You need to use ViaVoice_Outloud 5.0 with "any" ALSA driver. -ViaVoice Outloud 5.1 does not work with ALSA, but it seems that downgrading to 5.0 fixes it. -You can get the it here: -http://dittos.yi.org/automation/ViaVoice_Outloud_rtk-5.0-1.0.i386.rpm - -The older version ViaVoiceTTS that supports the older ViaVoice Outloud can be foudn at Brad's website: -http://www.reednet.org/ViaVoiceTTS/ViaVoiceTTS-0.02.tar.gz - - -=head2 3.2 How can I set the default volume level for Festival? - -This might work... it has not been tested. - -You can globally increase the volume of all waveforms generated by -festival by adding the following to your siteinit.scm. Put that in -the festival/lib directory, (where all the other .scm files are) -probably /usr/lib/festival/lib if you used the standard rpms. Add the -following - - (set! default_after_synth_hooks - (list - (lambda (utt) - (utt.wave.rescale utt 1.0 t)))) - -This will maximises the volume within a wavefrom, this wont necessary -make all voices the same loudness (though it will be close). - -Alternatively - - (set! default_after_synth_hooks - (list - (lambda (utt) - (utt.wave.rescale utt 4.0)))) - -will mutiply the waveform by 4 but this has the problem that it may overflow. - -If you want X% of the maximum without overflow use the first example -and lower the 1.0 until you get an acceptable volume - -=head2 3.3 How do I add a multi-port serial card in linux? - -From Dave Lounsberry on 1/1/2002 - -First make sure you have the device nodes for the extra serial ports. -You should have /dev/ttyS[16..23]. If not: - - # cd /dev - # ./MAKEDEV ttyS16 (repeat for each device) - -Next you need to run setserial to setup the board. The kernel defaults -to only two serial ports. Put the following in a file called -/etc/rc.serial and make sure it runs each boot. - - ---- clip here ----------------------------------------------- - #!/bin/sh - setserial /dev/ttyS16 port 0x180 irq 4 uart 16550A ^fourport - setserial /dev/ttyS17 port 0x188 irq 4 uart 16550A ^fourport - setserial /dev/ttyS18 port 0x190 irq 4 uart 16550A ^fourport - setserial /dev/ttyS19 port 0x198 irq 4 uart 16550A ^fourport - setserial /dev/ttyS20 port 0x1a0 irq 4 uart 16550A ^fourport - setserial /dev/ttyS21 port 0x1a8 irq 4 uart 16550A ^fourport - setserial /dev/ttyS22 port 0x1b0 irq 4 uart 16550A ^fourport - setserial /dev/ttyS23 port 0x1b8 irq 4 uart 16550A ^fourport - setserial /dev/ttyS16 set_multiport port1 0x1c0 mask1 0xff match1 0xff - - # chown to dbl (runs misterhouse) - for a in 16 17 18 19 20 21 22 23 - do - chown dbl.wheel /dev/ttyS${a} - done - ----- clip here ---------------------------------------------- - - -Note that I am using irq 4 for my byterunner. Be careful that you don't have one of the built in serial ports on the same IRQ. - -You can probably take out the for loop in the bottom if you are running MH as root. - -Here is a link explaining the settings and jumpers (in more depth). -http://www.mail-archive.com/linux-hardware%40senator-bedfellow.mit.edu/msg01897.html - -Another good resource is always the Linux HOWTOs. The serial HOWTO can be found at http://www.ibiblio.org/mdw/HOWTO/Serial-HOWTO.html#toc5 - - -Here is a followup from Nigel Titley: - -I have found that the linux serial card driver doesn't like the serial card to be on a shared interrupt (I assume your Byterunner is a PCI card and not ISA). I had exactly this problem and worked around it by using the BIOS to move interrupts around until the serial card set itself up on an unshared IRQ. Everything then worked. This does seem to be a Linux issue, and I haven't taken the plunge yet and tried to find out where in the serial port driver the bug exists (I guess I am hoping that someone else will do it). - - -Here is some useful serial info from Bob Hughes: - -Here is another way to look at the current serial port settings - - setserial -a /dev/ttySX where "X" is the port - -To set the serial port baud rate 9600 - - setserial /dev/ttyS0 baud_base 9600 - -You can use setserial -G to get a list of current setting and have them -in a format that can be fed back into setserial... - - setserial -G /dev/ttyS0 - -You can put setserial setting in /etc/rc.local so the port is ready for -your interface program once boot up is complete - - - -=head2 3.4: How do I get Mister House to start up automatically when my linux box boots - -Check out various .rc startup scripts in mh/bin/*.rc - -Here is a good tutorial note from Mike Bruno on 4/2002 - -OK, here's the quick and dirty on startup scripts. - -(You also may wish to check out http://www.linux-mandrake.com/en/doc/82/en/ref.html/sysv.html which is where I pulled some of the stuff you'll find below) - -Just for reference, there's two methodologies that are used for bringing up Unix systems: BSD and SysV. Mandrake uses SysV. BSD is initially simpler, but the SysV method is more flexible. The names of the methods and their merits should not really concern you at the moment, its just background in case someone jumps out of the woods and asks you. - -I should also mention that there may be some sort of fancy graphical interface into all this. But its much more satisfying to get your hands dirty and roll your own solution. - - -The boot scripts are located in /etc/rc.d. If you look in there, you'll find several subdirectories named init.d and rc0.d -> rc6.d. The rc?.d directories correspond directly to the various runlevels that Unix supports. Runlevel is a set of predefined modes that define what the system does. Mandrake has the runlevels defined as - - 0: complete machine stop; - - 1: single-user mode; to be used in the event of major - problems or system recovery; - - 2: multi-user mode, without networking; - - 3: Multi-user mode, but this time with networking; - - 4: unused; - - 5: like 3, but also launches the graphical login interface; - - 6: restart. - -If your machine comes up and immediately goes into an X-Window interface, then its default runlevel is 5. - -When you say 'reboot' or 'shutdown -r now', what you are really doing is putting the system into runlevel 6. - -When things have gone completely to shreds (typically after hardware failure or severe operator error ;), you want to get the machine into runlevel 1 - there are minimal services running and only one person can be logged in -at a time. - -Now, on to the nitty-gritty. - -The init.d subdirectory is where all the startup/shutdown scripts are kept. They are all text files, and you can -get a good idea of their format by cracking one open (I'd recommend a short one) and looking at it. You don't -need to be an expert in shell programming to get the gist of it. - -These scripts are called with one arguement (more on how that arguement gets supplied later). Virtually all scripts have the start and stop arguement, most have a restart, and then some have all sorts of custom ones. Lets look at a quick example. - -This is an init script for starting up Apache. Most are more cluttered, but not really more complicated. - - ----------------- - #!/bin/sh - # - # Start the Apache web server - # - - case "$1" in - 'start') - /usr/sbin/apachectl start ;; - 'stop') - /usr/sbin/apachectl stop ;; - 'restart') - /usr/sbin/apachectl restart ;; - *) - echo "usage $0 start|stop|restart" ;; - esac - ----------------- - -Anything after a # is a comment, except for the first line. -If that is #!, then the shell starts up the command -interpreter specified (in this case another /bin/sh) and -sends all the following commands through that. -(Don't know how far into Linux you are yet, but suffice -it to say that shell scripts are like batch files on -steroids). - -Moving down, we get to the first line of code. This is just -like a case statement in C, except for the syntax -(case block ends with a esac, which, of course, is case backwards). -$x are the positional parameters of the program -($0 is the name of the program, $1 is the first arguement, etc.) - -The rest of the script is pretty self-explanatory. -This particular one is easy as all the complicated parts -are handled by apachectl. - -So that's all well and good, but how and when does it -get called? OK, lets go back to the rc0.d -> rc6.d directories. -Looking in there, you can see a bunch of files that start -with K or S and a number. If you're using a color ls, -then you'll notice that they are a different color and there -might even be a @ after the name. That's because these are -all symlinks to the real files in the init.d directory. - -When the system enters a runlevel, it goes into the -appropriate /etc/rc.d/rc?.d directory and begins -executing the files in there. If the file begins -with a K, then the symlink gets called with a stop -(kill) arguement; if it starts with an S the symlink -is called with a start arguement. - -So if rc5.d has these links in it - - K15postgresql@ K60atd@ S15netfs@ S60lpd@ S90xfs@ - K20nfs@ K96pcmcia@ S20random@ S60nfs@ S99linuxconf@ - K20rstatd@ S05apmd@ S30syslog@ S66yppasswdd@ S99local@ - K20rusersd@ S10network@ S40crond@ S75keytable@ - K20rwhod@ S11portmap@ S50inet@ S85gpm@ - K30sendmail@ S12ypserv@ S55named@ S85httpd@ - K35smb@ S13ypbind@ S55routed@ S85sound@ - -the system is going to kill off postgress, nfs, rstatd, etc first -(in that order). Then its going to start apmd, network, portmap, etc. -(in that order). - -OK, so now you have a rough background in how the system starts. -So how do you get mh working in it? - -The easiest way is to go into init.d, copy an existing file, -and edit that. (Remember, you're root. Type carefully) - - cp httpd mh - vi mh - -and change the lines so that they look something -like this - - ----------------- - #!/bin/sh - # - # Start mh - # - - set mh_parms=/home/house/misterhouse/mh.private.ini - - case "$1" in - 'start') - /home/house/misterhouse/mh/mh & ;; - 'stop') - killall mh ;; - 'restart') - killall mh - sleep 5 - /home/house/misterhouse/mh/mh & ;; - *) - echo "usage $0 start|stop|restart" ;; - esac - ----------------- - -This is very, very sloppy, but you get the idea. - -Before you go further, TEST IT! Just call it by -hand from the command line like - - /etc/rc.d/init.d/mh start - - /etc/rc.d/init.d/mh stop - -etc. - -(you'll need to include the full path as init.d is -not in your PATH and neither is the currect directory for root) - -Once you've gotten all the bugs worked out and its working -the way you like it, figure out which runlevels you want -this to be running in. I'd guess 3 and 5, but its up -to you. - -Change into rc3.d. Figure out when you want mh to start -compared to all the other programs. I'd guess you'd want -to let everything else go first, then light up mh. -So we'll give it a number of 99, effectively going last. -Make the symlink like this - - ln -s /etc/rc.d/init.d/mh S99mh - -Do the same thing in rc5.d - -Now, you'll also want to have a good clean shutdown. -So you'll have to take care of runlevels 0 and 6. -Here, you'll want mh to get killed off early in -the process, so give it a low number, say 01. - - ln -s /etc/rc.d/init.d/mh K01mh - -You'll also want to do the same in rc1.d - - -Another option is to use a package called daemontools, available at - - http://cr.yp.to/daemontools.html - -His take on why you should use it is at - - http://cr.yp.to/daemontools/faq/create.html - -Its essentially inetd with the features it should have. - -When the system comes up, the daemontools program -svscan starts and then starts up any programs -you've asked it to. Those programs are then -monitored and get restarted if they die. The -good part here is that you can control any -program from the command line without editing -files - - svc -h /service/yourdaemon: sends HUP - svc -t /service/yourdaemon: sends TERM, and - automatically restarts the daemon after it dies - svc -d /service/yourdaemon: sends TERM, and leaves the service down - svc -u /service/yourdaemon: brings the service back up - svc -o /service/yourdaemon: runs the service once - - -------------------------- - -The following is from Harald Koch on 04/2002 - -I'll offer an alternate method. Neither is really better or worse; it -depends on your environment and requirements. - -I have a strong preference for running mission critical software -directly from init, instead of from startup scripts; init will -automatically restart software that crashes. I do this with misterhouse, -SSH, and a database that I run. - -init is controlled from a file called (usually) /etc/inittab. Here's my -MH entry from inittab: - - mh:2345:respawn:/home/mhouse/mhinit - -This tells init to run "/home/mhouse/mhinit" when the computer is in any -multi-user mode (see Mike's message for a definition of runlevels), and -to run it again when it exits. - -And here's my script: - - ----- /home/mhouse/mhinit ----- - #!/bin/sh - - mhhome=/home/mhouse - - cd ${mhhome} - - exec >> error_log 2>&1 - - export PATH=${mhhome}/mh/bin:${mhhome}/bin:$PATH - export mh_parms=${mhhome}/mh.ini - - # rotate logs - /bin/mv log.3 log.4 - /bin/mv log.2 log.3 - /bin/mv log.1 log.2 - /bin/mv log.0 log.1 - /bin/mv log log.0 - - # start - exec ${mhhome}/mh/bin/mh -log_file ${mhhome}/log - - ----- /home/mhouse/mhinit ----- - -This method *does* make it harder to stop MH, because you have to edit -/etc/inittab, change the "respawn" to "off", and then run "telinit q" to -tell init to re-read the config file. - -On the other hand, it means that MH will restart itself automatically -even if it exits due to bad code, which does happen to me occaisionally. - - -=head2 3.4: How do get Linux to play more than one sound at the same time - -Posted by Richard Phillips and Sean Walker on 03/2003 - -FYI This is a bit of a "head's up" for those people who may be -trying/struggling to get misterhouse to - for example - play music and also -at the same time be able to speak under linux. You may find, for example, -that misterhouse is silent whilst playing music and only after stopping -(say) xmms your misterhouse server starts speaking all the queued up -messages. - -There are a number of different ways to enable your linux server to -multiplex sound, and many different theories as to which way is better. Some -people prefer to use ALSA because it's not proprietary as opposed to ARTS -which is installed by default under many distributions such as Redhat. The -trouble with alsa at the moment is it can be a bit fiddly to configure as it -is not currently part of the linux kernel used in most distributions (it -will be when 2.5 is released). Anyhow, even after successfully getting alsa -installed, there can be some issues with getting multiple applications to -use the sound card concurrently - this is because not all alsa sound card -drivers support multiplexing. - -So..... - -If you want to use alsa, good for you. I won't get involved in which system -is better, and of course there are some other commercial alternatives out -there that a number of people also use and swear by. Anyhow, I'll just -describe what can be done to get things working with arts IF you are using -it AND are also having problems. - -1. Install your distribution as you would normally, and ensure that arts is -also installed/configured. As previously noted, many distros such as Redhat -use arts by default so you won't have to do anything special - everything -should be automagically detected. If you open up a terminal session and type -"ps -ef | grep artsd" you should see a line showing the details of the artsd -daemon. -2. Install flite - again depends on distro. With gentoo you can just do an -"emerge flite". If you don't have a package available for your distro, you -can get the source from http://www.speech.cs.cmu.edu/flite/ and try to build -it yourself. There are other speech engines you can use of course, but this -is probably the easiest to get running so it's what I'll use in this -demonstration. -3. From a command prompt type "flite -t hello -o play" - you should hear a -very bad hello from your computer -4. Fire up an application such as a music player that can use arts. If you -use something like xmms, make sure that it is actually using arts (with -xmms, go into options/preferences and check that the outplut plugin being -used is the "aRts driver"). Now load up a playlist, put it on repeat, and -start playing music. -5. Now try step 3 again - you'll probably find that it appears to hang. Just -kill it with a "ctrl c" -6. Now try "artsdsp flite -t hello -o play" instead - you should hear music -AND a bad "hello". - -Why is it so? Well, by default flite - and a number of other applications - -when trying to create sound directly access the sound device (usually -something like /dev/dsp). By using the wrapper "artsdsp" before running an -application such as flite, any calls to the sound device by the program are -trapped and redirected to the arts server - basically it forces the -application to "play nice" and not grab exclusive use of the sound device. - -So how do I now use flite and misterhouse? Well, you just need to change the -line in your ini file that tells misterhouse where flite is located. Go to a -command prompt and type "which artsdsp". This will let you know exactly -where the artsdsp program is located. Do the same for flite (eg "which -flite"). Now change your mh.private.ini file as follows: - -voice_text = flite -voice_text_flite = /usr/kde/3.1/bin/artsdsp /usr/bin/flite - -(Naturally, change the program locations depending on where your programs -live). - -Now, restart misterhouse and keep xmms (or whatever) running and playing -music. You should now be able to do something like go to the ia5 web -interface, select misterhouse, then misterhouse home, then browse mr house, -and then click on the "what is your up time" and be able to hear mister -house talking whilst music is also a happening thing! - -Of course, as noted before you can use the same wrapper for other programs -that may not directly support arts - for example if you install festival, -you can test it by typing something like "echo 'Hello from Festival'| -artsdsp festival --tts". - -The same also works for using esd as well as artsd. You can use JACK, -I'm sure, but I haven't tried it yet. Also, you can configure festival -to use any play program directly instead of using the artsdsp or esddsp -hacks. Those are hacks and will not work with all programs, just for -reference. To configure festival use something like this in your -siteinit.scm file (located in the festival root directory): - - (Parameter.set 'Audio_Required_Format 'snd) - (Parameter.set 'Audio_Command "esdplay -s localhost:5001 $FILE") - (Parameter.set 'Audio_Method 'Audio_Command) - -In the Audio_Command line, the only thing critical is the $FILE -You can use any player that you want in place of esdplay in the above, -but it must be compatible with the audio formats that festival can -produce. These, coming from the docs, are as follows: -The default is unheadered raw, but this may be any of the values -supported by the speech tools (including nist, esps, snd, riff, aiff, -audlab, raw and, if you really want it, ascii). - -More information can be found in the festival doc files festival_6.html -and festival_23.html wherever your festival documentation is installed, -including the handy: - - (Parameter.set 'Audio_Required_Rate 16000) - (Parameter.set 'Audio_Device "/dev/dsp2") - - -These parameters can be specified in alternative methods as well. We -should be able to tell festival which sound card to use or modify other -settings on the fly as well. I haven't looked into that yet. - -By default festival should be asynchronous and you should be able to -start playing from the file even before the file if finished writing. At -least that is what I gathered out of the doc files. Setting synchronous -or asynchronous modes are as easy as (audio_mode sync) or (audio_mode async) - - - -=head1 4: Windows specific questions - -=head2 4.1: mh seems to cause some windows to hang - -If you are experiencing problems with windows not popping up when they should (e.g. control panel or install shield), you will want to install DCOM 1.3, available from here: - - http://www.microsoft.com/com/dcom/dcom98/dcom1_3.asp - -Note, I had run all the 'Windows updates', including the Service Pack 2, but -I still had the problem until I installed the above. - -For Windows 95, the update is at: - - http://www.microsoft.com/com/dcom/dcom95/dcom1_3.asp - - -=head2 4.2: How do I set setup networking between Windows boxes - -You need to have the TCP/IP protocol enabled for your Networking Interface Card (NIC). -If you use a modem to reach the internet, you already have TCP/IP enable for the dial up adaptor, -but you need to seperately enable it for your NIC card, using the control panel Network icon. - -You can find instuctions on how to do this at: http://win98central.acauth.com/inside98/networking.htm . - -IP addresses that start with 10. (e.g. 10.0.0.1) are reserved for internal lans, so you can use -10.0.0.1, 10.0.0.2, etc. - -=head2 4.3 How do I run MH when not logged in to Win2K or XP? - -You can run any program as a service, using a program called srvany.exe, -available in the Resource Kit, or from here: - - http://www.electrasoft.com/srvany/srvany.htm - -Here is an example of registry entries after configuring my to run with srvany: - - HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MisterHouse\Parameters AppDirectory REG_SZ c:\mh\bin - HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MisterHouse\Parameters Application REG_SZ c:\perl\bin\perl.exe -S c:\mh\bin\mh - - -Rather than call mh as a service, many people have Windows do an auto-login and just add mh to the startup. - - -=head1 5: Perl questions - -=head2 5.1: Whats the best way to learn perl? - -Using MisterHouse ;-) Much more fun that trying to code up that report generator at work! -Some good books are referenced in mh.html. - - -=head2 5.2 What are good editors to use with perl? - -Codeing perl is much easier and less error prone if you use an programing -editor that automatically highlights and indents code. - -One very popular and powerful (and free) editor is Emacs: http://www.gnu.org/software/emacs/. -A pre-compiled windows version can be found here: http://www.gnu.org/software/emacs/windows/ntemacs.html - -Alan Jackson liks vim: http://www.vim.org. "Does syntax highlighting quite nicely for perl" - -Kieran Ames likes UltraEdit32: http://www.ultraedit.com. -"I swear by UltraEdit32 available at It works fully functional for 30 days and then expires. -Registration goes for around $30. I'm coding Perl all day long and would die without it! -Fairly thin and goes the distance with just about any language you'd want to use." - -Mark Yocom writes "I myself have grown fond of PrimalSCRIPT, which handles Perl, Java (and -JScript & JavaScript), HTML, ASP, LotusScript, Livewire, Python, SQL, Tcl, -REXX, VBScript, and good old fashioned .BAT/.CMD files. In addition to the -obligatory syntax coloring, it also has a number of other handy features. -It's spendy, but a trial version is available at their website: -http://www.sapien.com/PrimalSCRIPT.htm " - -=head2 5.3 When are you supposed to use '=>' as opposed to '->' ? - -You can use => for a synatically pretty way to seperate entries in a list. -When creating a hash, you can pass it a set of key/value pairs via a list, so instead of this: - - %hash = ('key1', 'value1', 'key2', 'value2'); - -You can use => instead of a , to make it look pretty: - - %hash = ('key1' => 'value1', 'key2' => 'value2'); - -Now to really confuse you, I'll try to explain how -> is used for dereferencing pointers. All mh objects can be viewed as pointers, and the state of an object is stored in a hash key of {state}, so there are 4 ways to get the current object state: - - $state = state $light; # Use the state method to get {state} - $state = $light->state; # Dereference with -> to get the state method - $state = $light->{state}; # Dereference with -> to get the state hash - $state = $$light{state}; # Dereference object with an extra - - -=head1 6: Misc questions - -=head2 6.1: Misterhouse Timeline - -Here is a rough timeline of MisterHouse development: - - - 12/96 Coded a predecessor called House_Menu - - 8/98 Re-wrote everything, called it MisterHouse - - 9/98 Uploaded to the web - - 10/98 Created compiled version. - - 11/98 First known user. - - 01/99 Started a mailing list at onelist.com - - 02/99 Ported mh to Unix/Linux - - 03/99 Article in Popular Home Automation - - 04/99 Posted to comp.home.automation - - 05/99 Registered misterhouse.net domain name. - - 06/99 Added CM17 & HomeBase support - - 06/99 Article in HomeToys: http://hometoys.com/htinews/jun99/articles/winter/winter.htm - - 08/99 Mailing list reaches 100 members - - 09/99 Added LCDproc and IRman support - - 10/99 Article in Circuit Cellar Ink: http://www.circuitcellar.com/pastissues/articles/winter111/winter.pdf - - 11/99 Linux VR using IBM's ViaVoice - - 11/99 HomeVision support - - 11/99 Mailing list reaches 200 members - - 01/00 Moved the mailing list and CVS repostitory to sourceforge.net - - 02/00 Re-worked the web interface - - 03/00 Added support for iButtons - - 04/00 Added support for X10's IR Commander transmiter - - 05/00 Added voice modem, ISDN modem, and Compool support. - - 05/00 Article in The Perl Journal #17 - - 06/00 Mailing list reaches 300 members - - 06/00 Added support for table item/event input and tie_items/tie_events. - - 08/00 Enable perl -w checks, .jpg and .gif, and Slinke support. - - 08/00 Added rpm and tarballs, NetGear router support - - 09/00 Added ViaVoiceTTS.pm, Jabber, and Applied Digital cpuxad support - - 10/00 Added barcode scanner and MS Agent support - - 11/00 Support for ical, rrd, html email access, WAP, and VXML for phone access via tellme.com - - 12/00 wx200d and wunderground personal weather project support, new add_sound function. - - 01/00 SMS and snnp messaging, improved iButton and benchmarking support. - - 01/01 Mailing list reaches 400 members - - 02/01 2 way AOL AIM messaging, earthquake and satellite tracking. - - 03/01 Backup program and menu templates for WAP, VXML, and LCD displays. - - 05/01 Audible menus, X10 Mr26 support, linux volume control. - - 07/01 Browser dependent web pages, zap2it tv support, call waiting callerid. - - 09/01 Audrey support, more MS TTS controls. - - 10/01 Web mh.ini editing, remote browse TTS support, zap2it tv pages. - - 11/01 New web interface, Audrey pictureframes, multiple sound cards. - - 12/01 Wireless wmr968 and Ultimeter2000 weather stations, Lynx10 X10 support. - - 01/02 Xmms, audrey callerid, Linksys rounter, TTS flite engine, standard CGIs. - - 03/02 GD on-the-fly web buttons, support for comics, xmms, redrat. - - 04/02 Added web calendar, contact, todo list, and tk photo slideshow - - 05/02 Improved TTS support, TWiki web site, and support for DSS units, BX24 interface, and MSN, - - 05/02 Rewrote web menu interface, added AudioTron support and improved im support. - - 06/02 Registered misterhouse.com domain name. - - 08/02 Caddx alarm, iButton humidity sensor, undo, light/dark hawkey sensor - - 09/02 Proxy mh, improved %cpu used, Group member and idle_time methods, Linux NaturalVoice, Clipsal CBUS - - 10/02 Enabled web based code selection, file based alarms, palm reader, and Mac OSX support. - - 11/02 UIRT2 control, better im control, web interface for triggers and items - - 12/02 Improved callerID, new respond and Text_Item functions, xAP support. - - 01/03 Mailing list reaches 500 members. - - 02/03 Addex xPL support, stacked and overloaded states, and multi-user logon. - - 03/03 Linux NaturalVoiceWine, web based TV and photo setup, improved Lynx X10 support. - - 04/03 Compiled mhe for linux, support for X10 W800, CallerID Identifier, USB-UIRT, and more iButton modules. - - 07/03 Improved web floorplan, DBI interface, xAP news and weather interfaces. - - 09/03 Cepstral TTS engine support, a new PA room object, improved proxy support. - - 11/03 Support for Sphinx2 VR, myHTPC tv menus, Asterisk phone interface, SMTP authentication, and a xAP command server. - - 12/03 Code for RCS TR40 theormostate, phone xAP. - - 01/04 Code for rrd weather graphs, v4lctl. - - 04/04 Code for ER1 robot, switched to the par compiler, improved mp3 Jukebox. - - 06/04 Code for Musica whole house audio, rss feeds, xAP Slimserver - - 07/04 Code for ICQ, vocp, tivo, Alpha LED displays, xAP IR for RedRat, RoboSapien. - - 08/04 Mailing list reaches 600 members. - - 09/04 Support for SVG floorplans, mp3 ripping, ebay monitoring, siteplayer, irc. - - 11/04 Support for servo motors, the ESRA robot, cell phone minutes monitoring, motherboard monitoring. - - 03/05 Support for serving multimedia files, cell phone minute monitors, shopping lists. - - 05/05 Support for distributed xAP speech, bluetooth proximity detection. - - 10/05 Support for xAP BCS, ssh, osd232, vocp, Wish x10, and EIB. - - 01/06 New spam free Wiki, support for azureus, Insteon iplcs, ical, k8055, TI103. - - 04/06 Moved from CVS to SVN, support for Xantrex power inverter, spanish web pages, french EDF power rates, DSC5401, xAP asterisk, - - -=head2 6.2: Who is that Bruce guy anyway? - -Bruce was the the original author of MisterHouse, but can no longer -claim much authorship credit as it is now truly a group effort with many contributors. -He has a day job with IBM in Rochester, Minnesota, designing integrated circuits. -Lots of stuff of interest only to family and aliens at http://brucewinter.net - -=head2 6.3: Why open source? - -Because open source only has 3 syllables, and proprietary has 5. - -That and MisterHouse has developed much faster with the help of a wider user base -and help from other coders than it would have if it were proprietary. - -Plus it feels good to give stuff back to the growing open source community. - - -=head2 6.4: Misc Home Automation links - -Here are some useful/related Home Automation links: - - Dan Hoehnens most excellent collection of HA links: - http://my.ohio.voyager.net/~dhoehnen/ha/list.html - - Mark Henrichs has a great 'Home Wiring Guide' site here: - http://www.wildtracks.cihost.com/homewire/ - - Over 500 links on about everything with HA: - http://home-automation.org/ - - Lots of good tutorials and articles are here: - http://hometoys.com/ - - X10 has many of its products documented here: - http://www.x10.com/support/support_manuals.htm - - Lots of good X10 data/ideas can be found here: - http://www.geocities.com/ido_bartana/toc.htm - http://www.x10ideas.com - http://www.x10.com/automation/homeautomation_e.htm - - Dave Houston has some good X10 info (e.g. causes of CM11A lockups) here: - http://www.laser.com/dhouston/ - - Europeans can get X10 stuff distributers listed here: - http://www.x10.com/x10euro.htm - Some examples are: - http://www.intellihome.be - http://www.marmitek.com/ - - Neil Cherry has created a home for linux related Home Automation programs at - http://linuxha.sourceforge.net - - Rene Mueller has a nice set of web pages with lots of info on HomeAppliances at - http://the-labs.com/HomeNetwork/ - - Jay Archer recommends this free Windows mail server and mult-platform web server: - http://www.argosoft.com - http://www.xitami.com - - Nokia sponsered open source site for home entertainment devices. - http://www.ostdev.net/ - - -=head2 6.5: How do I contribute additions or updates to the mh code? - -The MisterHouse code is maintained via a GitHub repository. -Instructions on how to commit changes can are here: -https://github.com/hollie/misterhouse/wiki/Contributing - -If you have minor changes and don't want to bother with a sourceforge ID, feel free to mail them to the mailing list. - - -=head2 6.6 What is X10? - -The people at http://www.x10.com sell affordable devices that communicate with each other over -normal household power lines. Manuals for many of there devices are listed -at http://www.x10.com/support/support_manuals.htm . -They have their home automation product slited here: http://www.x10.com/automation/homeautomation_e.htm/#homeautomation - -The most common modules are the Lamp module and the Appliance module. Both sell for around $10-$15. -The lamp module has can turn up to 300 watt lamps on, off, and various levels of bright. -The appliance module is an on/off relay. - -The 2 most popular X10 PC interfaces are -the $50 ActiveHome/CM11 and the $6 Firecracker/CM17. - -The CM17 is a transmit only interface. In addition to being almost free, another advantage is -it is wireless. It is a small, 1 inch box that just dangles off of your serial port and transmits -to a receiver that plugs into the wall. The other advantage to the CM17 is gives -instant ON/OFF control to the relay built into the wireless receiver. All other X10 devices -have about a 1/2 second delay to send and receive the protocol over the power lines. - -The CM11 is a 2 way interface, so you can program mh events to react to X10 motion sensor -and button pushes from various X10 controlers (e.g. keychain and palmpads). -Some of the newer, more expensive modules, like the $35 LM14A lamp module, -allow you to query their status, so you can determine it status even if it were manually turned on or off. - -If decide you splurge for the CM11 activehome kit, you might as well also get the Firecracker kit. -For the extra $6, you also get a few extra modules and mh can support both the CM11 and the CM17 -on the same port. - -There is lots of great X10 info at http://www.geocities.com/ido_bartana , -including information on using and buying X10 hardward outside of the US. - -Here is some more info, copied from a misterhouse-users post by Kevin Olalde. - - -The firecracker hardware, CM17 which plugs into the PC, can only send radio -frequency (RF) commands to devices. With the kit you will also get a -transceiver, TM751, that receives commands from the CM17 and relays -commands for the house code it's set to via your power lines. You also get -a lamp module, LM465, which can be control with via the transceiver. With -the PalmPad or the CM17 you send RF commands to the TM751, those commands -are then relayed to the lamp module (or other power line receiver modules, -like light switches). All of this is one way communications, the LM465 -lamp module and the TM751 transceiver are not capable of reporting status. -The TM751 doubles as an appliance module hard coded to unit 1. You can -only control it though RF commands, it does not respond to power pline -commands. - -The next step I took was to order the Active Home kit. With it you get -(among other stuff), another transceiver, RR501, and another computer -interface, CM11A. The RR501 works like the TM751, in that it receives RF -commands, and relays then on to the power line. A difference is that the -RR501 can report it's status. The computer interface, CM11A, can send -power line commands (you use computer software to tell the interface what -commands to send), 'sees' all commands being sent over the power line (and -reports those commands to computer software), and can ask modules for their -status. The RR501 also doubles as an appliance module, it can be set to -unit code 1 or 9, and can be controlled with both RF and power line -commands. - -The majority of the devices, lamp modules, appliance modules, wall -switches, ..... are one way devices, they can only receive. I have no -historical perspective, but it looks as though that was the way the -original X10 spec went, one way only. - -There are devices that can report their current status to something like -the CM11A, but I haven't used any of then (except for the RR501), since -that are much more expense. - - http://www.x10.com/products/x10_lm465.htm one way lamp module from X10, $13 - http://www.x10.com/products/x10_lm14a.htm two way lamp module from X10, $33 - -Bill Bass sent in these links to a bunch of links from -Phil Kingery's great series of X10 articles at http://hometoys.com - - Which One Should I Use? - http://www.hometoys.com/htinews/dec96/articles/kingery/kingery.htm - - Controlling Motors and Transformers - http://www.hometoys.com/htinews/apr97/articles/kingery/kingery2.htm - - 120/240v Residential Coupling - http://www.hometoys.com/htinews/jun97/articles/kingery/kingery3.htm - - Complex Residential Coupling with Considerations for Dim/Bright - http://www.hometoys.com/htinews/aug97/articles/kingery/kingery4.htm - - Dim/Bright Commands and Coupler-Repeaters - http://www.hometoys.com/htinews/oct97/articles/kingery/kingery5.htm - - Three-Way Switch Circuits - http://www.hometoys.com/htinews/dec97/articles/kingery/kingery6.htm - - More Three-Way and Four-Way Switch Circuits - http://www.hometoys.com/htinews/feb98/articles/kingery/kingery7.htm - - Troubleshooting Three-Way and Four-Way Switch Circuits - http://www.hometoys.com/htinews/apr98/articles/kingery/kingery8.htm - - Noise and Filtering - http://www.hometoys.com/htinews/jun98/articles/kingery/kingery9.htm - - -=head2 6.7 What do I need to use the iButtons? - -Ray Dzek created a how to get started with iButton guide at -http://www.solarbugs.com/home/ibutton.htm - -Here is a post from Clay Jackson - -Right now, I've got 3 sensors (DS1822, in the three-lead package, inside an -epoxy filled soda straw) at the end of 150' of Radio Shack "flat" cable, -talking to a DS9097U (the U is CRITICAL) with no problems. On the bench, I -had another 2 sensors on the end of a 50' cable (for a total of 5 sensors). -When I strung the 50' cable, I must have crimped a lead, because it now -meters "closed" and when it was plugged into the "net", I got no readings. - -I also have an HA5 from Point-Six; that I'm gonna play with soon. The code -Bruce did is for the 9097U, so that's why I started there (and, it's -cheaper). The 9097U has all the "passive" pull-up stuff the data sheets -talk about (courtesy of a DS2048 inside the DB9); so I'm hoping it will do -the trick. Anyway, I'll keep the list posted as things progress. - -Bottom line - all I needed was Bruce's code, a 9097U from Dallas, the -sensors and some Radio Shack "phone" cable. - -One more thing to watch - as best I can tell, Dallas has not standardized -their thermal devices with respect to their outputs. For example, the -DS1820 and DS1920 both return a 9 bit value that's basically an integer. -However, the DS1822 returns a VARIABLE length value (12 bits by default), -with a binary point between the 3rd and 4th digits. According to someone -else on the list, the ThermoCRON iButton is different yet again. - - -Here is another post from Jeff Pagel (06/2002). - - -The Dallas '1-wire' bus for the ibutton family is actually 2 wires(there -are only 2 connecting points on the devices). 1 for signal and power, the -other for ground. They talk about cat-5, which has 8 conductors, a lot -because the signal charactoristics of it are very good for the '1-wire' -bus, twisted pairs, size of the wire, etc.. Of the 8, you only need to use -2. Theoretically, the other 6 could be used for something else. - -So, back to the original problem, you would need to run a twisted pair, -like cat5 or cat3, from your mh machine to the garage. I have cat5 runs of -over 500 feet that work fine without any special conditioning. - -The cool part about the 1-wire bus is that you just need to run 1(ok -actually 2,but usually in 1 bundle) wire(s) around your house. It's like a -'multi-drop' system. One connects to the other, to the other, etc., in -parallel. - - In this case, you would run a line, from 1 spool of wire, to the bedroom -to the living room to the kids room back to the computer. If this is a -pre-existing structure, you will face some wiring issues. It's much easier -for new construction. For each ibutton, you would connect 'across' each -line. Just solder plus to plus, minus to minus and keep the wire going. -Think of a railroad track as the 'wire' and the railroad ties as the -ibutton devices. - -The 1-wire bus is fairly restrictive on how you wire it vs how long the -runs are. You should avoid 'star' type configurations. They are ok, but -only for short runs, the stars introduce reflections of the signal. - -Here will be a really bad ascii representation: - - - Host + ----------------------------------------------------- - | | | - ibutton1 ibutton2 ibutton3 - | | | - Host - ---------------------------------------------------- - - - -From Yannick Moussette (06/2002) - -The DS1820 Temp sensors come in 2 models: iButton and TO-220 -(transistor-type casing). - -The Ibutton has a total of 2 connections, ground and signal. It functions in -"parasitic" mode to draw its power from the signal connections (kinda of -like what X10 modules do to piggyback signals on powerlines...). - -Where things that may confuse you on Dallas's Website site, is the TO-220 -format of this same sensor which has 3 leads (connections). 1 for power, 1 -for ground and 1 for signal(data). The power lead may be omitted, in which -case the sensor will work exactly like its iButton counterpart: Through -parasitic powering. - -Here is a link with some detailed electronic info: http://www.maxim-ic.com/1st_pages/tb1.htm - -Kieran Ames has a nice writeup on how he used iButtons to log and plot his -pond temperatures: http://ames.homeip.net:81/pages/My_iButtonVenture.htm - - -=head1 7.0: Setup Questions - -=head2 7.1: Password Management - -To password protect Misterhouse, you can set passwords for different users via set_password. -The 'admin' link from the main page is only for logging in once you have setup a password. - -To enable password protection, run mh/bin/set_password command like this: - - set_password -user family -password xyz1 - set_password -user admin -password xyz2 - - Note: only the first 8 characters are used. The admin password is - required for controling the mh web setup menus (e.g. item and code - selection menus).(Unix or Windows) - -This will create a file pointed to with the mh.ini password_file parm (e.g. mh/data/.password). -To further extend which user can do what see password_allow in mh/data/ - - -=head2 7.2: Customizing the TV guide - -To set the MisterHouse TV Today so it will display listings relevant to your area and provider -Start by finding your tv_provider ID, run this command (located in mh/bin): - - get_tv_grid -zip your_zipcode -get_provider - -Then edit your mh.ini and set the tv_provider_name parm. -Listed below are two examples or related parms, one that uses sat_ and one that is tv_. -You can copy these tv_* parms as another set of sat_* or cable_* or -or whatever_*, then run get_tv_grid multiple times to support -multiple sets of tv schedules. For example: - - get_tv_grid -db tv (-db tv is the default) - get_tv_grid -db sat - - sat_provider = 128772 - sat_provider_name = DirectTV Washington - sat_name =SAT @ Used to give a useful label on the web pages - sat_hours=all @ Which hours to get. Use all for all hours - sat_label=SATELLITE @ Which web link name. Use none to disable, - sat_channels_keep= @ Which channels to keep - sat_channels_skip= @ Which channels to skip - sat_channel_min=0 @ Keep only channels above this number - sat_channel_max=99999 - - tv_provider_name = Charter Communications - Rochester - tv_name = TV @ Used to give a useful label on the web pages - tv_hours=02,06,10,14,18,22 @ Which hours to get. Use all for all hours - tv_label=VCR @ Which web link name. Use none to disable, - tv_channels_keep= @ Which channels to keep - tv_channels_skip= @ Which channels to skip - tv_channel_min=0 @ Keep only channels above this number - tv_channel_max=99999 @ Keep only channels below this number - -Use mh/common/tv_grid.pl to do daily call get_tv_grid to update your tv database and web pages. -and mh/code/common/tv_info.pl to search and announce shows. -If you change your tv_provider, you can use get_tv_grid -reget to refresh the tv/*.html files. - -When I first setup MH it took me three weeks to figure out that my providerID -had changed since install (matter of a week) and thats why the tv listings came up broken. -So if you have a problem, and you think the syntax in the mh.ini is correct, try re-checking your provider ID. - - -=head2 7.3: How can I point MisterHouse to a custom web interface? - -From David Norwood on 10/2001 - -Here's a way to configure Misterhouse so you can have your own custom web -interface AND still have access to web pages that are introduced with new -releases (like the Audrey pages). - -Let's assume your misterhouse distribution is in /misterhouse/mh and your -custom html files are in /misterhouse/web/custom. Set these private.ini -parameters: - - html_dir=$Pgm_Root/web - html_file=/custom/index.html - html_alias_custom=/custom /misterhouse/web/custom - -Now your custom web pages will come up by default, but you will still have -access to the latest and greatest "mh4" and "ia*" interfaces. Your pages -will not be over-written by new releases. If you need another directory -for, say, your audrey interface, just add another html_alias_whatever -parameter. - -You can now also override just specific pages from an existing web interface. -For example, create your own web/ia5 and web/ia5/security -directories, then add this mh.ini parm to point to it: - - html_alias2_ia5 = /misterhouse/web/ia5 - -For example, you can change the ia5 Security menu to point to different webcams or floorplan -images by creating your own web/ia5/security directory with just the files you want to -modify. - - - -=cut - - - +=pod + +=head1 1: MisterHouse operational questions + +=head2 1.1: What OSes can it run on? + +It has been tested on Windows 95, 98, NT 4.0, 2K, XP, Vista (!) and 7. +On Unix, it has been run on Linux (Ubuntu, Debian, Fedora and others), AIX, and Sun. + +In theory, any OS that runs perl should be able to run mh. In practice, some platforms will likely have strange Serial Interfaces, so the Serial I/O related function would take some work. + +=head2 1.2: How can I make it run faster and use less memory? + +On my Celron 400 Mhz computer running Windows 98, using 60 code files with 6000 lines of code, mh takes 10% the cpu, running at 9 passes per second and using about 30 MB of memory. + +If you are happy with the Web interface and don't need the Tk interface, turn it off by editing the Tk mh.ini parm or run: + + mh -tk 0 + +If you want the tk window, but don't use the Command/Items/Group pull down menus, try: + + mh -tk_comands 0 -tk_items 0 -tk_groups 0 + +You can turn off the tk 'eye' that bounces back and forth by deleting the tk_eye.pl code file. + +You can speed up / slow down mh, using more/less cpu, by mh.ini sleep_time parm. + +Here are some memory stats: + + Linux + 10 Meg <- mh -tk 0 mh_control.pl (run just one code file) + 12 Meg <- mh -tk 0 (run all the default code) + 17 Meg <- mh (run all the default code with tk gui on) + + Windows + 18 Meg <- mh -tk 0 mh_control.pl (run just one code file) + 20 Meg <- mh -tk 0 (run all the default code) + 27 Meg <- mh (run all the default code with tk gui on) + + + +=head2 1.3: How can I easily upgrade to the latest version of mh? + +Point your mh.ini code_dir, data_dir, and html_dir parms to somplace other than your mh path. +Then you should be able to rename your old mh directory and unzip the new one in its place without loosing +anything. Also keep your mh.private.ini somewhere else (see next question). + +=head2 1.4: How does mh.ini work? + +All the entries in mh.ini are read on mh Startup and on a Reload. They are stored +in the %config_parms array. So, for example, if you added: + + myparm1=a b c + +Then, mh would set $config_parms{myparm1} = 'a b c'; + +These parms also defined what are legal startup values, so if you want to try +a different value, instead of editing your mh.ini, you can run: + + mh -myparm1 'd e f' + +Rather then edit the default mh.ini file, the best approach is to copy just the parms you want to change to your own .ini file (e.g. mh.private.ini), then set the environmental variable mh_parms to point where you keep that file (see the header of mh/bin/mh.ini for more detail). + +While the %config_parms array IS refreshed on a mh Reload, some parms are only used on mh startup (e.g. http_port), so will only be reloaded when you re-start mh. + +=head2 1.5: Can I run run mh in a 'fast debug mode'? + +Funny you should ask :) Check out the time_start, time_stop, and time_increment parms +in the mh.ini file. Here are some examples: + + mh -time_start 0 -time_stop 24 -tk 0 -voice_text 0 + mh -time_start "6 AM" -time_stop "11 PM" -time_increment 1 + mh -time_start "5/14 7:10" -time_stop "5/15 10 PM" -time_increment 300 + + +=head2 1.6: How are the X10 and Serial Items implemented, indpendent of the platform? + +The user works with an X10_Item which reflects physical hardware and the state of that hardware. The user "loop" code deals with the state and +conditions which alter it. This code is independent of OS and specific to mh. + +X10_Items talk to Serial_Items which implement a software protocol required by the model and type of the actual hardware. The Serial_Items +can use protocol translator modules such as CM11.pm, HomeBase.pm, and CM17.pm. But the output is platform-independent serial commands +to the next layer. The protocol translator modules are publicly available outside mh and may be used by code other than Serial_Items. These modules will eventually be on CPAN. The code is still independent of OS. Other Serial_Item types, including "generic" read/write ports, also exist. + +Win32::SerialPort.pm and Device::SerialPort.pm are CPAN modules which actually implement the serial command interface. Device::SerialPort is a +clone of its Win32 cousin. These modules handle high-level OS issues including device names, configuration files, and validation of port +settings. + +Each also calls a CPAN module to handle low-level interfacing to the OS and the serial driver: Win32API::CommPort.pm or POSIX.pm (you guessed +correctly ;-) + + +=head2 1.7: mh seems way too complicated. How can I run something simple? + +Copy mh\code\test\test_x10.pl into your own code dir (e.g. c:\mh_code). Then run: + + mh -code_dir c:\mh_code + +You can then use http://localhost:8080 for control. This will also run all the +code in mh/code/common. To run JUST a specific code member(s), specify +it on the command line. For example: + + mh test_x10.pl + + +=head2 1.8: Is there a way I can have direct X10 control with a simple perl script? + +If all you want to do is control X10 devices with a cron job or from your own cgi scripts, +running mh is probably overkill. You can send X10 commands using either the ActiveHome +interface (CM11, available from http://www.x10.com/automation/x10_ck11a_d.htm ) +or the cheaper Firecracker interface (CM17, available from http://www.x10.com/automation/firecracker_d.htm ) +using the modules and documentation found in mh/lib/site/ControlX10 or download them from CPAN. +Examples can be found in mh/lib/site/test_cm11.pl and test_cm17.pl. + + + +=head2 1.9: Can I send mh commands from other programs? + +You can send mh commands (Voice_Cmd text strings) from other programs in many ways: + +- Write commands to the file specified in the mh.ini xcmd_file parm. By default this +is mh/house_cmd.txt. See mh/bin/house and mh/bin/house.bat for examples. + +- Use mh/bin/mhsend to send commands via sockets (can be over the internet). If you have +run mh/bin/set_password, the mhsend -password parm is required. Some examples: + + mh/bin/mhsend -host ip.A -run "All Lights On" + mhsend -p password -run "turn living room light on" + +Where ip.A is computer A's ip address (or name if you have a HOST file setup +to alias a name to an ip address). mhsend is a stand alone perl script, +so you do not need mh running there (although it will not hurt). +The code that allows this is in mh/code/common/mhsend_server.pl. +An example of mh code to act as a client to different mh box is in mh/code/examples/test_server2.pl + +- You can also control mh remotely via email (see faq question 'Can do I send mh comands via email?'), +telnet sessions (if running mh/code/common/telent.pl), and via the built in web server. An example url +for the web server is: + + http://192.168.1.10:8080/RUN;last_response?turn_living_room_light_on + +- If you are runing IM client (if running code file internet_im.pl) +like AOL AIM, MSN, or Jabber, you can any voice command to +MisterHouse if you give Misterhouse its own account to logon with. + + + +=head2 1.11: How can I debug a problem? + +Lots of debug errata can be controled with the mh -debug switch. Most of this code is probably +meaningful only if you dig into the source code, so most people probably will not use +it unless you are generating debug to send to the author. To create a log file with debug in it, +run: + + mh -log debug.log -debug xyz + +Where xyz can be any of the following: + + x10 + serial + socket + startup + homebase + misc + http + port_name of socket port + +You can also stack debug flags, using ; as a separator. fro example: + + mh -debug "serial;x10" + + +=head2 1.12: How can I debug timed event problem? + +One way is to create a code member that contains just the event you are trying to debug, then run mh on just that one member. For example, create a member called \temp\test_time.pl with this code: + + print "min=$Minute\n" if $New_Minute; + print "hour=$Hour\n" if $New_Hour; + print "debug1 it is now $Time_Now\n" if time_now "11:59 PM"; + print "debug2 it is now $Time_Now\n" if time_now "23:59"; + print "debug3 it is now $Time_Now\n" if time_now "12:00 AM"; + print "debug4 it is now $Time_Now\n" if time_now "00:00"; + +Then run mh using these parameters: + + mh -code_dir \temp -tk 0 -time_start '12/15 11:58 pm' -time_start '12/16 12:01 am' + +Note, you can run this test mh quickly without stopping your normal mh. + +=head2 1.13: What are the advantages/disadvantages to running the compiled versions of mh? + +Advantages: + +=over + +=item * + +You don't need the latest version of perl installed + +=item * + +You don't have to install additional perl modules + +=back + +Disadvantages: + +=over + +=item * + +For some reason, starting mh with the compiled versions is slower. On my windows box, it is 10 seconds -vs- 20 seconds. + +=item * + +You can apply quick patches to mh, -vs- downloading a larger, less frequently updated mh.exe file. + +=back + +The memory and cpu used, once mh is started, is the same. + + +=head2 1.14: How does mh read and use the user code in code_dir + +Each time mh is started or a Reload is done, mh will re-read all the .pl code members +in the code_dir directory (this is a mh.ini parm) +and the file data_dir/mh_temp.user_code is created. +All of the objects and global variables (see FAQ question 2.4), are put at top +of this file, and everything else is put in seperate subroutines, one for each file read. +A &loop_code subroutine is also created (at the very bottom of mh_temp.user_code), that +calls each member subroutine. + +After creating this file, mh runs the perl eval function on its contents. This re-creates all the objects, +global variables, and code_dir member subroutines. Then mh goes back into its normal loop where +it queries various input data (e.g. serial, tcp/ip, voice, time), updates objects and variables, +evals the &loop_code code (it does an eval, rather then execute it directly, so we can trap errors and +not kill mh), sleeps for a bit, then repeats. + + +=head2 1.15 How can I control mh from a non-mh web server like Apache? + +You can think of the built in mh web server as 2 parts: + + - The part that auto-generates web pages that reflect your items and commands. + + - The part that responds to commands you give it via URLs. + +You don't really have to use the first part. +Some mh users custom generate their own mh pages, to be served by the mh server or +by a different server. This allows for full control of +what is on the web control page and how it looks. + +There is more (probably not so easy to read) documentation on how to do this +under 'Customizing the Web Interface' in mh/docs/mh.html + +You can also use something like mh/code/common/mhsend_server.pl +and then read/write to sockets directly from non-mh web server cgi scripts. +Or have the web server script simply call mh/bin/mhsend to run mh commands. + +=head2 1.16 Is there a front-end tool for defining/editing mh items/events? + +Not yet, but you can now define items/events in a table-like format. +This should allow us to enable a gui or web based front end tools for editing. + +Currently we have defined just one format defined, creativly named A, +but other formats (e.g. XML) should be easy to implement. + +To use, create one or more *.mht (mh table) files in your code directory, specifying the table format with Format=A +at the top of the table. Then mh will process (on starup and reload) each record with mh/lib/read_table_A.pl, to create +*.mhp (mh processed) files, which are then processed with all your other mh *.pl code files. + +Examples are in mh/code/bruce/*.mht and mh/code/test/*.mht. +If you want to create another table format, all you need to do is create a mh/lib/read_table_xyz.pl member, then set Format=xyz. + +=head2 1.17: How can I use Family Radios with MisterHouse? + +=begin html +There is seperate FAQ on the FRS topic in +mh/docs/faq_frs.html + +=end html + + +=head2 1.18: Is there a way to tell if MH is shutdown correctly or not? + +On windows, mh/bin/mh.bat checks errorlevel and on linux, mh/bin/mhl finds the return code via $?. +Both of these shells will restart mh unless they detect a normal exit (rc=1). + +This code in mh/code/common/mh_control.pl will detect an abnormal restart: + + if ($Startup and $Save{mh_exit} ne 'normal') { + display "MisterHouse auto restarted: $Save{mh_exit}", 0; + } + +If you want to detect a power outage and you have a CM11, +code in mh/code/common/mh_control.pl will set $Power_Supply, so you can do something like this: + + if (state_now $Power_Supply eq 'Restored') { + speak 'You should have bought a ups'; + } + + +=head2 1.19: How can I use an Internet Appliance with MisterHouse? + +=begin html +There is seperate FAQ on varous Internet Appliances in +mh/docs/faq_ia.html + +=end html + + +=head2 1.20: Where is perl program code located? + +From Harald Koch on 2/2002. + +There are (at least) three different locations for scripts: + +/mh/bin - standalone programs (such as get_weather, get_tv_grid); they are called from the main MH process via "run" or a Process_Item; + they do not get any environment or variables from the main MH program, and typically return results to MH by writing output files + which MH later reads. + + MH typically looks for external programs here, and also programs + here typically assume they're being run from this directory; they + try to find files in ../lib, for example. + +$config_parms{code_dir} - scripts in this directory are (after some parsing) loaded into the middle of the MH event loop, and the + commands in them run constantly. They run within the MH environment, + and can use all the variables available there. + +$config_parms{html_dir} - scripts in this directory are run by the HTTP + server, and are expected to return blobs of HTML. They too run + inside the MH process, and can access all variables, subroutines, + etc. + +Each script directory has a different purpose, and programs in one are +generally not terribly useful in another. My weather processing, for +example, has scripts in all three places; mh/bin/get_weather_ca is the +standalone that fetches/parses HTML, code/weather_ec.pl is the script +that creates and processes voice commands and timer events, and then I +have scripts in /web/weather/ that format the HTML for display. + + + + +=head1 2: MisterHouse coding questions + +=head2 2.1: How do I debug syntax errors in my code? + +If there are syntax errors in your code, most of them will be caught on startup or reload. +The error, and (hopefully) relevant code member and/or line number, will show up in +your console, and if running Tk, in a Tk window on reload. + +When you go to write new code, first start mh with old code that works, then edit or copy in +your new code and do a mh Reload. Fixing errors with code Reloads is much +quicker than trying to fix them on mh startup. You can add/delete the .pl suffix +on code members between reloads to enable/disable them. + +Some errors (e.g. too few/many {} around code) will point to a misleading line number. +In these cases, if you can not find the file with the error, +you can try a new option: mh -error_by_file 1 . +This will try loading one file at a time untill the error is found, but +it runs pretty slow and may not work with members that have # noloop directives. + +Some errors will only show up after mh triggers events. The first time this happens, a console/tk message +will be displayed, and a message spoken. Since these errors might happen on sequential passes of mh, subsequent errors +are only listed in the console window. If more than 10 occur, another window is displayed, +and the code member there error was in is disabled. + +All of these on-the-fly errors are logged in the file mh_temp.error_log in your code directory. +This is useful if you are not running tk and the error has scrolled out of your console window. + +You can enable the perl diagnostics module (same as perl -w) to get additional warnings on potential +coding errors. Either use mh -diagnostics 1 or set diagnostics=1 in your mh.ini. This takes +about 1 meg more of memory and causes mh to run about 10% slower, but the messages it displays +often point to valid problems. +To simplify coding, the 'uninitialized value' warnings disabled in the user code processing. + +=head2 2.2: How should I code events that should occur only at infrequently, or +only at Startup? + +You can use the $Startup or $Reload variables for events that should only +occur when mh is Started, or when code is Reloaded. Note that $Reload +is also true when $Startup is true. + +To run events infrequently, you can use the modulus operator (%), +$New_Second/Hour/Day/Month/Year variables, +or the Time_Now/Time_Cron functions to control events. + +The $New_xxx variables are 1 (true) for each pass that the xxx variable gets incremented, +and 0 for all other passes. For example, $New_Hour is true for one pass +when $Hour changes from one hour to another. + +Here are some examples: + + print_log "Only at startup" if $Startup; + + print_log "Every 5 minutes" if $New_Minute and !($Minute % 5); + + print_log "At 10:05 pm" if time_now '10:05 PM'; + + print_log "Every weekday at 2:15 am" time_cron '2 15 * * 1-5'; + + +=head2 2.3: How should I structure my Perl code? + +Pretty much anyway you want! You may want to put object definitions all in one +member (e.g. mh/code/Bruce/items.pl) so you can easily change device codes, +or you may want to define the objects in the file members you use them in. + +If the same object is defined in multiple members, it will still work ok, although +some object types (e.g. Voice_Item) will warn about duplicate names. + + +=head2 2.4: Can I create global variables that can be shared between code members? + +Yep. All objects definitions (e.g. $light = new X10_Item('C5')) are always shared. They are pulled out of the member loop by mh. + +All other variable declarations must be made using with 'my' or 'use vars': + + my ($my_var1, $my_var2); + use vars '$my_var1', '$my_var2'; + use vars qw($my_var1 $my_var2); + +The last example uses the qw function (Quote Words) to save you from the ' and , punctuation. + +If you code a 'my' or 'use var' record that starts in column 1, +mh will pull these records out of the member loop code (as it did with object definitions), +so that the variable can now be shared with other code members. + +If the 'my' or 'use var' record does not start in column 1, the variable declaration +is left in with the rest of the member code and is local to that member. + +If you want to use the variable with any mh/web/*.pl script when creating +mh web pages, you must use 'use vars', as 'my' variables can not be shared with the server. + + +=head2 2.5: How can I move code out of the loop code? + +Sometimes you may need to have mh run code out of the loop code. For example, if you +want to create a Voice_Cmd object that has states that are read from a file. Since +objects are moved out of the loop code by mh, we need to also move any other code used +to define that object. + +You can tell mh to do this using '# noloop=start/stop' comments. For example: + + # noloop=start + my $mp3names; + while ( my $mp3name = ) { + $mp3name =~ s#^.*/##; # remove path + $mp3name =~ s#\..*$##; # remove extension + $mp3names .= "," if $mp3names; + $mp3names .= $mp3name; + } + # noloop=stop + + $v_play_music = new Voice_Cmd("Play [$mp3names]"); + if ($state = said $v_play_music) { + speak "Playing song $state"; + run("winamp d:/songs/$state.mp3") + } + + +=head2 2.6: How can I add my own subroutines? + +You can add your own subroutines anywhere in any of your code files. These can then be +called by that member or any other member. Here is a couple of examples: + + print_log &mysub1(9990, 9); + + sub mysub1 { + my ($a, $b) = @_; + my $c = $a + $b; + return "The answer is: $a + $b = $c"; + } + + + $v_bedroom_curtain = new Voice_Cmd('[open,close] the bedroom curtains'); + &curtain_on('bedroom', $state) if $state = said $v_bedroom_curtain; + + sub curtain_on { + my($room, $action) = @_; + set $curtain_updown $action; + eval "set \$curtain_$room ON;"; + } + +=head2 2.7: What other special global variables are there? + +Here are a few: + + $Pgm_Path is the directory that that mh/bin/mh is found in. + $Pgm_Root is one directory above $Pgm_Path + $OS_win is true on windows, false otherwise. $^O will reflect the OS name. + +=head2 2.8: Can I use mh to control other windows + +On Windows (95,98, NT), you can use the SendKeys to send keystrokes to other windows. See mh.html for details. + +If anyone knows of an equivalent function for linux, let me know. Linux is usually clever enough to have +command line options, so there is less of a need here. + + + +=head2 2.9: What data is saved when mh is exited and restarted? + +The state of all objects (Timer Serial_Item X10_Item X10_Appliance File_Item Generic_Item) and all the $Save variables. + +Every 5 minutes while mh is running, and whenever mh is exited, the state of these variables are saved to your code directory in a member called mh_temp.saved_states. When you start mh, this member is run to restore the states. + +You can make up any members to the $Save array. For example, you can use $Save{sleeping_cat} to track whenever your cat is asleep. + + +=head2 2.10: How can I turn a bunch of lights on/off all at once? + +You can turn all the lights on a single house code on/off by specifying just the house code letter. For example: + + $v_bedroom_lights = new Voice_Cmd('Bedroom lights [on,off]'); + $bedroom_lights = new X10_Item('P'); + set $bedroom_lights $state if $state = said $v_bedroom_lights; + +You can also create a group of X10 items. Here is an example that would turn all lights on/off in several rooms. + + $all_lights_bed = new X10_Item('P'); + $all_lights_living = new X10_Item('O'); + $all_lights = new Group($all_lights_bed, $all_lights_living); + + $v_lights_all = new Voice_Cmd('All lights [on,off]'); + set $all_lights $state if $state = said $v_lights_all; + + +=head2 2.11: How do I get mh to stop telling the time each minute? + +Out of the box, mh defaults to running most of the code in the mh/code/common directory and all the code in mh/code/test. +Members test_tagline.pl and the hello_speak.pl members are the main sources of the periodic test speech. +Once you have played around with the example code, you will want to use +http://localhost:8080/bin/code_select.pl menu (avaliable on the web ia5 MrHouse menu 'Select Code' button) +and select only the code files you find useful. + +See the FAQ question 'How does mh.ini work' on how to modify the code_dir parm to point to your new code directory. + + +=head2 2.12: Can do I send mh comands via email? + +Set the mh.ini net_mail_command_code parm to a secret code, and copy mh/code/common/internet_mail.pl +into your code directory. Now you can send email with the following string either +in the subject or the body of your email: + + command:your command code:your_code + +The internet_mail.pl code periodicaly runs the get_email process and then scans for the above string. +If found, it will send a confirmation email saying either "command run", "command not found", +or "command not authorized". + + +=head2 2.13: How can I control Command Categories + +All Voice_Cmd objects are listed in the Tk Command pull down and Web Category list. +These lists are organize by Category, which is specified with a Category=value comment in the user code. +For example: + + # Category=Timers + +This can be specified multiple times in the same file, with the most recent specification appling +to all subsequent Voice_Cmd entries. + +If a file does not have a # Category record, the file name will be used. + + +=head2 2.14: How can I measure time between two nearly spaced events? + +If you are on Windows, or on Unix with the Time::HiRes module installed, +a call to get_tickcount will give sub-second resolution (returns milliseconds). +For example, to 'debounce' a doorbell button, (i.e. don't trigger it twice within a 250 millisecond time): + + my $doorbell_time; + if (state_now $doorbell) { + play 'doorbell' if get_tickcount - $doorbell_time > 250; + $doorbell_time = get_tickcount; + } + +If you are on unix without Time::HiRes, you can use $LoopCount and count number of passes through the loop code. + + +=head2 2.15: What is the format of MisterHouse X10 codes? + +At a low level MH sends and receives X10 data in character strings (called +Serial Items) that start with the letter X. It is usually easier to create an +X10 Item (or similar) for each X10 device you have and manipulate those +instead of using X10 strings. Most of the functionality descibed here is +available in various items in easy to use states. + +Here is format of Misterhouse X10 strings: X10 strings always begin with an +uppercase X (all letters in X10 strings must be uppercase.) The X is followed +by one or more token pairs that are either a housecode and unitcode, or a +housecode and command. + +A housecode is a letter in the range A through P. + +A unitcode is a number in the range 1 through 16. Unitcodes above 9 can be +specified as two digit numbers or their hex equivalent A through G. Okay, +there is no such thing as hex G, but X10 unitcodes start at 1, not 0. A note +to X10 interface module developers: the Serial_Item module converts unitcodes +above 9 to their hex equivalent before passing them to the interface module. + +The following four basic X10 commands are the most common and are supported by +all the interface modules. Each command is listed here followed by the +corresponding X10 command that is sent or received, plus any notes about its +use: + + J - ON + K - OFF + L - Brighten + M - Dim + + +These commands are less common and are not supported by all the interface modules: + + O - All Lights ON + P - All Units OFF + STATUS - accepted by some 2-way X10 devices to request status + PRESET_DIM1 - these are the original direct dim commands specified but never used by X10 + PRESET_DIM2 - still used by some X10 venders, including PCS, SwitchLinc and RCS + +# - increase brightness by # percent, not used for receive + -# - decrease brightness by # percent, not used for receive + Serial_Item rounds # to a multiple of 5 before it's passed to the interface module, + the interface module calculates the number of Bright/Dim commands to send + by multiplying # by 22/100, since there are 22 standard dim levels + &P# - send an extended direct dim command accepted by some X10 devices, + # is the brightness level in the range 0 through 63 + #% - same as &P command, but # is a percentage in the range 0 through 100, + Serial_Item converts this to a &P command before it's passed to the interface module, + neither &P nor % are currently used for receive + Z## - intended for sending and receiving EXTENDED_CODE commands with arbitrary extended + hex ## bytes appended, but appears only receive is implemented by the CM11 module, + no other tokens may follow this command + + +These commands are rarely used and only supported for completeness by a few interface modules: + + ALL_LIGHTS_OFF - reported as P on receive + HAIL_REQUEST + HAIL_ACK + EXTENDED_CODE - see Z above EXTENDED_DATA + STATUS_ON + STATUS_OFF + + +Here are examples of valid X10 strings and what they do: + + XM1MK - turn off M1 + XC1C2C3CJ - turn on C1, C2, and C3 simultaneously + XI6IJIKIJIK - flash I6 twice + + +For more examples, take a look at mh/lib/X10_Items.pm. + +Many of the X10 interface modules expect only a housecode/unitcode pair and a +housecode/command pair, like the first example above. This makes it +impossible to send more complicated strings like the other examples, and is +therefore discouraged. This can't be avoided with interfaces like the CM17 and +BX24 that transmit using the X10 wireless protocol, since it combines +housecode, unitcode, and command in one transmission. + + +=head2 2.16: How are states set for each pass though the user code loop? + +Misterhouse works on a multipass system where a state becomes 'new' for +one (and only) one pass thru the system. The actually timing of the +passes varies (based on the machine, the load, the code, etc) but you +generally can presume multiple passes will occur per second (I'm getting +about 19 per second with fairly light load on a 400mhz laptop). I ran +into an early problem where MH didn't handle multiple states being set +during one pass (each subsequent state would 'overwrite' the previous +one). As an example if my object read from a device and noticed that +both the volume and treble setting on my mixer changed, it would +generate a volume state and a treble state. The treble state when +overwrite the volume state. + +To fix this Bruce introduced a queue of states. So, when a module sets +a state, it gets set for the 'next' *available* pass thru the system. +In the example above (presuming no other states are outstanding) the +volume state would be 'new' during currentpass+1 and the treble state +would be 'new' during currentpass+2. + +The reason the 'current' pass is never effected is that you don't always +know where you are in the list of code looking at the time you make the +change. If your in the 'middle' and set a state, code that already ran +during that pass wouldn't have a chance to 'see' it before the pass +ended. By making states always start at the beginning of the next pass, +we can insure that all modules see a coherent state in the system. When +the states becomes valid, if effects the tied_items (that is when they +fire) as well as calls to 'state_now'. + + +=head2 2.17: Common Perl and mh coding errors + +=begin html +Clive Freedman sent in some useful tips on common coding errors in +mh/docs/faq_damnblast.html + +=end html + +=head2 2.18: What's the difference between 'on', 'ON' and ON? + +In MisterHouse, the state of many objects comes down to a question of whether +they are on or off. As this is a common situation, it is important to know +exactly how to ask whether something is on or off. + +First, it is important to know that case matters. PERL is a case sensitive +language, so 'On' does not equal 'on'. Second, by default, everytime that +the state of an item is set, mh converts that state to its lowercase +equivalent. Hence, setting a light 'ON' actually sets it 'on'. Third, the +correct operator to check for string equality is eq, not == or =. + +Now, MisterHouse adds a little twist to the situation. As 'on' and 'off' are +common states, MisterHouse defines the PERL constants ON and OFF. N.B. that +there are no dollar signs ($) at the beginning of the constants. Whenever +MisterHouse code encounters ON and OFF as bare words (not within strings), they +are replaced with 'on' and 'off' (both are lowercase). This means: + +if (($state=$myLight->state_now) eq 'ON') # always fails as 'ON' is uppercase + +if (($state=$myLight->state_now) eq 'on') # could be true if light just turned on + +if (($state=$myLight->state_now) eq ON) # could be true as ON is the same as 'on' + +=head1 3: Linux specific questions + +=head2 3.1: Problem: When I run Viavoice and festival at the same time I get "Can't open output file '/dev/dsp'" + +The problem is, festival or another sound program, is locking your dsp device. +The sound drivers that come stock in the Linux kernel do not allow more than one program to access the /dev/dsp at one time. +If you are using RedHat, you can use ESD to multiplex the soundcard usage. +The problem is, not all sound programs are esd aware. Festival and Viavoice do not directly support esd. +ESD does have a workaround that _sometimes_ works with non-esd aware programs. +Try starting your sound programs, festival and viavoice under esddsp. +For example: "esddsp festival --server &". +I had some success with this route but it doesn't always work because of sampling rates and such. + +The best fix would be to replace OSS with ALSA. http://www.alsa-project.org. +The ALSA drivers directly support multiplexing the dsp devices. +The only problem is they currently support fewer cards than the OSS drivers that come with the Linux kernel. +Check out the web page and see if your card is supported. If it is, the best avenue would be replacing OSS with ALSA. + + +=head2 3.1.1: Problem: The speech stutters and then stops half way through, and I end up with 'hung' vv_tts.pl processes that do not complete. + +Solution: You need to use ViaVoice_Outloud 5.0 with "any" ALSA driver. +ViaVoice Outloud 5.1 does not work with ALSA, but it seems that downgrading to 5.0 fixes it. +You can get the it here: +http://dittos.yi.org/automation/ViaVoice_Outloud_rtk-5.0-1.0.i386.rpm + +The older version ViaVoiceTTS that supports the older ViaVoice Outloud can be foudn at Brad's website: +http://www.reednet.org/ViaVoiceTTS/ViaVoiceTTS-0.02.tar.gz + + +=head2 3.2 How can I set the default volume level for Festival? + +This might work... it has not been tested. + +You can globally increase the volume of all waveforms generated by +festival by adding the following to your siteinit.scm. Put that in +the festival/lib directory, (where all the other .scm files are) +probably /usr/lib/festival/lib if you used the standard rpms. Add the +following + + (set! default_after_synth_hooks + (list + (lambda (utt) + (utt.wave.rescale utt 1.0 t)))) + +This will maximises the volume within a wavefrom, this wont necessary +make all voices the same loudness (though it will be close). + +Alternatively + + (set! default_after_synth_hooks + (list + (lambda (utt) + (utt.wave.rescale utt 4.0)))) + +will mutiply the waveform by 4 but this has the problem that it may overflow. + +If you want X% of the maximum without overflow use the first example +and lower the 1.0 until you get an acceptable volume + +=head2 3.3 How do I add a multi-port serial card in linux? + +From Dave Lounsberry on 1/1/2002 + +First make sure you have the device nodes for the extra serial ports. +You should have /dev/ttyS[16..23]. If not: + + # cd /dev + # ./MAKEDEV ttyS16 (repeat for each device) + +Next you need to run setserial to setup the board. The kernel defaults +to only two serial ports. Put the following in a file called +/etc/rc.serial and make sure it runs each boot. + + ---- clip here ----------------------------------------------- + #!/bin/sh + setserial /dev/ttyS16 port 0x180 irq 4 uart 16550A ^fourport + setserial /dev/ttyS17 port 0x188 irq 4 uart 16550A ^fourport + setserial /dev/ttyS18 port 0x190 irq 4 uart 16550A ^fourport + setserial /dev/ttyS19 port 0x198 irq 4 uart 16550A ^fourport + setserial /dev/ttyS20 port 0x1a0 irq 4 uart 16550A ^fourport + setserial /dev/ttyS21 port 0x1a8 irq 4 uart 16550A ^fourport + setserial /dev/ttyS22 port 0x1b0 irq 4 uart 16550A ^fourport + setserial /dev/ttyS23 port 0x1b8 irq 4 uart 16550A ^fourport + setserial /dev/ttyS16 set_multiport port1 0x1c0 mask1 0xff match1 0xff + + # chown to dbl (runs misterhouse) + for a in 16 17 18 19 20 21 22 23 + do + chown dbl.wheel /dev/ttyS${a} + done + ----- clip here ---------------------------------------------- + + +Note that I am using irq 4 for my byterunner. Be careful that you don't have one of the built in serial ports on the same IRQ. + +You can probably take out the for loop in the bottom if you are running MH as root. + +Here is a link explaining the settings and jumpers (in more depth). +http://www.mail-archive.com/linux-hardware%40senator-bedfellow.mit.edu/msg01897.html + +Another good resource is always the Linux HOWTOs. The serial HOWTO can be found at http://www.ibiblio.org/mdw/HOWTO/Serial-HOWTO.html#toc5 + + +Here is a followup from Nigel Titley: + +I have found that the linux serial card driver doesn't like the serial card to be on a shared interrupt (I assume your Byterunner is a PCI card and not ISA). I had exactly this problem and worked around it by using the BIOS to move interrupts around until the serial card set itself up on an unshared IRQ. Everything then worked. This does seem to be a Linux issue, and I haven't taken the plunge yet and tried to find out where in the serial port driver the bug exists (I guess I am hoping that someone else will do it). + + +Here is some useful serial info from Bob Hughes: + +Here is another way to look at the current serial port settings + + setserial -a /dev/ttySX where "X" is the port + +To set the serial port baud rate 9600 + + setserial /dev/ttyS0 baud_base 9600 + +You can use setserial -G to get a list of current setting and have them +in a format that can be fed back into setserial... + + setserial -G /dev/ttyS0 + +You can put setserial setting in /etc/rc.local so the port is ready for +your interface program once boot up is complete + + + +=head2 3.4: How do I get Mister House to start up automatically when my linux box boots + +Check out various .rc startup scripts in mh/bin/*.rc + +Here is a good tutorial note from Mike Bruno on 4/2002 + +OK, here's the quick and dirty on startup scripts. + +(You also may wish to check out http://www.linux-mandrake.com/en/doc/82/en/ref.html/sysv.html which is where I pulled some of the stuff you'll find below) + +Just for reference, there's two methodologies that are used for bringing up Unix systems: BSD and SysV. Mandrake uses SysV. BSD is initially simpler, but the SysV method is more flexible. The names of the methods and their merits should not really concern you at the moment, its just background in case someone jumps out of the woods and asks you. + +I should also mention that there may be some sort of fancy graphical interface into all this. But its much more satisfying to get your hands dirty and roll your own solution. + + +The boot scripts are located in /etc/rc.d. If you look in there, you'll find several subdirectories named init.d and rc0.d -> rc6.d. The rc?.d directories correspond directly to the various runlevels that Unix supports. Runlevel is a set of predefined modes that define what the system does. Mandrake has the runlevels defined as + + 0: complete machine stop; + + 1: single-user mode; to be used in the event of major + problems or system recovery; + + 2: multi-user mode, without networking; + + 3: Multi-user mode, but this time with networking; + + 4: unused; + + 5: like 3, but also launches the graphical login interface; + + 6: restart. + +If your machine comes up and immediately goes into an X-Window interface, then its default runlevel is 5. + +When you say 'reboot' or 'shutdown -r now', what you are really doing is putting the system into runlevel 6. + +When things have gone completely to shreds (typically after hardware failure or severe operator error ;), you want to get the machine into runlevel 1 - there are minimal services running and only one person can be logged in +at a time. + +Now, on to the nitty-gritty. + +The init.d subdirectory is where all the startup/shutdown scripts are kept. They are all text files, and you can +get a good idea of their format by cracking one open (I'd recommend a short one) and looking at it. You don't +need to be an expert in shell programming to get the gist of it. + +These scripts are called with one arguement (more on how that arguement gets supplied later). Virtually all scripts have the start and stop arguement, most have a restart, and then some have all sorts of custom ones. Lets look at a quick example. + +This is an init script for starting up Apache. Most are more cluttered, but not really more complicated. + + ----------------- + #!/bin/sh + # + # Start the Apache web server + # + + case "$1" in + 'start') + /usr/sbin/apachectl start ;; + 'stop') + /usr/sbin/apachectl stop ;; + 'restart') + /usr/sbin/apachectl restart ;; + *) + echo "usage $0 start|stop|restart" ;; + esac + ----------------- + +Anything after a # is a comment, except for the first line. +If that is #!, then the shell starts up the command +interpreter specified (in this case another /bin/sh) and +sends all the following commands through that. +(Don't know how far into Linux you are yet, but suffice +it to say that shell scripts are like batch files on +steroids). + +Moving down, we get to the first line of code. This is just +like a case statement in C, except for the syntax +(case block ends with a esac, which, of course, is case backwards). +$x are the positional parameters of the program +($0 is the name of the program, $1 is the first arguement, etc.) + +The rest of the script is pretty self-explanatory. +This particular one is easy as all the complicated parts +are handled by apachectl. + +So that's all well and good, but how and when does it +get called? OK, lets go back to the rc0.d -> rc6.d directories. +Looking in there, you can see a bunch of files that start +with K or S and a number. If you're using a color ls, +then you'll notice that they are a different color and there +might even be a @ after the name. That's because these are +all symlinks to the real files in the init.d directory. + +When the system enters a runlevel, it goes into the +appropriate /etc/rc.d/rc?.d directory and begins +executing the files in there. If the file begins +with a K, then the symlink gets called with a stop +(kill) arguement; if it starts with an S the symlink +is called with a start arguement. + +So if rc5.d has these links in it + + K15postgresql@ K60atd@ S15netfs@ S60lpd@ S90xfs@ + K20nfs@ K96pcmcia@ S20random@ S60nfs@ S99linuxconf@ + K20rstatd@ S05apmd@ S30syslog@ S66yppasswdd@ S99local@ + K20rusersd@ S10network@ S40crond@ S75keytable@ + K20rwhod@ S11portmap@ S50inet@ S85gpm@ + K30sendmail@ S12ypserv@ S55named@ S85httpd@ + K35smb@ S13ypbind@ S55routed@ S85sound@ + +the system is going to kill off postgress, nfs, rstatd, etc first +(in that order). Then its going to start apmd, network, portmap, etc. +(in that order). + +OK, so now you have a rough background in how the system starts. +So how do you get mh working in it? + +The easiest way is to go into init.d, copy an existing file, +and edit that. (Remember, you're root. Type carefully) + + cp httpd mh + vi mh + +and change the lines so that they look something +like this + + ----------------- + #!/bin/sh + # + # Start mh + # + + set mh_parms=/home/house/misterhouse/mh.private.ini + + case "$1" in + 'start') + /home/house/misterhouse/mh/mh & ;; + 'stop') + killall mh ;; + 'restart') + killall mh + sleep 5 + /home/house/misterhouse/mh/mh & ;; + *) + echo "usage $0 start|stop|restart" ;; + esac + ----------------- + +This is very, very sloppy, but you get the idea. + +Before you go further, TEST IT! Just call it by +hand from the command line like + + /etc/rc.d/init.d/mh start + + /etc/rc.d/init.d/mh stop + +etc. + +(you'll need to include the full path as init.d is +not in your PATH and neither is the currect directory for root) + +Once you've gotten all the bugs worked out and its working +the way you like it, figure out which runlevels you want +this to be running in. I'd guess 3 and 5, but its up +to you. + +Change into rc3.d. Figure out when you want mh to start +compared to all the other programs. I'd guess you'd want +to let everything else go first, then light up mh. +So we'll give it a number of 99, effectively going last. +Make the symlink like this + + ln -s /etc/rc.d/init.d/mh S99mh + +Do the same thing in rc5.d + +Now, you'll also want to have a good clean shutdown. +So you'll have to take care of runlevels 0 and 6. +Here, you'll want mh to get killed off early in +the process, so give it a low number, say 01. + + ln -s /etc/rc.d/init.d/mh K01mh + +You'll also want to do the same in rc1.d + + +Another option is to use a package called daemontools, available at + + http://cr.yp.to/daemontools.html + +His take on why you should use it is at + + http://cr.yp.to/daemontools/faq/create.html + +Its essentially inetd with the features it should have. + +When the system comes up, the daemontools program +svscan starts and then starts up any programs +you've asked it to. Those programs are then +monitored and get restarted if they die. The +good part here is that you can control any +program from the command line without editing +files + + svc -h /service/yourdaemon: sends HUP + svc -t /service/yourdaemon: sends TERM, and + automatically restarts the daemon after it dies + svc -d /service/yourdaemon: sends TERM, and leaves the service down + svc -u /service/yourdaemon: brings the service back up + svc -o /service/yourdaemon: runs the service once + + +------------------------- + +The following is from Harald Koch on 04/2002 + +I'll offer an alternate method. Neither is really better or worse; it +depends on your environment and requirements. + +I have a strong preference for running mission critical software +directly from init, instead of from startup scripts; init will +automatically restart software that crashes. I do this with misterhouse, +SSH, and a database that I run. + +init is controlled from a file called (usually) /etc/inittab. Here's my +MH entry from inittab: + + mh:2345:respawn:/home/mhouse/mhinit + +This tells init to run "/home/mhouse/mhinit" when the computer is in any +multi-user mode (see Mike's message for a definition of runlevels), and +to run it again when it exits. + +And here's my script: + + ----- /home/mhouse/mhinit ----- + #!/bin/sh + + mhhome=/home/mhouse + + cd ${mhhome} + + exec >> error_log 2>&1 + + export PATH=${mhhome}/mh/bin:${mhhome}/bin:$PATH + export mh_parms=${mhhome}/mh.ini + + # rotate logs + /bin/mv log.3 log.4 + /bin/mv log.2 log.3 + /bin/mv log.1 log.2 + /bin/mv log.0 log.1 + /bin/mv log log.0 + + # start + exec ${mhhome}/mh/bin/mh -log_file ${mhhome}/log + + ----- /home/mhouse/mhinit ----- + +This method *does* make it harder to stop MH, because you have to edit +/etc/inittab, change the "respawn" to "off", and then run "telinit q" to +tell init to re-read the config file. + +On the other hand, it means that MH will restart itself automatically +even if it exits due to bad code, which does happen to me occaisionally. + + +=head2 3.4: How do get Linux to play more than one sound at the same time + +Posted by Richard Phillips and Sean Walker on 03/2003 + +FYI This is a bit of a "head's up" for those people who may be +trying/struggling to get misterhouse to - for example - play music and also +at the same time be able to speak under linux. You may find, for example, +that misterhouse is silent whilst playing music and only after stopping +(say) xmms your misterhouse server starts speaking all the queued up +messages. + +There are a number of different ways to enable your linux server to +multiplex sound, and many different theories as to which way is better. Some +people prefer to use ALSA because it's not proprietary as opposed to ARTS +which is installed by default under many distributions such as Redhat. The +trouble with alsa at the moment is it can be a bit fiddly to configure as it +is not currently part of the linux kernel used in most distributions (it +will be when 2.5 is released). Anyhow, even after successfully getting alsa +installed, there can be some issues with getting multiple applications to +use the sound card concurrently - this is because not all alsa sound card +drivers support multiplexing. + +So..... + +If you want to use alsa, good for you. I won't get involved in which system +is better, and of course there are some other commercial alternatives out +there that a number of people also use and swear by. Anyhow, I'll just +describe what can be done to get things working with arts IF you are using +it AND are also having problems. + +1. Install your distribution as you would normally, and ensure that arts is +also installed/configured. As previously noted, many distros such as Redhat +use arts by default so you won't have to do anything special - everything +should be automagically detected. If you open up a terminal session and type +"ps -ef | grep artsd" you should see a line showing the details of the artsd +daemon. +2. Install flite - again depends on distro. With gentoo you can just do an +"emerge flite". If you don't have a package available for your distro, you +can get the source from http://www.speech.cs.cmu.edu/flite/ and try to build +it yourself. There are other speech engines you can use of course, but this +is probably the easiest to get running so it's what I'll use in this +demonstration. +3. From a command prompt type "flite -t hello -o play" - you should hear a +very bad hello from your computer +4. Fire up an application such as a music player that can use arts. If you +use something like xmms, make sure that it is actually using arts (with +xmms, go into options/preferences and check that the outplut plugin being +used is the "aRts driver"). Now load up a playlist, put it on repeat, and +start playing music. +5. Now try step 3 again - you'll probably find that it appears to hang. Just +kill it with a "ctrl c" +6. Now try "artsdsp flite -t hello -o play" instead - you should hear music +AND a bad "hello". + +Why is it so? Well, by default flite - and a number of other applications - +when trying to create sound directly access the sound device (usually +something like /dev/dsp). By using the wrapper "artsdsp" before running an +application such as flite, any calls to the sound device by the program are +trapped and redirected to the arts server - basically it forces the +application to "play nice" and not grab exclusive use of the sound device. + +So how do I now use flite and misterhouse? Well, you just need to change the +line in your ini file that tells misterhouse where flite is located. Go to a +command prompt and type "which artsdsp". This will let you know exactly +where the artsdsp program is located. Do the same for flite (eg "which +flite"). Now change your mh.private.ini file as follows: + +voice_text = flite +voice_text_flite = /usr/kde/3.1/bin/artsdsp /usr/bin/flite + +(Naturally, change the program locations depending on where your programs +live). + +Now, restart misterhouse and keep xmms (or whatever) running and playing +music. You should now be able to do something like go to the ia5 web +interface, select misterhouse, then misterhouse home, then browse mr house, +and then click on the "what is your up time" and be able to hear mister +house talking whilst music is also a happening thing! + +Of course, as noted before you can use the same wrapper for other programs +that may not directly support arts - for example if you install festival, +you can test it by typing something like "echo 'Hello from Festival'| +artsdsp festival --tts". + +The same also works for using esd as well as artsd. You can use JACK, +I'm sure, but I haven't tried it yet. Also, you can configure festival +to use any play program directly instead of using the artsdsp or esddsp +hacks. Those are hacks and will not work with all programs, just for +reference. To configure festival use something like this in your +siteinit.scm file (located in the festival root directory): + + (Parameter.set 'Audio_Required_Format 'snd) + (Parameter.set 'Audio_Command "esdplay -s localhost:5001 $FILE") + (Parameter.set 'Audio_Method 'Audio_Command) + +In the Audio_Command line, the only thing critical is the $FILE +You can use any player that you want in place of esdplay in the above, +but it must be compatible with the audio formats that festival can +produce. These, coming from the docs, are as follows: +The default is unheadered raw, but this may be any of the values +supported by the speech tools (including nist, esps, snd, riff, aiff, +audlab, raw and, if you really want it, ascii). + +More information can be found in the festival doc files festival_6.html +and festival_23.html wherever your festival documentation is installed, +including the handy: + + (Parameter.set 'Audio_Required_Rate 16000) + (Parameter.set 'Audio_Device "/dev/dsp2") + + +These parameters can be specified in alternative methods as well. We +should be able to tell festival which sound card to use or modify other +settings on the fly as well. I haven't looked into that yet. + +By default festival should be asynchronous and you should be able to +start playing from the file even before the file if finished writing. At +least that is what I gathered out of the doc files. Setting synchronous +or asynchronous modes are as easy as (audio_mode sync) or (audio_mode async) + + + +=head1 4: Windows specific questions + +=head2 4.1: mh seems to cause some windows to hang + +If you are experiencing problems with windows not popping up when they should (e.g. control panel or install shield), you will want to install DCOM 1.3, available from here: + + http://www.microsoft.com/com/dcom/dcom98/dcom1_3.asp + +Note, I had run all the 'Windows updates', including the Service Pack 2, but +I still had the problem until I installed the above. + +For Windows 95, the update is at: + + http://www.microsoft.com/com/dcom/dcom95/dcom1_3.asp + + +=head2 4.2: How do I set setup networking between Windows boxes + +You need to have the TCP/IP protocol enabled for your Networking Interface Card (NIC). +If you use a modem to reach the internet, you already have TCP/IP enable for the dial up adaptor, +but you need to seperately enable it for your NIC card, using the control panel Network icon. + +You can find instuctions on how to do this at: http://win98central.acauth.com/inside98/networking.htm . + +IP addresses that start with 10. (e.g. 10.0.0.1) are reserved for internal lans, so you can use +10.0.0.1, 10.0.0.2, etc. + +=head2 4.3 How do I run MH when not logged in to Win2K or XP? + +You can run any program as a service, using a program called srvany.exe, +available in the Resource Kit, or from here: + + http://www.electrasoft.com/srvany/srvany.htm + +Here is an example of registry entries after configuring my to run with srvany: + + HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MisterHouse\Parameters AppDirectory REG_SZ c:\mh\bin + HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MisterHouse\Parameters Application REG_SZ c:\perl\bin\perl.exe -S c:\mh\bin\mh + + +Rather than call mh as a service, many people have Windows do an auto-login and just add mh to the startup. + + +=head1 5: Perl questions + +=head2 5.1: Whats the best way to learn perl? + +Using MisterHouse ;-) Much more fun that trying to code up that report generator at work! +Some good books are referenced in mh.html. + + +=head2 5.2 What are good editors to use with perl? + +Codeing perl is much easier and less error prone if you use an programing +editor that automatically highlights and indents code. + +One very popular and powerful (and free) editor is Emacs: http://www.gnu.org/software/emacs/. +A pre-compiled windows version can be found here: http://www.gnu.org/software/emacs/windows/ntemacs.html + +Alan Jackson liks vim: http://www.vim.org. "Does syntax highlighting quite nicely for perl" + +Kieran Ames likes UltraEdit32: http://www.ultraedit.com. +"I swear by UltraEdit32 available at It works fully functional for 30 days and then expires. +Registration goes for around $30. I'm coding Perl all day long and would die without it! +Fairly thin and goes the distance with just about any language you'd want to use." + +Mark Yocom writes "I myself have grown fond of PrimalSCRIPT, which handles Perl, Java (and +JScript & JavaScript), HTML, ASP, LotusScript, Livewire, Python, SQL, Tcl, +REXX, VBScript, and good old fashioned .BAT/.CMD files. In addition to the +obligatory syntax coloring, it also has a number of other handy features. +It's spendy, but a trial version is available at their website: +http://www.sapien.com/PrimalSCRIPT.htm " + +=head2 5.3 When are you supposed to use '=>' as opposed to '->' ? + +You can use => for a synatically pretty way to seperate entries in a list. +When creating a hash, you can pass it a set of key/value pairs via a list, so instead of this: + + %hash = ('key1', 'value1', 'key2', 'value2'); + +You can use => instead of a , to make it look pretty: + + %hash = ('key1' => 'value1', 'key2' => 'value2'); + +Now to really confuse you, I'll try to explain how -> is used for dereferencing pointers. All mh objects can be viewed as pointers, and the state of an object is stored in a hash key of {state}, so there are 4 ways to get the current object state: + + $state = state $light; # Use the state method to get {state} + $state = $light->state; # Dereference with -> to get the state method + $state = $light->{state}; # Dereference with -> to get the state hash + $state = $$light{state}; # Dereference object with an extra + + +=head1 6: Misc questions + +=head2 6.1: Misterhouse Timeline + +Here is a rough timeline of MisterHouse development: + + - 12/96 Coded a predecessor called House_Menu + - 8/98 Re-wrote everything, called it MisterHouse + - 9/98 Uploaded to the web + - 10/98 Created compiled version. + - 11/98 First known user. + - 01/99 Started a mailing list at onelist.com + - 02/99 Ported mh to Unix/Linux + - 03/99 Article in Popular Home Automation + - 04/99 Posted to comp.home.automation + - 05/99 Registered misterhouse.net domain name. + - 06/99 Added CM17 & HomeBase support + - 06/99 Article in HomeToys: http://hometoys.com/htinews/jun99/articles/winter/winter.htm + - 08/99 Mailing list reaches 100 members + - 09/99 Added LCDproc and IRman support + - 10/99 Article in Circuit Cellar Ink: http://www.circuitcellar.com/pastissues/articles/winter111/winter.pdf + - 11/99 Linux VR using IBM's ViaVoice + - 11/99 HomeVision support + - 11/99 Mailing list reaches 200 members + - 01/00 Moved the mailing list and CVS repostitory to sourceforge.net + - 02/00 Re-worked the web interface + - 03/00 Added support for iButtons + - 04/00 Added support for X10's IR Commander transmiter + - 05/00 Added voice modem, ISDN modem, and Compool support. + - 05/00 Article in The Perl Journal #17 + - 06/00 Mailing list reaches 300 members + - 06/00 Added support for table item/event input and tie_items/tie_events. + - 08/00 Enable perl -w checks, .jpg and .gif, and Slinke support. + - 08/00 Added rpm and tarballs, NetGear router support + - 09/00 Added ViaVoiceTTS.pm, Jabber, and Applied Digital cpuxad support + - 10/00 Added barcode scanner and MS Agent support + - 11/00 Support for ical, rrd, html email access, WAP, and VXML for phone access via tellme.com + - 12/00 wx200d and wunderground personal weather project support, new add_sound function. + - 01/00 SMS and snnp messaging, improved iButton and benchmarking support. + - 01/01 Mailing list reaches 400 members + - 02/01 2 way AOL AIM messaging, earthquake and satellite tracking. + - 03/01 Backup program and menu templates for WAP, VXML, and LCD displays. + - 05/01 Audible menus, X10 Mr26 support, linux volume control. + - 07/01 Browser dependent web pages, zap2it tv support, call waiting callerid. + - 09/01 Audrey support, more MS TTS controls. + - 10/01 Web mh.ini editing, remote browse TTS support, zap2it tv pages. + - 11/01 New web interface, Audrey pictureframes, multiple sound cards. + - 12/01 Wireless wmr968 and Ultimeter2000 weather stations, Lynx10 X10 support. + - 01/02 Xmms, audrey callerid, Linksys rounter, TTS flite engine, standard CGIs. + - 03/02 GD on-the-fly web buttons, support for comics, xmms, redrat. + - 04/02 Added web calendar, contact, todo list, and tk photo slideshow + - 05/02 Improved TTS support, TWiki web site, and support for DSS units, BX24 interface, and MSN, + - 05/02 Rewrote web menu interface, added AudioTron support and improved im support. + - 06/02 Registered misterhouse.com domain name. + - 08/02 Caddx alarm, iButton humidity sensor, undo, light/dark hawkey sensor + - 09/02 Proxy mh, improved %cpu used, Group member and idle_time methods, Linux NaturalVoice, Clipsal CBUS + - 10/02 Enabled web based code selection, file based alarms, palm reader, and Mac OSX support. + - 11/02 UIRT2 control, better im control, web interface for triggers and items + - 12/02 Improved callerID, new respond and Text_Item functions, xAP support. + - 01/03 Mailing list reaches 500 members. + - 02/03 Addex xPL support, stacked and overloaded states, and multi-user logon. + - 03/03 Linux NaturalVoiceWine, web based TV and photo setup, improved Lynx X10 support. + - 04/03 Compiled mhe for linux, support for X10 W800, CallerID Identifier, USB-UIRT, and more iButton modules. + - 07/03 Improved web floorplan, DBI interface, xAP news and weather interfaces. + - 09/03 Cepstral TTS engine support, a new PA room object, improved proxy support. + - 11/03 Support for Sphinx2 VR, myHTPC tv menus, Asterisk phone interface, SMTP authentication, and a xAP command server. + - 12/03 Code for RCS TR40 theormostate, phone xAP. + - 01/04 Code for rrd weather graphs, v4lctl. + - 04/04 Code for ER1 robot, switched to the par compiler, improved mp3 Jukebox. + - 06/04 Code for Musica whole house audio, rss feeds, xAP Slimserver + - 07/04 Code for ICQ, vocp, tivo, Alpha LED displays, xAP IR for RedRat, RoboSapien. + - 08/04 Mailing list reaches 600 members. + - 09/04 Support for SVG floorplans, mp3 ripping, ebay monitoring, siteplayer, irc. + - 11/04 Support for servo motors, the ESRA robot, cell phone minutes monitoring, motherboard monitoring. + - 03/05 Support for serving multimedia files, cell phone minute monitors, shopping lists. + - 05/05 Support for distributed xAP speech, bluetooth proximity detection. + - 10/05 Support for xAP BCS, ssh, osd232, vocp, Wish x10, and EIB. + - 01/06 New spam free Wiki, support for azureus, Insteon iplcs, ical, k8055, TI103. + - 04/06 Moved from CVS to SVN, support for Xantrex power inverter, spanish web pages, french EDF power rates, DSC5401, xAP asterisk, + + +=head2 6.2: Who is that Bruce guy anyway? + +Bruce was the the original author of MisterHouse, but can no longer +claim much authorship credit as it is now truly a group effort with many contributors. +He has a day job with IBM in Rochester, Minnesota, designing integrated circuits. +Lots of stuff of interest only to family and aliens at http://brucewinter.net + +=head2 6.3: Why open source? + +Because open source only has 3 syllables, and proprietary has 5. + +That and MisterHouse has developed much faster with the help of a wider user base +and help from other coders than it would have if it were proprietary. + +Plus it feels good to give stuff back to the growing open source community. + + +=head2 6.4: Misc Home Automation links + +Here are some useful/related Home Automation links: + + Dan Hoehnens most excellent collection of HA links: + http://my.ohio.voyager.net/~dhoehnen/ha/list.html + + Mark Henrichs has a great 'Home Wiring Guide' site here: + http://www.wildtracks.cihost.com/homewire/ + + Over 500 links on about everything with HA: + http://home-automation.org/ + + Lots of good tutorials and articles are here: + http://hometoys.com/ + + X10 has many of its products documented here: + http://www.x10.com/support/support_manuals.htm + + Lots of good X10 data/ideas can be found here: + http://www.geocities.com/ido_bartana/toc.htm + http://www.x10ideas.com + http://www.x10.com/automation/homeautomation_e.htm + + Dave Houston has some good X10 info (e.g. causes of CM11A lockups) here: + http://www.laser.com/dhouston/ + + Europeans can get X10 stuff distributers listed here: + http://www.x10.com/x10euro.htm + Some examples are: + http://www.intellihome.be + http://www.marmitek.com/ + + Neil Cherry has created a home for linux related Home Automation programs at + http://linuxha.sourceforge.net + + Rene Mueller has a nice set of web pages with lots of info on HomeAppliances at + http://the-labs.com/HomeNetwork/ + + Jay Archer recommends this free Windows mail server and mult-platform web server: + http://www.argosoft.com + http://www.xitami.com + + Nokia sponsered open source site for home entertainment devices. + http://www.ostdev.net/ + + +=head2 6.5: How do I contribute additions or updates to the mh code? + +The MisterHouse code is maintained via a GitHub repository. +Instructions on how to commit changes can are here: +https://github.com/hollie/misterhouse/wiki/Contributing + +If you have minor changes and don't want to bother with a sourceforge ID, feel free to mail them to the mailing list. + + +=head2 6.6 What is X10? + +The people at http://www.x10.com sell affordable devices that communicate with each other over +normal household power lines. Manuals for many of there devices are listed +at http://www.x10.com/support/support_manuals.htm . +They have their home automation product slited here: http://www.x10.com/automation/homeautomation_e.htm/#homeautomation + +The most common modules are the Lamp module and the Appliance module. Both sell for around $10-$15. +The lamp module has can turn up to 300 watt lamps on, off, and various levels of bright. +The appliance module is an on/off relay. + +The 2 most popular X10 PC interfaces are +the $50 ActiveHome/CM11 and the $6 Firecracker/CM17. + +The CM17 is a transmit only interface. In addition to being almost free, another advantage is +it is wireless. It is a small, 1 inch box that just dangles off of your serial port and transmits +to a receiver that plugs into the wall. The other advantage to the CM17 is gives +instant ON/OFF control to the relay built into the wireless receiver. All other X10 devices +have about a 1/2 second delay to send and receive the protocol over the power lines. + +The CM11 is a 2 way interface, so you can program mh events to react to X10 motion sensor +and button pushes from various X10 controlers (e.g. keychain and palmpads). +Some of the newer, more expensive modules, like the $35 LM14A lamp module, +allow you to query their status, so you can determine it status even if it were manually turned on or off. + +If decide you splurge for the CM11 activehome kit, you might as well also get the Firecracker kit. +For the extra $6, you also get a few extra modules and mh can support both the CM11 and the CM17 +on the same port. + +There is lots of great X10 info at http://www.geocities.com/ido_bartana , +including information on using and buying X10 hardward outside of the US. + +Here is some more info, copied from a misterhouse-users post by Kevin Olalde. + + +The firecracker hardware, CM17 which plugs into the PC, can only send radio +frequency (RF) commands to devices. With the kit you will also get a +transceiver, TM751, that receives commands from the CM17 and relays +commands for the house code it's set to via your power lines. You also get +a lamp module, LM465, which can be control with via the transceiver. With +the PalmPad or the CM17 you send RF commands to the TM751, those commands +are then relayed to the lamp module (or other power line receiver modules, +like light switches). All of this is one way communications, the LM465 +lamp module and the TM751 transceiver are not capable of reporting status. +The TM751 doubles as an appliance module hard coded to unit 1. You can +only control it though RF commands, it does not respond to power pline +commands. + +The next step I took was to order the Active Home kit. With it you get +(among other stuff), another transceiver, RR501, and another computer +interface, CM11A. The RR501 works like the TM751, in that it receives RF +commands, and relays then on to the power line. A difference is that the +RR501 can report it's status. The computer interface, CM11A, can send +power line commands (you use computer software to tell the interface what +commands to send), 'sees' all commands being sent over the power line (and +reports those commands to computer software), and can ask modules for their +status. The RR501 also doubles as an appliance module, it can be set to +unit code 1 or 9, and can be controlled with both RF and power line +commands. + +The majority of the devices, lamp modules, appliance modules, wall +switches, ..... are one way devices, they can only receive. I have no +historical perspective, but it looks as though that was the way the +original X10 spec went, one way only. + +There are devices that can report their current status to something like +the CM11A, but I haven't used any of then (except for the RR501), since +that are much more expense. + + http://www.x10.com/products/x10_lm465.htm one way lamp module from X10, $13 + http://www.x10.com/products/x10_lm14a.htm two way lamp module from X10, $33 + +Bill Bass sent in these links to a bunch of links from +Phil Kingery's great series of X10 articles at http://hometoys.com + + Which One Should I Use? + http://www.hometoys.com/htinews/dec96/articles/kingery/kingery.htm + + Controlling Motors and Transformers + http://www.hometoys.com/htinews/apr97/articles/kingery/kingery2.htm + + 120/240v Residential Coupling + http://www.hometoys.com/htinews/jun97/articles/kingery/kingery3.htm + + Complex Residential Coupling with Considerations for Dim/Bright + http://www.hometoys.com/htinews/aug97/articles/kingery/kingery4.htm + + Dim/Bright Commands and Coupler-Repeaters + http://www.hometoys.com/htinews/oct97/articles/kingery/kingery5.htm + + Three-Way Switch Circuits + http://www.hometoys.com/htinews/dec97/articles/kingery/kingery6.htm + + More Three-Way and Four-Way Switch Circuits + http://www.hometoys.com/htinews/feb98/articles/kingery/kingery7.htm + + Troubleshooting Three-Way and Four-Way Switch Circuits + http://www.hometoys.com/htinews/apr98/articles/kingery/kingery8.htm + + Noise and Filtering + http://www.hometoys.com/htinews/jun98/articles/kingery/kingery9.htm + + +=head2 6.7 What do I need to use the iButtons? + +Ray Dzek created a how to get started with iButton guide at +http://www.solarbugs.com/home/ibutton.htm + +Here is a post from Clay Jackson + +Right now, I've got 3 sensors (DS1822, in the three-lead package, inside an +epoxy filled soda straw) at the end of 150' of Radio Shack "flat" cable, +talking to a DS9097U (the U is CRITICAL) with no problems. On the bench, I +had another 2 sensors on the end of a 50' cable (for a total of 5 sensors). +When I strung the 50' cable, I must have crimped a lead, because it now +meters "closed" and when it was plugged into the "net", I got no readings. + +I also have an HA5 from Point-Six; that I'm gonna play with soon. The code +Bruce did is for the 9097U, so that's why I started there (and, it's +cheaper). The 9097U has all the "passive" pull-up stuff the data sheets +talk about (courtesy of a DS2048 inside the DB9); so I'm hoping it will do +the trick. Anyway, I'll keep the list posted as things progress. + +Bottom line - all I needed was Bruce's code, a 9097U from Dallas, the +sensors and some Radio Shack "phone" cable. + +One more thing to watch - as best I can tell, Dallas has not standardized +their thermal devices with respect to their outputs. For example, the +DS1820 and DS1920 both return a 9 bit value that's basically an integer. +However, the DS1822 returns a VARIABLE length value (12 bits by default), +with a binary point between the 3rd and 4th digits. According to someone +else on the list, the ThermoCRON iButton is different yet again. + + +Here is another post from Jeff Pagel (06/2002). + + +The Dallas '1-wire' bus for the ibutton family is actually 2 wires(there +are only 2 connecting points on the devices). 1 for signal and power, the +other for ground. They talk about cat-5, which has 8 conductors, a lot +because the signal charactoristics of it are very good for the '1-wire' +bus, twisted pairs, size of the wire, etc.. Of the 8, you only need to use +2. Theoretically, the other 6 could be used for something else. + +So, back to the original problem, you would need to run a twisted pair, +like cat5 or cat3, from your mh machine to the garage. I have cat5 runs of +over 500 feet that work fine without any special conditioning. + +The cool part about the 1-wire bus is that you just need to run 1(ok +actually 2,but usually in 1 bundle) wire(s) around your house. It's like a +'multi-drop' system. One connects to the other, to the other, etc., in +parallel. + + In this case, you would run a line, from 1 spool of wire, to the bedroom +to the living room to the kids room back to the computer. If this is a +pre-existing structure, you will face some wiring issues. It's much easier +for new construction. For each ibutton, you would connect 'across' each +line. Just solder plus to plus, minus to minus and keep the wire going. +Think of a railroad track as the 'wire' and the railroad ties as the +ibutton devices. + +The 1-wire bus is fairly restrictive on how you wire it vs how long the +runs are. You should avoid 'star' type configurations. They are ok, but +only for short runs, the stars introduce reflections of the signal. + +Here will be a really bad ascii representation: + + + Host + ----------------------------------------------------- + | | | + ibutton1 ibutton2 ibutton3 + | | | + Host - ---------------------------------------------------- + + + +From Yannick Moussette (06/2002) + +The DS1820 Temp sensors come in 2 models: iButton and TO-220 +(transistor-type casing). + +The Ibutton has a total of 2 connections, ground and signal. It functions in +"parasitic" mode to draw its power from the signal connections (kinda of +like what X10 modules do to piggyback signals on powerlines...). + +Where things that may confuse you on Dallas's Website site, is the TO-220 +format of this same sensor which has 3 leads (connections). 1 for power, 1 +for ground and 1 for signal(data). The power lead may be omitted, in which +case the sensor will work exactly like its iButton counterpart: Through +parasitic powering. + +Here is a link with some detailed electronic info: http://www.maxim-ic.com/1st_pages/tb1.htm + +Kieran Ames has a nice writeup on how he used iButtons to log and plot his +pond temperatures: http://ames.homeip.net:81/pages/My_iButtonVenture.htm + + +=head1 7.0: Setup Questions + +=head2 7.1: Password Management + +To password protect Misterhouse, you can set passwords for different users via set_password. +The 'admin' link from the main page is only for logging in once you have setup a password. + +To enable password protection, run mh/bin/set_password command like this: + + set_password -user family -password xyz1 + set_password -user admin -password xyz2 + + Note: only the first 8 characters are used. The admin password is + required for controling the mh web setup menus (e.g. item and code + selection menus).(Unix or Windows) + +This will create a file pointed to with the mh.ini password_file parm (e.g. mh/data/.password). +To further extend which user can do what see password_allow in mh/data/ + + +=head2 7.2: Customizing the TV guide + +To set the MisterHouse TV Today so it will display listings relevant to your area and provider +Start by finding your tv_provider ID, run this command (located in mh/bin): + + get_tv_grid -zip your_zipcode -get_provider + +Then edit your mh.ini and set the tv_provider_name parm. +Listed below are two examples or related parms, one that uses sat_ and one that is tv_. +You can copy these tv_* parms as another set of sat_* or cable_* or +or whatever_*, then run get_tv_grid multiple times to support +multiple sets of tv schedules. For example: + + get_tv_grid -db tv (-db tv is the default) + get_tv_grid -db sat + + sat_provider = 128772 + sat_provider_name = DirectTV Washington + sat_name =SAT @ Used to give a useful label on the web pages + sat_hours=all @ Which hours to get. Use all for all hours + sat_label=SATELLITE @ Which web link name. Use none to disable, + sat_channels_keep= @ Which channels to keep + sat_channels_skip= @ Which channels to skip + sat_channel_min=0 @ Keep only channels above this number + sat_channel_max=99999 + + tv_provider_name = Charter Communications - Rochester + tv_name = TV @ Used to give a useful label on the web pages + tv_hours=02,06,10,14,18,22 @ Which hours to get. Use all for all hours + tv_label=VCR @ Which web link name. Use none to disable, + tv_channels_keep= @ Which channels to keep + tv_channels_skip= @ Which channels to skip + tv_channel_min=0 @ Keep only channels above this number + tv_channel_max=99999 @ Keep only channels below this number + +Use mh/common/tv_grid.pl to do daily call get_tv_grid to update your tv database and web pages. +and mh/code/common/tv_info.pl to search and announce shows. +If you change your tv_provider, you can use get_tv_grid -reget to refresh the tv/*.html files. + +When I first setup MH it took me three weeks to figure out that my providerID +had changed since install (matter of a week) and thats why the tv listings came up broken. +So if you have a problem, and you think the syntax in the mh.ini is correct, try re-checking your provider ID. + + +=head2 7.3: How can I point MisterHouse to a custom web interface? + +From David Norwood on 10/2001 + +Here's a way to configure Misterhouse so you can have your own custom web +interface AND still have access to web pages that are introduced with new +releases (like the Audrey pages). + +Let's assume your misterhouse distribution is in /misterhouse/mh and your +custom html files are in /misterhouse/web/custom. Set these private.ini +parameters: + + html_dir=$Pgm_Root/web + html_file=/custom/index.html + html_alias_custom=/custom /misterhouse/web/custom + +Now your custom web pages will come up by default, but you will still have +access to the latest and greatest "mh4" and "ia*" interfaces. Your pages +will not be over-written by new releases. If you need another directory +for, say, your audrey interface, just add another html_alias_whatever +parameter. + +You can now also override just specific pages from an existing web interface. +For example, create your own web/ia5 and web/ia5/security +directories, then add this mh.ini parm to point to it: + + html_alias2_ia5 = /misterhouse/web/ia5 + +For example, you can change the ia5 Security menu to point to different webcams or floorplan +images by creating your own web/ia5/security directory with just the files you want to +modify. + + + +=cut + + + diff --git a/docs/usenet_post.txt b/docs/usenet_post.txt index 75c6a5427..3a8f20394 100644 --- a/docs/usenet_post.txt +++ b/docs/usenet_post.txt @@ -1,103 +1,103 @@ -All messages from thread -Message 1 in thread -From: Bruce Winter (brucewinter@home.net) -Subject: MisterHouse open source HA program - - -View this article only -Newsgroups: comp.home.automation -Date: 1999/04/01 - - -Hi all, - -I am the author of a free, perl based HomeAutomation (HA) program called -mysterious (mh). I've been using/extending for about a year or so, but it -has just recently become stable enough that I feel comfortable posting this -announcement. - -Perl is a free, powerful, interpreted language that is available on many -different operating systems. By extending perl with various HA related -objects and methods, mh can use the perl language for its event language. -Since it is written in perl, it can in theory run on any OS that runs perl. -I've run it on Windows 95, 98, NT, linux, and AIX. - -On the windows platform, mh can use free MS TTS and VR engines. On linux, -it can use the Festival TTS engine. - -On the downside, it is a bit of a memory hog, using between 10 and 20 mb, -depending on if you enable the Tk gui interface and how many events you -have. On my house computer, for example, I have about 3k lines of code in -40 different event files. This uses 20 mb and about 30% of a 100 mhz -Pentium cpu, set at 10 passes per second. - -On the upside, since it is an extension of a powerful programming language, -you can fairly easily program complex events. Here are a few of the tasks I -have running: - -- Control of south facing window curtains. By monitoring sunlight and -temperatures, mh can maximize passive solar heat gain. - -- Internet data retrieval and processing. Monitors URLs, sends and monitors -email, announcing who has what mail. Other example data is weather info and -TV programming. - -- Controls a relay enabled PA speaker system, so it can speak to one room or -all rooms in the house. - -- For those with radio receivers, it can tracks HAM radio GPS enable cars, -announcing speed and position. Also gathers weather data from HAM/packet -weather stations. - -You can control events with voice, a web interface, a Tk widget GUI -interface, from a command line, via tcpip sockets, time of day, and/or -serial port data. It currently has code to interface with the CM11, various -weeder PIC kits, wx200 weather station, and various other serial devices. - -Programming mh is not as easy to program as some of the other fine HA -programs out there, and it requires a 32 Meg 100 mhz Pentium or bigger, so -it is not for everyone. But it is free and open sourced, so if it doesn't -do what you want it to do, you can change it or ask for a change from any -the other mh users. Currently there are about 50 people on the mailing -list. - - You can download it and or view the mailing list archives from any of the -following: - - http://members.home.net/winters/house/programs/index.html - http://www.geocities.com/ResearchTriangle/Lab/5514/index.html - http://misterhouse.webjump.com/index.html - - -Bruce Winter -Message 2 in thread -From: Bruce Winter (brucewinter@home.net) -Subject: Re: MisterHouse open source HA program - - -View this article only -Newsgroups: comp.home.automation -Date: 1999/04/02 - - -blush ... my spell checker went out of control. I ment to say: - - "program called MisterHouse" - -instead of: - - "program called mysterious" - -One other note: you can take a peek at the web interface at - -http://winter.tzo.com:8080 - -This is MisterHouse running on my house computer, connected to the net via a -cable modem. - -Bruce - -> I am the author of a free, perl based HomeAutomation (HA) program called -> mysterious (mh). I've been using/extending for about a year or so, but it -> has just recently become stable enough that I feel comfortable posting this -> announcement. +All messages from thread +Message 1 in thread +From: Bruce Winter (brucewinter@home.net) +Subject: MisterHouse open source HA program + + +View this article only +Newsgroups: comp.home.automation +Date: 1999/04/01 + + +Hi all, + +I am the author of a free, perl based HomeAutomation (HA) program called +mysterious (mh). I've been using/extending for about a year or so, but it +has just recently become stable enough that I feel comfortable posting this +announcement. + +Perl is a free, powerful, interpreted language that is available on many +different operating systems. By extending perl with various HA related +objects and methods, mh can use the perl language for its event language. +Since it is written in perl, it can in theory run on any OS that runs perl. +I've run it on Windows 95, 98, NT, linux, and AIX. + +On the windows platform, mh can use free MS TTS and VR engines. On linux, +it can use the Festival TTS engine. + +On the downside, it is a bit of a memory hog, using between 10 and 20 mb, +depending on if you enable the Tk gui interface and how many events you +have. On my house computer, for example, I have about 3k lines of code in +40 different event files. This uses 20 mb and about 30% of a 100 mhz +Pentium cpu, set at 10 passes per second. + +On the upside, since it is an extension of a powerful programming language, +you can fairly easily program complex events. Here are a few of the tasks I +have running: + +- Control of south facing window curtains. By monitoring sunlight and +temperatures, mh can maximize passive solar heat gain. + +- Internet data retrieval and processing. Monitors URLs, sends and monitors +email, announcing who has what mail. Other example data is weather info and +TV programming. + +- Controls a relay enabled PA speaker system, so it can speak to one room or +all rooms in the house. + +- For those with radio receivers, it can tracks HAM radio GPS enable cars, +announcing speed and position. Also gathers weather data from HAM/packet +weather stations. + +You can control events with voice, a web interface, a Tk widget GUI +interface, from a command line, via tcpip sockets, time of day, and/or +serial port data. It currently has code to interface with the CM11, various +weeder PIC kits, wx200 weather station, and various other serial devices. + +Programming mh is not as easy to program as some of the other fine HA +programs out there, and it requires a 32 Meg 100 mhz Pentium or bigger, so +it is not for everyone. But it is free and open sourced, so if it doesn't +do what you want it to do, you can change it or ask for a change from any +the other mh users. Currently there are about 50 people on the mailing +list. + + You can download it and or view the mailing list archives from any of the +following: + + http://members.home.net/winters/house/programs/index.html + http://www.geocities.com/ResearchTriangle/Lab/5514/index.html + http://misterhouse.webjump.com/index.html + + +Bruce Winter +Message 2 in thread +From: Bruce Winter (brucewinter@home.net) +Subject: Re: MisterHouse open source HA program + + +View this article only +Newsgroups: comp.home.automation +Date: 1999/04/02 + + +blush ... my spell checker went out of control. I ment to say: + + "program called MisterHouse" + +instead of: + + "program called mysterious" + +One other note: you can take a peek at the web interface at + +http://winter.tzo.com:8080 + +This is MisterHouse running on my house computer, connected to the net via a +cable modem. + +Bruce + +> I am the author of a free, perl based HomeAutomation (HA) program called +> mysterious (mh). I've been using/extending for about a year or so, but it +> has just recently become stable enough that I feel comfortable posting this +> announcement. diff --git a/lib/Insteon.pm b/lib/Insteon.pm index 58afb690f..ba9c5a810 100755 --- a/lib/Insteon.pm +++ b/lib/Insteon.pm @@ -1,1370 +1,1370 @@ -package Insteon; - -use strict; - -# Category=Insteon - -#@ This module creates voice commands for all insteon related items. - -=head1 B - -=head2 DESCRIPTION - -Provides the basic infrastructure for the Insteon stack, contains many of the -startup routines. - -=head2 INHERITS - -None - -=head2 VOICE COMMANDS - -=head3 PLM - -=over - -=item C - -If a device is first placed into linking mode, calling this command will cause -the PLM to complete the link, thus making the PLM the responder. The -C device voice command is likely an easier way to do this, -but this may be need for hard to reach devices or deaf devices. - -=item C - -Call this first, then press and hold the set button on a device that you wish -to have the PLM control. The C device voice command is -likely an easier way to do this, but this may be need for hard to reach devices -or deaf devices. This is also needed for i2cs devices in which the first link -must currently be manually created this way. - -=item C - -Cancel either of the above two commands without completing a link. - -=item C - -This does nothing and shoudl be removed. - -=item C - -This will scan and output to the log only the PLM link table. - -=item C - -This will output only the PLM link table to log. - -=item C - -Misterhouse will review the state of all of the links in your system, as it knows -them without any additional scanning. If any of these links are not defined in -your mht file or the links are only half links (controller with no responder or -vice versa) MisterHouse will delete these links. - -It is usually best to: - -1. Run C first unless you know that the -information in MisterHouse is up-to-date. - -2. Run C and verify that what is being added is correct. - -3. Run C to add the links - -4. Run C first to see what will happen. - -5. If everything looks right, run C to clean up the old links - -Deleting the orphan links will make your devices happier. If you have unintended -links on your devices, they can run slower and may unnecessarily increase the -number of messages sent on your network. - -=item C - -Does the same thing as C but doesn't actually delete anything -instead it just prints what it would have done to the log. - -=item C - -Scans the link tables of the PLM and all devices on your network. On a large -network this can take sometime. You can generally run C -which is much faster without any issue. - -=item C - -Scans the link tables of the PLM and all devices whose link tables have changed -on your network. - -=item C - -Similar to C exccept this adds any links that are missing. -This is helpful when adding a bunch of new devices, new scenes, or cleaning things -up. - -See the workflow described in C. - -=item C - -Same as C but prints what it would do to the log, without doing -anything else. - -=item C - -Prints the message stats for all devices plus a summary of the entire network -stats. For full details on what is contained in this printout please see -the description for the C voice command under the device -heading below. - -=item C - -Resets all the message stats for all devices including the Unk_Error stat. - -=item C - -Performs a 5 count stress test on all devices on the network. See the description -of what a stress test is under the device voice commands. - -=item C - -Performs a 5 count ping test on all devices on the network. See the description -of what a ping test is under the device voice commands. - -=item C - -Logs some details about each device to the log. See C - -=back - -=head3 Devices - -=over - -=item C - -Turns the device on. - -=item C - -Turns the device off. - -=item C - -Similar to C above, but this will only add links that are related -to this device. Useful when adding a new device. - -=item C - -Will create the controller/responder links between the device and the PLM. - -=item C - -Will delete the controller/responder links between the device and the PLM. -Useful if you are removing a device from your network. - -=item C - -Requests the status of the device. - -=item C - -Requests the engine version of the device. Generally you would not need to call -this, but every now and then it is needed when a new device is installed. - -=item C - -This will scan and output to the log only the link table of this device. - -=item C - -Will output to the log only the link table of this device. - -=item C - -Generally only available for PLM Scenes. This places the PLM in linking mode -and adds any device which the set button is pressed for 4 seconds as a responder -to this scene. Generally not needed. - -=item C - -Cancels the above linking session without creating a link. - -=item C - -Simulates a read of a 5 link addresses from the device. This routine is meant to -be used as a diagnostic tool. - -This is also similar to the C test, however, rather than simply requesting -an ACK, this requests a full set of data equivalent to a link entry. Similar to -C this should be used with C to diagnose issues and try -different settings. - -=item C - -Sends 5 ping messages to the device. A ping message is a basic -message that simply asks the device to respond with an ACKnowledgement. For -i1 devices this will send a standard length command, for i2 and i2cs devices -this will send an extended ping command. In both cases, the device responds -back with a standard length ACKnowledgement only. - -Much like the ping command in IP networks, this command is useful for testing the -connectivity of a device on your network. You likely want to use this in -conjunction with the C routine. For example, you can use -this to compare the message stats for a device when changing settings in -MisterHouse. - -=item C - -Prints message statistics for this device to the print log. - -=back - -=over8 - -=item * - -In - The number of incoming messages received - -=item * - -Corrupt - The number of incoming corrupt messages received - -=item * - -%Corrpt - Of the incoming messages received, the percentage that were -corrupt - -=item * - -Dupe - The number of duplicate messages that have been received from this -device. - -=item * - -%Dupe - The percentage of duplilicate incoming messages received. - -=item * - -Hops_Left - The average hops left in the messages received from this device. - -=item * - -Max_Hops - The average maximum hops in the messages received from this device. - -=item * - -Act_Hops - Max_Hops - Hops_Left, this is the average number of hops that have -been required for a message sent from the device to reach MisterHouse. - -=item * - -Out - The number of unique outgoing messages, without retries, sent. - -=item * - -Fail - The number times that all retries were exhausted without a successful -delivery of a message. - -=item * - -%Fail - Of the outgoing messages sent, the percentage that failed. - -=item * - -Retry - The number of retry attempts that have been made to deliver a message. -Ideally this is 0, but Sends/Msg is a better indication of this parameter. - -=item * - -AvgSend - The average number of send attempts that must be made in order to -successfully deliver a message. Ideally this would be 1.0. - -NOTE: If the number of retries exceeds the value set in the configuration file -for Insteon_retry_count, MisterHouse will abandon sending the message. As a -result, as this number approaches Insteon_retry_count it becomes a less accurate -representation of the number of retries needed to reach a device. - -=item * - -Avg_Hops - The average number of hops that have been used by MisterHouse when -sending messages to this device. - -=item * - -Hop_Count - The current hop count being used by MH. This count is dynamically -controlled by MH and is not reset by calling C - -=back - -=over - -=item C - -Resets the message stats back to 0 for this device. - -=back - -=head2 METHODS - -=over - -=cut - - -my (@_insteon_plm,@_insteon_device,@_insteon_link,@_scannable_link,$_scan_cnt,$_sync_cnt,$_sync_failure_cnt); -my $init_complete; -my (@_scan_devices,@_scan_device_failures,$current_scan_device); -my (@_sync_devices,@_sync_device_failures,$current_sync_device); -my ($_stress_test_count, $_stress_test_one_pass, @_stress_test_devices); -my ($_ping_count, @_ping_devices); - -=item C - -Sequentially goes through every Insteon device and performs a stress_test on it. -See L -for a more detailed description of stress_test. - -Parameters: - Count: defines the number of stress_tests to perform on each device. - is_one_pass: if true, all stress_tests will be performed on a device - before proceeding to the next device. if false, the routine - loops through all devices performing one stress_test on each - device before moving on to the next device. - -=cut - -sub stress_test_all -{ - my ($p_count, $is_one_pass) = @_; - if (defined $p_count){ - $_stress_test_count = $p_count; - $_stress_test_one_pass = $is_one_pass; - @_stress_test_devices = undef; - push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); - main::print_log("[Insteon::Stress Test All Devices] Stress Testing All Devices $p_count times"); - }; - if (!@_stress_test_devices) { - #Iteration may be complete, start over from the beginning - $_stress_test_count = ($_stress_test_one_pass) ? 0 : $_stress_test_count--; - push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); - } - if ($_stress_test_count > 0){ - my $current_stress_test_device; - my $complete_callback = '&Insteon::stress_test_all()'; - while (@_stress_test_devices){ - $current_stress_test_device = pop @_stress_test_devices; - next unless $current_stress_test_device->is_root(); - next unless $current_stress_test_device->is_responder(); - last; - } - my $run_count = ($_stress_test_one_pass) ? $_stress_test_count : 1; - if (ref $current_stress_test_device && $current_stress_test_device->can('stress_test')){ - $current_stress_test_device->stress_test($run_count, $complete_callback); - } - } - else { - $_stress_test_one_pass = 0; - main::print_log("[Insteon::Stress Test All Devices] Complete"); - } -} - -=item C - -Walks through every Insteon device calling the device's scan links command. -Does not output anything but will recreate the device's aldb from the actual -entries in the device. - -=cut - -sub scan_all_linktables -{ - my $skip_unchanged = pop(@_); - $skip_unchanged = 0 if (ref $skip_unchanged || !defined($skip_unchanged)); - if ($current_scan_device) - { - &main::print_log("[Scan all linktables] WARN: link already underway. Ignoring request for new scan ..."); - return; - } - my @candidate_devices = (); - # clear @_scan_devices - @_scan_devices = (); - @_scan_device_failures = (); - $current_scan_device = undef; - # alwayws include the active interface (e.g., plm) - push @_scan_devices, &Insteon::active_interface; - - push @candidate_devices, &Insteon::find_members("Insteon::BaseDevice"); - - # don't try to scan devices that are not responders - if (@candidate_devices) - { - foreach (@candidate_devices) - { - my $candidate_object = $_; - if ($candidate_object->is_root and - !($candidate_object->is_deaf - or $candidate_object->isa('Insteon::InterfaceController'))) - { - push @_scan_devices, $candidate_object; - &main::print_log("[Scan all linktables] INFO1: " - . $candidate_object->get_object_name - . " will be scanned.") if $candidate_object->debuglevel(1, 'insteon'); - } - else - { - &main::print_log("[Scan all linktables] INFO: !!! " - . $candidate_object->get_object_name - . " is NOT a candidate for scanning."); - } - } - } - else - { - &main::print_log("[Scan all linktables] WARN: No insteon devices could be found"); - } - $_scan_cnt = scalar @_scan_devices; - - &_get_next_linkscan($skip_unchanged); -} - -=item C<_get_next_linkscan_failure()> - -Called if a the scanning of a device fails. Logs the failure and proceeds to -the next device. - -=cut - -sub _get_next_linkscan_failure -{ - my($skip_unchanged) = @_; - push @_scan_device_failures, $current_scan_device; - &main::print_log("[Scan all link tables] WARN: failure occurred when scanning " - . $current_scan_device->get_object_name . ". Moving on..."); - &_get_next_linkscan($skip_unchanged); - -} - -=item C<_get_next_linkscan()> - -Gets the next device to scan. - -=cut - -sub _get_next_linkscan -{ - my($skip_unchanged, $changed_device) = @_; - my $checking = 0; - if (!defined($changed_device)) { - $current_scan_device = shift @_scan_devices; - if ($skip_unchanged && $current_scan_device && ($current_scan_device != &Insteon::active_interface)){ - ## check if aldb_delta has changed; - $current_scan_device->_aldb->{_aldb_unchanged_callback} = '&Insteon::_get_next_linkscan('.$skip_unchanged.')'; - $current_scan_device->_aldb->{_aldb_changed_callback} = '&Insteon::_get_next_linkscan('.$skip_unchanged.', '.$current_scan_device->get_object_name.')'; - $current_scan_device->_aldb->{_failure_callback} = '&Insteon::_get_next_linkscan_failure('.$skip_unchanged.')'; - $current_scan_device->_aldb->query_aldb_delta("check"); - $checking = 1; - } - } else { - $current_scan_device = $changed_device; - } - if ($current_scan_device && ($checking == 0)) - { - &main::print_log("[Scan all link tables] Now scanning: " - . $current_scan_device->get_object_name . " (" - . ($_scan_cnt - scalar @_scan_devices) - . " of $_scan_cnt)"); - # pass first the success callback followed by the failure callback - $current_scan_device->scan_link_table('&Insteon::_get_next_linkscan('.$skip_unchanged.')','&Insteon::_get_next_linkscan_failure('.$skip_unchanged.')'); - } elsif (scalar(@_scan_devices) == 0 && ($checking == 0)) - { - &main::print_log("[Scan all link tables] All tables have completed scanning"); - my $_scan_failure_cnt = scalar @_scan_device_failures; - if ($_scan_failure_cnt){ - my $obj_list; - for my $failed_obj (@_scan_device_failures){ - $obj_list .= $failed_obj->get_object_name .", "; - } - ::print_log("[Scan all link tables] However, some failures " - ."were noted with the following devices: $obj_list"); - } - } -} - -=item C - -Initiates a process that will walk through every device that is a Insteon::InterfaceController -calling the device's sync_links() command. sync_all_links() loads up the module -global variable @_sync_devices then kicks off the recursive call backs by calling -_get_next_linksync. - -Paramter B - Causes sync to walk through but not actually -send any commands to the devices. Useful with the insteon:3 debug setting for -troubleshooting. - -=cut - -sub sync_all_links -{ - my ($audit_mode) = @_; - &main::print_log("[Sync all links] Starting now!"); - @_sync_devices = (); - # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links - for my $obj (&Insteon::find_members('Insteon::BaseController')) - { - my %sync_req = ('sync_object' => $obj, 'audit_mode' => ($audit_mode) ? 1 : 0); - &main::print_log("[Sync all links] Adding " . $obj->get_object_name - . " to sync queue"); - push @_sync_devices, \%sync_req - } - - $_sync_cnt = scalar @_sync_devices; - - &_get_next_linksync(); -} - -=item C<_get_next_linksync()> - -Calls the sync_links() function for each device in the module global variable -@_sync_devices. This function will be called recursively since the callback -passed to sync_links() is this function again. Will also ask sync_links() to -call _get_next_linksync_failure() if sync_links() fails. - -=cut - -sub _get_next_linksync -{ - $current_scan_device = shift @_scan_devices; - my $sync_req_ptr = shift(@_sync_devices); - my %sync_req = ($sync_req_ptr) ? %$sync_req_ptr : undef; - if (%sync_req) - { - - $current_sync_device = $sync_req{'sync_object'}; - } - else - { - $current_sync_device = undef; - } - - if ($current_sync_device) - { - &main::print_log("[Sync all links] Now syncing: " - . $current_sync_device->get_object_name . " (" - . ($_sync_cnt - scalar @_sync_devices) - . " of $_sync_cnt)"); - my $skip_deaf = 1; - # pass first the success callback followed by the failure callback - $current_sync_device->sync_links($sync_req{'audit_mode'}, '&Insteon::_get_next_linksync()','&Insteon::_get_next_linksync_failure()', $skip_deaf); - } - else - { - &main::print_log("[Sync all links] All links have completed syncing"); - my $_sync_failure_cnt = scalar @_sync_device_failures; - if ($_sync_failure_cnt){ - my $obj_list; - for my $failed_obj (@_sync_device_failures){ - $obj_list .= $failed_obj->get_object_name .", "; - } - ::print_log("[Sync all links] WARN! Failures occured, " - ."some links involving the following objects remain out-of-sync: $obj_list"); - } - } - -} - -=item C<_get_next_linksync()> - -Called by the failure callback in a device's sync_links() function. Will add -the failed device to the module global variable @_sync_device_failures. - -=cut - -sub _get_next_linksync_failure -{ - push @_sync_device_failures, $current_sync_device - unless (grep{$current_sync_device == $_} @_sync_device_failures); - &main::print_log("[Sync all links] WARN: failure occurred when syncing links for " - . $current_sync_device->get_object_name . ". Resuming sync queue if it exists."); - my $num_sync_queue = @{$$current_sync_device{sync_queue}}; - if ($num_sync_queue){ - $current_sync_device->_process_sync_queue(); - } - else { #No other pending links in the queue - &_get_next_linksync(); - } - -} - -=item C - -Walks through every Insteon device and pings it as many times as defined by -count. See L -for a more detailed description of ping. - -=cut - -sub ping_all -{ - my ($p_count) = @_; - if (defined $p_count){ - $_ping_count = $p_count; - @_ping_devices = (); - push @_ping_devices, Insteon::find_members("Insteon::BaseDevice"); - main::print_log("[Insteon::Ping All Devices] Ping All Devices $p_count times"); - } - if (@_ping_devices) - { - my $current_ping_device; - while(@_ping_devices) - { - $current_ping_device = pop @_ping_devices; - next unless $current_ping_device->is_root(); - next unless $current_ping_device->is_responder(); - last; - } - $current_ping_device->ping($_ping_count, '&Insteon::ping_all()') - if $current_ping_device->can('ping'); - } else - { - $_ping_count = 0; - main::print_log("[Insteon::Ping All Devices] Ping All Complete"); - } -} - -=item C - -Walks through every Insteon device and logs: - -=back - -=over8 - -=item * - -Hop Count - -=item * - -Engine Version - -=item * - -ALDB Type - -=item * - -ALDB Health - -=item * - -ALDB Scan Time - -=back - -=over - -=cut - -sub log_all_ADLB_status -{ - my @_log_ALDB_devices = (); - # alwayws include the active interface (e.g., plm) -# push @_log_ALDB_devices, &Insteon::active_interface; - - push @_log_ALDB_devices, Insteon::find_members("Insteon::BaseDevice"); - - # don't try to scan devices that are not responders - if (@_log_ALDB_devices) - { - my $log_ALDB_cnt = @_log_ALDB_devices; - my $count = 0; - foreach my $current_log_ALDB_device (@_log_ALDB_devices) - { - $count++; - if ($current_log_ALDB_device->is_root and - !($current_log_ALDB_device->isa('Insteon::InterfaceController'))) - { - &main::print_log("[log all device ALDB status] Now logging: " - . $current_log_ALDB_device->get_object_name() - . " ($count of $log_ALDB_cnt)"); - $current_log_ALDB_device->log_aldb_status(); - } else - { - main::print_log("[log all device ALDB status] INFO: !!! " - . $current_log_ALDB_device->get_object_name - . " is NOT a candidate for logging ($count of $log_ALDB_cnt)"); - } - } - main::print_log("[log all device ALDB status] All devices have completed logging"); - } else - { - main::print_log("[log all device ALDB status] WARN: No insteon devices could be found"); - } -} - -=item C - -Walks through every Insteon device and prints statistical information about -its message handling, as well as a summary average of the entire network. See -L -for more detailed information. - -This command adds the following extra data points: - -=back - -=over8 - -=item * - -Unk_Error - The number of messages which have arrived at the PLM which cannot -be associated with any know device. - -=back - -=over - -=cut - -sub print_all_message_stats -{ - my @_log_devices = (); - push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); - - if (@_log_devices) - { - #Initialize all of the tracking variables - my $retry_average = 0; - my $fail_percentage = 0; - my $corrupt_percentage = 0; - my $dupe_percentage = 0; - my $avg_hops_left = 0; - my $avg_max_hops = 0; - my $avg_out_hops = 0; - my $curr_hops_avg = 0; - - my $incoming_count_log = 0; - my $corrupt_count_log = 0; - my $dupe_count_log = 0; - my $retry_count_log = 0; - my $outgoing_count_log = 0; - my $fail_count_log = 0; - my $default_hop_count = 0; - my $hops_left_count = 0; - my $max_hops_count = 0; - my $outgoing_hop_count =0; - - my $device_count = 0; - - foreach my $current_log_device (@_log_devices) - { - #Skip non-root items - next unless $current_log_device->is_root; - - $device_count++; - - #Prints the Individual Message for the Device - $current_log_device->print_message_stats; - - #Add values for each device to the master count - $incoming_count_log += $current_log_device->incoming_count_log; - $corrupt_count_log += $current_log_device->corrupt_count_log; - $dupe_count_log += $current_log_device->dupe_count_log; - $retry_count_log += $current_log_device->retry_count_log; - $outgoing_count_log += $current_log_device->outgoing_count_log; - $fail_count_log += $current_log_device->fail_count_log; - $default_hop_count += $current_log_device->default_hop_count; - $hops_left_count += $current_log_device->hops_left_count; - $max_hops_count += $current_log_device->max_hops_count; - $outgoing_hop_count += $current_log_device->outgoing_hop_count; - } - - #Calculate the averages - $retry_average = sprintf("%.1f", ($retry_count_log / - $outgoing_count_log) + 1) if ($outgoing_count_log > 0); - $fail_percentage = sprintf("%.1f", ($fail_count_log / - $outgoing_count_log) * 100 ) if ($outgoing_count_log > 0); - $corrupt_percentage = sprintf("%.1f", ($corrupt_count_log / - $incoming_count_log) * 100 ) if ($incoming_count_log > 0); - $dupe_percentage = sprintf("%.1f", ($dupe_count_log / - $incoming_count_log) * 100 ) if ($incoming_count_log > 0); - $avg_hops_left = sprintf("%.1f", ($hops_left_count / - $incoming_count_log)) if ($incoming_count_log > 0); - $avg_max_hops = sprintf("%.1f", ($max_hops_count / - $incoming_count_log)) if ($incoming_count_log > 0); - $avg_out_hops = sprintf("%.1f", ($outgoing_hop_count / - $outgoing_count_log)) if ($outgoing_count_log > 0); - $curr_hops_avg = sprintf("%.1f", ($default_hop_count / - $device_count)) if ($device_count > 0); - ::print_log( - "[Insteon] Average Network Statistics:\n" - . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops Unk_Error\n" - . sprintf("%6s", $incoming_count_log) - . sprintf("%8s", $corrupt_count_log) - . sprintf("%8s", $corrupt_percentage . '%') - . sprintf("%6s", $dupe_count_log) - . sprintf("%8s", $dupe_percentage . '%') - . sprintf("%9s", $avg_hops_left) - . sprintf("%9s", $avg_max_hops) - . sprintf("%9s", $avg_max_hops - $avg_hops_left) - . sprintf("%10s", &Insteon::active_interface->corrupt_count_log) - . "\n" - . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" - . sprintf("%6s", $outgoing_count_log) - . sprintf("%8s", $fail_count_log) - . sprintf("%8s", $fail_percentage . '%') - . sprintf("%6s", $retry_count_log) - . sprintf("%8s", $retry_average) - . sprintf("%9s", $avg_out_hops) - . sprintf("%9s", $curr_hops_avg) - ); - main::print_log("[Insteon::Print_All_Message_Stats] All devices have completed logging"); - } else - { - main::print_log("[Insteon::Print_All_Message_Stats] WARN: No insteon devices could be found"); - } -} - -=item C - -Walks through every Insteon device and resets the statistical information about -its message handling. - -=cut - -sub reset_all_message_stats -{ - my @_log_devices = (); - &Insteon::active_interface->reset_message_stats; - push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); - - if (@_log_devices) - { - foreach my $current_log_device (@_log_devices) - { - $current_log_device->reset_message_stats - if $current_log_device->can('reset_message_stats'); - } - main::print_log("[Insteon::Reset_All_Message_Stats] All devices have been reset"); - } else - { - main::print_log("[Insteon::Reset_All_Message_Stats] WARN: No insteon devices could be found"); - } -} - -=item C - -Initiates the insteon stack, mostly just sets the trigger. - -=cut - -sub init { - - # only run once - return if $init_complete; - $init_complete = 1; - - # initialize scan and sync counters - $_scan_cnt = 0; - $_sync_cnt = 0; - @_scan_devices = (); - - ################################################################# - ## Trigger creation - ################################################################# - my ($trigger_event, $trigger_code, $trigger_type); - - my @trigger_info = &main::trigger_get('scan insteon link tables'); - if (@trigger_info) { - # Trigger exists; modify just the minimum so the trigger continues - # to work if we change the trigger code, but respect everything - # else (trigger type and time to run). This prevents unconditionally - # re-enabling the trigger if the user has disabled it. - $trigger_event = $trigger_info[0]; - $trigger_type = $trigger_info[2]; - } else { - # Trigger does not exist; create one with our default values. - $trigger_event = "time_cron '00 02 * * *'"; - $trigger_type = 'NoExpire'; - } - - $trigger_code = '&Insteon::scan_all_linktables()'; - - # Create/update trigger for a nightly link table scan - &main::trigger_set($trigger_event, $trigger_code, $trigger_type, - 'scan insteon link tables', 1); - ################################################################# - - @_insteon_plm = (); - @_insteon_device = (); - @_insteon_link = (); - -} - -=item C - -Generates and sets the voice commands for all Insteon devices. - -Note: At some point, this function will be pushed out to the specific classes -so that each class can have its own unique set of voice commands. - -=cut - -sub generate_voice_commands -{ - - &main::print_log("Generating Voice commands for all Insteon objects"); - my $object_string; - for my $object (&main::list_all_objects) { - next unless ref $object; - next unless $object->isa('Insteon::BaseInterface') or $object->isa('Insteon::BaseObject'); - - #get object name to use as part of variable in voice command - my $object_name = $object->get_object_name; - my $object_name_v = $object_name . '_v'; - $object_string .= "use vars '${object_name}_v';\n"; - - #Convert object name into readable voice command words - my $command = $object_name; - $command =~ s/^\$//; - $command =~ tr/_/ /; - - my $group = ($object->isa('Insteon_PLM')) ? '' : $object->group; - - #Get list of all voice commands from the object - my $voice_cmds = $object->get_voice_cmds(); - - #Initialize the voice command with all of the possible device commands - $object_string .= "$object_name_v = new Voice_Cmd '$command [" - . join(",", sort keys %$voice_cmds) . "]';\n"; - - #Tie the proper routine to each voice command - foreach (keys %$voice_cmds) { - $object_string .= "$object_name_v -> tie_event('" . $voice_cmds->{$_} - . "', '$_');\n\n"; - } - - #Add this object to the list of Insteon Voice Commands on the Web Interface - $object_string .= ::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_PLM_commands'); - } - - #Evaluate the resulting object generating string - package main; - eval $object_string; - print "Error in insteon_item_commands: $@\n" if $@; - package Insteon; -} - -=item C - -Adds object to the list of insteon objects that are managed by the stack. Makes -the object eligible for linking, scanning, and global functions. - -=cut - -sub add -{ - my ($object) = @_; - - my $insteon_manager = InsteonManager->instance(); - if ($insteon_manager->remove_item($object)) { - # print out debug info - } - $insteon_manager->add_item($object); -} - -=item C - -Called as a non-object routine. Returns the object named name. - -=cut - -sub find_members -{ - my ($name) = @_; - - my $insteon_manager = InsteonManager->instance(); - return $insteon_manager->find_members($name); -} - -=item C - -Returns the object identified by p_id and p_group. Where p_id is the 6 digit -hexadecimal address of the object without periods and group is a two digit -representation of the group number of the device. - -=cut - -sub get_object -{ - my ($p_deviceid, $p_group) = @_; - - my $retObj = undef; - - my $insteon_manager = InsteonManager->instance(); - my @search_objects = (); - push @search_objects, $insteon_manager->find_members('Insteon::BaseObject'); - for my $obj (@search_objects) - { - #Match on Insteon objects only - # if ($obj->isa("Insteon::Insteon_Device")) - # { - if (lc $obj->device_id() eq lc $p_deviceid) - { - if ($p_group) - { - if (lc $p_group eq lc $obj->group) - { - $retObj = $obj; - last; - } - } else { - $retObj = $obj; - last; - } - } - # } - } - - return $retObj; -} - -=item C - -Sets p_interface as the new active interface. Should likely only be called on -startup or reload. - -=cut - -sub active_interface -{ - my ($interface) = @_; - my $insteon_manager = InsteonManager->instance(); - - $insteon_manager->_active_interface($interface) - if $interface && ref $interface && $interface->isa('Insteon::BaseInterface'); -#print "############### active interface is: " . $insteon_manager->_active_interface->get_object_name . "\n"; - return $insteon_manager->_active_interface; - -} - -=item C - -Walks through every Insteon device and checks the aldb object version for I1 vs. I2 - -=cut - -sub check_all_aldb_versions -{ - main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices") if ($main::Debug{insteon} >= 4); - - my @ALDB_devices = (); - push @ALDB_devices, Insteon::find_members("Insteon::BaseDevice"); - my $ALDB_cnt = @ALDB_devices; - my $count = 0; - foreach my $ALDB_device (@ALDB_devices) - { - $count++; - if ($ALDB_device->is_root and - !($ALDB_device->isa('Insteon::InterfaceController'))) - { - main::print_log("[Insteon] DEBUG4 Checking aldb version for " - . $ALDB_device->get_object_name() - . " ($count of $ALDB_cnt)") if ($ALDB_device->debuglevel(4, 'insteon')); - $ALDB_device->check_aldb_version(); - } else - { - main::print_log("[Insteon] DEBUG4 " . $ALDB_device->get_object_name - . " does not have its own aldb ($count of $ALDB_cnt)") - if ($ALDB_device->debuglevel(4, 'insteon')); - } - } - main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); -} - -sub check_thermo_versions -{ - main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if ($main::Debug{insteon} >= 4); - - my @thermo_devices = (); - push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); - foreach my $thermo_device (@thermo_devices) - { - if ($thermo_device->isa('Insteon::Thermostat') && - $thermo_device->get_root()->engine_version eq "I2CS"){ - main::print_log("[Insteon] DEBUG4 Setting thermostat " - . $thermo_device->get_object_name() . " to i2CS") - if ($thermo_device->debuglevel(4, 'insteon')); - bless $thermo_device, 'Insteon::Thermo_i2CS'; - $thermo_device->init(); - } - else { - main::print_log("[Insteon] DEBUG4 Setting thermostat " - . $thermo_device->get_object_name() . " to i1") - if ($thermo_device->debuglevel(4, 'insteon')); - bless $thermo_device, 'Insteon::Thermo_i1'; - } - } -} - -=back - -=head2 INI PARAMETERS - -=over - -=item insteon_menu_states - -A comma seperated list of states that will be added as voice commands to dimmable -devices. - -=back - -=head2 AUTHOR - -Gregg Limming, Kevin Robert Keegan, Micheal Stovenour, many others - -=head2 LICENSE - -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 B - -=head2 DESCRIPTION - -Provides the basic infrastructure for the Insteon stack, contains many of the -startup routines. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package InsteonManager; - -use strict; -use base 'Class::Singleton'; - -=item C<_new_instance()> - -Defines a new instance of the class. - -=cut - -sub _new_instance -{ - my $class = shift; - my $self = bless {}, $class; - - return $self; -} - -=item C<_active_interface()> - -Sets and returns the active interface. Likely should only be caled on startup -or reload. It also sets all of the hooks for the Insteon stack. - -=cut - -sub _active_interface -{ - my ($self, $interface) = @_; - # setup hooks the first time that an interface is made active - if (!($$self{active_interface}) and $interface) { - &main::print_log("[Insteon] Setting up initialization hooks") if $main::Debug{insteon}; - &main::MainLoop_pre_add_hook(\&Insteon::BaseInterface::check_for_data, 1); - &main::Reload_post_add_hook(\&Insteon::check_all_aldb_versions, 1); - &main::Reload_post_add_hook(\&Insteon::BaseInterface::poll_all, 1); - $init_complete = 0; - &main::MainLoop_pre_add_hook(\&Insteon::init, 1); - &main::Reload_post_add_hook(\&Insteon::check_thermo_versions, 1); - &main::Reload_post_add_hook(\&Insteon::generate_voice_commands, 1); - } - $$self{active_interface} = $interface if $interface; - return $$self{active_interface}; -} - -=item C - -Adds a list of objects to be tracked. - -=cut - -sub add -{ - my ($self,@p_objects) = @_; - - my @l_objects; - - for my $l_object (@p_objects) { - if ($l_object->isa('Group_Item') ) { - @l_objects = $$l_object{members}; - for my $obj (@l_objects) { - $self->add($obj); - } - } else { - $self->add_item($l_object); - } - } -} - -=item C - -Adds an object to be tracked. - -=cut - -sub add_item -{ - my ($self,$p_object) = @_; - - push @{$$self{objects}}, $p_object; - if ($p_object->isa('Insteon::BaseInterface')) { - $self->_active_interface($p_object); - } - return $p_object; -} - -=item C - -Removes all of the Insteon objects. - -=cut - -sub remove_all_items { - my ($self) = @_; - - if (ref $$self{objects}) { - foreach (@{$$self{objects}}) { - # $_->untie_items($self); - } - } - delete $self->{objects}; -} - -=item C - -Adds an item to be tracked if it is not already in the list. - -=cut - -sub add_item_if_not_present { - my ($self, $p_object) = @_; - - if (ref $$self{objects}) { - foreach (@{$$self{objects}}) { - if ($_->equals($p_object)) { - return 0; - } - } - } - $self->add_item($p_object); - return 1; -} - -=item C - -Removes the Insteon object. - -=cut - -sub remove_item { - my ($self, $p_object) = @_; - return 0 unless $p_object and ref $p_object; - if (ref $$self{objects}) { - for (my $i = 0; $i < scalar(@{$$self{objects}}); $i++) { - if ($p_object->equals($$self{objects}->[$i])) { - splice @{$$self{objects}}, $i, 1; - return 1; - } - } - } - return 0; -} - -=item C - -Returns true if object is in the list. - -=cut - -sub is_member { - my ($self, $p_object) = @_; - - my @l_objects = @{$$self{objects}}; - for my $l_object (@l_objects) { - if ($l_object->equals($p_object)) { - return 1; - } - } - return 0; -} - -=item C - -Find and return all tracked objects of type p_type where p_type is an object -class. - -=cut - -sub find_members { - my ($self,$p_type) = @_; - - my @l_found; - my @l_objects = @{$$self{objects}}; - for my $l_object (@l_objects) { - if ($l_object->isa($p_type)) { - push @l_found, $l_object; - } - } - return @l_found; -} - -=back - -=head2 INI PARAMETERS - -=over - -=item C - -For debugging debug=insteon or debug=insteon:level where level is 1-4. - -=back - -=head2 AUTHOR - -Bruce Winter, Gregg Liming, Kevin Robert Keegan, Michael Stovenour, many others - -=head2 SEE ALSO - -None - -=head2 LICENSE - -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. - -=cut - - -1; +package Insteon; + +use strict; + +# Category=Insteon + +#@ This module creates voice commands for all insteon related items. + +=head1 B + +=head2 DESCRIPTION + +Provides the basic infrastructure for the Insteon stack, contains many of the +startup routines. + +=head2 INHERITS + +None + +=head2 VOICE COMMANDS + +=head3 PLM + +=over + +=item C + +If a device is first placed into linking mode, calling this command will cause +the PLM to complete the link, thus making the PLM the responder. The +C device voice command is likely an easier way to do this, +but this may be need for hard to reach devices or deaf devices. + +=item C + +Call this first, then press and hold the set button on a device that you wish +to have the PLM control. The C device voice command is +likely an easier way to do this, but this may be need for hard to reach devices +or deaf devices. This is also needed for i2cs devices in which the first link +must currently be manually created this way. + +=item C + +Cancel either of the above two commands without completing a link. + +=item C + +This does nothing and shoudl be removed. + +=item C + +This will scan and output to the log only the PLM link table. + +=item C + +This will output only the PLM link table to log. + +=item C + +Misterhouse will review the state of all of the links in your system, as it knows +them without any additional scanning. If any of these links are not defined in +your mht file or the links are only half links (controller with no responder or +vice versa) MisterHouse will delete these links. + +It is usually best to: + +1. Run C first unless you know that the +information in MisterHouse is up-to-date. + +2. Run C and verify that what is being added is correct. + +3. Run C to add the links + +4. Run C first to see what will happen. + +5. If everything looks right, run C to clean up the old links + +Deleting the orphan links will make your devices happier. If you have unintended +links on your devices, they can run slower and may unnecessarily increase the +number of messages sent on your network. + +=item C + +Does the same thing as C but doesn't actually delete anything +instead it just prints what it would have done to the log. + +=item C + +Scans the link tables of the PLM and all devices on your network. On a large +network this can take sometime. You can generally run C +which is much faster without any issue. + +=item C + +Scans the link tables of the PLM and all devices whose link tables have changed +on your network. + +=item C + +Similar to C exccept this adds any links that are missing. +This is helpful when adding a bunch of new devices, new scenes, or cleaning things +up. + +See the workflow described in C. + +=item C + +Same as C but prints what it would do to the log, without doing +anything else. + +=item C + +Prints the message stats for all devices plus a summary of the entire network +stats. For full details on what is contained in this printout please see +the description for the C voice command under the device +heading below. + +=item C + +Resets all the message stats for all devices including the Unk_Error stat. + +=item C + +Performs a 5 count stress test on all devices on the network. See the description +of what a stress test is under the device voice commands. + +=item C + +Performs a 5 count ping test on all devices on the network. See the description +of what a ping test is under the device voice commands. + +=item C + +Logs some details about each device to the log. See C + +=back + +=head3 Devices + +=over + +=item C + +Turns the device on. + +=item C + +Turns the device off. + +=item C + +Similar to C above, but this will only add links that are related +to this device. Useful when adding a new device. + +=item C + +Will create the controller/responder links between the device and the PLM. + +=item C + +Will delete the controller/responder links between the device and the PLM. +Useful if you are removing a device from your network. + +=item C + +Requests the status of the device. + +=item C + +Requests the engine version of the device. Generally you would not need to call +this, but every now and then it is needed when a new device is installed. + +=item C + +This will scan and output to the log only the link table of this device. + +=item C + +Will output to the log only the link table of this device. + +=item C + +Generally only available for PLM Scenes. This places the PLM in linking mode +and adds any device which the set button is pressed for 4 seconds as a responder +to this scene. Generally not needed. + +=item C + +Cancels the above linking session without creating a link. + +=item C + +Simulates a read of a 5 link addresses from the device. This routine is meant to +be used as a diagnostic tool. + +This is also similar to the C test, however, rather than simply requesting +an ACK, this requests a full set of data equivalent to a link entry. Similar to +C this should be used with C to diagnose issues and try +different settings. + +=item C + +Sends 5 ping messages to the device. A ping message is a basic +message that simply asks the device to respond with an ACKnowledgement. For +i1 devices this will send a standard length command, for i2 and i2cs devices +this will send an extended ping command. In both cases, the device responds +back with a standard length ACKnowledgement only. + +Much like the ping command in IP networks, this command is useful for testing the +connectivity of a device on your network. You likely want to use this in +conjunction with the C routine. For example, you can use +this to compare the message stats for a device when changing settings in +MisterHouse. + +=item C + +Prints message statistics for this device to the print log. + +=back + +=over8 + +=item * + +In - The number of incoming messages received + +=item * + +Corrupt - The number of incoming corrupt messages received + +=item * + +%Corrpt - Of the incoming messages received, the percentage that were +corrupt + +=item * + +Dupe - The number of duplicate messages that have been received from this +device. + +=item * + +%Dupe - The percentage of duplilicate incoming messages received. + +=item * + +Hops_Left - The average hops left in the messages received from this device. + +=item * + +Max_Hops - The average maximum hops in the messages received from this device. + +=item * + +Act_Hops - Max_Hops - Hops_Left, this is the average number of hops that have +been required for a message sent from the device to reach MisterHouse. + +=item * + +Out - The number of unique outgoing messages, without retries, sent. + +=item * + +Fail - The number times that all retries were exhausted without a successful +delivery of a message. + +=item * + +%Fail - Of the outgoing messages sent, the percentage that failed. + +=item * + +Retry - The number of retry attempts that have been made to deliver a message. +Ideally this is 0, but Sends/Msg is a better indication of this parameter. + +=item * + +AvgSend - The average number of send attempts that must be made in order to +successfully deliver a message. Ideally this would be 1.0. + +NOTE: If the number of retries exceeds the value set in the configuration file +for Insteon_retry_count, MisterHouse will abandon sending the message. As a +result, as this number approaches Insteon_retry_count it becomes a less accurate +representation of the number of retries needed to reach a device. + +=item * + +Avg_Hops - The average number of hops that have been used by MisterHouse when +sending messages to this device. + +=item * + +Hop_Count - The current hop count being used by MH. This count is dynamically +controlled by MH and is not reset by calling C + +=back + +=over + +=item C + +Resets the message stats back to 0 for this device. + +=back + +=head2 METHODS + +=over + +=cut + + +my (@_insteon_plm,@_insteon_device,@_insteon_link,@_scannable_link,$_scan_cnt,$_sync_cnt,$_sync_failure_cnt); +my $init_complete; +my (@_scan_devices,@_scan_device_failures,$current_scan_device); +my (@_sync_devices,@_sync_device_failures,$current_sync_device); +my ($_stress_test_count, $_stress_test_one_pass, @_stress_test_devices); +my ($_ping_count, @_ping_devices); + +=item C + +Sequentially goes through every Insteon device and performs a stress_test on it. +See L +for a more detailed description of stress_test. + +Parameters: + Count: defines the number of stress_tests to perform on each device. + is_one_pass: if true, all stress_tests will be performed on a device + before proceeding to the next device. if false, the routine + loops through all devices performing one stress_test on each + device before moving on to the next device. + +=cut + +sub stress_test_all +{ + my ($p_count, $is_one_pass) = @_; + if (defined $p_count){ + $_stress_test_count = $p_count; + $_stress_test_one_pass = $is_one_pass; + @_stress_test_devices = undef; + push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); + main::print_log("[Insteon::Stress Test All Devices] Stress Testing All Devices $p_count times"); + }; + if (!@_stress_test_devices) { + #Iteration may be complete, start over from the beginning + $_stress_test_count = ($_stress_test_one_pass) ? 0 : $_stress_test_count--; + push @_stress_test_devices, Insteon::find_members("Insteon::BaseDevice"); + } + if ($_stress_test_count > 0){ + my $current_stress_test_device; + my $complete_callback = '&Insteon::stress_test_all()'; + while (@_stress_test_devices){ + $current_stress_test_device = pop @_stress_test_devices; + next unless $current_stress_test_device->is_root(); + next unless $current_stress_test_device->is_responder(); + last; + } + my $run_count = ($_stress_test_one_pass) ? $_stress_test_count : 1; + if (ref $current_stress_test_device && $current_stress_test_device->can('stress_test')){ + $current_stress_test_device->stress_test($run_count, $complete_callback); + } + } + else { + $_stress_test_one_pass = 0; + main::print_log("[Insteon::Stress Test All Devices] Complete"); + } +} + +=item C + +Walks through every Insteon device calling the device's scan links command. +Does not output anything but will recreate the device's aldb from the actual +entries in the device. + +=cut + +sub scan_all_linktables +{ + my $skip_unchanged = pop(@_); + $skip_unchanged = 0 if (ref $skip_unchanged || !defined($skip_unchanged)); + if ($current_scan_device) + { + &main::print_log("[Scan all linktables] WARN: link already underway. Ignoring request for new scan ..."); + return; + } + my @candidate_devices = (); + # clear @_scan_devices + @_scan_devices = (); + @_scan_device_failures = (); + $current_scan_device = undef; + # alwayws include the active interface (e.g., plm) + push @_scan_devices, &Insteon::active_interface; + + push @candidate_devices, &Insteon::find_members("Insteon::BaseDevice"); + + # don't try to scan devices that are not responders + if (@candidate_devices) + { + foreach (@candidate_devices) + { + my $candidate_object = $_; + if ($candidate_object->is_root and + !($candidate_object->is_deaf + or $candidate_object->isa('Insteon::InterfaceController'))) + { + push @_scan_devices, $candidate_object; + &main::print_log("[Scan all linktables] INFO1: " + . $candidate_object->get_object_name + . " will be scanned.") if $candidate_object->debuglevel(1, 'insteon'); + } + else + { + &main::print_log("[Scan all linktables] INFO: !!! " + . $candidate_object->get_object_name + . " is NOT a candidate for scanning."); + } + } + } + else + { + &main::print_log("[Scan all linktables] WARN: No insteon devices could be found"); + } + $_scan_cnt = scalar @_scan_devices; + + &_get_next_linkscan($skip_unchanged); +} + +=item C<_get_next_linkscan_failure()> + +Called if a the scanning of a device fails. Logs the failure and proceeds to +the next device. + +=cut + +sub _get_next_linkscan_failure +{ + my($skip_unchanged) = @_; + push @_scan_device_failures, $current_scan_device; + &main::print_log("[Scan all link tables] WARN: failure occurred when scanning " + . $current_scan_device->get_object_name . ". Moving on..."); + &_get_next_linkscan($skip_unchanged); + +} + +=item C<_get_next_linkscan()> + +Gets the next device to scan. + +=cut + +sub _get_next_linkscan +{ + my($skip_unchanged, $changed_device) = @_; + my $checking = 0; + if (!defined($changed_device)) { + $current_scan_device = shift @_scan_devices; + if ($skip_unchanged && $current_scan_device && ($current_scan_device != &Insteon::active_interface)){ + ## check if aldb_delta has changed; + $current_scan_device->_aldb->{_aldb_unchanged_callback} = '&Insteon::_get_next_linkscan('.$skip_unchanged.')'; + $current_scan_device->_aldb->{_aldb_changed_callback} = '&Insteon::_get_next_linkscan('.$skip_unchanged.', '.$current_scan_device->get_object_name.')'; + $current_scan_device->_aldb->{_failure_callback} = '&Insteon::_get_next_linkscan_failure('.$skip_unchanged.')'; + $current_scan_device->_aldb->query_aldb_delta("check"); + $checking = 1; + } + } else { + $current_scan_device = $changed_device; + } + if ($current_scan_device && ($checking == 0)) + { + &main::print_log("[Scan all link tables] Now scanning: " + . $current_scan_device->get_object_name . " (" + . ($_scan_cnt - scalar @_scan_devices) + . " of $_scan_cnt)"); + # pass first the success callback followed by the failure callback + $current_scan_device->scan_link_table('&Insteon::_get_next_linkscan('.$skip_unchanged.')','&Insteon::_get_next_linkscan_failure('.$skip_unchanged.')'); + } elsif (scalar(@_scan_devices) == 0 && ($checking == 0)) + { + &main::print_log("[Scan all link tables] All tables have completed scanning"); + my $_scan_failure_cnt = scalar @_scan_device_failures; + if ($_scan_failure_cnt){ + my $obj_list; + for my $failed_obj (@_scan_device_failures){ + $obj_list .= $failed_obj->get_object_name .", "; + } + ::print_log("[Scan all link tables] However, some failures " + ."were noted with the following devices: $obj_list"); + } + } +} + +=item C + +Initiates a process that will walk through every device that is a Insteon::InterfaceController +calling the device's sync_links() command. sync_all_links() loads up the module +global variable @_sync_devices then kicks off the recursive call backs by calling +_get_next_linksync. + +Paramter B - Causes sync to walk through but not actually +send any commands to the devices. Useful with the insteon:3 debug setting for +troubleshooting. + +=cut + +sub sync_all_links +{ + my ($audit_mode) = @_; + &main::print_log("[Sync all links] Starting now!"); + @_sync_devices = (); + # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links + for my $obj (&Insteon::find_members('Insteon::BaseController')) + { + my %sync_req = ('sync_object' => $obj, 'audit_mode' => ($audit_mode) ? 1 : 0); + &main::print_log("[Sync all links] Adding " . $obj->get_object_name + . " to sync queue"); + push @_sync_devices, \%sync_req + } + + $_sync_cnt = scalar @_sync_devices; + + &_get_next_linksync(); +} + +=item C<_get_next_linksync()> + +Calls the sync_links() function for each device in the module global variable +@_sync_devices. This function will be called recursively since the callback +passed to sync_links() is this function again. Will also ask sync_links() to +call _get_next_linksync_failure() if sync_links() fails. + +=cut + +sub _get_next_linksync +{ + $current_scan_device = shift @_scan_devices; + my $sync_req_ptr = shift(@_sync_devices); + my %sync_req = ($sync_req_ptr) ? %$sync_req_ptr : undef; + if (%sync_req) + { + + $current_sync_device = $sync_req{'sync_object'}; + } + else + { + $current_sync_device = undef; + } + + if ($current_sync_device) + { + &main::print_log("[Sync all links] Now syncing: " + . $current_sync_device->get_object_name . " (" + . ($_sync_cnt - scalar @_sync_devices) + . " of $_sync_cnt)"); + my $skip_deaf = 1; + # pass first the success callback followed by the failure callback + $current_sync_device->sync_links($sync_req{'audit_mode'}, '&Insteon::_get_next_linksync()','&Insteon::_get_next_linksync_failure()', $skip_deaf); + } + else + { + &main::print_log("[Sync all links] All links have completed syncing"); + my $_sync_failure_cnt = scalar @_sync_device_failures; + if ($_sync_failure_cnt){ + my $obj_list; + for my $failed_obj (@_sync_device_failures){ + $obj_list .= $failed_obj->get_object_name .", "; + } + ::print_log("[Sync all links] WARN! Failures occured, " + ."some links involving the following objects remain out-of-sync: $obj_list"); + } + } + +} + +=item C<_get_next_linksync()> + +Called by the failure callback in a device's sync_links() function. Will add +the failed device to the module global variable @_sync_device_failures. + +=cut + +sub _get_next_linksync_failure +{ + push @_sync_device_failures, $current_sync_device + unless (grep{$current_sync_device == $_} @_sync_device_failures); + &main::print_log("[Sync all links] WARN: failure occurred when syncing links for " + . $current_sync_device->get_object_name . ". Resuming sync queue if it exists."); + my $num_sync_queue = @{$$current_sync_device{sync_queue}}; + if ($num_sync_queue){ + $current_sync_device->_process_sync_queue(); + } + else { #No other pending links in the queue + &_get_next_linksync(); + } + +} + +=item C + +Walks through every Insteon device and pings it as many times as defined by +count. See L +for a more detailed description of ping. + +=cut + +sub ping_all +{ + my ($p_count) = @_; + if (defined $p_count){ + $_ping_count = $p_count; + @_ping_devices = (); + push @_ping_devices, Insteon::find_members("Insteon::BaseDevice"); + main::print_log("[Insteon::Ping All Devices] Ping All Devices $p_count times"); + } + if (@_ping_devices) + { + my $current_ping_device; + while(@_ping_devices) + { + $current_ping_device = pop @_ping_devices; + next unless $current_ping_device->is_root(); + next unless $current_ping_device->is_responder(); + last; + } + $current_ping_device->ping($_ping_count, '&Insteon::ping_all()') + if $current_ping_device->can('ping'); + } else + { + $_ping_count = 0; + main::print_log("[Insteon::Ping All Devices] Ping All Complete"); + } +} + +=item C + +Walks through every Insteon device and logs: + +=back + +=over8 + +=item * + +Hop Count + +=item * + +Engine Version + +=item * + +ALDB Type + +=item * + +ALDB Health + +=item * + +ALDB Scan Time + +=back + +=over + +=cut + +sub log_all_ADLB_status +{ + my @_log_ALDB_devices = (); + # alwayws include the active interface (e.g., plm) +# push @_log_ALDB_devices, &Insteon::active_interface; + + push @_log_ALDB_devices, Insteon::find_members("Insteon::BaseDevice"); + + # don't try to scan devices that are not responders + if (@_log_ALDB_devices) + { + my $log_ALDB_cnt = @_log_ALDB_devices; + my $count = 0; + foreach my $current_log_ALDB_device (@_log_ALDB_devices) + { + $count++; + if ($current_log_ALDB_device->is_root and + !($current_log_ALDB_device->isa('Insteon::InterfaceController'))) + { + &main::print_log("[log all device ALDB status] Now logging: " + . $current_log_ALDB_device->get_object_name() + . " ($count of $log_ALDB_cnt)"); + $current_log_ALDB_device->log_aldb_status(); + } else + { + main::print_log("[log all device ALDB status] INFO: !!! " + . $current_log_ALDB_device->get_object_name + . " is NOT a candidate for logging ($count of $log_ALDB_cnt)"); + } + } + main::print_log("[log all device ALDB status] All devices have completed logging"); + } else + { + main::print_log("[log all device ALDB status] WARN: No insteon devices could be found"); + } +} + +=item C + +Walks through every Insteon device and prints statistical information about +its message handling, as well as a summary average of the entire network. See +L +for more detailed information. + +This command adds the following extra data points: + +=back + +=over8 + +=item * + +Unk_Error - The number of messages which have arrived at the PLM which cannot +be associated with any know device. + +=back + +=over + +=cut + +sub print_all_message_stats +{ + my @_log_devices = (); + push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); + + if (@_log_devices) + { + #Initialize all of the tracking variables + my $retry_average = 0; + my $fail_percentage = 0; + my $corrupt_percentage = 0; + my $dupe_percentage = 0; + my $avg_hops_left = 0; + my $avg_max_hops = 0; + my $avg_out_hops = 0; + my $curr_hops_avg = 0; + + my $incoming_count_log = 0; + my $corrupt_count_log = 0; + my $dupe_count_log = 0; + my $retry_count_log = 0; + my $outgoing_count_log = 0; + my $fail_count_log = 0; + my $default_hop_count = 0; + my $hops_left_count = 0; + my $max_hops_count = 0; + my $outgoing_hop_count =0; + + my $device_count = 0; + + foreach my $current_log_device (@_log_devices) + { + #Skip non-root items + next unless $current_log_device->is_root; + + $device_count++; + + #Prints the Individual Message for the Device + $current_log_device->print_message_stats; + + #Add values for each device to the master count + $incoming_count_log += $current_log_device->incoming_count_log; + $corrupt_count_log += $current_log_device->corrupt_count_log; + $dupe_count_log += $current_log_device->dupe_count_log; + $retry_count_log += $current_log_device->retry_count_log; + $outgoing_count_log += $current_log_device->outgoing_count_log; + $fail_count_log += $current_log_device->fail_count_log; + $default_hop_count += $current_log_device->default_hop_count; + $hops_left_count += $current_log_device->hops_left_count; + $max_hops_count += $current_log_device->max_hops_count; + $outgoing_hop_count += $current_log_device->outgoing_hop_count; + } + + #Calculate the averages + $retry_average = sprintf("%.1f", ($retry_count_log / + $outgoing_count_log) + 1) if ($outgoing_count_log > 0); + $fail_percentage = sprintf("%.1f", ($fail_count_log / + $outgoing_count_log) * 100 ) if ($outgoing_count_log > 0); + $corrupt_percentage = sprintf("%.1f", ($corrupt_count_log / + $incoming_count_log) * 100 ) if ($incoming_count_log > 0); + $dupe_percentage = sprintf("%.1f", ($dupe_count_log / + $incoming_count_log) * 100 ) if ($incoming_count_log > 0); + $avg_hops_left = sprintf("%.1f", ($hops_left_count / + $incoming_count_log)) if ($incoming_count_log > 0); + $avg_max_hops = sprintf("%.1f", ($max_hops_count / + $incoming_count_log)) if ($incoming_count_log > 0); + $avg_out_hops = sprintf("%.1f", ($outgoing_hop_count / + $outgoing_count_log)) if ($outgoing_count_log > 0); + $curr_hops_avg = sprintf("%.1f", ($default_hop_count / + $device_count)) if ($device_count > 0); + ::print_log( + "[Insteon] Average Network Statistics:\n" + . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops Unk_Error\n" + . sprintf("%6s", $incoming_count_log) + . sprintf("%8s", $corrupt_count_log) + . sprintf("%8s", $corrupt_percentage . '%') + . sprintf("%6s", $dupe_count_log) + . sprintf("%8s", $dupe_percentage . '%') + . sprintf("%9s", $avg_hops_left) + . sprintf("%9s", $avg_max_hops) + . sprintf("%9s", $avg_max_hops - $avg_hops_left) + . sprintf("%10s", &Insteon::active_interface->corrupt_count_log) + . "\n" + . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" + . sprintf("%6s", $outgoing_count_log) + . sprintf("%8s", $fail_count_log) + . sprintf("%8s", $fail_percentage . '%') + . sprintf("%6s", $retry_count_log) + . sprintf("%8s", $retry_average) + . sprintf("%9s", $avg_out_hops) + . sprintf("%9s", $curr_hops_avg) + ); + main::print_log("[Insteon::Print_All_Message_Stats] All devices have completed logging"); + } else + { + main::print_log("[Insteon::Print_All_Message_Stats] WARN: No insteon devices could be found"); + } +} + +=item C + +Walks through every Insteon device and resets the statistical information about +its message handling. + +=cut + +sub reset_all_message_stats +{ + my @_log_devices = (); + &Insteon::active_interface->reset_message_stats; + push @_log_devices, Insteon::find_members("Insteon::BaseDevice"); + + if (@_log_devices) + { + foreach my $current_log_device (@_log_devices) + { + $current_log_device->reset_message_stats + if $current_log_device->can('reset_message_stats'); + } + main::print_log("[Insteon::Reset_All_Message_Stats] All devices have been reset"); + } else + { + main::print_log("[Insteon::Reset_All_Message_Stats] WARN: No insteon devices could be found"); + } +} + +=item C + +Initiates the insteon stack, mostly just sets the trigger. + +=cut + +sub init { + + # only run once + return if $init_complete; + $init_complete = 1; + + # initialize scan and sync counters + $_scan_cnt = 0; + $_sync_cnt = 0; + @_scan_devices = (); + + ################################################################# + ## Trigger creation + ################################################################# + my ($trigger_event, $trigger_code, $trigger_type); + + my @trigger_info = &main::trigger_get('scan insteon link tables'); + if (@trigger_info) { + # Trigger exists; modify just the minimum so the trigger continues + # to work if we change the trigger code, but respect everything + # else (trigger type and time to run). This prevents unconditionally + # re-enabling the trigger if the user has disabled it. + $trigger_event = $trigger_info[0]; + $trigger_type = $trigger_info[2]; + } else { + # Trigger does not exist; create one with our default values. + $trigger_event = "time_cron '00 02 * * *'"; + $trigger_type = 'NoExpire'; + } + + $trigger_code = '&Insteon::scan_all_linktables()'; + + # Create/update trigger for a nightly link table scan + &main::trigger_set($trigger_event, $trigger_code, $trigger_type, + 'scan insteon link tables', 1); + ################################################################# + + @_insteon_plm = (); + @_insteon_device = (); + @_insteon_link = (); + +} + +=item C + +Generates and sets the voice commands for all Insteon devices. + +Note: At some point, this function will be pushed out to the specific classes +so that each class can have its own unique set of voice commands. + +=cut + +sub generate_voice_commands +{ + + &main::print_log("Generating Voice commands for all Insteon objects"); + my $object_string; + for my $object (&main::list_all_objects) { + next unless ref $object; + next unless $object->isa('Insteon::BaseInterface') or $object->isa('Insteon::BaseObject'); + + #get object name to use as part of variable in voice command + my $object_name = $object->get_object_name; + my $object_name_v = $object_name . '_v'; + $object_string .= "use vars '${object_name}_v';\n"; + + #Convert object name into readable voice command words + my $command = $object_name; + $command =~ s/^\$//; + $command =~ tr/_/ /; + + my $group = ($object->isa('Insteon_PLM')) ? '' : $object->group; + + #Get list of all voice commands from the object + my $voice_cmds = $object->get_voice_cmds(); + + #Initialize the voice command with all of the possible device commands + $object_string .= "$object_name_v = new Voice_Cmd '$command [" + . join(",", sort keys %$voice_cmds) . "]';\n"; + + #Tie the proper routine to each voice command + foreach (keys %$voice_cmds) { + $object_string .= "$object_name_v -> tie_event('" . $voice_cmds->{$_} + . "', '$_');\n\n"; + } + + #Add this object to the list of Insteon Voice Commands on the Web Interface + $object_string .= ::store_object_data($object_name_v, 'Voice_Cmd', 'Insteon', 'Insteon_PLM_commands'); + } + + #Evaluate the resulting object generating string + package main; + eval $object_string; + print "Error in insteon_item_commands: $@\n" if $@; + package Insteon; +} + +=item C + +Adds object to the list of insteon objects that are managed by the stack. Makes +the object eligible for linking, scanning, and global functions. + +=cut + +sub add +{ + my ($object) = @_; + + my $insteon_manager = InsteonManager->instance(); + if ($insteon_manager->remove_item($object)) { + # print out debug info + } + $insteon_manager->add_item($object); +} + +=item C + +Called as a non-object routine. Returns the object named name. + +=cut + +sub find_members +{ + my ($name) = @_; + + my $insteon_manager = InsteonManager->instance(); + return $insteon_manager->find_members($name); +} + +=item C + +Returns the object identified by p_id and p_group. Where p_id is the 6 digit +hexadecimal address of the object without periods and group is a two digit +representation of the group number of the device. + +=cut + +sub get_object +{ + my ($p_deviceid, $p_group) = @_; + + my $retObj = undef; + + my $insteon_manager = InsteonManager->instance(); + my @search_objects = (); + push @search_objects, $insteon_manager->find_members('Insteon::BaseObject'); + for my $obj (@search_objects) + { + #Match on Insteon objects only + # if ($obj->isa("Insteon::Insteon_Device")) + # { + if (lc $obj->device_id() eq lc $p_deviceid) + { + if ($p_group) + { + if (lc $p_group eq lc $obj->group) + { + $retObj = $obj; + last; + } + } else { + $retObj = $obj; + last; + } + } + # } + } + + return $retObj; +} + +=item C + +Sets p_interface as the new active interface. Should likely only be called on +startup or reload. + +=cut + +sub active_interface +{ + my ($interface) = @_; + my $insteon_manager = InsteonManager->instance(); + + $insteon_manager->_active_interface($interface) + if $interface && ref $interface && $interface->isa('Insteon::BaseInterface'); +#print "############### active interface is: " . $insteon_manager->_active_interface->get_object_name . "\n"; + return $insteon_manager->_active_interface; + +} + +=item C + +Walks through every Insteon device and checks the aldb object version for I1 vs. I2 + +=cut + +sub check_all_aldb_versions +{ + main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices") if ($main::Debug{insteon} >= 4); + + my @ALDB_devices = (); + push @ALDB_devices, Insteon::find_members("Insteon::BaseDevice"); + my $ALDB_cnt = @ALDB_devices; + my $count = 0; + foreach my $ALDB_device (@ALDB_devices) + { + $count++; + if ($ALDB_device->is_root and + !($ALDB_device->isa('Insteon::InterfaceController'))) + { + main::print_log("[Insteon] DEBUG4 Checking aldb version for " + . $ALDB_device->get_object_name() + . " ($count of $ALDB_cnt)") if ($ALDB_device->debuglevel(4, 'insteon')); + $ALDB_device->check_aldb_version(); + } else + { + main::print_log("[Insteon] DEBUG4 " . $ALDB_device->get_object_name + . " does not have its own aldb ($count of $ALDB_cnt)") + if ($ALDB_device->debuglevel(4, 'insteon')); + } + } + main::print_log("[Insteon] DEBUG4 Checking aldb version of all devices completed") if ($main::Debug{insteon} >= 4); +} + +sub check_thermo_versions +{ + main::print_log("[Insteon] DEBUG4 Initializing thermostat versions") if ($main::Debug{insteon} >= 4); + + my @thermo_devices = (); + push @thermo_devices, Insteon::find_members("Insteon::Thermostat"); + foreach my $thermo_device (@thermo_devices) + { + if ($thermo_device->isa('Insteon::Thermostat') && + $thermo_device->get_root()->engine_version eq "I2CS"){ + main::print_log("[Insteon] DEBUG4 Setting thermostat " + . $thermo_device->get_object_name() . " to i2CS") + if ($thermo_device->debuglevel(4, 'insteon')); + bless $thermo_device, 'Insteon::Thermo_i2CS'; + $thermo_device->init(); + } + else { + main::print_log("[Insteon] DEBUG4 Setting thermostat " + . $thermo_device->get_object_name() . " to i1") + if ($thermo_device->debuglevel(4, 'insteon')); + bless $thermo_device, 'Insteon::Thermo_i1'; + } + } +} + +=back + +=head2 INI PARAMETERS + +=over + +=item insteon_menu_states + +A comma seperated list of states that will be added as voice commands to dimmable +devices. + +=back + +=head2 AUTHOR + +Gregg Limming, Kevin Robert Keegan, Micheal Stovenour, many others + +=head2 LICENSE + +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 B + +=head2 DESCRIPTION + +Provides the basic infrastructure for the Insteon stack, contains many of the +startup routines. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package InsteonManager; + +use strict; +use base 'Class::Singleton'; + +=item C<_new_instance()> + +Defines a new instance of the class. + +=cut + +sub _new_instance +{ + my $class = shift; + my $self = bless {}, $class; + + return $self; +} + +=item C<_active_interface()> + +Sets and returns the active interface. Likely should only be caled on startup +or reload. It also sets all of the hooks for the Insteon stack. + +=cut + +sub _active_interface +{ + my ($self, $interface) = @_; + # setup hooks the first time that an interface is made active + if (!($$self{active_interface}) and $interface) { + &main::print_log("[Insteon] Setting up initialization hooks") if $main::Debug{insteon}; + &main::MainLoop_pre_add_hook(\&Insteon::BaseInterface::check_for_data, 1); + &main::Reload_post_add_hook(\&Insteon::check_all_aldb_versions, 1); + &main::Reload_post_add_hook(\&Insteon::BaseInterface::poll_all, 1); + $init_complete = 0; + &main::MainLoop_pre_add_hook(\&Insteon::init, 1); + &main::Reload_post_add_hook(\&Insteon::check_thermo_versions, 1); + &main::Reload_post_add_hook(\&Insteon::generate_voice_commands, 1); + } + $$self{active_interface} = $interface if $interface; + return $$self{active_interface}; +} + +=item C + +Adds a list of objects to be tracked. + +=cut + +sub add +{ + my ($self,@p_objects) = @_; + + my @l_objects; + + for my $l_object (@p_objects) { + if ($l_object->isa('Group_Item') ) { + @l_objects = $$l_object{members}; + for my $obj (@l_objects) { + $self->add($obj); + } + } else { + $self->add_item($l_object); + } + } +} + +=item C + +Adds an object to be tracked. + +=cut + +sub add_item +{ + my ($self,$p_object) = @_; + + push @{$$self{objects}}, $p_object; + if ($p_object->isa('Insteon::BaseInterface')) { + $self->_active_interface($p_object); + } + return $p_object; +} + +=item C + +Removes all of the Insteon objects. + +=cut + +sub remove_all_items { + my ($self) = @_; + + if (ref $$self{objects}) { + foreach (@{$$self{objects}}) { + # $_->untie_items($self); + } + } + delete $self->{objects}; +} + +=item C + +Adds an item to be tracked if it is not already in the list. + +=cut + +sub add_item_if_not_present { + my ($self, $p_object) = @_; + + if (ref $$self{objects}) { + foreach (@{$$self{objects}}) { + if ($_->equals($p_object)) { + return 0; + } + } + } + $self->add_item($p_object); + return 1; +} + +=item C + +Removes the Insteon object. + +=cut + +sub remove_item { + my ($self, $p_object) = @_; + return 0 unless $p_object and ref $p_object; + if (ref $$self{objects}) { + for (my $i = 0; $i < scalar(@{$$self{objects}}); $i++) { + if ($p_object->equals($$self{objects}->[$i])) { + splice @{$$self{objects}}, $i, 1; + return 1; + } + } + } + return 0; +} + +=item C + +Returns true if object is in the list. + +=cut + +sub is_member { + my ($self, $p_object) = @_; + + my @l_objects = @{$$self{objects}}; + for my $l_object (@l_objects) { + if ($l_object->equals($p_object)) { + return 1; + } + } + return 0; +} + +=item C + +Find and return all tracked objects of type p_type where p_type is an object +class. + +=cut + +sub find_members { + my ($self,$p_type) = @_; + + my @l_found; + my @l_objects = @{$$self{objects}}; + for my $l_object (@l_objects) { + if ($l_object->isa($p_type)) { + push @l_found, $l_object; + } + } + return @l_found; +} + +=back + +=head2 INI PARAMETERS + +=over + +=item C + +For debugging debug=insteon or debug=insteon:level where level is 1-4. + +=back + +=head2 AUTHOR + +Bruce Winter, Gregg Liming, Kevin Robert Keegan, Michael Stovenour, many others + +=head2 SEE ALSO + +None + +=head2 LICENSE + +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. + +=cut + + +1; diff --git a/lib/Insteon/AllLinkDatabase.pm b/lib/Insteon/AllLinkDatabase.pm index eec478977..3a4a3a5f9 100644 --- a/lib/Insteon/AllLinkDatabase.pm +++ b/lib/Insteon/AllLinkDatabase.pm @@ -1,2805 +1,2805 @@ -package Insteon::AllLinkDatabase; - -=head1 B - -=head2 SYNOPSIS - -Generic class implementation of an insteon device's all link database. - -=head2 DESCRIPTION - -Generally this object should be interacted with through the insteon objects and -not by directly calling any of the following methods. - -=head2 INHERITS - -None - -=head2 METHODS - -=over - -=cut - -use strict; - -=item C - -Instantiate a new object. - -=cut - -sub new -{ - my ($class, $device) = @_; - my $self={}; - bless $self,$class; - $$self{device} = $device; - $self->health("unknown"); # unknown - $self->aldb_version("I1"); - return $self; -} - -sub _send_cmd -{ - my ($self, $msg) = @_; - $$self{device}->_send_cmd($msg); -} - -=item C - -Used to track the ALDB version type. - -If provided, saves version to memory. - -Returns the saved version type. - -=cut - -sub aldb_version -{ - my ($self, $aldb_version) = @_; - $$self{aldb_version} = $aldb_version if defined $aldb_version; - return $$self{aldb_version}; -} - -=item C - -Used to track the health of MisterHouse's copy of a device's ALDB. - -If provided, saves status to memory. - -Returns the saved health status. - -=cut - -sub health -{ - my ($self, $health) = @_; - $$self{health} = $health if defined $health; - return $$self{health}; -} - -=item C - -Used to track the health of MisterHouse's copy of a device's ALDB. - -If provided, saves status to memory. - -Returns the saved health status. - -=cut - -sub get_linkkey { - my ($self, $deviceid, $group, $is_controller, $data3) = @_; - my $linkkey = $deviceid . $group . $is_controller; - # Data3 is irrelevant for the PLM itself, b/c for controller records - # data3 will always be equal to group. And for responder records it - # should always be 00, therefor blank data3 so it isn't used - if ($$self{device}->isa('Insteon_PLM')){ - $data3 = '00'; - } - # '00' and '01' are generally interchangable for $data3 values and are - # the most common values. So to make searching easier we only - # add data3 if it is unique - $linkkey .= $data3 if ($data3 ne '00' and $data3 ne '01'); - return lc $linkkey; -} - -=item C - -Used to track the time, in unix time seconds, of the last ALDB scan. - -If provided, saves the time to memory. - -Returns the time of the last ALDB scan. - -=cut - -sub scandatetime -{ - my ($self, $scandatetime) = @_; - $$self{scandatetime} = $scandatetime if defined $scandatetime; - return $$self{scandatetime}; -} - -=item C - -Used to track the ALDB Delta. The ALDB Delta starts at 00 and iterates -+1 for each change to a device's ALDB. The ALDB Delta will be reset to 00 -whenever power is lost to the device or if the device is factory reset. - -If provided, saves the hex value to memory. (This should likely only be done by -C) - -Returns the current ALDB Delta. - -=cut - -sub aldb_delta -{ - my ($self, $p_aldb_delta) = @_; - $$self{aldb_delta} = $p_aldb_delta if defined($p_aldb_delta); - return $$self{aldb_delta}; -} - -=item C - -Interacts with the device's ALDB Delta. - -If called with "check", MisterHouse will query the device to obtain the current -ALDB Delta. If the ALDB Delta matches the version stored in C -MisterHouse will eval the code stored in C<$self->{_aldb_unchanged_callback}>. -If the ALDB Delta does not match, MisterHouse will eval the code stored in -C<$self->{_aldb_changed_callback}>. - -If called with "set" will cause MisterHouse to query the device for its ALDB -Delta and will store it with C. - -=cut - -sub query_aldb_delta -{ - my ($self, $action) = @_; - $$self{aldb_delta_action} = $action; - if ($action eq "check" && $self->health ne "good" && $self->health ne "empty"){ - &::print_log("[Insteon::AllLinkDatabase] WARN The link table for " - . $self->{device}->get_object_name . " is out-of-sync."); - if (defined $self->{_aldb_changed_callback}) { - package main; - my $callback = $self->{_aldb_changed_callback}; - $self->{_aldb_changed_callback} = undef; - eval ($callback); - &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } elsif ($action eq "check" && ((&main::get_tickcount - $self->scandatetime()) <= 2000)){ - #if we just did a aldb_query less than 2 seconds ago, don't repeat - &::print_log("[Insteon::AllLinkDatabase] The link table for " - . $self->{device}->get_object_name . " is in sync."); - #Further extend Scan Time in case of serial aldb requests - $self->scandatetime(&main::get_tickcount); - if (defined $self->{_aldb_unchanged_callback}) { - package main; - my $callback = $self->{_aldb_unchanged_callback}; - $self->{_aldb_unchanged_callback} = undef; - eval ($callback); - &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } else { - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'status_request'); - if (defined($$self{_failure_callback})) {$message->failure_callback($$self{_failure_callback})}; - $self->_send_cmd($message); - } -} - -=item C - -This is called by mh on exit to save the cached ALDB of a device to persistant data. - -=cut - -sub restore_string -{ - my ($self) = @_; - my $restore_string = ''; - if ($$self{aldb}) { - my $aldb = ''; - foreach my $aldb_key (keys %{$$self{aldb}}) { - next unless $aldb_key eq 'empty' || $aldb_key eq 'duplicates' || $$self{aldb}{$aldb_key}{inuse}; - $aldb .= '|' if $aldb; # separate sections - my $record = ''; - if ($aldb_key eq 'empty') { - foreach my $address (@{$$self{aldb}{empty}}) { - $record .= ';' if $record; - $record .= $address; - } - $record = 'empty=' . $record; - } elsif ($aldb_key eq 'duplicates') { - my $duplicate_record = ''; - foreach my $address (@{$$self{aldb}{duplicates}}) { - $duplicate_record .= ';' if $duplicate_record; - $duplicate_record .= $address; - } - $record = 'duplicates=' . $duplicate_record; - } else { - my %aldb_record = %{$$self{aldb}{$aldb_key}}; - foreach my $record_key (keys %aldb_record) { - next unless $aldb_record{$record_key}; - $record .= ',' if $record; - $record .= $record_key . '=' . $aldb_record{$record_key}; - } - } - $aldb .= $record; - } - if (defined $self->scandatetime) - { - $restore_string .= $$self{device}->get_object_name . "->_aldb->scandatetime(q~" . $self->scandatetime . "~) if " - . $$self{device}->get_object_name . "->_aldb;\n"; - } - if (defined $self->aldb_delta) - { - $restore_string .= $$self{device}->get_object_name . "->_aldb->aldb_delta(q~" . $self->aldb_delta . "~) if " - . $$self{device}->get_object_name . "->_aldb;\n"; - } - $restore_string .= $$self{device}->get_object_name . "->_aldb->health(q~" . $self->health . "~) if " - . $$self{device}->get_object_name . "->_aldb;\n"; - $restore_string .= $$self{device}->get_object_name . "->_aldb->restore_aldb(q~$aldb~) if " . $$self{device}->get_object_name . "->_aldb;\n"; - } - return $restore_string; -} - -=item C - -Used to reload MisterHouse's cached version of a device's ALDB on restart. - -=cut - -sub restore_aldb -{ - my ($self,$aldb) = @_; - if ($aldb) { - foreach my $aldb_section (split(/\|/,$aldb)) { - my %aldb_record = (); - my @aldb_empty = (); - my @aldb_duplicates = (); - my $deviceid = ''; - my $groupid = '01'; - my $is_controller = 0; - my $subaddress = '00'; - foreach my $aldb_entry (split(/,/,$aldb_section)) { - my ($key,$value) = split(/=/,$aldb_entry); - next unless $key and defined($value) and $value ne ''; - if ($key eq 'empty') { - @aldb_empty = split(/;/,$value); - } elsif ($key eq 'duplicates') { - @aldb_duplicates = split(/;/,$value); - } else { - $deviceid = lc $value if ($key eq 'deviceid'); - $groupid = lc $value if ($key eq 'group'); - $is_controller = $value if ($key eq 'is_controller'); - $subaddress = $value if ($key eq 'data3'); - $aldb_record{$key} = $value if $key and defined($value); - } - } - if (@aldb_empty) { - @{$$self{aldb}{empty}} = @aldb_empty; - } elsif (@aldb_duplicates) { - @{$$self{aldb}{duplicates}} = @aldb_duplicates; - } elsif (scalar %aldb_record) { - next unless $deviceid; - my $aldbkey = $self->get_linkkey($deviceid, $groupid, - $is_controller, $subaddress); - %{$$self{aldb}{$aldbkey}} = %aldb_record; - } - } -# $self->log_alllink_table(); - } -} - -=item C - -Scans a device's link table and caches a copy. - -=cut - -sub scan_link_table -{ - my ($self,$success_callback,$failure_callback) = @_; - $$self{_mem_activity} = 'scan'; - $$self{_success_callback} = ($success_callback) ? $success_callback : undef; - $$self{_failure_callback} = ($failure_callback) ? $failure_callback : undef; - $self->health('out-of-sync'); # allow acknowledge to set otherwise - if($self->isa('Insteon::ALDB_i1')) { - $self->_peek('0FF8',0); - } else { - $self->send_read_aldb('0000'); - } -} - -=item C - -Deletes a specific link from a device. Generally called by C. - -=cut - -sub delete_link -{ - my ($self, $parms_text) = @_; - my %link_parms; - if ($parms_text eq 'ok' or $parms_text eq 'fail'){ - %link_parms = %{$self->{callback_parms}}; - $$self{callback_parms} = undef; - $link_parms{aldb_check} = $parms_text; - } - elsif (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; - if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ - ## Check whether ALDB is in sync - $self->{callback_parms} = \%link_parms; - $$self{_aldb_unchanged_callback} = '&Insteon::AllLinkDatabase::delete_link('.$$self{device}->{object_name}."->_aldb, 'ok')"; - $$self{_aldb_changed_callback} = '&Insteon::AllLinkDatabase::delete_link('.$$self{device}->{object_name}."->_aldb, 'fail')"; - $self->query_aldb_delta("check"); - } elsif ($link_parms{aldb_check} eq "fail"){ - &::print_log("[Insteon::AllLinkDatabase] WARN: Link NOT deleted, please rescan this device and sync again."); - if ($link_parms{callback}) - { - package main; - eval($link_parms{callback}); - &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } elsif ($link_parms{address} && $link_parms{aldb_check} eq "ok") - { - &main::print_log("[Insteon::AllLinkDatabase] Now deleting link [0x$link_parms{address}]"); - $$self{_mem_activity} = 'delete'; - $$self{pending_aldb}{address} = $link_parms{address}; - if($self->isa('Insteon::ALDB_i1')) { - $self->_peek($link_parms{address},0); - } else { - $self->_write_delete($link_parms{address}); - } - - } - elsif ($link_parms{aldb_check} eq "ok") - { - my $insteon_object = $link_parms{object}; - my $deviceid = ($insteon_object) ? $insteon_object->device_id : $link_parms{deviceid}; - my $groupid = $link_parms{group}; - $groupid = '01' unless $groupid; - my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - my $subaddress = ($link_parms{data3}) ? $link_parms{data3} : '00'; - # get the address via lookup into the hash - my $key = $self->get_linkkey($deviceid, $groupid, $is_controller, $subaddress); - my $address = $$self{aldb}{$key}{address}; - if ($address) - { - &main::print_log("[Insteon::AllLinkDatabase] Now deleting link [0x$address] with the following data" - . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller, subaddress=$subaddress"); - # now, alter the flags byte such that the in_use flag is set to 0 - $$self{_mem_activity} = 'delete'; - $$self{pending_aldb}{deviceid} = lc $deviceid; - $$self{pending_aldb}{group} = $groupid; - $$self{pending_aldb}{is_controller} = $is_controller; - $$self{pending_aldb}{address} = $address; - $$self{pending_aldb}{data3} = $subaddress; - if($self->isa('Insteon::ALDB_i1')) { - $self->_peek($address,0); - } else { - $self->_write_delete($address); - } - } - else - { - &main::print_log('[Insteon::AllLinkDatabase] WARN: (' . $$self{device}->get_object_name - . ') attempt to delete link that does not exist!' - . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller, subaddress=$subaddress"); - if ($link_parms{callback}) - { - package main; - eval($link_parms{callback}); - &::print_log("[Insteon::AllLinkDatabase] error encountered during delete_link callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDataBase; - } - } - } -} - -=item C - -Reviews the cached version of the link database for the device and removes -links from THIS device which are not present in the mht file, link to non-existant -devices, or which are only half-links. - -Since this routine only processes this device, it is best to run the voice -command 'delete all orphan links' from the interface so that all devices are -scanned and processed. - -=cut - -sub delete_orphan_links -{ - my ($self, $audit_mode, $failure_callback) = @_; - @{$$self{delete_queue}} = (); # reset the work queue - $$self{delete_queue_processed} = 0; - my $selfname = $$self{device}->get_object_name; - - # first, make sure that the health of ALDB is ok - if ($self->health ne 'good' || $$self{device}->is_deaf) { - my $sent_to_failure = 0; - if ($$self{device}->is_deaf) { - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not delete links on deaf device: $selfname"); - } - elsif ($self->health eq 'empty'){ - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Skipping $selfname, because it has no links"); - } - else { - ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " - . $self->health . ". Please rescan the link table of this device and rerun delete " - . "orphans if necessary"); - #log the failure - $sent_to_failure = 1; - if ($failure_callback) { - package main; - eval ($failure_callback); - &::print_log("[Insteon::AllLinkDatabase] error in delete orphans failure callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } - if (!$$self{device}->isa('Insteon_PLM') && !$sent_to_failure){ - $self->_process_delete_queue(); - } - return; - } - - # Loop through the device's ALDB table - LINKKEY: for my $linkkey (keys %{$$self{aldb}}) { - # Skip empty addresses - next LINKKEY if ($linkkey eq 'empty'); - - # Define delete request - my %delete_req = (callback => "$selfname->_aldb->_process_delete_queue()", - failure_callback => $failure_callback); - - # Delete duplicate entries - if ($linkkey eq 'duplicates') { - push my @duplicate_addresses, @{$$self{aldb}{duplicates}}; - foreach (@duplicate_addresses) { - %delete_req = (%delete_req, address => $_, cause => "it is a duplicate record"); - push @{$$self{delete_queue}}, \%delete_req; - } - next LINKKEY; - } - - # Initialize Variables - my ($linked_device, $plm_scene, $controller_object, $link_defined, - $controller_id, $responder_id, $link_data3, $recip_data3, - $group_object, $data3_object); - my $group = lc $$self{aldb}{$linkkey}{group}; - my $is_controller = $$self{aldb}{$linkkey}{is_controller}; - my $data3 = lc $$self{aldb}{$linkkey}{data3}; - my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; - my $self_id = lc $$self{device}->device_id; - my $interface_id = $self_id; - $interface_id = lc $$self{device}->interface->device_id if (!$$self{device}->isa('Insteon_PLM')); - $linked_device = Insteon::get_object($deviceid,'01'); - $linked_device = Insteon::active_interface() if ($deviceid eq $interface_id); - $group_object = ($is_controller) ? Insteon::get_object($self_id, $group) : Insteon::get_object($deviceid, $group); - $data3_object = Insteon::get_object($self_id, $data3); - %delete_req = (%delete_req, deviceid => $deviceid, group => $group, - is_controller => $is_controller, data3 => $data3); - - # Is the linked device defined in MH? - if (! ref $linked_device) { - $delete_req{cause} = "no device with deviceid: $deviceid could be found"; - push @{$$self{delete_queue}}, \%delete_req; - next LINKKEY; - } - - # If link is a PLM Scene, is the PLM Scene defined in MH? - if (($linked_device->isa("Insteon_PLM") and !$is_controller)|| - ($$self{device}->isa("Insteon_PLM") and $is_controller)) { - $plm_scene = Insteon::get_object('000000', $group); - if (!ref $plm_scene && $group ne '01' && $group ne '00') { - $delete_req{cause} = "no plm scene for group $group could be found"; - push @{$$self{delete_queue}}, \%delete_req; - next LINKKEY; - } - } - - # Is this link defined in MH? 3-Step Process - # Define variables based on type of link - $controller_id = ($is_controller) ? $self_id : lc $linked_device->device_id; - $responder_id = ($is_controller) ? lc $linked_device->device_id : $self_id; - $controller_object = Insteon::get_object($controller_id,$group); - $controller_object = $plm_scene if (ref $plm_scene); - - # First, iterate over the controller object members to find the link definition - MEMBERS: foreach my $member_ref (keys %{$$controller_object{members}}) { - my $member = $$controller_object{members}{$member_ref}{object}; - if ($member->isa('Light_Item')) { - my @lights = $member->find_members('Insteon::BaseLight'); - $member = $lights[0] if (@lights); # pick the first - } - # For resp, D3 = resp group; For cont, D3 = cont group - $link_data3 = ($is_controller) ? $group : $member->group; - if (lc($member->device_id) eq $responder_id) { - if ($data3 eq $link_data3){ - $link_defined = 1; - $recip_data3 = ($is_controller) ? $member->group : $group; - last MEMBERS; - } - elsif (($link_data3 eq '00' || $link_data3 eq '01') && - ($data3 eq '00' || $data3 eq '01' )){ - # Allow for 00 or 01 interchangability - $link_defined = 1; - $recip_data3 = ($is_controller) ? $member->group : $group; - last MEMBERS; - } - } - } - - # Second, is this a PLM->Device, Device->PLM link, these are not members - if ($$self{device}->isa("Insteon_PLM")){ - if ($is_controller && ($group eq '00' || $group eq '01')){ - #Valid Controller for PLM->Device link - $delete_req{skip} = "$selfname -- Skipping reciprocal link check for controller group 00 or 01 link to " - . $linked_device->get_object_name; - next LINKKEY; - } - elsif (!$is_controller && ref $group_object){ - #Valid Responder for Device->PLM link - $link_defined = 1; - $recip_data3 = $group; - } - } - elsif($deviceid eq $interface_id && (ref $data3_object || ($data3 eq '00' || $data3 eq '01'))){ - if ($is_controller && ref $group_object){ - #Valid Controller for Device->PLM link - $link_defined = 1; - $recip_data3 = '00'; - } - elsif (!$is_controller && ($group eq '00' || $group eq '01')) { - #Valid Responder for PLM->Device link - $link_defined = 1; - $recip_data3 = '00'; - } - - } - - # Third, delete link if not defined - if (!$link_defined){ - $delete_req{cause} = "link is not defined in MisterHouse"; - push @{$$self{delete_queue}}, \%delete_req; - next LINKKEY; - } - - # Do not delete links to deaf devices - if ($linked_device->is_deaf) { - $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on deaf device " . $linked_device->get_object_name; - next LINKKEY; - } - - # Do not delete links to unhealthy devices - if ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { - $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on " - . $linked_device->get_object_name . " because aldb health of that device is " - . $linked_device->_aldb->health . ". Please rescan this device."; - next LINKKEY; - } - - # Do not delete responder links from the PLM (prevents locking i2CS devices) - if ($linked_device->isa("Insteon_PLM") and !$is_controller && ($group eq '00' || $group eq '01')) { - $delete_req{skip} = "$selfname -- Skipping check for reciprocal controller link on PLM for group 00 or 01."; - next LINKKEY; - } - - # Does a reciprocal link exist? - if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $recip_data3)) { - $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name; - push @{$$self{delete_queue}}, \%delete_req; - } - - } # /LINKKEY Loop - my $index = 0; - foreach (@{$$self{delete_queue}}){ - my %delete_req = %{$_}; - my $audit_text = "(AUDIT)" if ($audit_mode); - my $log_text; - if ($delete_req{skip}){ - $log_text = "[Insteon::AllLinkDatabase] $audit_text " . $delete_req{skip}; - splice @{$$self{delete_queue}}, $index, 1; - } else { - $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; - $log_text .= $delete_req{cause} . "\n"; - PRINT: for (keys %delete_req) { - next PRINT if (($_ eq 'cause') || ($_ eq 'callback') || - ($_ eq 'failure_callback')); - $log_text .= "$_ = $delete_req{$_}; "; - } - if ($delete_req{deviceid}){ - my $reciprocal_object = Insteon::get_object($delete_req{deviceid}, '01'); - if (!$delete_req{is_controller}){ - $reciprocal_object = Insteon::get_object($delete_req{deviceid}, $delete_req{group}); - } - if (ref $reciprocal_object) { - $log_text .= "linked device name= " . $reciprocal_object->get_object_name; - } - } - $index ++; - } - ::print_log($log_text); - } - if ($audit_mode) { - @{$$self{delete_queue}} = (); - } - else { - ::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); - } - if (!$$self{device}->isa('Insteon_PLM')) { - $self->_process_delete_queue(); - } -} - -sub _process_delete_queue { - my ($self) = @_; - my $num_in_queue = @{$$self{delete_queue}}; - if ($num_in_queue) - { - my $delete_req_ptr = shift(@{$$self{delete_queue}}); - my %delete_req = %$delete_req_ptr; - if ($delete_req{address}) - { - &::print_log("[Insteon::AllLinkDatabase] (#$num_in_queue) " . $$self{device}->get_object_name . " now deleting duplicate record at address " - . $delete_req{address}); - } - else - { - &::print_log("[Insteon::AllLinkDatabase] (#$num_in_queue) " . $$self{device}->get_object_name . " now deleting orphaned link w/ details: " - . (($delete_req{is_controller}) ? "controller" : "responder") - . ", " . (($delete_req{object}) ? "device=" . $delete_req{object}->get_object_name - : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}, cause=$delete_req{cause}"); - } - $self->delete_link(%delete_req); - $$self{delete_queue_processed}++; - } - else - { - &::print_log("[Insteon::AllLinkDatabase] Nothing else to do for " . $$self{device}->get_object_name . " after deleting " - . $$self{delete_queue_processed} . " links") if $self->{device}->debuglevel(1, 'insteon'); - $$self{device}->interface->_aldb->_process_delete_queue($$self{delete_queue_processed}); - } -} - -=item C - -Adds address to the duplicate link hash. Called as part of C. - -=cut - -sub add_duplicate_link_address -{ - my ($self, $address) = @_; - - unshift @{$$self{aldb}{duplicates}}, $address; - - # now, keep the list sorted! - @{$$self{aldb}{duplicates}} = sort(@{$$self{aldb}{duplicates}}); - -} - -=item C - -Removes address from the duplicate link hash. Called as part of C. - -=cut - -sub delete_duplicate_link_address -{ - my ($self, $address) = @_; - my $num_duplicate_link_addresses = 0; - - $num_duplicate_link_addresses = @{$$self{aldb}{duplicates}} if (defined $$self{aldb}{duplicates}); - if ($num_duplicate_link_addresses) - { - my @temp_duplicates = (); - foreach my $temp_address (@{$$self{aldb}{duplicates}}) - { - if ($temp_address ne $address) - { - push @temp_duplicates, $temp_address; - } - } - # keep it sorted - @{$$self{aldb}{duplicates}} = sort(@temp_duplicates); - } -} - -=item C - -Adds address to the empty link hash. Called as part of C -or C. - -=cut - -sub add_empty_address -{ - my ($self, $address) = @_; - # before adding it, make sure that it isn't already in the list!! - my $num_addresses = 0; - $num_addresses = @{$$self{aldb}{empty}} if (defined $$self{aldb}{empty}); - my $exists = 0; - if ($num_addresses and $address) - { - foreach my $temp_address (@{$$self{aldb}{empty}}) - { - if ($temp_address eq $address) - { - $exists = 1; - last; - } - } - } - # add it to the list if it doesn't exist - if (!($exists) and $address) - { - unshift @{$$self{aldb}{empty}}, $address; - } - - # now, keep the list sorted! - @{$$self{aldb}{empty}} = sort(@{$$self{aldb}{empty}}); - -} - -=item C - -Returns the highest empty link address, or if no empty addresses exist, returns -the highest unused address. Called as part of C or -C.. - -=cut - -sub get_first_empty_address -{ - my ($self) = @_; - - # NOTE: The issue here is that we give up an address from the list - # with the assumption that it will be made non-empty; - # So, if there is a problem during update/add, then will have - # a non-empty, but non-functional entry - my $first_address = pop @{$$self{aldb}{empty}}; - - if (!($first_address)) - { - # then, cycle through all of the existing non-empty addresses - # to find the lowest one and then decrement by 8 - # - # TO-DO: factor in appropriate use of the "highwater" flag - # - my $high_address = 0xffff; - for my $key (keys %{$$self{aldb}}) - { - next if $key eq 'empty' or $key eq 'duplicates'; - my $new_address = hex($$self{aldb}{$key}{address}); - if( $new_address and $new_address < $high_address ) { - $high_address = $new_address; - } - } - $first_address = ($high_address > 0) ? sprintf('%04x', $high_address - 8) : 0; - main::print_log("[Insteon::AllLinkDatabase] DEBUG4: No empty link entries; using next lowest link address [" - .$first_address."]") if $self->{device}->debuglevel(4, 'insteon'); - } else { - main::print_log("[Insteon::AllLinkDatabase] DEBUG4: Found empty address [" - .$first_address."] in empty array") if $self->{device}->debuglevel(4, 'insteon'); - } - - return $first_address; -} - -=item C - -Adds the link to the device's ALDB. Generally called from the "sync links" or -"link to interface" voice commands. - -=cut - -sub add_link -{ - my ($self, $parms_text) = @_; - my %link_parms; - if ($parms_text eq 'ok' or $parms_text eq 'fail'){ - %link_parms = %{$self->{callback_parms}}; - $$self{callback_parms} = undef; - $link_parms{aldb_check} = $parms_text; - } - elsif (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - my $device_id; - my $insteon_object = $link_parms{object}; - my $group = $link_parms{group}; - if (!(defined($insteon_object))) - { - $device_id = lc $link_parms{deviceid}; - $insteon_object = &Insteon::get_object($device_id, $group); - } - else - { - $device_id = lc $insteon_object->device_id; - } - my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - - # For I2CS devices the default data3 for links is 01 - # For all other devices the default data3 for links is 00 - my $data3_default = '00'; - if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { - $data3_default = '01'; - } - my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; - $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); - - # check whether the link already exists - my $key = $self->get_linkkey($device_id, $group, $is_controller, $data3); - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; - if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ - ## Check whether ALDB is in sync - $self->{callback_parms} = \%link_parms; - $$self{_aldb_unchanged_callback} = '&Insteon::AllLinkDatabase::add_link('.$$self{device}->{object_name}."->_aldb, 'ok')"; - $$self{_aldb_changed_callback} = '&Insteon::AllLinkDatabase::add_link('.$$self{device}->{object_name}."->_aldb, 'fail')"; - $self->query_aldb_delta("check"); - } elsif ($link_parms{aldb_check} eq "fail"){ - &::print_log("[Insteon::AllLinkDatabase] WARN: Link NOT added, please rescan this device and sync again."); - if ($link_parms{callback}) - { - package main; - eval($link_parms{callback}); - &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } - elsif (defined $$self{aldb}{$key} && defined $$self{aldb}{$key}{inuse}) - { - &::print_log("[Insteon::AllLinkDatabase] WARN: attempt to add link to " - . $$self{device}->get_object_name - . " that already exists! object=" . $insteon_object->get_object_name - . ", group=$group, is_controller=$is_controller, data3=$data3"); - if ($link_parms{callback}) - { - package main; - eval($link_parms{callback}); - &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " - . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } - elsif ($link_parms{aldb_check} eq "ok") - { - # strip optional % sign to append on_level - my $on_level = $link_parms{on_level}; - $on_level =~ s/(\d)%?/$1/; - $on_level = '100' unless defined($on_level); # 100% == on is the default - # strip optional s (seconds) to append ramp_rate - my $ramp_rate = $link_parms{ramp_rate}; - $ramp_rate =~ s/(\d)s?/$1/; - $ramp_rate = '0.1' unless $ramp_rate; # 0.1s is the default - # get the first available memory location - my $address = $self->get_first_empty_address(); - if ($address) - { - &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record to " . $$self{device}->get_object_name - . " light level controlled by " . $insteon_object->get_object_name - . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") - if $self->{device}->debuglevel(2, 'insteon'); - my ($data1, $data2); - if($link_parms{is_controller}) { - $data1 = '03'; #application retries == 3 - $data2 = '00'; #ignored for controller entries - } else { - $data1 = &Insteon::DimmableLight::convert_level($on_level); - $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; - } - #data3 is defined above - $$self{_mem_activity} = 'add'; - $self->_write_link($address, $device_id, $group, $is_controller, $data1, $data2, $data3); - # TO-DO: ensure that pop'd address is restored back to queue if the transaction fails - } - else - { - &::print_log("[Insteon::AllLinkDatabase] ERROR: adding link record failed because " - . $$self{device}->get_object_name - . " does not have a record of the first empty ALDB record." - . " Please rescan this device's link table") - if $self->{device}->debuglevel(1, 'insteon'); - - if ($$self{_success_callback}) - { - package main; - eval ($$self{_success_callback}); - &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } - } -} - -=item C - -Updates the on_level and/or ramp_rate associated with a link to match the defined -value in MisterHouse. Generally called from the "sync links" voice command. - -=cut - -sub update_link -{ - my ($self, %link_parms) = @_; - if ($_[1] eq 'ok' or $_[1] eq 'fail'){ - %link_parms = %{$self->{callback_parms}}; - $$self{callback_parms} = undef; - $link_parms{aldb_check} = $_[1]; - } - my $insteon_object = $link_parms{object}; - my $group = $link_parms{group}; - my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - # strip optional % sign to append on_level - my $on_level = $link_parms{on_level}; - $on_level =~ s/(\d+)%?/$1/; - # strip optional s (seconds) to append ramp_rate - my $ramp_rate = $link_parms{ramp_rate}; - $ramp_rate =~ s/(\d)s?/$1/; - &::print_log("[Insteon::AllLinkDatabase] updating " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name - . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->{device}->debuglevel(1, 'insteon'); - my $data1 = &Insteon::DimmableLight::convert_level($on_level); - my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; - - # For I2CS devices the default data3 for links is 01 - # For all other devices the default data3 for links is 00 - my $data3_default = '00'; - if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { - $data3_default = '01'; - } - my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; - $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); - - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; - - my $deviceid = $insteon_object->device_id; - my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); - if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ - ## Check whether ALDB is in sync - $self->{callback_parms} = \%link_parms; - $$self{_aldb_unchanged_callback} = '&Insteon::AllLinkDatabase::update_link('.$$self{device}->{object_name}."->_aldb, 'ok')"; - $$self{_aldb_changed_callback} = '&Insteon::AllLinkDatabase::update_link('.$$self{device}->{object_name}."->_aldb, 'fail')"; - $self->query_aldb_delta("check"); - } elsif ($link_parms{aldb_check} eq "fail"){ - &::print_log("[Insteon::AllLinkDatabase] WARN: Cannot update link, please rescan this device and sync again."); - if ($link_parms{callback}) - { - package main; - eval($link_parms{callback}); - &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } - elsif (defined $$self{aldb}{$key} && $link_parms{aldb_check} eq "ok"){ - my $address = $$self{aldb}{$key}{address}; - $$self{_mem_activity} = 'update'; - $self->_write_link($address, $deviceid, $group, $is_controller, $data1, $data2, $data3); - } else { - &::print_log("[Insteon::AllLinkDatabase] ERROR: updating link record failed because " - . $$self{device}->get_object_name - . " does not have an existing ALDB entry key=$key") - if $self->{device}->debuglevel(1, 'insteon'); - - if ($$self{_success_callback}) - { - package main; - eval ($$self{_success_callback}); - &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::AllLinkDatabase; - } - } -} - -=item C - -Prints a human readable form of MisterHouse's cached version of a device's ALDB -to the print log. Called as part of the "scan links" voice command -or in response to the "log links" voice command. - -=cut - -sub log_alllink_table -{ - my ($self) = @_; - my %aldb; - - &::print_log("[Insteon::AllLinkDatabase] Link table for " - . $$self{device}->get_object_name - . " health: " . $self->health); - - # We want to log links sorted by ALDB address. Since the ALDB - # addresses are scattered throughout the %{$$self{aldb}} hash, - # and it is not easy to obtain them in a linear manner, - # we build a new data structure that will allow us to easily - # traverse the ALDB by address in a sorted manner. The new - # data structure is a bidimensional hash (%aldb) where rows - # are the ALDB addresses and the columns can be "empty" - # (indicates that the ALDB at the corresponding address is - # empty), "duplicate" (indicates that the ALDB at the - # corresponding address is a duplicate), or a hash key (which - # indicates that the ALDB at corresponding address contains - # a link). - foreach my $aldbkey (keys %{$$self{aldb}}) - { - if ($aldbkey eq "empty") - { - foreach my $address (@{$$self{aldb}{empty}}) - { - $aldb{$address}{empty} = undef; # Any value will do - } - } - elsif ($aldbkey eq "duplicates") - { - foreach my $address (@{$$self{aldb}{duplicates}}) - { - $aldb{$address}{duplicate} = undef; # Any value will do - } - } - else - { - $aldb{$$self{aldb}{$aldbkey}{address} }{$aldbkey} = $$self{aldb}{$aldbkey}; - } - } - - # Finally traverse the ALDB, but this time sorted by ALDB address - if ($self->health eq 'good') - { - foreach my $address (sort keys %aldb) - { - my $log_msg = "[Insteon::AllLinkDatabase] [0x$address] "; - - if (exists $aldb{$address}{empty}) - { - $log_msg .= "is empty"; - } - elsif (exists $aldb{$address}{duplicate}) - { - $log_msg .= "holds a duplicate entry"; - } - else - { - my ($key) = keys %{$aldb{$address} }; # There's only 1 key - my $aldb_entry = $aldb{$address}{$key}; - my $is_controller = $aldb_entry->{is_controller}; - my $device; - - if ($$self{device}->interface()->device_id() - && ($$self{device}->interface()->device_id() - eq $aldb_entry->{deviceid})) - { - $device = $$self{device}->interface; - } - else - { - $device = &Insteon::get_object($aldb_entry->{deviceid},'01'); - } - my $object_name = ($device) ? $device->get_object_name : $aldb_entry->{deviceid}; - - my $on_level = 'unknown'; - if (defined $aldb_entry->{data1}) - { - if ($aldb_entry->{data1}) - { - $on_level = int((hex($aldb_entry->{data1})*100/255) + .5) . "%"; - } - else - { - $on_level = '0%'; - } - } - - my $rspndr_group = $aldb_entry->{data3}; - $rspndr_group = '01' if $rspndr_group eq '00'; - - my $ramp_rate = 'unknown'; - if ($aldb_entry->{data2}) - { - if (!($$self{device}->isa('Insteon::DimmableLight')) - or (!$is_controller and ($rspndr_group != '01'))) - { - $ramp_rate = 'none'; - $on_level = $on_level eq '0%' ? 'off' : 'on'; - } - else - { - $ramp_rate = &Insteon::DimmableLight::get_ramp_from_code($aldb_entry->{data2}) . "s"; - } - } - - $log_msg .= $is_controller ? "contlr($aldb_entry->{group}) " - . "record to $object_name, " - . "(d1:$aldb_entry->{data1}, " - . "d2:$aldb_entry->{data2}, " - . "d3:$aldb_entry->{data3})" - : "rspndr($rspndr_group) record to $object_name " - . "($aldb_entry->{group}): onlevel=$on_level " - . "and ramp=$ramp_rate " - . "(d3:$aldb_entry->{data3})"; - } - - &::print_log($log_msg); - } - } - else - { - main::print_log("[Insteon::AllLinkDatabase] ALDB is ".$self->health." and will not be listed"); - } -} - -=item C - -Checks and returns true if a link with the passed details exists on the device -or false if it does not. Generally called as part of C. - -=cut - -sub has_link -{ - my ($self, $insteon_object, $group, $is_controller, $data3) = @_; - my $deviceid; - if ($insteon_object->isa('Insteon::AllLinkDatabase')) { - $deviceid = $$insteon_object{device}->device_id; - } else { - $deviceid = lc $insteon_object->device_id; - } - my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); - return (defined $$self{aldb}{$key}); -} - -=back - -=head2 INI PARAMETERS - -None - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -package Insteon::ALDB_i1; - -=head1 B - -=head2 SYNOPSIS - -Unique class for storing a cahced copy of a verion i1 device's ALDB. - -=head2 DESCRIPTION - -Generally this object should be interacted with through the insteon objects and -not by directly calling any of the following methods. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -use strict; - -@Insteon::ALDB_i1::ISA = ('Insteon::AllLinkDatabase'); - -=item C - -Instantiate a new object. - -=cut - -sub new -{ - my ($class,$device) = @_; - - my $self = new Insteon::AllLinkDatabase($device); - bless $self,$class; - $self->aldb_version("I1"); - return $self; -} - -sub _on_poke -{ - my ($self,%msg) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); - if (($$self{_mem_activity} eq 'update') or ($$self{_mem_activity} eq 'add')) - { - if ($$self{_mem_action} eq 'aldb_flag') - { - $$self{_mem_action} = 'aldb_group'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_group') - { - $$self{_mem_action} = 'aldb_devhi'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_devhi') - { - $$self{_mem_action} = 'aldb_devmid'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_devmid') - { - $$self{_mem_action} = 'aldb_devlo'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_devlo') - { - $$self{_mem_action} = 'aldb_data1'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_data1') - { - $$self{_mem_action} = 'aldb_data2'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_data2') - { - $$self{_mem_action} = 'aldb_data3'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_data3') - { - ## update the aldb records w/ the changes that were made - my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, - $$self{pending_aldb}{group}, - $$self{pending_aldb}{is_controller}, - $$self{pending_aldb}{data3}); - $$self{aldb}{$aldbkey}{data1} = $$self{pending_aldb}{data1}; - $$self{aldb}{$aldbkey}{data2} = $$self{pending_aldb}{data2}; - $$self{aldb}{$aldbkey}{data3} = $$self{pending_aldb}{data3}; - $$self{aldb}{$aldbkey}{inuse} = 1; # needed so that restore string will preserve record - if ($$self{_mem_activity} eq 'add') - { - $$self{aldb}{$aldbkey}{is_controller} = $$self{pending_aldb}{is_controller}; - $$self{aldb}{$aldbkey}{deviceid} = lc $$self{pending_aldb}{deviceid}; - $$self{aldb}{$aldbkey}{group} = lc $$self{pending_aldb}{group}; - $$self{aldb}{$aldbkey}{address} = $$self{pending_aldb}{address}; - $self->health("good"); - } - # clear out mem_activity flag - $$self{_mem_activity} = undef; - $self->health("good"); - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } - } - elsif ($$self{_mem_activity} eq 'update_local') - { - if ($$self{_mem_action} eq 'local_onlevel') - { - $$self{_mem_lsb} = '21'; - $$self{_mem_action} = 'local_ramprate'; - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'local_ramprate') - { - if ($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) - { - # update from eeprom--only a kpl issue - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'do_read_ee'); - $self->_send_cmd($message); - } - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } - } - elsif ($$self{_mem_activity} eq 'update_flags') - { - # update from eeprom--only a kpl issue - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'do_read_ee'); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_activity} eq 'delete') - { - # clear out mem_activity flag - $$self{_mem_activity} = undef; - # add the address of the deleted link to the empty list - $self->add_empty_address($$self{pending_aldb}{address}); - # and, remove from the duplicates list (if it is a member) - $self->delete_duplicate_link_address($$self{pending_aldb}{address}); - if (exists $$self{pending_aldb}{deviceid}) - { - my $key = $self->get_linkkey($$self{pending_aldb}{deviceid}, - $$self{pending_aldb}{group}, - $$self{pending_aldb}{is_controller}, - $$self{pending_aldb}{data3}); - delete $$self{aldb}{$key}; - } - $self->health("good"); - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } -} - -sub _on_peek -{ - my ($self,%msg) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); - if ($msg{is_extended}) { - &::print_log("[Insteon::ALDB_i1]: extended peek for " . $$self{device}->{object_name} - . " is " . $msg{extra}) if $self->{device}->debuglevel(1, 'insteon'); - } - else - { - if ($$self{_mem_action} eq 'aldb_peek') - { - if ($$self{_mem_activity} eq 'scan') - { - $$self{_mem_action} = 'aldb_flag'; - # if the device is responding to the peek, then init the link table - # if at the very start of a scan - if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8' - && !$$self{_stress_test_act}) - { - # reinit the aldb hash as there will be a new one - $$self{aldb} = undef; - # reinit the empty address list - @{$$self{aldb}{empty}} = (); - # and, also the duplicates list - @{$$self{aldb}{duplicates}} = (); - } - } - elsif ($$self{_mem_activity} eq 'update') - { - $$self{_mem_action} = 'aldb_data1'; - } - elsif ($$self{_mem_activity} eq 'update_local') - { - $$self{_mem_action} = 'local_onlevel'; - } - elsif ($$self{_mem_activity} eq 'update_flags') - { - $$self{_mem_action} = 'update_flags'; - } - elsif ($$self{_mem_activity} eq 'delete') - { - $$self{_mem_action} = 'aldb_flag'; - } - elsif ($$self{_mem_activity} eq 'add') - { - $$self{_mem_action} = 'aldb_flag'; - } - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_flag') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - my $flag = hex($msg{extra}); - $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; - $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; - $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; - if ($$self{_stress_test_act} && !($$self{pending_aldb}{highwater})){ - ::print_log("[Insteon::ALDB_i1] You need to create a link on this device before running stress_test"); - $$self{_mem_activity} = undef; - $$self{_mem_action} = undef; - $$self{_stress_test_act} = 0; - $$self{device}->stress_test(); - } - elsif (!($$self{pending_aldb}{highwater})) - { - # since this is the last unused memory location, then add it to the empty list - $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); - $$self{_mem_action} = undef; - # clear out mem_activity flag - $$self{_mem_activity} = undef; - if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8') - { - # set health as empty for now - $self->health("empty"); - } - else - { - $self->health("good"); - } - - &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " completed link memory scan") - if $self->{device}->debuglevel(1, 'insteon'); - $self->health("good"); - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } - elsif ($$self{pending_aldb}{inuse}) - { - $$self{pending_aldb}{flag} = $msg{extra}; - ## confirm that we have a high-water mark; otherwise stop - $$self{pending_aldb}{address} = $$self{_mem_msb} . $$self{_mem_lsb}; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{_mem_action} = 'aldb_group'; - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } else { - $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); - if ($$self{_mem_activity} eq 'scan'){ - my $newaddress = sprintf("%04X", hex($$self{_mem_msb} . $$self{_mem_lsb}) - 8); - $$self{pending_aldb} = undef; - $self->_peek($newaddress); - } - } - } - elsif ($$self{_mem_activity} eq 'add') - { - # TO-DO!!! Eventually add the ability to set the highwater mark - # the below flags never reset the highwater mark so that - # the scanner will continue scanning extra empty records - my $flag = ($$self{pending_aldb}{is_controller}) ? 'E2' : 'A2'; - $$self{pending_aldb}{flag} = $flag; - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($flag); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_activity} eq 'delete') - { - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra('02'); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - } - elsif ($$self{_mem_action} eq 'aldb_group') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{pending_aldb}{group} = lc $msg{extra}; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{_mem_action} = 'aldb_devhi'; - $message->extra($$self{_mem_lsb}); - } - else - { - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($$self{pending_aldb}{group}); - } - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_devhi') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{pending_aldb}{deviceid} = lc $msg{extra}; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{_mem_action} = 'aldb_devmid'; - $message->extra($$self{_mem_lsb}); - } - elsif ($$self{_mem_activity} eq 'add') - { - my $devid = substr($$self{pending_aldb}{deviceid},0,2); - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($devid); - } - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_devmid') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{pending_aldb}{deviceid} .= lc $msg{extra}; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{_mem_action} = 'aldb_devlo'; - $message->extra($$self{_mem_lsb}); - } - elsif ($$self{_mem_activity} eq 'add') - { - my $devid = substr($$self{pending_aldb}{deviceid},2,2); - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($devid); - } - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'aldb_devlo') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{pending_aldb}{deviceid} .= lc $msg{extra}; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{_mem_action} = 'aldb_data1'; - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_activity} eq 'add') - { - my $devid = substr($$self{pending_aldb}{deviceid},4,2); - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($devid); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - } - elsif ($$self{_mem_action} eq 'aldb_data1') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{_mem_action} = 'aldb_data2'; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{pending_aldb}{data1} = $msg{extra}; - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_activity} eq 'update' or $$self{_mem_activity} eq 'add') - { - # poke the new value - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($$self{pending_aldb}{data1}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - } - elsif ($$self{_mem_action} eq 'aldb_data2') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{pending_aldb}{data2} = $msg{extra}; - $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); - $$self{_mem_action} = 'aldb_data3'; - $message->extra($$self{_mem_lsb}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_activity} eq 'update' or $$self{_mem_activity} eq 'add') - { - # poke the new value - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($$self{pending_aldb}{data2}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - } - elsif ($$self{_mem_action} eq 'aldb_data3') - { - if ($$self{_mem_activity} eq 'scan') - { - &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{pending_aldb}{data3} = $msg{extra}; - - if ($$self{_stress_test_act}){ - $$self{_stress_test_act} = 0; - $$self{_mem_activity} = undef; - $$self{_mem_action} = undef; - $$self{device}->stress_test(); - } - elsif ($$self{pending_aldb}{highwater}) - { - if ($$self{pending_aldb}{inuse}) - { - # save pending_aldb and then clear it out - my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, - $$self{pending_aldb}{group}, - $$self{pending_aldb}{is_controller}, - $$self{pending_aldb}{data3}); - # check for duplicates - if (exists $$self{aldb}{$aldbkey} && $$self{aldb}{$aldbkey}{inuse}) - { - $self->add_duplicate_link_address($$self{pending_aldb}{address}); - } - else - { - %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; - } - } - else - { - $self->add_empty_address($$self{pending_aldb}{address}); - } - my $newaddress = sprintf("%04X", hex($$self{pending_aldb}{address}) - 8); - $$self{pending_aldb} = undef; - $self->_peek($newaddress); - } - } - elsif ($$self{_mem_activity} eq 'update' or $$self{_mem_activity} eq 'add') - { - # poke the new value - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($$self{pending_aldb}{data3}); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - } - elsif ($$self{_mem_action} eq 'local_onlevel') - { - my $device = $$self{device}; - my $on_level = $$device{_onlevel}; - $on_level = &Insteon::DimmableLight::convert_level($on_level); - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($on_level); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'local_ramprate') - { - my $device = $$self{device}; - my $ramp_rate = $$device{_ramprate}; - $ramp_rate = '1f' unless $ramp_rate; - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($ramp_rate); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - elsif ($$self{_mem_action} eq 'update_flags') - { - my $flags = $$self{_operating_flags}; - $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); - $message->extra($flags); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - else { - ::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name - . ": unhandled _mem_action=".$$self{_mem_action}) - if $self->{device}->debuglevel(1, 'insteon'); - } - } -} - -=item C - -Used to update the local on level and ramp rate of a device. Called by -L. - -=cut - -sub update_local_properties -{ - my ($self, $aldb_check) = @_; - if (defined($aldb_check)){ - $$self{_mem_activity} = 'update_local'; - $self->_peek('0032'); # 0032 is the address for the onlevel - } else { - $$self{_aldb_unchanged_callback} = '&Insteon::ALDB_i1::update_local_properties('.$$self{device}->{object_name}."->_aldb, 1)"; - $$self{_aldb_changed_callback} = '&Insteon::ALDB_i1::update_local_properties('.$$self{device}->{object_name}."->_aldb, 1)"; - $self->query_aldb_delta("check"); - } -} - -=item C - -Used to update the flags of a device. Called by L. - -=cut - -sub update_flags -{ - my ($self, $flags, $aldb_check) = @_; - return unless defined $flags; - if (defined($aldb_check)){ - $$self{_mem_activity} = 'update_flags'; - $$self{_operating_flags} = $flags; - $self->_peek('0023'); - } else { - $$self{_aldb_unchanged_callback} = '&Insteon::ALDB_i1::update_flags('.$$self{device}->{object_name}."->_aldb, '$flags', 1)"; - $$self{_aldb_changed_callback} = '&Insteon::ALDB_i1::update_flags('.$$self{device}->{object_name}."->_aldb, '$flags', 1)"; - $self->query_aldb_delta("check"); - } -} - -=item C - -Gets and returns the details of a link. Called by L. - -NOTE - This routine may be obsolete, its parent routine is not called by any code. - -=cut - -sub get_link_record -{ - my ($self,$link_key) = @_; - my %link_record = (); - %link_record = %{$$self{aldb}{$link_key}} if $$self{aldb}{$link_key}; - return %link_record; -} - -sub _write_link -{ - my ($self, $address, $deviceid, $group, $is_controller, $data1, $data2, $data3) = @_; - if ($address) - { - &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " address: $address found for device: $deviceid and group: $group"); - # change address for start of change to be address + offset - if ($$self{_mem_activity} eq 'update') - { - $address = sprintf('%04X',hex($address) + 5); - } - $$self{pending_aldb}{address} = $address; - $$self{pending_aldb}{deviceid} = lc $deviceid; - $$self{pending_aldb}{group} = lc $group; - $$self{pending_aldb}{is_controller} = $is_controller; - $$self{pending_aldb}{data1} = (defined $data1) ? lc $data1 : '00'; - $$self{pending_aldb}{data2} = (defined $data2) ? lc $data2 : '00'; - $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; - $self->_peek($address); - } - else - { - &::print_log("[Insteon::ALDB_i1] WARN: " . $$self{device}->get_object_name - . " write_link failure: no address available for record to device: $deviceid and group: $group" . - " and is_controller: $is_controller");; - if ($$self{_success_callback}) - { - package main; - eval ($$self{_success_callback}); - &::print_log("[Insteon::ALDB_i1] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::ALDB_i1; - } - } -} - -sub _peek -{ - my ($self, $address, $extended) = @_; - my $msb = substr($address,0,2); - my $lsb = substr($address,2,2); - if ($extended) - { - my $message = $self->device->derive_message('peek','insteon_ext_send', - $lsb . "0000000000000000000000000000"); - $self->interface->queue_message($message); - - } - else - { - $$self{_mem_lsb} = $lsb; - $$self{_mem_msb} = $msb; - $$self{_mem_action} = 'aldb_peek'; - &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " accessing memory at location: 0x" . $address); - my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'set_address_msb'); - $message->extra($msb); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); -# $self->_send_cmd('command' => 'set_address_msb', 'extra' => $msb, 'is_synchronous' => 1); - } -} - -=back - -=head2 INI PARAMETERS - -None - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -package Insteon::ALDB_i2; - -=head1 B - -=head2 SYNOPSIS - -Unique class for storing a cahced copy of a verion i2 device's ALDB. - -=head2 DESCRIPTION - -Generally this object should be interacted with through the insteon objects and -not by directly calling any of the following methods. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -use strict; - -@Insteon::ALDB_i2::ISA = ('Insteon::AllLinkDatabase'); - -=item C - -Instantiate a new object. - -=cut - -sub new -{ - my ($class,$device) = @_; - - my $self = new Insteon::AllLinkDatabase($device); - bless $self,$class; - $self->aldb_version("I2"); - return $self; -} - -=item C - -Called as part of any process to read or write to a device's ALDB. - -=cut - -sub on_read_write_aldb -{ - my ($self, %msg) = @_; - my $clear_message = 1; #Default Action is to Clear the Current Message - &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for _mem_activity=".$$self{_mem_activity} - ." _mem_action=". $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - - if ($$self{_mem_action} eq 'aldb_i2read') - { - #This is an ACK. Will be followed by a Link Data message, so don't clear - $clear_message = 0; - #Only move to the next state if the received message is a device ack - #if the ack is dropped the retransmission logic will resend the request - if($msg{is_ack}) { - $$self{_mem_action} = 'aldb_i2readack'; - &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received ack") - if $self->{device}->debuglevel(3, 'insteon'); - } else { - #otherwise just ignore the message because it is out of sequence - &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] ack not received. " - . "ignoring message") if $self->{device}->debuglevel(3, 'insteon'); - } - - } - elsif ($$self{_mem_action} eq 'aldb_i2readack') - { - if($msg{is_ack}) { - &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received duplicate ack. Ignoring.") - if $self->{device}->debuglevel(3, 'insteon'); - $clear_message = 0; - } elsif(length($msg{extra})<30) - { - &::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed: " - . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); - #can't clear message, if valid message doesn't arrive - #resend logic will kick in - $clear_message = 0; - } elsif ($$self{_mem_msb} . $$self{_mem_lsb} ne '0000' and - $$self{_mem_msb} . $$self{_mem_lsb} ne substr($msg{extra},6,4)){ - ::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed, " - . " address received did not match address requested: " - . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); - $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); - #can't clear message, if valid message doesn't arrive - #resend logic will kick in - $clear_message = 0; - } - elsif ($$self{_stress_test_act}){ - $$self{_mem_activity} = undef; - $$self{_mem_action} = undef; - $$self{_stress_test_act} = 0; - $$self{device}->stress_test(); - } - else - { - # init the link table if at the very start of a scan - if (lc $$self{_mem_msb} eq '00' and lc $$self{_mem_lsb} eq '00') - { - main::print_log("[Insteon::ALDB_i2] DEBUG4: Start of scan; initializing aldb structure") - if $self->{device}->debuglevel(4, 'insteon'); - # reinit the aldb hash as there will be a new one - $$self{aldb} = undef; - # reinit the empty address list - @{$$self{aldb}{empty}} = (); - # and, also the duplicates list - @{$$self{aldb}{duplicates}} = (); - $$self{_mem_msb} = substr($msg{extra},6,2); - $$self{_mem_lsb} = substr($msg{extra},8,2); - } - - #$msg{extra} includes cmd2 at the beginning so cmd2.d1.d2.d3... - #e.g. 0001010fff00a2042042d3fe1c00cc - # 0:cmd2:00 - unused - # 2: D1:01 - unused - # 4: D2:01 - command (read aldb response) - # 6: D3:0fff - aldb address (first entry in this case) - #10: D5:00 - unused in responses - #12: D6:a2 - flags - #14: D7:04 - group number - #16: D8:11.31.a2 - device id - #22: D11:fe - link data 1 - #24: D12:1c - link data 2 - #26: D13:00 - link data 3 (unused) - #28: D14:cc - unused in i2; checksum in i2CS - - $$self{pending_aldb}{address} = substr($msg{extra},6,4); - - $$self{pending_aldb}{flag} = substr($msg{extra},12,2); - my $flag = hex($$self{pending_aldb}{flag}); - $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; - $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; - $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; - unless($$self{pending_aldb}{highwater}) - { - #highwater is set for every entry that has been used before - #highwater being 0 indicates entry has never been used (i.e. top of list) - # since this is the last unused memory location, then add it to the empty list - &::print_log("[Insteon::ALDB_i2] WARNING: highwater not set but marked inuse: " - . $$self{device}->get_object_name - . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " - . lc $msg{extra} . " for " . $$self{_mem_action}) - if(($$self{pending_aldb}{inuse}) and $self->{device}->debuglevel(3, 'insteon')); - main::print_log("[Insteon::ALDB_i2] DEBUG4: scan done; adding last address [" - . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $self->{device}->debuglevel(4, 'insteon'); - $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); - # scan done; clear out state flags - $$self{_mem_action} = undef; - $$self{_mem_activity} = undef; - if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'ff') - { - # set health as empty for now - $self->health("empty"); - } - else - { - $self->health("good"); - } - - &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name - . " completed link memory scan: status: " . $self->health()) - if $self->{device}->debuglevel(1, 'insteon'); - $self->health("good"); - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } - else #($$self{pending_aldb}{highwater}) - { - unless($$self{pending_aldb}{inuse}) - { - main::print_log("[Insteon::ALDB_i2] DEBUG4: inuse flag == false; adding address [" - . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") - if $self->{device}->debuglevel(4, 'insteon'); - $self->add_empty_address($$self{pending_aldb}{address}); - } - else - { - $$self{pending_aldb}{group} = lc substr($msg{extra},14,2); - $$self{pending_aldb}{deviceid} = lc substr($msg{extra},16,6); - $$self{pending_aldb}{data1} = lc substr($msg{extra},22,2); - $$self{pending_aldb}{data2} = lc substr($msg{extra},24,2); - $$self{pending_aldb}{data3} = lc substr($msg{extra},26,2); - - # save pending_aldb and then clear it out - my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, - $$self{pending_aldb}{group}, - $$self{pending_aldb}{is_controller}, - $$self{pending_aldb}{data3}); - # check for duplicates - if (exists $$self{aldb}{$aldbkey} && $$self{aldb}{$aldbkey}{inuse}) - { - main::print_log("[Insteon::ALDB_i2] DEBUG4: duplicate link found; adding address [" - . $$self{_mem_msb} . $$self{_mem_lsb} ."] to duplicates array") - if $self->{device}->debuglevel(4, 'insteon'); - $self->add_duplicate_link_address($$self{pending_aldb}{address}); - } - else - { - main::print_log("[Insteon::ALDB_i2] DEBUG4: active link found; adding address [" - . $$self{_mem_msb} . $$self{_mem_lsb} ."] to aldb") - if $self->{device}->debuglevel(4, 'insteon'); - %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; - } - } - - #keep going; request the next record - $self->send_read_aldb(sprintf("%04x", hex($$self{pending_aldb}{address}) - 8)); - - } #($$self{pending_aldb}{highwater}) - } #else $msg{extra} !< 30 - } - elsif ($$self{_mem_action} eq 'aldb_i2writeack') - { - unless ($$self{_mem_activity} eq 'delete') { - ## update the aldb records w/ the changes that were made - my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, - $$self{pending_aldb}{group}, - $$self{pending_aldb}{is_controller}, - $$self{pending_aldb}{data3}); - $$self{aldb}{$aldbkey}{data1} = $$self{pending_aldb}{data1}; - $$self{aldb}{$aldbkey}{data2} = $$self{pending_aldb}{data2}; - $$self{aldb}{$aldbkey}{data3} = $$self{pending_aldb}{data3}; - $$self{aldb}{$aldbkey}{inuse} = 1; # needed so that restore string will preserve record - if ($$self{_mem_activity} eq 'add') - { - $$self{aldb}{$aldbkey}{is_controller} = $$self{pending_aldb}{is_controller}; - $$self{aldb}{$aldbkey}{deviceid} = lc $$self{pending_aldb}{deviceid}; - $$self{aldb}{$aldbkey}{group} = lc $$self{pending_aldb}{group}; - $$self{aldb}{$aldbkey}{address} = $$self{pending_aldb}{address}; - } - $$self{_mem_activity} = undef; - $$self{_mem_action} = undef; - $$self{pending_aldb} = undef; - main::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name - . " link write completed for [".$$self{aldb}{$aldbkey}{address}."]") - if $self->{device}->debuglevel(3, 'insteon'); - $self->health("good"); - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } else { - # clear out mem_activity flag - $$self{_mem_activity} = undef; - # add the address of the deleted link to the empty list - $self->add_empty_address($$self{pending_aldb}{address}); - # and, remove from the duplicates list (if it is a member) - $self->delete_duplicate_link_address($$self{pending_aldb}{address}); - if (exists $$self{pending_aldb}{deviceid}) - { - my $key = $self->get_linkkey($$self{pending_aldb}{deviceid}, - $$self{pending_aldb}{group}, - $$self{pending_aldb}{is_controller}, - $$self{pending_aldb}{data3}); - delete $$self{aldb}{$key}; - } - $self->health("good"); - # Put the new ALDB Delta into memory - $self->query_aldb_delta('set'); - } - } - else - { - main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name - . ": unhandled _mem_action=".$$self{_mem_action}) - if $self->{device}->debuglevel(1, 'insteon'); - $clear_message = 0; - } - return $clear_message; -} - -sub _write_link -{ - my ($self, $address, $deviceid, $group, $is_controller, $data1, $data2, $data3) = @_; - if ($address) - { - &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " writing address: $address for device: $deviceid and group: $group"); - - my $message = new Insteon::InsteonMessage('insteon_ext_send', $$self{device}, 'read_write_aldb'); - - #cmd2.00.write_aldb_record.addr_msb.addr_lsb.byte_count.d6-d14 bytes to write - my $message_extra = '00'.'00'.'02'; - - $$self{pending_aldb}{address} = $address; - $message_extra .= $address; - - $message_extra .= '08'; #write 8 bytes - - #D6-D13 aldb entry: flags.group.deviceid(3).data1.data2.data3 - #flags - $$self{pending_aldb}{is_controller} = $is_controller; - my $flag = ($$self{pending_aldb}{is_controller}) ? 'E2' : 'A2'; - $$self{pending_aldb}{flag} = $flag; - $message_extra .= $flag; - - #group - $$self{pending_aldb}{group} = lc $group; - $message_extra .= $$self{pending_aldb}{group}; - - #device ID - $$self{pending_aldb}{deviceid} = lc $deviceid; - $message_extra .= $$self{pending_aldb}{deviceid}; - - #data1 - data3 - $$self{pending_aldb}{data1} = (defined $data1) ? lc $data1 : '00'; - $message_extra .= $$self{pending_aldb}{data1}; - $$self{pending_aldb}{data2} = (defined $data2) ? lc $data2 : '00'; - $message_extra .= $$self{pending_aldb}{data2}; - $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; - $message_extra .= $$self{pending_aldb}{data3}; - $message_extra .= '00'; #byte 14 - $message->extra($message_extra); - $$self{_mem_action} = 'aldb_i2writeack'; - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - else - { - &::print_log("[Insteon::ALDB_i2] WARN: " . $$self{device}->get_object_name - . " write_link failure: no address available for record to device: $deviceid and group: $group" . - " and is_controller: $is_controller"); - if ($$self{_success_callback}) - { - package main; - eval ($$self{_success_callback}); - &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::ALDB_i2; - } - } -} - -sub _write_delete -{ - #pending_aldb must be populated before calling - my ($self, $address) = @_; - - if ($address) - { - &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " writing address as deleted: $address"); - - my $message = new Insteon::InsteonMessage('insteon_ext_send', $$self{device}, 'read_write_aldb'); - - #cmd2.00.write_aldb_record.addr_msb.addr_mid.addr_lsb.byte_count.d6-d14 bytes to write - my $message_extra = '00'.'00'.'02'; - $message_extra .= $address; - $message_extra .= '08'; #write 8 bytes - - #D6-D13 aldb entry: flags.group.deviceid(3).data1.data2.data3 - #flag = 02 deleted - $message_extra .= '02'; - #group - $message_extra .= '00'; - #device ID - $message_extra .= '000000'; - #data1 - data3 - $message_extra .= '00'; - $message_extra .= '00'; - $message_extra .= '00'; - #byte 14 - $message_extra .= '00'; - - $message->extra($message_extra); - $$self{_mem_action} = 'aldb_i2writeack'; - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); - } - else - { - &::print_log("[Insteon::ALDB_i2] WARN: " . $$self{device}->get_object_name - . " write_delete failure: no address available"); - if ($$self{_success_callback}) - { - package main; - eval ($$self{_success_callback}); - &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon::ALDB_i2; - } - } -} - -=item C - -Called as part of "scan link table" voice command. - -=cut - -sub send_read_aldb -{ - my ($self, $address) = @_; - - $$self{_mem_msb} = substr($address,0,2); - $$self{_mem_lsb} = substr($address,2,2); - $$self{_mem_action} = 'aldb_i2read'; - main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " reading ALDB at location: 0x" . $address); - my $message = new Insteon::InsteonMessage('insteon_ext_send', $$self{device}, 'read_write_aldb'); - #cmd2.00.read_aldb_record.addr_msb.addr_lsb.record_count(0 for all).d6-d14 unused - $message->extra("00"."00"."00".$$self{_mem_msb}.$$self{_mem_lsb}."01"."000000000000000000"); - $message->failure_callback($$self{_failure_callback}); - $self->_send_cmd($message); -} - -=back - -=head2 INI PARAMETERS - -None - -=head2 AUTHOR - -Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -package Insteon::ALDB_PLM; - -=head1 B - -=head2 SYNOPSIS - -Unique class for storing a cahced copy of a the PLM's link database. - -=head2 DESCRIPTION - -Generally this object should be interacted with through the insteon objects and -not by directly calling any of the following methods. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -use strict; - -@Insteon::ALDB_PLM::ISA = ('Insteon::AllLinkDatabase'); - -=item C - -Instantiate a new object. - -=cut - -sub new -{ - my ($class,$device) = @_; - - my $self = new Insteon::AllLinkDatabase($device); - bless $self,$class; - return $self; -} - -=item C - -This is called by mh on exit to save the cached ALDB of a device to persistant data. - -=cut - -sub restore_string -{ - my ($self) = @_; - my $restore_string = ''; - if ($$self{aldb}) - { - my $link = ''; - foreach my $link_key (keys %{$$self{aldb}}) - { - $link .= '|' if $link; # separate sections - my %link_record = %{$$self{aldb}{$link_key}}; - my $record = ''; - foreach my $record_key (keys %link_record) - { - next unless $link_record{$record_key}; - $record .= ',' if $record; - $record .= $record_key . '=' . $link_record{$record_key}; - } - $link .= $record; - } - $restore_string .= $$self{device}->get_object_name . "->_aldb->restore_linktable(q~$link~) if " . $$self{device}->get_object_name . "->_aldb;\n"; - } - if (defined $self->scandatetime) - { - $restore_string .= $$self{device}->get_object_name . "->_aldb->scandatetime(q~" . $self->scandatetime . "~) if " - . $$self{device}->get_object_name . "->_aldb;\n"; - } - $restore_string .= $$self{device}->get_object_name . "->_aldb->health(q~" . $self->health . "~) if " - . $$self{device}->get_object_name . "->_aldb;\n"; - return $restore_string; -} - -=item C - -Used to reload MisterHouse's cached version of a device's ALDB on restart. - -=cut - -sub restore_linktable -{ - my ($self, $links) = @_; - if ($links) - { - foreach my $link_section (split(/\|/,$links)) - { - my %link_record = (); - my $deviceid = ''; - my $groupid = '01'; - my $is_controller = 0; - my $subaddress = ''; - foreach my $link_record (split(/,/,$link_section)) - { - my ($key,$value) = split(/=/,$link_record); - $deviceid = $value if ($key eq 'deviceid'); - $groupid = $value if ($key eq 'group'); - $is_controller = $value if ($key eq 'is_controller'); - $subaddress = $value if ($key eq 'data3'); - $link_record{$key} = $value if $key and defined($value); - } - my $linkkey = $self->get_linkkey($deviceid, $groupid, $is_controller, $subaddress); - %{$$self{aldb}{lc $linkkey}} = %link_record; - } - } -} - -=item C - -Prints a human readable form of MisterHouse's cached version of a device's ALDB -to the print log. Called as part of the "scan links" voice command -or in response to the "log links" voice command. - -=cut - -sub log_alllink_table -{ - my ($self) = @_; - &::print_log("[Insteon::ALDB_PLM] Link table health: " . $self->health); - foreach my $linkkey (sort(keys(%{$$self{aldb}}))) { - my $is_controller = $$self{aldb}{$linkkey}{is_controller}; - my $group = $$self{aldb}{$linkkey}{group}; - $group = '01' if $group eq '00'; - my $deviceid = $$self{aldb}{$linkkey}{deviceid}; - my $linked_subgroup = '01'; - my $controller_device; - my $controller_name; - if (!$is_controller){ - $linked_subgroup = $group; - } - elsif ($group ne '00' && $group ne '01') { - $controller_device = Insteon::get_object('000000',$group); - $controller_name = $controller_device->get_object_name . " ($group)"; - } - else { - $controller_name = $group; - } - my $linked_object = Insteon::get_object($deviceid,$linked_subgroup); - my $linked_name = ''; - if ($linked_object) { - $linked_name = $linked_object->get_object_name; - } - else { - $linked_name = uc substr($deviceid,0,2) . '.' . - uc substr($deviceid,2,2) . '.' . - uc substr($deviceid,4,2); - } - &::print_log("[Insteon::ALDB_PLM] " . - (($is_controller) ? "cntlr($controller_name) record to " - . $linked_name - : "responder record to " . $linked_name . "($$self{aldb}{$linkkey}{group})") - . " (d1=$$self{aldb}{$linkkey}{data1}, d2=$$self{aldb}{$linkkey}{data2}, " - . "d3=$$self{aldb}{$linkkey}{data3})"); - } -} - -=item C - -Parses the alllink message sent from the PLM. - -=cut - -sub parse_alllink -{ - my ($self, $data) = @_; - if (substr($data,0,6)) - { - my %link = (); - my $flag = substr($data,0,1); - $link{is_controller} = (hex($flag) & 0x04) ? 1 : 0; - $link{flags} = substr($data,0,2); - $link{group} = lc substr($data,2,2); - $link{deviceid} = lc substr($data,4,6); - $link{data1} = substr($data,10,2); - $link{data2} = substr($data,12,2); - $link{data3} = substr($data,14,2); - my $key = $self->get_linkkey($link{deviceid}, $link{group}, - $link{is_controller}, $link{data3}); - %{$$self{aldb}{lc $key}} = %link; - } -} - -=item C - -Sends the request for the first alllink entry on the PLM. - -=cut - -sub get_first_alllink -{ - my ($self) = @_; - $self->health('out-of-sync'); # set as corrupt and allow acknowledge to set otherwise - $$self{device}->queue_message(new Insteon::InsteonMessage('all_link_first_rec', $$self{device})); -} - -=item C - -Sends the request for the next alllink entry on the PLM. - -=cut - -sub get_next_alllink -{ - my ($self) = @_; - $$self{device}->queue_message(new Insteon::InsteonMessage('all_link_next_rec', $$self{device})); -} - -=item C - -Reviews the cached version of all of the ALDBs and based on this review removes -links from this device which are not present in the mht file, not defined in the -code, or links which are only half-links.. - -=cut - -sub delete_orphan_links -{ - my ($self, $audit_mode) = @_; - - &::print_log("[Insteon::ALDB_PLM] #### NOW BEGINNING DELETE ORPHAN LINKS ####"); - - $self->SUPER::delete_orphan_links($audit_mode); - - # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links - for my $obj (&Insteon::find_members('Insteon::BaseDevice')) - { - #Match on real objects only - if (($obj->is_root)) - { - my %delete_req = ('root_object' => $obj, 'audit_mode' => $audit_mode); - push @{$$self{delete_queue}}, \%delete_req; - } - } - - $self->_process_delete_queue(); -} - -sub _process_delete_queue { - my ($self, $p_num_deleted) = @_; - $$self{delete_queue_processed} += $p_num_deleted if $p_num_deleted; - my $num_in_queue = @{$$self{delete_queue}}; - if ($num_in_queue) - { - my $delete_req_ptr = shift(@{$$self{delete_queue}}); - my %delete_req = %$delete_req_ptr; - my $failure_callback = $$self{device}->get_object_name . - "->_aldb->_process_delete_queue_failure"; - # distinguish between deleting PLM links and processing delete orphans for a root item - if ($delete_req{'root_object'}) - { - $$self{current_delete_device} = $delete_req{'root_object'}->get_object_name; - $delete_req{'root_object'}->delete_orphan_links(($delete_req{'audit_mode'}) ? 1 : 0, $failure_callback); - } - else - { - $$self{current_delete_device} = $$self{device}->get_object_name; - &::print_log("[Insteon::ALDB_PLM] now deleting orphaned link w/ details: " - . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") - . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name - : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") - if $self->{device}->debuglevel(1, 'insteon'); - $delete_req{failure_callback} = $failure_callback; - $self->delete_link(%delete_req); - $$self{delete_queue_processed}++; - } - } - else { - ::print_log("[Insteon::ALDB_PLM] Delete All Links has Completed."); - my $_delete_failure_cnt = scalar $$self{_delete_device_failures}; - if ($_delete_failure_cnt) { - my $obj_list; - for my $failed_obj (@{$$self{_delete_device_failures}}){ - $obj_list .= $failed_obj .", "; - } - ::print_log("[Insteon::ALDB_PLM] However, some failures were ". - "noted with the following devices: $obj_list"); - } - ::print_log("[Insteon::ALDB_PLM] A total of $$self{delete_queue_processed} orphaned link records were deleted."); - ::print_log("[Insteon::ALDB_PLM] #### END DELETE ORPHAN LINKS ####"); - } -} - -sub _process_delete_queue_failure { - my ($self) = @_; - push @{$$self{_delete_device_failures}}, $$self{current_delete_device}; - ::print_log("[Insteon::ALDB_PLM] WARN: failure occurred when deleting orphan links from: " - . $$self{current_delete_device} . ". Moving on..."); - $self->_process_delete_queue; - -} - -=item C - -Deletes a specific link from a device. Generally called by C. - -=cut - -sub delete_link -{ - # linkkey is concat of: deviceid, group, is_controller - my ($self, $parms_text) = @_; - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - my $num_deleted = 0; - my $insteon_object = $link_parms{object}; - my $deviceid = ($insteon_object) ? $insteon_object->device_id : $link_parms{deviceid}; - my $group = $link_parms{group}; - my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; - my $linkkey = $self->get_linkkey($deviceid, $group, $is_controller, $subaddress); - if (defined $$self{aldb}{$linkkey}) - { - my $cmd = '80' - . $$self{aldb}{$linkkey}{flags} - . $$self{aldb}{$linkkey}{group} - . $$self{aldb}{$linkkey}{deviceid} - . $$self{aldb}{$linkkey}{data1} - . $$self{aldb}{$linkkey}{data2} - . $$self{aldb}{$linkkey}{data3}; - delete $$self{aldb}{$linkkey}; - $num_deleted = 1; - my $message = new Insteon::InsteonMessage('all_link_manage_rec', $$self{device}); - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; - $message->interface_data($cmd); - $$self{device}->queue_message($message); - } - else - { - &::print_log("[Insteon::ALDB_PLM] no entry in linktable could be found for: ". - "deviceid=$deviceid, group=$group, is_controller=$is_controller, subaddress=$subaddress"); - if ($link_parms{callback}) - { - package main; - eval ($link_parms{callback}); - &::print_log("[Insteon_PLM] error in add link callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon_PLM; - } - } - return $num_deleted; -} - -=item C - -Adds the link to the device's ALDB. Generally called from the "sync links" or -"link to interface" voice commands. - -=cut - -sub add_link -{ - my ($self, $parms_text) = @_; - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - my $device_id; - my $group = ($link_parms{group}) ? $link_parms{group} : '01'; - my $insteon_object = $link_parms{object}; - if (!(defined($insteon_object))) - { - $device_id = lc $link_parms{deviceid}; - $insteon_object = &Insteon::get_object($device_id, $group); - } - else - { - $device_id = lc $insteon_object->device_id; - } - my $is_controller = ($link_parms{is_controller}) ? 1 : 0; - my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; - my $linkkey = $self->get_linkkey($device_id, $group, $is_controller, $subaddress); - if (defined $$self{aldb}{$linkkey}) - { - &::print_log("[Insteon::ALDB_PLM] WARN: attempt to add link to PLM that already exists! " - . "deviceid=$device_id, group=$group, is_controller=$is_controller, subaddress=$subaddress"); - if ($link_parms{callback}) - { - package main; - eval ($link_parms{callback}); - &::print_log("[Insteon::ALDB_PLM] error in add link callback: " . $@) - if $@ and $self->{device}->debuglevel(1, 'insteon'); - package Insteon_PLM; - } - } - else - { - # The modem developers guide appears to be wrong regarding control - # codes. 40 and 41 will respond with a NACK if a record for that - # group/device/is_controller combination already exist. It appears - # that code 20 can be used to edit existing but not create new records. - # However, since data1-3 are consistent for all PLM links we never - # really need to update a PLM link. NB prior MH code did not set - # data3 on control records to the group, however this does not - # appear to have any adverse effects, and the current MH code will - # not flag these entries as being incorrect or requiring an update - my $control_code = ($is_controller) ? '40' : '41'; - # flags should be 'a2' for responder and 'e2' for controller - my $flags = ($is_controller) ? 'E2' : 'A2'; - my $data1 = (defined $link_parms{data1}) ? $link_parms{data1} : (($is_controller) ? '01' : '00'); - my $data2 = (defined $link_parms{data2}) ? $link_parms{data2} : '00'; - my $data3 = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; - # from looking at manually linked records, data1 and data2 are both 00 for responder records - # and, data1 is 01 and usually data2 is 00 for controller records - - my $cmd = $control_code - . $flags - . $group - . $device_id - . $data1 - . $data2 - . $data3; - $$self{aldb}{$linkkey}{flags} = lc $flags; - $$self{aldb}{$linkkey}{group} = lc $group; - $$self{aldb}{$linkkey}{is_controller} = $is_controller; - $$self{aldb}{$linkkey}{deviceid} = lc $device_id; - $$self{aldb}{$linkkey}{data1} = lc $data1; - $$self{aldb}{$linkkey}{data2} = lc $data2; - $$self{aldb}{$linkkey}{data3} = lc $data3; - $$self{aldb}{$linkkey}{inuse} = 1; - $self->health('good') if($self->health() eq 'empty'); - my $message = new Insteon::InsteonMessage('all_link_manage_rec', $$self{device}); - $message->interface_data($cmd); - $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; - $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; - $message->interface_data($cmd); - $$self{device}->queue_message($message); - } -} - -=item C - -Checks and returns true if a link with the passed details exists on the device -or false if it does not. Generally called as part of C. - -=cut - -sub has_link -{ - my ($self, $insteon_object, $group, $is_controller, $data3) = @_; - my $key = $self->get_linkkey($insteon_object->device_id, - $group, $is_controller, $data3); - return (defined $$self{aldb}{$key}); -} - -=back - -=head2 INI PARAMETERS - -None - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - - -1; +package Insteon::AllLinkDatabase; + +=head1 B + +=head2 SYNOPSIS + +Generic class implementation of an insteon device's all link database. + +=head2 DESCRIPTION + +Generally this object should be interacted with through the insteon objects and +not by directly calling any of the following methods. + +=head2 INHERITS + +None + +=head2 METHODS + +=over + +=cut + +use strict; + +=item C + +Instantiate a new object. + +=cut + +sub new +{ + my ($class, $device) = @_; + my $self={}; + bless $self,$class; + $$self{device} = $device; + $self->health("unknown"); # unknown + $self->aldb_version("I1"); + return $self; +} + +sub _send_cmd +{ + my ($self, $msg) = @_; + $$self{device}->_send_cmd($msg); +} + +=item C + +Used to track the ALDB version type. + +If provided, saves version to memory. + +Returns the saved version type. + +=cut + +sub aldb_version +{ + my ($self, $aldb_version) = @_; + $$self{aldb_version} = $aldb_version if defined $aldb_version; + return $$self{aldb_version}; +} + +=item C + +Used to track the health of MisterHouse's copy of a device's ALDB. + +If provided, saves status to memory. + +Returns the saved health status. + +=cut + +sub health +{ + my ($self, $health) = @_; + $$self{health} = $health if defined $health; + return $$self{health}; +} + +=item C + +Used to track the health of MisterHouse's copy of a device's ALDB. + +If provided, saves status to memory. + +Returns the saved health status. + +=cut + +sub get_linkkey { + my ($self, $deviceid, $group, $is_controller, $data3) = @_; + my $linkkey = $deviceid . $group . $is_controller; + # Data3 is irrelevant for the PLM itself, b/c for controller records + # data3 will always be equal to group. And for responder records it + # should always be 00, therefor blank data3 so it isn't used + if ($$self{device}->isa('Insteon_PLM')){ + $data3 = '00'; + } + # '00' and '01' are generally interchangable for $data3 values and are + # the most common values. So to make searching easier we only + # add data3 if it is unique + $linkkey .= $data3 if ($data3 ne '00' and $data3 ne '01'); + return lc $linkkey; +} + +=item C + +Used to track the time, in unix time seconds, of the last ALDB scan. + +If provided, saves the time to memory. + +Returns the time of the last ALDB scan. + +=cut + +sub scandatetime +{ + my ($self, $scandatetime) = @_; + $$self{scandatetime} = $scandatetime if defined $scandatetime; + return $$self{scandatetime}; +} + +=item C + +Used to track the ALDB Delta. The ALDB Delta starts at 00 and iterates ++1 for each change to a device's ALDB. The ALDB Delta will be reset to 00 +whenever power is lost to the device or if the device is factory reset. + +If provided, saves the hex value to memory. (This should likely only be done by +C) + +Returns the current ALDB Delta. + +=cut + +sub aldb_delta +{ + my ($self, $p_aldb_delta) = @_; + $$self{aldb_delta} = $p_aldb_delta if defined($p_aldb_delta); + return $$self{aldb_delta}; +} + +=item C + +Interacts with the device's ALDB Delta. + +If called with "check", MisterHouse will query the device to obtain the current +ALDB Delta. If the ALDB Delta matches the version stored in C +MisterHouse will eval the code stored in C<$self->{_aldb_unchanged_callback}>. +If the ALDB Delta does not match, MisterHouse will eval the code stored in +C<$self->{_aldb_changed_callback}>. + +If called with "set" will cause MisterHouse to query the device for its ALDB +Delta and will store it with C. + +=cut + +sub query_aldb_delta +{ + my ($self, $action) = @_; + $$self{aldb_delta_action} = $action; + if ($action eq "check" && $self->health ne "good" && $self->health ne "empty"){ + &::print_log("[Insteon::AllLinkDatabase] WARN The link table for " + . $self->{device}->get_object_name . " is out-of-sync."); + if (defined $self->{_aldb_changed_callback}) { + package main; + my $callback = $self->{_aldb_changed_callback}; + $self->{_aldb_changed_callback} = undef; + eval ($callback); + &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } elsif ($action eq "check" && ((&main::get_tickcount - $self->scandatetime()) <= 2000)){ + #if we just did a aldb_query less than 2 seconds ago, don't repeat + &::print_log("[Insteon::AllLinkDatabase] The link table for " + . $self->{device}->get_object_name . " is in sync."); + #Further extend Scan Time in case of serial aldb requests + $self->scandatetime(&main::get_tickcount); + if (defined $self->{_aldb_unchanged_callback}) { + package main; + my $callback = $self->{_aldb_unchanged_callback}; + $self->{_aldb_unchanged_callback} = undef; + eval ($callback); + &::print_log("[Insteon::AllLinkDatabase] " . $self->{device}->get_object_name . ": error during scan callback $@") + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } else { + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'status_request'); + if (defined($$self{_failure_callback})) {$message->failure_callback($$self{_failure_callback})}; + $self->_send_cmd($message); + } +} + +=item C + +This is called by mh on exit to save the cached ALDB of a device to persistant data. + +=cut + +sub restore_string +{ + my ($self) = @_; + my $restore_string = ''; + if ($$self{aldb}) { + my $aldb = ''; + foreach my $aldb_key (keys %{$$self{aldb}}) { + next unless $aldb_key eq 'empty' || $aldb_key eq 'duplicates' || $$self{aldb}{$aldb_key}{inuse}; + $aldb .= '|' if $aldb; # separate sections + my $record = ''; + if ($aldb_key eq 'empty') { + foreach my $address (@{$$self{aldb}{empty}}) { + $record .= ';' if $record; + $record .= $address; + } + $record = 'empty=' . $record; + } elsif ($aldb_key eq 'duplicates') { + my $duplicate_record = ''; + foreach my $address (@{$$self{aldb}{duplicates}}) { + $duplicate_record .= ';' if $duplicate_record; + $duplicate_record .= $address; + } + $record = 'duplicates=' . $duplicate_record; + } else { + my %aldb_record = %{$$self{aldb}{$aldb_key}}; + foreach my $record_key (keys %aldb_record) { + next unless $aldb_record{$record_key}; + $record .= ',' if $record; + $record .= $record_key . '=' . $aldb_record{$record_key}; + } + } + $aldb .= $record; + } + if (defined $self->scandatetime) + { + $restore_string .= $$self{device}->get_object_name . "->_aldb->scandatetime(q~" . $self->scandatetime . "~) if " + . $$self{device}->get_object_name . "->_aldb;\n"; + } + if (defined $self->aldb_delta) + { + $restore_string .= $$self{device}->get_object_name . "->_aldb->aldb_delta(q~" . $self->aldb_delta . "~) if " + . $$self{device}->get_object_name . "->_aldb;\n"; + } + $restore_string .= $$self{device}->get_object_name . "->_aldb->health(q~" . $self->health . "~) if " + . $$self{device}->get_object_name . "->_aldb;\n"; + $restore_string .= $$self{device}->get_object_name . "->_aldb->restore_aldb(q~$aldb~) if " . $$self{device}->get_object_name . "->_aldb;\n"; + } + return $restore_string; +} + +=item C + +Used to reload MisterHouse's cached version of a device's ALDB on restart. + +=cut + +sub restore_aldb +{ + my ($self,$aldb) = @_; + if ($aldb) { + foreach my $aldb_section (split(/\|/,$aldb)) { + my %aldb_record = (); + my @aldb_empty = (); + my @aldb_duplicates = (); + my $deviceid = ''; + my $groupid = '01'; + my $is_controller = 0; + my $subaddress = '00'; + foreach my $aldb_entry (split(/,/,$aldb_section)) { + my ($key,$value) = split(/=/,$aldb_entry); + next unless $key and defined($value) and $value ne ''; + if ($key eq 'empty') { + @aldb_empty = split(/;/,$value); + } elsif ($key eq 'duplicates') { + @aldb_duplicates = split(/;/,$value); + } else { + $deviceid = lc $value if ($key eq 'deviceid'); + $groupid = lc $value if ($key eq 'group'); + $is_controller = $value if ($key eq 'is_controller'); + $subaddress = $value if ($key eq 'data3'); + $aldb_record{$key} = $value if $key and defined($value); + } + } + if (@aldb_empty) { + @{$$self{aldb}{empty}} = @aldb_empty; + } elsif (@aldb_duplicates) { + @{$$self{aldb}{duplicates}} = @aldb_duplicates; + } elsif (scalar %aldb_record) { + next unless $deviceid; + my $aldbkey = $self->get_linkkey($deviceid, $groupid, + $is_controller, $subaddress); + %{$$self{aldb}{$aldbkey}} = %aldb_record; + } + } +# $self->log_alllink_table(); + } +} + +=item C + +Scans a device's link table and caches a copy. + +=cut + +sub scan_link_table +{ + my ($self,$success_callback,$failure_callback) = @_; + $$self{_mem_activity} = 'scan'; + $$self{_success_callback} = ($success_callback) ? $success_callback : undef; + $$self{_failure_callback} = ($failure_callback) ? $failure_callback : undef; + $self->health('out-of-sync'); # allow acknowledge to set otherwise + if($self->isa('Insteon::ALDB_i1')) { + $self->_peek('0FF8',0); + } else { + $self->send_read_aldb('0000'); + } +} + +=item C + +Deletes a specific link from a device. Generally called by C. + +=cut + +sub delete_link +{ + my ($self, $parms_text) = @_; + my %link_parms; + if ($parms_text eq 'ok' or $parms_text eq 'fail'){ + %link_parms = %{$self->{callback_parms}}; + $$self{callback_parms} = undef; + $link_parms{aldb_check} = $parms_text; + } + elsif (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; + if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ + ## Check whether ALDB is in sync + $self->{callback_parms} = \%link_parms; + $$self{_aldb_unchanged_callback} = '&Insteon::AllLinkDatabase::delete_link('.$$self{device}->{object_name}."->_aldb, 'ok')"; + $$self{_aldb_changed_callback} = '&Insteon::AllLinkDatabase::delete_link('.$$self{device}->{object_name}."->_aldb, 'fail')"; + $self->query_aldb_delta("check"); + } elsif ($link_parms{aldb_check} eq "fail"){ + &::print_log("[Insteon::AllLinkDatabase] WARN: Link NOT deleted, please rescan this device and sync again."); + if ($link_parms{callback}) + { + package main; + eval($link_parms{callback}); + &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } elsif ($link_parms{address} && $link_parms{aldb_check} eq "ok") + { + &main::print_log("[Insteon::AllLinkDatabase] Now deleting link [0x$link_parms{address}]"); + $$self{_mem_activity} = 'delete'; + $$self{pending_aldb}{address} = $link_parms{address}; + if($self->isa('Insteon::ALDB_i1')) { + $self->_peek($link_parms{address},0); + } else { + $self->_write_delete($link_parms{address}); + } + + } + elsif ($link_parms{aldb_check} eq "ok") + { + my $insteon_object = $link_parms{object}; + my $deviceid = ($insteon_object) ? $insteon_object->device_id : $link_parms{deviceid}; + my $groupid = $link_parms{group}; + $groupid = '01' unless $groupid; + my $is_controller = ($link_parms{is_controller}) ? 1 : 0; + my $subaddress = ($link_parms{data3}) ? $link_parms{data3} : '00'; + # get the address via lookup into the hash + my $key = $self->get_linkkey($deviceid, $groupid, $is_controller, $subaddress); + my $address = $$self{aldb}{$key}{address}; + if ($address) + { + &main::print_log("[Insteon::AllLinkDatabase] Now deleting link [0x$address] with the following data" + . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller, subaddress=$subaddress"); + # now, alter the flags byte such that the in_use flag is set to 0 + $$self{_mem_activity} = 'delete'; + $$self{pending_aldb}{deviceid} = lc $deviceid; + $$self{pending_aldb}{group} = $groupid; + $$self{pending_aldb}{is_controller} = $is_controller; + $$self{pending_aldb}{address} = $address; + $$self{pending_aldb}{data3} = $subaddress; + if($self->isa('Insteon::ALDB_i1')) { + $self->_peek($address,0); + } else { + $self->_write_delete($address); + } + } + else + { + &main::print_log('[Insteon::AllLinkDatabase] WARN: (' . $$self{device}->get_object_name + . ') attempt to delete link that does not exist!' + . " deviceid=$deviceid, groupid=$groupid, is_controller=$is_controller, subaddress=$subaddress"); + if ($link_parms{callback}) + { + package main; + eval($link_parms{callback}); + &::print_log("[Insteon::AllLinkDatabase] error encountered during delete_link callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDataBase; + } + } + } +} + +=item C + +Reviews the cached version of the link database for the device and removes +links from THIS device which are not present in the mht file, link to non-existant +devices, or which are only half-links. + +Since this routine only processes this device, it is best to run the voice +command 'delete all orphan links' from the interface so that all devices are +scanned and processed. + +=cut + +sub delete_orphan_links +{ + my ($self, $audit_mode, $failure_callback) = @_; + @{$$self{delete_queue}} = (); # reset the work queue + $$self{delete_queue_processed} = 0; + my $selfname = $$self{device}->get_object_name; + + # first, make sure that the health of ALDB is ok + if ($self->health ne 'good' || $$self{device}->is_deaf) { + my $sent_to_failure = 0; + if ($$self{device}->is_deaf) { + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Will not delete links on deaf device: $selfname"); + } + elsif ($self->health eq 'empty'){ + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: Skipping $selfname, because it has no links"); + } + else { + ::print_log("[Insteon::AllLinkDatabase] Delete orphan links: skipping $selfname because health: " + . $self->health . ". Please rescan the link table of this device and rerun delete " + . "orphans if necessary"); + #log the failure + $sent_to_failure = 1; + if ($failure_callback) { + package main; + eval ($failure_callback); + &::print_log("[Insteon::AllLinkDatabase] error in delete orphans failure callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } + if (!$$self{device}->isa('Insteon_PLM') && !$sent_to_failure){ + $self->_process_delete_queue(); + } + return; + } + + # Loop through the device's ALDB table + LINKKEY: for my $linkkey (keys %{$$self{aldb}}) { + # Skip empty addresses + next LINKKEY if ($linkkey eq 'empty'); + + # Define delete request + my %delete_req = (callback => "$selfname->_aldb->_process_delete_queue()", + failure_callback => $failure_callback); + + # Delete duplicate entries + if ($linkkey eq 'duplicates') { + push my @duplicate_addresses, @{$$self{aldb}{duplicates}}; + foreach (@duplicate_addresses) { + %delete_req = (%delete_req, address => $_, cause => "it is a duplicate record"); + push @{$$self{delete_queue}}, \%delete_req; + } + next LINKKEY; + } + + # Initialize Variables + my ($linked_device, $plm_scene, $controller_object, $link_defined, + $controller_id, $responder_id, $link_data3, $recip_data3, + $group_object, $data3_object); + my $group = lc $$self{aldb}{$linkkey}{group}; + my $is_controller = $$self{aldb}{$linkkey}{is_controller}; + my $data3 = lc $$self{aldb}{$linkkey}{data3}; + my $deviceid = lc $$self{aldb}{$linkkey}{deviceid}; + my $self_id = lc $$self{device}->device_id; + my $interface_id = $self_id; + $interface_id = lc $$self{device}->interface->device_id if (!$$self{device}->isa('Insteon_PLM')); + $linked_device = Insteon::get_object($deviceid,'01'); + $linked_device = Insteon::active_interface() if ($deviceid eq $interface_id); + $group_object = ($is_controller) ? Insteon::get_object($self_id, $group) : Insteon::get_object($deviceid, $group); + $data3_object = Insteon::get_object($self_id, $data3); + %delete_req = (%delete_req, deviceid => $deviceid, group => $group, + is_controller => $is_controller, data3 => $data3); + + # Is the linked device defined in MH? + if (! ref $linked_device) { + $delete_req{cause} = "no device with deviceid: $deviceid could be found"; + push @{$$self{delete_queue}}, \%delete_req; + next LINKKEY; + } + + # If link is a PLM Scene, is the PLM Scene defined in MH? + if (($linked_device->isa("Insteon_PLM") and !$is_controller)|| + ($$self{device}->isa("Insteon_PLM") and $is_controller)) { + $plm_scene = Insteon::get_object('000000', $group); + if (!ref $plm_scene && $group ne '01' && $group ne '00') { + $delete_req{cause} = "no plm scene for group $group could be found"; + push @{$$self{delete_queue}}, \%delete_req; + next LINKKEY; + } + } + + # Is this link defined in MH? 3-Step Process + # Define variables based on type of link + $controller_id = ($is_controller) ? $self_id : lc $linked_device->device_id; + $responder_id = ($is_controller) ? lc $linked_device->device_id : $self_id; + $controller_object = Insteon::get_object($controller_id,$group); + $controller_object = $plm_scene if (ref $plm_scene); + + # First, iterate over the controller object members to find the link definition + MEMBERS: foreach my $member_ref (keys %{$$controller_object{members}}) { + my $member = $$controller_object{members}{$member_ref}{object}; + if ($member->isa('Light_Item')) { + my @lights = $member->find_members('Insteon::BaseLight'); + $member = $lights[0] if (@lights); # pick the first + } + # For resp, D3 = resp group; For cont, D3 = cont group + $link_data3 = ($is_controller) ? $group : $member->group; + if (lc($member->device_id) eq $responder_id) { + if ($data3 eq $link_data3){ + $link_defined = 1; + $recip_data3 = ($is_controller) ? $member->group : $group; + last MEMBERS; + } + elsif (($link_data3 eq '00' || $link_data3 eq '01') && + ($data3 eq '00' || $data3 eq '01' )){ + # Allow for 00 or 01 interchangability + $link_defined = 1; + $recip_data3 = ($is_controller) ? $member->group : $group; + last MEMBERS; + } + } + } + + # Second, is this a PLM->Device, Device->PLM link, these are not members + if ($$self{device}->isa("Insteon_PLM")){ + if ($is_controller && ($group eq '00' || $group eq '01')){ + #Valid Controller for PLM->Device link + $delete_req{skip} = "$selfname -- Skipping reciprocal link check for controller group 00 or 01 link to " + . $linked_device->get_object_name; + next LINKKEY; + } + elsif (!$is_controller && ref $group_object){ + #Valid Responder for Device->PLM link + $link_defined = 1; + $recip_data3 = $group; + } + } + elsif($deviceid eq $interface_id && (ref $data3_object || ($data3 eq '00' || $data3 eq '01'))){ + if ($is_controller && ref $group_object){ + #Valid Controller for Device->PLM link + $link_defined = 1; + $recip_data3 = '00'; + } + elsif (!$is_controller && ($group eq '00' || $group eq '01')) { + #Valid Responder for PLM->Device link + $link_defined = 1; + $recip_data3 = '00'; + } + + } + + # Third, delete link if not defined + if (!$link_defined){ + $delete_req{cause} = "link is not defined in MisterHouse"; + push @{$$self{delete_queue}}, \%delete_req; + next LINKKEY; + } + + # Do not delete links to deaf devices + if ($linked_device->is_deaf) { + $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on deaf device " . $linked_device->get_object_name; + next LINKKEY; + } + + # Do not delete links to unhealthy devices + if ($linked_device->_aldb->health ne 'good' && $linked_device->_aldb->health ne 'empty') { + $delete_req{skip} = "$selfname -- Skipping check for reciprocal links on " + . $linked_device->get_object_name . " because aldb health of that device is " + . $linked_device->_aldb->health . ". Please rescan this device."; + next LINKKEY; + } + + # Do not delete responder links from the PLM (prevents locking i2CS devices) + if ($linked_device->isa("Insteon_PLM") and !$is_controller && ($group eq '00' || $group eq '01')) { + $delete_req{skip} = "$selfname -- Skipping check for reciprocal controller link on PLM for group 00 or 01."; + next LINKKEY; + } + + # Does a reciprocal link exist? + if (! $linked_device->has_link($$self{device},$group,($is_controller) ? 0 : 1, lc $recip_data3)) { + $delete_req{cause} = "no reciprocal link was found on " . $linked_device->get_object_name; + push @{$$self{delete_queue}}, \%delete_req; + } + + } # /LINKKEY Loop + my $index = 0; + foreach (@{$$self{delete_queue}}){ + my %delete_req = %{$_}; + my $audit_text = "(AUDIT)" if ($audit_mode); + my $log_text; + if ($delete_req{skip}){ + $log_text = "[Insteon::AllLinkDatabase] $audit_text " . $delete_req{skip}; + splice @{$$self{delete_queue}}, $index, 1; + } else { + $log_text = "[Insteon::AllLinkDatabase] $audit_text Deleting the following link on $selfname because "; + $log_text .= $delete_req{cause} . "\n"; + PRINT: for (keys %delete_req) { + next PRINT if (($_ eq 'cause') || ($_ eq 'callback') || + ($_ eq 'failure_callback')); + $log_text .= "$_ = $delete_req{$_}; "; + } + if ($delete_req{deviceid}){ + my $reciprocal_object = Insteon::get_object($delete_req{deviceid}, '01'); + if (!$delete_req{is_controller}){ + $reciprocal_object = Insteon::get_object($delete_req{deviceid}, $delete_req{group}); + } + if (ref $reciprocal_object) { + $log_text .= "linked device name= " . $reciprocal_object->get_object_name; + } + } + $index ++; + } + ::print_log($log_text); + } + if ($audit_mode) { + @{$$self{delete_queue}} = (); + } + else { + ::print_log("[Insteon::AllLinkDatabase] ## Begin processing delete queue for: $selfname"); + } + if (!$$self{device}->isa('Insteon_PLM')) { + $self->_process_delete_queue(); + } +} + +sub _process_delete_queue { + my ($self) = @_; + my $num_in_queue = @{$$self{delete_queue}}; + if ($num_in_queue) + { + my $delete_req_ptr = shift(@{$$self{delete_queue}}); + my %delete_req = %$delete_req_ptr; + if ($delete_req{address}) + { + &::print_log("[Insteon::AllLinkDatabase] (#$num_in_queue) " . $$self{device}->get_object_name . " now deleting duplicate record at address " + . $delete_req{address}); + } + else + { + &::print_log("[Insteon::AllLinkDatabase] (#$num_in_queue) " . $$self{device}->get_object_name . " now deleting orphaned link w/ details: " + . (($delete_req{is_controller}) ? "controller" : "responder") + . ", " . (($delete_req{object}) ? "device=" . $delete_req{object}->get_object_name + : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}, cause=$delete_req{cause}"); + } + $self->delete_link(%delete_req); + $$self{delete_queue_processed}++; + } + else + { + &::print_log("[Insteon::AllLinkDatabase] Nothing else to do for " . $$self{device}->get_object_name . " after deleting " + . $$self{delete_queue_processed} . " links") if $self->{device}->debuglevel(1, 'insteon'); + $$self{device}->interface->_aldb->_process_delete_queue($$self{delete_queue_processed}); + } +} + +=item C + +Adds address to the duplicate link hash. Called as part of C. + +=cut + +sub add_duplicate_link_address +{ + my ($self, $address) = @_; + + unshift @{$$self{aldb}{duplicates}}, $address; + + # now, keep the list sorted! + @{$$self{aldb}{duplicates}} = sort(@{$$self{aldb}{duplicates}}); + +} + +=item C + +Removes address from the duplicate link hash. Called as part of C. + +=cut + +sub delete_duplicate_link_address +{ + my ($self, $address) = @_; + my $num_duplicate_link_addresses = 0; + + $num_duplicate_link_addresses = @{$$self{aldb}{duplicates}} if (defined $$self{aldb}{duplicates}); + if ($num_duplicate_link_addresses) + { + my @temp_duplicates = (); + foreach my $temp_address (@{$$self{aldb}{duplicates}}) + { + if ($temp_address ne $address) + { + push @temp_duplicates, $temp_address; + } + } + # keep it sorted + @{$$self{aldb}{duplicates}} = sort(@temp_duplicates); + } +} + +=item C + +Adds address to the empty link hash. Called as part of C +or C. + +=cut + +sub add_empty_address +{ + my ($self, $address) = @_; + # before adding it, make sure that it isn't already in the list!! + my $num_addresses = 0; + $num_addresses = @{$$self{aldb}{empty}} if (defined $$self{aldb}{empty}); + my $exists = 0; + if ($num_addresses and $address) + { + foreach my $temp_address (@{$$self{aldb}{empty}}) + { + if ($temp_address eq $address) + { + $exists = 1; + last; + } + } + } + # add it to the list if it doesn't exist + if (!($exists) and $address) + { + unshift @{$$self{aldb}{empty}}, $address; + } + + # now, keep the list sorted! + @{$$self{aldb}{empty}} = sort(@{$$self{aldb}{empty}}); + +} + +=item C + +Returns the highest empty link address, or if no empty addresses exist, returns +the highest unused address. Called as part of C or +C.. + +=cut + +sub get_first_empty_address +{ + my ($self) = @_; + + # NOTE: The issue here is that we give up an address from the list + # with the assumption that it will be made non-empty; + # So, if there is a problem during update/add, then will have + # a non-empty, but non-functional entry + my $first_address = pop @{$$self{aldb}{empty}}; + + if (!($first_address)) + { + # then, cycle through all of the existing non-empty addresses + # to find the lowest one and then decrement by 8 + # + # TO-DO: factor in appropriate use of the "highwater" flag + # + my $high_address = 0xffff; + for my $key (keys %{$$self{aldb}}) + { + next if $key eq 'empty' or $key eq 'duplicates'; + my $new_address = hex($$self{aldb}{$key}{address}); + if( $new_address and $new_address < $high_address ) { + $high_address = $new_address; + } + } + $first_address = ($high_address > 0) ? sprintf('%04x', $high_address - 8) : 0; + main::print_log("[Insteon::AllLinkDatabase] DEBUG4: No empty link entries; using next lowest link address [" + .$first_address."]") if $self->{device}->debuglevel(4, 'insteon'); + } else { + main::print_log("[Insteon::AllLinkDatabase] DEBUG4: Found empty address [" + .$first_address."] in empty array") if $self->{device}->debuglevel(4, 'insteon'); + } + + return $first_address; +} + +=item C + +Adds the link to the device's ALDB. Generally called from the "sync links" or +"link to interface" voice commands. + +=cut + +sub add_link +{ + my ($self, $parms_text) = @_; + my %link_parms; + if ($parms_text eq 'ok' or $parms_text eq 'fail'){ + %link_parms = %{$self->{callback_parms}}; + $$self{callback_parms} = undef; + $link_parms{aldb_check} = $parms_text; + } + elsif (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + my $device_id; + my $insteon_object = $link_parms{object}; + my $group = $link_parms{group}; + if (!(defined($insteon_object))) + { + $device_id = lc $link_parms{deviceid}; + $insteon_object = &Insteon::get_object($device_id, $group); + } + else + { + $device_id = lc $insteon_object->device_id; + } + my $is_controller = ($link_parms{is_controller}) ? 1 : 0; + + # For I2CS devices the default data3 for links is 01 + # For all other devices the default data3 for links is 00 + my $data3_default = '00'; + if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { + $data3_default = '01'; + } + my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; + $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); + + # check whether the link already exists + my $key = $self->get_linkkey($device_id, $group, $is_controller, $data3); + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; + if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ + ## Check whether ALDB is in sync + $self->{callback_parms} = \%link_parms; + $$self{_aldb_unchanged_callback} = '&Insteon::AllLinkDatabase::add_link('.$$self{device}->{object_name}."->_aldb, 'ok')"; + $$self{_aldb_changed_callback} = '&Insteon::AllLinkDatabase::add_link('.$$self{device}->{object_name}."->_aldb, 'fail')"; + $self->query_aldb_delta("check"); + } elsif ($link_parms{aldb_check} eq "fail"){ + &::print_log("[Insteon::AllLinkDatabase] WARN: Link NOT added, please rescan this device and sync again."); + if ($link_parms{callback}) + { + package main; + eval($link_parms{callback}); + &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } + elsif (defined $$self{aldb}{$key} && defined $$self{aldb}{$key}{inuse}) + { + &::print_log("[Insteon::AllLinkDatabase] WARN: attempt to add link to " + . $$self{device}->get_object_name + . " that already exists! object=" . $insteon_object->get_object_name + . ", group=$group, is_controller=$is_controller, data3=$data3"); + if ($link_parms{callback}) + { + package main; + eval($link_parms{callback}); + &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " + . $$self{device}->get_object_name . ":" . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } + elsif ($link_parms{aldb_check} eq "ok") + { + # strip optional % sign to append on_level + my $on_level = $link_parms{on_level}; + $on_level =~ s/(\d)%?/$1/; + $on_level = '100' unless defined($on_level); # 100% == on is the default + # strip optional s (seconds) to append ramp_rate + my $ramp_rate = $link_parms{ramp_rate}; + $ramp_rate =~ s/(\d)s?/$1/; + $ramp_rate = '0.1' unless $ramp_rate; # 0.1s is the default + # get the first available memory location + my $address = $self->get_first_empty_address(); + if ($address) + { + &::print_log("[Insteon::AllLinkDatabase] DEBUG2: adding link record to " . $$self{device}->get_object_name + . " light level controlled by " . $insteon_object->get_object_name + . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") + if $self->{device}->debuglevel(2, 'insteon'); + my ($data1, $data2); + if($link_parms{is_controller}) { + $data1 = '03'; #application retries == 3 + $data2 = '00'; #ignored for controller entries + } else { + $data1 = &Insteon::DimmableLight::convert_level($on_level); + $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; + } + #data3 is defined above + $$self{_mem_activity} = 'add'; + $self->_write_link($address, $device_id, $group, $is_controller, $data1, $data2, $data3); + # TO-DO: ensure that pop'd address is restored back to queue if the transaction fails + } + else + { + &::print_log("[Insteon::AllLinkDatabase] ERROR: adding link record failed because " + . $$self{device}->get_object_name + . " does not have a record of the first empty ALDB record." + . " Please rescan this device's link table") + if $self->{device}->debuglevel(1, 'insteon'); + + if ($$self{_success_callback}) + { + package main; + eval ($$self{_success_callback}); + &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } + } +} + +=item C + +Updates the on_level and/or ramp_rate associated with a link to match the defined +value in MisterHouse. Generally called from the "sync links" voice command. + +=cut + +sub update_link +{ + my ($self, %link_parms) = @_; + if ($_[1] eq 'ok' or $_[1] eq 'fail'){ + %link_parms = %{$self->{callback_parms}}; + $$self{callback_parms} = undef; + $link_parms{aldb_check} = $_[1]; + } + my $insteon_object = $link_parms{object}; + my $group = $link_parms{group}; + my $is_controller = ($link_parms{is_controller}) ? 1 : 0; + # strip optional % sign to append on_level + my $on_level = $link_parms{on_level}; + $on_level =~ s/(\d+)%?/$1/; + # strip optional s (seconds) to append ramp_rate + my $ramp_rate = $link_parms{ramp_rate}; + $ramp_rate =~ s/(\d)s?/$1/; + &::print_log("[Insteon::AllLinkDatabase] updating " . $$self{device}->get_object_name . " light level controlled by " . $insteon_object->get_object_name + . " and group: $group with on level: $on_level and ramp rate: $ramp_rate") if $self->{device}->debuglevel(1, 'insteon'); + my $data1 = &Insteon::DimmableLight::convert_level($on_level); + my $data2 = ($$self{device}->isa('Insteon::DimmableLight')) ? &Insteon::DimmableLight::convert_ramp($ramp_rate) : '00'; + + # For I2CS devices the default data3 for links is 01 + # For all other devices the default data3 for links is 00 + my $data3_default = '00'; + if ($insteon_object->can('engine_version') && $insteon_object->engine_version eq 'I2CS') { + $data3_default = '01'; + } + my $data3 = ($link_parms{data3}) ? $link_parms{data3} : '00'; + $data3 = $data3_default if ($data3 eq '00' || $data3 eq '01'); + + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; + + my $deviceid = $insteon_object->device_id; + my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); + if (!defined($link_parms{aldb_check}) && (!$$self{device}->isa('Insteon_PLM'))){ + ## Check whether ALDB is in sync + $self->{callback_parms} = \%link_parms; + $$self{_aldb_unchanged_callback} = '&Insteon::AllLinkDatabase::update_link('.$$self{device}->{object_name}."->_aldb, 'ok')"; + $$self{_aldb_changed_callback} = '&Insteon::AllLinkDatabase::update_link('.$$self{device}->{object_name}."->_aldb, 'fail')"; + $self->query_aldb_delta("check"); + } elsif ($link_parms{aldb_check} eq "fail"){ + &::print_log("[Insteon::AllLinkDatabase] WARN: Cannot update link, please rescan this device and sync again."); + if ($link_parms{callback}) + { + package main; + eval($link_parms{callback}); + &::print_log("[Insteon::AllLinkDatabase] failure occurred in callback eval for " . $$self{device}->get_object_name . ":" . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } + elsif (defined $$self{aldb}{$key} && $link_parms{aldb_check} eq "ok"){ + my $address = $$self{aldb}{$key}{address}; + $$self{_mem_activity} = 'update'; + $self->_write_link($address, $deviceid, $group, $is_controller, $data1, $data2, $data3); + } else { + &::print_log("[Insteon::AllLinkDatabase] ERROR: updating link record failed because " + . $$self{device}->get_object_name + . " does not have an existing ALDB entry key=$key") + if $self->{device}->debuglevel(1, 'insteon'); + + if ($$self{_success_callback}) + { + package main; + eval ($$self{_success_callback}); + &::print_log("[Insteon::AllLinkDatabase] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::AllLinkDatabase; + } + } +} + +=item C + +Prints a human readable form of MisterHouse's cached version of a device's ALDB +to the print log. Called as part of the "scan links" voice command +or in response to the "log links" voice command. + +=cut + +sub log_alllink_table +{ + my ($self) = @_; + my %aldb; + + &::print_log("[Insteon::AllLinkDatabase] Link table for " + . $$self{device}->get_object_name + . " health: " . $self->health); + + # We want to log links sorted by ALDB address. Since the ALDB + # addresses are scattered throughout the %{$$self{aldb}} hash, + # and it is not easy to obtain them in a linear manner, + # we build a new data structure that will allow us to easily + # traverse the ALDB by address in a sorted manner. The new + # data structure is a bidimensional hash (%aldb) where rows + # are the ALDB addresses and the columns can be "empty" + # (indicates that the ALDB at the corresponding address is + # empty), "duplicate" (indicates that the ALDB at the + # corresponding address is a duplicate), or a hash key (which + # indicates that the ALDB at corresponding address contains + # a link). + foreach my $aldbkey (keys %{$$self{aldb}}) + { + if ($aldbkey eq "empty") + { + foreach my $address (@{$$self{aldb}{empty}}) + { + $aldb{$address}{empty} = undef; # Any value will do + } + } + elsif ($aldbkey eq "duplicates") + { + foreach my $address (@{$$self{aldb}{duplicates}}) + { + $aldb{$address}{duplicate} = undef; # Any value will do + } + } + else + { + $aldb{$$self{aldb}{$aldbkey}{address} }{$aldbkey} = $$self{aldb}{$aldbkey}; + } + } + + # Finally traverse the ALDB, but this time sorted by ALDB address + if ($self->health eq 'good') + { + foreach my $address (sort keys %aldb) + { + my $log_msg = "[Insteon::AllLinkDatabase] [0x$address] "; + + if (exists $aldb{$address}{empty}) + { + $log_msg .= "is empty"; + } + elsif (exists $aldb{$address}{duplicate}) + { + $log_msg .= "holds a duplicate entry"; + } + else + { + my ($key) = keys %{$aldb{$address} }; # There's only 1 key + my $aldb_entry = $aldb{$address}{$key}; + my $is_controller = $aldb_entry->{is_controller}; + my $device; + + if ($$self{device}->interface()->device_id() + && ($$self{device}->interface()->device_id() + eq $aldb_entry->{deviceid})) + { + $device = $$self{device}->interface; + } + else + { + $device = &Insteon::get_object($aldb_entry->{deviceid},'01'); + } + my $object_name = ($device) ? $device->get_object_name : $aldb_entry->{deviceid}; + + my $on_level = 'unknown'; + if (defined $aldb_entry->{data1}) + { + if ($aldb_entry->{data1}) + { + $on_level = int((hex($aldb_entry->{data1})*100/255) + .5) . "%"; + } + else + { + $on_level = '0%'; + } + } + + my $rspndr_group = $aldb_entry->{data3}; + $rspndr_group = '01' if $rspndr_group eq '00'; + + my $ramp_rate = 'unknown'; + if ($aldb_entry->{data2}) + { + if (!($$self{device}->isa('Insteon::DimmableLight')) + or (!$is_controller and ($rspndr_group != '01'))) + { + $ramp_rate = 'none'; + $on_level = $on_level eq '0%' ? 'off' : 'on'; + } + else + { + $ramp_rate = &Insteon::DimmableLight::get_ramp_from_code($aldb_entry->{data2}) . "s"; + } + } + + $log_msg .= $is_controller ? "contlr($aldb_entry->{group}) " + . "record to $object_name, " + . "(d1:$aldb_entry->{data1}, " + . "d2:$aldb_entry->{data2}, " + . "d3:$aldb_entry->{data3})" + : "rspndr($rspndr_group) record to $object_name " + . "($aldb_entry->{group}): onlevel=$on_level " + . "and ramp=$ramp_rate " + . "(d3:$aldb_entry->{data3})"; + } + + &::print_log($log_msg); + } + } + else + { + main::print_log("[Insteon::AllLinkDatabase] ALDB is ".$self->health." and will not be listed"); + } +} + +=item C + +Checks and returns true if a link with the passed details exists on the device +or false if it does not. Generally called as part of C. + +=cut + +sub has_link +{ + my ($self, $insteon_object, $group, $is_controller, $data3) = @_; + my $deviceid; + if ($insteon_object->isa('Insteon::AllLinkDatabase')) { + $deviceid = $$insteon_object{device}->device_id; + } else { + $deviceid = lc $insteon_object->device_id; + } + my $key = $self->get_linkkey($deviceid, $group, $is_controller, $data3); + return (defined $$self{aldb}{$key}); +} + +=back + +=head2 INI PARAMETERS + +None + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +package Insteon::ALDB_i1; + +=head1 B + +=head2 SYNOPSIS + +Unique class for storing a cahced copy of a verion i1 device's ALDB. + +=head2 DESCRIPTION + +Generally this object should be interacted with through the insteon objects and +not by directly calling any of the following methods. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +use strict; + +@Insteon::ALDB_i1::ISA = ('Insteon::AllLinkDatabase'); + +=item C + +Instantiate a new object. + +=cut + +sub new +{ + my ($class,$device) = @_; + + my $self = new Insteon::AllLinkDatabase($device); + bless $self,$class; + $self->aldb_version("I1"); + return $self; +} + +sub _on_poke +{ + my ($self,%msg) = @_; + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); + if (($$self{_mem_activity} eq 'update') or ($$self{_mem_activity} eq 'add')) + { + if ($$self{_mem_action} eq 'aldb_flag') + { + $$self{_mem_action} = 'aldb_group'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_group') + { + $$self{_mem_action} = 'aldb_devhi'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_devhi') + { + $$self{_mem_action} = 'aldb_devmid'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_devmid') + { + $$self{_mem_action} = 'aldb_devlo'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_devlo') + { + $$self{_mem_action} = 'aldb_data1'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_data1') + { + $$self{_mem_action} = 'aldb_data2'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_data2') + { + $$self{_mem_action} = 'aldb_data3'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_data3') + { + ## update the aldb records w/ the changes that were made + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); + $$self{aldb}{$aldbkey}{data1} = $$self{pending_aldb}{data1}; + $$self{aldb}{$aldbkey}{data2} = $$self{pending_aldb}{data2}; + $$self{aldb}{$aldbkey}{data3} = $$self{pending_aldb}{data3}; + $$self{aldb}{$aldbkey}{inuse} = 1; # needed so that restore string will preserve record + if ($$self{_mem_activity} eq 'add') + { + $$self{aldb}{$aldbkey}{is_controller} = $$self{pending_aldb}{is_controller}; + $$self{aldb}{$aldbkey}{deviceid} = lc $$self{pending_aldb}{deviceid}; + $$self{aldb}{$aldbkey}{group} = lc $$self{pending_aldb}{group}; + $$self{aldb}{$aldbkey}{address} = $$self{pending_aldb}{address}; + $self->health("good"); + } + # clear out mem_activity flag + $$self{_mem_activity} = undef; + $self->health("good"); + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } + } + elsif ($$self{_mem_activity} eq 'update_local') + { + if ($$self{_mem_action} eq 'local_onlevel') + { + $$self{_mem_lsb} = '21'; + $$self{_mem_action} = 'local_ramprate'; + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'local_ramprate') + { + if ($$self{device}->isa('Insteon::KeyPadLincRelay') or $$self{device}->isa('Insteon::KeyPadLinc')) + { + # update from eeprom--only a kpl issue + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'do_read_ee'); + $self->_send_cmd($message); + } + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } + } + elsif ($$self{_mem_activity} eq 'update_flags') + { + # update from eeprom--only a kpl issue + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'do_read_ee'); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_activity} eq 'delete') + { + # clear out mem_activity flag + $$self{_mem_activity} = undef; + # add the address of the deleted link to the empty list + $self->add_empty_address($$self{pending_aldb}{address}); + # and, remove from the duplicates list (if it is a member) + $self->delete_duplicate_link_address($$self{pending_aldb}{address}); + if (exists $$self{pending_aldb}{deviceid}) + { + my $key = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); + delete $$self{aldb}{$key}; + } + $self->health("good"); + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } +} + +sub _on_peek +{ + my ($self,%msg) = @_; + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'peek'); + if ($msg{is_extended}) { + &::print_log("[Insteon::ALDB_i1]: extended peek for " . $$self{device}->{object_name} + . " is " . $msg{extra}) if $self->{device}->debuglevel(1, 'insteon'); + } + else + { + if ($$self{_mem_action} eq 'aldb_peek') + { + if ($$self{_mem_activity} eq 'scan') + { + $$self{_mem_action} = 'aldb_flag'; + # if the device is responding to the peek, then init the link table + # if at the very start of a scan + if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8' + && !$$self{_stress_test_act}) + { + # reinit the aldb hash as there will be a new one + $$self{aldb} = undef; + # reinit the empty address list + @{$$self{aldb}{empty}} = (); + # and, also the duplicates list + @{$$self{aldb}{duplicates}} = (); + } + } + elsif ($$self{_mem_activity} eq 'update') + { + $$self{_mem_action} = 'aldb_data1'; + } + elsif ($$self{_mem_activity} eq 'update_local') + { + $$self{_mem_action} = 'local_onlevel'; + } + elsif ($$self{_mem_activity} eq 'update_flags') + { + $$self{_mem_action} = 'update_flags'; + } + elsif ($$self{_mem_activity} eq 'delete') + { + $$self{_mem_action} = 'aldb_flag'; + } + elsif ($$self{_mem_activity} eq 'add') + { + $$self{_mem_action} = 'aldb_flag'; + } + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_flag') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + my $flag = hex($msg{extra}); + $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; + $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; + $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; + if ($$self{_stress_test_act} && !($$self{pending_aldb}{highwater})){ + ::print_log("[Insteon::ALDB_i1] You need to create a link on this device before running stress_test"); + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; + $$self{_stress_test_act} = 0; + $$self{device}->stress_test(); + } + elsif (!($$self{pending_aldb}{highwater})) + { + # since this is the last unused memory location, then add it to the empty list + $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); + $$self{_mem_action} = undef; + # clear out mem_activity flag + $$self{_mem_activity} = undef; + if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'f8') + { + # set health as empty for now + $self->health("empty"); + } + else + { + $self->health("good"); + } + + &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " completed link memory scan") + if $self->{device}->debuglevel(1, 'insteon'); + $self->health("good"); + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } + elsif ($$self{pending_aldb}{inuse}) + { + $$self{pending_aldb}{flag} = $msg{extra}; + ## confirm that we have a high-water mark; otherwise stop + $$self{pending_aldb}{address} = $$self{_mem_msb} . $$self{_mem_lsb}; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{_mem_action} = 'aldb_group'; + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } else { + $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); + if ($$self{_mem_activity} eq 'scan'){ + my $newaddress = sprintf("%04X", hex($$self{_mem_msb} . $$self{_mem_lsb}) - 8); + $$self{pending_aldb} = undef; + $self->_peek($newaddress); + } + } + } + elsif ($$self{_mem_activity} eq 'add') + { + # TO-DO!!! Eventually add the ability to set the highwater mark + # the below flags never reset the highwater mark so that + # the scanner will continue scanning extra empty records + my $flag = ($$self{pending_aldb}{is_controller}) ? 'E2' : 'A2'; + $$self{pending_aldb}{flag} = $flag; + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($flag); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_activity} eq 'delete') + { + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra('02'); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + } + elsif ($$self{_mem_action} eq 'aldb_group') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{pending_aldb}{group} = lc $msg{extra}; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{_mem_action} = 'aldb_devhi'; + $message->extra($$self{_mem_lsb}); + } + else + { + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($$self{pending_aldb}{group}); + } + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_devhi') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{pending_aldb}{deviceid} = lc $msg{extra}; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{_mem_action} = 'aldb_devmid'; + $message->extra($$self{_mem_lsb}); + } + elsif ($$self{_mem_activity} eq 'add') + { + my $devid = substr($$self{pending_aldb}{deviceid},0,2); + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($devid); + } + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_devmid') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{pending_aldb}{deviceid} .= lc $msg{extra}; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{_mem_action} = 'aldb_devlo'; + $message->extra($$self{_mem_lsb}); + } + elsif ($$self{_mem_activity} eq 'add') + { + my $devid = substr($$self{pending_aldb}{deviceid},2,2); + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($devid); + } + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'aldb_devlo') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{pending_aldb}{deviceid} .= lc $msg{extra}; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{_mem_action} = 'aldb_data1'; + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_activity} eq 'add') + { + my $devid = substr($$self{pending_aldb}{deviceid},4,2); + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($devid); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + } + elsif ($$self{_mem_action} eq 'aldb_data1') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{_mem_action} = 'aldb_data2'; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{pending_aldb}{data1} = $msg{extra}; + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_activity} eq 'update' or $$self{_mem_activity} eq 'add') + { + # poke the new value + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($$self{pending_aldb}{data1}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + } + elsif ($$self{_mem_action} eq 'aldb_data2') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{pending_aldb}{data2} = $msg{extra}; + $$self{_mem_lsb} = sprintf("%02X", hex($$self{_mem_lsb}) + 1); + $$self{_mem_action} = 'aldb_data3'; + $message->extra($$self{_mem_lsb}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_activity} eq 'update' or $$self{_mem_activity} eq 'add') + { + # poke the new value + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($$self{pending_aldb}{data2}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + } + elsif ($$self{_mem_action} eq 'aldb_data3') + { + if ($$self{_mem_activity} eq 'scan') + { + &::print_log("[Insteon::ALDB_i1] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{pending_aldb}{data3} = $msg{extra}; + + if ($$self{_stress_test_act}){ + $$self{_stress_test_act} = 0; + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; + $$self{device}->stress_test(); + } + elsif ($$self{pending_aldb}{highwater}) + { + if ($$self{pending_aldb}{inuse}) + { + # save pending_aldb and then clear it out + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); + # check for duplicates + if (exists $$self{aldb}{$aldbkey} && $$self{aldb}{$aldbkey}{inuse}) + { + $self->add_duplicate_link_address($$self{pending_aldb}{address}); + } + else + { + %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; + } + } + else + { + $self->add_empty_address($$self{pending_aldb}{address}); + } + my $newaddress = sprintf("%04X", hex($$self{pending_aldb}{address}) - 8); + $$self{pending_aldb} = undef; + $self->_peek($newaddress); + } + } + elsif ($$self{_mem_activity} eq 'update' or $$self{_mem_activity} eq 'add') + { + # poke the new value + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($$self{pending_aldb}{data3}); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + } + elsif ($$self{_mem_action} eq 'local_onlevel') + { + my $device = $$self{device}; + my $on_level = $$device{_onlevel}; + $on_level = &Insteon::DimmableLight::convert_level($on_level); + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($on_level); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'local_ramprate') + { + my $device = $$self{device}; + my $ramp_rate = $$device{_ramprate}; + $ramp_rate = '1f' unless $ramp_rate; + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($ramp_rate); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + elsif ($$self{_mem_action} eq 'update_flags') + { + my $flags = $$self{_operating_flags}; + $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'poke'); + $message->extra($flags); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + else { + ::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name + . ": unhandled _mem_action=".$$self{_mem_action}) + if $self->{device}->debuglevel(1, 'insteon'); + } + } +} + +=item C + +Used to update the local on level and ramp rate of a device. Called by +L. + +=cut + +sub update_local_properties +{ + my ($self, $aldb_check) = @_; + if (defined($aldb_check)){ + $$self{_mem_activity} = 'update_local'; + $self->_peek('0032'); # 0032 is the address for the onlevel + } else { + $$self{_aldb_unchanged_callback} = '&Insteon::ALDB_i1::update_local_properties('.$$self{device}->{object_name}."->_aldb, 1)"; + $$self{_aldb_changed_callback} = '&Insteon::ALDB_i1::update_local_properties('.$$self{device}->{object_name}."->_aldb, 1)"; + $self->query_aldb_delta("check"); + } +} + +=item C + +Used to update the flags of a device. Called by L. + +=cut + +sub update_flags +{ + my ($self, $flags, $aldb_check) = @_; + return unless defined $flags; + if (defined($aldb_check)){ + $$self{_mem_activity} = 'update_flags'; + $$self{_operating_flags} = $flags; + $self->_peek('0023'); + } else { + $$self{_aldb_unchanged_callback} = '&Insteon::ALDB_i1::update_flags('.$$self{device}->{object_name}."->_aldb, '$flags', 1)"; + $$self{_aldb_changed_callback} = '&Insteon::ALDB_i1::update_flags('.$$self{device}->{object_name}."->_aldb, '$flags', 1)"; + $self->query_aldb_delta("check"); + } +} + +=item C + +Gets and returns the details of a link. Called by L. + +NOTE - This routine may be obsolete, its parent routine is not called by any code. + +=cut + +sub get_link_record +{ + my ($self,$link_key) = @_; + my %link_record = (); + %link_record = %{$$self{aldb}{$link_key}} if $$self{aldb}{$link_key}; + return %link_record; +} + +sub _write_link +{ + my ($self, $address, $deviceid, $group, $is_controller, $data1, $data2, $data3) = @_; + if ($address) + { + &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " address: $address found for device: $deviceid and group: $group"); + # change address for start of change to be address + offset + if ($$self{_mem_activity} eq 'update') + { + $address = sprintf('%04X',hex($address) + 5); + } + $$self{pending_aldb}{address} = $address; + $$self{pending_aldb}{deviceid} = lc $deviceid; + $$self{pending_aldb}{group} = lc $group; + $$self{pending_aldb}{is_controller} = $is_controller; + $$self{pending_aldb}{data1} = (defined $data1) ? lc $data1 : '00'; + $$self{pending_aldb}{data2} = (defined $data2) ? lc $data2 : '00'; + $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; + $self->_peek($address); + } + else + { + &::print_log("[Insteon::ALDB_i1] WARN: " . $$self{device}->get_object_name + . " write_link failure: no address available for record to device: $deviceid and group: $group" . + " and is_controller: $is_controller");; + if ($$self{_success_callback}) + { + package main; + eval ($$self{_success_callback}); + &::print_log("[Insteon::ALDB_i1] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::ALDB_i1; + } + } +} + +sub _peek +{ + my ($self, $address, $extended) = @_; + my $msb = substr($address,0,2); + my $lsb = substr($address,2,2); + if ($extended) + { + my $message = $self->device->derive_message('peek','insteon_ext_send', + $lsb . "0000000000000000000000000000"); + $self->interface->queue_message($message); + + } + else + { + $$self{_mem_lsb} = $lsb; + $$self{_mem_msb} = $msb; + $$self{_mem_action} = 'aldb_peek'; + &::print_log("[Insteon::ALDB_i1] " . $$self{device}->get_object_name . " accessing memory at location: 0x" . $address); + my $message = new Insteon::InsteonMessage('insteon_send', $$self{device}, 'set_address_msb'); + $message->extra($msb); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); +# $self->_send_cmd('command' => 'set_address_msb', 'extra' => $msb, 'is_synchronous' => 1); + } +} + +=back + +=head2 INI PARAMETERS + +None + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +package Insteon::ALDB_i2; + +=head1 B + +=head2 SYNOPSIS + +Unique class for storing a cahced copy of a verion i2 device's ALDB. + +=head2 DESCRIPTION + +Generally this object should be interacted with through the insteon objects and +not by directly calling any of the following methods. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +use strict; + +@Insteon::ALDB_i2::ISA = ('Insteon::AllLinkDatabase'); + +=item C + +Instantiate a new object. + +=cut + +sub new +{ + my ($class,$device) = @_; + + my $self = new Insteon::AllLinkDatabase($device); + bless $self,$class; + $self->aldb_version("I2"); + return $self; +} + +=item C + +Called as part of any process to read or write to a device's ALDB. + +=cut + +sub on_read_write_aldb +{ + my ($self, %msg) = @_; + my $clear_message = 1; #Default Action is to Clear the Current Message + &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for _mem_activity=".$$self{_mem_activity} + ." _mem_action=". $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + + if ($$self{_mem_action} eq 'aldb_i2read') + { + #This is an ACK. Will be followed by a Link Data message, so don't clear + $clear_message = 0; + #Only move to the next state if the received message is a device ack + #if the ack is dropped the retransmission logic will resend the request + if($msg{is_ack}) { + $$self{_mem_action} = 'aldb_i2readack'; + &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received ack") + if $self->{device}->debuglevel(3, 'insteon'); + } else { + #otherwise just ignore the message because it is out of sequence + &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] ack not received. " + . "ignoring message") if $self->{device}->debuglevel(3, 'insteon'); + } + + } + elsif ($$self{_mem_action} eq 'aldb_i2readack') + { + if($msg{is_ack}) { + &::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received duplicate ack. Ignoring.") + if $self->{device}->debuglevel(3, 'insteon'); + $clear_message = 0; + } elsif(length($msg{extra})<30) + { + &::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed: " + . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); + #can't clear message, if valid message doesn't arrive + #resend logic will kick in + $clear_message = 0; + } elsif ($$self{_mem_msb} . $$self{_mem_lsb} ne '0000' and + $$self{_mem_msb} . $$self{_mem_lsb} ne substr($msg{extra},6,4)){ + ::print_log("[Insteon::ALDB_i2] WARNING: Corrupted I2 response not processed, " + . " address received did not match address requested: " + . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) if $self->{device}->debuglevel(3, 'insteon'); + $$self{device}->corrupt_count_log(1) if $$self{device}->can('corrupt_count_log'); + #can't clear message, if valid message doesn't arrive + #resend logic will kick in + $clear_message = 0; + } + elsif ($$self{_stress_test_act}){ + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; + $$self{_stress_test_act} = 0; + $$self{device}->stress_test(); + } + else + { + # init the link table if at the very start of a scan + if (lc $$self{_mem_msb} eq '00' and lc $$self{_mem_lsb} eq '00') + { + main::print_log("[Insteon::ALDB_i2] DEBUG4: Start of scan; initializing aldb structure") + if $self->{device}->debuglevel(4, 'insteon'); + # reinit the aldb hash as there will be a new one + $$self{aldb} = undef; + # reinit the empty address list + @{$$self{aldb}{empty}} = (); + # and, also the duplicates list + @{$$self{aldb}{duplicates}} = (); + $$self{_mem_msb} = substr($msg{extra},6,2); + $$self{_mem_lsb} = substr($msg{extra},8,2); + } + + #$msg{extra} includes cmd2 at the beginning so cmd2.d1.d2.d3... + #e.g. 0001010fff00a2042042d3fe1c00cc + # 0:cmd2:00 - unused + # 2: D1:01 - unused + # 4: D2:01 - command (read aldb response) + # 6: D3:0fff - aldb address (first entry in this case) + #10: D5:00 - unused in responses + #12: D6:a2 - flags + #14: D7:04 - group number + #16: D8:11.31.a2 - device id + #22: D11:fe - link data 1 + #24: D12:1c - link data 2 + #26: D13:00 - link data 3 (unused) + #28: D14:cc - unused in i2; checksum in i2CS + + $$self{pending_aldb}{address} = substr($msg{extra},6,4); + + $$self{pending_aldb}{flag} = substr($msg{extra},12,2); + my $flag = hex($$self{pending_aldb}{flag}); + $$self{pending_aldb}{inuse} = ($flag & 0x80) ? 1 : 0; + $$self{pending_aldb}{is_controller} = ($flag & 0x40) ? 1 : 0; + $$self{pending_aldb}{highwater} = ($flag & 0x02) ? 1 : 0; + unless($$self{pending_aldb}{highwater}) + { + #highwater is set for every entry that has been used before + #highwater being 0 indicates entry has never been used (i.e. top of list) + # since this is the last unused memory location, then add it to the empty list + &::print_log("[Insteon::ALDB_i2] WARNING: highwater not set but marked inuse: " + . $$self{device}->get_object_name + . " [0x" . $$self{_mem_msb} . $$self{_mem_lsb} . "] received: " + . lc $msg{extra} . " for " . $$self{_mem_action}) + if(($$self{pending_aldb}{inuse}) and $self->{device}->debuglevel(3, 'insteon')); + main::print_log("[Insteon::ALDB_i2] DEBUG4: scan done; adding last address [" + . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") + if $self->{device}->debuglevel(4, 'insteon'); + $self->add_empty_address($$self{_mem_msb} . $$self{_mem_lsb}); + # scan done; clear out state flags + $$self{_mem_action} = undef; + $$self{_mem_activity} = undef; + if (lc $$self{_mem_msb} eq '0f' and lc $$self{_mem_lsb} eq 'ff') + { + # set health as empty for now + $self->health("empty"); + } + else + { + $self->health("good"); + } + + &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name + . " completed link memory scan: status: " . $self->health()) + if $self->{device}->debuglevel(1, 'insteon'); + $self->health("good"); + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } + else #($$self{pending_aldb}{highwater}) + { + unless($$self{pending_aldb}{inuse}) + { + main::print_log("[Insteon::ALDB_i2] DEBUG4: inuse flag == false; adding address [" + . $$self{_mem_msb} . $$self{_mem_lsb} ."] to empty array") + if $self->{device}->debuglevel(4, 'insteon'); + $self->add_empty_address($$self{pending_aldb}{address}); + } + else + { + $$self{pending_aldb}{group} = lc substr($msg{extra},14,2); + $$self{pending_aldb}{deviceid} = lc substr($msg{extra},16,6); + $$self{pending_aldb}{data1} = lc substr($msg{extra},22,2); + $$self{pending_aldb}{data2} = lc substr($msg{extra},24,2); + $$self{pending_aldb}{data3} = lc substr($msg{extra},26,2); + + # save pending_aldb and then clear it out + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); + # check for duplicates + if (exists $$self{aldb}{$aldbkey} && $$self{aldb}{$aldbkey}{inuse}) + { + main::print_log("[Insteon::ALDB_i2] DEBUG4: duplicate link found; adding address [" + . $$self{_mem_msb} . $$self{_mem_lsb} ."] to duplicates array") + if $self->{device}->debuglevel(4, 'insteon'); + $self->add_duplicate_link_address($$self{pending_aldb}{address}); + } + else + { + main::print_log("[Insteon::ALDB_i2] DEBUG4: active link found; adding address [" + . $$self{_mem_msb} . $$self{_mem_lsb} ."] to aldb") + if $self->{device}->debuglevel(4, 'insteon'); + %{$$self{aldb}{$aldbkey}} = %{$$self{pending_aldb}}; + } + } + + #keep going; request the next record + $self->send_read_aldb(sprintf("%04x", hex($$self{pending_aldb}{address}) - 8)); + + } #($$self{pending_aldb}{highwater}) + } #else $msg{extra} !< 30 + } + elsif ($$self{_mem_action} eq 'aldb_i2writeack') + { + unless ($$self{_mem_activity} eq 'delete') { + ## update the aldb records w/ the changes that were made + my $aldbkey = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); + $$self{aldb}{$aldbkey}{data1} = $$self{pending_aldb}{data1}; + $$self{aldb}{$aldbkey}{data2} = $$self{pending_aldb}{data2}; + $$self{aldb}{$aldbkey}{data3} = $$self{pending_aldb}{data3}; + $$self{aldb}{$aldbkey}{inuse} = 1; # needed so that restore string will preserve record + if ($$self{_mem_activity} eq 'add') + { + $$self{aldb}{$aldbkey}{is_controller} = $$self{pending_aldb}{is_controller}; + $$self{aldb}{$aldbkey}{deviceid} = lc $$self{pending_aldb}{deviceid}; + $$self{aldb}{$aldbkey}{group} = lc $$self{pending_aldb}{group}; + $$self{aldb}{$aldbkey}{address} = $$self{pending_aldb}{address}; + } + $$self{_mem_activity} = undef; + $$self{_mem_action} = undef; + $$self{pending_aldb} = undef; + main::print_log("[Insteon::ALDB_i2] DEBUG3: " . $$self{device}->get_object_name + . " link write completed for [".$$self{aldb}{$aldbkey}{address}."]") + if $self->{device}->debuglevel(3, 'insteon'); + $self->health("good"); + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } else { + # clear out mem_activity flag + $$self{_mem_activity} = undef; + # add the address of the deleted link to the empty list + $self->add_empty_address($$self{pending_aldb}{address}); + # and, remove from the duplicates list (if it is a member) + $self->delete_duplicate_link_address($$self{pending_aldb}{address}); + if (exists $$self{pending_aldb}{deviceid}) + { + my $key = $self->get_linkkey($$self{pending_aldb}{deviceid}, + $$self{pending_aldb}{group}, + $$self{pending_aldb}{is_controller}, + $$self{pending_aldb}{data3}); + delete $$self{aldb}{$key}; + } + $self->health("good"); + # Put the new ALDB Delta into memory + $self->query_aldb_delta('set'); + } + } + else + { + main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name + . ": unhandled _mem_action=".$$self{_mem_action}) + if $self->{device}->debuglevel(1, 'insteon'); + $clear_message = 0; + } + return $clear_message; +} + +sub _write_link +{ + my ($self, $address, $deviceid, $group, $is_controller, $data1, $data2, $data3) = @_; + if ($address) + { + &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " writing address: $address for device: $deviceid and group: $group"); + + my $message = new Insteon::InsteonMessage('insteon_ext_send', $$self{device}, 'read_write_aldb'); + + #cmd2.00.write_aldb_record.addr_msb.addr_lsb.byte_count.d6-d14 bytes to write + my $message_extra = '00'.'00'.'02'; + + $$self{pending_aldb}{address} = $address; + $message_extra .= $address; + + $message_extra .= '08'; #write 8 bytes + + #D6-D13 aldb entry: flags.group.deviceid(3).data1.data2.data3 + #flags + $$self{pending_aldb}{is_controller} = $is_controller; + my $flag = ($$self{pending_aldb}{is_controller}) ? 'E2' : 'A2'; + $$self{pending_aldb}{flag} = $flag; + $message_extra .= $flag; + + #group + $$self{pending_aldb}{group} = lc $group; + $message_extra .= $$self{pending_aldb}{group}; + + #device ID + $$self{pending_aldb}{deviceid} = lc $deviceid; + $message_extra .= $$self{pending_aldb}{deviceid}; + + #data1 - data3 + $$self{pending_aldb}{data1} = (defined $data1) ? lc $data1 : '00'; + $message_extra .= $$self{pending_aldb}{data1}; + $$self{pending_aldb}{data2} = (defined $data2) ? lc $data2 : '00'; + $message_extra .= $$self{pending_aldb}{data2}; + $$self{pending_aldb}{data3} = (defined $data3) ? lc $data3 : '00'; + $message_extra .= $$self{pending_aldb}{data3}; + $message_extra .= '00'; #byte 14 + $message->extra($message_extra); + $$self{_mem_action} = 'aldb_i2writeack'; + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + else + { + &::print_log("[Insteon::ALDB_i2] WARN: " . $$self{device}->get_object_name + . " write_link failure: no address available for record to device: $deviceid and group: $group" . + " and is_controller: $is_controller"); + if ($$self{_success_callback}) + { + package main; + eval ($$self{_success_callback}); + &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::ALDB_i2; + } + } +} + +sub _write_delete +{ + #pending_aldb must be populated before calling + my ($self, $address) = @_; + + if ($address) + { + &::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " writing address as deleted: $address"); + + my $message = new Insteon::InsteonMessage('insteon_ext_send', $$self{device}, 'read_write_aldb'); + + #cmd2.00.write_aldb_record.addr_msb.addr_mid.addr_lsb.byte_count.d6-d14 bytes to write + my $message_extra = '00'.'00'.'02'; + $message_extra .= $address; + $message_extra .= '08'; #write 8 bytes + + #D6-D13 aldb entry: flags.group.deviceid(3).data1.data2.data3 + #flag = 02 deleted + $message_extra .= '02'; + #group + $message_extra .= '00'; + #device ID + $message_extra .= '000000'; + #data1 - data3 + $message_extra .= '00'; + $message_extra .= '00'; + $message_extra .= '00'; + #byte 14 + $message_extra .= '00'; + + $message->extra($message_extra); + $$self{_mem_action} = 'aldb_i2writeack'; + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); + } + else + { + &::print_log("[Insteon::ALDB_i2] WARN: " . $$self{device}->get_object_name + . " write_delete failure: no address available"); + if ($$self{_success_callback}) + { + package main; + eval ($$self{_success_callback}); + &::print_log("[Insteon::ALDB_i2] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon::ALDB_i2; + } + } +} + +=item C + +Called as part of "scan link table" voice command. + +=cut + +sub send_read_aldb +{ + my ($self, $address) = @_; + + $$self{_mem_msb} = substr($address,0,2); + $$self{_mem_lsb} = substr($address,2,2); + $$self{_mem_action} = 'aldb_i2read'; + main::print_log("[Insteon::ALDB_i2] " . $$self{device}->get_object_name . " reading ALDB at location: 0x" . $address); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $$self{device}, 'read_write_aldb'); + #cmd2.00.read_aldb_record.addr_msb.addr_lsb.record_count(0 for all).d6-d14 unused + $message->extra("00"."00"."00".$$self{_mem_msb}.$$self{_mem_lsb}."01"."000000000000000000"); + $message->failure_callback($$self{_failure_callback}); + $self->_send_cmd($message); +} + +=back + +=head2 INI PARAMETERS + +None + +=head2 AUTHOR + +Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +package Insteon::ALDB_PLM; + +=head1 B + +=head2 SYNOPSIS + +Unique class for storing a cahced copy of a the PLM's link database. + +=head2 DESCRIPTION + +Generally this object should be interacted with through the insteon objects and +not by directly calling any of the following methods. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +use strict; + +@Insteon::ALDB_PLM::ISA = ('Insteon::AllLinkDatabase'); + +=item C + +Instantiate a new object. + +=cut + +sub new +{ + my ($class,$device) = @_; + + my $self = new Insteon::AllLinkDatabase($device); + bless $self,$class; + return $self; +} + +=item C + +This is called by mh on exit to save the cached ALDB of a device to persistant data. + +=cut + +sub restore_string +{ + my ($self) = @_; + my $restore_string = ''; + if ($$self{aldb}) + { + my $link = ''; + foreach my $link_key (keys %{$$self{aldb}}) + { + $link .= '|' if $link; # separate sections + my %link_record = %{$$self{aldb}{$link_key}}; + my $record = ''; + foreach my $record_key (keys %link_record) + { + next unless $link_record{$record_key}; + $record .= ',' if $record; + $record .= $record_key . '=' . $link_record{$record_key}; + } + $link .= $record; + } + $restore_string .= $$self{device}->get_object_name . "->_aldb->restore_linktable(q~$link~) if " . $$self{device}->get_object_name . "->_aldb;\n"; + } + if (defined $self->scandatetime) + { + $restore_string .= $$self{device}->get_object_name . "->_aldb->scandatetime(q~" . $self->scandatetime . "~) if " + . $$self{device}->get_object_name . "->_aldb;\n"; + } + $restore_string .= $$self{device}->get_object_name . "->_aldb->health(q~" . $self->health . "~) if " + . $$self{device}->get_object_name . "->_aldb;\n"; + return $restore_string; +} + +=item C + +Used to reload MisterHouse's cached version of a device's ALDB on restart. + +=cut + +sub restore_linktable +{ + my ($self, $links) = @_; + if ($links) + { + foreach my $link_section (split(/\|/,$links)) + { + my %link_record = (); + my $deviceid = ''; + my $groupid = '01'; + my $is_controller = 0; + my $subaddress = ''; + foreach my $link_record (split(/,/,$link_section)) + { + my ($key,$value) = split(/=/,$link_record); + $deviceid = $value if ($key eq 'deviceid'); + $groupid = $value if ($key eq 'group'); + $is_controller = $value if ($key eq 'is_controller'); + $subaddress = $value if ($key eq 'data3'); + $link_record{$key} = $value if $key and defined($value); + } + my $linkkey = $self->get_linkkey($deviceid, $groupid, $is_controller, $subaddress); + %{$$self{aldb}{lc $linkkey}} = %link_record; + } + } +} + +=item C + +Prints a human readable form of MisterHouse's cached version of a device's ALDB +to the print log. Called as part of the "scan links" voice command +or in response to the "log links" voice command. + +=cut + +sub log_alllink_table +{ + my ($self) = @_; + &::print_log("[Insteon::ALDB_PLM] Link table health: " . $self->health); + foreach my $linkkey (sort(keys(%{$$self{aldb}}))) { + my $is_controller = $$self{aldb}{$linkkey}{is_controller}; + my $group = $$self{aldb}{$linkkey}{group}; + $group = '01' if $group eq '00'; + my $deviceid = $$self{aldb}{$linkkey}{deviceid}; + my $linked_subgroup = '01'; + my $controller_device; + my $controller_name; + if (!$is_controller){ + $linked_subgroup = $group; + } + elsif ($group ne '00' && $group ne '01') { + $controller_device = Insteon::get_object('000000',$group); + $controller_name = $controller_device->get_object_name . " ($group)"; + } + else { + $controller_name = $group; + } + my $linked_object = Insteon::get_object($deviceid,$linked_subgroup); + my $linked_name = ''; + if ($linked_object) { + $linked_name = $linked_object->get_object_name; + } + else { + $linked_name = uc substr($deviceid,0,2) . '.' . + uc substr($deviceid,2,2) . '.' . + uc substr($deviceid,4,2); + } + &::print_log("[Insteon::ALDB_PLM] " . + (($is_controller) ? "cntlr($controller_name) record to " + . $linked_name + : "responder record to " . $linked_name . "($$self{aldb}{$linkkey}{group})") + . " (d1=$$self{aldb}{$linkkey}{data1}, d2=$$self{aldb}{$linkkey}{data2}, " + . "d3=$$self{aldb}{$linkkey}{data3})"); + } +} + +=item C + +Parses the alllink message sent from the PLM. + +=cut + +sub parse_alllink +{ + my ($self, $data) = @_; + if (substr($data,0,6)) + { + my %link = (); + my $flag = substr($data,0,1); + $link{is_controller} = (hex($flag) & 0x04) ? 1 : 0; + $link{flags} = substr($data,0,2); + $link{group} = lc substr($data,2,2); + $link{deviceid} = lc substr($data,4,6); + $link{data1} = substr($data,10,2); + $link{data2} = substr($data,12,2); + $link{data3} = substr($data,14,2); + my $key = $self->get_linkkey($link{deviceid}, $link{group}, + $link{is_controller}, $link{data3}); + %{$$self{aldb}{lc $key}} = %link; + } +} + +=item C + +Sends the request for the first alllink entry on the PLM. + +=cut + +sub get_first_alllink +{ + my ($self) = @_; + $self->health('out-of-sync'); # set as corrupt and allow acknowledge to set otherwise + $$self{device}->queue_message(new Insteon::InsteonMessage('all_link_first_rec', $$self{device})); +} + +=item C + +Sends the request for the next alllink entry on the PLM. + +=cut + +sub get_next_alllink +{ + my ($self) = @_; + $$self{device}->queue_message(new Insteon::InsteonMessage('all_link_next_rec', $$self{device})); +} + +=item C + +Reviews the cached version of all of the ALDBs and based on this review removes +links from this device which are not present in the mht file, not defined in the +code, or links which are only half-links.. + +=cut + +sub delete_orphan_links +{ + my ($self, $audit_mode) = @_; + + &::print_log("[Insteon::ALDB_PLM] #### NOW BEGINNING DELETE ORPHAN LINKS ####"); + + $self->SUPER::delete_orphan_links($audit_mode); + + # iterate over all registered objects and compare whether the link tables match defined scene linkages in known Insteon_Links + for my $obj (&Insteon::find_members('Insteon::BaseDevice')) + { + #Match on real objects only + if (($obj->is_root)) + { + my %delete_req = ('root_object' => $obj, 'audit_mode' => $audit_mode); + push @{$$self{delete_queue}}, \%delete_req; + } + } + + $self->_process_delete_queue(); +} + +sub _process_delete_queue { + my ($self, $p_num_deleted) = @_; + $$self{delete_queue_processed} += $p_num_deleted if $p_num_deleted; + my $num_in_queue = @{$$self{delete_queue}}; + if ($num_in_queue) + { + my $delete_req_ptr = shift(@{$$self{delete_queue}}); + my %delete_req = %$delete_req_ptr; + my $failure_callback = $$self{device}->get_object_name . + "->_aldb->_process_delete_queue_failure"; + # distinguish between deleting PLM links and processing delete orphans for a root item + if ($delete_req{'root_object'}) + { + $$self{current_delete_device} = $delete_req{'root_object'}->get_object_name; + $delete_req{'root_object'}->delete_orphan_links(($delete_req{'audit_mode'}) ? 1 : 0, $failure_callback); + } + else + { + $$self{current_delete_device} = $$self{device}->get_object_name; + &::print_log("[Insteon::ALDB_PLM] now deleting orphaned link w/ details: " + . (($delete_req{is_controller}) ? "controller($delete_req{data3})" : "responder") + . ", " . (($delete_req{object}) ? "object=" . $delete_req{object}->get_object_name + : "deviceid=$delete_req{deviceid}") . ", group=$delete_req{group}") + if $self->{device}->debuglevel(1, 'insteon'); + $delete_req{failure_callback} = $failure_callback; + $self->delete_link(%delete_req); + $$self{delete_queue_processed}++; + } + } + else { + ::print_log("[Insteon::ALDB_PLM] Delete All Links has Completed."); + my $_delete_failure_cnt = scalar $$self{_delete_device_failures}; + if ($_delete_failure_cnt) { + my $obj_list; + for my $failed_obj (@{$$self{_delete_device_failures}}){ + $obj_list .= $failed_obj .", "; + } + ::print_log("[Insteon::ALDB_PLM] However, some failures were ". + "noted with the following devices: $obj_list"); + } + ::print_log("[Insteon::ALDB_PLM] A total of $$self{delete_queue_processed} orphaned link records were deleted."); + ::print_log("[Insteon::ALDB_PLM] #### END DELETE ORPHAN LINKS ####"); + } +} + +sub _process_delete_queue_failure { + my ($self) = @_; + push @{$$self{_delete_device_failures}}, $$self{current_delete_device}; + ::print_log("[Insteon::ALDB_PLM] WARN: failure occurred when deleting orphan links from: " + . $$self{current_delete_device} . ". Moving on..."); + $self->_process_delete_queue; + +} + +=item C + +Deletes a specific link from a device. Generally called by C. + +=cut + +sub delete_link +{ + # linkkey is concat of: deviceid, group, is_controller + my ($self, $parms_text) = @_; + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + my $num_deleted = 0; + my $insteon_object = $link_parms{object}; + my $deviceid = ($insteon_object) ? $insteon_object->device_id : $link_parms{deviceid}; + my $group = $link_parms{group}; + my $is_controller = ($link_parms{is_controller}) ? 1 : 0; + my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; + my $linkkey = $self->get_linkkey($deviceid, $group, $is_controller, $subaddress); + if (defined $$self{aldb}{$linkkey}) + { + my $cmd = '80' + . $$self{aldb}{$linkkey}{flags} + . $$self{aldb}{$linkkey}{group} + . $$self{aldb}{$linkkey}{deviceid} + . $$self{aldb}{$linkkey}{data1} + . $$self{aldb}{$linkkey}{data2} + . $$self{aldb}{$linkkey}{data3}; + delete $$self{aldb}{$linkkey}; + $num_deleted = 1; + my $message = new Insteon::InsteonMessage('all_link_manage_rec', $$self{device}); + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; + $message->interface_data($cmd); + $$self{device}->queue_message($message); + } + else + { + &::print_log("[Insteon::ALDB_PLM] no entry in linktable could be found for: ". + "deviceid=$deviceid, group=$group, is_controller=$is_controller, subaddress=$subaddress"); + if ($link_parms{callback}) + { + package main; + eval ($link_parms{callback}); + &::print_log("[Insteon_PLM] error in add link callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon_PLM; + } + } + return $num_deleted; +} + +=item C + +Adds the link to the device's ALDB. Generally called from the "sync links" or +"link to interface" voice commands. + +=cut + +sub add_link +{ + my ($self, $parms_text) = @_; + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + my $device_id; + my $group = ($link_parms{group}) ? $link_parms{group} : '01'; + my $insteon_object = $link_parms{object}; + if (!(defined($insteon_object))) + { + $device_id = lc $link_parms{deviceid}; + $insteon_object = &Insteon::get_object($device_id, $group); + } + else + { + $device_id = lc $insteon_object->device_id; + } + my $is_controller = ($link_parms{is_controller}) ? 1 : 0; + my $subaddress = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; + my $linkkey = $self->get_linkkey($device_id, $group, $is_controller, $subaddress); + if (defined $$self{aldb}{$linkkey}) + { + &::print_log("[Insteon::ALDB_PLM] WARN: attempt to add link to PLM that already exists! " + . "deviceid=$device_id, group=$group, is_controller=$is_controller, subaddress=$subaddress"); + if ($link_parms{callback}) + { + package main; + eval ($link_parms{callback}); + &::print_log("[Insteon::ALDB_PLM] error in add link callback: " . $@) + if $@ and $self->{device}->debuglevel(1, 'insteon'); + package Insteon_PLM; + } + } + else + { + # The modem developers guide appears to be wrong regarding control + # codes. 40 and 41 will respond with a NACK if a record for that + # group/device/is_controller combination already exist. It appears + # that code 20 can be used to edit existing but not create new records. + # However, since data1-3 are consistent for all PLM links we never + # really need to update a PLM link. NB prior MH code did not set + # data3 on control records to the group, however this does not + # appear to have any adverse effects, and the current MH code will + # not flag these entries as being incorrect or requiring an update + my $control_code = ($is_controller) ? '40' : '41'; + # flags should be 'a2' for responder and 'e2' for controller + my $flags = ($is_controller) ? 'E2' : 'A2'; + my $data1 = (defined $link_parms{data1}) ? $link_parms{data1} : (($is_controller) ? '01' : '00'); + my $data2 = (defined $link_parms{data2}) ? $link_parms{data2} : '00'; + my $data3 = (defined $link_parms{data3}) ? $link_parms{data3} : '00'; + # from looking at manually linked records, data1 and data2 are both 00 for responder records + # and, data1 is 01 and usually data2 is 00 for controller records + + my $cmd = $control_code + . $flags + . $group + . $device_id + . $data1 + . $data2 + . $data3; + $$self{aldb}{$linkkey}{flags} = lc $flags; + $$self{aldb}{$linkkey}{group} = lc $group; + $$self{aldb}{$linkkey}{is_controller} = $is_controller; + $$self{aldb}{$linkkey}{deviceid} = lc $device_id; + $$self{aldb}{$linkkey}{data1} = lc $data1; + $$self{aldb}{$linkkey}{data2} = lc $data2; + $$self{aldb}{$linkkey}{data3} = lc $data3; + $$self{aldb}{$linkkey}{inuse} = 1; + $self->health('good') if($self->health() eq 'empty'); + my $message = new Insteon::InsteonMessage('all_link_manage_rec', $$self{device}); + $message->interface_data($cmd); + $$self{_success_callback} = ($link_parms{callback}) ? $link_parms{callback} : undef; + $$self{_failure_callback} = ($link_parms{failure_callback}) ? $link_parms{failure_callback} : undef; + $message->interface_data($cmd); + $$self{device}->queue_message($message); + } +} + +=item C + +Checks and returns true if a link with the passed details exists on the device +or false if it does not. Generally called as part of C. + +=cut + +sub has_link +{ + my ($self, $insteon_object, $group, $is_controller, $data3) = @_; + my $key = $self->get_linkkey($insteon_object->device_id, + $group, $is_controller, $data3); + return (defined $$self{aldb}{$key}); +} + +=back + +=head2 INI PARAMETERS + +None + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + + +1; diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index efe8ab1a7..d115da551 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1,3669 +1,3669 @@ -=head1 B - -=head2 SYNOPSIS - -Usage - -In user code: - - $ip_patio_light = new Insteon_Device($myPLM,"33.44.55"); - $ip_patio_light->set("ON"); - -=head2 DESCRIPTION - -Generic class implementation of an Insteon Device. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::BaseObject; - -use strict; -use Insteon::AllLinkDatabase; - -@Insteon::BaseObject::ISA = ('Generic_Item'); - -our %message_types = ( - on => 0x11, - off => 0x13 -); - -our %nack_messages = ( - fb => 'illegal_value_in_cmd', - fc => 'pre_nak_long_db_search', - fd => 'bad_checksum_or_unknown_cmd', - fe => 'load_sense_detects_no_load', - ff => 'sender_id_not_in_responder_aldb', -); - -=item C - -Takes the various states available to insteon devices and returns a derived -state of either ON or OFF. - -=cut - -sub derive_link_state -{ - my ($self, $p_state) = @_; - $p_state = $self if !(ref $self); #Old code made direct calls - #Convert Relative State to Absolute State - if ($p_state =~ /^([+-])(\d+)/) { - my $rel_state = $1 . $2; - my $curr_state = '100'; - $curr_state = '0' if ($self->state eq 'off'); - $curr_state = $1 if $self->state =~ /(\d{1,3})/; - $p_state = $curr_state + $rel_state; - $p_state = 'on' if ($p_state > 0); - $p_state = 'off' if ($p_state <= 0); - } - - my $link_state = 'on'; - if ($p_state eq 'off' or $p_state eq 'off_fast') - { - $link_state = 'off'; - } - elsif ($p_state =~ /\d+%?/) - { - my ($dim_state) = $p_state =~ /(\d+)%?/; - $link_state = 'off' if $dim_state == 0; - } - - return $link_state; -} - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - my $self={}; - bless $self,$class; - - $$self{message_types} = \%message_types; - - if (defined $p_deviceid) { - my ($deviceid, $group) = $p_deviceid =~ /(\w\w\.\w\w\.\w\w):?(.+)?/; - # if a group is passed in, then assume it can be a controller - $$self{is_controller} = ($group) ? 1 : 0; - $self->device_id($deviceid); - $group = '01' unless $group; - $group = '0' . $group if length($group) == 1; - $self->group(uc $group); - } - - if ($p_interface) { - $self->interface($p_interface); - } else { - $self->interface(&Insteon::active_interface()); - } - - $self->restore_data('default_hop_count', 'engine_version'); - - $self->initialize(); - $$self{max_hops} = 3; - $$self{min_hops} = 0; - $$self{level} = undef; - $$self{flag} = "0F"; - $$self{ackMode} = "1"; - $$self{awaiting_ack} = 0; - $$self{is_acknowledged} = 0; - $$self{max_queue_time} = $::config_parms{'Insteon_PLM_max_queue_time'}; - $$self{max_queue_time} = 10 unless $$self{max_queue_time}; # 10 seconds is max time allowed in command stack - @{$$self{command_stack}} = (); - $$self{_onlevel} = undef; - $$self{is_responder} = 1; - $$self{default_hop_count} = 0; - $$self{timeout_factor} = 1.0; - $$self{is_deaf} = 0; - - &Insteon::add($self); - return $self; -} - -=item C - -A former holdover from when MisterHouse used to ping devices to get their device -category. May not be needed anymore. - -=cut - -sub initialize -{ - my ($self) = @_; - $$self{m_write} = 1; - $$self{m_is_locally_set} = 0; - # persist local, simple attribs -} - -=item C - -Used to store and return the associated interface of a device. - -If provided, stores interface as the device's interface. - -=cut - -sub interface -{ - my ($self,$p_interface) = @_; - if (defined $p_interface) { - $$self{interface} = $p_interface; - } - elsif (!($$self{interface})) - { - $$self{interface} = &Insteon::active_interface; - } - return $$self{interface}; -} - -=item C - -Used to store and return the associated device_id of a device. - -If provided, stores id as the device's id. - -Returns device id without any delimiters. - -=cut - -sub device_id -{ - my ($self,$p_device_id) = @_; - - if (defined $p_device_id) - { - $p_device_id =~ /(\w\w)\W?(\w\w)\W?(\w\w)/; - $$self{device_id}=$1 . $2 . $3; - } - return $$self{device_id}; -} - -=item C - -Used to store and return the associated group of a device. - -If provided, stores group as the device group. - -=cut - -sub group -{ - my ($self, $p_group) = @_; - $$self{m_group} = $p_group if $p_group; - return $$self{m_group}; -} - -=item C - -Changes the amount of time MH will wait to receive a response from a device before -resending the message. The value set will be multiplied by the predefined value -in MH. $float can be set to any positive decimal number. For example using 1.0 -will not change the preset values; 1.1 will increase the time MH waits by 10%; and -0.9 will force MH to wait for only 90% of the predefined time. - -This value is NOT saved on reboot, as such likely should be called in a $Reload loop. - -=cut - -sub timeout_factor { - my ($self, $factor) = @_; - $$self{timeout_factor} = $factor if $factor; - return $$self{timeout_factor}; -} - -=item C - -Sets the maximum number of hops that may be used in a message sent to the device. -The default and maximum number is 3. $int is an integer between 0-3. - -This value is NOT saved on reboot, as such likely should be called in a $Reload loop. - -=cut - -sub max_hops { - my ($self, $hops) = @_; - $$self{max_hops} = $hops if $hops; - return $$self{max_hops}; -} - -=item C - -Sets the minimum number of hops that may be used in a message sent to the device. -The default and minimum number is 0. $int is an integer between 0-3. - -This value is NOT saved on reboot, as such likely should be called in a $Reload loop. - -=cut - -sub min_hops { - my ($self, $hops) = @_; - $$self{min_hops} = $hops if $hops; - return $$self{min_hops}; -} - -=item C - -Used to track the number of hops needed to reach a device. Will store the past -20 hop counts for the device. Hop counts are added based on the number of hops -needed for an incomming message to arrive. Additionally, any time a message is -resent, a hop count equal to the prior default_hop_count + 1 is added. - -If provided, stores hop as a new hop count for the device. If more than 20 hop -counts have been stored, will drop the oldest hop count until only 20 exist. - -Returns the highest hop count of the past 20 hop counts - -=cut - -sub default_hop_count -{ - my ($self, $hop_count) = @_; - if (defined($hop_count)){ - ::print_log("[Insteon::BaseObject] DEBUG3: Adding hop count of " . $hop_count . " to hop_array of " - . $self->get_object_name) if $self->debuglevel(3, 'insteon'); - if (!defined(@{$$self{hop_array}})) { - unshift(@{$$self{hop_array}}, $$self{default_hop_count}); - $$self{hop_sum} = $$self{default_hop_count}; - } - #Calculate a simple moving average - unshift(@{$$self{hop_array}}, $hop_count); - $$self{hop_sum} += ${$$self{hop_array}}[0]; - $$self{hop_sum} -= pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >10); - $$self{default_hop_count} = int(($$self{hop_sum} / scalar(@{$$self{hop_array}})) + 0.5); - - ::print_log("[Insteon::BaseObject] DEBUG4: ".$self->get_object_name - ."->default_hop_count()=".$$self{default_hop_count} - ." :: hop_array[]=". join("",@{$$self{hop_array}})) - if $self->debuglevel(4, 'insteon'); - } - - #Allow for per-device settings - $$self{default_hop_count} = $$self{max_hops} if ($$self{max_hops} && - $$self{default_hop_count} > $$self{max_hops}); - $$self{default_hop_count} = $$self{min_hops} if ($$self{min_hops} && - $$self{default_hop_count} < $$self{min_hops}); - - return $$self{default_hop_count}; -} - -=item C - -Used to store and return the associated engine version of a device. - -If provided, stores the engine version of the device. - -=cut - -sub engine_version -{ - my ($self, $p_engine_version) = @_; - $$self{engine_version} = $p_engine_version if $p_engine_version; - return $$self{engine_version}; -} - -=item C - -Returns 1 if object is the same as $self, otherwise returns 0. - -=cut - -sub equals -{ - my ($self, $compare_object) = @_; - # make sure that the compare_object is legitimate - return 0 unless $compare_object && ref $compare_object && $compare_object->isa('Insteon::BaseObject'); - return 1 if $compare_object eq $self; - # self and compare_object need to have device_ids and groups to be equal - return 0 unless $self->device_id && $self->group && $compare_object->device_id && $compare_object->group; - return 1 if (($compare_object->device_id eq $self->device_id) - && ($compare_object->group eq $self->group)); - # default to false; - return 0; -} - -=item C - -Used to set the device state. If called by device or a device linked to device, -calls C. If called by something -else, will send the command to the device. - -=cut - -sub set -{ - my ($self,$p_state,$p_setby,$p_response) = @_; - return if &main::check_for_tied_filters($self, $p_state); - - # Override any set_with_timer requests - if ($$self{set_timer}) { - &Timer::unset($$self{set_timer}); - delete $$self{set_timer}; - } - - if ($self->_is_valid_state($p_state)) { - # always reset the is_locally_set property unless set_by is the device - $$self{m_is_locally_set} = 0 unless ref $p_setby and $p_setby eq $self; - - if ($p_state eq 'toggle') - { - $p_state = ($self->state eq 'on')? 'off' : 'on'; - } - - my $setby_name = $p_setby; - $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); - if (ref $p_setby and $p_setby eq $self) - { #If set by device, update MH state, - my $derived_state = $self->derive_link_state($p_state); - &::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . "::set_receive($derived_state, $setby_name)") if $self->debuglevel(1, 'insteon'); - $self->set_receive($derived_state,$p_setby,$p_response); - $self->set_linked_devices($p_state); - } - elsif (ref $p_setby and $p_setby eq $self->interface) - { #If set by interface, this was a manual status_request response - &::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . "::set_receive($p_state, $setby_name)") if $self->debuglevel(1, 'insteon'); - $self->set_receive($p_state,$p_setby,$p_response); - } - else { # Not called by device, send set command - if ($self->is_responder){ - my $message = $self->derive_message($p_state); - $self->_send_cmd($message); - &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") - if $self->debuglevel(1, 'insteon'); - $self->is_acknowledged(0); - $$self{pending_state} = $p_state; - $$self{pending_setby} = $p_setby; - $$self{pending_response} = $p_response; - } - else { - ::print_log("[Insteon::BaseObject] " . $self->get_object_name() - . " is not a responder and cannot be set to a state."); - } - } - $self->level($p_state) if $self->can('level'); # update the level value - } else { - &::print_log("[Insteon::BaseObject] failed state validation with state=$p_state"); - } -} - -=item C - -If ack is true, calls C. If ack is false, clears awaiting_ack flag. - -=cut - -sub is_acknowledged -{ - my ($self, $p_ack) = @_; - if (defined $p_ack) - { - if ($p_ack) - { - $self->set_receive($$self{pending_state},$$self{pending_setby}, $$self{pending_response}) if defined $$self{pending_state}; - } - else - { - # if we are not acknowledged, then clear the awaiting acknowledgement flag - # we won't do the converse as it is set in _process_command_stack - $$self{awaiting_ack} = 0; - } - $$self{is_acknowledged} = $p_ack; - $$self{pending_state} = undef; - $$self{pending_setby} = undef; - $$self{pending_response} = undef; - } - return $$self{is_acknowledged}; -} - -=item C - -Updates the device state in MisterHouse. Triggers state_now, state_changed, and -state_final variables to update accordingly. Which causes tie_events to occur. - -If state was set to the same state within the last 1 second, then this is ignored. -This prevents the accidental calling of state_now if duplicate messages are received. - -=cut - -sub set_receive -{ - my ($self, $p_state, $p_setby, $p_response) = @_; - my $curr_milli = sprintf('%.0f', &main::get_tickcount); - my $window = 1000; - $p_state = $self->derive_link_state($p_state); - if (($p_state eq $self->state || $p_state eq $self->state_final) - && ($curr_milli - $$self{set_milliseconds} < $window)){ - ::print_log("[Insteon::BaseObject] Ignoring duplicate set " . $p_state . - " state command for " . $self->get_object_name . " received in " . - "less than $window milliseconds") if $self->debuglevel(1, 'insteon'); - } else { - $$self{set_milliseconds} = $curr_milli; - $self->level($p_state) if $self->can('level'); # update the level value - $self->SUPER::set($p_state, $p_setby, $p_response); - } -} - -=item C - -NOTE - This routine appears to be nearly identical, if not identical to the -C routine, it is not clear why this routine is -needed here. - -See full description of this routine in C - -=cut - -sub set_with_timer { - my ($self, $state, $time, $return_state, $additional_return_states) = @_; - return if &main::check_for_tied_filters($self, $state); - - $self->set($state) unless $state eq ''; - - return unless $time; - - my $state_change = ($state eq 'off') ? 'on' : 'off'; - $state_change = $return_state if defined $return_state; - $state_change = $self->{state} if $return_state and lc $return_state eq 'previous'; - - $state_change .= ';' . $additional_return_states if $additional_return_states; - - $$self{set_timer} = &Timer::new() unless $$self{set_timer}; - my $object_name = $self->{object_name}; - my $action = "$object_name->set('$state_change')"; - $$self{set_timer}->set($time, $action); -} - -sub _send_cmd -{ - my ($self, $message) = @_; -# $msg{type} = 'standard' unless $msg{type}; - -# my $message = $self->derive_message($msg{command},$msg{type},$msg{extra}); - - if ($message->command eq 'peek' - or $message->command eq 'poke' - or $message->command eq 'status_request' - or $message->command eq 'do_read_ee' - or $message->command eq 'set_address_msb' - or $message->command eq 'read_write_aldb' - ) - { - push(@{$$self{command_stack}}, $message); - } - else - { - unshift(@{$$self{command_stack}},$message); - } - $self->_process_command_stack(); -} - -=item C - -Generates and returns a basic on/off message from a command. - -=cut - -sub derive_message -{ - my ($self, $p_command, $p_extra) = @_; - my @args; - my $level; - - #msg id - my ($command, $subcommand) = split(/:/, $p_command, 2); - $command=lc($command); -# &::print_log("XLATE:$msg:$substate:$p_state:"); - - my $message; - - if ($self->isa("Insteon::BaseController")) - { - # only send out as all-link if the link originates from the plm - if ($self->isa("Insteon::InterfaceController")) - { # return the size of the command stack - $message = new Insteon::InsteonMessage('all_link_send', $self); - } - elsif ($self->is_root) - { # return the size of the command stack - $message = new Insteon::InsteonMessage('insteon_send', $self); - } else { - # silently ignore as this is now permitted if via "surrogate" - } - } elsif ($self->isa("Insteon::BaseObject")) { - $message = new Insteon::InsteonMessage('insteon_send', $self); - } - - if (!(defined $p_extra)) { - if ($command eq 'on') - { - if ($self->can('local_onlevel') && defined $self->local_onlevel) { - $level = 2.55 * $self->local_onlevel; - $command = 'on_fast'; - } else { - $level=255; - } - } elsif ($command eq 'off') - { - $level = 0; - } elsif ($command=~/^([1]?[0-9]?[0-9])/) - { - if ($1 < 1) { - $command='off'; - $level = 0; - } else { - $level = ($self->isa('Insteon::DimmableLight')) ? $1 * 2.55 : 255; - $command='on'; - } - } - } - - # confirm that the resulting $msg is legitimate - if (!(defined($self->message_type_code($command)))) { - &::print_log("[Insteon::BaseInsteon] invalid state=$command") if $self->debuglevel(1, 'insteon'); - return undef; - } - - if ($self->isa("Insteon::InterfaceController")) - { - $message->extra('00') #All PLM Scenes are Cmd2=00; - } elsif ($p_extra) - { $message->extra($p_extra); - - } elsif ($subcommand) { - $message->extra($subcommand); - } else { - if ($command eq 'on') - { - $message->extra(sprintf("%02X",int($level+.5))); - } else { - $message->extra('00'); - } - } - - $message->command($command); - return $message; -} - -=item C - -Takes msg, a text based msg type, and returns the corresponding text version of -the hexadecimal message type. - -=cut - -sub message_type_code -{ - my ($self, $msg) = @_; - return $$self{message_types}->{$msg}; -} - -=item C - -Takes msg, a text based msg type, and returns the corresponding message type as -a hex. - -=cut - -sub message_type_hex -{ - my ($self, $msg) = @_; - return unpack( 'H*', pack( 'c', $self->message_type_code($msg))); -} - -=item C - -Takes cmd, text based hexadecimal code, and returns the text based description -of the message code. - -=cut - -sub message_type -{ - my ($self, $cmd1) = @_; - my $msg_type; - my $msg_type_ptr = $$self{message_types}; - my %msg_types = %$msg_type_ptr; - for my $key (keys %msg_types){ - if (pack("C",$msg_types{$key}) eq pack("H*",$cmd1)) - { -# &::print_log("[Insteon::BaseObject] found: $key"); - $msg_type=$key; - last; - } - } - return $msg_type; -} - -sub _is_info_request -{ - my ($self, $cmd, $ack_setby, %msg) = @_; - my $is_info_request = 0; - if ($cmd eq 'status_request') { - $is_info_request++; - my $ack_on_level = sprintf("%d", int((hex($msg{extra}) * 100 / 255)+.5)); - &::print_log("[Insteon::BaseObject] received status for " . - $self->{object_name} . " with on-level: $ack_on_level%, " - . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); - $self->level($ack_on_level) if $self->can('level'); # update the level value - if ($ack_on_level == 0) { - $self->set('off', $ack_setby); - } elsif ($ack_on_level > 0 and !($self->isa('Insteon::DimmableLight'))) { - $self->set('on', $ack_setby); - } else { - $self->set($ack_on_level . '%', $ack_setby); - } - # if this were a scene controller, then also propogate the result to all members - my $callback; - if ($self->_aldb->{aldb_delta_action} eq 'set'){ - if ($msg{cmd_code} eq "00") { - $self->_aldb->{_mem_activity} = 'delete'; - $self->_aldb->{pending_aldb}{address} = $self->_aldb->get_first_empty_address(); - if($self->_aldb->isa('Insteon::ALDB_i1')) { - $self->_aldb->_peek($self->_aldb->{pending_aldb}{address},0); - } else { - $self->_aldb->_write_delete($self->_aldb->{pending_aldb}{address}); - } - } else { - $self->_aldb->aldb_delta($msg{cmd_code}); - $self->_aldb->scandatetime(&main::get_tickcount); - &::print_log("[Insteon::BaseObject] The Link Table Version for " - . $self->{object_name} . " has been updated to version number " . $self->_aldb->aldb_delta()); - if (defined $self->_aldb->{_success_callback}) { - $callback = $self->_aldb->{_success_callback}; - $self->_aldb->{_success_callback} = undef; - } - } - } - elsif ($self->_aldb->{aldb_delta_action} eq 'check') - { - if ($self->_aldb->aldb_delta() eq $msg{cmd_code}){ - &::print_log("[Insteon::BaseObject] The link table for " - . $self->{object_name} . " is in sync."); - #Link Table Scan Successful, Record Current Time - $self->_aldb->scandatetime(&main::get_tickcount); - if (defined $self->_aldb->{_aldb_unchanged_callback}) { - $callback = $self->_aldb->{_aldb_unchanged_callback}; - $self->_aldb->{_aldb_unchanged_callback} = undef; - } - } else { - &::print_log("[Insteon::BaseObject] WARN The link table for " - . $self->{object_name} . " is out-of-sync."); - $self->_aldb->health('out-of-sync'); - if (defined $self->_aldb->{_aldb_changed_callback}) { - $callback = $self->_aldb->{_aldb_changed_callback}; - $self->_aldb->{_aldb_changed_callback} = undef; - } - } - } - $self->_aldb->{aldb_delta_action} = undef; - $self->_aldb->health('out-of-sync') if($self->_aldb->aldb_delta() ne $msg{cmd_code}); - if ($callback){ - package main; - eval ($callback); - &::print_log("[Insteon::BaseObject] " . $self->get_object_name . ": error during scan callback $@") - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon::BaseObject; - } - } - elsif ( $cmd eq 'get_engine_version' ) { - $is_info_request++; - my @engine_types = (qw/I1 I2 I2CS/); - my $version = $engine_types[$msg{extra}]; - $self->engine_version($version); - &::print_log("[Insteon::BaseObject] received engine version for " - . $self->{object_name} . " of $version. " - . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); - } - return $is_info_request; -} - -sub _process_message -{ - my ($self,$p_setby,%msg) = @_; - my $p_state = undef; - - # the current approach assumes that links from other controllers to some responder - # would be seen by the plm by also direct linking the controller as a responder - # and not putting the plm into monitor mode. This means that updating the state - # of the responder based upon the link controller's request is handled - # by Insteon_Link. - - main::print_log("[Insteon::BaseObject] WARN: Message has invalid checksum") - if ($self->debuglevel(1, 'insteon') && !($msg{crc_valid}) - && $msg{is_extended} && $self->engine_version() eq 'I2CS'); - - my $clear_message = 0; - $$self{m_is_locally_set} = 1 if $msg{source} eq lc $self->device_id; - $self->default_hop_count($msg{maxhops}-$msg{hopsleft}) if (!$self->isa('Insteon::InterfaceController')); - if ($msg{is_ack}) { - #Default to clearing message transaction for ACK - $clear_message = 1; - my $corrupt_cmd = 0; - my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; - if ($$self{awaiting_ack}) - { - my $ack_setby = (ref $$self{m_status_request_pending}) - ? $$self{m_status_request_pending} : $p_setby; - if ($self->_is_info_request($pending_cmd,$ack_setby,%msg)) - { - $self->is_acknowledged(1); - $$self{m_status_request_pending} = 0; - $self->_process_command_stack(%msg); - } - elsif ($pending_cmd eq 'peek') - { - if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { - $self->_aldb->_on_peek(%msg) if $self->_aldb; - $self->_process_command_stack(%msg); - } else { - $corrupt_cmd = 1; - $clear_message = 0; - } - } - elsif ($pending_cmd eq 'set_address_msb') - { - if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { - $self->_aldb->_on_peek(%msg) if $self->_aldb; - $self->_process_command_stack(%msg); - } else { - $corrupt_cmd = 1; - $clear_message = 0; - } - } - elsif (($pending_cmd eq 'poke')) - { - if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { - $self->_aldb->_on_poke(%msg) if $self->_aldb; - $self->_process_command_stack(%msg); - } else { - $corrupt_cmd = 1; - $clear_message = 0; - } - } - elsif ($pending_cmd eq 'read_write_aldb') { - if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { - $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; - $self->_process_command_stack(%msg) if ($clear_message); - } else { - $corrupt_cmd = 1; - $clear_message = 0; - } - } - elsif ($pending_cmd eq 'ping'){ - $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); - $corrupt_cmd = 1 if ($msg{extra} ne sprintf ("%02d", $$self{ping_count})); - if (!$corrupt_cmd){ - $self->_process_command_stack(%msg); - &::print_log("[Insteon::BaseObject] received ping acknowledgement from " . $self->{object_name}) - if $self->debuglevel(1, 'insteon'); - $self->ping(); - $clear_message = 1; - } - } - elsif ($pending_cmd eq 'linking_mode'){ - $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); - if (!$corrupt_cmd){ - &::print_log("[Insteon::BaseObject] received linking mode ACK from " . $self->{object_name}) - if $self->debuglevel(1, 'insteon'); - $self->interface->_set_timeout('xmit', 2000); - $clear_message = 0; - } - } - else - { - if (($pending_cmd eq 'do_read_ee') && - ($self->_aldb->health eq "good" || $self->_aldb->health eq "empty") && - ($self->isa('Insteon::KeyPadLincRelay') || $self->isa('Insteon::KeyPadLinc'))){ - ## Update_Flags ends up here, set aldb_delta to new value - $self->_aldb->query_aldb_delta("set"); - } - $self->is_acknowledged(1); - # signal receipt of message to the command stack in case commands are queued - $self->_process_command_stack(%msg); - &::print_log("[Insteon::BaseObject] received command/state (awaiting) acknowledge from " . $self->{object_name} - . ": $pending_cmd and data: $msg{extra}") if $self->debuglevel(1, 'insteon'); - } - } - else - { - # allow non-synchronous messages to also use the _is_info_request hook - $self->_is_info_request($pending_cmd,$p_setby,%msg); - $self->is_acknowledged(1); - # signal receipt of message to the command stack in case commands are queued - $self->_process_command_stack(%msg); - &::print_log("[Insteon::BaseObject] received command/state acknowledge from " . $self->{object_name} - . ": " . (($msg{command}) ? $msg{command} : "(unknown)") - . " and data: $msg{extra}") if $self->debuglevel(1, 'insteon'); - } - if ($corrupt_cmd) { - main::print_log("[Insteon::BaseObject] WARN: received a message from " - . $self->get_object_name . " in response to a " - . $pending_cmd . " command, but the command code " - . $msg{cmd_code} . " is incorrect. Ignorring received message."); - $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); - $p_setby->active_message->no_hop_increase(1); - } - } - elsif ($msg{is_nack}) - { - #Default to clearing message transaction for NAK - $clear_message = 1; - if ($self->isa('Insteon::BaseLight')) { - &::print_log("[Insteon::BaseObject] WARN!! encountered a nack message (" - . $self->get_nack_msg_for( $msg{extra} ) .") for " . $self->{object_name} - . ". It may be unplugged, have a burned out bulb, or this may be a new I2CS " - . "type device that must first be manually linked to the PLM using the set button.") - if $self->debuglevel(1, 'insteon'); - } - else - { - &::print_log("[Insteon::BaseObject] WARN!! encountered a nack message (" - . $self->get_nack_msg_for( $msg{extra} ) .") for " . $self->{object_name} - . " ... skipping"); - } - $p_setby->active_message->no_hop_increase(1); - $self->is_acknowledged(0); - $self->_process_command_stack(%msg); - if($p_setby->active_message->failure_callback) - { - main::print_log("[Insteon::BaseObject] WARN: Now calling message failure callback: " - . $p_setby->active_message->failure_callback) if $self->debuglevel(1, 'insteon'); - $self->failure_reason('NAK'); - package main; - eval $p_setby->active_message->failure_callback; - main::print_log("[Insteon::BaseObject] problem w/ retry callback: $@") if $@; - package Insteon::BaseObject; - } - $p_setby->active_message->no_hop_increase(1); - $self->is_acknowledged(0); - $self->_process_command_stack(%msg); - } - elsif ($msg{command} eq 'start_manual_change') - { - $$self{manual_direction} = $msg{extra}; - $$self{manual_start} = ::get_tickcount() - (($msg{maxhops}-$msg{hopsleft})*50); - } elsif ($msg{command} eq 'stop_manual_change') { - # Determine percent change based on time interval - my $finish_time = &main::get_tickcount - (($msg{maxhops}-$msg{hopsleft})*50); - my $total_time = $finish_time - $$self{manual_start}; - my $percent_change = int($total_time / 42); - if ($$self{manual_direction} eq '00') { - $percent_change = "-".$percent_change; - } else { - $percent_change = "+".$percent_change; - } - $self->set($percent_change, $self); - } elsif ($msg{command} eq 'read_write_aldb') { - if ($self->_aldb){ - $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; - $self->_process_command_stack(%msg) if($clear_message); - } - } elsif ($msg{type} eq 'broadcast') { - $self->devcat($msg{devcat}); - $self->firmware($msg{firmware}); - &::print_log("[Insteon::BaseObject] device category: $msg{devcat}" - . " firmware: $msg{firmware} received for " . $self->{object_name}); - } else { - ## TO-DO: make sure that the state passed by command is something that is reasonable to set - $p_state = $msg{command}; - if ($msg{type} eq 'alllink') - { - if ($msg{command} eq 'link_cleanup_report'){ - if ($msg{extra} == 0){ - ::print_log("[Insteon::BaseObject] DEBUG Received AllLink Cleanup Success for " - . $self->{object_name}) if $self->debuglevel(1, 'insteon'); - } else { - ::print_log("[Insteon::BaseObject] WARN " . $msg{extra} . " Device(s) failed to " - . "acknowledge the command from " . $self->{object_name}); - } - } else { - $self->set($p_state, $self); - $$self{_pending_cleanup} = 1; - #Wait to avoid clobbering incomming subsequent cleanup messages - my @links = $self->find_members('Insteon::BaseDevice'); - #Timeout is the number of linked devices multiplied by - #the maximum hop length for a direct message and an ACK - #PLM is the +1 - my $timeout = (scalar(@links)+1) * 300; - ::print_log("[Insteon::BaseObject] DEBUG3 Delaying any outgoing messages ". - "by $timeout milliseconds to avoid collision with subsequent cleanup ". - "messages from " . $self->get_object_name) if ($self->debuglevel(3, 'insteon')); - $self->interface->_set_timeout('xmit', $timeout); - } - } - elsif ($msg{type} eq 'cleanup') - { - if (($self->state eq $p_state or $self->state_final eq $p_state) - and $$self{_pending_cleanup}){ - ::print_log("[Insteon::BaseObject] Ignoring Received Direct AllLink Cleanup Message for " - . $self->{object_name} . " since AllLink Broadcast Message was Received.") if $self->debuglevel(1, 'insteon'); - } else { - $self->set($p_state, $self); - } - $$self{_pending_cleanup} = 0; - } else { - main::print_log("[Insteon::BaseObject] Ignoring unsupported command from " - . $self->{object_name}) if $self->debuglevel(1, 'insteon'); - $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); - } - } - return $clear_message; -} - -sub _process_command_stack -{ - my ($self, %ackmsg) = @_; - if (%ackmsg) { # which may also be something that can be interpretted as a "nack" - # determine whether to unset awaiting_ack - # for now, be "dumb" and just unset it - $$self{awaiting_ack} = 0; - } - if (!($$self{awaiting_ack})) { - my $callback = undef; - my $message = pop(@{$$self{command_stack}}); - # convert ptr to cmd hash - if ($message) - { - my $plm_queue_size = $self->interface->queue_message($message); - - # send msg - if ($message->command eq 'peek' - or $message->command eq 'poke' - or $message->command eq 'status_request' - or $message->command eq 'get_engine_version' - or $message->command eq 'id_request' - or $message->command eq 'do_read_ee' - or $message->command eq 'set_address_msb' - or $message->command eq 'sensor_status' - or $message->command eq 'set_operating_flags' - or $message->command eq 'get_operating_flags' - or $message->command eq 'read_write_aldb' - or $message->command eq 'thermostat_control' - or $message->command eq 'thermostat_get_zone_info' - or $message->command eq 'extended_set_get' - or $message->command eq 'ping' - or $message->command eq 'linking_mode' - ) - { - $$self{awaiting_ack} = 1; - } - else - { - $$self{awaiting_ack} = 0; - } - - $$self{_prior_msg} = $message; - # TO-DO: adjust timer based upon (1) type of message and (2) retry_count - my $queue_time = $$self{max_queue_time} + $plm_queue_size; - # if is_synchronous, then no other command can be sent until an insteon ack or nack is received - # for this command - } else { - # and, always clear awaiting_ack and _prior_msg - $$self{awaiting_ack} = 0; - $$self{_prior_msg} = undef; - } - if ($callback) { - package main; - eval ($callback); - &::print_log("[Insteon::BaseObject] error in queue timer callback: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon::BaseObject; - } - } else { -# &::print_log("[Insteon_Device] " . $self->get_object_name . " command queued but not yet sent; awaiting ack from prior command") if $self->debuglevel(1, 'insteon'); - } -} - -sub _is_valid_state -{ - my ($self,$state) = @_; - if (!(defined($state)) or $state eq '') { - return 0; - } - - my ($msg, $substate) = split(/:/, $state, 2); - $msg=lc($msg); - - if ($msg=~/^[+-]?([1]?[0-9]?[0-9])/) - { - if ($1 < 1) { - $msg='off'; - } else { - $msg='on'; - } - } - elsif ($msg eq 'toggle') - { - if ($self->state eq 'on') - { - $msg = 'off'; - } - elsif ($self->state eq 'off') - { - $msg = 'on'; - } - } - - # confirm that the resulting $msg is legitimate - if (!(defined($$self{message_types}{$msg}))) { - return 0; - } else { - return 1; - } -} - -=item C - -Takes msg, text based hexadecimal code, and returns the text based description -of the NACK code. - -=cut - -sub get_nack_msg_for { - my ($self,$msg) = @_; - return $nack_messages{ $msg }; -} - -=item C - -Stores the resaon for the most recent message failure [NAK | timeout]. Used to -process message callbacks after a message fails. If called with no parameter -returns the saved failure reason. - -Parameters: - reason: failure reason - -Returns: failure reason - -=cut - -sub failure_reason -{ - my ($self, $reason) = @_; - $$self{failure_reason} = $reason if $reason; - return $$self{failure_reason}; -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my %voice_cmds = ( - #The Sync Links routine really resides in DeviceController, but that - #class seems a little redundant as in practice all devices are controllers - #in some sense. As a result, that class will likely be folded into - #BaseObject/Device at some future date. In order to avoid a bizarre - #inheritance of this routine by higher classes, this command was placed - #here - 'sync links' => $self->get_object_name . '->sync_links(0)' - ); - return \%voice_cmds; -} - -sub _aldb -{ - my ($self) = @_; - my $root_obj = $self->get_root(); - return $$root_obj{aldb}; -} - -=item C - -Returns true if the device must be awake in order to respond to messages. Most -devices are not deaf, currently devices that are deaf are battery operated -devices such as the Motion Sensor, RemoteLinc and TriggerLinc. - -At the BaseObject level all devices are defined as not deaf. Objects which -inherit BaseObject should redefine is_deaf as necessary. - -=cut - -sub is_deaf -{ - my ($self) = @_; - return $$self{is_deaf}; -} - -=item C - -Returns true if the device is a controller. - -=cut - -sub is_controller -{ - my ($self) = @_; - return $$self{is_controller}; -} - -=item C - -Stores and returns whether a device is a responder. - -=cut - -sub is_responder -{ - my ($self,$is_responder) = @_; - $$self{is_responder} = $is_responder if defined $is_responder; - if ($self->is_root || $self->isa('Insteon::InterfaceController')) { - return $$self{is_responder}; - } - else - { - my $root_obj = $self->get_root(); - if (ref $root_obj) - { - return $$root_obj{is_responder}; - } - else - { - return 0; - } - } -} - -=back - -=head2 INI PARAMETERS - -=over - -=item Insteon_PLM_max_queue_time - -Was previously used to set the maximum amount of time -a message could remain in the queue. This parameter is no longer used in the code -but it still appears in the initialization. It may be removed at a future date. -This also gets set in L -as well for some reason, but is not used there either. - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 NOTES - -Special thanks to: - Brian Warren for significant testing and patches - Bruce Winter - MH - -=head2 SEE ALSO - -=head2 LICENSE - -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. - -=cut - -#################################### -### ##################### -### BaseDevice ##################### -### ##################### -#################################### - -=head1 B - -=head2 DESCRIPTION - -Generic class implementation of a Base Insteon Device. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::BaseDevice; - -@Insteon::BaseDevice::ISA = ('Insteon::BaseObject'); - -our %message_types = ( - %Insteon::BaseObject::message_types, - assign_to_group => 0x01, - delete_from_group => 0x02, - link_cleanup_report => 0x06, - linking_mode => 0x09, - unlinking_mode => 0x0A, - get_engine_version => 0x0D, - ping => 0x0F, - id_request => 0x10, - on_fast => 0x12, - off_fast => 0x14, - start_manual_change => 0x17, - stop_manual_change => 0x18, - status_request => 0x19, - get_operating_flags => 0x1f, - set_operating_flags => 0x20, - do_read_ee => 0x24, - remote_set_button_tap => 0x25, - set_led_status => 0x27, - set_address_msb => 0x28, - poke => 0x29, - poke_extended => 0x2a, - peek => 0x2b, - peek_internal => 0x2c, - poke_internal => 0x2d, - extended_set_get => 0x2e, - read_write_aldb => 0x2f, -); - - -my %operating_flags = ( - 'program_lock_on' => '00', - 'program_lock_off' => '01', - 'led_on_during_tx' => '02', - 'led_off_during_tx' => '03', - 'resume_dim_on' => '04', - 'beeper_enabled' => '04', - 'resume_dim_off' => '05', - 'beeper_off' => '05', - 'eight_key_kpl' => '06', - 'load_sense_on' => '06', - 'six_key_kpl' => '07', - 'load_sense_off' => '07', - 'led_backlight_off' => '08', - 'led_off' => '08', - 'led_backlight_on' => '09', - 'led_enabled' => '09', - 'key_beep_enabled' => '0a', - 'one_minute_warn_disabled' => '0a', - 'key_beep_off' => '0b', - 'one_minute_warn_enabled' => '0b' -); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - my $self= new Insteon::BaseObject($p_deviceid,$p_interface); - bless $self,$class; - - $$self{message_types} = \%message_types; - $$self{operating_flags} = \%operating_flags; - - if ($self->group eq '01') { - $$self{aldb} = new Insteon::ALDB_i1($self); - } - - $self->restore_data('devcat', 'firmware', 'level', 'retry_count_log', 'fail_count_log', - 'outgoing_count_log', 'incoming_count_log', 'corrupt_count_log', - 'dupe_count_log', 'hops_left_count', 'max_hops_count', - 'outgoing_hop_count'); - - $self->initialize(); - $self->rate(undef); - $$self{level} = undef; - $$self{flag} = "0F"; - $$self{ackMode} = "1"; - $$self{awaiting_ack} = 0; - $$self{is_acknowledged} = 0; - $$self{max_queue_time} = $::config_parms{'Insteon_PLM_max_queue_time'}; - $$self{max_queue_time} = 10 unless $$self{max_queue_time}; # 10 seconds is max time allowed in command stack - @{$$self{command_stack}} = (); - $$self{_onlevel} = undef; - $$self{retry_count_log} = 0; - $$self{fail_count_log} = 0; - $$self{outgoing_count_log} = 0; - $$self{incoming_count_log} = 0; - $$self{corrupt_count_log} = 0; - $$self{dupe_count_log} = 0; - $$self{hops_left_count} = 0; - $$self{max_hops_count} = 0; - $$self{outgoing_hop_count} = 0; - - - return $self; -} - -=item C - -A former holdover from when MisterHouse used to ping devices to get their device -category. May not be needed anymore. - -=cut - -sub initialize -{ - my ($self) = @_; - $$self{m_write} = 1; - $$self{m_is_locally_set} = 0; - # persist local, simple attribs -} - -=item C - -Used to store and return the associated ramp rate of a device. - -If provided, stores rate as the device's ramp rate in MisterHouse. - -=cut - -sub rate -{ - my ($self,$p_rate) = @_; - $$self{rate} = $p_rate if defined $p_rate; - return $$self{rate}; -} - -=item C - -If a controller link from the device to the interface does not exist, this will -create that link on the device. - -Next, if a responder link from the device to the interface does not exist on the -interface, this will create that link on the interface. - -The group is the group on the device that is the controller, such as a button on -a keypad link. It will default to 01. - -Data3 is optional and is used to set the Data3 value in the controller link on -the device. - -=cut - -sub link_to_interface -{ - my ($self,$p_group, $p_data3, $step) = @_; - $p_group = $self->group unless (defined $p_group); - $p_data3 = $self->group unless (defined $p_data3); - my $success_callback_prefix = $self->get_object_name."->link_to_interface(\"$p_group\",\"$p_data3\","; - my $success_callback = ""; - my $failure_callback = '::print_log("[Insteon::BaseInsteon] Error: The Link_To_Interface '. - 'routine failed for device: '.$self->get_object_name.'")'; - $step = 0 if ($step eq ''); - if ($step == 0) { #If NAK on get_engine, then this is an I2CS device - $success_callback = $success_callback_prefix . "\"1\")"; - $failure_callback = $self->get_object_name."->link_to_interface_i2cs(\"$p_group\",\"$p_data3\")"; - $self->get_engine_version($success_callback, $failure_callback); - } - elsif ($step == 1) { #Add Link from object->PLM - $success_callback = $success_callback_prefix . "\"2\")"; - my %link_info = ( object => $self->interface, group => $p_group, is_controller => 1, - callback => "$success_callback", failure_callback=> "$failure_callback"); - $link_info{data3} = $p_data3 if $p_data3; - if ($self->_aldb) { - $self->_aldb->add_link(%link_info); - } - else - { - &main::print_log("[Insteon::BaseInsteon] Error: This item, " . $self->get_object_name . - ", does not have an ALDB object. Linking is not permitted."); - } - } - elsif ($step == 2){ #Add Link from PLM->object - $success_callback = $success_callback_prefix . "\"3\")"; - my $link_info = "deviceid=" . lc ($self->device_id) . " group=$p_group is_controller=0 " . - "callback=$success_callback failure_callback=$failure_callback"; - $self->interface->add_link($link_info); - } - elsif ($step == 3){ #Add surrogate link on device if surrogate exists - if (ref $$self{surrogate}){ - $success_callback = $success_callback_prefix . "\"4\")"; - my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( object => $self->interface, - group => $surrogate_group, is_controller => 0, - callback => "$success_callback", - failure_callback=> "$failure_callback", - data3 => $p_group); - $self->_aldb->add_link(%link_info); - } else { - ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. - ' for device ' .$self->get_object_name); - } - } - elsif ($step == 4){ #Add surrogate link on PLM if surrogate exists - $success_callback = $success_callback_prefix . "\"5\")"; - my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( deviceid=> lc($self->device_id), - group => $surrogate_group, is_controller => 1, - callback => "$success_callback", - failure_callback=> "$failure_callback", - data3 => $surrogate_group); - $self->interface->add_link(%link_info); - } - elsif ($step == 5){ - ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. - ' for device ' .$self->get_object_name); - } -} - -=item C - -Performs the same task as C however this routine is designed -to perform the initial link to I2CS devices. These devices cannot be initially -linked to the PLM in the normal way. This process requires more steps than the -normal routine which will take longer to perform and therefore is more prone to -faile. As such, this should likely only be used if necessary. - -=cut - -sub link_to_interface_i2cs -{ - my ($self,$p_group, $p_data3, $step) = @_; - my $success_callback_prefix = $self->get_object_name."->link_to_interface_i2cs('$p_group','$p_data3',"; - my $success_callback = ""; - my $failure_callback = "::print_log('[Insteon::BaseInsteon] Error Link_To_Interface_I2CS ". - "routine failed for device: ".$self->get_object_name."')"; - $step = 0 if ($step eq ''); - if ($step == 0) { #Put PLM into initiate linking mode - $success_callback = $success_callback_prefix . "'1')"; - $self->interface()->initiate_linking_as_controller('00', $success_callback, $failure_callback); - } - elsif ($step == 1) { #Ask device to respond to link request - $success_callback = $success_callback_prefix . "'2')"; - $self->enter_linking_mode($p_group, $success_callback, $failure_callback); - } - elsif ($step == 2) { #Scan device to get an accurate link table - #return to normal link_to_interface routine if successful - $success_callback_prefix = $self->get_object_name."->link_to_interface('$p_group','$p_data3',"; - $success_callback = $success_callback_prefix . "'1')"; - $self->scan_link_table($success_callback, $failure_callback); - } -} - -=item C - -Will delete the contoller link from the device to the interface if such a link exists. - -Next, will delete the responder link from the device to the interface on the -interface, if such a link exists. - -The group is the group on the device that is the controller, such as a button on -a keypad link. It will default to 01. - -=cut - -sub unlink_to_interface -{ - my ($self,$p_group,$step) = @_; - $p_group = $self->group unless $p_group; - #It is possible to nest all of the callbacks in at once, but the quoting - #becomes very complicated and happers readability - my $success_callback_prefix = $self->get_object_name."->unlink_to_interface('$p_group',"; - my $success_callback = ""; - my $failure_callback = "::print_log('[Insteon::BaseInsteon] ERROR: Unlink_To_Interface ". - "failed for device: ".$self->get_object_name."')"; - $step = 0 if ($step eq ''); - if ($step == 0) { #Delete link on the device - if ($self->_aldb) { - $success_callback = $success_callback_prefix . "'1')"; - $self->_aldb->delete_link(object => $self->interface, - group => $p_group, - data3=> $p_group, is_controller => 1, - callback => $success_callback, - failure_callback=> $failure_callback); - } - else - { - &main::print_log("[BaseInsteon] This item " . $self->get_object_name . - " does not have an ALDB object. Unlinking is not permitted."); - } - } - elsif ($step == 1) { #Delete link on the PLM - $success_callback = $success_callback_prefix . "'2')"; - $self->interface->delete_link( - deviceid => lc($self->device_id), - group=> $p_group, is_controller=>0, - callback=>$success_callback, - failure_callback=>$failure_callback); - } - elsif ($step == 2){ #Delete surrogate link on device if surrogate exists - if (ref $$self{surrogate}){ - $success_callback = $success_callback_prefix . "'3')"; - my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( object => $self->interface, - group => $surrogate_group, is_controller => 0, - callback => "$success_callback", - failure_callback=> "$failure_callback", - data3 => $p_group); - $self->_aldb->delete_link(%link_info); - } else { - ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". - " successfully completed for device " - . $self->get_object_name); - } - } - elsif ($step == 3){ #Delete surrogate link on PLM if surrogate exists - $success_callback = $success_callback_prefix . "'4')"; - my $surrogate_group = $$self{surrogate}->group; - my %link_info = ( deviceid=> lc($self->device_id), - group => $surrogate_group, is_controller => 1, - callback => "$success_callback", - failure_callback=> "$failure_callback", - data3 => $surrogate_group); - $self->interface->delete_link(%link_info); - } - elsif ($step == 4) { - ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". - " successfully completed for device " - . $self->get_object_name); - } -} - -=item C - -Places an i2 object into linking mode as if you had held down the set button on -the device. i1 objects will not respond to this command. This is needed to -link i2CS devices that will not respond without a manual link. - -This process is included as part of the link_to_interface voice command and -should not need to be called seperately. - -Returns: nothing - -=cut - -sub enter_linking_mode -{ - my ($self,$p_group, $success_callback, $failure_callback) = @_; - my $group = $p_group; - $group = '01' unless $group; - my $extra = sprintf("%02x", $group); - $extra .= '0' x (30 - length $extra); - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'linking_mode', $extra); - $message->success_callback($success_callback); - $message->failure_callback($failure_callback); - $self->_send_cmd($message); -} - -=item C - -Sets the defined flag on the device. - -=cut - -sub set_operating_flag { - my ($self, $flag) = @_; - - if (!(exists($$self{operating_flags}{$flag}))) - { - &::print_log("[Insteon::BaseDevice] $flag is not a support operating flag"); - return; - } - - if ($self->is_root and !($self->isa('Insteon::InterfaceController'))) - { - my $message; - if (ref $self->_aldb && $self->_aldb->isa('Insteon::ALDB_i2')) - { - $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'set_operating_flags'); - $message->extra($$self{operating_flags}{$flag} . "0000000000000000000000000000"); - } else { - $message = new Insteon::InsteonMessage('insteon_send', $self, 'set_operating_flags'); - $message->extra($$self{operating_flags}{$flag}); - } - $self->_send_cmd($message); - } - else - { - &::print_log("[Insteon::BaseDevice] " . $self->get_object_name . " is either not a root device or is a plm controlled scene"); - return; - } -} - -=item C - -Requests the device's operating flag and prints it to the log. - -=cut - -sub get_operating_flag { - my ($self) = @_; - - if ($self->is_root and !($self->isa('Insteon::InterfaceController'))) - { - # TO-DO: check devcat to determine if the action is supported by the device - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'get_operating_flags'); - $self->_send_cmd($message); -# $self->_send_cmd('command' => 'get_operating_flags'); - } - else - { - &::print_log("[Insteon::BaseDevice] " . $self->get_object_name . " is either not a root device or is a plm controlled scene"); - return; - } -} - -=item C - -Appears to set and or return the "writable" state. Does not appear to be used -in the Insteon code, but may be necessary for supporting other classes? - -=cut - -sub writable { - my ($self, $p_write) = @_; - if (defined $p_write) - { - if ($p_write =~ /r/i or $p_write =~/^0/) - { - $$self{m_write} = 0; - } - else - { - $$self{m_write} = 1; - } - } - return $$self{m_write}; -} - -=item C - -Returns the is_locally_set variable. Doesn't appear to be used in Insteon code. -Likely was used to test whether setby was equal to the device itself. May not -always be properly updated since it appears unused. - -=cut - -sub is_locally_set { - my ($self) = @_; - return $$self{m_is_locally_set}; -} - -=item C - -Returns true if the object is the base object, generally group 01, on the device. - -=cut - -sub is_root { - my ($self) = @_; - return (($self->group eq '01') and !($self->isa('Insteon::InterfaceController'))) ? 1 : 0; -} - -=item C - -Returns the root object of a device. - -=cut - -sub get_root { - my ($self) = @_; - if ($self->is_root) - { - return $self; - } - else - { - my $root_obj = &Insteon::get_object($self->device_id, '01'); - ::print_log ("[Insteon::BaseDevice] ERROR! Cannot find the root object for " - . $self->get_object_name . ". Please check your mht file to make sure " - . "that device id " . $self->device_id . ":01 is defined.") - if (!defined($root_obj)); - return $root_obj; - } -} - -=item C - -If a device has an ALDB, passes link_details onto one of the has_link() routines -within L. Generally called as part of C. - -=cut - -sub has_link -{ - my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - return $aldb->has_link($insteon_object, $group, $is_controller, $subaddress); - } - else - { - return 0; - } - -} - -=item C - -If a device has an ALDB, passes link_details onto one of the add_link() routines -within L. Generally called from the "sync links" or -"link to interface" voice commands. - -=cut - -sub add_link -{ - my ($self, $parms_text) = @_; - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - $aldb->add_link(%link_parms); - } - -} - -=item C - -If a device has an ALDB, passes link_details onto one of the update_link() routines -within L. Generally called from the "sync links" -voice command. - -=cut - -sub update_link -{ - my ($self, $parms_text) = @_; - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - $aldb->update_link(%link_parms); - } -} - -=item C - -If a device has an ALDB, passes link_details onto one of the delete_link() routines -within L. Generally called by C. - -=cut - -sub delete_link -{ - my ($self, $parms_text) = @_; - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - $aldb->delete_link(%link_parms); - } -} - -=item C - -Scans a device link table and caches a copy. - -=cut - -sub scan_link_table -{ - my ($self, $success_callback, $failure_callback) = @_; - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - return $aldb->scan_link_table($success_callback, $failure_callback); - } - -} - -=item C - -Prints a human readable description of various settings and attributes associated -with a device including: - -Hop Count, Engine Version, ALDB Type, ALDB Health, and Last ALDB Scan Time - -=cut - -sub log_aldb_status -{ - my ($self) = @_; - main::print_log( " Device ID: ".$self->device_id()); - my $hop_array; - $hop_array = join("",@{$$self{hop_array}}) if (defined($$self{hop_array})); - main::print_log( " Hop Count: ".$self->default_hop_count()." :: [$hop_array]"); - main::print_log( "Engine Version: ".$self->engine_version()); - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - main::print_log( " ALDB Type: ".ref($aldb)); - main::print_log( " ALDB Health: ".$aldb->health()); - main::print_log( "ALDB Scan Time: ".$aldb->scandatetime()); - } -} - -=item C - -In theory, would have the same effect as manually tapping the set button on the -device repeat times. - -WARN: Testing using the following does not produce results as expected. Use at -your own risk. [GL] - -=cut - -sub remote_set_button_tap -{ - my ($self,$p_number_taps) = @_; - my $taps = ($p_number_taps =~ /2/) ? '02' : '01'; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'remote_set_button_tap'); - $message->extra($taps); - $self->_send_cmd($message); -# $self->_send_cmd('command' => 'remote_set_button_tap', 'extra' => $taps); -} - -=item C - -Requests the current status of the device and calls C on the response. -This will trigger tied_events. - -=cut - -sub request_status -{ - my ($self, $requestor) = @_; - $$self{m_status_request_pending} = ($requestor) ? $requestor : 1; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'status_request'); - $self->_send_cmd($message); -# $self->_send_cmd('command' => 'status_request', 'is_synchronous' => 1); -} - -=item C - -Queues a get engine version insteon message using L -and sets a message failure callback to L. -Message response is processed in L - -Returns: nothing - -=cut - -sub get_engine_version { - my ($self, $success_callback, $failure_callback) = @_; - - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'get_engine_version'); - my $self_object_name = $self->get_object_name; - $message->failure_callback("$self_object_name->_get_engine_version_failure();$failure_callback"); - $message->success_callback($success_callback); - $self->_send_cmd($message); -} - -=item C<_get_engine_version_failure> - -Callback failure for L; called for NAK -and message timeout. Will force engine_version to I2CS which will also remap -the aldb version if the device responds with a NAK. Does nothing for timeouts -except print a message. - -Returns: nothing - -=cut - -sub _get_engine_version_failure -{ - my ($self) = @_; - my $failure_reason = $self->failure_reason(); - - main::print_log("[Insteon::BaseDevice::_get_engine_version_failure] DEBUG4: " - ."failure reason: $failure_reason") if $self->debuglevel(4, 'insteon'); - - if($failure_reason eq 'NAK') - { - #assume I2CS because no other device will NAK this command - main::print_log("[Insteon::BaseDevice] WARN: I2CS device (" . $self->get_object_name . ") is not " - ."linked; Please use 'link to interface' voice command"); - $self->engine_version('I2CS'); - } - #Clear success callback, otherwise it will run when message is cleared - $self->interface->active_message->success_callback('0'); -} - -=item C - -Sends the number of ping messages defined by count. A ping message is a basic -message that simply asks the device to respond with an ACKnowledgement. For -i1 devices this will send a standard length command, for i2 and i2cs devices -this will send an extended ping command. In both cases, the device responds -back with a standard length ACKnowledgement only. - -Much like the ping command in IP networks, this command is useful for testing the -connectivity of a device on your network. You likely want to use this in -conjunction with the C routine. For example, you can use -this to compare the message stats for a device when changing settings in -MisterHouse. - -Parameters: - count = the number of pings to send. - -Returns: Nothing. - -=cut - -sub ping -{ - my ($self, $p_count, $p_callback) = @_; - $$self{ping_count} = $p_count if defined($p_count); - $$self{ping_callback} = $p_callback if defined($p_callback); - if ($$self{ping_count}) { - $$self{ping_count}--; - my $message; - my $extra = sprintf("%02d", $$self{ping_count}); - if (uc($self->engine_version) eq 'I1'){ - $message = new Insteon::InsteonMessage('insteon_send', $self, - 'ping', $extra); - } - else { - my $extra = $extra . '0' x 28; - $message = new Insteon::InsteonMessage('insteon_ext_send', $self, - 'ping', $extra); - } - ::print_log("[Insteon::BaseDevice] Sending ping request to " - . $self->get_object_name . " " . $$self{ping_count} - . " more ping requests queued."); - $message->failure_callback($self->get_object_name . '->ping()'); - $self->_send_cmd($message); - } - else { - ::print_log("[Insteon::BaseDevice] Completed ping queue for " - . $self->get_object_name); - if (defined $$self{ping_callback}){ - my $complete_callback = $$self{ping_callback}; - package main; - eval ($complete_callback); - &::print_log("[Insteon::BaseDevice] error in ping callback: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon::BaseDevice; - delete $$self{ping_callback}; - } - } -} - -=item C - -Used to set the led staatus of SwitchLinc companion leds. - -=cut - -sub set_led_status -{ - my ($self, $status_mask) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'set_led_status'); - $message->extra($status_mask); - $self->_send_cmd($message); -# $self->_send_cmd('command' => 'set_led_status', 'extra' => $status_mask); -} - -=item C - -This is called by mh on exit to save the cached ALDB of a device to persistant data. - -=cut - -sub restore_string -{ - my ($self) = @_; - my $restore_string = $self->SUPER::restore_string(); - if ($self->_aldb) - { - $restore_string .= $self->_aldb->restore_string(); - } - - return $restore_string; -} - -=item C - -Obsolete / do not use. - -Function should remain so that upgrading users will not have issues starting -MH from previous versions that referenced this function in the -mh_temp.saved_states file. - -=cut - -sub restore_states -{ - my ($self, $states) = @_; -} - -=item C - -Used to reload the aldb of a device on restart. - -=cut - -sub restore_aldb -{ - my ($self,$aldb) = @_; - if ($self->_aldb and $aldb) - { - $self->_aldb->restore_aldb($aldb); - } -} - -=item C - -Sets and returns the device category of a device. Devcat can be requested by -calling C. - -=cut - -sub devcat -{ - my ($self, $devcat) = @_; - if ($devcat) - { - $$self{devcat} = $devcat; - } - return $$self{devcat}; -} - -=item C - -Requests the device category for the device. The returned value can be obtained -by calling C. - -=cut - -sub get_devcat -{ - my ($self) = @_; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'id_request'); - $self->_send_cmd($message); -} - -=item C - -Sets and returns the device firmware version. Value can be obtained from the -device by calling C. - -=cut - -sub firmware -{ - my ($self, $firmware) = @_; - $$self{firmware} = $firmware if (defined $firmware); - return $$self{firmware}; -} - -=item C - -Sets and returns the available states for a device. - -=cut - -sub states -{ - my ($self, $states) = @_; - if ($states) - { - @{$$self{states}} = split(/,/,$states); - } - if ($$self{states}) - { - return @{$$self{states}}; - } else { - return undef; - } - -} - -=item C - -Reviews the cached version of all of the ALDBs and based on this review removes -links from this device which are not present in the mht file, not defined in the -code, or links which are only half-links. - -If audit_mode is true, prints the actions that would be taken to the log, but -does nothing. - -=cut - -sub delete_orphan_links -{ - my ($self, $audit_mode, $failure_callback) = @_; - return $self->_aldb->delete_orphan_links($audit_mode, $failure_callback) if $self->_aldb; -} - -sub _process_delete_queue { - my ($self) = @_; - $self->_aldb->_process_delete_queue() if $self->_aldb; -} - -=item C - -Prints a human readable form of MisterHouse's cached version of a device's ALDB -to the print log. Called as part of the "scan links" voice command -or in response to the "log links" voice command. - -=cut - -sub log_alllink_table -{ - my ($self) = @_; - $self->_aldb->log_alllink_table if $self->_aldb; -} - -=item C - -Sets or gets the device object engine version. If setting the engine version, -will also call check_aldb_version to map the aldb correctly for I2 devices. - -Parameters: - p_engine_version: [I1|I2|I2CS] to set engine version - -Returns: engine version string [I1|I2|I2CS] - -=cut - -sub engine_version -{ - my ($self, $p_engine_version) = @_; - my $engine_version = $self->SUPER::engine_version($p_engine_version); - $self->check_aldb_version() if $p_engine_version; - return $engine_version; -} - -=item C - -Sets or gets the number of message retries that have occured for this device -since the last time C was called. - -If type is set, to any value, will increment retry log by one. - -Returns: current retry count. - -=cut - -sub retry_count_log -{ - my ($self, $retry_count_log) = @_; - $self = $self->get_root; - $$self{retry_count_log}++ if $retry_count_log; - return $$self{retry_count_log}; -} - -=item C - -Sets or gets the number of message failures that have occured for this device -since the last time C was called. - -If type is set, to any value, will increment fail log by one. - -Returns: current fail count. - -=cut - -sub fail_count_log -{ - my ($self, $fail_count_log) = @_; - $self = $self->get_root; - $$self{fail_count_log}++ if $fail_count_log; - return $$self{fail_count_log}; -} - -=item C - -Sets or gets the number of outgoing message that have occured for this device -since the last time C was called. - -If type is set, to any value, will increment output count by one. - -Returns: current output count. - -=cut - -sub outgoing_count_log -{ - my ($self, $outgoing_count_log) = @_; - $self = $self->get_root; - $$self{outgoing_count_log}++ if $outgoing_count_log; - return $$self{outgoing_count_log}; -} - -=item C - -Simulates a read of a link address from the device. Repeats this process as many -times as defined by count. This routine is meant to be used as a diagnostic tool. -It is similar to scan_link_table, however, if a failure occurs, this process will -continue with the next iteration. Scan link table will stop as soon as a single -failure occurs. - -This is also similar to the C test, however, rather than simply requesting -an ACK, this requests a full set of data equivalent to a link entry. Similar to -C this should be used with C to diagnose issues and try -different settings. - -Note: This routine can create a lot of traffic if count is set very high. Try -setting it to 5 first and working your way up. - -=cut - -sub stress_test -{ - my ($self, $p_count, $complete_callback) = @_; - $$self{stress_test_count} = $p_count if (defined ($p_count)); - $$self{stress_test_callback} = $complete_callback if (defined ($complete_callback)); - if ($$self{stress_test_count}){ - &::print_log("[Insteon::BaseDevice] " . $self->get_object_name - . " - Stress Test " . $$self{stress_test_count} . " iterations left"); - my $aldb = $self->get_root()->_aldb; - if ($aldb) - { - $$aldb{_mem_activity} = 'scan'; - $$aldb{_stress_test_act} = 1; - $$aldb{_failure_callback} = $self->get_object_name - . '->stress_test()'; - if($aldb->isa('Insteon::ALDB_i1')) { - $aldb->_peek('0FF8'); - } else { - #Prevents duplicate commands in queue error - #Also allows for better identification of - #sequential dupe incoming messages - my $odd = $$self{stress_test_count} % 2; - if ($odd){ - $aldb->send_read_aldb('0fff'); - } else { - $aldb->send_read_aldb('0ff7'); - } - } - } - $$self{stress_test_count}--; - } else { - &::print_log("[Insteon::BaseDevice] Stress Test Complete for " - . $self->get_object_name); - if (defined $$self{stress_test_callback}){ - $complete_callback = $$self{stress_test_callback}; - package main; - eval ($complete_callback); - &::print_log("[Insteon::BaseDevice] error in stress_test callback: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon::BaseDevice; - delete $$self{stress_test_callback}; - } - } -} - -=item C - -Sets or gets the number of hops that have been used in all outgoing messages -since the last time C was called. - -If type is set, to any value, will increment output count by that value. - -Returns: current hop count. - -=cut - -sub outgoing_hop_count -{ - my ($self, $outgoing_hop_count) = @_; - $self = $self->get_root; - $$self{outgoing_hop_count} += $outgoing_hop_count if $outgoing_hop_count; - return $$self{outgoing_hop_count}; -} - -=item C - -Sets or gets the number of incoming message that have occured for this device -since the last time C was called. - -If type is set, to any value, will increment incoming count by one. - -Returns: current incoming count. - -=cut - -sub incoming_count_log -{ - my ($self, $incoming_count_log) = @_; - $self = $self->get_root; - $$self{incoming_count_log}++ if $incoming_count_log; - return $$self{incoming_count_log}; -} - -=item C - -Sets or gets the number of currupt message that have arrived from this device -since the last time C was called. - -If type is set, to any value, will increment corrupt count by one. - -Returns: current corrupt count. - -=cut - -sub corrupt_count_log -{ - my ($self, $corrupt_count_log) = @_; - $self = $self->get_root; - $$self{corrupt_count_log}++ if $corrupt_count_log; - return $$self{corrupt_count_log}; -} - -=item C - -Sets or gets the number of duplicate message that have arrived from this device -since the last time C was called. - -If type is set, to any value, will increment corrupt count by one. - -Returns: current duplicate count. - -=cut - -sub dupe_count_log -{ - my ($self, $dupe_count_log) = @_; - $self = $self->get_root; - $$self{dupe_count_log}++ if $dupe_count_log; - return $$self{dupe_count_log}; -} - -=item C - -Sets or gets the number of hops_left for messages that arrive from this device -since the last time C was called. - -If type is set, to any value, will increment corrupt count by one. - -Returns: current hops_left count. - -=cut - -sub hops_left_count -{ - my ($self, $hops_left_count) = @_; - $self = $self->get_root; - $$self{hops_left_count} += $hops_left_count if $hops_left_count; - return $$self{hops_left_count}; -} - -=item C - -Sets or gets the number of max_hops for messages that arrive from this device -since the last time C was called. - -If type is set, to any value, will increment corrupt count by one. - -Returns: current duplicate count. - -=cut - -sub max_hops_count -{ - my ($self, $max_hops_count) = @_; - $self = $self->get_root; - $$self{max_hops_count} += $max_hops_count if $max_hops_count; - return $$self{max_hops_count}; -} - - -=item C - -Resets the retry, fail, outgoing, incoming, and corrupt message counters. - -=cut - -sub reset_message_stats -{ - my ($self) = @_; - $self = $self->get_root; - $$self{retry_count_log} = 0; - $$self{fail_count_log} = 0; - $$self{outgoing_count_log} = 0; - $$self{incoming_count_log} = 0; - $$self{corrupt_count_log} = 0; - $$self{dupe_count_log} = 0; - $$self{hops_left_count} = 0; - $$self{max_hops_count} = 0; - $$self{outgoing_hop_count} = 0; -} - -=item C - -Prints message statistics for this device to the print log. The output contains: - -=back - -=over8 - -=item * - -In - The number of incoming messages received - -=item * - -Corrupt - The number of incoming corrupt messages received - -=item * - -%Corrpt - Of the incoming messages received, the percentage that were -corrupt - -=item * - -Dupe - The number of duplicate messages that have been received from this -device. - -=item * - -%Dupe - The percentage of duplilicate incoming messages received. - -=item * - -Hops_Left - The average hops left in the messages received from this device. - -=item * - -Max_Hops - The average maximum hops in the messages received from this device. - -=item * - -Act_Hops - Max_Hops - Hops_Left, this is the average number of hops that have -been required for a message sent from the device to reach MisterHouse. - -=item * - -Out - The number of unique outgoing messages, without retries, sent. - -=item * - -Fail - The number times that all retries were exhausted without a successful -delivery of a message. - -=item * - -%Fail - Of the outgoing messages sent, the percentage that failed. - -=item * - -Retry - The number of retry attempts that have been made to deliver a message. -Ideally this is 0, but Sends/Msg is a better indication of this parameter. - -=item * - -AvgSend - The average number of send attempts that must be made in order to -successfully deliver a message. Ideally this would be 1.0. - -NOTE: If the number of retries exceeds the value set in the configuration file -for Insteon_retry_count, MisterHouse will abandon sending the message. As a -result, as this number approaches Insteon_retry_count it becomes a less accurate -representation of the number of retries needed to reach a device. - -=item * - -Avg_Hops - The average number of hops that have been used by MisterHouse when -sending messages to this device. - -=item * - -Hop_Count - The current hop count being used by MH. This count is dynamically -controlled by MH and is not reset by calling C - -=back - -=over - -=cut - -sub print_message_stats -{ - my ($self) = @_; - $self = $self->get_root; - my $object_name = $self->get_object_name; - my $retry_average = 0; - $retry_average = sprintf("%.1f", ($$self{retry_count_log} / - $$self{outgoing_count_log}) + 1) if ($$self{outgoing_count_log} > 0); - my $fail_percentage = 0; - $fail_percentage = sprintf("%.1f", ($$self{fail_count_log} / - $$self{outgoing_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); - my $corrupt_percentage = 0; - $corrupt_percentage = sprintf("%.1f", ($$self{corrupt_count_log} / - $$self{incoming_count_log}) * 100 ) if ($$self{incoming_count_log} > 0); - my $dupe_percentage = 0; - $dupe_percentage = sprintf("%.1f", ($$self{dupe_count_log} / - $$self{incoming_count_log}) * 100 ) if ($$self{incoming_count_log} > 0); - my $avg_hops_left = 0; - $avg_hops_left = sprintf("%.1f", ($$self{hops_left_count} / - $$self{incoming_count_log})) if ($$self{incoming_count_log} > 0); - my $avg_max_hops = 0; - $avg_max_hops = sprintf("%.1f", ($$self{max_hops_count} / - $$self{incoming_count_log})) if ($$self{incoming_count_log} > 0); - my $avg_out_hops = 0; - $avg_out_hops = sprintf("%.1f", ($$self{outgoing_hop_count} / - $$self{outgoing_count_log})) if ($$self{outgoing_count_log} > 0); - ::print_log( - "[Insteon::BaseDevice] Message statistics for $object_name:\n" - . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops\n" - . sprintf("%6s", $$self{incoming_count_log}) - . sprintf("%8s", $$self{corrupt_count_log}) - . sprintf("%8s", $corrupt_percentage . '%') - . sprintf("%6s", $$self{dupe_count_log}) - . sprintf("%8s", $dupe_percentage . '%') - . sprintf("%9s", $avg_hops_left) - . sprintf("%9s", $avg_max_hops) - . sprintf("%9s", $avg_max_hops - $avg_hops_left) - . "\n" - . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" - . sprintf("%6s", $$self{outgoing_count_log}) - . sprintf("%8s", $$self{fail_count_log}) - . sprintf("%8s", $fail_percentage . '%') - . sprintf("%6s", $$self{retry_count_log}) - . sprintf("%8s", $retry_average) - . sprintf("%9s", $avg_out_hops) - . sprintf("%9s", $self->default_hop_count) - ); -} - - -=item C - -Because of the way MH saves / restores states "after" object creation -the aldb must be initially created before the engine_version is restored. -It is therefore impossible to know the device is i2 before creating -the aldb object. The solution is to keep the existing logic which assumes -the device is peek/poke capable (i1 or i2) and then delete/recreate the -aldb object if it is later determined to be an i2 device. - -There is a use case where a device is initially I2 but the user replaces -the device with an I1 device, reusing the same object name. In this case -the object state restore will build an I2 aldb object. The user must -manually initiate the 'get engine version' voice command or stop/start -MH so the initial poll will detect the change. - -This is called anytime the engine_version is queried (initial startup poll) and -in the Reload_post_hooks once object_states_restore completes - -=cut - -sub check_aldb_version -{ - my ($self) = @_; - - my $engine_version = $self->SUPER::engine_version(); - my $new_version = ""; - if($engine_version and $engine_version ne 'I1' and $self->_aldb->aldb_version() ne 'I2') { - $new_version = "I2"; - } - elsif($engine_version eq 'I1' and $self->_aldb->aldb_version() ne 'I1') { - $new_version = "I1"; - } - if ($new_version) { - main::print_log("[Insteon::BaseDevice] DEBUG4: aldb_version is " - .$self->_aldb->aldb_version()." but device is ".$engine_version. - ". Remapping aldb version to $new_version") if $self->debuglevel(4, 'insteon'); - my $restore_string = ''; - if ($self->_aldb) { - $restore_string = $self->_aldb->restore_string(); - } - undef $self->{aldb}; - - if ($new_version eq "I2") { - $self->{aldb} = new Insteon::ALDB_i2($self); - } - else { - $self->{aldb} = new Insteon::ALDB_i1($self); - } - - package main; - eval ($restore_string); - &::print_log("[Insteon::BaseDevice] error in eval creating ALDB object: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon::BaseDevice; - } -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds}, - 'link to interface' => "$object_name->link_to_interface", - 'unlink with interface' => "$object_name->unlink_to_interface" - ); - if ($self->is_root){ - %voice_cmds = ( - %voice_cmds, - 'status' => "$object_name->request_status", - 'get engine version' => "$object_name->get_engine_version", - 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", - 'print message stats' => "$object_name->print_message_stats", - 'reset message stats' => "$object_name->reset_message_stats", - 'run stress test' => "$object_name->stress_test(5)", - 'run ping test' => "$object_name->ping(5)", - 'log links' => "$object_name->log_alllink_table()" - ) - } - return \%voice_cmds; -} - -=back - -=head2 INI PARAMETERS - -=over - -=item Insteon_PLM_max_queue_time - -Was previously used to set the maximum amount of time -a message could remain in the queue. This parameter is no longer used in the code -but it still appears in the initialization. It may be removed at a future date. -This also gets set in L -as well for some reason, but is not used there either. - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -#################################### -### ############### -### MultigroupDevice ############### -### ############### -#################################### - -=head1 B - -=head2 DESCRIPTION - -Contains functions which are unique to insteon devices which have more than -one group. This includes, KeyPadLincs, RemoteLincs, FanLincs, Thermostats -(i2 versions). - -=head2 INHERITS - -Nothing. - -This package is meant to provide supplemental support and should only be added -as a secondary inheritance to an object. - -=head2 METHODS - -=over - -=cut - -package Insteon::MultigroupDevice; - -=item C - -Syncs all links on the object, including all subgroups such as additional -buttons. - -Paramter B - Causes sync to walk through but not actually -send any commands to the devices. Useful with the insteon:3 debug setting for -troubleshooting. - -=cut - -sub sync_all_links -{ - my ($self, $audit_mode) = @_; - $self = $self->get_root(); - @{$$self{_sync_devices}} = (); - @{$$self{_sync_device_failures}} = (); - my $device_id = $self->device_id(); - my ($subgroup_object, $group, $dec_group); - - ::print_log("[Insteon::MultigroupDevice] Sync All Links on device " - .$self->get_object_name . " starting ..."); - # Find all subgroup items check groups from 02 - FF; - for ($dec_group = 01; $dec_group <= 255; $dec_group++) { - $group = sprintf("%02X", $dec_group); - $subgroup_object = Insteon::get_object($device_id, $group); - if (ref $subgroup_object){ - my %sync_req = ('sync_object' => $subgroup_object, - 'audit_mode' => ($audit_mode) ? 1 : 0); - ::print_log("[Insteon::MultigroupDevice] " - ."Adding " . $subgroup_object->get_object_name - ." to sync queue."); - push @{$$self{_sync_devices}}, \%sync_req - } - } - $$self{_sync_cnt} = scalar @{$$self{_sync_devices}}; - $self->_get_next_linksync(); -} - -=item C<_get_next_linksync()> - -Calls the sync_links() function for each device identified by sync_all_link. -This function will be called recursively since the callback passed to sync_links() -is this function again. Will also ask sync_links() to call -_get_next_linksync_failure() if sync_links() fails. - -=cut - -sub _get_next_linksync -{ - my ($self) = @_; - $self = $self->get_root(); - my $sync_req_ptr = shift(@{$$self{_sync_devices}}); - my %sync_req = ($sync_req_ptr) ? %$sync_req_ptr : undef; - my $current_sync_device; - if (%sync_req) { - $current_sync_device = $sync_req{'sync_object'}; - } - else { - $current_sync_device = undef; - } - - if ($current_sync_device) { - ::print_log("[Insteon::MultigroupDevice] Now syncing: " - . $current_sync_device->get_object_name . " (" - . ($$self{_sync_cnt} - scalar @{$$self{_sync_devices}}) - . " of ".$$self{_sync_cnt}.")"); - $current_sync_device->sync_links($sync_req{'audit_mode'}, - $self->get_object_name . '->_get_next_linksync()', - $self->get_object_name . '->_get_next_linksync_failure('.$current_sync_device->get_object_name.')'); - } - else { - ::print_log("[Insteon::MultigroupDevice] All links have completed syncing " - . "on device " . $self->get_object_name); - my $_sync_failure_cnt = scalar @{$$self{_sync_device_failures}}; - if ($_sync_failure_cnt) { - ::print_log("[Insteon::MultigroupDevice] However, some failures were noted:"); - for my $failed_obj (@{$$self{_sync_device_failures}}) { - ::print_log("[Insteon::MultigroupDevice] WARN: failure occurred when syncing " - . $failed_obj->get_object_name); - } - } - } -} - -=item C<_get_next_linksync()> - -Called by the failure callback in a device's sync_links() function. Will add -the failed device to the module global variable @_sync_device_failures. - -=cut - -sub _get_next_linksync_failure -{ - my ($self, $current_sync_device) = @_; - $self = $self->get_root(); - push @{$$self{_sync_device_failures}}, $current_sync_device; - ::print_log("[Insteon::MultigroupDevice] WARN: failure occurred when syncing " - . $current_sync_device->get_object_name . ". Moving on..."); - $self->_get_next_linksync(); -} - -=back - -=head2 AUTHOR - -Kevin Robert Keegan - -=head2 LICENSE - -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. - -=cut - -#################################### -### ################# -### BaseController ################# -### ################# -#################################### - -=head1 B - -=head2 DESCRIPTION - -Generic class implementation of an Insteon Controller. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::BaseController; - -use strict; - -@Insteon::BaseController::ISA = ('Generic_Item'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface,$p_devcat) = @_; - - # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller - my $self = {}; - bless $self,$class; - return $self; -} - -=item C - -Adds object as a responder to the device. If on_level and ramp_rate are specified -they will be used, otherwise 100% .1s will be used. - -=cut - -sub add -{ - my ($self, $obj, $on_level, $ramp_rate) = @_; - if (ref $obj and ($obj->isa('Light_Item') or $obj->isa('Insteon::BaseDevice'))) - { - if ($$self{members} && $$self{members}{$obj}) - { - print "[Insteon::BaseController] An object (" . $obj->{object_name} . ") already exists " - . "in this scene. Aborting add request.\n"; - return; - } - if ($on_level =~ /^sur/i) - { - $on_level = '100%'; - $$obj{surrogate} = $self; - } - elsif (lc $on_level eq 'on') - { - $on_level = '100%'; - } - elsif (lc $on_level eq 'off') - { - $on_level = '0%'; - } - $on_level = '100%' unless $on_level; - $$self{members}{$obj}{on_level} = $on_level; - $$self{members}{$obj}{object} = $obj; - $ramp_rate =~ s/s$//i; - $$self{members}{$obj}{ramp_rate} = $ramp_rate if defined $ramp_rate; - } else { - &::print_log("[Insteon::BaseController] WARN: unable to add ".$obj->{device_id}.":".$obj->{m_group} - ." as items of type ".ref($obj)." are not supported!"); - } -} - -=item C - -Causes MisterHouse to review the list of objects that should be linked to device -and to check to make sure these links are complete. If any link is not complete -MisterHouse will add the link. - -If audit_mode is true, MisterHouse will print the actions it would have taken to -the log, but will not take any actions. - -The process does the following 5 checks in order: - - 1. Does a controller link exist for Device-> PLM - 2. Does a responder link exist on the PLM - -it then loops through all of the links defined for a device and checks: - - 3. Does the responder link exist - 4. Is the responder link accurate - 5. Does the controller link on this device exist - -=cut - -sub sync_links -{ - my ($self, $audit_mode, $callback, $failure_callback, $skip_deaf) = @_; - - # Intialize Variables - @{$$self{sync_queue}} = (); # reset the work queue - $$self{sync_queue_callback} = ($callback) ? $callback : undef; - $$self{sync_queue_failure_callback} = ($failure_callback) ? $failure_callback : undef; - $$self{sync_queue_failure} = undef; - my $self_link_name = $self->get_object_name; - my $insteon_object = $self->interface; - my $interface_object = Insteon::active_interface(); - my $interface_name = $interface_object->get_object_name; - $insteon_object = $self->get_root; - ::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " - . "Please double check your items.mht file.") if (!(defined($insteon_object))); - - # Abort if $insteon_object doesn't exist - $self->_process_sync_queue() unless $insteon_object; - - # Warn if device is deaf or ALDB out of sync - my $insteon_object_is_syncable = 1; - if ($insteon_object->is_deaf && $skip_deaf) { - ::print_log("[Insteon::BaseController] $self_link_name is deaf, only responder links will be added to devices " - ."controlled by this device. To sync links on this device, put it in awake mode and run the 'Sync Links' " - ."command on this specific device."); - $insteon_object_is_syncable = 0; - } - elsif ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty'){ - ::print_log("[Insteon::BaseController] WARN! The ALDB of $self_link_name is ".$insteon_object->_aldb->health - .", links will be added to devices " - ."linked to this device, but no links will be added to $self_link_name. Please rescan this device and attempt " - ."sync links again."); - $insteon_object_is_syncable = 0; - $$self{sync_queue_failure} = 1; - } - - # 1. Does a controller link exist for Device-> PLM - if (!$insteon_object->isa('Insteon_PLM') && - !$insteon_object->has_link($self->interface,$self->group,1,$self->group) && - $insteon_object_is_syncable) { - my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, - group => $self->group, is_controller => 1, - callback => "$self_link_name->_process_sync_queue()", - failure_callback => $failure_callback, - data3 => $self->group); - $link_req{cause} = "Adding controller record to $self_link_name for $interface_name"; - push @{$$self{sync_queue}}, \%link_req; - } - - # 2. Does a responder link exist on the PLM - if ((!$insteon_object->isa('Insteon_PLM') && - !$self->interface->has_link($insteon_object,$self->group,0,'00'))) { - my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, - group => $self->group, is_controller => 0, - callback => "$self_link_name->_process_sync_queue()", - failure_callback => $failure_callback, - data3 => '00'); - $link_req{cause} = "Adding responder record to $interface_name from $self_link_name"; - push @{$$self{sync_queue}}, \%link_req; - } - - # Loop members - foreach my $member_ref (keys %{$$self{members}}) { - my $member = $$self{members}{$member_ref}{object}; - - # find real device if member is a Light_Item - if ($member->isa('Light_Item')) { - my @children = $member->find_members('Insteon::BaseDevice'); - $member = $children[0]; - } - - #Initialize Loop Variables - my $member_name = $member->get_object_name; - my $member_root = $member->get_root; - my $requires_update = 0; - my $has_link = 1; - my $cause; - my $tgt_on_level = $$self{members}{$member_ref}{on_level}; - $tgt_on_level = '100' unless defined $tgt_on_level; - my $tgt_ramp_rate = $$self{members}{$member_ref}{ramp_rate}; - $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; - $tgt_on_level =~ s/(\d+)%?/$1/; - $tgt_ramp_rate =~ s/(\d)s?/$1/; - my $resp_aldbkey = $member_root->_aldb->get_linkkey($insteon_object->device_id, - $self->group, - '0', - $member->group); - - # 3. Does the responder link exist - if (!$member_root->has_link($insteon_object, $self->group, 0, $member->group) && - ($member_root->_aldb->health eq 'good' || $member_root->_aldb->health eq 'empty')){ - my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, - group => $self->group, is_controller => 0, - on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, - callback => "$self_link_name->_process_sync_queue()", - failure_callback => $failure_callback, - data3 => $member->group); - $link_req{cause} = "Adding responder record to $member_name from $self_link_name"; - push @{$$self{sync_queue}}, \%link_req; - $has_link = 0; - } - elsif ($member_root->_aldb->health ne 'good' && $member_root->_aldb->health ne 'empty'){ - my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, - group => $self->group, is_controller => 0, - on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, - callback => "$self_link_name->_process_sync_queue()", - failure_callback => $failure_callback, - data3 => $member->group); - $link_req{skip} = "Unable to add the following responder record to $member_name " - ."from $self_link_name because the aldb of $member_name is " - . $member_root->_aldb->health; - push @{$$self{sync_queue}}, \%link_req; - $$self{sync_queue_failure} = 1; - } - - # 4. Is the responder link accurate - if ($member->isa('Insteon::DimmableLight') && $has_link) { - my $member_aldb = $member_root->_aldb; - my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; - my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; - my $cur_on_level = hex($data1)/2.55; - my $raw_cur_ramp_rate = $data2; - my $raw_tgt_ramp_rate = Insteon::DimmableLight::convert_ramp($tgt_ramp_rate); - if ($raw_cur_ramp_rate ne $raw_tgt_ramp_rate) { - $requires_update = 1; - $cause .= "Ramp rate "; - } - elsif ($cur_on_level-1 > $tgt_on_level && $cur_on_level+1 < $tgt_on_level){ - $requires_update = 1; - $cause .= "On level "; - } - } - elsif ($has_link){ - my $member_aldb = $member->_aldb; - my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; - my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; - if ($tgt_on_level >= 1 and $data1 ne 'ff') { - $requires_update = 1; - $tgt_on_level = 100; - $cause .= "On level "; - } - elsif ($tgt_on_level == 0 and $data1 ne '00') { - $requires_update = 1; - $cause .= "On level "; - } - if ($data2 ne '00') { - $requires_update = 1; - $tgt_ramp_rate = 0; - $cause .= "Ramp rate "; - } - } - if ($requires_update && - ($member_root->_aldb->health eq 'good' || $member_root->_aldb->health eq 'empty')) { - my %link_req = ( member => $member, cmd => 'update', object => $insteon_object, - group => $self->group, is_controller => 0, - on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, - callback => "$self_link_name->_process_sync_queue()", - failure_callback => $failure_callback, - data3 => $member->group); - $link_req{cause} = "Updating responder record on $member_name " - . "to fix $cause"; - push @{$$self{sync_queue}}, \%link_req; - } - - # 5. Does the controller link on this device exist - if (!($insteon_object->has_link($member, $self->group, 1, $self->group)) && - $insteon_object_is_syncable) { - my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, - group => $self->group, is_controller => 1, - callback => "$self_link_name->_process_sync_queue()", - failure_callback => $failure_callback, - data3 => $self->group); - $link_req{cause} = "Adding controller record to $self_link_name for $member_name"; - push @{$$self{sync_queue}}, \%link_req; - } - } - - my $num_sync_queue = @{$$self{sync_queue}}; - my $index = 0; - if (!($num_sync_queue)) - { - &::print_log("[Insteon::BaseController] Nothing to do when syncing links for " . $self->get_object_name) - if $self->debuglevel(1, 'insteon'); - } - foreach (@{$$self{sync_queue}}){ - my %sync_req = %{$_}; - my $audit_text = "(AUDIT)" if ($audit_mode); - my $log_text = "[Insteon::BaseController] $audit_text "; - if ($sync_req{skip}){ - $log_text .= $sync_req{skip} ."\n"; - splice @{$$self{sync_queue}}, $index, 1; - } else { - $index++; - $log_text .= $sync_req{cause} . "\n"; - } - PRINT: for (keys %sync_req) { - next PRINT if (($_ eq 'cause') || ($_ eq 'callback') || - ($_ eq 'member') || ($_ eq 'object') || - ($_ eq 'skip') || ($_ eq 'failure_callback')); - $log_text .= "$_ = $sync_req{$_}; "; - } - ::print_log($log_text); - } - if ($audit_mode) { - @{$$self{sync_queue}} = (); - } - - $self->_process_sync_queue(); -} - -sub _process_sync_queue { - my ($self) = @_; - # get next in queue if it exists - my $num_sync_queue = @{$$self{sync_queue}}; - if ($num_sync_queue) { - my $link_req_ptr = shift(@{$$self{sync_queue}}); - my %link_req = %$link_req_ptr; - if ($link_req{cmd} eq 'update') { - my $link_member = $link_req{member}; - $link_member->update_link(%link_req); - } elsif ($link_req{cmd} eq 'add') { - my $link_member = $link_req{member}; - $link_member->add_link(%link_req); - } - } elsif ($$self{sync_queue_callback}) { - my $callback = $$self{sync_queue_callback}; - if ($$self{sync_queue_failure}){ - $callback = $$self{sync_queue_failure_callback}; - } - package main; - eval ($callback); - &::print_log("[Insteon::BaseController] error in sync links callback: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon::BaseController; - } else { - main::print_log($self->get_object_name." completed sync links"); - } -} - -=item C - -Checks each linked member of device. If the linked member is a C, -then sets that item to state. If the linked member is a C -then call C for the member. - -=cut - -sub set_linked_devices -{ - my ($self, $link_state) = @_; - # iterate over the members - if ($$self{members}) - { - foreach my $member_ref (keys %{$$self{members}}) - { - my $member = $$self{members}{$member_ref}{object}; - # If controller is on, set member to stored on_level - # else set to controller value - my $local_state = $$self{members}{$member_ref}{on_level}; - $local_state = '100' unless $local_state; - - if ($member->isa('Light_Item')) - { - # if they are Light_Items, then set their on_dim attrib to the member on level - # and then "blank" them via the manual method for a tad over the ramp rate - # In addition, locate the Light_Item's Insteon_Device member and do the - # same as if the member were an Insteon_Device - my $ramp_rate = $$self{members}{$member_ref}{ramp_rate}; - $ramp_rate = 0 unless defined $ramp_rate; - $ramp_rate = $ramp_rate + 2; - my @lights = $member->find_members('Insteon::BaseDevice'); - if (@lights) - { - my $light = $lights[0]; - # remember the current state to support resume - $$self{members}{$member_ref}{resume_state} = $light->state; - $member->manual($light, $ramp_rate); - if (lc $link_state ne 'on'){ - $local_state = $light->$link_state; - } - $light->set_receive($local_state,$self); - } - else - { - $member->manual(1, $ramp_rate); - } - $member->set_on_state($local_state) unless $link_state eq 'off'; - } - elsif ($member->isa('Insteon::BaseDevice')) - { - # remember the current state to support resume - $$self{members}{$member_ref}{resume_state} = $member->state; - # if they are Insteon_Device objects, then simply set_receive their state to - # the member on level - if (lc $link_state ne 'on'){ - $local_state = $link_state; - } - $member->set_receive($local_state,$self); - } - } - } -} - -=item C - -NOTE - This routine appears to be nearly identical, if not identical to the -C routine, it is not clear why this routine is -needed here. - -See full description of this routine in C - -=cut - -sub set_with_timer { - my ($self, $state, $time, $return_state, $additional_return_states) = @_; - return if &main::check_for_tied_filters($self, $state); - - $self->set($state) unless $state eq ''; - - return unless $time; - - my $state_change = ($state eq 'off') ? 'on' : 'off'; - $state_change = $return_state if defined $return_state; - $state_change = $self->{state} if $return_state and lc $return_state eq 'previous'; - - $state_change .= ';' . $additional_return_states if $additional_return_states; - - $$self{set_timer} = &Timer::new() unless $$self{set_timer}; - my $object_name = $self->{object_name}; - my $action = "$object_name->set('$state_change')"; - $$self{set_timer}->set($time, $action); -} - -=item C - -This appears to be a depricated routine that is no longer used. At a cursory -glance, it may throw an error if called. - -=cut - -sub update_members -{ - my ($self) = @_; - # iterate over the members - if ($$self{members}) { - foreach my $member_ref (keys %{$$self{members}}) { - my ($device); - my $member = $$self{members}{$member_ref}{object}; - my $on_state = $$self{members}{$member_ref}{on_level}; - $on_state = '100%' unless $on_state; - my $ramp_rate = $$self{members}{$member_ref}{ramp_rate}; - $ramp_rate = 0 unless defined $ramp_rate; - if ($member->isa('Light_Item')) { - # if they are Light_Items, then locate the Light_Item's Insteon_Device member - my @lights = $member->find_members('Insteon::BaseDevice'); - if (@lights) { - $device = $lights[0]; - } - } elsif ($member->isa('Insteon::BaseDevice')) { - $device = $member; - } - if ($device) { - my %current_record = $device->get_link_record($self->device_id . $self->group); - if (%current_record) { - &::print_log("[Insteon::BaseController] remote record: $current_record{data1}") - if $self->debuglevel(1, 'insteon'); - } - } - } - } -} - -=item C - -Places the interface in linking mode as the controller. To complete the process -press the set button on the responder device until it beeps. Alternatively, -you may be able to complete the process with -C. - -=cut - -sub initiate_linking_as_controller -{ - my ($self, $p_group, $success_callback, $failure_callback) = @_; - # iterate over the members - if ($$self{members}) { - foreach my $member_ref (keys %{$$self{members}}) { - my $member = $$self{members}{$member_ref}{object}; - if ($member->isa('Light_Item')) { - # if they are Light_Items, then set them to manual to avoid automation - # while manually setting light parameters - $member->manual(1,120,120); # 120 seconds should be enough - } - } - } - $self->interface()->initiate_linking_as_controller($p_group, $success_callback, $failure_callback); -} - -=item C - -Returns a list of objects that are members of device. If type is specified, only -members of that type are returned. Type is a package name, for example: - - $member->find_members('Insteon::BaseDevice'); - -=cut - -sub find_members -{ - my ($self,$p_type) = @_; - - my @l_found; - if ($$self{members}) - { - foreach my $member_ref (keys %{$$self{members}}) - { - my $member = $$self{members}{$member_ref}{object}; - if ($member->isa($p_type)) - { - push @l_found, $member; - } - } - } - return @l_found; - -} - -=item C - -Returns true if object is a member of device, else returns false. - -=cut - -sub has_member -{ - my ($self, $compare_object) = @_; - foreach my $member_ref (keys %{$$self{members}}) - { - my $member = $$self{members}{$member_ref}{object}; - if ($member eq $compare_object) - { - return 1; - } - } - return 0; -} - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -#################################### -### ##################### -### DeviceController ############### -### ############### -#################################### - -=head1 B - -=head2 DESCRIPTION - -Generic class implementation of an Device Controller. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::DeviceController; - -use strict; - -@Insteon::DeviceController::ISA = ('Insteon::BaseController'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface,$p_devcat) = @_; - - # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller - my $self = new Insteon::BaseController($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - -=item C - -Requests the current status of the device and calls C on the response. -This will trigger tied_events. - -=cut - -sub request_status -{ - my ($self,$requestor) = @_; -# if ($self->group ne '01') { - if ($$self{members} and !($self->isa('Insteon::InterfaceController')) - and (!(ref $requestor) or ($requestor eq $self))) { - &::print_log("[Insteon::DeviceController] requesting status for members of " . $$self{object_name}); - foreach my $member (keys %{$$self{members}}) { - next unless $member->isa('Insteon::BaseObject'); - my $member_obj = $$self{members}{$member}{object}; - next if $requestor eq $member_obj; - if ($member_obj->isa('Insteon::BaseDevice')) { - &::print_log("[Insteon::DeviceController] checking status of " . $member_obj->get_object_name() - . " for requestor " . $requestor->get_object_name()); - $member_obj->request_status($self); - } - } - } - # the following has bad assumptions in that we don't always know if a device is a responder - # since it could be a slave - if ($self->is_root && $self->is_responder) { - $self->Insteon::BaseDevice::request_status($requestor); - } -} - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -#################################### -### ############ -### InterfaceController ############ -### ############ -#################################### - -=head1 B - -=head2 DESCRIPTION - -Generic class implementation of an Interface Controller. These are the PLM Scenes. - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::InterfaceController; - -use strict; - -@Insteon::InterfaceController::ISA = ('Insteon::BaseController','Insteon::BaseObject'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller - my $self = new Insteon::BaseObject($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - - -#Otherwise BaseController will call Generic_Item::Set -sub set -{ - my ($self,$p_state,$p_setby,$p_response) = @_; - $self->Insteon::BaseObject::set($p_state,$p_setby,$p_response); -} - -sub is_root -{ - return 0; -} - -=item C - -Returns the root object of a device, in this case the interface. - -=cut - -sub get_root { - my ($self) = @_; - return $self->interface; -} - -# For IFaceControllers, need to call set_linked_devices -sub is_acknowledged { - my ($self, $p_ack) = @_; - if ($p_ack) { - $self->set_linked_devices($$self{pending_state}) if defined $$self{pending_state}; - } - return $self->Insteon::BaseObject::is_acknowledged($p_ack); -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my $group = $self->group; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds}, - 'on' => "$object_name->set(\"on\")", - 'off' => "$object_name->set(\"off\")", - 'initiate linking as controller' => "$object_name->initiate_linking_as_controller(\"$group\")", - 'cancel linking' => "$object_name->interface()->cancel_linking" - ); - return \%voice_cmds; -} - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -1; +=head1 B + +=head2 SYNOPSIS + +Usage + +In user code: + + $ip_patio_light = new Insteon_Device($myPLM,"33.44.55"); + $ip_patio_light->set("ON"); + +=head2 DESCRIPTION + +Generic class implementation of an Insteon Device. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::BaseObject; + +use strict; +use Insteon::AllLinkDatabase; + +@Insteon::BaseObject::ISA = ('Generic_Item'); + +our %message_types = ( + on => 0x11, + off => 0x13 +); + +our %nack_messages = ( + fb => 'illegal_value_in_cmd', + fc => 'pre_nak_long_db_search', + fd => 'bad_checksum_or_unknown_cmd', + fe => 'load_sense_detects_no_load', + ff => 'sender_id_not_in_responder_aldb', +); + +=item C + +Takes the various states available to insteon devices and returns a derived +state of either ON or OFF. + +=cut + +sub derive_link_state +{ + my ($self, $p_state) = @_; + $p_state = $self if !(ref $self); #Old code made direct calls + #Convert Relative State to Absolute State + if ($p_state =~ /^([+-])(\d+)/) { + my $rel_state = $1 . $2; + my $curr_state = '100'; + $curr_state = '0' if ($self->state eq 'off'); + $curr_state = $1 if $self->state =~ /(\d{1,3})/; + $p_state = $curr_state + $rel_state; + $p_state = 'on' if ($p_state > 0); + $p_state = 'off' if ($p_state <= 0); + } + + my $link_state = 'on'; + if ($p_state eq 'off' or $p_state eq 'off_fast') + { + $link_state = 'off'; + } + elsif ($p_state =~ /\d+%?/) + { + my ($dim_state) = $p_state =~ /(\d+)%?/; + $link_state = 'off' if $dim_state == 0; + } + + return $link_state; +} + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + my $self={}; + bless $self,$class; + + $$self{message_types} = \%message_types; + + if (defined $p_deviceid) { + my ($deviceid, $group) = $p_deviceid =~ /(\w\w\.\w\w\.\w\w):?(.+)?/; + # if a group is passed in, then assume it can be a controller + $$self{is_controller} = ($group) ? 1 : 0; + $self->device_id($deviceid); + $group = '01' unless $group; + $group = '0' . $group if length($group) == 1; + $self->group(uc $group); + } + + if ($p_interface) { + $self->interface($p_interface); + } else { + $self->interface(&Insteon::active_interface()); + } + + $self->restore_data('default_hop_count', 'engine_version'); + + $self->initialize(); + $$self{max_hops} = 3; + $$self{min_hops} = 0; + $$self{level} = undef; + $$self{flag} = "0F"; + $$self{ackMode} = "1"; + $$self{awaiting_ack} = 0; + $$self{is_acknowledged} = 0; + $$self{max_queue_time} = $::config_parms{'Insteon_PLM_max_queue_time'}; + $$self{max_queue_time} = 10 unless $$self{max_queue_time}; # 10 seconds is max time allowed in command stack + @{$$self{command_stack}} = (); + $$self{_onlevel} = undef; + $$self{is_responder} = 1; + $$self{default_hop_count} = 0; + $$self{timeout_factor} = 1.0; + $$self{is_deaf} = 0; + + &Insteon::add($self); + return $self; +} + +=item C + +A former holdover from when MisterHouse used to ping devices to get their device +category. May not be needed anymore. + +=cut + +sub initialize +{ + my ($self) = @_; + $$self{m_write} = 1; + $$self{m_is_locally_set} = 0; + # persist local, simple attribs +} + +=item C + +Used to store and return the associated interface of a device. + +If provided, stores interface as the device's interface. + +=cut + +sub interface +{ + my ($self,$p_interface) = @_; + if (defined $p_interface) { + $$self{interface} = $p_interface; + } + elsif (!($$self{interface})) + { + $$self{interface} = &Insteon::active_interface; + } + return $$self{interface}; +} + +=item C + +Used to store and return the associated device_id of a device. + +If provided, stores id as the device's id. + +Returns device id without any delimiters. + +=cut + +sub device_id +{ + my ($self,$p_device_id) = @_; + + if (defined $p_device_id) + { + $p_device_id =~ /(\w\w)\W?(\w\w)\W?(\w\w)/; + $$self{device_id}=$1 . $2 . $3; + } + return $$self{device_id}; +} + +=item C + +Used to store and return the associated group of a device. + +If provided, stores group as the device group. + +=cut + +sub group +{ + my ($self, $p_group) = @_; + $$self{m_group} = $p_group if $p_group; + return $$self{m_group}; +} + +=item C + +Changes the amount of time MH will wait to receive a response from a device before +resending the message. The value set will be multiplied by the predefined value +in MH. $float can be set to any positive decimal number. For example using 1.0 +will not change the preset values; 1.1 will increase the time MH waits by 10%; and +0.9 will force MH to wait for only 90% of the predefined time. + +This value is NOT saved on reboot, as such likely should be called in a $Reload loop. + +=cut + +sub timeout_factor { + my ($self, $factor) = @_; + $$self{timeout_factor} = $factor if $factor; + return $$self{timeout_factor}; +} + +=item C + +Sets the maximum number of hops that may be used in a message sent to the device. +The default and maximum number is 3. $int is an integer between 0-3. + +This value is NOT saved on reboot, as such likely should be called in a $Reload loop. + +=cut + +sub max_hops { + my ($self, $hops) = @_; + $$self{max_hops} = $hops if $hops; + return $$self{max_hops}; +} + +=item C + +Sets the minimum number of hops that may be used in a message sent to the device. +The default and minimum number is 0. $int is an integer between 0-3. + +This value is NOT saved on reboot, as such likely should be called in a $Reload loop. + +=cut + +sub min_hops { + my ($self, $hops) = @_; + $$self{min_hops} = $hops if $hops; + return $$self{min_hops}; +} + +=item C + +Used to track the number of hops needed to reach a device. Will store the past +20 hop counts for the device. Hop counts are added based on the number of hops +needed for an incomming message to arrive. Additionally, any time a message is +resent, a hop count equal to the prior default_hop_count + 1 is added. + +If provided, stores hop as a new hop count for the device. If more than 20 hop +counts have been stored, will drop the oldest hop count until only 20 exist. + +Returns the highest hop count of the past 20 hop counts + +=cut + +sub default_hop_count +{ + my ($self, $hop_count) = @_; + if (defined($hop_count)){ + ::print_log("[Insteon::BaseObject] DEBUG3: Adding hop count of " . $hop_count . " to hop_array of " + . $self->get_object_name) if $self->debuglevel(3, 'insteon'); + if (!defined(@{$$self{hop_array}})) { + unshift(@{$$self{hop_array}}, $$self{default_hop_count}); + $$self{hop_sum} = $$self{default_hop_count}; + } + #Calculate a simple moving average + unshift(@{$$self{hop_array}}, $hop_count); + $$self{hop_sum} += ${$$self{hop_array}}[0]; + $$self{hop_sum} -= pop(@{$$self{hop_array}}) if (scalar(@{$$self{hop_array}}) >10); + $$self{default_hop_count} = int(($$self{hop_sum} / scalar(@{$$self{hop_array}})) + 0.5); + + ::print_log("[Insteon::BaseObject] DEBUG4: ".$self->get_object_name + ."->default_hop_count()=".$$self{default_hop_count} + ." :: hop_array[]=". join("",@{$$self{hop_array}})) + if $self->debuglevel(4, 'insteon'); + } + + #Allow for per-device settings + $$self{default_hop_count} = $$self{max_hops} if ($$self{max_hops} && + $$self{default_hop_count} > $$self{max_hops}); + $$self{default_hop_count} = $$self{min_hops} if ($$self{min_hops} && + $$self{default_hop_count} < $$self{min_hops}); + + return $$self{default_hop_count}; +} + +=item C + +Used to store and return the associated engine version of a device. + +If provided, stores the engine version of the device. + +=cut + +sub engine_version +{ + my ($self, $p_engine_version) = @_; + $$self{engine_version} = $p_engine_version if $p_engine_version; + return $$self{engine_version}; +} + +=item C + +Returns 1 if object is the same as $self, otherwise returns 0. + +=cut + +sub equals +{ + my ($self, $compare_object) = @_; + # make sure that the compare_object is legitimate + return 0 unless $compare_object && ref $compare_object && $compare_object->isa('Insteon::BaseObject'); + return 1 if $compare_object eq $self; + # self and compare_object need to have device_ids and groups to be equal + return 0 unless $self->device_id && $self->group && $compare_object->device_id && $compare_object->group; + return 1 if (($compare_object->device_id eq $self->device_id) + && ($compare_object->group eq $self->group)); + # default to false; + return 0; +} + +=item C + +Used to set the device state. If called by device or a device linked to device, +calls C. If called by something +else, will send the command to the device. + +=cut + +sub set +{ + my ($self,$p_state,$p_setby,$p_response) = @_; + return if &main::check_for_tied_filters($self, $p_state); + + # Override any set_with_timer requests + if ($$self{set_timer}) { + &Timer::unset($$self{set_timer}); + delete $$self{set_timer}; + } + + if ($self->_is_valid_state($p_state)) { + # always reset the is_locally_set property unless set_by is the device + $$self{m_is_locally_set} = 0 unless ref $p_setby and $p_setby eq $self; + + if ($p_state eq 'toggle') + { + $p_state = ($self->state eq 'on')? 'off' : 'on'; + } + + my $setby_name = $p_setby; + $setby_name = $p_setby->get_object_name() if (ref $p_setby and $p_setby->can('get_object_name')); + if (ref $p_setby and $p_setby eq $self) + { #If set by device, update MH state, + my $derived_state = $self->derive_link_state($p_state); + &::print_log("[Insteon::BaseObject] " . $self->get_object_name() + . "::set_receive($derived_state, $setby_name)") if $self->debuglevel(1, 'insteon'); + $self->set_receive($derived_state,$p_setby,$p_response); + $self->set_linked_devices($p_state); + } + elsif (ref $p_setby and $p_setby eq $self->interface) + { #If set by interface, this was a manual status_request response + &::print_log("[Insteon::BaseObject] " . $self->get_object_name() + . "::set_receive($p_state, $setby_name)") if $self->debuglevel(1, 'insteon'); + $self->set_receive($p_state,$p_setby,$p_response); + } + else { # Not called by device, send set command + if ($self->is_responder){ + my $message = $self->derive_message($p_state); + $self->_send_cmd($message); + &::print_log("[Insteon::BaseObject] " . $self->get_object_name() . "::set($p_state, $setby_name)") + if $self->debuglevel(1, 'insteon'); + $self->is_acknowledged(0); + $$self{pending_state} = $p_state; + $$self{pending_setby} = $p_setby; + $$self{pending_response} = $p_response; + } + else { + ::print_log("[Insteon::BaseObject] " . $self->get_object_name() + . " is not a responder and cannot be set to a state."); + } + } + $self->level($p_state) if $self->can('level'); # update the level value + } else { + &::print_log("[Insteon::BaseObject] failed state validation with state=$p_state"); + } +} + +=item C + +If ack is true, calls C. If ack is false, clears awaiting_ack flag. + +=cut + +sub is_acknowledged +{ + my ($self, $p_ack) = @_; + if (defined $p_ack) + { + if ($p_ack) + { + $self->set_receive($$self{pending_state},$$self{pending_setby}, $$self{pending_response}) if defined $$self{pending_state}; + } + else + { + # if we are not acknowledged, then clear the awaiting acknowledgement flag + # we won't do the converse as it is set in _process_command_stack + $$self{awaiting_ack} = 0; + } + $$self{is_acknowledged} = $p_ack; + $$self{pending_state} = undef; + $$self{pending_setby} = undef; + $$self{pending_response} = undef; + } + return $$self{is_acknowledged}; +} + +=item C + +Updates the device state in MisterHouse. Triggers state_now, state_changed, and +state_final variables to update accordingly. Which causes tie_events to occur. + +If state was set to the same state within the last 1 second, then this is ignored. +This prevents the accidental calling of state_now if duplicate messages are received. + +=cut + +sub set_receive +{ + my ($self, $p_state, $p_setby, $p_response) = @_; + my $curr_milli = sprintf('%.0f', &main::get_tickcount); + my $window = 1000; + $p_state = $self->derive_link_state($p_state); + if (($p_state eq $self->state || $p_state eq $self->state_final) + && ($curr_milli - $$self{set_milliseconds} < $window)){ + ::print_log("[Insteon::BaseObject] Ignoring duplicate set " . $p_state . + " state command for " . $self->get_object_name . " received in " . + "less than $window milliseconds") if $self->debuglevel(1, 'insteon'); + } else { + $$self{set_milliseconds} = $curr_milli; + $self->level($p_state) if $self->can('level'); # update the level value + $self->SUPER::set($p_state, $p_setby, $p_response); + } +} + +=item C + +NOTE - This routine appears to be nearly identical, if not identical to the +C routine, it is not clear why this routine is +needed here. + +See full description of this routine in C + +=cut + +sub set_with_timer { + my ($self, $state, $time, $return_state, $additional_return_states) = @_; + return if &main::check_for_tied_filters($self, $state); + + $self->set($state) unless $state eq ''; + + return unless $time; + + my $state_change = ($state eq 'off') ? 'on' : 'off'; + $state_change = $return_state if defined $return_state; + $state_change = $self->{state} if $return_state and lc $return_state eq 'previous'; + + $state_change .= ';' . $additional_return_states if $additional_return_states; + + $$self{set_timer} = &Timer::new() unless $$self{set_timer}; + my $object_name = $self->{object_name}; + my $action = "$object_name->set('$state_change')"; + $$self{set_timer}->set($time, $action); +} + +sub _send_cmd +{ + my ($self, $message) = @_; +# $msg{type} = 'standard' unless $msg{type}; + +# my $message = $self->derive_message($msg{command},$msg{type},$msg{extra}); + + if ($message->command eq 'peek' + or $message->command eq 'poke' + or $message->command eq 'status_request' + or $message->command eq 'do_read_ee' + or $message->command eq 'set_address_msb' + or $message->command eq 'read_write_aldb' + ) + { + push(@{$$self{command_stack}}, $message); + } + else + { + unshift(@{$$self{command_stack}},$message); + } + $self->_process_command_stack(); +} + +=item C + +Generates and returns a basic on/off message from a command. + +=cut + +sub derive_message +{ + my ($self, $p_command, $p_extra) = @_; + my @args; + my $level; + + #msg id + my ($command, $subcommand) = split(/:/, $p_command, 2); + $command=lc($command); +# &::print_log("XLATE:$msg:$substate:$p_state:"); + + my $message; + + if ($self->isa("Insteon::BaseController")) + { + # only send out as all-link if the link originates from the plm + if ($self->isa("Insteon::InterfaceController")) + { # return the size of the command stack + $message = new Insteon::InsteonMessage('all_link_send', $self); + } + elsif ($self->is_root) + { # return the size of the command stack + $message = new Insteon::InsteonMessage('insteon_send', $self); + } else { + # silently ignore as this is now permitted if via "surrogate" + } + } elsif ($self->isa("Insteon::BaseObject")) { + $message = new Insteon::InsteonMessage('insteon_send', $self); + } + + if (!(defined $p_extra)) { + if ($command eq 'on') + { + if ($self->can('local_onlevel') && defined $self->local_onlevel) { + $level = 2.55 * $self->local_onlevel; + $command = 'on_fast'; + } else { + $level=255; + } + } elsif ($command eq 'off') + { + $level = 0; + } elsif ($command=~/^([1]?[0-9]?[0-9])/) + { + if ($1 < 1) { + $command='off'; + $level = 0; + } else { + $level = ($self->isa('Insteon::DimmableLight')) ? $1 * 2.55 : 255; + $command='on'; + } + } + } + + # confirm that the resulting $msg is legitimate + if (!(defined($self->message_type_code($command)))) { + &::print_log("[Insteon::BaseInsteon] invalid state=$command") if $self->debuglevel(1, 'insteon'); + return undef; + } + + if ($self->isa("Insteon::InterfaceController")) + { + $message->extra('00') #All PLM Scenes are Cmd2=00; + } elsif ($p_extra) + { $message->extra($p_extra); + + } elsif ($subcommand) { + $message->extra($subcommand); + } else { + if ($command eq 'on') + { + $message->extra(sprintf("%02X",int($level+.5))); + } else { + $message->extra('00'); + } + } + + $message->command($command); + return $message; +} + +=item C + +Takes msg, a text based msg type, and returns the corresponding text version of +the hexadecimal message type. + +=cut + +sub message_type_code +{ + my ($self, $msg) = @_; + return $$self{message_types}->{$msg}; +} + +=item C + +Takes msg, a text based msg type, and returns the corresponding message type as +a hex. + +=cut + +sub message_type_hex +{ + my ($self, $msg) = @_; + return unpack( 'H*', pack( 'c', $self->message_type_code($msg))); +} + +=item C + +Takes cmd, text based hexadecimal code, and returns the text based description +of the message code. + +=cut + +sub message_type +{ + my ($self, $cmd1) = @_; + my $msg_type; + my $msg_type_ptr = $$self{message_types}; + my %msg_types = %$msg_type_ptr; + for my $key (keys %msg_types){ + if (pack("C",$msg_types{$key}) eq pack("H*",$cmd1)) + { +# &::print_log("[Insteon::BaseObject] found: $key"); + $msg_type=$key; + last; + } + } + return $msg_type; +} + +sub _is_info_request +{ + my ($self, $cmd, $ack_setby, %msg) = @_; + my $is_info_request = 0; + if ($cmd eq 'status_request') { + $is_info_request++; + my $ack_on_level = sprintf("%d", int((hex($msg{extra}) * 100 / 255)+.5)); + &::print_log("[Insteon::BaseObject] received status for " . + $self->{object_name} . " with on-level: $ack_on_level%, " + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); + $self->level($ack_on_level) if $self->can('level'); # update the level value + if ($ack_on_level == 0) { + $self->set('off', $ack_setby); + } elsif ($ack_on_level > 0 and !($self->isa('Insteon::DimmableLight'))) { + $self->set('on', $ack_setby); + } else { + $self->set($ack_on_level . '%', $ack_setby); + } + # if this were a scene controller, then also propogate the result to all members + my $callback; + if ($self->_aldb->{aldb_delta_action} eq 'set'){ + if ($msg{cmd_code} eq "00") { + $self->_aldb->{_mem_activity} = 'delete'; + $self->_aldb->{pending_aldb}{address} = $self->_aldb->get_first_empty_address(); + if($self->_aldb->isa('Insteon::ALDB_i1')) { + $self->_aldb->_peek($self->_aldb->{pending_aldb}{address},0); + } else { + $self->_aldb->_write_delete($self->_aldb->{pending_aldb}{address}); + } + } else { + $self->_aldb->aldb_delta($msg{cmd_code}); + $self->_aldb->scandatetime(&main::get_tickcount); + &::print_log("[Insteon::BaseObject] The Link Table Version for " + . $self->{object_name} . " has been updated to version number " . $self->_aldb->aldb_delta()); + if (defined $self->_aldb->{_success_callback}) { + $callback = $self->_aldb->{_success_callback}; + $self->_aldb->{_success_callback} = undef; + } + } + } + elsif ($self->_aldb->{aldb_delta_action} eq 'check') + { + if ($self->_aldb->aldb_delta() eq $msg{cmd_code}){ + &::print_log("[Insteon::BaseObject] The link table for " + . $self->{object_name} . " is in sync."); + #Link Table Scan Successful, Record Current Time + $self->_aldb->scandatetime(&main::get_tickcount); + if (defined $self->_aldb->{_aldb_unchanged_callback}) { + $callback = $self->_aldb->{_aldb_unchanged_callback}; + $self->_aldb->{_aldb_unchanged_callback} = undef; + } + } else { + &::print_log("[Insteon::BaseObject] WARN The link table for " + . $self->{object_name} . " is out-of-sync."); + $self->_aldb->health('out-of-sync'); + if (defined $self->_aldb->{_aldb_changed_callback}) { + $callback = $self->_aldb->{_aldb_changed_callback}; + $self->_aldb->{_aldb_changed_callback} = undef; + } + } + } + $self->_aldb->{aldb_delta_action} = undef; + $self->_aldb->health('out-of-sync') if($self->_aldb->aldb_delta() ne $msg{cmd_code}); + if ($callback){ + package main; + eval ($callback); + &::print_log("[Insteon::BaseObject] " . $self->get_object_name . ": error during scan callback $@") + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon::BaseObject; + } + } + elsif ( $cmd eq 'get_engine_version' ) { + $is_info_request++; + my @engine_types = (qw/I1 I2 I2CS/); + my $version = $engine_types[$msg{extra}]; + $self->engine_version($version); + &::print_log("[Insteon::BaseObject] received engine version for " + . $self->{object_name} . " of $version. " + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); + } + return $is_info_request; +} + +sub _process_message +{ + my ($self,$p_setby,%msg) = @_; + my $p_state = undef; + + # the current approach assumes that links from other controllers to some responder + # would be seen by the plm by also direct linking the controller as a responder + # and not putting the plm into monitor mode. This means that updating the state + # of the responder based upon the link controller's request is handled + # by Insteon_Link. + + main::print_log("[Insteon::BaseObject] WARN: Message has invalid checksum") + if ($self->debuglevel(1, 'insteon') && !($msg{crc_valid}) + && $msg{is_extended} && $self->engine_version() eq 'I2CS'); + + my $clear_message = 0; + $$self{m_is_locally_set} = 1 if $msg{source} eq lc $self->device_id; + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}) if (!$self->isa('Insteon::InterfaceController')); + if ($msg{is_ack}) { + #Default to clearing message transaction for ACK + $clear_message = 1; + my $corrupt_cmd = 0; + my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; + if ($$self{awaiting_ack}) + { + my $ack_setby = (ref $$self{m_status_request_pending}) + ? $$self{m_status_request_pending} : $p_setby; + if ($self->_is_info_request($pending_cmd,$ack_setby,%msg)) + { + $self->is_acknowledged(1); + $$self{m_status_request_pending} = 0; + $self->_process_command_stack(%msg); + } + elsif ($pending_cmd eq 'peek') + { + if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { + $self->_aldb->_on_peek(%msg) if $self->_aldb; + $self->_process_command_stack(%msg); + } else { + $corrupt_cmd = 1; + $clear_message = 0; + } + } + elsif ($pending_cmd eq 'set_address_msb') + { + if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { + $self->_aldb->_on_peek(%msg) if $self->_aldb; + $self->_process_command_stack(%msg); + } else { + $corrupt_cmd = 1; + $clear_message = 0; + } + } + elsif (($pending_cmd eq 'poke')) + { + if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { + $self->_aldb->_on_poke(%msg) if $self->_aldb; + $self->_process_command_stack(%msg); + } else { + $corrupt_cmd = 1; + $clear_message = 0; + } + } + elsif ($pending_cmd eq 'read_write_aldb') { + if ($msg{cmd_code} eq $self->message_type_hex($pending_cmd)) { + $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; + $self->_process_command_stack(%msg) if ($clear_message); + } else { + $corrupt_cmd = 1; + $clear_message = 0; + } + } + elsif ($pending_cmd eq 'ping'){ + $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); + $corrupt_cmd = 1 if ($msg{extra} ne sprintf ("%02d", $$self{ping_count})); + if (!$corrupt_cmd){ + $self->_process_command_stack(%msg); + &::print_log("[Insteon::BaseObject] received ping acknowledgement from " . $self->{object_name}) + if $self->debuglevel(1, 'insteon'); + $self->ping(); + $clear_message = 1; + } + } + elsif ($pending_cmd eq 'linking_mode'){ + $corrupt_cmd = 1 if ($msg{cmd_code} ne $self->message_type_hex($pending_cmd)); + if (!$corrupt_cmd){ + &::print_log("[Insteon::BaseObject] received linking mode ACK from " . $self->{object_name}) + if $self->debuglevel(1, 'insteon'); + $self->interface->_set_timeout('xmit', 2000); + $clear_message = 0; + } + } + else + { + if (($pending_cmd eq 'do_read_ee') && + ($self->_aldb->health eq "good" || $self->_aldb->health eq "empty") && + ($self->isa('Insteon::KeyPadLincRelay') || $self->isa('Insteon::KeyPadLinc'))){ + ## Update_Flags ends up here, set aldb_delta to new value + $self->_aldb->query_aldb_delta("set"); + } + $self->is_acknowledged(1); + # signal receipt of message to the command stack in case commands are queued + $self->_process_command_stack(%msg); + &::print_log("[Insteon::BaseObject] received command/state (awaiting) acknowledge from " . $self->{object_name} + . ": $pending_cmd and data: $msg{extra}") if $self->debuglevel(1, 'insteon'); + } + } + else + { + # allow non-synchronous messages to also use the _is_info_request hook + $self->_is_info_request($pending_cmd,$p_setby,%msg); + $self->is_acknowledged(1); + # signal receipt of message to the command stack in case commands are queued + $self->_process_command_stack(%msg); + &::print_log("[Insteon::BaseObject] received command/state acknowledge from " . $self->{object_name} + . ": " . (($msg{command}) ? $msg{command} : "(unknown)") + . " and data: $msg{extra}") if $self->debuglevel(1, 'insteon'); + } + if ($corrupt_cmd) { + main::print_log("[Insteon::BaseObject] WARN: received a message from " + . $self->get_object_name . " in response to a " + . $pending_cmd . " command, but the command code " + . $msg{cmd_code} . " is incorrect. Ignorring received message."); + $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); + $p_setby->active_message->no_hop_increase(1); + } + } + elsif ($msg{is_nack}) + { + #Default to clearing message transaction for NAK + $clear_message = 1; + if ($self->isa('Insteon::BaseLight')) { + &::print_log("[Insteon::BaseObject] WARN!! encountered a nack message (" + . $self->get_nack_msg_for( $msg{extra} ) .") for " . $self->{object_name} + . ". It may be unplugged, have a burned out bulb, or this may be a new I2CS " + . "type device that must first be manually linked to the PLM using the set button.") + if $self->debuglevel(1, 'insteon'); + } + else + { + &::print_log("[Insteon::BaseObject] WARN!! encountered a nack message (" + . $self->get_nack_msg_for( $msg{extra} ) .") for " . $self->{object_name} + . " ... skipping"); + } + $p_setby->active_message->no_hop_increase(1); + $self->is_acknowledged(0); + $self->_process_command_stack(%msg); + if($p_setby->active_message->failure_callback) + { + main::print_log("[Insteon::BaseObject] WARN: Now calling message failure callback: " + . $p_setby->active_message->failure_callback) if $self->debuglevel(1, 'insteon'); + $self->failure_reason('NAK'); + package main; + eval $p_setby->active_message->failure_callback; + main::print_log("[Insteon::BaseObject] problem w/ retry callback: $@") if $@; + package Insteon::BaseObject; + } + $p_setby->active_message->no_hop_increase(1); + $self->is_acknowledged(0); + $self->_process_command_stack(%msg); + } + elsif ($msg{command} eq 'start_manual_change') + { + $$self{manual_direction} = $msg{extra}; + $$self{manual_start} = ::get_tickcount() - (($msg{maxhops}-$msg{hopsleft})*50); + } elsif ($msg{command} eq 'stop_manual_change') { + # Determine percent change based on time interval + my $finish_time = &main::get_tickcount - (($msg{maxhops}-$msg{hopsleft})*50); + my $total_time = $finish_time - $$self{manual_start}; + my $percent_change = int($total_time / 42); + if ($$self{manual_direction} eq '00') { + $percent_change = "-".$percent_change; + } else { + $percent_change = "+".$percent_change; + } + $self->set($percent_change, $self); + } elsif ($msg{command} eq 'read_write_aldb') { + if ($self->_aldb){ + $clear_message = $self->_aldb->on_read_write_aldb(%msg) if $self->_aldb; + $self->_process_command_stack(%msg) if($clear_message); + } + } elsif ($msg{type} eq 'broadcast') { + $self->devcat($msg{devcat}); + $self->firmware($msg{firmware}); + &::print_log("[Insteon::BaseObject] device category: $msg{devcat}" + . " firmware: $msg{firmware} received for " . $self->{object_name}); + } else { + ## TO-DO: make sure that the state passed by command is something that is reasonable to set + $p_state = $msg{command}; + if ($msg{type} eq 'alllink') + { + if ($msg{command} eq 'link_cleanup_report'){ + if ($msg{extra} == 0){ + ::print_log("[Insteon::BaseObject] DEBUG Received AllLink Cleanup Success for " + . $self->{object_name}) if $self->debuglevel(1, 'insteon'); + } else { + ::print_log("[Insteon::BaseObject] WARN " . $msg{extra} . " Device(s) failed to " + . "acknowledge the command from " . $self->{object_name}); + } + } else { + $self->set($p_state, $self); + $$self{_pending_cleanup} = 1; + #Wait to avoid clobbering incomming subsequent cleanup messages + my @links = $self->find_members('Insteon::BaseDevice'); + #Timeout is the number of linked devices multiplied by + #the maximum hop length for a direct message and an ACK + #PLM is the +1 + my $timeout = (scalar(@links)+1) * 300; + ::print_log("[Insteon::BaseObject] DEBUG3 Delaying any outgoing messages ". + "by $timeout milliseconds to avoid collision with subsequent cleanup ". + "messages from " . $self->get_object_name) if ($self->debuglevel(3, 'insteon')); + $self->interface->_set_timeout('xmit', $timeout); + } + } + elsif ($msg{type} eq 'cleanup') + { + if (($self->state eq $p_state or $self->state_final eq $p_state) + and $$self{_pending_cleanup}){ + ::print_log("[Insteon::BaseObject] Ignoring Received Direct AllLink Cleanup Message for " + . $self->{object_name} . " since AllLink Broadcast Message was Received.") if $self->debuglevel(1, 'insteon'); + } else { + $self->set($p_state, $self); + } + $$self{_pending_cleanup} = 0; + } else { + main::print_log("[Insteon::BaseObject] Ignoring unsupported command from " + . $self->{object_name}) if $self->debuglevel(1, 'insteon'); + $self->corrupt_count_log(1) if $self->can('corrupt_count_log'); + } + } + return $clear_message; +} + +sub _process_command_stack +{ + my ($self, %ackmsg) = @_; + if (%ackmsg) { # which may also be something that can be interpretted as a "nack" + # determine whether to unset awaiting_ack + # for now, be "dumb" and just unset it + $$self{awaiting_ack} = 0; + } + if (!($$self{awaiting_ack})) { + my $callback = undef; + my $message = pop(@{$$self{command_stack}}); + # convert ptr to cmd hash + if ($message) + { + my $plm_queue_size = $self->interface->queue_message($message); + + # send msg + if ($message->command eq 'peek' + or $message->command eq 'poke' + or $message->command eq 'status_request' + or $message->command eq 'get_engine_version' + or $message->command eq 'id_request' + or $message->command eq 'do_read_ee' + or $message->command eq 'set_address_msb' + or $message->command eq 'sensor_status' + or $message->command eq 'set_operating_flags' + or $message->command eq 'get_operating_flags' + or $message->command eq 'read_write_aldb' + or $message->command eq 'thermostat_control' + or $message->command eq 'thermostat_get_zone_info' + or $message->command eq 'extended_set_get' + or $message->command eq 'ping' + or $message->command eq 'linking_mode' + ) + { + $$self{awaiting_ack} = 1; + } + else + { + $$self{awaiting_ack} = 0; + } + + $$self{_prior_msg} = $message; + # TO-DO: adjust timer based upon (1) type of message and (2) retry_count + my $queue_time = $$self{max_queue_time} + $plm_queue_size; + # if is_synchronous, then no other command can be sent until an insteon ack or nack is received + # for this command + } else { + # and, always clear awaiting_ack and _prior_msg + $$self{awaiting_ack} = 0; + $$self{_prior_msg} = undef; + } + if ($callback) { + package main; + eval ($callback); + &::print_log("[Insteon::BaseObject] error in queue timer callback: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon::BaseObject; + } + } else { +# &::print_log("[Insteon_Device] " . $self->get_object_name . " command queued but not yet sent; awaiting ack from prior command") if $self->debuglevel(1, 'insteon'); + } +} + +sub _is_valid_state +{ + my ($self,$state) = @_; + if (!(defined($state)) or $state eq '') { + return 0; + } + + my ($msg, $substate) = split(/:/, $state, 2); + $msg=lc($msg); + + if ($msg=~/^[+-]?([1]?[0-9]?[0-9])/) + { + if ($1 < 1) { + $msg='off'; + } else { + $msg='on'; + } + } + elsif ($msg eq 'toggle') + { + if ($self->state eq 'on') + { + $msg = 'off'; + } + elsif ($self->state eq 'off') + { + $msg = 'on'; + } + } + + # confirm that the resulting $msg is legitimate + if (!(defined($$self{message_types}{$msg}))) { + return 0; + } else { + return 1; + } +} + +=item C + +Takes msg, text based hexadecimal code, and returns the text based description +of the NACK code. + +=cut + +sub get_nack_msg_for { + my ($self,$msg) = @_; + return $nack_messages{ $msg }; +} + +=item C + +Stores the resaon for the most recent message failure [NAK | timeout]. Used to +process message callbacks after a message fails. If called with no parameter +returns the saved failure reason. + +Parameters: + reason: failure reason + +Returns: failure reason + +=cut + +sub failure_reason +{ + my ($self, $reason) = @_; + $$self{failure_reason} = $reason if $reason; + return $$self{failure_reason}; +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my %voice_cmds = ( + #The Sync Links routine really resides in DeviceController, but that + #class seems a little redundant as in practice all devices are controllers + #in some sense. As a result, that class will likely be folded into + #BaseObject/Device at some future date. In order to avoid a bizarre + #inheritance of this routine by higher classes, this command was placed + #here + 'sync links' => $self->get_object_name . '->sync_links(0)' + ); + return \%voice_cmds; +} + +sub _aldb +{ + my ($self) = @_; + my $root_obj = $self->get_root(); + return $$root_obj{aldb}; +} + +=item C + +Returns true if the device must be awake in order to respond to messages. Most +devices are not deaf, currently devices that are deaf are battery operated +devices such as the Motion Sensor, RemoteLinc and TriggerLinc. + +At the BaseObject level all devices are defined as not deaf. Objects which +inherit BaseObject should redefine is_deaf as necessary. + +=cut + +sub is_deaf +{ + my ($self) = @_; + return $$self{is_deaf}; +} + +=item C + +Returns true if the device is a controller. + +=cut + +sub is_controller +{ + my ($self) = @_; + return $$self{is_controller}; +} + +=item C + +Stores and returns whether a device is a responder. + +=cut + +sub is_responder +{ + my ($self,$is_responder) = @_; + $$self{is_responder} = $is_responder if defined $is_responder; + if ($self->is_root || $self->isa('Insteon::InterfaceController')) { + return $$self{is_responder}; + } + else + { + my $root_obj = $self->get_root(); + if (ref $root_obj) + { + return $$root_obj{is_responder}; + } + else + { + return 0; + } + } +} + +=back + +=head2 INI PARAMETERS + +=over + +=item Insteon_PLM_max_queue_time + +Was previously used to set the maximum amount of time +a message could remain in the queue. This parameter is no longer used in the code +but it still appears in the initialization. It may be removed at a future date. +This also gets set in L +as well for some reason, but is not used there either. + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 NOTES + +Special thanks to: + Brian Warren for significant testing and patches + Bruce Winter - MH + +=head2 SEE ALSO + +=head2 LICENSE + +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. + +=cut + +#################################### +### ##################### +### BaseDevice ##################### +### ##################### +#################################### + +=head1 B + +=head2 DESCRIPTION + +Generic class implementation of a Base Insteon Device. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::BaseDevice; + +@Insteon::BaseDevice::ISA = ('Insteon::BaseObject'); + +our %message_types = ( + %Insteon::BaseObject::message_types, + assign_to_group => 0x01, + delete_from_group => 0x02, + link_cleanup_report => 0x06, + linking_mode => 0x09, + unlinking_mode => 0x0A, + get_engine_version => 0x0D, + ping => 0x0F, + id_request => 0x10, + on_fast => 0x12, + off_fast => 0x14, + start_manual_change => 0x17, + stop_manual_change => 0x18, + status_request => 0x19, + get_operating_flags => 0x1f, + set_operating_flags => 0x20, + do_read_ee => 0x24, + remote_set_button_tap => 0x25, + set_led_status => 0x27, + set_address_msb => 0x28, + poke => 0x29, + poke_extended => 0x2a, + peek => 0x2b, + peek_internal => 0x2c, + poke_internal => 0x2d, + extended_set_get => 0x2e, + read_write_aldb => 0x2f, +); + + +my %operating_flags = ( + 'program_lock_on' => '00', + 'program_lock_off' => '01', + 'led_on_during_tx' => '02', + 'led_off_during_tx' => '03', + 'resume_dim_on' => '04', + 'beeper_enabled' => '04', + 'resume_dim_off' => '05', + 'beeper_off' => '05', + 'eight_key_kpl' => '06', + 'load_sense_on' => '06', + 'six_key_kpl' => '07', + 'load_sense_off' => '07', + 'led_backlight_off' => '08', + 'led_off' => '08', + 'led_backlight_on' => '09', + 'led_enabled' => '09', + 'key_beep_enabled' => '0a', + 'one_minute_warn_disabled' => '0a', + 'key_beep_off' => '0b', + 'one_minute_warn_enabled' => '0b' +); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + my $self= new Insteon::BaseObject($p_deviceid,$p_interface); + bless $self,$class; + + $$self{message_types} = \%message_types; + $$self{operating_flags} = \%operating_flags; + + if ($self->group eq '01') { + $$self{aldb} = new Insteon::ALDB_i1($self); + } + + $self->restore_data('devcat', 'firmware', 'level', 'retry_count_log', 'fail_count_log', + 'outgoing_count_log', 'incoming_count_log', 'corrupt_count_log', + 'dupe_count_log', 'hops_left_count', 'max_hops_count', + 'outgoing_hop_count'); + + $self->initialize(); + $self->rate(undef); + $$self{level} = undef; + $$self{flag} = "0F"; + $$self{ackMode} = "1"; + $$self{awaiting_ack} = 0; + $$self{is_acknowledged} = 0; + $$self{max_queue_time} = $::config_parms{'Insteon_PLM_max_queue_time'}; + $$self{max_queue_time} = 10 unless $$self{max_queue_time}; # 10 seconds is max time allowed in command stack + @{$$self{command_stack}} = (); + $$self{_onlevel} = undef; + $$self{retry_count_log} = 0; + $$self{fail_count_log} = 0; + $$self{outgoing_count_log} = 0; + $$self{incoming_count_log} = 0; + $$self{corrupt_count_log} = 0; + $$self{dupe_count_log} = 0; + $$self{hops_left_count} = 0; + $$self{max_hops_count} = 0; + $$self{outgoing_hop_count} = 0; + + + return $self; +} + +=item C + +A former holdover from when MisterHouse used to ping devices to get their device +category. May not be needed anymore. + +=cut + +sub initialize +{ + my ($self) = @_; + $$self{m_write} = 1; + $$self{m_is_locally_set} = 0; + # persist local, simple attribs +} + +=item C + +Used to store and return the associated ramp rate of a device. + +If provided, stores rate as the device's ramp rate in MisterHouse. + +=cut + +sub rate +{ + my ($self,$p_rate) = @_; + $$self{rate} = $p_rate if defined $p_rate; + return $$self{rate}; +} + +=item C + +If a controller link from the device to the interface does not exist, this will +create that link on the device. + +Next, if a responder link from the device to the interface does not exist on the +interface, this will create that link on the interface. + +The group is the group on the device that is the controller, such as a button on +a keypad link. It will default to 01. + +Data3 is optional and is used to set the Data3 value in the controller link on +the device. + +=cut + +sub link_to_interface +{ + my ($self,$p_group, $p_data3, $step) = @_; + $p_group = $self->group unless (defined $p_group); + $p_data3 = $self->group unless (defined $p_data3); + my $success_callback_prefix = $self->get_object_name."->link_to_interface(\"$p_group\",\"$p_data3\","; + my $success_callback = ""; + my $failure_callback = '::print_log("[Insteon::BaseInsteon] Error: The Link_To_Interface '. + 'routine failed for device: '.$self->get_object_name.'")'; + $step = 0 if ($step eq ''); + if ($step == 0) { #If NAK on get_engine, then this is an I2CS device + $success_callback = $success_callback_prefix . "\"1\")"; + $failure_callback = $self->get_object_name."->link_to_interface_i2cs(\"$p_group\",\"$p_data3\")"; + $self->get_engine_version($success_callback, $failure_callback); + } + elsif ($step == 1) { #Add Link from object->PLM + $success_callback = $success_callback_prefix . "\"2\")"; + my %link_info = ( object => $self->interface, group => $p_group, is_controller => 1, + callback => "$success_callback", failure_callback=> "$failure_callback"); + $link_info{data3} = $p_data3 if $p_data3; + if ($self->_aldb) { + $self->_aldb->add_link(%link_info); + } + else + { + &main::print_log("[Insteon::BaseInsteon] Error: This item, " . $self->get_object_name . + ", does not have an ALDB object. Linking is not permitted."); + } + } + elsif ($step == 2){ #Add Link from PLM->object + $success_callback = $success_callback_prefix . "\"3\")"; + my $link_info = "deviceid=" . lc ($self->device_id) . " group=$p_group is_controller=0 " . + "callback=$success_callback failure_callback=$failure_callback"; + $self->interface->add_link($link_info); + } + elsif ($step == 3){ #Add surrogate link on device if surrogate exists + if (ref $$self{surrogate}){ + $success_callback = $success_callback_prefix . "\"4\")"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( object => $self->interface, + group => $surrogate_group, is_controller => 0, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $p_group); + $self->_aldb->add_link(%link_info); + } else { + ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. + ' for device ' .$self->get_object_name); + } + } + elsif ($step == 4){ #Add surrogate link on PLM if surrogate exists + $success_callback = $success_callback_prefix . "\"5\")"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( deviceid=> lc($self->device_id), + group => $surrogate_group, is_controller => 1, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $surrogate_group); + $self->interface->add_link(%link_info); + } + elsif ($step == 5){ + ::print_log('[Insteon::BaseInsteon] Link_To_Interface successfully completed'. + ' for device ' .$self->get_object_name); + } +} + +=item C + +Performs the same task as C however this routine is designed +to perform the initial link to I2CS devices. These devices cannot be initially +linked to the PLM in the normal way. This process requires more steps than the +normal routine which will take longer to perform and therefore is more prone to +faile. As such, this should likely only be used if necessary. + +=cut + +sub link_to_interface_i2cs +{ + my ($self,$p_group, $p_data3, $step) = @_; + my $success_callback_prefix = $self->get_object_name."->link_to_interface_i2cs('$p_group','$p_data3',"; + my $success_callback = ""; + my $failure_callback = "::print_log('[Insteon::BaseInsteon] Error Link_To_Interface_I2CS ". + "routine failed for device: ".$self->get_object_name."')"; + $step = 0 if ($step eq ''); + if ($step == 0) { #Put PLM into initiate linking mode + $success_callback = $success_callback_prefix . "'1')"; + $self->interface()->initiate_linking_as_controller('00', $success_callback, $failure_callback); + } + elsif ($step == 1) { #Ask device to respond to link request + $success_callback = $success_callback_prefix . "'2')"; + $self->enter_linking_mode($p_group, $success_callback, $failure_callback); + } + elsif ($step == 2) { #Scan device to get an accurate link table + #return to normal link_to_interface routine if successful + $success_callback_prefix = $self->get_object_name."->link_to_interface('$p_group','$p_data3',"; + $success_callback = $success_callback_prefix . "'1')"; + $self->scan_link_table($success_callback, $failure_callback); + } +} + +=item C + +Will delete the contoller link from the device to the interface if such a link exists. + +Next, will delete the responder link from the device to the interface on the +interface, if such a link exists. + +The group is the group on the device that is the controller, such as a button on +a keypad link. It will default to 01. + +=cut + +sub unlink_to_interface +{ + my ($self,$p_group,$step) = @_; + $p_group = $self->group unless $p_group; + #It is possible to nest all of the callbacks in at once, but the quoting + #becomes very complicated and happers readability + my $success_callback_prefix = $self->get_object_name."->unlink_to_interface('$p_group',"; + my $success_callback = ""; + my $failure_callback = "::print_log('[Insteon::BaseInsteon] ERROR: Unlink_To_Interface ". + "failed for device: ".$self->get_object_name."')"; + $step = 0 if ($step eq ''); + if ($step == 0) { #Delete link on the device + if ($self->_aldb) { + $success_callback = $success_callback_prefix . "'1')"; + $self->_aldb->delete_link(object => $self->interface, + group => $p_group, + data3=> $p_group, is_controller => 1, + callback => $success_callback, + failure_callback=> $failure_callback); + } + else + { + &main::print_log("[BaseInsteon] This item " . $self->get_object_name . + " does not have an ALDB object. Unlinking is not permitted."); + } + } + elsif ($step == 1) { #Delete link on the PLM + $success_callback = $success_callback_prefix . "'2')"; + $self->interface->delete_link( + deviceid => lc($self->device_id), + group=> $p_group, is_controller=>0, + callback=>$success_callback, + failure_callback=>$failure_callback); + } + elsif ($step == 2){ #Delete surrogate link on device if surrogate exists + if (ref $$self{surrogate}){ + $success_callback = $success_callback_prefix . "'3')"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( object => $self->interface, + group => $surrogate_group, is_controller => 0, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $p_group); + $self->_aldb->delete_link(%link_info); + } else { + ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". + " successfully completed for device " + . $self->get_object_name); + } + } + elsif ($step == 3){ #Delete surrogate link on PLM if surrogate exists + $success_callback = $success_callback_prefix . "'4')"; + my $surrogate_group = $$self{surrogate}->group; + my %link_info = ( deviceid=> lc($self->device_id), + group => $surrogate_group, is_controller => 1, + callback => "$success_callback", + failure_callback=> "$failure_callback", + data3 => $surrogate_group); + $self->interface->delete_link(%link_info); + } + elsif ($step == 4) { + ::print_log("[Insteon::BaseInsteon] Unlink_To_Interface". + " successfully completed for device " + . $self->get_object_name); + } +} + +=item C + +Places an i2 object into linking mode as if you had held down the set button on +the device. i1 objects will not respond to this command. This is needed to +link i2CS devices that will not respond without a manual link. + +This process is included as part of the link_to_interface voice command and +should not need to be called seperately. + +Returns: nothing + +=cut + +sub enter_linking_mode +{ + my ($self,$p_group, $success_callback, $failure_callback) = @_; + my $group = $p_group; + $group = '01' unless $group; + my $extra = sprintf("%02x", $group); + $extra .= '0' x (30 - length $extra); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'linking_mode', $extra); + $message->success_callback($success_callback); + $message->failure_callback($failure_callback); + $self->_send_cmd($message); +} + +=item C + +Sets the defined flag on the device. + +=cut + +sub set_operating_flag { + my ($self, $flag) = @_; + + if (!(exists($$self{operating_flags}{$flag}))) + { + &::print_log("[Insteon::BaseDevice] $flag is not a support operating flag"); + return; + } + + if ($self->is_root and !($self->isa('Insteon::InterfaceController'))) + { + my $message; + if (ref $self->_aldb && $self->_aldb->isa('Insteon::ALDB_i2')) + { + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'set_operating_flags'); + $message->extra($$self{operating_flags}{$flag} . "0000000000000000000000000000"); + } else { + $message = new Insteon::InsteonMessage('insteon_send', $self, 'set_operating_flags'); + $message->extra($$self{operating_flags}{$flag}); + } + $self->_send_cmd($message); + } + else + { + &::print_log("[Insteon::BaseDevice] " . $self->get_object_name . " is either not a root device or is a plm controlled scene"); + return; + } +} + +=item C + +Requests the device's operating flag and prints it to the log. + +=cut + +sub get_operating_flag { + my ($self) = @_; + + if ($self->is_root and !($self->isa('Insteon::InterfaceController'))) + { + # TO-DO: check devcat to determine if the action is supported by the device + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'get_operating_flags'); + $self->_send_cmd($message); +# $self->_send_cmd('command' => 'get_operating_flags'); + } + else + { + &::print_log("[Insteon::BaseDevice] " . $self->get_object_name . " is either not a root device or is a plm controlled scene"); + return; + } +} + +=item C + +Appears to set and or return the "writable" state. Does not appear to be used +in the Insteon code, but may be necessary for supporting other classes? + +=cut + +sub writable { + my ($self, $p_write) = @_; + if (defined $p_write) + { + if ($p_write =~ /r/i or $p_write =~/^0/) + { + $$self{m_write} = 0; + } + else + { + $$self{m_write} = 1; + } + } + return $$self{m_write}; +} + +=item C + +Returns the is_locally_set variable. Doesn't appear to be used in Insteon code. +Likely was used to test whether setby was equal to the device itself. May not +always be properly updated since it appears unused. + +=cut + +sub is_locally_set { + my ($self) = @_; + return $$self{m_is_locally_set}; +} + +=item C + +Returns true if the object is the base object, generally group 01, on the device. + +=cut + +sub is_root { + my ($self) = @_; + return (($self->group eq '01') and !($self->isa('Insteon::InterfaceController'))) ? 1 : 0; +} + +=item C + +Returns the root object of a device. + +=cut + +sub get_root { + my ($self) = @_; + if ($self->is_root) + { + return $self; + } + else + { + my $root_obj = &Insteon::get_object($self->device_id, '01'); + ::print_log ("[Insteon::BaseDevice] ERROR! Cannot find the root object for " + . $self->get_object_name . ". Please check your mht file to make sure " + . "that device id " . $self->device_id . ":01 is defined.") + if (!defined($root_obj)); + return $root_obj; + } +} + +=item C + +If a device has an ALDB, passes link_details onto one of the has_link() routines +within L. Generally called as part of C. + +=cut + +sub has_link +{ + my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + return $aldb->has_link($insteon_object, $group, $is_controller, $subaddress); + } + else + { + return 0; + } + +} + +=item C + +If a device has an ALDB, passes link_details onto one of the add_link() routines +within L. Generally called from the "sync links" or +"link to interface" voice commands. + +=cut + +sub add_link +{ + my ($self, $parms_text) = @_; + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + $aldb->add_link(%link_parms); + } + +} + +=item C + +If a device has an ALDB, passes link_details onto one of the update_link() routines +within L. Generally called from the "sync links" +voice command. + +=cut + +sub update_link +{ + my ($self, $parms_text) = @_; + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + $aldb->update_link(%link_parms); + } +} + +=item C + +If a device has an ALDB, passes link_details onto one of the delete_link() routines +within L. Generally called by C. + +=cut + +sub delete_link +{ + my ($self, $parms_text) = @_; + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + $aldb->delete_link(%link_parms); + } +} + +=item C + +Scans a device link table and caches a copy. + +=cut + +sub scan_link_table +{ + my ($self, $success_callback, $failure_callback) = @_; + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + return $aldb->scan_link_table($success_callback, $failure_callback); + } + +} + +=item C + +Prints a human readable description of various settings and attributes associated +with a device including: + +Hop Count, Engine Version, ALDB Type, ALDB Health, and Last ALDB Scan Time + +=cut + +sub log_aldb_status +{ + my ($self) = @_; + main::print_log( " Device ID: ".$self->device_id()); + my $hop_array; + $hop_array = join("",@{$$self{hop_array}}) if (defined($$self{hop_array})); + main::print_log( " Hop Count: ".$self->default_hop_count()." :: [$hop_array]"); + main::print_log( "Engine Version: ".$self->engine_version()); + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + main::print_log( " ALDB Type: ".ref($aldb)); + main::print_log( " ALDB Health: ".$aldb->health()); + main::print_log( "ALDB Scan Time: ".$aldb->scandatetime()); + } +} + +=item C + +In theory, would have the same effect as manually tapping the set button on the +device repeat times. + +WARN: Testing using the following does not produce results as expected. Use at +your own risk. [GL] + +=cut + +sub remote_set_button_tap +{ + my ($self,$p_number_taps) = @_; + my $taps = ($p_number_taps =~ /2/) ? '02' : '01'; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'remote_set_button_tap'); + $message->extra($taps); + $self->_send_cmd($message); +# $self->_send_cmd('command' => 'remote_set_button_tap', 'extra' => $taps); +} + +=item C + +Requests the current status of the device and calls C on the response. +This will trigger tied_events. + +=cut + +sub request_status +{ + my ($self, $requestor) = @_; + $$self{m_status_request_pending} = ($requestor) ? $requestor : 1; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'status_request'); + $self->_send_cmd($message); +# $self->_send_cmd('command' => 'status_request', 'is_synchronous' => 1); +} + +=item C + +Queues a get engine version insteon message using L +and sets a message failure callback to L. +Message response is processed in L + +Returns: nothing + +=cut + +sub get_engine_version { + my ($self, $success_callback, $failure_callback) = @_; + + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'get_engine_version'); + my $self_object_name = $self->get_object_name; + $message->failure_callback("$self_object_name->_get_engine_version_failure();$failure_callback"); + $message->success_callback($success_callback); + $self->_send_cmd($message); +} + +=item C<_get_engine_version_failure> + +Callback failure for L; called for NAK +and message timeout. Will force engine_version to I2CS which will also remap +the aldb version if the device responds with a NAK. Does nothing for timeouts +except print a message. + +Returns: nothing + +=cut + +sub _get_engine_version_failure +{ + my ($self) = @_; + my $failure_reason = $self->failure_reason(); + + main::print_log("[Insteon::BaseDevice::_get_engine_version_failure] DEBUG4: " + ."failure reason: $failure_reason") if $self->debuglevel(4, 'insteon'); + + if($failure_reason eq 'NAK') + { + #assume I2CS because no other device will NAK this command + main::print_log("[Insteon::BaseDevice] WARN: I2CS device (" . $self->get_object_name . ") is not " + ."linked; Please use 'link to interface' voice command"); + $self->engine_version('I2CS'); + } + #Clear success callback, otherwise it will run when message is cleared + $self->interface->active_message->success_callback('0'); +} + +=item C + +Sends the number of ping messages defined by count. A ping message is a basic +message that simply asks the device to respond with an ACKnowledgement. For +i1 devices this will send a standard length command, for i2 and i2cs devices +this will send an extended ping command. In both cases, the device responds +back with a standard length ACKnowledgement only. + +Much like the ping command in IP networks, this command is useful for testing the +connectivity of a device on your network. You likely want to use this in +conjunction with the C routine. For example, you can use +this to compare the message stats for a device when changing settings in +MisterHouse. + +Parameters: + count = the number of pings to send. + +Returns: Nothing. + +=cut + +sub ping +{ + my ($self, $p_count, $p_callback) = @_; + $$self{ping_count} = $p_count if defined($p_count); + $$self{ping_callback} = $p_callback if defined($p_callback); + if ($$self{ping_count}) { + $$self{ping_count}--; + my $message; + my $extra = sprintf("%02d", $$self{ping_count}); + if (uc($self->engine_version) eq 'I1'){ + $message = new Insteon::InsteonMessage('insteon_send', $self, + 'ping', $extra); + } + else { + my $extra = $extra . '0' x 28; + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, + 'ping', $extra); + } + ::print_log("[Insteon::BaseDevice] Sending ping request to " + . $self->get_object_name . " " . $$self{ping_count} + . " more ping requests queued."); + $message->failure_callback($self->get_object_name . '->ping()'); + $self->_send_cmd($message); + } + else { + ::print_log("[Insteon::BaseDevice] Completed ping queue for " + . $self->get_object_name); + if (defined $$self{ping_callback}){ + my $complete_callback = $$self{ping_callback}; + package main; + eval ($complete_callback); + &::print_log("[Insteon::BaseDevice] error in ping callback: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon::BaseDevice; + delete $$self{ping_callback}; + } + } +} + +=item C + +Used to set the led staatus of SwitchLinc companion leds. + +=cut + +sub set_led_status +{ + my ($self, $status_mask) = @_; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'set_led_status'); + $message->extra($status_mask); + $self->_send_cmd($message); +# $self->_send_cmd('command' => 'set_led_status', 'extra' => $status_mask); +} + +=item C + +This is called by mh on exit to save the cached ALDB of a device to persistant data. + +=cut + +sub restore_string +{ + my ($self) = @_; + my $restore_string = $self->SUPER::restore_string(); + if ($self->_aldb) + { + $restore_string .= $self->_aldb->restore_string(); + } + + return $restore_string; +} + +=item C + +Obsolete / do not use. + +Function should remain so that upgrading users will not have issues starting +MH from previous versions that referenced this function in the +mh_temp.saved_states file. + +=cut + +sub restore_states +{ + my ($self, $states) = @_; +} + +=item C + +Used to reload the aldb of a device on restart. + +=cut + +sub restore_aldb +{ + my ($self,$aldb) = @_; + if ($self->_aldb and $aldb) + { + $self->_aldb->restore_aldb($aldb); + } +} + +=item C + +Sets and returns the device category of a device. Devcat can be requested by +calling C. + +=cut + +sub devcat +{ + my ($self, $devcat) = @_; + if ($devcat) + { + $$self{devcat} = $devcat; + } + return $$self{devcat}; +} + +=item C + +Requests the device category for the device. The returned value can be obtained +by calling C. + +=cut + +sub get_devcat +{ + my ($self) = @_; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'id_request'); + $self->_send_cmd($message); +} + +=item C + +Sets and returns the device firmware version. Value can be obtained from the +device by calling C. + +=cut + +sub firmware +{ + my ($self, $firmware) = @_; + $$self{firmware} = $firmware if (defined $firmware); + return $$self{firmware}; +} + +=item C + +Sets and returns the available states for a device. + +=cut + +sub states +{ + my ($self, $states) = @_; + if ($states) + { + @{$$self{states}} = split(/,/,$states); + } + if ($$self{states}) + { + return @{$$self{states}}; + } else { + return undef; + } + +} + +=item C + +Reviews the cached version of all of the ALDBs and based on this review removes +links from this device which are not present in the mht file, not defined in the +code, or links which are only half-links. + +If audit_mode is true, prints the actions that would be taken to the log, but +does nothing. + +=cut + +sub delete_orphan_links +{ + my ($self, $audit_mode, $failure_callback) = @_; + return $self->_aldb->delete_orphan_links($audit_mode, $failure_callback) if $self->_aldb; +} + +sub _process_delete_queue { + my ($self) = @_; + $self->_aldb->_process_delete_queue() if $self->_aldb; +} + +=item C + +Prints a human readable form of MisterHouse's cached version of a device's ALDB +to the print log. Called as part of the "scan links" voice command +or in response to the "log links" voice command. + +=cut + +sub log_alllink_table +{ + my ($self) = @_; + $self->_aldb->log_alllink_table if $self->_aldb; +} + +=item C + +Sets or gets the device object engine version. If setting the engine version, +will also call check_aldb_version to map the aldb correctly for I2 devices. + +Parameters: + p_engine_version: [I1|I2|I2CS] to set engine version + +Returns: engine version string [I1|I2|I2CS] + +=cut + +sub engine_version +{ + my ($self, $p_engine_version) = @_; + my $engine_version = $self->SUPER::engine_version($p_engine_version); + $self->check_aldb_version() if $p_engine_version; + return $engine_version; +} + +=item C + +Sets or gets the number of message retries that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment retry log by one. + +Returns: current retry count. + +=cut + +sub retry_count_log +{ + my ($self, $retry_count_log) = @_; + $self = $self->get_root; + $$self{retry_count_log}++ if $retry_count_log; + return $$self{retry_count_log}; +} + +=item C + +Sets or gets the number of message failures that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment fail log by one. + +Returns: current fail count. + +=cut + +sub fail_count_log +{ + my ($self, $fail_count_log) = @_; + $self = $self->get_root; + $$self{fail_count_log}++ if $fail_count_log; + return $$self{fail_count_log}; +} + +=item C + +Sets or gets the number of outgoing message that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment output count by one. + +Returns: current output count. + +=cut + +sub outgoing_count_log +{ + my ($self, $outgoing_count_log) = @_; + $self = $self->get_root; + $$self{outgoing_count_log}++ if $outgoing_count_log; + return $$self{outgoing_count_log}; +} + +=item C + +Simulates a read of a link address from the device. Repeats this process as many +times as defined by count. This routine is meant to be used as a diagnostic tool. +It is similar to scan_link_table, however, if a failure occurs, this process will +continue with the next iteration. Scan link table will stop as soon as a single +failure occurs. + +This is also similar to the C test, however, rather than simply requesting +an ACK, this requests a full set of data equivalent to a link entry. Similar to +C this should be used with C to diagnose issues and try +different settings. + +Note: This routine can create a lot of traffic if count is set very high. Try +setting it to 5 first and working your way up. + +=cut + +sub stress_test +{ + my ($self, $p_count, $complete_callback) = @_; + $$self{stress_test_count} = $p_count if (defined ($p_count)); + $$self{stress_test_callback} = $complete_callback if (defined ($complete_callback)); + if ($$self{stress_test_count}){ + &::print_log("[Insteon::BaseDevice] " . $self->get_object_name + . " - Stress Test " . $$self{stress_test_count} . " iterations left"); + my $aldb = $self->get_root()->_aldb; + if ($aldb) + { + $$aldb{_mem_activity} = 'scan'; + $$aldb{_stress_test_act} = 1; + $$aldb{_failure_callback} = $self->get_object_name + . '->stress_test()'; + if($aldb->isa('Insteon::ALDB_i1')) { + $aldb->_peek('0FF8'); + } else { + #Prevents duplicate commands in queue error + #Also allows for better identification of + #sequential dupe incoming messages + my $odd = $$self{stress_test_count} % 2; + if ($odd){ + $aldb->send_read_aldb('0fff'); + } else { + $aldb->send_read_aldb('0ff7'); + } + } + } + $$self{stress_test_count}--; + } else { + &::print_log("[Insteon::BaseDevice] Stress Test Complete for " + . $self->get_object_name); + if (defined $$self{stress_test_callback}){ + $complete_callback = $$self{stress_test_callback}; + package main; + eval ($complete_callback); + &::print_log("[Insteon::BaseDevice] error in stress_test callback: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon::BaseDevice; + delete $$self{stress_test_callback}; + } + } +} + +=item C + +Sets or gets the number of hops that have been used in all outgoing messages +since the last time C was called. + +If type is set, to any value, will increment output count by that value. + +Returns: current hop count. + +=cut + +sub outgoing_hop_count +{ + my ($self, $outgoing_hop_count) = @_; + $self = $self->get_root; + $$self{outgoing_hop_count} += $outgoing_hop_count if $outgoing_hop_count; + return $$self{outgoing_hop_count}; +} + +=item C + +Sets or gets the number of incoming message that have occured for this device +since the last time C was called. + +If type is set, to any value, will increment incoming count by one. + +Returns: current incoming count. + +=cut + +sub incoming_count_log +{ + my ($self, $incoming_count_log) = @_; + $self = $self->get_root; + $$self{incoming_count_log}++ if $incoming_count_log; + return $$self{incoming_count_log}; +} + +=item C + +Sets or gets the number of currupt message that have arrived from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current corrupt count. + +=cut + +sub corrupt_count_log +{ + my ($self, $corrupt_count_log) = @_; + $self = $self->get_root; + $$self{corrupt_count_log}++ if $corrupt_count_log; + return $$self{corrupt_count_log}; +} + +=item C + +Sets or gets the number of duplicate message that have arrived from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current duplicate count. + +=cut + +sub dupe_count_log +{ + my ($self, $dupe_count_log) = @_; + $self = $self->get_root; + $$self{dupe_count_log}++ if $dupe_count_log; + return $$self{dupe_count_log}; +} + +=item C + +Sets or gets the number of hops_left for messages that arrive from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current hops_left count. + +=cut + +sub hops_left_count +{ + my ($self, $hops_left_count) = @_; + $self = $self->get_root; + $$self{hops_left_count} += $hops_left_count if $hops_left_count; + return $$self{hops_left_count}; +} + +=item C + +Sets or gets the number of max_hops for messages that arrive from this device +since the last time C was called. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current duplicate count. + +=cut + +sub max_hops_count +{ + my ($self, $max_hops_count) = @_; + $self = $self->get_root; + $$self{max_hops_count} += $max_hops_count if $max_hops_count; + return $$self{max_hops_count}; +} + + +=item C + +Resets the retry, fail, outgoing, incoming, and corrupt message counters. + +=cut + +sub reset_message_stats +{ + my ($self) = @_; + $self = $self->get_root; + $$self{retry_count_log} = 0; + $$self{fail_count_log} = 0; + $$self{outgoing_count_log} = 0; + $$self{incoming_count_log} = 0; + $$self{corrupt_count_log} = 0; + $$self{dupe_count_log} = 0; + $$self{hops_left_count} = 0; + $$self{max_hops_count} = 0; + $$self{outgoing_hop_count} = 0; +} + +=item C + +Prints message statistics for this device to the print log. The output contains: + +=back + +=over8 + +=item * + +In - The number of incoming messages received + +=item * + +Corrupt - The number of incoming corrupt messages received + +=item * + +%Corrpt - Of the incoming messages received, the percentage that were +corrupt + +=item * + +Dupe - The number of duplicate messages that have been received from this +device. + +=item * + +%Dupe - The percentage of duplilicate incoming messages received. + +=item * + +Hops_Left - The average hops left in the messages received from this device. + +=item * + +Max_Hops - The average maximum hops in the messages received from this device. + +=item * + +Act_Hops - Max_Hops - Hops_Left, this is the average number of hops that have +been required for a message sent from the device to reach MisterHouse. + +=item * + +Out - The number of unique outgoing messages, without retries, sent. + +=item * + +Fail - The number times that all retries were exhausted without a successful +delivery of a message. + +=item * + +%Fail - Of the outgoing messages sent, the percentage that failed. + +=item * + +Retry - The number of retry attempts that have been made to deliver a message. +Ideally this is 0, but Sends/Msg is a better indication of this parameter. + +=item * + +AvgSend - The average number of send attempts that must be made in order to +successfully deliver a message. Ideally this would be 1.0. + +NOTE: If the number of retries exceeds the value set in the configuration file +for Insteon_retry_count, MisterHouse will abandon sending the message. As a +result, as this number approaches Insteon_retry_count it becomes a less accurate +representation of the number of retries needed to reach a device. + +=item * + +Avg_Hops - The average number of hops that have been used by MisterHouse when +sending messages to this device. + +=item * + +Hop_Count - The current hop count being used by MH. This count is dynamically +controlled by MH and is not reset by calling C + +=back + +=over + +=cut + +sub print_message_stats +{ + my ($self) = @_; + $self = $self->get_root; + my $object_name = $self->get_object_name; + my $retry_average = 0; + $retry_average = sprintf("%.1f", ($$self{retry_count_log} / + $$self{outgoing_count_log}) + 1) if ($$self{outgoing_count_log} > 0); + my $fail_percentage = 0; + $fail_percentage = sprintf("%.1f", ($$self{fail_count_log} / + $$self{outgoing_count_log}) * 100 ) if ($$self{outgoing_count_log} > 0); + my $corrupt_percentage = 0; + $corrupt_percentage = sprintf("%.1f", ($$self{corrupt_count_log} / + $$self{incoming_count_log}) * 100 ) if ($$self{incoming_count_log} > 0); + my $dupe_percentage = 0; + $dupe_percentage = sprintf("%.1f", ($$self{dupe_count_log} / + $$self{incoming_count_log}) * 100 ) if ($$self{incoming_count_log} > 0); + my $avg_hops_left = 0; + $avg_hops_left = sprintf("%.1f", ($$self{hops_left_count} / + $$self{incoming_count_log})) if ($$self{incoming_count_log} > 0); + my $avg_max_hops = 0; + $avg_max_hops = sprintf("%.1f", ($$self{max_hops_count} / + $$self{incoming_count_log})) if ($$self{incoming_count_log} > 0); + my $avg_out_hops = 0; + $avg_out_hops = sprintf("%.1f", ($$self{outgoing_hop_count} / + $$self{outgoing_count_log})) if ($$self{outgoing_count_log} > 0); + ::print_log( + "[Insteon::BaseDevice] Message statistics for $object_name:\n" + . " In Corrupt %Corrpt Dupe %Dupe HopsLeft Max_Hops Act_Hops\n" + . sprintf("%6s", $$self{incoming_count_log}) + . sprintf("%8s", $$self{corrupt_count_log}) + . sprintf("%8s", $corrupt_percentage . '%') + . sprintf("%6s", $$self{dupe_count_log}) + . sprintf("%8s", $dupe_percentage . '%') + . sprintf("%9s", $avg_hops_left) + . sprintf("%9s", $avg_max_hops) + . sprintf("%9s", $avg_max_hops - $avg_hops_left) + . "\n" + . " Out Fail %Fail Retry AvgSend Avg_Hops CurrHops\n" + . sprintf("%6s", $$self{outgoing_count_log}) + . sprintf("%8s", $$self{fail_count_log}) + . sprintf("%8s", $fail_percentage . '%') + . sprintf("%6s", $$self{retry_count_log}) + . sprintf("%8s", $retry_average) + . sprintf("%9s", $avg_out_hops) + . sprintf("%9s", $self->default_hop_count) + ); +} + + +=item C + +Because of the way MH saves / restores states "after" object creation +the aldb must be initially created before the engine_version is restored. +It is therefore impossible to know the device is i2 before creating +the aldb object. The solution is to keep the existing logic which assumes +the device is peek/poke capable (i1 or i2) and then delete/recreate the +aldb object if it is later determined to be an i2 device. + +There is a use case where a device is initially I2 but the user replaces +the device with an I1 device, reusing the same object name. In this case +the object state restore will build an I2 aldb object. The user must +manually initiate the 'get engine version' voice command or stop/start +MH so the initial poll will detect the change. + +This is called anytime the engine_version is queried (initial startup poll) and +in the Reload_post_hooks once object_states_restore completes + +=cut + +sub check_aldb_version +{ + my ($self) = @_; + + my $engine_version = $self->SUPER::engine_version(); + my $new_version = ""; + if($engine_version and $engine_version ne 'I1' and $self->_aldb->aldb_version() ne 'I2') { + $new_version = "I2"; + } + elsif($engine_version eq 'I1' and $self->_aldb->aldb_version() ne 'I1') { + $new_version = "I1"; + } + if ($new_version) { + main::print_log("[Insteon::BaseDevice] DEBUG4: aldb_version is " + .$self->_aldb->aldb_version()." but device is ".$engine_version. + ". Remapping aldb version to $new_version") if $self->debuglevel(4, 'insteon'); + my $restore_string = ''; + if ($self->_aldb) { + $restore_string = $self->_aldb->restore_string(); + } + undef $self->{aldb}; + + if ($new_version eq "I2") { + $self->{aldb} = new Insteon::ALDB_i2($self); + } + else { + $self->{aldb} = new Insteon::ALDB_i1($self); + } + + package main; + eval ($restore_string); + &::print_log("[Insteon::BaseDevice] error in eval creating ALDB object: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon::BaseDevice; + } +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'link to interface' => "$object_name->link_to_interface", + 'unlink with interface' => "$object_name->unlink_to_interface" + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'status' => "$object_name->request_status", + 'get engine version' => "$object_name->get_engine_version", + 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", + 'print message stats' => "$object_name->print_message_stats", + 'reset message stats' => "$object_name->reset_message_stats", + 'run stress test' => "$object_name->stress_test(5)", + 'run ping test' => "$object_name->ping(5)", + 'log links' => "$object_name->log_alllink_table()" + ) + } + return \%voice_cmds; +} + +=back + +=head2 INI PARAMETERS + +=over + +=item Insteon_PLM_max_queue_time + +Was previously used to set the maximum amount of time +a message could remain in the queue. This parameter is no longer used in the code +but it still appears in the initialization. It may be removed at a future date. +This also gets set in L +as well for some reason, but is not used there either. + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +#################################### +### ############### +### MultigroupDevice ############### +### ############### +#################################### + +=head1 B + +=head2 DESCRIPTION + +Contains functions which are unique to insteon devices which have more than +one group. This includes, KeyPadLincs, RemoteLincs, FanLincs, Thermostats +(i2 versions). + +=head2 INHERITS + +Nothing. + +This package is meant to provide supplemental support and should only be added +as a secondary inheritance to an object. + +=head2 METHODS + +=over + +=cut + +package Insteon::MultigroupDevice; + +=item C + +Syncs all links on the object, including all subgroups such as additional +buttons. + +Paramter B - Causes sync to walk through but not actually +send any commands to the devices. Useful with the insteon:3 debug setting for +troubleshooting. + +=cut + +sub sync_all_links +{ + my ($self, $audit_mode) = @_; + $self = $self->get_root(); + @{$$self{_sync_devices}} = (); + @{$$self{_sync_device_failures}} = (); + my $device_id = $self->device_id(); + my ($subgroup_object, $group, $dec_group); + + ::print_log("[Insteon::MultigroupDevice] Sync All Links on device " + .$self->get_object_name . " starting ..."); + # Find all subgroup items check groups from 02 - FF; + for ($dec_group = 01; $dec_group <= 255; $dec_group++) { + $group = sprintf("%02X", $dec_group); + $subgroup_object = Insteon::get_object($device_id, $group); + if (ref $subgroup_object){ + my %sync_req = ('sync_object' => $subgroup_object, + 'audit_mode' => ($audit_mode) ? 1 : 0); + ::print_log("[Insteon::MultigroupDevice] " + ."Adding " . $subgroup_object->get_object_name + ." to sync queue."); + push @{$$self{_sync_devices}}, \%sync_req + } + } + $$self{_sync_cnt} = scalar @{$$self{_sync_devices}}; + $self->_get_next_linksync(); +} + +=item C<_get_next_linksync()> + +Calls the sync_links() function for each device identified by sync_all_link. +This function will be called recursively since the callback passed to sync_links() +is this function again. Will also ask sync_links() to call +_get_next_linksync_failure() if sync_links() fails. + +=cut + +sub _get_next_linksync +{ + my ($self) = @_; + $self = $self->get_root(); + my $sync_req_ptr = shift(@{$$self{_sync_devices}}); + my %sync_req = ($sync_req_ptr) ? %$sync_req_ptr : undef; + my $current_sync_device; + if (%sync_req) { + $current_sync_device = $sync_req{'sync_object'}; + } + else { + $current_sync_device = undef; + } + + if ($current_sync_device) { + ::print_log("[Insteon::MultigroupDevice] Now syncing: " + . $current_sync_device->get_object_name . " (" + . ($$self{_sync_cnt} - scalar @{$$self{_sync_devices}}) + . " of ".$$self{_sync_cnt}.")"); + $current_sync_device->sync_links($sync_req{'audit_mode'}, + $self->get_object_name . '->_get_next_linksync()', + $self->get_object_name . '->_get_next_linksync_failure('.$current_sync_device->get_object_name.')'); + } + else { + ::print_log("[Insteon::MultigroupDevice] All links have completed syncing " + . "on device " . $self->get_object_name); + my $_sync_failure_cnt = scalar @{$$self{_sync_device_failures}}; + if ($_sync_failure_cnt) { + ::print_log("[Insteon::MultigroupDevice] However, some failures were noted:"); + for my $failed_obj (@{$$self{_sync_device_failures}}) { + ::print_log("[Insteon::MultigroupDevice] WARN: failure occurred when syncing " + . $failed_obj->get_object_name); + } + } + } +} + +=item C<_get_next_linksync()> + +Called by the failure callback in a device's sync_links() function. Will add +the failed device to the module global variable @_sync_device_failures. + +=cut + +sub _get_next_linksync_failure +{ + my ($self, $current_sync_device) = @_; + $self = $self->get_root(); + push @{$$self{_sync_device_failures}}, $current_sync_device; + ::print_log("[Insteon::MultigroupDevice] WARN: failure occurred when syncing " + . $current_sync_device->get_object_name . ". Moving on..."); + $self->_get_next_linksync(); +} + +=back + +=head2 AUTHOR + +Kevin Robert Keegan + +=head2 LICENSE + +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. + +=cut + +#################################### +### ################# +### BaseController ################# +### ################# +#################################### + +=head1 B + +=head2 DESCRIPTION + +Generic class implementation of an Insteon Controller. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::BaseController; + +use strict; + +@Insteon::BaseController::ISA = ('Generic_Item'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface,$p_devcat) = @_; + + # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller + my $self = {}; + bless $self,$class; + return $self; +} + +=item C + +Adds object as a responder to the device. If on_level and ramp_rate are specified +they will be used, otherwise 100% .1s will be used. + +=cut + +sub add +{ + my ($self, $obj, $on_level, $ramp_rate) = @_; + if (ref $obj and ($obj->isa('Light_Item') or $obj->isa('Insteon::BaseDevice'))) + { + if ($$self{members} && $$self{members}{$obj}) + { + print "[Insteon::BaseController] An object (" . $obj->{object_name} . ") already exists " + . "in this scene. Aborting add request.\n"; + return; + } + if ($on_level =~ /^sur/i) + { + $on_level = '100%'; + $$obj{surrogate} = $self; + } + elsif (lc $on_level eq 'on') + { + $on_level = '100%'; + } + elsif (lc $on_level eq 'off') + { + $on_level = '0%'; + } + $on_level = '100%' unless $on_level; + $$self{members}{$obj}{on_level} = $on_level; + $$self{members}{$obj}{object} = $obj; + $ramp_rate =~ s/s$//i; + $$self{members}{$obj}{ramp_rate} = $ramp_rate if defined $ramp_rate; + } else { + &::print_log("[Insteon::BaseController] WARN: unable to add ".$obj->{device_id}.":".$obj->{m_group} + ." as items of type ".ref($obj)." are not supported!"); + } +} + +=item C + +Causes MisterHouse to review the list of objects that should be linked to device +and to check to make sure these links are complete. If any link is not complete +MisterHouse will add the link. + +If audit_mode is true, MisterHouse will print the actions it would have taken to +the log, but will not take any actions. + +The process does the following 5 checks in order: + + 1. Does a controller link exist for Device-> PLM + 2. Does a responder link exist on the PLM + +it then loops through all of the links defined for a device and checks: + + 3. Does the responder link exist + 4. Is the responder link accurate + 5. Does the controller link on this device exist + +=cut + +sub sync_links +{ + my ($self, $audit_mode, $callback, $failure_callback, $skip_deaf) = @_; + + # Intialize Variables + @{$$self{sync_queue}} = (); # reset the work queue + $$self{sync_queue_callback} = ($callback) ? $callback : undef; + $$self{sync_queue_failure_callback} = ($failure_callback) ? $failure_callback : undef; + $$self{sync_queue_failure} = undef; + my $self_link_name = $self->get_object_name; + my $insteon_object = $self->interface; + my $interface_object = Insteon::active_interface(); + my $interface_name = $interface_object->get_object_name; + $insteon_object = $self->get_root; + ::print_log("[Insteon::BaseController] WARN!! A device w/ insteon address: " . $self->device_id . ":01 could not be found. " + . "Please double check your items.mht file.") if (!(defined($insteon_object))); + + # Abort if $insteon_object doesn't exist + $self->_process_sync_queue() unless $insteon_object; + + # Warn if device is deaf or ALDB out of sync + my $insteon_object_is_syncable = 1; + if ($insteon_object->is_deaf && $skip_deaf) { + ::print_log("[Insteon::BaseController] $self_link_name is deaf, only responder links will be added to devices " + ."controlled by this device. To sync links on this device, put it in awake mode and run the 'Sync Links' " + ."command on this specific device."); + $insteon_object_is_syncable = 0; + } + elsif ($insteon_object->_aldb->health ne 'good' && $insteon_object->_aldb->health ne 'empty'){ + ::print_log("[Insteon::BaseController] WARN! The ALDB of $self_link_name is ".$insteon_object->_aldb->health + .", links will be added to devices " + ."linked to this device, but no links will be added to $self_link_name. Please rescan this device and attempt " + ."sync links again."); + $insteon_object_is_syncable = 0; + $$self{sync_queue_failure} = 1; + } + + # 1. Does a controller link exist for Device-> PLM + if (!$insteon_object->isa('Insteon_PLM') && + !$insteon_object->has_link($self->interface,$self->group,1,$self->group) && + $insteon_object_is_syncable) { + my %link_req = ( member => $insteon_object, cmd => 'add', object => $self->interface, + group => $self->group, is_controller => 1, + callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, + data3 => $self->group); + $link_req{cause} = "Adding controller record to $self_link_name for $interface_name"; + push @{$$self{sync_queue}}, \%link_req; + } + + # 2. Does a responder link exist on the PLM + if ((!$insteon_object->isa('Insteon_PLM') && + !$self->interface->has_link($insteon_object,$self->group,0,'00'))) { + my %link_req = ( member => $self->interface, cmd => 'add', object => $insteon_object, + group => $self->group, is_controller => 0, + callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, + data3 => '00'); + $link_req{cause} = "Adding responder record to $interface_name from $self_link_name"; + push @{$$self{sync_queue}}, \%link_req; + } + + # Loop members + foreach my $member_ref (keys %{$$self{members}}) { + my $member = $$self{members}{$member_ref}{object}; + + # find real device if member is a Light_Item + if ($member->isa('Light_Item')) { + my @children = $member->find_members('Insteon::BaseDevice'); + $member = $children[0]; + } + + #Initialize Loop Variables + my $member_name = $member->get_object_name; + my $member_root = $member->get_root; + my $requires_update = 0; + my $has_link = 1; + my $cause; + my $tgt_on_level = $$self{members}{$member_ref}{on_level}; + $tgt_on_level = '100' unless defined $tgt_on_level; + my $tgt_ramp_rate = $$self{members}{$member_ref}{ramp_rate}; + $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; + $tgt_on_level =~ s/(\d+)%?/$1/; + $tgt_ramp_rate =~ s/(\d)s?/$1/; + my $resp_aldbkey = $member_root->_aldb->get_linkkey($insteon_object->device_id, + $self->group, + '0', + $member->group); + + # 3. Does the responder link exist + if (!$member_root->has_link($insteon_object, $self->group, 0, $member->group) && + ($member_root->_aldb->health eq 'good' || $member_root->_aldb->health eq 'empty')){ + my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, + group => $self->group, is_controller => 0, + on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, + callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, + data3 => $member->group); + $link_req{cause} = "Adding responder record to $member_name from $self_link_name"; + push @{$$self{sync_queue}}, \%link_req; + $has_link = 0; + } + elsif ($member_root->_aldb->health ne 'good' && $member_root->_aldb->health ne 'empty'){ + my %link_req = ( member => $member, cmd => 'add', object => $insteon_object, + group => $self->group, is_controller => 0, + on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, + callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, + data3 => $member->group); + $link_req{skip} = "Unable to add the following responder record to $member_name " + ."from $self_link_name because the aldb of $member_name is " + . $member_root->_aldb->health; + push @{$$self{sync_queue}}, \%link_req; + $$self{sync_queue_failure} = 1; + } + + # 4. Is the responder link accurate + if ($member->isa('Insteon::DimmableLight') && $has_link) { + my $member_aldb = $member_root->_aldb; + my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; + my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; + my $cur_on_level = hex($data1)/2.55; + my $raw_cur_ramp_rate = $data2; + my $raw_tgt_ramp_rate = Insteon::DimmableLight::convert_ramp($tgt_ramp_rate); + if ($raw_cur_ramp_rate ne $raw_tgt_ramp_rate) { + $requires_update = 1; + $cause .= "Ramp rate "; + } + elsif ($cur_on_level-1 > $tgt_on_level && $cur_on_level+1 < $tgt_on_level){ + $requires_update = 1; + $cause .= "On level "; + } + } + elsif ($has_link){ + my $member_aldb = $member->_aldb; + my $data1 = $$member_aldb{aldb}{$resp_aldbkey}{data1}; + my $data2 = $$member_aldb{aldb}{$resp_aldbkey}{data2}; + if ($tgt_on_level >= 1 and $data1 ne 'ff') { + $requires_update = 1; + $tgt_on_level = 100; + $cause .= "On level "; + } + elsif ($tgt_on_level == 0 and $data1 ne '00') { + $requires_update = 1; + $cause .= "On level "; + } + if ($data2 ne '00') { + $requires_update = 1; + $tgt_ramp_rate = 0; + $cause .= "Ramp rate "; + } + } + if ($requires_update && + ($member_root->_aldb->health eq 'good' || $member_root->_aldb->health eq 'empty')) { + my %link_req = ( member => $member, cmd => 'update', object => $insteon_object, + group => $self->group, is_controller => 0, + on_level => $tgt_on_level, ramp_rate => $tgt_ramp_rate, + callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, + data3 => $member->group); + $link_req{cause} = "Updating responder record on $member_name " + . "to fix $cause"; + push @{$$self{sync_queue}}, \%link_req; + } + + # 5. Does the controller link on this device exist + if (!($insteon_object->has_link($member, $self->group, 1, $self->group)) && + $insteon_object_is_syncable) { + my %link_req = ( member => $insteon_object, cmd => 'add', object => $member, + group => $self->group, is_controller => 1, + callback => "$self_link_name->_process_sync_queue()", + failure_callback => $failure_callback, + data3 => $self->group); + $link_req{cause} = "Adding controller record to $self_link_name for $member_name"; + push @{$$self{sync_queue}}, \%link_req; + } + } + + my $num_sync_queue = @{$$self{sync_queue}}; + my $index = 0; + if (!($num_sync_queue)) + { + &::print_log("[Insteon::BaseController] Nothing to do when syncing links for " . $self->get_object_name) + if $self->debuglevel(1, 'insteon'); + } + foreach (@{$$self{sync_queue}}){ + my %sync_req = %{$_}; + my $audit_text = "(AUDIT)" if ($audit_mode); + my $log_text = "[Insteon::BaseController] $audit_text "; + if ($sync_req{skip}){ + $log_text .= $sync_req{skip} ."\n"; + splice @{$$self{sync_queue}}, $index, 1; + } else { + $index++; + $log_text .= $sync_req{cause} . "\n"; + } + PRINT: for (keys %sync_req) { + next PRINT if (($_ eq 'cause') || ($_ eq 'callback') || + ($_ eq 'member') || ($_ eq 'object') || + ($_ eq 'skip') || ($_ eq 'failure_callback')); + $log_text .= "$_ = $sync_req{$_}; "; + } + ::print_log($log_text); + } + if ($audit_mode) { + @{$$self{sync_queue}} = (); + } + + $self->_process_sync_queue(); +} + +sub _process_sync_queue { + my ($self) = @_; + # get next in queue if it exists + my $num_sync_queue = @{$$self{sync_queue}}; + if ($num_sync_queue) { + my $link_req_ptr = shift(@{$$self{sync_queue}}); + my %link_req = %$link_req_ptr; + if ($link_req{cmd} eq 'update') { + my $link_member = $link_req{member}; + $link_member->update_link(%link_req); + } elsif ($link_req{cmd} eq 'add') { + my $link_member = $link_req{member}; + $link_member->add_link(%link_req); + } + } elsif ($$self{sync_queue_callback}) { + my $callback = $$self{sync_queue_callback}; + if ($$self{sync_queue_failure}){ + $callback = $$self{sync_queue_failure_callback}; + } + package main; + eval ($callback); + &::print_log("[Insteon::BaseController] error in sync links callback: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon::BaseController; + } else { + main::print_log($self->get_object_name." completed sync links"); + } +} + +=item C + +Checks each linked member of device. If the linked member is a C, +then sets that item to state. If the linked member is a C +then call C for the member. + +=cut + +sub set_linked_devices +{ + my ($self, $link_state) = @_; + # iterate over the members + if ($$self{members}) + { + foreach my $member_ref (keys %{$$self{members}}) + { + my $member = $$self{members}{$member_ref}{object}; + # If controller is on, set member to stored on_level + # else set to controller value + my $local_state = $$self{members}{$member_ref}{on_level}; + $local_state = '100' unless $local_state; + + if ($member->isa('Light_Item')) + { + # if they are Light_Items, then set their on_dim attrib to the member on level + # and then "blank" them via the manual method for a tad over the ramp rate + # In addition, locate the Light_Item's Insteon_Device member and do the + # same as if the member were an Insteon_Device + my $ramp_rate = $$self{members}{$member_ref}{ramp_rate}; + $ramp_rate = 0 unless defined $ramp_rate; + $ramp_rate = $ramp_rate + 2; + my @lights = $member->find_members('Insteon::BaseDevice'); + if (@lights) + { + my $light = $lights[0]; + # remember the current state to support resume + $$self{members}{$member_ref}{resume_state} = $light->state; + $member->manual($light, $ramp_rate); + if (lc $link_state ne 'on'){ + $local_state = $light->$link_state; + } + $light->set_receive($local_state,$self); + } + else + { + $member->manual(1, $ramp_rate); + } + $member->set_on_state($local_state) unless $link_state eq 'off'; + } + elsif ($member->isa('Insteon::BaseDevice')) + { + # remember the current state to support resume + $$self{members}{$member_ref}{resume_state} = $member->state; + # if they are Insteon_Device objects, then simply set_receive their state to + # the member on level + if (lc $link_state ne 'on'){ + $local_state = $link_state; + } + $member->set_receive($local_state,$self); + } + } + } +} + +=item C + +NOTE - This routine appears to be nearly identical, if not identical to the +C routine, it is not clear why this routine is +needed here. + +See full description of this routine in C + +=cut + +sub set_with_timer { + my ($self, $state, $time, $return_state, $additional_return_states) = @_; + return if &main::check_for_tied_filters($self, $state); + + $self->set($state) unless $state eq ''; + + return unless $time; + + my $state_change = ($state eq 'off') ? 'on' : 'off'; + $state_change = $return_state if defined $return_state; + $state_change = $self->{state} if $return_state and lc $return_state eq 'previous'; + + $state_change .= ';' . $additional_return_states if $additional_return_states; + + $$self{set_timer} = &Timer::new() unless $$self{set_timer}; + my $object_name = $self->{object_name}; + my $action = "$object_name->set('$state_change')"; + $$self{set_timer}->set($time, $action); +} + +=item C + +This appears to be a depricated routine that is no longer used. At a cursory +glance, it may throw an error if called. + +=cut + +sub update_members +{ + my ($self) = @_; + # iterate over the members + if ($$self{members}) { + foreach my $member_ref (keys %{$$self{members}}) { + my ($device); + my $member = $$self{members}{$member_ref}{object}; + my $on_state = $$self{members}{$member_ref}{on_level}; + $on_state = '100%' unless $on_state; + my $ramp_rate = $$self{members}{$member_ref}{ramp_rate}; + $ramp_rate = 0 unless defined $ramp_rate; + if ($member->isa('Light_Item')) { + # if they are Light_Items, then locate the Light_Item's Insteon_Device member + my @lights = $member->find_members('Insteon::BaseDevice'); + if (@lights) { + $device = $lights[0]; + } + } elsif ($member->isa('Insteon::BaseDevice')) { + $device = $member; + } + if ($device) { + my %current_record = $device->get_link_record($self->device_id . $self->group); + if (%current_record) { + &::print_log("[Insteon::BaseController] remote record: $current_record{data1}") + if $self->debuglevel(1, 'insteon'); + } + } + } + } +} + +=item C + +Places the interface in linking mode as the controller. To complete the process +press the set button on the responder device until it beeps. Alternatively, +you may be able to complete the process with +C. + +=cut + +sub initiate_linking_as_controller +{ + my ($self, $p_group, $success_callback, $failure_callback) = @_; + # iterate over the members + if ($$self{members}) { + foreach my $member_ref (keys %{$$self{members}}) { + my $member = $$self{members}{$member_ref}{object}; + if ($member->isa('Light_Item')) { + # if they are Light_Items, then set them to manual to avoid automation + # while manually setting light parameters + $member->manual(1,120,120); # 120 seconds should be enough + } + } + } + $self->interface()->initiate_linking_as_controller($p_group, $success_callback, $failure_callback); +} + +=item C + +Returns a list of objects that are members of device. If type is specified, only +members of that type are returned. Type is a package name, for example: + + $member->find_members('Insteon::BaseDevice'); + +=cut + +sub find_members +{ + my ($self,$p_type) = @_; + + my @l_found; + if ($$self{members}) + { + foreach my $member_ref (keys %{$$self{members}}) + { + my $member = $$self{members}{$member_ref}{object}; + if ($member->isa($p_type)) + { + push @l_found, $member; + } + } + } + return @l_found; + +} + +=item C + +Returns true if object is a member of device, else returns false. + +=cut + +sub has_member +{ + my ($self, $compare_object) = @_; + foreach my $member_ref (keys %{$$self{members}}) + { + my $member = $$self{members}{$member_ref}{object}; + if ($member eq $compare_object) + { + return 1; + } + } + return 0; +} + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +#################################### +### ##################### +### DeviceController ############### +### ############### +#################################### + +=head1 B + +=head2 DESCRIPTION + +Generic class implementation of an Device Controller. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::DeviceController; + +use strict; + +@Insteon::DeviceController::ISA = ('Insteon::BaseController'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface,$p_devcat) = @_; + + # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller + my $self = new Insteon::BaseController($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + +=item C + +Requests the current status of the device and calls C on the response. +This will trigger tied_events. + +=cut + +sub request_status +{ + my ($self,$requestor) = @_; +# if ($self->group ne '01') { + if ($$self{members} and !($self->isa('Insteon::InterfaceController')) + and (!(ref $requestor) or ($requestor eq $self))) { + &::print_log("[Insteon::DeviceController] requesting status for members of " . $$self{object_name}); + foreach my $member (keys %{$$self{members}}) { + next unless $member->isa('Insteon::BaseObject'); + my $member_obj = $$self{members}{$member}{object}; + next if $requestor eq $member_obj; + if ($member_obj->isa('Insteon::BaseDevice')) { + &::print_log("[Insteon::DeviceController] checking status of " . $member_obj->get_object_name() + . " for requestor " . $requestor->get_object_name()); + $member_obj->request_status($self); + } + } + } + # the following has bad assumptions in that we don't always know if a device is a responder + # since it could be a slave + if ($self->is_root && $self->is_responder) { + $self->Insteon::BaseDevice::request_status($requestor); + } +} + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +#################################### +### ############ +### InterfaceController ############ +### ############ +#################################### + +=head1 B + +=head2 DESCRIPTION + +Generic class implementation of an Interface Controller. These are the PLM Scenes. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::InterfaceController; + +use strict; + +@Insteon::InterfaceController::ISA = ('Insteon::BaseController','Insteon::BaseObject'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + # note that $p_deviceid will be 00.00.00: if the link uses the interface as the controller + my $self = new Insteon::BaseObject($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + + +#Otherwise BaseController will call Generic_Item::Set +sub set +{ + my ($self,$p_state,$p_setby,$p_response) = @_; + $self->Insteon::BaseObject::set($p_state,$p_setby,$p_response); +} + +sub is_root +{ + return 0; +} + +=item C + +Returns the root object of a device, in this case the interface. + +=cut + +sub get_root { + my ($self) = @_; + return $self->interface; +} + +# For IFaceControllers, need to call set_linked_devices +sub is_acknowledged { + my ($self, $p_ack) = @_; + if ($p_ack) { + $self->set_linked_devices($$self{pending_state}) if defined $$self{pending_state}; + } + return $self->Insteon::BaseObject::is_acknowledged($p_ack); +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my $group = $self->group; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'on' => "$object_name->set(\"on\")", + 'off' => "$object_name->set(\"off\")", + 'initiate linking as controller' => "$object_name->initiate_linking_as_controller(\"$group\")", + 'cancel linking' => "$object_name->interface()->cancel_linking" + ); + return \%voice_cmds; +} + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +1; diff --git a/lib/Insteon/BaseInterface.pm b/lib/Insteon/BaseInterface.pm index 93d00e589..29348b067 100644 --- a/lib/Insteon/BaseInterface.pm +++ b/lib/Insteon/BaseInterface.pm @@ -1,1021 +1,1021 @@ -=head1 B - -=head2 SYNOPSIS - -Provides support for the Insteon Interface. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::BaseInterface; - -use strict; -use Insteon::Message; -@Insteon::BaseInterface::ISA = ('Class::Singleton'); - -=item C - -Locates the active_interface from the main Insteon class and calls -C on it. Called once per loop to get data from the PLM. - -=cut - -sub check_for_data -{ - my $interface = &Insteon::active_interface(); - $interface->check_for_data(); -} - -=item C - -Called on startup or reload. Will always request and print the plm_info, which -contains the PLM revision number, to the log on startup. - -If Insteon_PLM_scan_at_startup is set to 1 in the ini file, this routine will poll -all insteon devices and request their current state. Useful for making sure that -no devices changed their state while MisterHouse was off. Will also call -L on each device to ensure that -the proper ALDB object is created for them. - -=cut - -sub poll_all -{ - my $scan_at_startup = $main::config_parms{Insteon_PLM_scan_at_startup}; - $scan_at_startup = 1 unless defined $scan_at_startup; - $scan_at_startup = 0 unless $main::Save{mh_exit} eq 'normal'; - my $plm = &Insteon::active_interface(); - if (defined $plm) - { - if (!($plm->device_id) and !($$plm{_id_check})) - { - $$plm{_id_check} = 1; - $plm->queue_message(new Insteon::InsteonMessage('plm_info', $plm)); - } - if ($scan_at_startup) - { - - for my $insteon_device (&Insteon::find_members('Insteon::BaseDevice')) - { - if ($insteon_device and $insteon_device->is_root and $insteon_device->is_responder) - { - # don't request status for objects associated w/ other than the primary group - # as they are psuedo links - $insteon_device->get_engine_version(); - $insteon_device->request_status(); - } - } - } - } -} - -=item C - -Instantiate a new object. - -=cut - -sub new -{ - my ($class) = @_; - - my $self = {}; - @{$$self{command_stack2}} = (); - @{$$self{command_history}} = (); - $$self{received_commands} = {}; - bless $self, $class; - $self->transmit_in_progress(0); -# $self->debug(0) unless $self->debug; - return $self; -} - -=item C - -Returns 1 if object is the same as $self, otherwise returns 0. - -=cut - -sub equals -{ - my ($self, $compare_object) = @_; - # make sure that the compare_object is legitimate - return 0 unless $compare_object && ref $compare_object && $compare_object->isa('Insteon::BaseInterface'); - return 1 if $compare_object eq $self; - # if they don't both have device_ids then treat them as identical - return 1 unless $compare_object->device_id && $self->device_id; - if ($compare_object->device_id eq $self->device_id) - { - return 1; - } - else - { - return 0; - } -} - -=item C - -Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. - -=cut - -sub debuglevel -{ - my ($self, $debug_level, $debug_group) = @_; - return Generic_Item::debuglevel($self, $debug_level, $debug_group); +=head1 B + +=head2 SYNOPSIS + +Provides support for the Insteon Interface. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::BaseInterface; + +use strict; +use Insteon::Message; +@Insteon::BaseInterface::ISA = ('Class::Singleton'); + +=item C + +Locates the active_interface from the main Insteon class and calls +C on it. Called once per loop to get data from the PLM. + +=cut + +sub check_for_data +{ + my $interface = &Insteon::active_interface(); + $interface->check_for_data(); +} + +=item C + +Called on startup or reload. Will always request and print the plm_info, which +contains the PLM revision number, to the log on startup. + +If Insteon_PLM_scan_at_startup is set to 1 in the ini file, this routine will poll +all insteon devices and request their current state. Useful for making sure that +no devices changed their state while MisterHouse was off. Will also call +L on each device to ensure that +the proper ALDB object is created for them. + +=cut + +sub poll_all +{ + my $scan_at_startup = $main::config_parms{Insteon_PLM_scan_at_startup}; + $scan_at_startup = 1 unless defined $scan_at_startup; + $scan_at_startup = 0 unless $main::Save{mh_exit} eq 'normal'; + my $plm = &Insteon::active_interface(); + if (defined $plm) + { + if (!($plm->device_id) and !($$plm{_id_check})) + { + $$plm{_id_check} = 1; + $plm->queue_message(new Insteon::InsteonMessage('plm_info', $plm)); + } + if ($scan_at_startup) + { + + for my $insteon_device (&Insteon::find_members('Insteon::BaseDevice')) + { + if ($insteon_device and $insteon_device->is_root and $insteon_device->is_responder) + { + # don't request status for objects associated w/ other than the primary group + # as they are psuedo links + $insteon_device->get_engine_version(); + $insteon_device->request_status(); + } + } + } + } +} + +=item C + +Instantiate a new object. + +=cut + +sub new +{ + my ($class) = @_; + + my $self = {}; + @{$$self{command_stack2}} = (); + @{$$self{command_history}} = (); + $$self{received_commands} = {}; + bless $self, $class; + $self->transmit_in_progress(0); +# $self->debug(0) unless $self->debug; + return $self; +} + +=item C + +Returns 1 if object is the same as $self, otherwise returns 0. + +=cut + +sub equals +{ + my ($self, $compare_object) = @_; + # make sure that the compare_object is legitimate + return 0 unless $compare_object && ref $compare_object && $compare_object->isa('Insteon::BaseInterface'); + return 1 if $compare_object eq $self; + # if they don't both have device_ids then treat them as identical + return 1 unless $compare_object->device_id && $self->device_id; + if ($compare_object->device_id eq $self->device_id) + { + return 1; + } + else + { + return 0; + } +} + +=item C + +Returns 1 if Insteon or this device is at least debug level 'level', otherwise returns 0. + +=cut + +sub debuglevel +{ + my ($self, $debug_level, $debug_group) = @_; + return Generic_Item::debuglevel($self, $debug_level, $debug_group); } - -=item C<_is_duplicate(cmd)> - -Returns true if cmd already exists in the command stack. - -=cut - -sub _is_duplicate -{ - my ($self, $cmd) = @_; - return 1 if ($self->active_message && $self->active_message->interface_data eq $cmd); - my $duplicate_detected = 0; - # check for duplicates of $cmd already in command_stack and ignore if they exist - foreach my $message (@{$$self{command_stack2}}) - { - if ($message->interface_data eq $cmd) - { - $duplicate_detected = 1; - last; - } - } - return $duplicate_detected; -} - -=item C - -If a device has an ALDB, passes link_details onto one of the has_link() routines -within L. Generally called as part of C. - -=cut - -sub has_link -{ - my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; - if ($self->_aldb) - { - return $self->_aldb->has_link($insteon_object, $group, $is_controller, $subaddress); - } - return 0; -} - -=item C - -If a device has an ALDB, passes link_details onto one of the add_link() routines -within L. Generally called from the "sync links" or -"link to interface" voice commands. - -=cut - -sub add_link -{ - my ($self, $parms_text) = @_; - if ($self->_aldb) - { - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - $self->_aldb->add_link(%link_parms); - } -} - -=item C - -If a device has an ALDB, passes link_details onto one of the delete_link() routines -within L. Generally called by C. - -=cut - -sub delete_link -{ - my ($self, $parms_text) = @_; - if ($self->_aldb) - { - my %link_parms; - if (@_ > 2) - { - shift @_; - %link_parms = @_; - } - else - { - %link_parms = &main::parse_func_parms($parms_text); - } - $self->_aldb->delete_link(%link_parms); - } -} - -=item C - -Optionally stores, and returns the message currently being processed by the interface. - -=cut - -sub active_message -{ - my ($self, $message) = @_; - if (defined $message) - { - $$self{active_message} = $message; - } - return $$self{active_message}; -} - -=item C - -Clears the message currently being processed by the interface, and sets the -transmit in progress flag to false. - -=cut - -sub clear_active_message -{ - my ($self) = @_; - $$self{active_message} = undef; - $self->transmit_in_progress(0); -} - -=item C - -Sets the transmit in progress flag to false. - -=cut - -sub retry_active_message -{ - my ($self) = @_; - $self->transmit_in_progress(0); -} - -=item C - -Sets the transmit in progress flag to xmit_flag, returns true if xmit_flag -true or xmit timout has not elapsed. - -=cut - -sub transmit_in_progress -{ - my ($self, $xmit_flag) = @_; - if (defined $xmit_flag) - { - $$self{xmit_in_progress} = $xmit_flag; - } - # also factor in xmit timer since this must be honored to allow - # adequate time to elapse - return $$self{xmit_in_progress} || ($self->_check_timeout('xmit')==0); -} - -=item C - -If no msg is passed, returns the queue length. - -Msg is optionally a message, if sent is added to the message queue. -C is then called. - -=cut - -sub queue_message -{ - my ($self, $message) = @_; - - my $command_queue_size = @{$$self{command_stack2}}; - return $command_queue_size unless $message; - - #queue any new command - if (defined $message) - { - my $setby = $message->setby; - if ($self->_is_duplicate($message->interface_data) && !($message->isa('Insteon::X10Message'))) - { - &main::print_log("[Insteon::BaseInterface] Attempt to queue command already in queue; skipping ...") if $self->debuglevel(1, 'insteon'); - } - else - { - if ($setby and ref($setby) and $setby->can('set_retry_timeout') - and $setby->get_object_name) - { - $message->callback($setby->get_object_name . "->set_retry_timeout()"); - } - unshift(@{$$self{command_stack2}}, $message); - } - } - # and, begin processing either this entry or the oldest one in the queue - $self->process_queue(); -} - -=item C - -If C is true returns queue size. - -If there is a pending message, will leave it as active_message. If retries are -exceeded, will log an error, clear the message, and call the message failure_callback. - -Else, will pull a message from the queue and place it as the active_message. - -=cut - -sub process_queue -{ - my ($self) = @_; - - my $command_queue_size = @{$$self{command_stack2}}; - - if ($self->transmit_in_progress) - { - return $command_queue_size; - } - else - #we dont transmit on top of another xmit - { # no transmission is progress that has not already been acked or nacked by the PLM - # get pending command record - my $pending_message = $self->active_message; - - if (!($pending_message)) - { # no prior message remains; so, get one from the queue - $pending_message = pop(@{$$self{command_stack2}}); - $self->active_message($pending_message) if $pending_message; - } - - if ($pending_message) - { # a message exists to be sent (whether previously sent or queued) - - if ($self->active_message->send($self) == 0) - { # this only occurs if the retry count has been exceeded - # which also means that there wasn't a message actually sent - &::print_log("[Insteon::BaseInterface] WARN: number of retries (" - . $self->active_message->send_attempts - . ") for " . $self->active_message->to_string() - . " exceeds limit. Now moving on...") if $self->debuglevel(1, 'insteon'); - # !!!!!!!!! TO-DO - handle failure timeout ??? - my $failed_message = $self->active_message; - # make sure to let the sending object know!!! - if (defined($failed_message->setby) and $failed_message->setby->can('is_acknowledged')) - { - $failed_message->setby->is_acknowledged(0); - $failed_message->setby->fail_count_log(1) - if $failed_message->setby->can('fail_count_log'); - } - else - { - &main::print_log("[Insteon::BaseInterface] WARN! Unable to clear acknowledge for " - . ((defined($failed_message->setby)) ? $failed_message->setby->get_object_name : "undefined")); - } - - # Get failed message details before clearing - my $callback = $failed_message->failure_callback; - my $setby = $failed_message->setby; - # clear active message - $self->clear_active_message(); - - if ($callback) - { - &::print_log("[Insteon::BaseInterface] WARN: Message Timeout: Now calling callback: " . - $callback) if $self->debuglevel(1, 'insteon'); - $setby->failure_reason('timeout') - if (defined($setby) and $setby->can('failure_reason')); - package main; - eval $callback; - &::print_log("[Insteon::BaseInterface] problem w/ retry callback: $@") if $@; - package Insteon::BaseInterface; - } - - #Any other outgoing messages pending in the queue? - $self->process_queue(); - } - } - else # no pending message - { - # clear the timer - $self->_clear_timeout('command'); - return 0; - } - } - my $command_queue_size = @{$$self{command_stack2}}; - return $command_queue_size; -} - -=item C - -Used to store and return the associated device_id of a device. - -If provided, stores id as the device's id. - -Returns device id without any delimiters. - -=cut - -sub device_id { - my ($self, $p_deviceid) = @_; - $$self{deviceid} = $p_deviceid if defined $p_deviceid; - return $$self{deviceid}; -} - -=item C - -This is called by mh on exit to save the cached ALDB of a device to persistant data. - -=cut - -sub restore_string -{ - my ($self) = @_; - my $restore_string = $self->SUPER::restore_string(); - $restore_string .= $self->_aldb->restore_string(); - return $restore_string; -} - -=item C - -Used to reload the link table of a device on restart. - -=cut - -sub restore_linktable -{ - my ($self,$aldb) = @_; - if ($self->_aldb and $aldb) { - $self->_aldb->restore_linktable($aldb); - } -} - -=item C - -Prints a human readable form of MisterHouse's cached version of a device's ALDB -to the print log. Called as part of the "scan links" voice command -or in response to the "log links" voice command. - -=cut - -sub log_alllink_table -{ - my ($self) = @_; - $self->_aldb->log_alllink_table if $self->_aldb; -} - -=item C - -Reviews the cached version of all of the ALDBs and based on this review removes -links from this device which are not present in the mht file, not defined in the -code, or links which are only half-links. - -If audit_mode is true, prints the actions that would be taken to the log, but -does nothing. - -=cut - -sub delete_orphan_links -{ - my ($self, $audit_mode) = @_; - return $self->_aldb->delete_orphan_links($audit_mode) if $self->_aldb; -} - -###################### -### EVENT HANDLERS ### -###################### - -=item C - -Called to process the plm_info request sent by the C command. -Prints output to log. - -=cut - -sub on_interface_info_received -{ - my ($self) = @_; - &::print_log("[Insteon_PLM] PLM id: " . $self->device_id . - " firmware: " . $self->firmware) - if $self->debuglevel(1, 'insteon'); - $self->clear_active_message(); -} - -=item C - -Called to process standard length insteon messages. The routine is rather complex -some messsages are processed right here. The majority are passed off to the -C<_is_info_request()> and C<_process_message()> routines for each device. - -=cut - -sub on_standard_insteon_received -{ - my ($self, $message_data) = @_; - my %msg = &Insteon::InsteonMessage::command_to_hash($message_data); - return if $self->_is_duplicate_received($message_data, %msg); - if (%msg) - { - my $wait_time; - my $wait_message = "[Insteon::BaseInterface] DEBUG3: Message received " - ."with $msg{hopsleft} hops left, "; - if (!$msg{is_ack} && !$msg{is_nack} && $msg{type} ne 'alllink' - && $msg{type} ne 'broadcast') { - #Wait for ACK to be delivered - $wait_time = $msg{maxhops}; - $wait_message .= "plus ACK will take $msg{maxhops} to deliver, "; - } - $wait_time += $msg{hopsleft}; - #Standard msgs should only take 50 millis, but in practice additional - #time has been required. Extra 50 millis helps prevent dupes - $wait_time = ($wait_time * 100) + 50; - $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; - ::print_log($wait_message) if ($self->debuglevel(3, 'insteon') && $wait_time > 50); - $self->_set_timeout('xmit', $wait_time); - - # get the matching object - my $object = &Insteon::get_object($msg{source}, $msg{group}); - if (defined $object) - { - $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); - $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); - $object->incoming_count_log(1) if $object->can('incoming_count_log'); - if ($msg{type} ne 'broadcast') - { - $msg{command} = $object->message_type($msg{cmd_code}); - &::print_log("[Insteon::BaseInterface] Received message from: ". $object->get_object_name - ."; command: $msg{command}; type: $msg{type}; group: $msg{group}") - if (!($msg{is_ack} or $msg{is_nack})) and $self->debuglevel(1, 'insteon'); - } - if ($msg{is_ack} or $msg{is_nack}) - { - main::print_log("[Insteon::BaseInterface] DEBUG3: PLM command:insteon_received; " - . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") - if $self->debuglevel(3, 'insteon'); - # need to confirm that this message corresponds to the current active one before clearing it - # TO-DO!!! This is a brute force and poor compare technique; needs to be replaced by full compare - if ($self->active_message && ref $self->active_message->setby) - { - if ($self->active_message->send_attempts == 0) - { - &main::print_log("[Insteon::BaseInterface] WARN: received ACK/NACK message for " - . $object->get_object_name . " but cannot correlate to sent message " - . "(active but send attempts = 0). IGNORING received message!!"); - } - elsif ($msg{type} eq 'direct') - { - if (lc $self->active_message->setby->device_id eq lc $msg{source}) - { - # prevent re-processing transmit queue until after clearing occurs - $self->transmit_in_progress(1); - # ask the object to process the received message and update its state - # Object will return true if this is the end of the send transaction - if($object->_process_message($self, %msg)) { - if ($self->active_message->success_callback){ - main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $object->debuglevel(4, 'insteon'); - package main; - eval $self->active_message->success_callback; - ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; - package Insteon::BaseInterface; - } - $self->clear_active_message(); - } - } - else - { - &main::print_log("[Insteon::BaseInterface] WARN: deviceid of " - . "active message != received message source (" - . $object->get_object_name() . "). IGNORING received message!!"); - #These generally seem to be duplicate messages - $object->dupe_count_log(1) if $object->can('dupe_count_log'); - } - } - elsif ($msg{type} eq 'cleanup') - { - my $setby_object = $object; - $object = &Insteon::get_object('000000', $msg{extra}); - if ($object) - { - # prevent re-processing transmit queue until after clearing occurs - $self->transmit_in_progress(1); - # Don't clear active message as ACK is only one of many - if (($msg{extra} == $self->active_message->setby->group)){ - &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received for scene " - . $object->get_object_name . " from " . $setby_object->get_object_name) - if $object->debuglevel(3, 'insteon'); - } elsif ($self->active_message->command_type eq 'all_link_direct_cleanup' && - lc($self->active_message->setby->device_id) eq $msg{source}) - { - &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $object->debuglevel(2, 'insteon'); - $self->clear_active_message(); - } - else { - &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received from " - . $setby_object->get_object_name . " for scene " - . $object->get_object_name . ", but group in recent message " - . $msg{extra}. " did not match group in " - . "prior sent message group " . $self->active_message->setby->group) - if $object->debuglevel(3, 'insteon'); - } - # If ACK or NACK received then PLM is still working on the ALL Link Command - # Increase the command timeout to wait for next one - $self->_set_timeout('command', 3000); - } - else - { - &main::print_log("[Insteon::BaseInterface] ERROR: received cleanup message from " - . $setby_object->get_object_name . " that does not correspond to a valid PLM group. Corrupted message is assumed " - . "and will be skipped! Was group " . $msg{extra}); - $setby_object->corrupt_count_log(1) if $setby_object->can('corrupt_count_log'); - } - } - else #not direct or cleanup - { - &main::print_log("[Insteon::BaseInterface] ERROR: received ACK/NACK message from " - . $object->get_object_name . " but unable to process $msg{type} message type." - . " IGNORING received message!!"); - $self->active_message->no_hop_increase(1); - $object->corrupt_count_log(1) if $object->can('corrupt_count_log'); - } - } - else #does not correspond to current active message - { - if ($msg{type} eq 'direct') - { - &main::print_log("[Insteon::BaseInterface] WARN: received insteon ACK/NACK message from " - . $object->get_object_name . " but cannot correlate to sent message! IGNORING received message!!"); - } - elsif ($msg{type} eq 'cleanup') - { - # this is just going to be ignored since there is a virtual processing done - # in the Insteon_PLM handler for cleanup messages. - # however, if the virtual handler was not invoked due to receipt of the broadcast message - # then, the above cleanup handler would be run - my $plm_group_obj = Insteon::get_object('000000', $msg{extra}); - my $group_name = $msg{extra}; - $group_name = $plm_group_obj->get_object_name if (ref $plm_group_obj); - $plm_group_obj = $self if (!ref $plm_group_obj); - &main::print_log("[Insteon::BaseInterface] DEBUG3: received cleanup message responding to " - . "PLM controller group: $group_name. Ignoring as this has already been processed") - if $plm_group_obj->debuglevel(3, 'insteon'); - } - else - { - # ask the object to process the received message and update its state - $object->_process_message($self, %msg); - } - } - } - else # not ACK or NAK - { - # ask the object to process the received message and update its state - $object->_process_message($self, %msg); - } - } - else - { - &::print_log("[Insteon::BaseInterface] Warn! Unable to locate object for source: $msg{source} and group: $msg{group}"); - $self->corrupt_count_log(1); - } - # treat the message as legitimate even if an object match did not occur - } -} - -=item C - -Called to process extended length insteon messages. The majority of messages are -passed off to the C<_process_message()> routines for each device. - -=cut - - -sub on_extended_insteon_received -{ - my ($self, $message_data) = @_; - my %msg = &Insteon::InsteonMessage::command_to_hash($message_data); - return if $self->_is_duplicate_received($message_data, %msg); - if (%msg) - { - my $wait_time; - my $wait_message = "[Insteon::BaseInterface] DEBUG3: Message received " - ."with $msg{hopsleft} hops left, "; - if (!$msg{is_ack} && !$msg{is_nack} && $msg{type} ne 'alllink' - && $msg{type} ne 'broadcast') { - #Wait for ACK to be delivered - $wait_time = $msg{maxhops}; - $wait_message .= "plus ACK will take $msg{maxhops} to deliver, "; - } - $wait_time += $msg{hopsleft}; - #Standard msgs should only take 108 millis, but in practice additional - #time has been required. Extra 50 millis helps prevent dupes - $wait_time = ($wait_time * 200) + 50; - $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; - ::print_log($wait_message) if ($self->debuglevel(3, 'insteon') && $wait_time > 50); - $self->_set_timeout('xmit', $wait_time); - - # get the matching object - my $object = &Insteon::get_object($msg{source}, $msg{group}); - if (defined $object) - { - $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); - $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); - $object->incoming_count_log(1) if $object->can('incoming_count_log'); - if ($msg{type} ne 'broadcast') - { - $msg{command} = $object->message_type($msg{cmd_code}); - main::print_log("[Insteon::BaseInterface] DEBUG: PLM command:insteon_ext_received; " - . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") - if( (!($msg{is_ack} or $msg{is_nack}) and $self->debuglevel(1, 'insteon')) - or $self->debuglevel(3, 'insteon')); - } - &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $object->debuglevel(3, 'insteon'); - if($object->_process_message($self, %msg)) { - if (ref $self->active_message && $self->active_message->success_callback){ - main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $object->debuglevel(4, 'insteon'); - package main; - eval $self->active_message->success_callback; - ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; - package Insteon::BaseInterface; - } - $self->clear_active_message(); - } - } - else - { - &::print_log("[Insteon::BaseInterface] Warn! Unable to locate object for source: $msg{source} and group: $msg{group}"); - } - # treat the message as legitimate even if an object match did not occur - } - -} - -################################# -### INTERNAL METHODS/FUNCTION ### -################################# - -=item C<_set_timeout(timeout_name, timeout_millis)> - -Sets an internal variable, timeout_name, the current time plus the number of -milliseconds specified by timeout_millis. - -=cut - - -sub _set_timeout -{ - my ($self, $timeout_name, $timeout_in_millis) = @_; - my $tickcount = &main::get_tickcount + $timeout_in_millis; - $tickcount += 2**32 if $tickcount < 0; # force a wrap; to be handleded by check timeout - $$self{"_timeout_$timeout_name"} = $tickcount; -} - -=item C<_check_timeout(timeout_name)> - -Checks to see if the current number of milliseconds has exceeded the number of -milliseconds defined in timeout_name, which was set by C<_set_timeout()>. - -return -1 if timeout_name does not match an existing timer -return 0 if timer has not expired -return 1 if timer has expired - -=cut - -sub _check_timeout -{ - my ($self, $timeout_name) = @_; - return 0 unless $timeout_name; - return -1 unless defined $$self{"_timeout_$timeout_name"}; - my $current_tickcount = &main::get_tickcount; - return 0 if (($current_tickcount >= 2**16) and ($$self{"_timeout_$timeout_name"} < 2**16)); - return ($current_tickcount > $$self{"_timeout_$timeout_name"}) ? 1 : 0; -} - -=item C<_check_timeout(timeout_name)> - -Erases timeout_name, which was set by C<_set_timeout()>. - -=cut -sub _clear_timeout -{ - my ($self, $timeout_name) = @_; - $$self{"_timeout_$timeout_name"} = undef; -} - -=item C<_aldb()> - -Returns the ALDB object associated with the device. - -=cut - -sub _aldb -{ - my ($self) = @_; - return $$self{aldb}; -} - -=item C<_is_duplicate_received()> - -This function attempts to identify erroneous duplicative incoming messages -while still permitting identical messages to arrive in close proximity. For -example, a valid identical message is the ACK of an extended aldb read which -is always 2F00. - -Messages are deemed to be identical if, excluding the max_hops and hops_left -bits, they are otherwise the same. Identical messages are deemed to be -erroneous if they are received within a calculated message window, $delay. - -The message window is calculated based on the amount of time that should have -elapsed before a subsequent identical message could have been received.. - -Returns 1 if the received message is an erroneous duplicate message - -See discussion at: https://github.com/hollie/misterhouse/issues/169 - -=cut - -sub _is_duplicate_received { - my ($self, $message_data, %msg) = @_; - my $is_duplicate; - - my $curr_milli = sprintf('%.0f', &main::get_tickcount); - - # $key will be set to $message_data with max hops and hops left set to 0 - my $key = $message_data; - substr($key,13,1) = 0; - - #Standard = 50 millis; Extended = 108 millis; - #In practice requires 75% more - my $message_time = (length($message_data) > 18) ? 183 : 87; - - #Wait period before PLM can send ACK or next request - my $max_hops = $msg{hopsleft}; - - if (!$msg{is_ack} && !$msg{is_nack} && $msg{type} ne 'alllink' - && $msg{type} ne 'broadcast') - { - #ACK sent with same max hops plus 1 for initial timeslot - $max_hops += $msg{maxhops} + 1; - #Subsequent Reply, arrives in same number of hops + 1 for intial timeslot - $max_hops += ($msg{maxhops} - $msg{hopsleft}) + 1; - } else { - #Subsequent PLM request is sent with max hops + 1 for intial timeslot - $max_hops += $msg{maxhops} + 1; - } - - my $delay = ($message_time * $max_hops); - - #Clean hash of outdated entries - for (keys %{$$self{received_commands}}){ - if ($$self{received_commands}{$_} < $curr_milli){ - delete($$self{received_commands}{$_}); - } - } - - #Check if the message exists - if (exists($$self{received_commands}{$key})){ - $is_duplicate = 1; - #Reset the time in case there are multiple duplicates - $$self{received_commands}{$key} = $curr_milli + $delay; - #Make a nicer name - my $source = $msg{source}; - my $object = &Insteon::get_object($msg{source}, $msg{group}); - if (defined $object) { - $source = $object->get_object_name(); - $object->dupe_count_log(1) if $object->can('dupe_count_log'); - $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); - $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); - $object->incoming_count_log(1) if $object->can('incoming_count_log'); - #This message still provides a data point on how many hops it is - #taking for messages to arrive. - $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); - ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " - . $message_data . ", from ". $object->get_object_name) if $object->debuglevel(1, 'insteon'); - } - else { - ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " - . " from an unknown device. Id: $msg{source} Grp: $msg{group}") if $main::Debug{'insteon'}; - } - } else { - #Message was not in hash, so add it - $$self{received_commands}{$key} = $curr_milli + $delay; - } - return $is_duplicate; -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - 'complete linking as responder' => "$object_name->complete_linking_as_responder", - 'initiate linking as controller' => "$object_name->initiate_linking_as_controller", - 'initiate unlinking' => "$object_name->initiate_unlinking_as_controller", - 'cancel linking' => "$object_name->cancel_linking", - 'log links' => "$object_name->log_alllink_table", - 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", - 'scan changed device link tables' => "Insteon::scan_all_linktables(1)", - 'delete orphan links' => "$object_name->delete_orphan_links", - 'AUDIT - delete orphan links' => "$object_name->delete_orphan_links(1)", - 'scan all device link tables' => "Insteon::scan_all_linktables", - 'sync all links' => "Insteon::sync_all_links(0)", - 'AUDIT - sync all links' => "Insteon::sync_all_links(1)", - 'print all message stats' => "Insteon::print_all_message_stats", - 'reset all message stats' => "Insteon::reset_all_message_stats", - 'stress test ALL devices' => "Insteon::stress_test_all(5,1)", - 'ping test ALL devices' => "Insteon::ping_all(5)", - 'log all device ALDB status' => "Insteon::log_all_ADLB_status" - ); - return \%voice_cmds; -} - -=item C - -Returns false. - -=cut - -sub is_deaf -{ - return 0; -} - -=item C - -Returns true. - -=cut - -sub is_controller -{ - return 1; -} - -=item C - -Returns true. - -=cut - -sub is_responder -{ - return 1; -} - - -=back - -=head2 INI PARAMETERS - -=over - -=item Insteon_PLM_scan_at_startup - -By default, MisterHouse will scan all devices at startup. This scan involves -asking each device for its current state and asking each device for its engine -version. In a larger network this can take a few seconds to complete and it does -send a lot of messages all at once, but polling at startup is a good way to make -sure that MisterHouse has an accurate understanding of the network. - -If set to false, will disable the scan at startup. - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -1 + +=item C<_is_duplicate(cmd)> + +Returns true if cmd already exists in the command stack. + +=cut + +sub _is_duplicate +{ + my ($self, $cmd) = @_; + return 1 if ($self->active_message && $self->active_message->interface_data eq $cmd); + my $duplicate_detected = 0; + # check for duplicates of $cmd already in command_stack and ignore if they exist + foreach my $message (@{$$self{command_stack2}}) + { + if ($message->interface_data eq $cmd) + { + $duplicate_detected = 1; + last; + } + } + return $duplicate_detected; +} + +=item C + +If a device has an ALDB, passes link_details onto one of the has_link() routines +within L. Generally called as part of C. + +=cut + +sub has_link +{ + my ($self, $insteon_object, $group, $is_controller, $subaddress) = @_; + if ($self->_aldb) + { + return $self->_aldb->has_link($insteon_object, $group, $is_controller, $subaddress); + } + return 0; +} + +=item C + +If a device has an ALDB, passes link_details onto one of the add_link() routines +within L. Generally called from the "sync links" or +"link to interface" voice commands. + +=cut + +sub add_link +{ + my ($self, $parms_text) = @_; + if ($self->_aldb) + { + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + $self->_aldb->add_link(%link_parms); + } +} + +=item C + +If a device has an ALDB, passes link_details onto one of the delete_link() routines +within L. Generally called by C. + +=cut + +sub delete_link +{ + my ($self, $parms_text) = @_; + if ($self->_aldb) + { + my %link_parms; + if (@_ > 2) + { + shift @_; + %link_parms = @_; + } + else + { + %link_parms = &main::parse_func_parms($parms_text); + } + $self->_aldb->delete_link(%link_parms); + } +} + +=item C + +Optionally stores, and returns the message currently being processed by the interface. + +=cut + +sub active_message +{ + my ($self, $message) = @_; + if (defined $message) + { + $$self{active_message} = $message; + } + return $$self{active_message}; +} + +=item C + +Clears the message currently being processed by the interface, and sets the +transmit in progress flag to false. + +=cut + +sub clear_active_message +{ + my ($self) = @_; + $$self{active_message} = undef; + $self->transmit_in_progress(0); +} + +=item C + +Sets the transmit in progress flag to false. + +=cut + +sub retry_active_message +{ + my ($self) = @_; + $self->transmit_in_progress(0); +} + +=item C + +Sets the transmit in progress flag to xmit_flag, returns true if xmit_flag +true or xmit timout has not elapsed. + +=cut + +sub transmit_in_progress +{ + my ($self, $xmit_flag) = @_; + if (defined $xmit_flag) + { + $$self{xmit_in_progress} = $xmit_flag; + } + # also factor in xmit timer since this must be honored to allow + # adequate time to elapse + return $$self{xmit_in_progress} || ($self->_check_timeout('xmit')==0); +} + +=item C + +If no msg is passed, returns the queue length. + +Msg is optionally a message, if sent is added to the message queue. +C is then called. + +=cut + +sub queue_message +{ + my ($self, $message) = @_; + + my $command_queue_size = @{$$self{command_stack2}}; + return $command_queue_size unless $message; + + #queue any new command + if (defined $message) + { + my $setby = $message->setby; + if ($self->_is_duplicate($message->interface_data) && !($message->isa('Insteon::X10Message'))) + { + &main::print_log("[Insteon::BaseInterface] Attempt to queue command already in queue; skipping ...") if $self->debuglevel(1, 'insteon'); + } + else + { + if ($setby and ref($setby) and $setby->can('set_retry_timeout') + and $setby->get_object_name) + { + $message->callback($setby->get_object_name . "->set_retry_timeout()"); + } + unshift(@{$$self{command_stack2}}, $message); + } + } + # and, begin processing either this entry or the oldest one in the queue + $self->process_queue(); +} + +=item C + +If C is true returns queue size. + +If there is a pending message, will leave it as active_message. If retries are +exceeded, will log an error, clear the message, and call the message failure_callback. + +Else, will pull a message from the queue and place it as the active_message. + +=cut + +sub process_queue +{ + my ($self) = @_; + + my $command_queue_size = @{$$self{command_stack2}}; + + if ($self->transmit_in_progress) + { + return $command_queue_size; + } + else + #we dont transmit on top of another xmit + { # no transmission is progress that has not already been acked or nacked by the PLM + # get pending command record + my $pending_message = $self->active_message; + + if (!($pending_message)) + { # no prior message remains; so, get one from the queue + $pending_message = pop(@{$$self{command_stack2}}); + $self->active_message($pending_message) if $pending_message; + } + + if ($pending_message) + { # a message exists to be sent (whether previously sent or queued) + + if ($self->active_message->send($self) == 0) + { # this only occurs if the retry count has been exceeded + # which also means that there wasn't a message actually sent + &::print_log("[Insteon::BaseInterface] WARN: number of retries (" + . $self->active_message->send_attempts + . ") for " . $self->active_message->to_string() + . " exceeds limit. Now moving on...") if $self->debuglevel(1, 'insteon'); + # !!!!!!!!! TO-DO - handle failure timeout ??? + my $failed_message = $self->active_message; + # make sure to let the sending object know!!! + if (defined($failed_message->setby) and $failed_message->setby->can('is_acknowledged')) + { + $failed_message->setby->is_acknowledged(0); + $failed_message->setby->fail_count_log(1) + if $failed_message->setby->can('fail_count_log'); + } + else + { + &main::print_log("[Insteon::BaseInterface] WARN! Unable to clear acknowledge for " + . ((defined($failed_message->setby)) ? $failed_message->setby->get_object_name : "undefined")); + } + + # Get failed message details before clearing + my $callback = $failed_message->failure_callback; + my $setby = $failed_message->setby; + # clear active message + $self->clear_active_message(); + + if ($callback) + { + &::print_log("[Insteon::BaseInterface] WARN: Message Timeout: Now calling callback: " . + $callback) if $self->debuglevel(1, 'insteon'); + $setby->failure_reason('timeout') + if (defined($setby) and $setby->can('failure_reason')); + package main; + eval $callback; + &::print_log("[Insteon::BaseInterface] problem w/ retry callback: $@") if $@; + package Insteon::BaseInterface; + } + + #Any other outgoing messages pending in the queue? + $self->process_queue(); + } + } + else # no pending message + { + # clear the timer + $self->_clear_timeout('command'); + return 0; + } + } + my $command_queue_size = @{$$self{command_stack2}}; + return $command_queue_size; +} + +=item C + +Used to store and return the associated device_id of a device. + +If provided, stores id as the device's id. + +Returns device id without any delimiters. + +=cut + +sub device_id { + my ($self, $p_deviceid) = @_; + $$self{deviceid} = $p_deviceid if defined $p_deviceid; + return $$self{deviceid}; +} + +=item C + +This is called by mh on exit to save the cached ALDB of a device to persistant data. + +=cut + +sub restore_string +{ + my ($self) = @_; + my $restore_string = $self->SUPER::restore_string(); + $restore_string .= $self->_aldb->restore_string(); + return $restore_string; +} + +=item C + +Used to reload the link table of a device on restart. + +=cut + +sub restore_linktable +{ + my ($self,$aldb) = @_; + if ($self->_aldb and $aldb) { + $self->_aldb->restore_linktable($aldb); + } +} + +=item C + +Prints a human readable form of MisterHouse's cached version of a device's ALDB +to the print log. Called as part of the "scan links" voice command +or in response to the "log links" voice command. + +=cut + +sub log_alllink_table +{ + my ($self) = @_; + $self->_aldb->log_alllink_table if $self->_aldb; +} + +=item C + +Reviews the cached version of all of the ALDBs and based on this review removes +links from this device which are not present in the mht file, not defined in the +code, or links which are only half-links. + +If audit_mode is true, prints the actions that would be taken to the log, but +does nothing. + +=cut + +sub delete_orphan_links +{ + my ($self, $audit_mode) = @_; + return $self->_aldb->delete_orphan_links($audit_mode) if $self->_aldb; +} + +###################### +### EVENT HANDLERS ### +###################### + +=item C + +Called to process the plm_info request sent by the C command. +Prints output to log. + +=cut + +sub on_interface_info_received +{ + my ($self) = @_; + &::print_log("[Insteon_PLM] PLM id: " . $self->device_id . + " firmware: " . $self->firmware) + if $self->debuglevel(1, 'insteon'); + $self->clear_active_message(); +} + +=item C + +Called to process standard length insteon messages. The routine is rather complex +some messsages are processed right here. The majority are passed off to the +C<_is_info_request()> and C<_process_message()> routines for each device. + +=cut + +sub on_standard_insteon_received +{ + my ($self, $message_data) = @_; + my %msg = &Insteon::InsteonMessage::command_to_hash($message_data); + return if $self->_is_duplicate_received($message_data, %msg); + if (%msg) + { + my $wait_time; + my $wait_message = "[Insteon::BaseInterface] DEBUG3: Message received " + ."with $msg{hopsleft} hops left, "; + if (!$msg{is_ack} && !$msg{is_nack} && $msg{type} ne 'alllink' + && $msg{type} ne 'broadcast') { + #Wait for ACK to be delivered + $wait_time = $msg{maxhops}; + $wait_message .= "plus ACK will take $msg{maxhops} to deliver, "; + } + $wait_time += $msg{hopsleft}; + #Standard msgs should only take 50 millis, but in practice additional + #time has been required. Extra 50 millis helps prevent dupes + $wait_time = ($wait_time * 100) + 50; + $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; + ::print_log($wait_message) if ($self->debuglevel(3, 'insteon') && $wait_time > 50); + $self->_set_timeout('xmit', $wait_time); + + # get the matching object + my $object = &Insteon::get_object($msg{source}, $msg{group}); + if (defined $object) + { + $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); + $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); + $object->incoming_count_log(1) if $object->can('incoming_count_log'); + if ($msg{type} ne 'broadcast') + { + $msg{command} = $object->message_type($msg{cmd_code}); + &::print_log("[Insteon::BaseInterface] Received message from: ". $object->get_object_name + ."; command: $msg{command}; type: $msg{type}; group: $msg{group}") + if (!($msg{is_ack} or $msg{is_nack})) and $self->debuglevel(1, 'insteon'); + } + if ($msg{is_ack} or $msg{is_nack}) + { + main::print_log("[Insteon::BaseInterface] DEBUG3: PLM command:insteon_received; " + . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") + if $self->debuglevel(3, 'insteon'); + # need to confirm that this message corresponds to the current active one before clearing it + # TO-DO!!! This is a brute force and poor compare technique; needs to be replaced by full compare + if ($self->active_message && ref $self->active_message->setby) + { + if ($self->active_message->send_attempts == 0) + { + &main::print_log("[Insteon::BaseInterface] WARN: received ACK/NACK message for " + . $object->get_object_name . " but cannot correlate to sent message " + . "(active but send attempts = 0). IGNORING received message!!"); + } + elsif ($msg{type} eq 'direct') + { + if (lc $self->active_message->setby->device_id eq lc $msg{source}) + { + # prevent re-processing transmit queue until after clearing occurs + $self->transmit_in_progress(1); + # ask the object to process the received message and update its state + # Object will return true if this is the end of the send transaction + if($object->_process_message($self, %msg)) { + if ($self->active_message->success_callback){ + main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " + . $self->active_message->success_callback) if $object->debuglevel(4, 'insteon'); + package main; + eval $self->active_message->success_callback; + ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; + package Insteon::BaseInterface; + } + $self->clear_active_message(); + } + } + else + { + &main::print_log("[Insteon::BaseInterface] WARN: deviceid of " + . "active message != received message source (" + . $object->get_object_name() . "). IGNORING received message!!"); + #These generally seem to be duplicate messages + $object->dupe_count_log(1) if $object->can('dupe_count_log'); + } + } + elsif ($msg{type} eq 'cleanup') + { + my $setby_object = $object; + $object = &Insteon::get_object('000000', $msg{extra}); + if ($object) + { + # prevent re-processing transmit queue until after clearing occurs + $self->transmit_in_progress(1); + # Don't clear active message as ACK is only one of many + if (($msg{extra} == $self->active_message->setby->group)){ + &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received for scene " + . $object->get_object_name . " from " . $setby_object->get_object_name) + if $object->debuglevel(3, 'insteon'); + } elsif ($self->active_message->command_type eq 'all_link_direct_cleanup' && + lc($self->active_message->setby->device_id) eq $msg{source}) + { + &::print_log("[Insteon::BaseInterface] DEBUG2: ALL-Linking Direct Completed with ". $self->active_message->setby->get_object_name) if $object->debuglevel(2, 'insteon'); + $self->clear_active_message(); + } + else { + &main::print_log("[Insteon::BaseInterface] DEBUG3: Cleanup message received from " + . $setby_object->get_object_name . " for scene " + . $object->get_object_name . ", but group in recent message " + . $msg{extra}. " did not match group in " + . "prior sent message group " . $self->active_message->setby->group) + if $object->debuglevel(3, 'insteon'); + } + # If ACK or NACK received then PLM is still working on the ALL Link Command + # Increase the command timeout to wait for next one + $self->_set_timeout('command', 3000); + } + else + { + &main::print_log("[Insteon::BaseInterface] ERROR: received cleanup message from " + . $setby_object->get_object_name . " that does not correspond to a valid PLM group. Corrupted message is assumed " + . "and will be skipped! Was group " . $msg{extra}); + $setby_object->corrupt_count_log(1) if $setby_object->can('corrupt_count_log'); + } + } + else #not direct or cleanup + { + &main::print_log("[Insteon::BaseInterface] ERROR: received ACK/NACK message from " + . $object->get_object_name . " but unable to process $msg{type} message type." + . " IGNORING received message!!"); + $self->active_message->no_hop_increase(1); + $object->corrupt_count_log(1) if $object->can('corrupt_count_log'); + } + } + else #does not correspond to current active message + { + if ($msg{type} eq 'direct') + { + &main::print_log("[Insteon::BaseInterface] WARN: received insteon ACK/NACK message from " + . $object->get_object_name . " but cannot correlate to sent message! IGNORING received message!!"); + } + elsif ($msg{type} eq 'cleanup') + { + # this is just going to be ignored since there is a virtual processing done + # in the Insteon_PLM handler for cleanup messages. + # however, if the virtual handler was not invoked due to receipt of the broadcast message + # then, the above cleanup handler would be run + my $plm_group_obj = Insteon::get_object('000000', $msg{extra}); + my $group_name = $msg{extra}; + $group_name = $plm_group_obj->get_object_name if (ref $plm_group_obj); + $plm_group_obj = $self if (!ref $plm_group_obj); + &main::print_log("[Insteon::BaseInterface] DEBUG3: received cleanup message responding to " + . "PLM controller group: $group_name. Ignoring as this has already been processed") + if $plm_group_obj->debuglevel(3, 'insteon'); + } + else + { + # ask the object to process the received message and update its state + $object->_process_message($self, %msg); + } + } + } + else # not ACK or NAK + { + # ask the object to process the received message and update its state + $object->_process_message($self, %msg); + } + } + else + { + &::print_log("[Insteon::BaseInterface] Warn! Unable to locate object for source: $msg{source} and group: $msg{group}"); + $self->corrupt_count_log(1); + } + # treat the message as legitimate even if an object match did not occur + } +} + +=item C + +Called to process extended length insteon messages. The majority of messages are +passed off to the C<_process_message()> routines for each device. + +=cut + + +sub on_extended_insteon_received +{ + my ($self, $message_data) = @_; + my %msg = &Insteon::InsteonMessage::command_to_hash($message_data); + return if $self->_is_duplicate_received($message_data, %msg); + if (%msg) + { + my $wait_time; + my $wait_message = "[Insteon::BaseInterface] DEBUG3: Message received " + ."with $msg{hopsleft} hops left, "; + if (!$msg{is_ack} && !$msg{is_nack} && $msg{type} ne 'alllink' + && $msg{type} ne 'broadcast') { + #Wait for ACK to be delivered + $wait_time = $msg{maxhops}; + $wait_message .= "plus ACK will take $msg{maxhops} to deliver, "; + } + $wait_time += $msg{hopsleft}; + #Standard msgs should only take 108 millis, but in practice additional + #time has been required. Extra 50 millis helps prevent dupes + $wait_time = ($wait_time * 200) + 50; + $wait_message .= "delaying next transmit by $wait_time milliseconds to avoid collisions."; + ::print_log($wait_message) if ($self->debuglevel(3, 'insteon') && $wait_time > 50); + $self->_set_timeout('xmit', $wait_time); + + # get the matching object + my $object = &Insteon::get_object($msg{source}, $msg{group}); + if (defined $object) + { + $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); + $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); + $object->incoming_count_log(1) if $object->can('incoming_count_log'); + if ($msg{type} ne 'broadcast') + { + $msg{command} = $object->message_type($msg{cmd_code}); + main::print_log("[Insteon::BaseInterface] DEBUG: PLM command:insteon_ext_received; " + . "Device command:$msg{command}; type:$msg{type}; group: $msg{group}") + if( (!($msg{is_ack} or $msg{is_nack}) and $self->debuglevel(1, 'insteon')) + or $self->debuglevel(3, 'insteon')); + } + &::print_log("[Insteon::BaseInterface] Processing message for " . $object->get_object_name) if $object->debuglevel(3, 'insteon'); + if($object->_process_message($self, %msg)) { + if (ref $self->active_message && $self->active_message->success_callback){ + main::print_log("[Insteon::BaseInterface] DEBUG4: Now calling message success callback: " + . $self->active_message->success_callback) if $object->debuglevel(4, 'insteon'); + package main; + eval $self->active_message->success_callback; + ::print_log("[Insteon::BaseInterface] problem w/ success callback: $@") if $@; + package Insteon::BaseInterface; + } + $self->clear_active_message(); + } + } + else + { + &::print_log("[Insteon::BaseInterface] Warn! Unable to locate object for source: $msg{source} and group: $msg{group}"); + } + # treat the message as legitimate even if an object match did not occur + } + +} + +################################# +### INTERNAL METHODS/FUNCTION ### +################################# + +=item C<_set_timeout(timeout_name, timeout_millis)> + +Sets an internal variable, timeout_name, the current time plus the number of +milliseconds specified by timeout_millis. + +=cut + + +sub _set_timeout +{ + my ($self, $timeout_name, $timeout_in_millis) = @_; + my $tickcount = &main::get_tickcount + $timeout_in_millis; + $tickcount += 2**32 if $tickcount < 0; # force a wrap; to be handleded by check timeout + $$self{"_timeout_$timeout_name"} = $tickcount; +} + +=item C<_check_timeout(timeout_name)> + +Checks to see if the current number of milliseconds has exceeded the number of +milliseconds defined in timeout_name, which was set by C<_set_timeout()>. + +return -1 if timeout_name does not match an existing timer +return 0 if timer has not expired +return 1 if timer has expired + +=cut + +sub _check_timeout +{ + my ($self, $timeout_name) = @_; + return 0 unless $timeout_name; + return -1 unless defined $$self{"_timeout_$timeout_name"}; + my $current_tickcount = &main::get_tickcount; + return 0 if (($current_tickcount >= 2**16) and ($$self{"_timeout_$timeout_name"} < 2**16)); + return ($current_tickcount > $$self{"_timeout_$timeout_name"}) ? 1 : 0; +} + +=item C<_check_timeout(timeout_name)> + +Erases timeout_name, which was set by C<_set_timeout()>. + +=cut +sub _clear_timeout +{ + my ($self, $timeout_name) = @_; + $$self{"_timeout_$timeout_name"} = undef; +} + +=item C<_aldb()> + +Returns the ALDB object associated with the device. + +=cut + +sub _aldb +{ + my ($self) = @_; + return $$self{aldb}; +} + +=item C<_is_duplicate_received()> + +This function attempts to identify erroneous duplicative incoming messages +while still permitting identical messages to arrive in close proximity. For +example, a valid identical message is the ACK of an extended aldb read which +is always 2F00. + +Messages are deemed to be identical if, excluding the max_hops and hops_left +bits, they are otherwise the same. Identical messages are deemed to be +erroneous if they are received within a calculated message window, $delay. + +The message window is calculated based on the amount of time that should have +elapsed before a subsequent identical message could have been received.. + +Returns 1 if the received message is an erroneous duplicate message + +See discussion at: https://github.com/hollie/misterhouse/issues/169 + +=cut + +sub _is_duplicate_received { + my ($self, $message_data, %msg) = @_; + my $is_duplicate; + + my $curr_milli = sprintf('%.0f', &main::get_tickcount); + + # $key will be set to $message_data with max hops and hops left set to 0 + my $key = $message_data; + substr($key,13,1) = 0; + + #Standard = 50 millis; Extended = 108 millis; + #In practice requires 75% more + my $message_time = (length($message_data) > 18) ? 183 : 87; + + #Wait period before PLM can send ACK or next request + my $max_hops = $msg{hopsleft}; + + if (!$msg{is_ack} && !$msg{is_nack} && $msg{type} ne 'alllink' + && $msg{type} ne 'broadcast') + { + #ACK sent with same max hops plus 1 for initial timeslot + $max_hops += $msg{maxhops} + 1; + #Subsequent Reply, arrives in same number of hops + 1 for intial timeslot + $max_hops += ($msg{maxhops} - $msg{hopsleft}) + 1; + } else { + #Subsequent PLM request is sent with max hops + 1 for intial timeslot + $max_hops += $msg{maxhops} + 1; + } + + my $delay = ($message_time * $max_hops); + + #Clean hash of outdated entries + for (keys %{$$self{received_commands}}){ + if ($$self{received_commands}{$_} < $curr_milli){ + delete($$self{received_commands}{$_}); + } + } + + #Check if the message exists + if (exists($$self{received_commands}{$key})){ + $is_duplicate = 1; + #Reset the time in case there are multiple duplicates + $$self{received_commands}{$key} = $curr_milli + $delay; + #Make a nicer name + my $source = $msg{source}; + my $object = &Insteon::get_object($msg{source}, $msg{group}); + if (defined $object) { + $source = $object->get_object_name(); + $object->dupe_count_log(1) if $object->can('dupe_count_log'); + $object->max_hops_count($msg{maxhops}) if $object->can('max_hops_count'); + $object->hops_left_count($msg{hopsleft}) if $object->can('hops_left_count'); + $object->incoming_count_log(1) if $object->can('incoming_count_log'); + #This message still provides a data point on how many hops it is + #taking for messages to arrive. + $object->default_hop_count($msg{maxhops}-$msg{hopsleft}) if $object->can('default_hop_count'); + ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " + . $message_data . ", from ". $object->get_object_name) if $object->debuglevel(1, 'insteon'); + } + else { + ::print_log("[Insteon::BaseInterface] WARN! Dropped duplicate incoming message " + . " from an unknown device. Id: $msg{source} Grp: $msg{group}") if $main::Debug{'insteon'}; + } + } else { + #Message was not in hash, so add it + $$self{received_commands}{$key} = $curr_milli + $delay; + } + return $is_duplicate; +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + 'complete linking as responder' => "$object_name->complete_linking_as_responder", + 'initiate linking as controller' => "$object_name->initiate_linking_as_controller", + 'initiate unlinking' => "$object_name->initiate_unlinking_as_controller", + 'cancel linking' => "$object_name->cancel_linking", + 'log links' => "$object_name->log_alllink_table", + 'scan link table' => "$object_name->scan_link_table(\"" . '\$self->log_alllink_table' . "\")", + 'scan changed device link tables' => "Insteon::scan_all_linktables(1)", + 'delete orphan links' => "$object_name->delete_orphan_links", + 'AUDIT - delete orphan links' => "$object_name->delete_orphan_links(1)", + 'scan all device link tables' => "Insteon::scan_all_linktables", + 'sync all links' => "Insteon::sync_all_links(0)", + 'AUDIT - sync all links' => "Insteon::sync_all_links(1)", + 'print all message stats' => "Insteon::print_all_message_stats", + 'reset all message stats' => "Insteon::reset_all_message_stats", + 'stress test ALL devices' => "Insteon::stress_test_all(5,1)", + 'ping test ALL devices' => "Insteon::ping_all(5)", + 'log all device ALDB status' => "Insteon::log_all_ADLB_status" + ); + return \%voice_cmds; +} + +=item C + +Returns false. + +=cut + +sub is_deaf +{ + return 0; +} + +=item C + +Returns true. + +=cut + +sub is_controller +{ + return 1; +} + +=item C + +Returns true. + +=cut + +sub is_responder +{ + return 1; +} + + +=back + +=head2 INI PARAMETERS + +=over + +=item Insteon_PLM_scan_at_startup + +By default, MisterHouse will scan all devices at startup. This scan involves +asking each device for its current state and asking each device for its engine +version. In a larger network this can take a few seconds to complete and it does +send a lot of messages all at once, but polling at startup is a good way to make +sure that MisterHouse has an accurate understanding of the network. + +If set to false, will disable the scan at startup. + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +1 diff --git a/lib/Insteon/Controller.pm b/lib/Insteon/Controller.pm index 4956549b1..f1ca08f7a 100644 --- a/lib/Insteon/Controller.pm +++ b/lib/Insteon/Controller.pm @@ -1,376 +1,376 @@ -=head1 B - -=head2 SYNOPSIS - -Configuration: - -Depending on your device and your settings, your remote may offer 1, 4, or 8 -groups. Your configuration should vary depeninding on you remote style. - -In user code: - - use Insteon::RemoteLinc; - $remote_1 = new Insteon::RemoteLinc('12.34.56:01',$myPLM); - $remote_2 = new Insteon::RemoteLinc('12.34.56:02',$myPLM); - $remote_3 = new Insteon::RemoteLinc('12.34.56:03',$myPLM); - $remote_4 = new Insteon::RemoteLinc('12.34.56:04',$myPLM); - -In items.mht: - - INSTEON_REMOTELINC, 12.34.56:01, remote_1, remote_group - INSTEON_REMOTELINC, 12.34.56:02, remote_2, remote_group - INSTEON_REMOTELINC, 12.34.56:03, remote_3, remote_group - INSTEON_REMOTELINC, 12.34.56:04, remote_4, remote_group - -=head2 DESCRIPTION - -Provides basic support for Insteon RemoteLinc models 1 and 2. Basic support -includes, linking and receiving set commands from the device. More advanced -support is offered for RemoteLinc 2 in the form of battery level notifications. - -MisterHouse is only able to communicate with a RemoteLinc when it is in "awake -mode." The device is in "awake mode" while in its linking state. To put the -RemoteLinc into "awake mode", follow the instructions for placing the device into -linking mode. In short, the instructions are to hold down the set button for 4-10 -seconds until you hear a beep and see the light flash. The RemoteLinc will now -remain in "awake mode" for approximately 4 minutes. - -To scan the link table, sync links, or set settings on the device, the RemoteLinc -must first be put into "awake mode." - -=head2 INHERITS - -L, -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::RemoteLinc; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::RemoteLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController', 'Insteon::MultigroupDevice'); - -my %message_types = ( - %Insteon::BaseDevice::message_types, - bright => 0x15, - dim => 0x16 -); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); - $$self{message_types} = \%message_types; - if ($self->is_root){ - $self->restore_data('battery_timer', 'last_battery_time'); - $$self{queue_timer} = new Timer; - } - bless $self,$class; - $$self{is_responder} = 0; - $$self{is_deaf} = 1; - return $self; -} - -=item C - -Only available for RemoteLinc 2 models. - -Sets the amount of time, in seconds, that the RemoteLinc will remain "awake" -after sending a command. MH uses the awake time to send battery level requests -to the device. If the device is not responding to the battery level requests, -consider increasing this value. However, keep in mind that a longer awake time -will result in more battery usage. - -The factory setting is 4 seconds, 10 seconds seems to work well with MisterHouse -without causing adverse battery drain. - -=cut - -sub set_awake_time { - my ($self, $awake) = @_; - $awake = sprintf("%02x", $awake); - my $root = $self->get_root(); - my $extra = '000102' . $awake . '0000000000000000000000'; - $$root{_ext_set_get_action} = "set"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $root, 'extended_set_get', $extra); - $root->_send_cmd($message); - return; -} - -=item C - -Only available for RemoteLinc 2 models. - -Requests the status of various settings on the device. Currently this is only -used to obtain the battery level. If the device is awake, the battery level -will be printed to the log. - -You likely do not need to directly call this message, rather MisterHouse will issue -this request when it sees activity from the device and the C has -expired. - -=cut - -sub get_extended_info { - my ($self,$no_retry) = @_; - my $root = $self->get_root(); - my $extra = '000100000000000000000000000000'; - $$root{_ext_set_get_action} = "get"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $root, 'extended_set_get', $extra); - if ($no_retry){ - $message->retry_count(1); - } - $root->_send_cmd($message); - return; -} - -=item C - -Only available for RemoteLinc 2 models. - -Sets the minimum amount of time between battery level requests. When this time -expires, Misterhouse will request the battery level from the device the next time -MisterHouse sees activity from the device. Misterhouse will continue to request -the battery level until it gets a response from the device. - -Setting to 0 will disable automatic battery level requests. 1440 equals a day. - -This setting will be saved between MisterHouse reboots. - -=cut - -sub set_battery_timer { - my ($self, $minutes) = @_; - my $root = $self->get_root(); - $$root{battery_timer} = sprintf("%u", $minutes); - ::print_log("[Insteon::RemoteLinc] Set battery timer to ". - $$root{battery_timer}." minutes"); - return; -} - -=item C<_is_battery_time_expired()> - -Returns true if the battery timer has expired, else returns false. - -=cut - -sub _is_battery_time_expired { - my ($self) = @_; - my $root = $self->get_root(); - if ($$root{battery_timer} > 0 && - (time - $$root{last_battery_time}) > ($$root{battery_timer} * 60)) { - return 1; - } - return 0; -} - -=item C<_process_message()> - -Checks for and handles unique RemoteLinc messages such as battery voltage messages. -All other messages are transferred to L. - -Also checks the battery timer and sends a battery request if needed. - -=cut - -sub _process_message { - my ($self,$p_setby,%msg) = @_; - my $clear_message = 0; - my $root = $self->get_root(); - if ($root->_is_battery_time_expired){ - #Queue an get_extended_info request - if ($$root{queue_timer}->active){ - $$root{queue_timer}->restart(); - } - else { - $$root{queue_timer}->set(3, '$root->get_extended_info(1)'); - } - } - my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; - my $ack_setby = (ref $$self{m_status_request_pending}) ? $$self{m_status_request_pending} : $p_setby; - if ($msg{is_ack} && $self->_is_info_request($pending_cmd,$ack_setby,%msg)) { - $clear_message = 1; - $$self{m_status_request_pending} = 0; - $self->_process_command_stack(%msg); - } - elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ - $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - #If this was a get request don't clear until data packet received - main::print_log("[Insteon::RemoteLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); - if ($$self{_ext_set_get_action} eq 'set'){ - main::print_log("[Insteon::RemoteLinc] Clearing active message") if $self->debuglevel(1, 'insteon'); - $clear_message = 1; - $$self{_ext_set_get_action} = undef; - $self->_process_command_stack(%msg); - } - } - elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { - if (substr($msg{extra},0,6) eq "000001") { - $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); - #D10 = Battery; - my $voltage = (hex(substr($msg{extra}, 20, 2))/50); - main::print_log("[Insteon::RemoteLinc] The battery level ". - "for device ". $self->get_object_name . " is: ". - $voltage . " of 3.70 volts."); - $$root{last_battery_time} = time; - if (ref $$root{battery_object} && $$root{battery_object}->can('set_receive')) - { - $$root{battery_object}->set_receive($voltage, $root); - } - $clear_message = 1; - $self->_process_command_stack(%msg); - } else { - main::print_log("[Insteon::RemoteLinc] WARN: Corrupt Extended " - ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); - } - } - else { - $clear_message = $self->SUPER::_process_message($p_setby,%msg); - } - return $clear_message; -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds} - ); - if ($self->is_root){ - %voice_cmds = ( - %voice_cmds, - 'sync all device links' => "$object_name->sync_all_links()", - 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" - ); - } - return \%voice_cmds; -} - -=back - -=head2 AUTHOR - -Gregg Liming / gregg@limings.net, Kevin Robert Keegan - -=head2 LICENSE - -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 B - -=head2 SYNOPSIS - -Configuration: - -Currently the object can only be defined in the user code. - -In user code: - - use Insteon::RemoteLinc_Battery; - $remote_battery = new Insteon::RemoteLinc_Battery($remote); - -Where $remote is the RemoteLinc device you wish to monitor. - -=head2 DESCRIPTION - -This basic class creates a simple object that displays the current battery voltage -as its state. This is helpful if you want to be able to view the battery level -through a web page. Battery level tracking is likely only available on RemoteLinc 2 -devices. - -This object's state will be updated based on interval defined for C -in the parent B object. - -Once created, you can tie_events directly to this object, for example to alert -you when the battery is low. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::RemoteLinc_Battery; -use strict; - -@Insteon::RemoteLinc_Battery::ISA = ('Generic_Item'); - -=item C - -Instantiates a new object. - -=cut - -sub new { - my ($class, $parent) = @_; - my $self = new Generic_Item(); - my $root = $parent->get_root(); - bless $self, $class; - $$root{battery_object} = $self; - return $self; -} - -=item C - -Receives voltage messages from the parent object and sets the state of this -device accordingly. - -=cut - -sub set_receive { - my ($self, $p_state) = @_; - $self->SUPER::set($p_state); -} - -=back - -=head2 AUTHOR - -Kevin Robert Keegan - -=head2 LICENSE - -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. - -=cut +=head1 B + +=head2 SYNOPSIS + +Configuration: + +Depending on your device and your settings, your remote may offer 1, 4, or 8 +groups. Your configuration should vary depeninding on you remote style. + +In user code: + + use Insteon::RemoteLinc; + $remote_1 = new Insteon::RemoteLinc('12.34.56:01',$myPLM); + $remote_2 = new Insteon::RemoteLinc('12.34.56:02',$myPLM); + $remote_3 = new Insteon::RemoteLinc('12.34.56:03',$myPLM); + $remote_4 = new Insteon::RemoteLinc('12.34.56:04',$myPLM); + +In items.mht: + + INSTEON_REMOTELINC, 12.34.56:01, remote_1, remote_group + INSTEON_REMOTELINC, 12.34.56:02, remote_2, remote_group + INSTEON_REMOTELINC, 12.34.56:03, remote_3, remote_group + INSTEON_REMOTELINC, 12.34.56:04, remote_4, remote_group + +=head2 DESCRIPTION + +Provides basic support for Insteon RemoteLinc models 1 and 2. Basic support +includes, linking and receiving set commands from the device. More advanced +support is offered for RemoteLinc 2 in the form of battery level notifications. + +MisterHouse is only able to communicate with a RemoteLinc when it is in "awake +mode." The device is in "awake mode" while in its linking state. To put the +RemoteLinc into "awake mode", follow the instructions for placing the device into +linking mode. In short, the instructions are to hold down the set button for 4-10 +seconds until you hear a beep and see the light flash. The RemoteLinc will now +remain in "awake mode" for approximately 4 minutes. + +To scan the link table, sync links, or set settings on the device, the RemoteLinc +must first be put into "awake mode." + +=head2 INHERITS + +L, +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::RemoteLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::RemoteLinc::ISA = ('Insteon::BaseDevice','Insteon::DeviceController', 'Insteon::MultigroupDevice'); + +my %message_types = ( + %Insteon::BaseDevice::message_types, + bright => 0x15, + dim => 0x16 +); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); + $$self{message_types} = \%message_types; + if ($self->is_root){ + $self->restore_data('battery_timer', 'last_battery_time'); + $$self{queue_timer} = new Timer; + } + bless $self,$class; + $$self{is_responder} = 0; + $$self{is_deaf} = 1; + return $self; +} + +=item C + +Only available for RemoteLinc 2 models. + +Sets the amount of time, in seconds, that the RemoteLinc will remain "awake" +after sending a command. MH uses the awake time to send battery level requests +to the device. If the device is not responding to the battery level requests, +consider increasing this value. However, keep in mind that a longer awake time +will result in more battery usage. + +The factory setting is 4 seconds, 10 seconds seems to work well with MisterHouse +without causing adverse battery drain. + +=cut + +sub set_awake_time { + my ($self, $awake) = @_; + $awake = sprintf("%02x", $awake); + my $root = $self->get_root(); + my $extra = '000102' . $awake . '0000000000000000000000'; + $$root{_ext_set_get_action} = "set"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $root, 'extended_set_get', $extra); + $root->_send_cmd($message); + return; +} + +=item C + +Only available for RemoteLinc 2 models. + +Requests the status of various settings on the device. Currently this is only +used to obtain the battery level. If the device is awake, the battery level +will be printed to the log. + +You likely do not need to directly call this message, rather MisterHouse will issue +this request when it sees activity from the device and the C has +expired. + +=cut + +sub get_extended_info { + my ($self,$no_retry) = @_; + my $root = $self->get_root(); + my $extra = '000100000000000000000000000000'; + $$root{_ext_set_get_action} = "get"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $root, 'extended_set_get', $extra); + if ($no_retry){ + $message->retry_count(1); + } + $root->_send_cmd($message); + return; +} + +=item C + +Only available for RemoteLinc 2 models. + +Sets the minimum amount of time between battery level requests. When this time +expires, Misterhouse will request the battery level from the device the next time +MisterHouse sees activity from the device. Misterhouse will continue to request +the battery level until it gets a response from the device. + +Setting to 0 will disable automatic battery level requests. 1440 equals a day. + +This setting will be saved between MisterHouse reboots. + +=cut + +sub set_battery_timer { + my ($self, $minutes) = @_; + my $root = $self->get_root(); + $$root{battery_timer} = sprintf("%u", $minutes); + ::print_log("[Insteon::RemoteLinc] Set battery timer to ". + $$root{battery_timer}." minutes"); + return; +} + +=item C<_is_battery_time_expired()> + +Returns true if the battery timer has expired, else returns false. + +=cut + +sub _is_battery_time_expired { + my ($self) = @_; + my $root = $self->get_root(); + if ($$root{battery_timer} > 0 && + (time - $$root{last_battery_time}) > ($$root{battery_timer} * 60)) { + return 1; + } + return 0; +} + +=item C<_process_message()> + +Checks for and handles unique RemoteLinc messages such as battery voltage messages. +All other messages are transferred to L. + +Also checks the battery timer and sends a battery request if needed. + +=cut + +sub _process_message { + my ($self,$p_setby,%msg) = @_; + my $clear_message = 0; + my $root = $self->get_root(); + if ($root->_is_battery_time_expired){ + #Queue an get_extended_info request + if ($$root{queue_timer}->active){ + $$root{queue_timer}->restart(); + } + else { + $$root{queue_timer}->set(3, '$root->get_extended_info(1)'); + } + } + my $pending_cmd = ($$self{_prior_msg}) ? $$self{_prior_msg}->command : $msg{command}; + my $ack_setby = (ref $$self{m_status_request_pending}) ? $$self{m_status_request_pending} : $p_setby; + if ($msg{is_ack} && $self->_is_info_request($pending_cmd,$ack_setby,%msg)) { + $clear_message = 1; + $$self{m_status_request_pending} = 0; + $self->_process_command_stack(%msg); + } + elsif ($msg{command} eq "extended_set_get" && $msg{is_ack}){ + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); + #If this was a get request don't clear until data packet received + main::print_log("[Insteon::RemoteLinc] Extended Set/Get ACK Received for " . $self->get_object_name) if $self->debuglevel(1, 'insteon'); + if ($$self{_ext_set_get_action} eq 'set'){ + main::print_log("[Insteon::RemoteLinc] Clearing active message") if $self->debuglevel(1, 'insteon'); + $clear_message = 1; + $$self{_ext_set_get_action} = undef; + $self->_process_command_stack(%msg); + } + } + elsif ($msg{command} eq "extended_set_get" && $msg{is_extended}) { + if (substr($msg{extra},0,6) eq "000001") { + $self->default_hop_count($msg{maxhops}-$msg{hopsleft}); + #D10 = Battery; + my $voltage = (hex(substr($msg{extra}, 20, 2))/50); + main::print_log("[Insteon::RemoteLinc] The battery level ". + "for device ". $self->get_object_name . " is: ". + $voltage . " of 3.70 volts."); + $$root{last_battery_time} = time; + if (ref $$root{battery_object} && $$root{battery_object}->can('set_receive')) + { + $$root{battery_object}->set_receive($voltage, $root); + } + $clear_message = 1; + $self->_process_command_stack(%msg); + } else { + main::print_log("[Insteon::RemoteLinc] WARN: Corrupt Extended " + ."Set/Get Data Received for ". $self->get_object_name) if $self->debuglevel(1, 'insteon'); + } + } + else { + $clear_message = $self->SUPER::_process_message($p_setby,%msg); + } + return $clear_message; +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" + ); + } + return \%voice_cmds; +} + +=back + +=head2 AUTHOR + +Gregg Liming / gregg@limings.net, Kevin Robert Keegan + +=head2 LICENSE + +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 B + +=head2 SYNOPSIS + +Configuration: + +Currently the object can only be defined in the user code. + +In user code: + + use Insteon::RemoteLinc_Battery; + $remote_battery = new Insteon::RemoteLinc_Battery($remote); + +Where $remote is the RemoteLinc device you wish to monitor. + +=head2 DESCRIPTION + +This basic class creates a simple object that displays the current battery voltage +as its state. This is helpful if you want to be able to view the battery level +through a web page. Battery level tracking is likely only available on RemoteLinc 2 +devices. + +This object's state will be updated based on interval defined for C +in the parent B object. + +Once created, you can tie_events directly to this object, for example to alert +you when the battery is low. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::RemoteLinc_Battery; +use strict; + +@Insteon::RemoteLinc_Battery::ISA = ('Generic_Item'); + +=item C + +Instantiates a new object. + +=cut + +sub new { + my ($class, $parent) = @_; + my $self = new Generic_Item(); + my $root = $parent->get_root(); + bless $self, $class; + $$root{battery_object} = $self; + return $self; +} + +=item C + +Receives voltage messages from the parent object and sets the state of this +device accordingly. + +=cut + +sub set_receive { + my ($self, $p_state) = @_; + $self->SUPER::set($p_state); +} + +=back + +=head2 AUTHOR + +Kevin Robert Keegan + +=head2 LICENSE + +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. + +=cut 1 diff --git a/lib/Insteon/Irrigation.pm b/lib/Insteon/Irrigation.pm index 686586780..fe7ca00d5 100755 --- a/lib/Insteon/Irrigation.pm +++ b/lib/Insteon/Irrigation.pm @@ -1,312 +1,312 @@ -=head1 B - -=head2 SYNOPSIS - -In user code: - - use Insteon::Irrigation; - $irrigation = new Insteon::Irrigation('12.34.56', $myPLM); - -In items.mht: - - INSTEON_IRRIGATION, 12.34.56, irrigation, Irrigation - -Creating the object: - - use Insteon::Irrigation; - $irrigation = new Insteon::Irrigation('12.34.56', $myPLM); - -Turning on a valve: - - $v_valve_on = new Voice_Cmd "Turn on valve [1,2,3,4,5,6,7,8]"; - if (my $valve = state_now $v_valve_on) { - $valve--; - set_valve $irrigation "0$valve", "on"; - } - -Turning off a valve: - - $v_valve_off = new Voice_Cmd "Turn off valve [1,2,3,4,5,6,7,8]"; - if (my $valve = state_now $v_valve_off) { - $valve--; - set_valve $irrigation "0$valve", "off"; - } - -Requesting valve status: - - $v_valve_status = new Voice_Cmd "Request valve status"; - if (state_now $v_valve_status) { - poll_valve_status $irrigation; - } - -=head2 DESCRIPTION - -Provides basic support for the EzFlora (aka EzRain) sprinkler controller. - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -use strict; -use Insteon::BaseInsteon; - -package Insteon::Irrigation; - -@Insteon::Irrigation::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); - -our %message_types = ( - %Insteon::BaseDevice::message_types, - sprinkler_control => 0x44, - sprinkler_valve_on => 0x40, - sprinkler_valve_off => 0x41, - sprinkler_program_on => 0x42, - sprinkler_program_off => 0x43, - sprinkler_timers_request => 0x45 -); - -# -------------------- START OF SUBROUTINES -------------------- -# -------------------------------------------------------------- - -=item C - -Instantiates a new object. - -=cut - -sub new { - my ($class, $p_deviceid, $p_interface) = @_; - - my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); - bless $self, $class; - $$self{active_valve_id} = undef; - $$self{active_program_number} = undef; - $$self{program_is_running} = undef; - $$self{pump_enabled} = undef; - $$self{valve_is_running} = undef; - $self->restore_data('active_valve_id', 'active_program_number', 'program_is_running', 'pump_enabled', 'valve_is_running'); - $$self{message_types} = \%message_types; - return $self; -} - -=item C - -Sends a message to the device requesting the valve status. The response from the -device is printed to the log and stores the result in memory. - -=cut - -sub poll_valve_status { - my ($self) = @_; - my $subcmd = '02'; - my $message = new Insteon::InsteonMessage('insteon_send', $self, 'sprinkler_control', $subcmd); - $self->_send_cmd($message); - return; -} - -=item C - -Used to directly control valves. Valve_id is a two digit number 00-07, -valve_state may be on or off. - -=cut - -sub set_valve { - my ($self, $valve_id, $state) = @_; - my $subcmd = $valve_id; - my $cmd = undef; - if ($state eq 'on') { - $cmd = 'sprinkler_valve_on'; - } elsif ($state eq 'off') { - $cmd = 'sprinkler_valve_off'; - } - unless ($cmd and $subcmd) { - &::print_log("Insteon::Irrigation] ERROR: You must specify a valve number and a valid state (ON or OFF)") - if $self->debuglevel(1, 'insteon'); - return; - } - my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); - $self->_send_cmd($message); - return; -} - -=item C - -Used to directly control programs. Program_id is a two digit number 00-03, -valve_state may be on or off. - -=cut - -sub set_program { - my ($self, $program_id, $state) = @_; - my $subcmd = $program_id; - my $cmd = undef; - if ($state eq 'on') { - $cmd = 'sprinkler_program_on'; - } elsif ($state eq 'off') { - $cmd = 'sprinkler_program_off'; - } - unless ($cmd and $subcmd) { - &::print_log("Insteon::Irrigation] ERROR: You must specify a program number and a valid state (ON or OFF)") - if $self->debuglevel(1, 'insteon'); - return; - } - my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); - $self->_send_cmd($message); - return; -} - -=item C - -Returns the active valve number identified by the device in response to the last -C request. - -=cut - -sub get_active_valve_id() { - my ($self) = @_; - return $$self{'active_valve_id'}; -} - -=item C - -Returns true if the active valve identified by the device in response to the last -C request is running. - -=cut - -sub get_valve_is_running() { - my ($self) = @_; - return $$self{'valve_is_running'}; -} - -=item C - -Returns the active program number identified by the device in response to the last -C request. - -=cut - -sub get_active_program_number() { - my ($self) = @_; - return $$self{'active_program_number'}; -} - -=item C - -Returns true if the active program identified by the device in response to the last -C request is running. - -=cut - -sub get_program_is_running() { - my ($self) = @_; - return $$self{'program_is_running'}; -} - -=item C - -Returns true if valve 8 is set to be a pump. In this setup, valve 8 will also -turn on when any other valve is enabled. Generally used if you have some sort -of water pump that runs to provide water to your sprinklers. - -=cut - -sub get_pump_enabled() { - my ($self) = @_; - return $$self{'pump_enabled'}; -} - -=item C - -Sends a request to the device asking for it to respond with the current timers. -It does not appear that there is code to interpret the response provided by the -device. - -=cut - -sub get_timers() { - my ($self) = @_; - my $cmd = 'sprinkler_timers_request'; - my $subcmd = 0x1; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, $cmd, $subcmd); - $self->_send_cmd($message); - return; -} - -=item C<_is_info_request()> - -Used to intercept and handle unique EZFlora messages, all others are passed on -to C. - -=cut - -sub _is_info_request { - my ($self, $cmd, $ack_setby, %msg) = @_; - my $is_info_request = 0; - if ($cmd eq 'sprinkler_control' - or $cmd eq 'sprinkler_valve_on' - or $cmd eq 'sprinkler_valve_off' - or $cmd eq 'sprinkler_program_on' - or $cmd eq 'sprinkler_program_off') { - $is_info_request = 1; - my $val = hex($msg{extra}); - &::print_log("[Insteon::Irrigation] Processing data for $cmd with value: $val") if $self->debuglevel(1, 'insteon'); - $$self{'active_valve_id'} = ($val & 7) + 1; - $$self{'active_program_number'} = (($val >> 3) & 3) + 1; - $$self{'program_is_running'} = ($val >> 5) & 1; - $$self{'pump_enabled'} = ($val >> 6) & 1; - $$self{'valve_is_running'} = ($val >> 7) & 1; - &::print_log("[Insteon::Irrigation] active_valve_id: $$self{'active_valve_id'}," - . " valve_is_running: $$self{'valve_is_running'}, active_program: $$self{'active_program_number'}," - . " program_is_running: $$self{'program_is_running'}, pump_enabled: $$self{'pump_enabled'}") if $self->debuglevel(1, 'insteon'); - } - else { - #Check if this was a generic info_request - $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); - } - return $is_info_request; - -} - -=item C - -This does nothing and returns 0, it prevents a request_status message, which the -device does not support, from being sent to the device. - -=cut - -# Overload methods we don't use, but would otherwise cause Insteon traffic. -sub request_status { return 0 } - -=back - -=head2 AUTHOR - -Gregg Liming -David Norwood -Evan P. Hall -Kevin Robert Keegan - -=head2 SEE ALSO - -L, -L - -=head2 LICENSE - -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. - -=cut - +=head1 B + +=head2 SYNOPSIS + +In user code: + + use Insteon::Irrigation; + $irrigation = new Insteon::Irrigation('12.34.56', $myPLM); + +In items.mht: + + INSTEON_IRRIGATION, 12.34.56, irrigation, Irrigation + +Creating the object: + + use Insteon::Irrigation; + $irrigation = new Insteon::Irrigation('12.34.56', $myPLM); + +Turning on a valve: + + $v_valve_on = new Voice_Cmd "Turn on valve [1,2,3,4,5,6,7,8]"; + if (my $valve = state_now $v_valve_on) { + $valve--; + set_valve $irrigation "0$valve", "on"; + } + +Turning off a valve: + + $v_valve_off = new Voice_Cmd "Turn off valve [1,2,3,4,5,6,7,8]"; + if (my $valve = state_now $v_valve_off) { + $valve--; + set_valve $irrigation "0$valve", "off"; + } + +Requesting valve status: + + $v_valve_status = new Voice_Cmd "Request valve status"; + if (state_now $v_valve_status) { + poll_valve_status $irrigation; + } + +=head2 DESCRIPTION + +Provides basic support for the EzFlora (aka EzRain) sprinkler controller. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +use strict; +use Insteon::BaseInsteon; + +package Insteon::Irrigation; + +@Insteon::Irrigation::ISA = ('Insteon::BaseDevice','Insteon::DeviceController'); + +our %message_types = ( + %Insteon::BaseDevice::message_types, + sprinkler_control => 0x44, + sprinkler_valve_on => 0x40, + sprinkler_valve_off => 0x41, + sprinkler_program_on => 0x42, + sprinkler_program_off => 0x43, + sprinkler_timers_request => 0x45 +); + +# -------------------- START OF SUBROUTINES -------------------- +# -------------------------------------------------------------- + +=item C + +Instantiates a new object. + +=cut + +sub new { + my ($class, $p_deviceid, $p_interface) = @_; + + my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); + bless $self, $class; + $$self{active_valve_id} = undef; + $$self{active_program_number} = undef; + $$self{program_is_running} = undef; + $$self{pump_enabled} = undef; + $$self{valve_is_running} = undef; + $self->restore_data('active_valve_id', 'active_program_number', 'program_is_running', 'pump_enabled', 'valve_is_running'); + $$self{message_types} = \%message_types; + return $self; +} + +=item C + +Sends a message to the device requesting the valve status. The response from the +device is printed to the log and stores the result in memory. + +=cut + +sub poll_valve_status { + my ($self) = @_; + my $subcmd = '02'; + my $message = new Insteon::InsteonMessage('insteon_send', $self, 'sprinkler_control', $subcmd); + $self->_send_cmd($message); + return; +} + +=item C + +Used to directly control valves. Valve_id is a two digit number 00-07, +valve_state may be on or off. + +=cut + +sub set_valve { + my ($self, $valve_id, $state) = @_; + my $subcmd = $valve_id; + my $cmd = undef; + if ($state eq 'on') { + $cmd = 'sprinkler_valve_on'; + } elsif ($state eq 'off') { + $cmd = 'sprinkler_valve_off'; + } + unless ($cmd and $subcmd) { + &::print_log("Insteon::Irrigation] ERROR: You must specify a valve number and a valid state (ON or OFF)") + if $self->debuglevel(1, 'insteon'); + return; + } + my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); + $self->_send_cmd($message); + return; +} + +=item C + +Used to directly control programs. Program_id is a two digit number 00-03, +valve_state may be on or off. + +=cut + +sub set_program { + my ($self, $program_id, $state) = @_; + my $subcmd = $program_id; + my $cmd = undef; + if ($state eq 'on') { + $cmd = 'sprinkler_program_on'; + } elsif ($state eq 'off') { + $cmd = 'sprinkler_program_off'; + } + unless ($cmd and $subcmd) { + &::print_log("Insteon::Irrigation] ERROR: You must specify a program number and a valid state (ON or OFF)") + if $self->debuglevel(1, 'insteon'); + return; + } + my $message = new Insteon::InsteonMessage('insteon_send', $self, $cmd, $subcmd); + $self->_send_cmd($message); + return; +} + +=item C + +Returns the active valve number identified by the device in response to the last +C request. + +=cut + +sub get_active_valve_id() { + my ($self) = @_; + return $$self{'active_valve_id'}; +} + +=item C + +Returns true if the active valve identified by the device in response to the last +C request is running. + +=cut + +sub get_valve_is_running() { + my ($self) = @_; + return $$self{'valve_is_running'}; +} + +=item C + +Returns the active program number identified by the device in response to the last +C request. + +=cut + +sub get_active_program_number() { + my ($self) = @_; + return $$self{'active_program_number'}; +} + +=item C + +Returns true if the active program identified by the device in response to the last +C request is running. + +=cut + +sub get_program_is_running() { + my ($self) = @_; + return $$self{'program_is_running'}; +} + +=item C + +Returns true if valve 8 is set to be a pump. In this setup, valve 8 will also +turn on when any other valve is enabled. Generally used if you have some sort +of water pump that runs to provide water to your sprinklers. + +=cut + +sub get_pump_enabled() { + my ($self) = @_; + return $$self{'pump_enabled'}; +} + +=item C + +Sends a request to the device asking for it to respond with the current timers. +It does not appear that there is code to interpret the response provided by the +device. + +=cut + +sub get_timers() { + my ($self) = @_; + my $cmd = 'sprinkler_timers_request'; + my $subcmd = 0x1; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, $cmd, $subcmd); + $self->_send_cmd($message); + return; +} + +=item C<_is_info_request()> + +Used to intercept and handle unique EZFlora messages, all others are passed on +to C. + +=cut + +sub _is_info_request { + my ($self, $cmd, $ack_setby, %msg) = @_; + my $is_info_request = 0; + if ($cmd eq 'sprinkler_control' + or $cmd eq 'sprinkler_valve_on' + or $cmd eq 'sprinkler_valve_off' + or $cmd eq 'sprinkler_program_on' + or $cmd eq 'sprinkler_program_off') { + $is_info_request = 1; + my $val = hex($msg{extra}); + &::print_log("[Insteon::Irrigation] Processing data for $cmd with value: $val") if $self->debuglevel(1, 'insteon'); + $$self{'active_valve_id'} = ($val & 7) + 1; + $$self{'active_program_number'} = (($val >> 3) & 3) + 1; + $$self{'program_is_running'} = ($val >> 5) & 1; + $$self{'pump_enabled'} = ($val >> 6) & 1; + $$self{'valve_is_running'} = ($val >> 7) & 1; + &::print_log("[Insteon::Irrigation] active_valve_id: $$self{'active_valve_id'}," + . " valve_is_running: $$self{'valve_is_running'}, active_program: $$self{'active_program_number'}," + . " program_is_running: $$self{'program_is_running'}, pump_enabled: $$self{'pump_enabled'}") if $self->debuglevel(1, 'insteon'); + } + else { + #Check if this was a generic info_request + $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); + } + return $is_info_request; + +} + +=item C + +This does nothing and returns 0, it prevents a request_status message, which the +device does not support, from being sent to the device. + +=cut + +# Overload methods we don't use, but would otherwise cause Insteon traffic. +sub request_status { return 0 } + +=back + +=head2 AUTHOR + +Gregg Liming +David Norwood +Evan P. Hall +Kevin Robert Keegan + +=head2 SEE ALSO + +L, +L + +=head2 LICENSE + +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. + +=cut + 1; \ No newline at end of file diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 4483f212c..72452196b 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -1,1230 +1,1230 @@ -=head1 B - -=head2 DESCRIPTION - -A generic base class for all Insteon lighting objects. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::BaseLight; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::BaseLight::ISA = ('Insteon::BaseDevice'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); - bless $self,$class; - # include very basic states; off first so web interface up/down works - $self->set_states('off','on'); - - return $self; -} - -=item C - -Takes the p_level, and stores it as a numeric level in memory. - -=cut - -sub level -{ - my ($self, $p_level) = @_; - if (defined $p_level) { - my $level = 100; - if ($p_level eq 'off') - { - $level = 0; - } - $$self{level} = $level; - } - return $$self{level}; - -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds}, - 'on' => "$object_name->set(\"on\")", - 'off' => "$object_name->set(\"off\")" - ); - return \%voice_cmds; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 DESCRIPTION - -A generic base class for all dimmable Insteon lighting objects. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::DimmableLight; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::DimmableLight::ISA = ('Insteon::BaseLight'); - -my %message_types = ( - %SUPER::message_types, - bright => 0x15, - dim => 0x16 -); - -my %ramp_h2n = ( - '00' => 540, - '01' => 480, - '02' => 420, - '03' => 360, - '04' => 300, - '05' => 270, - '06' => 240, - '07' => 210, - '08' => 180, - '09' => 150, - '0a' => 120, - '0b' => 90, - '0c' => 60, - '0d' => 47, - '0e' => 43, - '0f' => 39, - '10' => 34, - '11' => 32, - '12' => 30, - '13' => 28, - '14' => 26, - '15' => 23.5, - '16' => 21.5, - '17' => 19, - '18' => 8.5, - '19' => 6.5, - '1a' => 4.5, - '1b' => 2, - '1c' => .5, - '1d' => .3, - '1e' => .2, - '1f' => .1 -); - -=item C - -Overrides routine in BaseObject. Takes the various states available to insteon -devices and returns a derived state of on, off, or 0%-100%. - -=cut - -sub derive_link_state -{ - my ($self, $p_state) = @_; - #Convert Relative State to Absolute State - if ($p_state =~ /^([+-])(\d+)/) { - my $rel_state = $1 . $2; - my $curr_state = '100'; - $curr_state = '0' if ($self->state eq 'off'); - $curr_state = $1 if $self->state =~ /(\d{1,3})/; - $p_state = $curr_state + $rel_state; - $p_state = 100 if ($p_state > 100); - $p_state = 0 if ($p_state < 0); - } - - my $link_state = 'on'; - if (grep(/$p_state/i, @{['on_fast', 'off', 'off_fast']})) { - $link_state = $p_state; - } - elsif ($p_state =~ /\d+%?/) - { - $p_state =~ /(\d+)%?/; - $link_state = $1 . '%'; - } - return $link_state; -} - -=item C - -Takes ramp_seconds in numeric seconds and returns the hexadecimal value of that -ramp rate or the next lowest value if the passed value doesn't exist. Possible -ramp rates are: - -540, 480, 420, 360, 300, 270, 240, 210, 180, 150, 120, 90, 60, 47, 43, 39, 34, -32, 30, 28, 26, 23.5, 21.5, 19, 8.5, 6.5, 4.5, 2, .5, .3, .2, and .1 - -=cut - -sub convert_ramp -{ - my ($ramp_in_seconds) = @_; - if ($ramp_in_seconds) { - foreach my $rampkey (sort keys %ramp_h2n) { - return $rampkey if $ramp_in_seconds >= $ramp_h2n{$rampkey}; - } - } else { - return '1f'; - } -} - -=item C - -Takes ramp_code as a hexadecimal representation of the device's ramp rate and -returns the equivalent ramp rate in decimal seconds. - -=cut - -sub get_ramp_from_code -{ - my ($ramp_code) = @_; - if ($ramp_code) { - return $ramp_h2n{$ramp_code}; - } else { - return 0; - } -} - -=item C - -Takes on_level as an integer percentage and converts it to a hexadecimal -representation of that on_level that is used by a device. - -=cut - -sub convert_level -{ - my ($on_level) = @_; - my $level = 'ff'; - if (defined ($on_level)) { - $on_level =~ s/(\d+)%?/$1/; - $level = sprintf('%02X',int(($on_level * 2.55) + .5)); - } - return $level; -} - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::BaseLight($p_deviceid,$p_interface); - bless $self,$class; - - if( $main::config_parms{insteon_menu_states}) { - $self->set_states(split( ',', $main::config_parms{insteon_menu_states})); - } - - return $self; -} - -=item C - -Sets and returns the local onlevel for the device in MH only. Level is a -percentage from 0%-100%. - -This setting can be pushed to the device using C. - -Parameters: level [0-100] - -Returns: [0-100] - -=cut - -sub local_onlevel -{ - my ($self, $p_onlevel) = @_; - if (defined $p_onlevel) - { - my ($onlevel) = $p_onlevel =~ /(\d+)%?/; - $$self{_onlevel} = $onlevel; - } - return $$self{_onlevel}; -} - -=item C - -Sets and returns the local ramp rate for the device in MH only. Rate is a time -between .1 and 540 seconds. Only 32 rate steps exist, to MH will pick a time -equal to of the closest below this time. - -This setting can be pushed to the device using C. - -Parameters: rate = ramp rate [.1s - 540s] see C for valid values - -Returns: hexadecimal representation of the ramprate. - -=cut - -sub local_ramprate -{ - my ($self, $p_ramprate) = @_; - if (defined $p_ramprate) { - $$self{_ramprate} = &Insteon::DimmableLight::convert_ramp($p_ramprate); - } - return $$self{_ramprate}; - -} - -=item C - -Pushes the values set in C and C to the device. - -I1 Devices: - -The device will only reread these values when it is power-cycled. This can be -done by pulling the air-gap for 4 seconds or unplugging the device. - -I2 & I2CS Devices - -The device will immediately read and update the values. - -=cut - -sub update_local_properties -{ - my ($self) = @_; - if ($self->engine_version eq 'I1'){ - $self->_aldb->update_local_properties() if $self->_aldb; - } - else { - #Queue Ramp Rate First - my $extra = '000005' . $self->local_ramprate(); - $extra .= '0' x (30 - length $extra); - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); - $self->_send_cmd($message); - - #Now queue on level - $extra = '000006' . ::Insteon::DimmableLight::convert_level($self->local_onlevel()); - $extra .= '0' x (30 - length $extra); - $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); - $self->_send_cmd($message); - } -} - -=item C - -Stores and returns the objects current on_level as a percentage. If p_level -is ON and the device has a defined local_onlevel, the local_onlevel is stored -as the numeric level in memory. - -Returns [0-100] - -=cut - -sub level -{ - my ($self, $p_level) = @_; - if (defined $p_level) { - my $level = undef; - if ($p_level eq 'on') - { - # set the level based on any locally defined on level - $level = $self->local_onlevel if $self->can('local_onlevel'); - # set to 100 if a local on level is not defined - $level=100 unless defined($level); - } elsif ($p_level eq 'off') - { - $level = 0; - } elsif ($p_level =~ /^([1]?[0-9]?[0-9])%?$/) - { - if ($1 < 1) { - $level = 0; - } else { - $level = $1; - } - } - $$self{level} = $level if defined $level; - } - return $$self{level}; - -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my $insteon_menu_states = $main::config_parms{insteon_menu_states} if $main::config_parms{insteon_menu_states}; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds}, - 'update onlevel/ramprate' => "$object_name->update_local_properties" - ); - if ($insteon_menu_states){ - foreach my $state (split(/,/,$insteon_menu_states)) { - $voice_cmds{$state} = "$object_name->set(\"$state\")"; - } - } - return \%voice_cmds; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::ApplianceLinc; - $appliance_device = new Insteon::ApplianceLinc('12.34.56',$myPLM); - -In mht file: - - INSTEON_APPLIANCELINC, 12.34.56, appliance_device, appliance_group - -=head2 DESCRIPTION - -Provides support for the Insteon ApplianceLinc. - -=head2 INHERITS - -L -L - -=head2 METHODS - -=over - -=cut - -package Insteon::ApplianceLinc; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::ApplianceLinc::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::BaseLight($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::LampLinc; - $lamp_device = new Insteon::LampLinc('12.34.56',$myPLM); - -In mht file: - - INSTEON_LAMPLINC, 12.34.56, lamp_device, All_Lights - -=head2 DESCRIPTION - -Provides support for the Insteon LampLinc. - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::LampLinc; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::LampLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::SwitchLincRelay; - $light_device = new Insteon::SwitchLincRelay('12.34.56',$myPLM); - -In mht file: - - INSTEON_SWITCHLINCRELAY, 12.34.56, light_device, All_Lights - -=head2 DESCRIPTION - -Provides support for the Insteon SwitchLinc Relay. - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::SwitchLincRelay; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::SwitchLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::BaseLight($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::SwitchLinc; - $light_device = new Insteon::SwitchLinc('12.34.56',$myPLM); - -In mht file: - - INSTEON_SWITCHLINC, 12.34.56, light_device, All_Lights - -=head2 DESCRIPTION - -Provides support for the Insteon SwitchLinc. - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::SwitchLinc; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::SwitchLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - - my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::KeyPadLincRelay; - $light_device = new Insteon::KeyPadLincRelay('12.34.56:01',$myPLM); - $button1_device = new Insteon::KeyPadLincRelay('12.34.56:02',$myPLM); - $button2_device = new Insteon::KeyPadLincRelay('12.34.56:03',$myPLM); - -In mht file: - - INSTEON_KEYPADLINCRELAY, 12.34.56:01, light_device, All_Lights - INSTEON_KEYPADLINCRELAY, 12.34.56:02, button1_device, All_Buttons - INSTEON_KEYPADLINCRELAY, 12.34.56:03, button2_device, All_Buttons - -=head2 DESCRIPTION - -Provides support for the Insteon KeypadLinc Relay. - -=head2 INHERITS - -L, -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::KeyPadLincRelay; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::KeyPadLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController', 'Insteon::MultigroupDevice'); - -our %operating_flags = ( - 'program_lock_on' => '00', - 'program_lock_off' => '01', - 'led_on_during_tx' => '02', - 'led_off_during_tx' => '03', - 'resume_dim_on' => '04', - 'resume_dim_off' => '05', - '8_key_mode' => '06', - '6_key_mode' => '07', - 'led_off' => '08', - 'led_enabled' => '09', - 'key_beep_enabled' => '0a', - 'key_beep_off' => '0b' -); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - my $self = new Insteon::BaseLight($p_deviceid,$p_interface); - $$self{operating_flags} = \%operating_flags; - bless $self,$class; - return $self; -} - -=item C - -Handles setting and receiving states from the device and specifically its -subordinate buttons. - -=cut - -sub set -{ - my ($self, $p_state, $p_setby, $p_respond) = @_; - if (!($self->is_root) and !(ref $p_setby && $p_setby eq $self)) - { - if (ref $$self{surrogate} && ($$self{surrogate}->isa('Insteon::InterfaceController'))) { - $$self{surrogate}->set($p_state, $p_setby, $p_respond); - } - else { - ::print_log("[Insteon::KeyPadLinc] You may not directly attempt to set a keypadlinc's button " - ."unless you have defined a reverse link with the \"surrogate\" keyword"); - } - } - else - { - return $self->SUPER::set($p_state, $p_setby, $p_respond); - } -} - -=item C - -Can be used to set the button layout and light level on a keypadlinc. Flag -options include: - - '0a' - 8 button; backlighting dim - '06' - 8 button; backlighting off - '02' - 8 button; backlighting normal - - '08' - 6 button; backlighting dim - '04' - 6 button; backlighting off - '00' - 6 button; backlighting normal - -=cut - -sub update_flags -{ - my ($self, $flags) = @_; - return unless defined $flags; - if ($self->engine_version eq 'I1') { - $self->_aldb->update_flags($flags) if $self->_aldb; - } - else { - if ($flags & 0x02) { - $self->set_operating_flag('8_key_mode'); - } - else { - $self->set_operating_flag('6_key_mode'); - } - if ($flags & 0x04) { - $self->set_operating_flag('led_off'); - } - else { - $self->set_operating_flag('led_enabled'); - } - if ($flags & 0x08) { - $self->set_operating_flag('resume_dim_on'); - } - else { - $self->set_operating_flag('resume_dim_off'); - } - } -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds} - ); - if ($self->is_root){ - %voice_cmds = ( - %voice_cmds, - 'set 8 button - backlight dim' => "$object_name->update_flags(\"0a\")", - 'set 8 button - backlight off' => "$object_name->update_flags(\"06\")", - 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", - 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", - 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", - 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")", - 'sync all device links' => "$object_name->sync_all_links()", - 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" - ); - } - return \%voice_cmds; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::KeyPadLinc; - $light_device = new Insteon::KeyPadLinc('12.34.56:01',$myPLM); - $button1_device = new Insteon::KeyPadLinc('12.34.56:02',$myPLM); - $button2_device = new Insteon::KeyPadLinc('12.34.56:03',$myPLM); - -In mht file: - - INSTEON_KEYPADLINC, 12.34.56:01, light_device, All_Lights - INSTEON_KEYPADLINC, 12.34.56:02, button1_device, All_Buttons - INSTEON_KEYPADLINC, 12.34.56:03, button2_device, All_Buttons - -=head2 DESCRIPTION - -Provides support for the Insteon KeypadLinc. - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::KeyPadLinc; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::KeyPadLinc::ISA = ('Insteon::KeyPadLincRelay', 'Insteon::DimmableLight','Insteon::DeviceController'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); - $$self{operating_flags} = \%Insteon::KeyPadLincRelay::operating_flags; - bless $self,$class; - return $self; -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds} - ); - if ($self->is_root){ - %voice_cmds = ( - %voice_cmds, - 'set 8 button - backlight dim' => "$object_name->update_flags(\"0a\")", - 'set 8 button - backlight off' => "$object_name->update_flags(\"06\")", - 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", - 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", - 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", - 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")" - ); - } - return \%voice_cmds; -} - -# The subgroup items are not dimmable, so call BaseInsteon for them - -sub derive_link_state -{ - my ($self, $p_state) = @_; - if ($self->group eq '01'){ - return $self->SUPER::derive_link_state($p_state); - } else { - return $self->Insteon::BaseObject::derive_link_state($p_state); - } -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 SYNOPSIS - -User code: - - use Insteon::FanLinc; - $light_device = new Insteon::FanLinc('12.34.56:01',$myPLM); - $fan_device = new Insteon::FanLinc('12.34.56:02',$myPLM); - -In mht file: - - INSTEON_FANLINC, 12.34.56:01, light_device, All_Lights - INSTEON_FANLINC, 12.34.56:02, fan_device, All_Fans - -=head2 DESCRIPTION - -Provides support for the Insteon FanLinc. - -=head2 INHERITS - -L, -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon::FanLinc; - -use strict; -use Insteon::BaseInsteon; - -@Insteon::FanLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController', 'Insteon::MultigroupDevice'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class,$p_deviceid,$p_interface) = @_; - my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); - bless $self,$class; - return $self; -} - -=item C - -Generates set commands for the fan, light requests are passed to BaseObject - -=cut - -sub derive_message -{ - my ($self, $p_command, $p_extra) = @_; - if ($self->is_root){ - $self->SUPER::derive_message($self, $p_command, $p_extra); - } - else { - my $level; - - #msg id - my ($command, $subcommand) = split(/:/, $p_command, 2); - $command=lc($command); - - if ($command eq 'on') - { - $command='100'; - } - elsif ($command eq 'off'){ - $command = '00'; - } - $command = ::Insteon::DimmableLight::convert_level($command); - my $extra = $command ."0200000000000000000000000000"; - my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'on', $extra); - return $message; - } -} - -=item C - -Will request the status of the device. For the light device, the process is -handed off to the L routine. This routine -specifically handles the fan request. - -=cut - -sub request_status -{ - my ($self,$requestor) = @_; - if ($self->is_root()){ - return $self->SUPER::request_status($requestor); - } else { - # Setting Fan Level - my $parent = $self->get_root(); - $$parent{child_status_request_pending} = $self->group; - $$self{m_status_request_pending} = ($requestor) ? $requestor : 1; - my $message = new Insteon::InsteonMessage('insteon_send', $parent, 'status_request', '03'); - $parent->_send_cmd($message); - } -} - -=item C<_is_info_request()> - -Handles incoming messages from the device which are unique to the FanLinc, -specifically this handles the C response for the Fan device, -all other responses are handed off to the C. - -=cut - -sub _is_info_request -{ - my ($self, $cmd, $ack_setby, %msg) = @_; - my $is_info_request = 0; - my $parent = $self->get_root(); - if ($$parent{child_status_request_pending}) { - $is_info_request++; - my $child_obj = Insteon::get_object($self->device_id, '02'); - my $child_state = $child_obj->derive_link_state(hex($msg{extra})); - &::print_log("[Insteon::FanLinc] received status for " . - $child_obj->{object_name} . " of: $child_state " - . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); - $ack_setby = $$child_obj{m_status_request_pending} if ref $$child_obj{m_status_request_pending}; - $child_obj->SUPER::set($child_state, $ack_setby); - delete($$parent{child_status_request_pending}); - } else { - $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); - } - return $is_info_request; -} - -=item C - -Handles command acknowledgement messages received from the device that are -unique to the FanLinc, specifically the acknowledgement of commands sent to the -fan device. All other instances are handed off to the C. - -=cut - -sub is_acknowledged -{ - my ($self, $p_ack) = @_; - my $parent = $self->get_root(); - if ($p_ack && $$parent{child_pending_state}) - { - my $child_obj = Insteon::get_object($self->device_id, '02'); - $child_obj->set_receive($$child_obj{pending_state},$$child_obj{pending_setby}, $$child_obj{pending_response}) if defined $$child_obj{pending_state}; - $$child_obj{is_acknowledged} = $p_ack; - $$child_obj{pending_state} = undef; - $$child_obj{pending_setby} = undef; - $$child_obj{pending_response} = undef; - $$parent{child_pending_state} = undef; - &::print_log("[Insteon::FanLinc] received command/state acknowledge from " . $child_obj->{object_name}) if $self->debuglevel(1, 'insteon'); - return $$self{is_acknowledged}; - } else { - return $self->SUPER::is_acknowledged($p_ack); - } -} - -=item C - -Returns a hash of voice commands where the key is the voice command name and the -value is the perl code to run when the voice command name is called. - -Higher classes which inherit this object may add to this list of voice commands by -redefining this routine while inheriting this routine using the SUPER function. - -This routine is called by L to generate the -necessary voice commands. - -=cut - -sub get_voice_cmds -{ - my ($self) = @_; - my $object_name = $self->get_object_name; - my %voice_cmds = ( - %{$self->SUPER::get_voice_cmds} - ); - if ($self->is_root){ - %voice_cmds = ( - %voice_cmds, - 'sync all device links' => "$object_name->sync_all_links()", - 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" - ); - } - return \%voice_cmds; -} - -=back - -=head2 AUTHOR - -Kevin Robert Keegan - -=head2 LICENSE - -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. - -=cut - -1 +=head1 B + +=head2 DESCRIPTION + +A generic base class for all Insteon lighting objects. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::BaseLight; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::BaseLight::ISA = ('Insteon::BaseDevice'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::BaseDevice($p_deviceid,$p_interface); + bless $self,$class; + # include very basic states; off first so web interface up/down works + $self->set_states('off','on'); + + return $self; +} + +=item C + +Takes the p_level, and stores it as a numeric level in memory. + +=cut + +sub level +{ + my ($self, $p_level) = @_; + if (defined $p_level) { + my $level = 100; + if ($p_level eq 'off') + { + $level = 0; + } + $$self{level} = $level; + } + return $$self{level}; + +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'on' => "$object_name->set(\"on\")", + 'off' => "$object_name->set(\"off\")" + ); + return \%voice_cmds; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 DESCRIPTION + +A generic base class for all dimmable Insteon lighting objects. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::DimmableLight; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::DimmableLight::ISA = ('Insteon::BaseLight'); + +my %message_types = ( + %SUPER::message_types, + bright => 0x15, + dim => 0x16 +); + +my %ramp_h2n = ( + '00' => 540, + '01' => 480, + '02' => 420, + '03' => 360, + '04' => 300, + '05' => 270, + '06' => 240, + '07' => 210, + '08' => 180, + '09' => 150, + '0a' => 120, + '0b' => 90, + '0c' => 60, + '0d' => 47, + '0e' => 43, + '0f' => 39, + '10' => 34, + '11' => 32, + '12' => 30, + '13' => 28, + '14' => 26, + '15' => 23.5, + '16' => 21.5, + '17' => 19, + '18' => 8.5, + '19' => 6.5, + '1a' => 4.5, + '1b' => 2, + '1c' => .5, + '1d' => .3, + '1e' => .2, + '1f' => .1 +); + +=item C + +Overrides routine in BaseObject. Takes the various states available to insteon +devices and returns a derived state of on, off, or 0%-100%. + +=cut + +sub derive_link_state +{ + my ($self, $p_state) = @_; + #Convert Relative State to Absolute State + if ($p_state =~ /^([+-])(\d+)/) { + my $rel_state = $1 . $2; + my $curr_state = '100'; + $curr_state = '0' if ($self->state eq 'off'); + $curr_state = $1 if $self->state =~ /(\d{1,3})/; + $p_state = $curr_state + $rel_state; + $p_state = 100 if ($p_state > 100); + $p_state = 0 if ($p_state < 0); + } + + my $link_state = 'on'; + if (grep(/$p_state/i, @{['on_fast', 'off', 'off_fast']})) { + $link_state = $p_state; + } + elsif ($p_state =~ /\d+%?/) + { + $p_state =~ /(\d+)%?/; + $link_state = $1 . '%'; + } + return $link_state; +} + +=item C + +Takes ramp_seconds in numeric seconds and returns the hexadecimal value of that +ramp rate or the next lowest value if the passed value doesn't exist. Possible +ramp rates are: + +540, 480, 420, 360, 300, 270, 240, 210, 180, 150, 120, 90, 60, 47, 43, 39, 34, +32, 30, 28, 26, 23.5, 21.5, 19, 8.5, 6.5, 4.5, 2, .5, .3, .2, and .1 + +=cut + +sub convert_ramp +{ + my ($ramp_in_seconds) = @_; + if ($ramp_in_seconds) { + foreach my $rampkey (sort keys %ramp_h2n) { + return $rampkey if $ramp_in_seconds >= $ramp_h2n{$rampkey}; + } + } else { + return '1f'; + } +} + +=item C + +Takes ramp_code as a hexadecimal representation of the device's ramp rate and +returns the equivalent ramp rate in decimal seconds. + +=cut + +sub get_ramp_from_code +{ + my ($ramp_code) = @_; + if ($ramp_code) { + return $ramp_h2n{$ramp_code}; + } else { + return 0; + } +} + +=item C + +Takes on_level as an integer percentage and converts it to a hexadecimal +representation of that on_level that is used by a device. + +=cut + +sub convert_level +{ + my ($on_level) = @_; + my $level = 'ff'; + if (defined ($on_level)) { + $on_level =~ s/(\d+)%?/$1/; + $level = sprintf('%02X',int(($on_level * 2.55) + .5)); + } + return $level; +} + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::BaseLight($p_deviceid,$p_interface); + bless $self,$class; + + if( $main::config_parms{insteon_menu_states}) { + $self->set_states(split( ',', $main::config_parms{insteon_menu_states})); + } + + return $self; +} + +=item C + +Sets and returns the local onlevel for the device in MH only. Level is a +percentage from 0%-100%. + +This setting can be pushed to the device using C. + +Parameters: level [0-100] + +Returns: [0-100] + +=cut + +sub local_onlevel +{ + my ($self, $p_onlevel) = @_; + if (defined $p_onlevel) + { + my ($onlevel) = $p_onlevel =~ /(\d+)%?/; + $$self{_onlevel} = $onlevel; + } + return $$self{_onlevel}; +} + +=item C + +Sets and returns the local ramp rate for the device in MH only. Rate is a time +between .1 and 540 seconds. Only 32 rate steps exist, to MH will pick a time +equal to of the closest below this time. + +This setting can be pushed to the device using C. + +Parameters: rate = ramp rate [.1s - 540s] see C for valid values + +Returns: hexadecimal representation of the ramprate. + +=cut + +sub local_ramprate +{ + my ($self, $p_ramprate) = @_; + if (defined $p_ramprate) { + $$self{_ramprate} = &Insteon::DimmableLight::convert_ramp($p_ramprate); + } + return $$self{_ramprate}; + +} + +=item C + +Pushes the values set in C and C to the device. + +I1 Devices: + +The device will only reread these values when it is power-cycled. This can be +done by pulling the air-gap for 4 seconds or unplugging the device. + +I2 & I2CS Devices + +The device will immediately read and update the values. + +=cut + +sub update_local_properties +{ + my ($self) = @_; + if ($self->engine_version eq 'I1'){ + $self->_aldb->update_local_properties() if $self->_aldb; + } + else { + #Queue Ramp Rate First + my $extra = '000005' . $self->local_ramprate(); + $extra .= '0' x (30 - length $extra); + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $self->_send_cmd($message); + + #Now queue on level + $extra = '000006' . ::Insteon::DimmableLight::convert_level($self->local_onlevel()); + $extra .= '0' x (30 - length $extra); + $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'extended_set_get', $extra); + $self->_send_cmd($message); + } +} + +=item C + +Stores and returns the objects current on_level as a percentage. If p_level +is ON and the device has a defined local_onlevel, the local_onlevel is stored +as the numeric level in memory. + +Returns [0-100] + +=cut + +sub level +{ + my ($self, $p_level) = @_; + if (defined $p_level) { + my $level = undef; + if ($p_level eq 'on') + { + # set the level based on any locally defined on level + $level = $self->local_onlevel if $self->can('local_onlevel'); + # set to 100 if a local on level is not defined + $level=100 unless defined($level); + } elsif ($p_level eq 'off') + { + $level = 0; + } elsif ($p_level =~ /^([1]?[0-9]?[0-9])%?$/) + { + if ($1 < 1) { + $level = 0; + } else { + $level = $1; + } + } + $$self{level} = $level if defined $level; + } + return $$self{level}; + +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my $insteon_menu_states = $main::config_parms{insteon_menu_states} if $main::config_parms{insteon_menu_states}; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds}, + 'update onlevel/ramprate' => "$object_name->update_local_properties" + ); + if ($insteon_menu_states){ + foreach my $state (split(/,/,$insteon_menu_states)) { + $voice_cmds{$state} = "$object_name->set(\"$state\")"; + } + } + return \%voice_cmds; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::ApplianceLinc; + $appliance_device = new Insteon::ApplianceLinc('12.34.56',$myPLM); + +In mht file: + + INSTEON_APPLIANCELINC, 12.34.56, appliance_device, appliance_group + +=head2 DESCRIPTION + +Provides support for the Insteon ApplianceLinc. + +=head2 INHERITS + +L +L + +=head2 METHODS + +=over + +=cut + +package Insteon::ApplianceLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::ApplianceLinc::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::BaseLight($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::LampLinc; + $lamp_device = new Insteon::LampLinc('12.34.56',$myPLM); + +In mht file: + + INSTEON_LAMPLINC, 12.34.56, lamp_device, All_Lights + +=head2 DESCRIPTION + +Provides support for the Insteon LampLinc. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::LampLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::LampLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::SwitchLincRelay; + $light_device = new Insteon::SwitchLincRelay('12.34.56',$myPLM); + +In mht file: + + INSTEON_SWITCHLINCRELAY, 12.34.56, light_device, All_Lights + +=head2 DESCRIPTION + +Provides support for the Insteon SwitchLinc Relay. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::SwitchLincRelay; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::SwitchLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::BaseLight($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::SwitchLinc; + $light_device = new Insteon::SwitchLinc('12.34.56',$myPLM); + +In mht file: + + INSTEON_SWITCHLINC, 12.34.56, light_device, All_Lights + +=head2 DESCRIPTION + +Provides support for the Insteon SwitchLinc. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::SwitchLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::SwitchLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + + my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::KeyPadLincRelay; + $light_device = new Insteon::KeyPadLincRelay('12.34.56:01',$myPLM); + $button1_device = new Insteon::KeyPadLincRelay('12.34.56:02',$myPLM); + $button2_device = new Insteon::KeyPadLincRelay('12.34.56:03',$myPLM); + +In mht file: + + INSTEON_KEYPADLINCRELAY, 12.34.56:01, light_device, All_Lights + INSTEON_KEYPADLINCRELAY, 12.34.56:02, button1_device, All_Buttons + INSTEON_KEYPADLINCRELAY, 12.34.56:03, button2_device, All_Buttons + +=head2 DESCRIPTION + +Provides support for the Insteon KeypadLinc Relay. + +=head2 INHERITS + +L, +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::KeyPadLincRelay; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::KeyPadLincRelay::ISA = ('Insteon::BaseLight','Insteon::DeviceController', 'Insteon::MultigroupDevice'); + +our %operating_flags = ( + 'program_lock_on' => '00', + 'program_lock_off' => '01', + 'led_on_during_tx' => '02', + 'led_off_during_tx' => '03', + 'resume_dim_on' => '04', + 'resume_dim_off' => '05', + '8_key_mode' => '06', + '6_key_mode' => '07', + 'led_off' => '08', + 'led_enabled' => '09', + 'key_beep_enabled' => '0a', + 'key_beep_off' => '0b' +); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + my $self = new Insteon::BaseLight($p_deviceid,$p_interface); + $$self{operating_flags} = \%operating_flags; + bless $self,$class; + return $self; +} + +=item C + +Handles setting and receiving states from the device and specifically its +subordinate buttons. + +=cut + +sub set +{ + my ($self, $p_state, $p_setby, $p_respond) = @_; + if (!($self->is_root) and !(ref $p_setby && $p_setby eq $self)) + { + if (ref $$self{surrogate} && ($$self{surrogate}->isa('Insteon::InterfaceController'))) { + $$self{surrogate}->set($p_state, $p_setby, $p_respond); + } + else { + ::print_log("[Insteon::KeyPadLinc] You may not directly attempt to set a keypadlinc's button " + ."unless you have defined a reverse link with the \"surrogate\" keyword"); + } + } + else + { + return $self->SUPER::set($p_state, $p_setby, $p_respond); + } +} + +=item C + +Can be used to set the button layout and light level on a keypadlinc. Flag +options include: + + '0a' - 8 button; backlighting dim + '06' - 8 button; backlighting off + '02' - 8 button; backlighting normal + + '08' - 6 button; backlighting dim + '04' - 6 button; backlighting off + '00' - 6 button; backlighting normal + +=cut + +sub update_flags +{ + my ($self, $flags) = @_; + return unless defined $flags; + if ($self->engine_version eq 'I1') { + $self->_aldb->update_flags($flags) if $self->_aldb; + } + else { + if ($flags & 0x02) { + $self->set_operating_flag('8_key_mode'); + } + else { + $self->set_operating_flag('6_key_mode'); + } + if ($flags & 0x04) { + $self->set_operating_flag('led_off'); + } + else { + $self->set_operating_flag('led_enabled'); + } + if ($flags & 0x08) { + $self->set_operating_flag('resume_dim_on'); + } + else { + $self->set_operating_flag('resume_dim_off'); + } + } +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'set 8 button - backlight dim' => "$object_name->update_flags(\"0a\")", + 'set 8 button - backlight off' => "$object_name->update_flags(\"06\")", + 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", + 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", + 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", + 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")", + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" + ); + } + return \%voice_cmds; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::KeyPadLinc; + $light_device = new Insteon::KeyPadLinc('12.34.56:01',$myPLM); + $button1_device = new Insteon::KeyPadLinc('12.34.56:02',$myPLM); + $button2_device = new Insteon::KeyPadLinc('12.34.56:03',$myPLM); + +In mht file: + + INSTEON_KEYPADLINC, 12.34.56:01, light_device, All_Lights + INSTEON_KEYPADLINC, 12.34.56:02, button1_device, All_Buttons + INSTEON_KEYPADLINC, 12.34.56:03, button2_device, All_Buttons + +=head2 DESCRIPTION + +Provides support for the Insteon KeypadLinc. + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::KeyPadLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::KeyPadLinc::ISA = ('Insteon::KeyPadLincRelay', 'Insteon::DimmableLight','Insteon::DeviceController'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); + $$self{operating_flags} = \%Insteon::KeyPadLincRelay::operating_flags; + bless $self,$class; + return $self; +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'set 8 button - backlight dim' => "$object_name->update_flags(\"0a\")", + 'set 8 button - backlight off' => "$object_name->update_flags(\"06\")", + 'set 8 button - backlight normal' => "$object_name->update_flags(\"02\")", + 'set 6 button - backlight dim' => "$object_name->update_flags(\"08\")", + 'set 6 button - backlight off' => "$object_name->update_flags(\"04\")", + 'set 6 button - backlight normal' => "$object_name->update_flags(\"00\")" + ); + } + return \%voice_cmds; +} + +# The subgroup items are not dimmable, so call BaseInsteon for them + +sub derive_link_state +{ + my ($self, $p_state) = @_; + if ($self->group eq '01'){ + return $self->SUPER::derive_link_state($p_state); + } else { + return $self->Insteon::BaseObject::derive_link_state($p_state); + } +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 SYNOPSIS + +User code: + + use Insteon::FanLinc; + $light_device = new Insteon::FanLinc('12.34.56:01',$myPLM); + $fan_device = new Insteon::FanLinc('12.34.56:02',$myPLM); + +In mht file: + + INSTEON_FANLINC, 12.34.56:01, light_device, All_Lights + INSTEON_FANLINC, 12.34.56:02, fan_device, All_Fans + +=head2 DESCRIPTION + +Provides support for the Insteon FanLinc. + +=head2 INHERITS + +L, +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon::FanLinc; + +use strict; +use Insteon::BaseInsteon; + +@Insteon::FanLinc::ISA = ('Insteon::DimmableLight','Insteon::DeviceController', 'Insteon::MultigroupDevice'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class,$p_deviceid,$p_interface) = @_; + my $self = new Insteon::DimmableLight($p_deviceid,$p_interface); + bless $self,$class; + return $self; +} + +=item C + +Generates set commands for the fan, light requests are passed to BaseObject + +=cut + +sub derive_message +{ + my ($self, $p_command, $p_extra) = @_; + if ($self->is_root){ + $self->SUPER::derive_message($self, $p_command, $p_extra); + } + else { + my $level; + + #msg id + my ($command, $subcommand) = split(/:/, $p_command, 2); + $command=lc($command); + + if ($command eq 'on') + { + $command='100'; + } + elsif ($command eq 'off'){ + $command = '00'; + } + $command = ::Insteon::DimmableLight::convert_level($command); + my $extra = $command ."0200000000000000000000000000"; + my $message = new Insteon::InsteonMessage('insteon_ext_send', $self, 'on', $extra); + return $message; + } +} + +=item C + +Will request the status of the device. For the light device, the process is +handed off to the L routine. This routine +specifically handles the fan request. + +=cut + +sub request_status +{ + my ($self,$requestor) = @_; + if ($self->is_root()){ + return $self->SUPER::request_status($requestor); + } else { + # Setting Fan Level + my $parent = $self->get_root(); + $$parent{child_status_request_pending} = $self->group; + $$self{m_status_request_pending} = ($requestor) ? $requestor : 1; + my $message = new Insteon::InsteonMessage('insteon_send', $parent, 'status_request', '03'); + $parent->_send_cmd($message); + } +} + +=item C<_is_info_request()> + +Handles incoming messages from the device which are unique to the FanLinc, +specifically this handles the C response for the Fan device, +all other responses are handed off to the C. + +=cut + +sub _is_info_request +{ + my ($self, $cmd, $ack_setby, %msg) = @_; + my $is_info_request = 0; + my $parent = $self->get_root(); + if ($$parent{child_status_request_pending}) { + $is_info_request++; + my $child_obj = Insteon::get_object($self->device_id, '02'); + my $child_state = $child_obj->derive_link_state(hex($msg{extra})); + &::print_log("[Insteon::FanLinc] received status for " . + $child_obj->{object_name} . " of: $child_state " + . "hops left: $msg{hopsleft}") if $self->debuglevel(1, 'insteon'); + $ack_setby = $$child_obj{m_status_request_pending} if ref $$child_obj{m_status_request_pending}; + $child_obj->SUPER::set($child_state, $ack_setby); + delete($$parent{child_status_request_pending}); + } else { + $is_info_request = $self->SUPER::_is_info_request($cmd, $ack_setby, %msg); + } + return $is_info_request; +} + +=item C + +Handles command acknowledgement messages received from the device that are +unique to the FanLinc, specifically the acknowledgement of commands sent to the +fan device. All other instances are handed off to the C. + +=cut + +sub is_acknowledged +{ + my ($self, $p_ack) = @_; + my $parent = $self->get_root(); + if ($p_ack && $$parent{child_pending_state}) + { + my $child_obj = Insteon::get_object($self->device_id, '02'); + $child_obj->set_receive($$child_obj{pending_state},$$child_obj{pending_setby}, $$child_obj{pending_response}) if defined $$child_obj{pending_state}; + $$child_obj{is_acknowledged} = $p_ack; + $$child_obj{pending_state} = undef; + $$child_obj{pending_setby} = undef; + $$child_obj{pending_response} = undef; + $$parent{child_pending_state} = undef; + &::print_log("[Insteon::FanLinc] received command/state acknowledge from " . $child_obj->{object_name}) if $self->debuglevel(1, 'insteon'); + return $$self{is_acknowledged}; + } else { + return $self->SUPER::is_acknowledged($p_ack); + } +} + +=item C + +Returns a hash of voice commands where the key is the voice command name and the +value is the perl code to run when the voice command name is called. + +Higher classes which inherit this object may add to this list of voice commands by +redefining this routine while inheriting this routine using the SUPER function. + +This routine is called by L to generate the +necessary voice commands. + +=cut + +sub get_voice_cmds +{ + my ($self) = @_; + my $object_name = $self->get_object_name; + my %voice_cmds = ( + %{$self->SUPER::get_voice_cmds} + ); + if ($self->is_root){ + %voice_cmds = ( + %voice_cmds, + 'sync all device links' => "$object_name->sync_all_links()", + 'AUDIT sync all device links' => "$object_name->sync_all_links(1)" + ); + } + return \%voice_cmds; +} + +=back + +=head2 AUTHOR + +Kevin Robert Keegan + +=head2 LICENSE + +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. + +=cut + +1 diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 843547980..3274fe885 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -1,1151 +1,1151 @@ -=head1 B - -=head2 DESCRIPTION - -Generic base class for L object messages, including -L. Primarily just -stores variables for the object. - -=head2 INHERITS - -Nothing - -=head2 METHODS - -=over - -=cut - -package Insteon::BaseMessage; - -use strict; - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class) = @_; - my $self={}; - bless $self,$class; - - $$self{queue_time} = &main::get_tickcount; - $$self{send_attempts} = 0; - - return $self; -} - -=item C - -Save and retrieve the interface data defined by C. - -=cut - -sub interface_data -{ - my ($self, $interface_data) = @_; - if ($interface_data) - { - $$self{interface_data} = $interface_data; - } - return $$self{interface_data}; -} - -=item C - -Stores the time at which a message was added to the queue. Used for calculating -how long a message was delayed. - -=cut - -sub queue_time -{ - my ($self, $queue_time) = @_; - if ($queue_time) - { - $$self{queue_time} = $queue_time; - } - return $$self{queue_time}; -} - -=item C - -Data will be evaluated each time the message is sent. - -=cut - -sub callback -{ - my ($self, $callback) = @_; - if ($callback) - { - $$self{callback} = $callback; - } - return $$self{callback}; -} - -=item C - -Data will be evaluated if the maximum number of retry attempts has been made -and the message is not acknowledged. - -=cut - -sub failure_callback -{ - my ($self, $callback) = @_; - if ($callback) - { - $$self{failure_callback} = $callback; - } - return $$self{failure_callback}; -} - -=item C - -Data will be evaluated after the receipt of an ACK from the device for this command. - -=cut - -sub success_callback -{ - my ($self, $callback) = @_; - if (defined $callback) - { - $$self{success_callback} = $callback; - } - return $$self{success_callback}; -} - -=item C - -Stores and retrieves the number of times Misterhouse has tried to send the message. - -=cut - -sub send_attempts -{ - my ($self, $send_attempts) = @_; - if ($send_attempts) - { - $$self{send_attempts} = $send_attempts; - } - return $$self{send_attempts}; -} - -=item C - -Stores and retrieves what the source of this message was. - -=cut - -sub setby -{ - my ($self, $setby) = @_; - if ($setby) - { - $$self{setby} = $setby; - } - return $$self{setby}; -} - -=item C - -Stores and retrieves respond variable. - -=cut - -sub respond -{ - my ($self, $respond) = @_; - if ($respond) - { - $$self{respond} = $respond; - } - return $$self{respond}; -} - - -=item C - -Stores and retrieves no_hop_increase variable, if set to true, when the message -is retried, no additional hops will be added. Typically used where the failure -to deliver the last message attempt was not caused by a failure of the object -to receive the message such as when the PLM is too busy to even attempt sending -the message. - -=cut - -sub no_hop_increase -{ - my ($self, $no_hop_increase) = @_; - if ($no_hop_increase) - { - $$self{no_hop_increase} = $no_hop_increase; - } - return $$self{no_hop_increase}; -} - -=item C - -Stores and retrieves the number of times MisterHouse should try to deliver this -message. If B is set in the ini parameters will default -to that value, otherwise defaults to 5. Some messages types have specific -retry counts, such as L -battery level requests which are only sent once. - -=cut - -sub retry_count { - my ($self, $retry_count) = @_; - if ($retry_count) - { - $$self{retry_count} = $retry_count; - } - my $result_retry = 5; - $result_retry = $::config_parms{'Insteon_retry_count'} if ($::config_parms{'Insteon_retry_count'}); - $result_retry = $$self{retry_count} if ($$self{retry_count}); - return $result_retry; -} - -=item C - -Sends this message using the interface p_interface. If C is -greater than 0 then -L -is increase by one. Calls C -when the message is sent. - -Returns 1 if message sent or 0 if message retry count exceeds C. - -=cut - -sub send -{ - my ($self, $interface) = @_; - if ($self->send_attempts < $self->retry_count) - { - - if ($self->send_attempts > 0) - { - &::print_log("[Insteon::BaseMessage] WARN: now resending " - . $self->to_string() . " after " . $self->send_attempts - . " attempts.") if $self->setby->debuglevel(1, 'insteon'); - # revise default hop count to reflect retries - if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') - && !defined($$self{no_hop_increase})) - { - $self->setby->retry_count_log(1) if $self->setby->can('retry_count_log'); - if ($self->setby->default_hop_count < 3) - { - $self->setby->default_hop_count($self->setby->default_hop_count + 1); - } - } - elsif (defined($$self{no_hop_increase}) && ref $self->setby - && $self->setby->isa('Insteon::BaseObject')){ - &main::print_log("[Insteon::BaseMessage] Hop count not increased for " - . $self->setby->get_object_name . " because no_hop_increase flag was set.") - if $self->setby->debuglevel(1, 'insteon'); - $$self{no_hop_increase} = undef; - } - } - - # need to set timeout as a function of retries; also need to alter hop count - if ($self->send_attempts <= 0 && ref $self->setby) { - $self->setby->outgoing_count_log(1) if $self->setby->can('outgoing_count_log'); - $self->setby->outgoing_hop_count($self->setby->default_hop_count) - if $self->setby->can('outgoing_hop_count'); - } - $self->send_attempts($self->send_attempts + 1); - $interface->_send_cmd($self, $self->send_timeout); - if ($self->callback) - { - package main; - eval $self->callback; - &::print_log("[Insteon::BaseMessage] problem w/ retry callback: $@") if $@; - package Insteon::Message; - } - return 1; - } - else - { - return 0; - } - -} - -=item C - -Returns the number of seconds that elapsed between time set in C -and when this routine was called. - -=cut - -sub seconds_delayed -{ - my ($self) = @_; - my $current_tickcount = &main::get_tickcount; - my $delay_time = $current_tickcount - $self->queue_time; - if ($self->queue_time > $current_tickcount) - { - return 'unknown'; - } - - $delay_time = $delay_time / 1000; - return $delay_time; -} - -=item C - -Stores and returns the number of milliseconds, p_timeout, that MisterHouse -should wait before retrying this message again. - -=cut - -sub send_timeout -{ - my ($self, $timeout) = @_; - $$self{send_timeout} = $timeout if defined $timeout; - return $$self{send_timeout}; -} - -=item C - -Returns the hexadecimal representation of the message. - -=cut - -sub to_string -{ - my ($self) = @_; - return $self->interface_data; -} - -=back - -=head2 INI PARAMETERS - -=over - -=item Insteon_retry_count - -Sets the number of times MisterHouse will attempt to resend a message that has -not been acknowledged. The default setting is 5. - -=back - -=head2 AUTHOR - -Gregg Limming, Kevin Robert Keegan - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 DESCRIPTION - -Main class for all L messages. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::InsteonMessage; -use strict; - -@Insteon::InsteonMessage::ISA = ('Insteon::BaseMessage'); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class, $command_type, $setby, $command, $extra) = @_; - my $self= new Insteon::BaseMessage(); - bless $self,$class; - - $self->command_type($command_type); - $self->setby($setby); - $self->command($command); - $self->extra($extra); - $self->send_timeout(2000); - - return $self; -} - -=item C - -Takes msg, a hexadecimal message, and returns a hash of the message details. - -=cut - -sub command_to_hash -{ - my ($p_state) = @_; - my %msg = (); - my $hopflag = hex(uc substr($p_state,13,1)); - $msg{maxhops} = $hopflag&0b0011; - $msg{hopsleft} = $hopflag >> 2; - my $msgflag = hex(uc substr($p_state,12,1)); - $msg{is_extended} = (0x01 & $msgflag) ? 1 : 0; - $msg{cmd_code} = substr($p_state,14,2); - $msg{crc_valid} = 1; - if ($msg{is_extended}) - { - $msg{type} = 'direct'; - $msg{source} = substr($p_state,0,6); - $msg{destination} = substr($p_state,6,6); - $msg{extra} = substr($p_state,16,length($p_state)-16); - $msg{crc_valid} = (calculate_checksum($msg{cmd_code}.$msg{extra}) eq '00'); - } - else - { - $msg{source} = substr($p_state,0,6); - $msgflag = $msgflag >> 1; - if ($msgflag == 4) - { - $msg{type} = 'broadcast'; - $msg{devcat} = substr($p_state,6,4); - $msg{firmware} = substr($p_state,10,2); - $msg{is_master} = substr($p_state,16,2); - $msg{dev_attribs} = substr($p_state,18,2); - } - elsif ($msgflag ==6) - { - $msg{type} = 'alllink'; - $msg{group} = substr($p_state,10,2); - $msg{extra} = substr($p_state,16,2) - if (length($p_state) >= 18); - } - else - { - $msg{destination} = substr($p_state,6,6); - if ($msgflag == 2) - { - $msg{type} = 'cleanup'; - $msg{group} = substr($p_state,16,2); - } - elsif ($msgflag == 3) - { - $msg{type} = 'cleanup'; - $msg{is_ack} = 1; - # the "extra" value will contain the controller's group ID - $msg{extra} = substr($p_state,16,2); - } - elsif ($msgflag == 7) - { - $msg{type} = 'cleanup'; - $msg{is_nack} = 1; - $msg{extra} = substr($p_state,16,2); - } - elsif ($msgflag == 0) - { - $msg{type} = 'direct'; - $msg{extra} = substr($p_state,16,2); - } - elsif ($msgflag == 1) - { - $msg{type} = 'direct'; - $msg{is_ack} = 1; - $msg{extra} = substr($p_state,16,2); - } - elsif ($msgflag == 5) - { - $msg{type} = 'direct'; - $msg{is_nack} = 1; - $msg{extra} = substr($p_state,16,2); - } - } - } - - return %msg; -} - -=item C - -Stores and retrieves the Cmd1 value for this message. - -=cut - -sub command -{ - my ($self, $command) = @_; - $$self{command} = $command if $command; - return $$self{command}; -} - -=item C - -Stores and retrieves the Command Type value for this message. - -=cut - -sub command_type -{ - my ($self, $command_type) = @_; - $$self{command_type} = $command_type if $command_type; - return $$self{command_type}; -} - -=item C - -Stores and retrieves the extra value for this message. For standard messages -extra is Cmd2. For extended messages extra is Cmd2 + D1-D14. - -=cut - -sub extra -{ - my ($self, $extra) = @_; - $$self{extra} = $extra if $extra; - return $$self{extra}; -} - -=item C - -Calculates and returns the number of milliseconds that MisterHouse should wait -for this message to be delivered. The time is based on message type, command type, -and hop count. - - Peek Related Messages = 4000 - PLM Scene Commands = 3000 - Ext / Std - 0 Hop 2220 1400 - 1 Hop 2690 1700 - 2 Hop 3000 1900 - 3 Hop 3170 2000 - -These times were intially calculated by Gregg Limming and appear to have been -calculated based on experience. In reality each hop of a standard message -should take 50 ms and 108 for extended messages. Each time needs to be at least -doubled to compensate for the return hops as well. - -In reality, the PLM and even some Insteon devices appear to react much slower -than the spec defines. These settings generally appear to work without causing -errors or too much undue delay. - -=cut - -sub send_timeout -{ - my ($self, $ignore) = @_; - my $hop_count = (ref $self->setby and $self->setby->isa('Insteon::BaseObject')) ? - $self->setby->default_hop_count : $self->send_attempts; - my $timeout = 1400; - if($self->command eq 'peek' || $self->command eq 'set_address_msb') - { - $timeout = 4000; - } - elsif ($self->command_type eq 'all_link_send') - { - # note, the following was set to 2000 and that was insufficient - $timeout = 3000; - } - elsif ($self->command_type eq 'insteon_ext_send') - { - if ($hop_count == 0) - { - $timeout = 2220; - } - elsif ($hop_count == 1) - { - $timeout = 2690; - } - elsif ($hop_count == 2) - { - $timeout = 3000; - } - elsif ($hop_count >= 3) - { - $timeout = 3170; - } - } - else - { - if ($hop_count == 0) - { - $timeout = 1400; - } - elsif ($hop_count == 1) - { - $timeout = 1700; - } - elsif ($hop_count == 2) - { - $timeout = 1900; - } - elsif ($hop_count >= 3) - { - $timeout = 2000; - } - } - if (ref $self->setby and $self->setby->isa('Insteon::BaseObject')){ - $timeout = int($timeout * $self->setby->timeout_factor); - } - return $timeout; -} - -=item C - -Returns text based human readable representation of the message. - -=cut - -sub to_string -{ - my ($self) = @_; - my $result = ''; - if ($self->setby) - { - $result .= 'obj=' . $self->setby->get_object_name; - } - if ($result) - { - $result .= '; '; - } - if ($self->command) - { - $result .= 'command=' . $self->command; - } - else - { - $result .= 'interface_data=' . $self->interface_data; - } - if ($self->extra) - { - $result .= '; extra=' . $self->extra; - } - - return $result; -} - -=item C - -Stores data as the hexadecimal message, or if data is not specified, then derives -the hexadecimal message and returns it. - -=cut - -sub interface_data -{ - my ($self, $interface_data) = @_; - my $result = $self->SUPER::interface_data($interface_data); - if (!($result) && - (($self->command_type eq 'insteon_send') - or ($self->command_type eq 'insteon_ext_send') - or ($self->command_type eq 'all_link_send') - or ($self->command_type eq 'all_link_direct_cleanup'))) - { - return $self->_derive_interface_data(); - } - else - { - return $result; - } -} - -=item C<_derive_interface_data()> - -Converts all of the attributes set for this message into a hexadecimal message -that can be sent to the PLM. Will add checksums and crcs when necessary. - -=cut - -sub _derive_interface_data -{ - - my ($self) = @_; - my $cmd = ''; - my $level; - if ($self->command_type =~ /all_link_send/i) - { - $cmd.=$self->setby->group; - } - else - { - my $hop_count = $self->setby->default_hop_count; - $cmd.=$self->setby->device_id(); - if ($self->command_type =~ /insteon_ext_send/i) - { - if ($hop_count == 0) - { - $cmd.='10'; - } - elsif ($hop_count == 1) - { - $cmd.='15'; - } - elsif ($hop_count == 2) - { - $cmd.='1A'; - } - elsif ($hop_count >= 3) - { - $cmd.='1F'; - } - } elsif ($self->command_type =~ /all_link_direct_cleanup/i){ - if ($hop_count == 0) - { - $cmd.='40'; - } - elsif ($hop_count == 1) - { - $cmd.='45'; - } - elsif ($hop_count == 2) - { - $cmd.='4A'; - } - elsif ($hop_count >= 3) - { - $cmd.='4F'; - } - } - else - { - if ($hop_count == 0) - { - $cmd.='00'; - } - elsif ($hop_count == 1) - { - $cmd.='05'; - } - elsif ($hop_count == 2) - { - $cmd.='0A'; - } - elsif ($hop_count >= 3) - { - $cmd.='0F'; - } - } - } - $cmd.= unpack("H*",pack("C",$self->setby->message_type_code($self->command))); - if ($self->extra) - { - $cmd.= $self->extra; - } - elsif ($self->command_type eq 'insteon_send') - { # auto append '00' if no extra defined for a standard insteon send - $cmd .= '00'; - } - - if( $self->command_type eq 'insteon_ext_send' and $$self{add_crc16}){ - if( length($cmd) < 40) { - main::print_log("[Insteon::InsteonMessage] WARN: insert_crc16 " - . "failed; cmd to short: $cmd"); - } else { - $cmd = substr($cmd,0,36).calculate_crc16(substr($cmd,8,28)); - } - } - elsif( $self->command_type eq 'insteon_ext_send' and $self->setby->engine_version eq 'I2CS') { - #$message is the entire insteon command (no 0262 PLM command) - # i.e. '02622042d31f2e000107110000000000000000000000' - # 111111111122222222223333333333 - # 0123456789012345678901234567890123456789 - # '2042d31f2e000107110000000000000000000000' - if( length($cmd) < 40) { - main::print_log("[Insteon::InsteonMessage] WARN: insert_checksum " - . "failed; cmd to short: $cmd"); - } else { - $cmd = substr($cmd,0,38).calculate_checksum(substr($cmd,8,30)); - } - } - - return $cmd; - -} - -=item C - -Calculates a checksum of all hex bytes in the string. Returns two hex nibbles -that represent the checksum in hex. One useful characteristic of the checksum -is that summing over all the bytes "including" the checksum will always equal 00. -This makes it very easy to validate a checksum. - -=cut - -sub calculate_checksum { - my ($string) = @_; - - #returns 2 characters as hex nibbles (e.g. AA) - my $sum = 0; - $sum += hex($_) for (unpack('(A2)*', $string)); - return unpack( 'H2', chr((~$sum + 1) & 0xff)); -} - -=item C - -Calculates a two byte CRC value of string. This two byte CRC differs from the -one byte checksum used in other extended commands. This CRC calculation is known -to be used by the 2441TH Insteon Thermostat as well as the iMeter INSTEON device. -It may be used by other devices in the future. - -The calculation if the crc value involves data bytes from command 1 to the data 12 -byte. This function will return two bytes, which are generally added to the -data 13 & 14 bytes in an extended message. - -To add a crc16 to a message set the $$message{add_crc16} flag to true. - -=cut - -sub calculate_crc16 -{ - #This function is nearly identical to the C++ sample provided by - #smartlabs, with only minor modifications to make it work in perl - my ($string) = @_; - my $crc = 0; - for(unpack('(A2)*', $string)) - { - my $byte = hex($_); - - for(my $bit = 0;$bit < 8;$bit++) - { - my $fb = $byte & 1; - $fb = ($crc & 0x8000) ? $fb ^ 1 : $fb; - $fb = ($crc & 0x4000) ? $fb ^ 1 : $fb; - $fb = ($crc & 0x1000) ? $fb ^ 1 : $fb; - $fb = ($crc & 0x0008) ? $fb ^ 1 : $fb; - $crc = (($crc << 1) & 0xFFFF) | $fb; - $byte = $byte >> 1; - } - } - return uc(sprintf("%x", $crc)); -} - -=back - -=head2 AUTHOR - -Gregg Limming, Kevin Robert Keegan, Michael Stovenour - -=head2 LICENSE - -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. - -=cut - -=head1 B - -=head2 DESCRIPTION - -Main class for all L X10 messages. - -=head2 INHERITS - -L - -=head2 METHODS - -=over - -=cut - -package Insteon::X10Message; -use strict; - -@Insteon::X10Message::ISA = ('Insteon::BaseMessage'); - -my %x10_house_codes = ( - a => 0x6, - b => 0xE, - c => 0x2, - d => 0xA, - e => 0x1, - f => 0x9, - g => 0x5, - h => 0xD, - i => 0x7, - j => 0xF, - k => 0x3, - l => 0xB, - m => 0x0, - n => 0x8, - o => 0x4, - p => 0xC -); - -my %mh_house_codes = ( - '6' => 'a', - 'e' => 'b', - '2' => 'c', - 'a' => 'd', - '1' => 'e', - '9' => 'f', - '5' => 'g', - 'd' => 'h', - '7' => 'i', - 'f' => 'j', - '3' => 'k', - 'b' => 'l', - '0' => 'm', - '8' => 'n', - '4' => 'o', - 'c' => 'p' -); - -my %x10_unit_codes = ( - 1 => 0x6, - 2 => 0xE, - 3 => 0x2, - 4 => 0xA, - 5 => 0x1, - 6 => 0x9, - 7 => 0x5, - 8 => 0xD, - 9 => 0x7, - 10 => 0xF, - a => 0xF, - 11 => 0x3, - b => 0x3, - 12 => 0xB, - c => 0xB, - 13 => 0x0, - d => 0x0, - 14 => 0x8, - e => 0x8, - 15 => 0x4, - f => 0x4, - 16 => 0xC, - g => 0xC - -); - -my %mh_unit_codes = ( - '6' => '1', - 'e' => '2', - '2' => '3', - 'a' => '4', - '1' => '5', - '9' => '6', - '5' => '7', - 'd' => '8', - '7' => '9', - 'f' => 'a', - '3' => 'b', - 'b' => 'c', - '0' => 'd', - '8' => 'e', - '4' => 'f', - 'c' => 'g' -); - -my %x10_commands = ( - on => 0x2, - j => 0x2, - off => 0x3, - k => 0x3, - bright => 0x5, - l => 0x5, - dim => 0x4, - m => 0x4, - preset_dim1 => 0xA, - preset_dim2 => 0xB, - all_off => 0x0, - p => 0x0, - all_lights_on => 0x1, - o => 0x1, - all_lights_off => 0x6, - status => 0xF, - status_on => 0xD, - status_off => 0xE, - hail_ack => 0x9, - ext_code => 0x7, - ext_data => 0xC, - hail_request => 0x8 -); - -my %mh_commands = ( - '2' => 'J', - '3' => 'K', - '5' => 'L', - '4' => 'M', - 'a' => 'preset_dim1', - 'b' => 'preset_dim2', -# '0' => 'all_off', - '0' => 'P', -# '1' => 'all_lights_on', - '1' => 'O', - '6' => 'all_lights_off', - 'f' => 'status', - 'd' => 'status_on', - 'e' => 'status_off', - '9' => 'hail_ack', - '7' => 'ext_code', - 'c' => 'ext_data', - '8' => 'hail_request' -); - -=item C - -Instantiates a new object. - -=cut - -sub new -{ - my ($class, $interface_data) = @_; - my $self= new Insteon::BaseMessage(); - bless $self,$class; - - $self->interface_data($interface_data); - $self->send_timeout(2000); - - return $self; -} - -=item C - -Converts an X10 message from the interface into the generic humand readable X10 -message format. - -=cut - -sub get_formatted_data -{ - my ($self) = @_; - - my $data = $self->interface_data; - - my $msg=undef; - if (uc(substr($data,length($data)-2,2)) eq '00') - { - $msg = "X"; - $msg.= uc($mh_house_codes{substr($data,4,1)}); - $msg.= uc($mh_unit_codes{substr($data,5,1)}); - for (my $index =6; $index - -Generates and returns the X10 hexadecimal message for sending to the PLM. - -=cut - -sub generate_commands -{ - my ($p_state, $p_setby) = @_; - - my @data = (); - - my $cmd=$p_state; - $cmd=~ s/\:.*$//; - $cmd=lc($cmd); - my $msg; - - my $id=lc($p_setby->{id_by_state}{$cmd}); - - my $hc = lc(substr($p_setby->{x10_id},1,1)); - my $uc = lc(substr($p_setby->{x10_id},2,1)); - - if ($hc eq undef) { - &main::print_log("[Insteon::Message] Object:$p_setby Doesnt have an x10 id (yet)"); - return undef; - } - - if ($uc eq undef) { - &main::print_log("[Insteon::Message] Message is for entire HC") if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); - } - else { - - #Every X10 message starts with the House and unit code - $msg = substr(unpack("H*",pack("C",$x10_house_codes{substr($id,1,1)})),1,1); - $msg.= substr(unpack("H*",pack("C",$x10_unit_codes{substr($id,2,1)})),1,1); - $msg.= "00"; - &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $uc) . " as insteon msg: " - . $msg) if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); - - push @data, $msg; - } - - my $ecmd; - #Iterate through the rest of the pairs of nibbles - my $spos = 3; - if ($uc eq undef) {$spos=1;} -# &::print_log("PLM:PAIR:$id:$spos:$ecmd:"); - for (my $pos = $spos; $posdebuglevel(1,'insteon')); - - push @data, $msg; - - } - - return @data; -} - -=back - -=head2 AUTHOR - -Gregg Limming - -=head2 LICENSE - -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. - -=cut - -1; +=head1 B + +=head2 DESCRIPTION + +Generic base class for L object messages, including +L. Primarily just +stores variables for the object. + +=head2 INHERITS + +Nothing + +=head2 METHODS + +=over + +=cut + +package Insteon::BaseMessage; + +use strict; + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class) = @_; + my $self={}; + bless $self,$class; + + $$self{queue_time} = &main::get_tickcount; + $$self{send_attempts} = 0; + + return $self; +} + +=item C + +Save and retrieve the interface data defined by C. + +=cut + +sub interface_data +{ + my ($self, $interface_data) = @_; + if ($interface_data) + { + $$self{interface_data} = $interface_data; + } + return $$self{interface_data}; +} + +=item C + +Stores the time at which a message was added to the queue. Used for calculating +how long a message was delayed. + +=cut + +sub queue_time +{ + my ($self, $queue_time) = @_; + if ($queue_time) + { + $$self{queue_time} = $queue_time; + } + return $$self{queue_time}; +} + +=item C + +Data will be evaluated each time the message is sent. + +=cut + +sub callback +{ + my ($self, $callback) = @_; + if ($callback) + { + $$self{callback} = $callback; + } + return $$self{callback}; +} + +=item C + +Data will be evaluated if the maximum number of retry attempts has been made +and the message is not acknowledged. + +=cut + +sub failure_callback +{ + my ($self, $callback) = @_; + if ($callback) + { + $$self{failure_callback} = $callback; + } + return $$self{failure_callback}; +} + +=item C + +Data will be evaluated after the receipt of an ACK from the device for this command. + +=cut + +sub success_callback +{ + my ($self, $callback) = @_; + if (defined $callback) + { + $$self{success_callback} = $callback; + } + return $$self{success_callback}; +} + +=item C + +Stores and retrieves the number of times Misterhouse has tried to send the message. + +=cut + +sub send_attempts +{ + my ($self, $send_attempts) = @_; + if ($send_attempts) + { + $$self{send_attempts} = $send_attempts; + } + return $$self{send_attempts}; +} + +=item C + +Stores and retrieves what the source of this message was. + +=cut + +sub setby +{ + my ($self, $setby) = @_; + if ($setby) + { + $$self{setby} = $setby; + } + return $$self{setby}; +} + +=item C + +Stores and retrieves respond variable. + +=cut + +sub respond +{ + my ($self, $respond) = @_; + if ($respond) + { + $$self{respond} = $respond; + } + return $$self{respond}; +} + + +=item C + +Stores and retrieves no_hop_increase variable, if set to true, when the message +is retried, no additional hops will be added. Typically used where the failure +to deliver the last message attempt was not caused by a failure of the object +to receive the message such as when the PLM is too busy to even attempt sending +the message. + +=cut + +sub no_hop_increase +{ + my ($self, $no_hop_increase) = @_; + if ($no_hop_increase) + { + $$self{no_hop_increase} = $no_hop_increase; + } + return $$self{no_hop_increase}; +} + +=item C + +Stores and retrieves the number of times MisterHouse should try to deliver this +message. If B is set in the ini parameters will default +to that value, otherwise defaults to 5. Some messages types have specific +retry counts, such as L +battery level requests which are only sent once. + +=cut + +sub retry_count { + my ($self, $retry_count) = @_; + if ($retry_count) + { + $$self{retry_count} = $retry_count; + } + my $result_retry = 5; + $result_retry = $::config_parms{'Insteon_retry_count'} if ($::config_parms{'Insteon_retry_count'}); + $result_retry = $$self{retry_count} if ($$self{retry_count}); + return $result_retry; +} + +=item C + +Sends this message using the interface p_interface. If C is +greater than 0 then +L +is increase by one. Calls C +when the message is sent. + +Returns 1 if message sent or 0 if message retry count exceeds C. + +=cut + +sub send +{ + my ($self, $interface) = @_; + if ($self->send_attempts < $self->retry_count) + { + + if ($self->send_attempts > 0) + { + &::print_log("[Insteon::BaseMessage] WARN: now resending " + . $self->to_string() . " after " . $self->send_attempts + . " attempts.") if $self->setby->debuglevel(1, 'insteon'); + # revise default hop count to reflect retries + if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') + && !defined($$self{no_hop_increase})) + { + $self->setby->retry_count_log(1) if $self->setby->can('retry_count_log'); + if ($self->setby->default_hop_count < 3) + { + $self->setby->default_hop_count($self->setby->default_hop_count + 1); + } + } + elsif (defined($$self{no_hop_increase}) && ref $self->setby + && $self->setby->isa('Insteon::BaseObject')){ + &main::print_log("[Insteon::BaseMessage] Hop count not increased for " + . $self->setby->get_object_name . " because no_hop_increase flag was set.") + if $self->setby->debuglevel(1, 'insteon'); + $$self{no_hop_increase} = undef; + } + } + + # need to set timeout as a function of retries; also need to alter hop count + if ($self->send_attempts <= 0 && ref $self->setby) { + $self->setby->outgoing_count_log(1) if $self->setby->can('outgoing_count_log'); + $self->setby->outgoing_hop_count($self->setby->default_hop_count) + if $self->setby->can('outgoing_hop_count'); + } + $self->send_attempts($self->send_attempts + 1); + $interface->_send_cmd($self, $self->send_timeout); + if ($self->callback) + { + package main; + eval $self->callback; + &::print_log("[Insteon::BaseMessage] problem w/ retry callback: $@") if $@; + package Insteon::Message; + } + return 1; + } + else + { + return 0; + } + +} + +=item C + +Returns the number of seconds that elapsed between time set in C +and when this routine was called. + +=cut + +sub seconds_delayed +{ + my ($self) = @_; + my $current_tickcount = &main::get_tickcount; + my $delay_time = $current_tickcount - $self->queue_time; + if ($self->queue_time > $current_tickcount) + { + return 'unknown'; + } + + $delay_time = $delay_time / 1000; + return $delay_time; +} + +=item C + +Stores and returns the number of milliseconds, p_timeout, that MisterHouse +should wait before retrying this message again. + +=cut + +sub send_timeout +{ + my ($self, $timeout) = @_; + $$self{send_timeout} = $timeout if defined $timeout; + return $$self{send_timeout}; +} + +=item C + +Returns the hexadecimal representation of the message. + +=cut + +sub to_string +{ + my ($self) = @_; + return $self->interface_data; +} + +=back + +=head2 INI PARAMETERS + +=over + +=item Insteon_retry_count + +Sets the number of times MisterHouse will attempt to resend a message that has +not been acknowledged. The default setting is 5. + +=back + +=head2 AUTHOR + +Gregg Limming, Kevin Robert Keegan + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 DESCRIPTION + +Main class for all L messages. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::InsteonMessage; +use strict; + +@Insteon::InsteonMessage::ISA = ('Insteon::BaseMessage'); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class, $command_type, $setby, $command, $extra) = @_; + my $self= new Insteon::BaseMessage(); + bless $self,$class; + + $self->command_type($command_type); + $self->setby($setby); + $self->command($command); + $self->extra($extra); + $self->send_timeout(2000); + + return $self; +} + +=item C + +Takes msg, a hexadecimal message, and returns a hash of the message details. + +=cut + +sub command_to_hash +{ + my ($p_state) = @_; + my %msg = (); + my $hopflag = hex(uc substr($p_state,13,1)); + $msg{maxhops} = $hopflag&0b0011; + $msg{hopsleft} = $hopflag >> 2; + my $msgflag = hex(uc substr($p_state,12,1)); + $msg{is_extended} = (0x01 & $msgflag) ? 1 : 0; + $msg{cmd_code} = substr($p_state,14,2); + $msg{crc_valid} = 1; + if ($msg{is_extended}) + { + $msg{type} = 'direct'; + $msg{source} = substr($p_state,0,6); + $msg{destination} = substr($p_state,6,6); + $msg{extra} = substr($p_state,16,length($p_state)-16); + $msg{crc_valid} = (calculate_checksum($msg{cmd_code}.$msg{extra}) eq '00'); + } + else + { + $msg{source} = substr($p_state,0,6); + $msgflag = $msgflag >> 1; + if ($msgflag == 4) + { + $msg{type} = 'broadcast'; + $msg{devcat} = substr($p_state,6,4); + $msg{firmware} = substr($p_state,10,2); + $msg{is_master} = substr($p_state,16,2); + $msg{dev_attribs} = substr($p_state,18,2); + } + elsif ($msgflag ==6) + { + $msg{type} = 'alllink'; + $msg{group} = substr($p_state,10,2); + $msg{extra} = substr($p_state,16,2) + if (length($p_state) >= 18); + } + else + { + $msg{destination} = substr($p_state,6,6); + if ($msgflag == 2) + { + $msg{type} = 'cleanup'; + $msg{group} = substr($p_state,16,2); + } + elsif ($msgflag == 3) + { + $msg{type} = 'cleanup'; + $msg{is_ack} = 1; + # the "extra" value will contain the controller's group ID + $msg{extra} = substr($p_state,16,2); + } + elsif ($msgflag == 7) + { + $msg{type} = 'cleanup'; + $msg{is_nack} = 1; + $msg{extra} = substr($p_state,16,2); + } + elsif ($msgflag == 0) + { + $msg{type} = 'direct'; + $msg{extra} = substr($p_state,16,2); + } + elsif ($msgflag == 1) + { + $msg{type} = 'direct'; + $msg{is_ack} = 1; + $msg{extra} = substr($p_state,16,2); + } + elsif ($msgflag == 5) + { + $msg{type} = 'direct'; + $msg{is_nack} = 1; + $msg{extra} = substr($p_state,16,2); + } + } + } + + return %msg; +} + +=item C + +Stores and retrieves the Cmd1 value for this message. + +=cut + +sub command +{ + my ($self, $command) = @_; + $$self{command} = $command if $command; + return $$self{command}; +} + +=item C + +Stores and retrieves the Command Type value for this message. + +=cut + +sub command_type +{ + my ($self, $command_type) = @_; + $$self{command_type} = $command_type if $command_type; + return $$self{command_type}; +} + +=item C + +Stores and retrieves the extra value for this message. For standard messages +extra is Cmd2. For extended messages extra is Cmd2 + D1-D14. + +=cut + +sub extra +{ + my ($self, $extra) = @_; + $$self{extra} = $extra if $extra; + return $$self{extra}; +} + +=item C + +Calculates and returns the number of milliseconds that MisterHouse should wait +for this message to be delivered. The time is based on message type, command type, +and hop count. + + Peek Related Messages = 4000 + PLM Scene Commands = 3000 + Ext / Std + 0 Hop 2220 1400 + 1 Hop 2690 1700 + 2 Hop 3000 1900 + 3 Hop 3170 2000 + +These times were intially calculated by Gregg Limming and appear to have been +calculated based on experience. In reality each hop of a standard message +should take 50 ms and 108 for extended messages. Each time needs to be at least +doubled to compensate for the return hops as well. + +In reality, the PLM and even some Insteon devices appear to react much slower +than the spec defines. These settings generally appear to work without causing +errors or too much undue delay. + +=cut + +sub send_timeout +{ + my ($self, $ignore) = @_; + my $hop_count = (ref $self->setby and $self->setby->isa('Insteon::BaseObject')) ? + $self->setby->default_hop_count : $self->send_attempts; + my $timeout = 1400; + if($self->command eq 'peek' || $self->command eq 'set_address_msb') + { + $timeout = 4000; + } + elsif ($self->command_type eq 'all_link_send') + { + # note, the following was set to 2000 and that was insufficient + $timeout = 3000; + } + elsif ($self->command_type eq 'insteon_ext_send') + { + if ($hop_count == 0) + { + $timeout = 2220; + } + elsif ($hop_count == 1) + { + $timeout = 2690; + } + elsif ($hop_count == 2) + { + $timeout = 3000; + } + elsif ($hop_count >= 3) + { + $timeout = 3170; + } + } + else + { + if ($hop_count == 0) + { + $timeout = 1400; + } + elsif ($hop_count == 1) + { + $timeout = 1700; + } + elsif ($hop_count == 2) + { + $timeout = 1900; + } + elsif ($hop_count >= 3) + { + $timeout = 2000; + } + } + if (ref $self->setby and $self->setby->isa('Insteon::BaseObject')){ + $timeout = int($timeout * $self->setby->timeout_factor); + } + return $timeout; +} + +=item C + +Returns text based human readable representation of the message. + +=cut + +sub to_string +{ + my ($self) = @_; + my $result = ''; + if ($self->setby) + { + $result .= 'obj=' . $self->setby->get_object_name; + } + if ($result) + { + $result .= '; '; + } + if ($self->command) + { + $result .= 'command=' . $self->command; + } + else + { + $result .= 'interface_data=' . $self->interface_data; + } + if ($self->extra) + { + $result .= '; extra=' . $self->extra; + } + + return $result; +} + +=item C + +Stores data as the hexadecimal message, or if data is not specified, then derives +the hexadecimal message and returns it. + +=cut + +sub interface_data +{ + my ($self, $interface_data) = @_; + my $result = $self->SUPER::interface_data($interface_data); + if (!($result) && + (($self->command_type eq 'insteon_send') + or ($self->command_type eq 'insteon_ext_send') + or ($self->command_type eq 'all_link_send') + or ($self->command_type eq 'all_link_direct_cleanup'))) + { + return $self->_derive_interface_data(); + } + else + { + return $result; + } +} + +=item C<_derive_interface_data()> + +Converts all of the attributes set for this message into a hexadecimal message +that can be sent to the PLM. Will add checksums and crcs when necessary. + +=cut + +sub _derive_interface_data +{ + + my ($self) = @_; + my $cmd = ''; + my $level; + if ($self->command_type =~ /all_link_send/i) + { + $cmd.=$self->setby->group; + } + else + { + my $hop_count = $self->setby->default_hop_count; + $cmd.=$self->setby->device_id(); + if ($self->command_type =~ /insteon_ext_send/i) + { + if ($hop_count == 0) + { + $cmd.='10'; + } + elsif ($hop_count == 1) + { + $cmd.='15'; + } + elsif ($hop_count == 2) + { + $cmd.='1A'; + } + elsif ($hop_count >= 3) + { + $cmd.='1F'; + } + } elsif ($self->command_type =~ /all_link_direct_cleanup/i){ + if ($hop_count == 0) + { + $cmd.='40'; + } + elsif ($hop_count == 1) + { + $cmd.='45'; + } + elsif ($hop_count == 2) + { + $cmd.='4A'; + } + elsif ($hop_count >= 3) + { + $cmd.='4F'; + } + } + else + { + if ($hop_count == 0) + { + $cmd.='00'; + } + elsif ($hop_count == 1) + { + $cmd.='05'; + } + elsif ($hop_count == 2) + { + $cmd.='0A'; + } + elsif ($hop_count >= 3) + { + $cmd.='0F'; + } + } + } + $cmd.= unpack("H*",pack("C",$self->setby->message_type_code($self->command))); + if ($self->extra) + { + $cmd.= $self->extra; + } + elsif ($self->command_type eq 'insteon_send') + { # auto append '00' if no extra defined for a standard insteon send + $cmd .= '00'; + } + + if( $self->command_type eq 'insteon_ext_send' and $$self{add_crc16}){ + if( length($cmd) < 40) { + main::print_log("[Insteon::InsteonMessage] WARN: insert_crc16 " + . "failed; cmd to short: $cmd"); + } else { + $cmd = substr($cmd,0,36).calculate_crc16(substr($cmd,8,28)); + } + } + elsif( $self->command_type eq 'insteon_ext_send' and $self->setby->engine_version eq 'I2CS') { + #$message is the entire insteon command (no 0262 PLM command) + # i.e. '02622042d31f2e000107110000000000000000000000' + # 111111111122222222223333333333 + # 0123456789012345678901234567890123456789 + # '2042d31f2e000107110000000000000000000000' + if( length($cmd) < 40) { + main::print_log("[Insteon::InsteonMessage] WARN: insert_checksum " + . "failed; cmd to short: $cmd"); + } else { + $cmd = substr($cmd,0,38).calculate_checksum(substr($cmd,8,30)); + } + } + + return $cmd; + +} + +=item C + +Calculates a checksum of all hex bytes in the string. Returns two hex nibbles +that represent the checksum in hex. One useful characteristic of the checksum +is that summing over all the bytes "including" the checksum will always equal 00. +This makes it very easy to validate a checksum. + +=cut + +sub calculate_checksum { + my ($string) = @_; + + #returns 2 characters as hex nibbles (e.g. AA) + my $sum = 0; + $sum += hex($_) for (unpack('(A2)*', $string)); + return unpack( 'H2', chr((~$sum + 1) & 0xff)); +} + +=item C + +Calculates a two byte CRC value of string. This two byte CRC differs from the +one byte checksum used in other extended commands. This CRC calculation is known +to be used by the 2441TH Insteon Thermostat as well as the iMeter INSTEON device. +It may be used by other devices in the future. + +The calculation if the crc value involves data bytes from command 1 to the data 12 +byte. This function will return two bytes, which are generally added to the +data 13 & 14 bytes in an extended message. + +To add a crc16 to a message set the $$message{add_crc16} flag to true. + +=cut + +sub calculate_crc16 +{ + #This function is nearly identical to the C++ sample provided by + #smartlabs, with only minor modifications to make it work in perl + my ($string) = @_; + my $crc = 0; + for(unpack('(A2)*', $string)) + { + my $byte = hex($_); + + for(my $bit = 0;$bit < 8;$bit++) + { + my $fb = $byte & 1; + $fb = ($crc & 0x8000) ? $fb ^ 1 : $fb; + $fb = ($crc & 0x4000) ? $fb ^ 1 : $fb; + $fb = ($crc & 0x1000) ? $fb ^ 1 : $fb; + $fb = ($crc & 0x0008) ? $fb ^ 1 : $fb; + $crc = (($crc << 1) & 0xFFFF) | $fb; + $byte = $byte >> 1; + } + } + return uc(sprintf("%x", $crc)); +} + +=back + +=head2 AUTHOR + +Gregg Limming, Kevin Robert Keegan, Michael Stovenour + +=head2 LICENSE + +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. + +=cut + +=head1 B + +=head2 DESCRIPTION + +Main class for all L X10 messages. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package Insteon::X10Message; +use strict; + +@Insteon::X10Message::ISA = ('Insteon::BaseMessage'); + +my %x10_house_codes = ( + a => 0x6, + b => 0xE, + c => 0x2, + d => 0xA, + e => 0x1, + f => 0x9, + g => 0x5, + h => 0xD, + i => 0x7, + j => 0xF, + k => 0x3, + l => 0xB, + m => 0x0, + n => 0x8, + o => 0x4, + p => 0xC +); + +my %mh_house_codes = ( + '6' => 'a', + 'e' => 'b', + '2' => 'c', + 'a' => 'd', + '1' => 'e', + '9' => 'f', + '5' => 'g', + 'd' => 'h', + '7' => 'i', + 'f' => 'j', + '3' => 'k', + 'b' => 'l', + '0' => 'm', + '8' => 'n', + '4' => 'o', + 'c' => 'p' +); + +my %x10_unit_codes = ( + 1 => 0x6, + 2 => 0xE, + 3 => 0x2, + 4 => 0xA, + 5 => 0x1, + 6 => 0x9, + 7 => 0x5, + 8 => 0xD, + 9 => 0x7, + 10 => 0xF, + a => 0xF, + 11 => 0x3, + b => 0x3, + 12 => 0xB, + c => 0xB, + 13 => 0x0, + d => 0x0, + 14 => 0x8, + e => 0x8, + 15 => 0x4, + f => 0x4, + 16 => 0xC, + g => 0xC + +); + +my %mh_unit_codes = ( + '6' => '1', + 'e' => '2', + '2' => '3', + 'a' => '4', + '1' => '5', + '9' => '6', + '5' => '7', + 'd' => '8', + '7' => '9', + 'f' => 'a', + '3' => 'b', + 'b' => 'c', + '0' => 'd', + '8' => 'e', + '4' => 'f', + 'c' => 'g' +); + +my %x10_commands = ( + on => 0x2, + j => 0x2, + off => 0x3, + k => 0x3, + bright => 0x5, + l => 0x5, + dim => 0x4, + m => 0x4, + preset_dim1 => 0xA, + preset_dim2 => 0xB, + all_off => 0x0, + p => 0x0, + all_lights_on => 0x1, + o => 0x1, + all_lights_off => 0x6, + status => 0xF, + status_on => 0xD, + status_off => 0xE, + hail_ack => 0x9, + ext_code => 0x7, + ext_data => 0xC, + hail_request => 0x8 +); + +my %mh_commands = ( + '2' => 'J', + '3' => 'K', + '5' => 'L', + '4' => 'M', + 'a' => 'preset_dim1', + 'b' => 'preset_dim2', +# '0' => 'all_off', + '0' => 'P', +# '1' => 'all_lights_on', + '1' => 'O', + '6' => 'all_lights_off', + 'f' => 'status', + 'd' => 'status_on', + 'e' => 'status_off', + '9' => 'hail_ack', + '7' => 'ext_code', + 'c' => 'ext_data', + '8' => 'hail_request' +); + +=item C + +Instantiates a new object. + +=cut + +sub new +{ + my ($class, $interface_data) = @_; + my $self= new Insteon::BaseMessage(); + bless $self,$class; + + $self->interface_data($interface_data); + $self->send_timeout(2000); + + return $self; +} + +=item C + +Converts an X10 message from the interface into the generic humand readable X10 +message format. + +=cut + +sub get_formatted_data +{ + my ($self) = @_; + + my $data = $self->interface_data; + + my $msg=undef; + if (uc(substr($data,length($data)-2,2)) eq '00') + { + $msg = "X"; + $msg.= uc($mh_house_codes{substr($data,4,1)}); + $msg.= uc($mh_unit_codes{substr($data,5,1)}); + for (my $index =6; $index + +Generates and returns the X10 hexadecimal message for sending to the PLM. + +=cut + +sub generate_commands +{ + my ($p_state, $p_setby) = @_; + + my @data = (); + + my $cmd=$p_state; + $cmd=~ s/\:.*$//; + $cmd=lc($cmd); + my $msg; + + my $id=lc($p_setby->{id_by_state}{$cmd}); + + my $hc = lc(substr($p_setby->{x10_id},1,1)); + my $uc = lc(substr($p_setby->{x10_id},2,1)); + + if ($hc eq undef) { + &main::print_log("[Insteon::Message] Object:$p_setby Doesnt have an x10 id (yet)"); + return undef; + } + + if ($uc eq undef) { + &main::print_log("[Insteon::Message] Message is for entire HC") if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); + } + else { + + #Every X10 message starts with the House and unit code + $msg = substr(unpack("H*",pack("C",$x10_house_codes{substr($id,1,1)})),1,1); + $msg.= substr(unpack("H*",pack("C",$x10_unit_codes{substr($id,2,1)})),1,1); + $msg.= "00"; + &main::print_log("[Insteon_PLM] x10 sending code: " . uc($hc . $uc) . " as insteon msg: " + . $msg) if (ref $p_setby && $p_setby->debuglevel(1,'insteon')); + + push @data, $msg; + } + + my $ecmd; + #Iterate through the rest of the pairs of nibbles + my $spos = 3; + if ($uc eq undef) {$spos=1;} +# &::print_log("PLM:PAIR:$id:$spos:$ecmd:"); + for (my $pos = $spos; $posdebuglevel(1,'insteon')); + + push @data, $msg; + + } + + return @data; +} + +=back + +=head2 AUTHOR + +Gregg Limming + +=head2 LICENSE + +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. + +=cut + +1; diff --git a/lib/Insteon/MessageDecoder.pm b/lib/Insteon/MessageDecoder.pm index a456dbc25..ee1c2a70b 100644 --- a/lib/Insteon/MessageDecoder.pm +++ b/lib/Insteon/MessageDecoder.pm @@ -1,953 +1,953 @@ -package Insteon::MessageDecoder; - -use strict; - -=head1 NAME - -B - Static class for decoding Insteon PLM messages - -=head1 SYNOPSIS - - use Insteon::MessageDecoder; - my $decodedMessage = Insteon::MessageDecoder::plm_decode($plm_string); - -=head1 DESCRIPTION - -Insteon::MessageDecoder will decode Insteon PLM messages. Functions are -provided to decode the PLM envelope, X10 commands, Insteon flags, and -Insteon Cmd1/Cmd2 bytes. User data (D1-D14) of extended messages is not -decoded but will be displayed. - -=head1 EXAMPLE - - use Insteon::MessageDecoder; - my $plm_string; - - $plm_string = '02621f058c1f2e000100000000000000000000000000'; - print( "PLM Message: $plm_string\n"); - print( Insteon::MessageDecoder::plm_decode($plm_string)."\n"); - - $plm_string = '02511f058c1edc30112e000101000020201cfe3f0001000000'; - print( "PLM Message: $plm_string\n"); - print( Insteon::MessageDecoder::plm_decode($plm_string)."\n"); - -=head1 LIMITATIONS - -The message decoder is not perfect. It does not keep message state and -several of the Insteon ACK message formats can "only" be decoded relative -to the most recent command sent to the ACKing device. Some ACK messages -will be incorrectly decoded as part of another Insteon message. For -example in the ACK for a "Light Status Request", cmd1 is the ALDB serial -number. This serial number will be interpreted as another Insteon message -where the serial number matches the cmd1 value. Be aware of this and you -should still be able to intrepret the decided messages. - -Extended messages are not decoded and only display the D1-D14 hex data. -You will need to manually decode the extended data. Patches for decoding -one or more extended messages are welcome. - -=head1 BUGS - -There are probably many bugs. The decoder was designed by transcribing -Insteon documentation, Jonathan Dale's excellent command reference, and -reviewing many message board discussions. The decoder has not been -tested with all devices (of all firmware revs) or all PLM combinations -(of all firmware revs). There are bugs; please report them in the -misterhouse GitHub issues list. - -=head1 METHODS - -=over - -=cut - -#These constants are intentionally copied here from other Insteon modules -#This is so any changes to those structures do not introduce errors -#in the decoders. These constants should only be modified to extend -#the decoders or correct defects in the decoders. - -#PLM Serial Commands -my %plmcmd = ( - insteon_received => '0250', - insteon_ext_received => '0251', - x10_received => '0252', - all_link_complete => '0253', - plm_button_event => '0254', - user_plm_reset => '0255', - all_link_clean_failed => '0256', - all_link_record => '0257', - all_link_clean_status => '0258', - plm_info => '0260', - all_link_send => '0261', - insteon_send => '0262', -# insteon_ext_send => '0262', - x10_send => '0263', - all_link_start => '0264', - all_link_cancel => '0265', - set_host_device_cat => '0266', - plm_reset => '0267', - set_insteon_ack_cmd2 => '0268', - all_link_first_rec => '0269', - all_link_next_rec => '026a', - plm_set_config => '026b', - get_sender_all_link_rec => '026c', - plm_led_on => '026d', - plm_led_off => '026e', - all_link_manage_rec => '026f', - insteon_nak => '0270', - insteon_ack => '0271', - rf_sleep => '0272', - plm_get_config => '0273' -); - -#create a backwards lookup on hex code -my %plmcmd2string = reverse %plmcmd; - -my %plmcmdlen = ( - '0250' => [11, 11], - '0251' => [25, 25], - '0252' => [4, 4], - '0253' => [10, 10], - '0254' => [3, 3], - '0255' => [2, 2], - '0256' => [6, 6], - '0257' => [10, 10], - '0258' => [3, 3], - '0260' => [2, 9], - '0261' => [5, 6], - '0262' => [8, 9, 22, 23], # could get 9 or 23 (Standard or Extended Message received) - '0263' => [4, 5], - '0264' => [4, 5], - '0265' => [2, 3], - '0266' => [5, 6], - '0267' => [2, 3], - '0268' => [3, 4], - '0269' => [2, 3], - '026A' => [2, 3], - '026B' => [3, 4], - '026C' => [2, 3], - '026D' => [2, 3], - '026E' => [2, 3], - '026F' => [11, 12], - '0270' => [3, 4], - '0271' => [4, 5], - '0272' => [2, 3], - '0273' => [5, 6], - ); - - -#Mapping from message type bit field to acronyms used in -# the INSTEON Command Tables documentation -#100 4 - SB: Standard Broadcast - -#000 0 - SD or ED: Standard/Extended Direct -#001 1 - SDA or EDA: Standard/Extended Direct ACK -#101 5 - SDN or EDN: Standard/Extended Direct NACK - -#110 6 - SA: Standard All-Link Broadcast -#010 2 - SC: Standard Cleanup Direct -#011 3 - SCA: Standard Cleanup Direct ACK -#111 7 - SCN: Standard Cleanup Direct NACK - -#List below is maintained in an Excel spreadsheet. Make -#changes there and cut-n-paste list to here -#You should understand the parsing logic before attempting -#to modify this table! -my %insteonCmd = ( -'SD01' => {Cmd1Name=>'Assign to All-Link Group',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SB01' => {Cmd1Name=>'SET Button Pressed Respond',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD02' => {Cmd1Name=>'Delete from All-Link Group',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SB02' => {Cmd1Name=>'SET Button Pressed Controller',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD03' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD0300' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Product Data Request'}, -'SD0301' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'FxName Request'}, -'SD0302' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Device Text String Request'}, -'ED03' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED0300' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Product Data Response'}, -'ED0301' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'FX Username Response'}, -'ED0302' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Device Text String Response'}, -'ED0303' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Set Device Text String'}, -'ED0304' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Set ALL-Link Command Alias'}, -'ED0305' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Set ALL-Link Command Alias ED'}, -'SB03' => {Cmd1Name=>'Test Powerline Phase',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SB0300' => {Cmd1Name=>'Test Powerline Phase',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Phase A'}, -'SB0301' => {Cmd1Name=>'Test Powerline Phase',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Phase B'}, -'SB04' => {Cmd1Name=>'Heartbeat',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Battery Level'}, -'SA06' => {Cmd1Name=>'All-Link Cleanup Report',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Fail Count'}, -'SD09' => {Cmd1Name=>'Enter Linking Mode',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD0a' => {Cmd1Name=>'Enter Unlinking Mode',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD0d' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SDA0d' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SDA0d00' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'i1'}, -'SDA0d01' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'i2'}, -'SDA0d02' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'i2CS'}, -'SD0f' => {Cmd1Name=>'Ping',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD10' => {Cmd1Name=>'ID Request',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD11' => {Cmd1Name=>'Light ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Level'}, -'SA11' => {Cmd1Name=>'ALL-Link Recall',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC11' => {Cmd1Name=>'ALL-Link Recall',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD12' => {Cmd1Name=>'Light ON Fast',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Level'}, -'SA12' => {Cmd1Name=>'ALL-Link Alias 2 High',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC12' => {Cmd1Name=>'ALL-Link Alias 2 High',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD13' => {Cmd1Name=>'Light OFF',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SA13' => {Cmd1Name=>'ALL-Link Alias 1 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC13' => {Cmd1Name=>'ALL-Link Alias 1 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD14' => {Cmd1Name=>'Light OFF Fast',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SA14' => {Cmd1Name=>'ALL-Link Alias 2 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC14' => {Cmd1Name=>'ALL-Link Alias 2 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD15' => {Cmd1Name=>'Light Brighten One Step',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SA15' => {Cmd1Name=>'ALL-Link Alias 3 High',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC15' => {Cmd1Name=>'ALL-Link Alias 3 High',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD16' => {Cmd1Name=>'Light Dim One Step',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SA16' => {Cmd1Name=>'ALL-Link Alias 3 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC16' => {Cmd1Name=>'ALL-Link Alias 3 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD17' => {Cmd1Name=>'Light Start Manual Change',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD1700' => {Cmd1Name=>'Light Start Manual Change',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Down'}, -'SD1701' => {Cmd1Name=>'Light Start Manual Change',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Up'}, -'SA17' => {Cmd1Name=>'ALL-Link Alias 4 High',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC17' => {Cmd1Name=>'ALL-Link Alias 4 High',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD18' => {Cmd1Name=>'Light Stop Manual Change',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SA18' => {Cmd1Name=>'ALL-Link Alias 4 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC18' => {Cmd1Name=>'ALL-Link Alias 4 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD19' => {Cmd1Name=>'Light Status Request',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD1900' => {Cmd1Name=>'Light Status Request',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'On Level'}, -'SD1901' => {Cmd1Name=>'Light Status Request',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'LED Bit Flags'}, -'SD1f' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD1f00' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Request Flags'}, -'SD1f01' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'All-Link Database Delta Number'}, -'SD1f02' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Signal-to-Noise'}, -'SDA1f' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Config Flags'}, -'SD20' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD2000' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Program Lock On'}, -'SD2001' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Program Lock Off'}, -'SD2002' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Deveice Dependent'}, -'SD2003' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Deveice Dependent'}, -'SD2004' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Deveice Dependent'}, -'SD2005' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Deveice Dependent'}, -'SD2006' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Deveice Dependent'}, -'SD2007' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'Deveice Dependent'}, -'SD2008' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'Deveice Dependent'}, -'SD2009' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Deveice Dependent'}, -'SD200a' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Deveice Dependent'}, -'SD200b' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Deveice Dependent'}, -'SD21' => {Cmd1Name=>'Light Instant Change',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'On Level'}, -'SA21' => {Cmd1Name=>'ALL-Link Alias 5',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SC21' => {Cmd1Name=>'ALL-Link Alias 5',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, -'SD22' => {Cmd1Name=>'Light Manually Turned Off',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD23' => {Cmd1Name=>'Light Manually Turned On',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD24' => {Cmd1Name=>'Reread Init Values(Deprecated)',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD25' => {Cmd1Name=>'Remote SET Button Tap',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD2501' => {Cmd1Name=>'Remote SET Button Tap',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'1 Tap'}, -'SD2502' => {Cmd1Name=>'Remote SET Button Tap',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'2 Taps'}, -'SD27' => {Cmd1Name=>'Light Set Status',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'On Level'}, -'SB27' => {Cmd1Name=>'Status Change',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, -'SD28' => {Cmd1Name=>'Set Address MSB(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'MSB'}, -'SD29' => {Cmd1Name=>'Poke One Byte(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, -'ED2a' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED2a00' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Transfer Failure'}, -'ED2a01' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Complete (1 byte)'}, -'ED2a02' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Complete (2 bytes)'}, -'ED2a03' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Complete (3 bytes)'}, -'ED2a04' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Complete (4 bytes)'}, -'ED2a05' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Complete (5 bytes)'}, -'ED2a06' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Complete (6 bytes)'}, -'ED2a07' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'Complete (7 bytes)'}, -'ED2a08' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'Complete (8 bytes)'}, -'ED2a09' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Complete (9 bytes)'}, -'ED2a0a' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Complete (10 bytes)'}, -'ED2a0b' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Complete (11 bytes)'}, -'ED2a0c' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Complete (12 bytes)'}, -'ED2a0d' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Complete (13 bytes)'}, -'ED2aff' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0xff',Cmd2Name=>'Request Block Data Transfer'}, -'SD2b' => {Cmd1Name=>'Peek One Byte(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'LSB of Address'}, -'SDA2b' => {Cmd1Name=>'Peek One Byte(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Peeked Byte'}, -'SD2c' => {Cmd1Name=>'Peek One Byte Internal(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'LSB of Address'}, -'SDA2c' => {Cmd1Name=>'Peek One Byte Internal(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Peeked Byte'}, -'SD2d' => {Cmd1Name=>'Poke One Byte Internal(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, -'SD2e' => {Cmd1Name=>'Light ON at Ramp Rate',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Level and Rate'}, -'ED2e' => {Cmd1Name=>'Extended Set/Get',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED2e00' => {Cmd1Name=>'Extended Set/Get',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Command in D2'}, -'SD2f' => {Cmd1Name=>'Light OFF at Ramp Rate',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Ramp Rate'}, -'ED2f' => {Cmd1Name=>'Read/Write ALL-Link Database',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED2f00' => {Cmd1Name=>'Read/Write ALL-Link Database',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Command in D2'}, -'SD30' => {Cmd1Name=>'Beep',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Duration'}, -'ED30' => {Cmd1Name=>'Trigger ALL-Link Command',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED3000' => {Cmd1Name=>'Trigger ALL-Link Command',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Trigger Command'}, -'SD40' => {Cmd1Name=>'Sprinkler Valve On',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Valve Number'}, -'ED40' => {Cmd1Name=>'Set Sprinkler Program',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, -'SD41' => {Cmd1Name=>'Sprinkler Valve Off',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Valve Number'}, -'ED41' => {Cmd1Name=>'Sprinkler Get Program Response',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, -'SD42' => {Cmd1Name=>'Sprinkler Program ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, -'SD43' => {Cmd1Name=>'Sprinkler Program OFF',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, -'SD44' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD4400' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, -'SD4401' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM From RAM'}, -'SD4402' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Get Valve Status'}, -'SD4403' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Inhibit Command Acceptance'}, -'SD4404' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Resume Command Acceptance'}, -'SD4405' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Skip Forward'}, -'SD4406' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Skip Backwards'}, -'SD4407' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'Enable Pump on V8'}, -'SD4408' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'Disable Pump on V8'}, -'SD4409' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Broadcast ON'}, -'SD440a' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Broadcast OFF'}, -'SD440b' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Load RAM from EEPROM'}, -'SD440c' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Sensor ON'}, -'SD440d' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Sensor OFF'}, -'SD440e' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0e',Cmd2Name=>'Diagnostics ON'}, -'SD440f' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0f',Cmd2Name=>'Diagnostics OFF'}, -'SD45' => {Cmd1Name=>'I/O Output ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Output Number'}, -'SD46' => {Cmd1Name=>'I/O Output OFF',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Output Number'}, -'SD47' => {Cmd1Name=>'I/O Alarm Data Request',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD48' => {Cmd1Name=>'I/O Write Output Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, -'SDA48' => {Cmd1Name=>'I/O Write Output Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data Written'}, -'SD49' => {Cmd1Name=>'I/O Read Input Port',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SDA49' => {Cmd1Name=>'I/O Read Input Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data Read'}, -'SD4a' => {Cmd1Name=>'Get Sensor Value',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Sensor Number'}, -'SDA4a' => {Cmd1Name=>'Get Sensor Value',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Sensor Value'}, -'SD4b' => {Cmd1Name=>'Set Sensor 1 Alarm Trigger OFF->ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Nominal Value'}, -'ED4b' => {Cmd1Name=>'I/O Set Sensor Nominal',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Sensor Number'}, -'SD4c' => {Cmd1Name=>'I/O Get Sensor Alarm Delta',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, -'ED4c' => {Cmd1Name=>'I/O Alarm Data Response',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED4c00' => {Cmd1Name=>'I/O Alarm Data Response',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Response'}, -'SD4d' => {Cmd1Name=>'I/O Write Configuration Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, -'SD4e' => {Cmd1Name=>'I/O Read Configuration Port',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'SD4e' => {Cmd1Name=>'I/O Read Configuration Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'I/O Port Config'}, -'SD4f' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD4f00' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, -'SD4f01' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM from RAM'}, -'SD4f02' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Status Request'}, -'SD4f03' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Read Analog once'}, -'SD4f04' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Read Analog Always'}, -'SD4f09' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Enable status change message'}, -'SD4f0a' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Disable status change message'}, -'SD4f0b' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Load RAM from EEPROM'}, -'SD4f0c' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Sensor On'}, -'SD4f0d' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Sensor Off'}, -'SD4f0e' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0e',Cmd2Name=>'Diagnostics On'}, -'SD4f0f' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0f',Cmd2Name=>'Diagnostics Off'}, -'SD50' => {Cmd1Name=>'Pool Device ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Device Number'}, -'ED50' => {Cmd1Name=>'Pool Set Device Temperature',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'ED5000' => {Cmd1Name=>'Pool Set Device Temperature',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Set Temperature'}, -'ED5001' => {Cmd1Name=>'Pool Set Device Temperature',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Set Hysteresis'}, -'SD51' => {Cmd1Name=>'Pool Device OFF',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Device Number'}, -'SD52' => {Cmd1Name=>'Pool Temperature Up',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Increment Count'}, -'SD53' => {Cmd1Name=>'Pool Temperature Down',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Decrement Count'}, -'SD54' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD5400' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, -'SD5401' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM From RAM'}, -'SD5402' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Get Pool Mode'}, -'SD5403' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Get Ambient Temp'}, -'SD5404' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Get Water Temp'}, -'SD5405' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Get pH'}, -'SD58' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD5800' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Raise Door'}, -'SD5801' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Lower Door'}, -'SD5802' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Open Door'}, -'SD5803' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Close Door'}, -'SD5804' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Stop Door'}, -'SD5805' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Single Door Open'}, -'SD5806' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Single Door Close'}, -'SD59' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD5900' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Raise Door'}, -'SD5901' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Lower Door'}, -'SD5902' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Open Door'}, -'SD5903' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Close Door'}, -'SD5904' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Stop Door'}, -'SD5905' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Single Door Open'}, -'SD5906' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Single Door Close'}, -'SD60' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD6000' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Open'}, -'SD6001' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Close'}, -'SD6002' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Stop'}, -'SD6003' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Program'}, -'SD61' => {Cmd1Name=>'Window Covering Position',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Position'}, -'SD68' => {Cmd1Name=>'Thermostat Temp Up',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Increment Count'}, -'ED68' => {Cmd1Name=>'Thermostat Zone Temp Up',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, -'SD69' => {Cmd1Name=>'Thermostat Temp Down',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Decrement Count'}, -'ED69' => {Cmd1Name=>'Thermostat Zone Temp Down',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, -'SD6a' => {Cmd1Name=>'Thermostat Get Zone Info',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, -'SDA6a' => {Cmd1Name=>'Thermostat Get Zone Info',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Requested Data'}, -'SD6b' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD6b00' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, -'SD6b01' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM from RAM'}, -'SD6b02' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Get Thermostat Mode'}, -'SD6b03' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Get ambient temperature'}, -'SD6b04' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'ON Heat'}, -'SD6b05' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'ON Cool'}, -'SD6b06' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'ON Auto'}, -'SD6b07' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'ON Fan'}, -'SD6b08' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'OFF Fan'}, -'SD6b09' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'OFF All'}, -'SD6b0a' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Program Heat'}, -'SD6b0b' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Program Cool'}, -'SD6b0c' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Program Auto'}, -'SD6b0d' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Get Equipment State'}, -'SD6b0e' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0e',Cmd2Name=>'Set Equipment State'}, -'SD6b0f' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0f',Cmd2Name=>'Get Temperature Units'}, -'SD6b10' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x10',Cmd2Name=>'Set Fahrenheit'}, -'SD6b11' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x11',Cmd2Name=>'Set Celsius'}, -'SD6b12' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x12',Cmd2Name=>'Get Fan-On Speed'}, -'SD6b13' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x13',Cmd2Name=>'Set Fan-On Speed Low'}, -'SD6b14' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x14',Cmd2Name=>'Set Fan-On Speed Medium'}, -'SD6b15' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x15',Cmd2Name=>'Set Fan-On Speed High'}, -'SD6b16' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x16',Cmd2Name=>'Enable status change message'}, -'SD6b17' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x17',Cmd2Name=>'Disable status change message'}, -'SD6c' => {Cmd1Name=>'Thermostat Set Cool Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Setpoint Value'}, -'ED6c' => {Cmd1Name=>'Thermostat Set Zone Cool Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, -'SD6d' => {Cmd1Name=>'Thermostat Set Heat Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Setpoint Value'}, -'ED6d' => {Cmd1Name=>'Thermostat Set Zone Heat Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, -'SD6e' => {Cmd1Name=>'Thermostat Set or Read Mode',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, -'SD70' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SD7000' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Leak Detected'}, -'SD7001' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'No Leak Detected'}, -'SD7002' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Battery Low'}, -'SD7003' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Battery OK'}, -'SD81' => {Cmd1Name=>'Assign to Companion Group(Deprecated)',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, -'EDf0' => {Cmd1Name=>'Read or Write Registers',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, -'SDf0' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, -'SDf000' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, -'SDf001' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Write a Code Record'}, -'SDf002' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Read a Code Record'}, -'SDf003' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Get a Code Record'}, -'SDf1' => {Cmd1Name=>'Specific Code Record Read',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Record Number'}, -'EDf1' => {Cmd1Name=>'Response to Read Registers',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, -'EDf1' => {Cmd1Name=>'Code Record Request Respon',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Record Number'}, -'EDf2' => {Cmd1Name=>'Specific Code Record Write',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Record Number'}, -); - - -#X10 PLM codes -my %x10_house_codes = ( - '6' => 'a', - 'e' => 'b', - '2' => 'c', - 'a' => 'd', - '1' => 'e', - '9' => 'f', - '5' => 'g', - 'd' => 'h', - '7' => 'i', - 'f' => 'j', - '3' => 'k', - 'b' => 'l', - '0' => 'm', - '8' => 'n', - '4' => 'o', - 'c' => 'p' -); - -my %x10_unit_codes = ( - '6' => '1', - 'e' => '2', - '2' => '3', - 'a' => '4', - '1' => '5', - '9' => '6', - '5' => '7', - 'd' => '8', - '7' => '9', - 'f' => 'a', - '3' => 'b', - 'b' => 'c', - '0' => 'd', - '8' => 'e', - '4' => 'f', - 'c' => 'g' -); - -my %x10_commands = ( - '2' => 'On(J)', - '3' => 'Off(K)', - '5' => 'Bright(L)', - '4' => 'Dim(M)', - 'a' => 'preset_dim1', - 'b' => 'preset_dim2', - '0' => 'all_units_off(P)', - '1' => 'all_lights_on(O)', - '6' => 'all_lights_off', - 'f' => 'status', - 'd' => 'status_on', - 'e' => 'status_off', - '9' => 'hail_ack', - '7' => 'ext_code', - 'c' => 'ext_data', - '8' => 'hail_request' -); - - -=item plm_decode(plm_string) - -Returns a string containing a decoded PLM data packet - -=cut - -sub plm_decode { - my ($plm_string) = @_; - $plm_string = lc($plm_string); - -#0262 1e5d8e 0f 0d00 -#0262 1e5d8e 0f 0d00 06 - -#FSM:0 - Look for PLM STX -#FSM:1 - Parse PLM command category -#FSM:2 - Parse command from PLM (50-58) -#FSM:3 - Parse command to PLM (60-73) and response - - my $plm_message = ''; - my $plm_cmd_id; - - my $FSM = 0; - my $abort = 0; - my $finished = 0; - while(!$abort and !$finished) { - if($FSM==0) { - #FSM:0 - Look for PLM STX - #Must start with STX or it is garbage - if(substr($plm_string,0,2) ne '02') { - $plm_message .= "Missing (02)STX: Invalid message\n"; - $abort++; - } else { - $FSM++; - } - } elsif($FSM==1) { - #FSM:1 - Parse PLM command category - #Must be at least 2 bytes (4 nibbles) or it is garbage - if(length($plm_string) < 4) { - $abort++; - } else { - #include the STX for historical reasons - $plm_cmd_id = substr($plm_string,0,4); - $plm_message .= sprintf("%20s: (","PLM Command").$plm_cmd_id.") ".$plmcmd2string{$plm_cmd_id}."\n"; - if(length($plm_string) < $plmcmdlen{uc($plm_cmd_id)}->[0] * 2) { - $plm_message .= " Message length too short for PLM command. Not parsed\n"; - $abort++; - } elsif(length($plm_string) > $plmcmdlen{uc($plm_cmd_id)}->[0] * 2 - and length($plm_string) < $plmcmdlen{uc($plm_cmd_id)}->[1] * 2) { - $plm_message .= " Message length too short for PLM command. Not parsed\n"; - $abort++; - } elsif(substr($plm_string,2,1) == '5') { - #commands from PLM are 50-58 - $FSM = 2; - } else { - $FSM = 3; - } - } - } elsif($FSM==2) { - #FSM:2 - Parse command from PLM (50-58) - if($plm_cmd_id eq '0250') { - $plm_message .= sprintf("%24s: ",'From Address').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; - $plm_message .= sprintf("%24s: ",'To Address').substr($plm_string,10,2).":".substr($plm_string,12,2).":".substr($plm_string,14,2)."\n"; - $plm_message .= sprintf("%24s: ",'Message Flags').substr($plm_string,16,2)."\n"; - $plm_message .= insteon_message_flags_decode(substr($plm_string,16,2)); - my $flag_ext = hex(substr($plm_string,16,1))&0b0001; - $plm_message .= sprintf("%24s: ",'Insteon Message').substr($plm_string,18,($flag_ext ? 32 : 4))."\n"; - $plm_message .= insteon_decode(substr($plm_string,16)); - } elsif($plm_cmd_id eq '0251'){ - $plm_message .= sprintf("%24s: ",'From Address').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; - $plm_message .= sprintf("%24s: ",'To Address').substr($plm_string,10,2).":".substr($plm_string,12,2).":".substr($plm_string,14,2)."\n"; - $plm_message .= sprintf("%24s: ",'Message Flags').substr($plm_string,16,2)."\n"; - $plm_message .= insteon_message_flags_decode(substr($plm_string,16,2)); - my $flag_ext = hex(substr($plm_string,16,1))&0b0001; - $plm_message .= sprintf("%24s: ",'Insteon Message').substr($plm_string,18,($flag_ext ? 32 : 4))."\n"; - $plm_message .= insteon_decode(substr($plm_string,16)); - } elsif($plm_cmd_id eq '0252'){ - $plm_message .= sprintf("%20s: ",'X10 Message').substr($plm_string,4,4)."\n"; - $plm_message .= plm_x10_decode(substr($plm_string,4,4)); - } elsif($plm_cmd_id eq '0253'){ - my @link_string = ('PLM is Responder', 'PLM is Controller', 'All-Link deleted'); - $plm_message .= sprintf("%20s: (",'Link Code').substr($plm_string,4,2).") ".$link_string[substr($plm_string,4,2)]."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,6,2)."\n"; - $plm_message .= sprintf("%20s: ",'Linked Device').substr($plm_string,8,2).":".substr($plm_string,10,2).":".substr($plm_string,12,2)."\n"; - $plm_message .= sprintf("%20s: ",'Device Category').substr($plm_string,14,2).":".substr($plm_string,16,2)."\n"; - $plm_message .= sprintf("%20s: ",'Firmware').substr($plm_string,18,2)."\n"; - } elsif($plm_cmd_id eq '0254'){ - my @buttons = ('SET Button ','Button 2 ','Button 3 '); - my @button_event = ('','','Tapped','Held 3 seconds','Released'); - $plm_message .= sprintf("%20s: (",'Button Event').substr($plm_string,4,2).") ".$buttons[substr($plm_string,4,1)].$button_event[substr($plm_string,5,1)]."\n"; - } elsif($plm_cmd_id eq '0255'){ - #Nothing else to do - } elsif($plm_cmd_id eq '0256'){ - $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,4,2)."\n"; - $plm_message .= sprintf("%20s: ",'Device').substr($plm_string,6,2).":".substr($plm_string,8,2).":".substr($plm_string,10,2)."\n"; - } elsif($plm_cmd_id eq '0257'){ - $plm_message .= sprintf("%20s: ",'All-Link Flags').substr($plm_string,4,2)."\n"; - my $flags = hex(substr($plm_string,4,2)); - $plm_message .= sprintf("%20s: Record is ",'Bit 7').($flags&0b10000000?'in use':'available')."\n"; - $plm_message .= sprintf("%20s: PLM is ",'Bit 6').($flags&0b01000000?'controller':'responder')."\n"; - $plm_message .= sprintf("%20s: ACK is ",'Bit 5').($flags&0b00100000?'required':'not required')."\n"; - $plm_message .= sprintf("%20s: Record has ",'Bit 1').($flags&0b00000001?'been used before':'not been used before')."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,6,2)."\n"; - $plm_message .= sprintf("%20s: ",'Linked Device').substr($plm_string,8,2).":".substr($plm_string,10,2).":".substr($plm_string,12,2)."\n"; -#XXXX $plm_message .= sprintf("%20s: ",'Link Data').substr($plm_string,14,6)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Command1').substr($plm_string,14,2)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Command2').substr($plm_string,16,2)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Data').substr($plm_string,18,2)."\n"; - #TODO: Find insteon information for link data decode - } elsif($plm_cmd_id eq '0258'){ - $plm_message .= sprintf("%20s: (",'Status Byte').substr($plm_string,4,2).") ".(substr($plm_string,4,2) eq '06' ? "ACK" : "NACK")."\n"; - } else { - $plm_message .= sprintf("%20s: (",'Undefined Cmd Data').substr($plm_string,4).")\n"; - } - $finished++; - } elsif($FSM==3) { - #FSM:3 - Parse command to PLM (60-73) and response - my $plm_ack_pos; - if($plm_cmd_id eq '0260') { - if(length($plm_string)>4) { - $plm_message .= sprintf("%20s: ",'PLM Device ID').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; - $plm_message .= sprintf("%20s: ",'Device Category').substr($plm_string,10,2).":".substr($plm_string,12,2)."\n"; - $plm_message .= sprintf("%20s: ",'Firmware').substr($plm_string,14,2)."\n"; - } - $plm_ack_pos = 16; - } elsif($plm_cmd_id eq '0261'){ - $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,4,2)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Command1').substr($plm_string,6,2)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Command2').substr($plm_string,8,2)."\n"; - $plm_ack_pos = 10; - #TODO: look up insteon information for all-link command1 / command2 decode - } elsif($plm_cmd_id eq '0262'){ - $plm_message .= sprintf("%24s: ",'To Address').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; - $plm_message .= sprintf("%24s: ",'Message Flags').substr($plm_string,10,2)."\n"; - $plm_message .= insteon_message_flags_decode(substr($plm_string,10,2)); - my $flag_ext = hex(substr($plm_string,10,1))&0b0001; - $plm_message .= sprintf("%24s: ",'Insteon Message').substr($plm_string,12,($flag_ext ? 32 : 4))."\n"; - $plm_message .= insteon_decode(substr($plm_string,10)); - $plm_ack_pos = $flag_ext ? 44 : 16; - } elsif($plm_cmd_id eq '0263'){ - $plm_message .= sprintf("%20s: ",'X10 Message').substr($plm_string,4,4)."\n"; - $plm_message .= plm_x10_decode(substr($plm_string,4,4)); - $plm_ack_pos = 8; - } elsif($plm_cmd_id eq '0264'){ - my %link_string = ('00'=>'PLM is Responder', - '01'=>'PLM is Controller', - '03'=>'PLM is either Responder or Controller', - 'ff'=>'Delete All-Link'); - $plm_message .= sprintf("%20s: (",'Link Code').substr($plm_string,4,2).") ".$link_string{substr($plm_string,4,2)}."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,6,2)."\n"; - $plm_ack_pos = 8; - } elsif($plm_cmd_id eq '0265'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '0266'){ - $plm_message .= sprintf("%20s: ",'Device Category').substr($plm_string,4,2).":".substr($plm_string,6,2)."\n"; - $plm_message .= sprintf("%20s: ",'Firmware').substr($plm_string,8,2)."\n"; - $plm_ack_pos = 10; - } elsif($plm_cmd_id eq '0267'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '0268'){ - $plm_message .= sprintf("%20s: ",'Command2 Data').substr($plm_string,4,2)."\n"; - $plm_ack_pos = 6; - } elsif($plm_cmd_id eq '0269'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '026a'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '026b'){ - $plm_message .= sprintf("%20s: (",'PLM Config Flags').substr($plm_string,4,2).")\n"; - my $flags = hex(substr($plm_string,4,2)); - $plm_message .= sprintf("%20s: Automatic Linking ",'Bit 7').($flags&0b10000000?'Disabled':'Enabled')."\n"; - $plm_message .= sprintf("%20s: Monitor Mode ",'Bit 6').($flags&0b01000000?'Enabled':'Disabled')."\n"; - $plm_message .= sprintf("%20s: Automatic LED ",'Bit 5').($flags&0b00100000?'Disabled':'Enabled')."\n"; - $plm_message .= sprintf("%20s: Deadman Feature ",'Bit 4').($flags&0b00010000?'Disabled':'Enabled')."\n"; - $plm_ack_pos = 6; - } elsif($plm_cmd_id eq '026c'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '026d'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '026e'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '026f'){ - my %control_string = ('00'=>'Find All-Link Record', - '01'=>'Find Next All-Link Record', - '20'=>'Update/Add All-Link Record', - '40'=>'Update/Add Controller All-Link Record', - '41'=>'Update/Add Responder All-Link Record', - '80'=>'Delete All-Link Record'); - $plm_message .= sprintf("%20s: (",'Control code').substr($plm_string,4,2).") ".$control_string{substr($plm_string,4,2)}."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Flags').substr($plm_string,6,2)."\n"; - my $flags = hex(substr($plm_string,6,2)); - $plm_message .= sprintf("%20s: Record is ",'Bit 7').($flags&0b10000000?'in use':'available')."\n"; - $plm_message .= sprintf("%20s: PLM is ",'Bit 6').($flags&0b01000000?'controller':'responder')."\n"; - $plm_message .= sprintf("%20s: ACK is ",'Bit 5').($flags&0b00100000?'required':'not required')."\n"; - $plm_message .= sprintf("%20s: Record has ",'Bit 1').($flags&0b00000001?'been used before':'not been used before')."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,8,2)."\n"; - $plm_message .= sprintf("%20s: ",'Linked Device').substr($plm_string,10,2).":".substr($plm_string,12,2).":".substr($plm_string,14,2)."\n"; -# $plm_message .= sprintf("%20s: ",'Link Data').substr($plm_string,16,6)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Command1').substr($plm_string,16,2)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Command2').substr($plm_string,18,2)."\n"; - $plm_message .= sprintf("%20s: ",'All-Link Data').substr($plm_string,20,2)."\n"; - $plm_ack_pos = 22; - #TODO: Find insteon information for link data decode - } elsif($plm_cmd_id eq '0270'){ - $plm_message .= sprintf("%20s: ",'Command2 Data').substr($plm_string,4,2)."\n"; - $plm_ack_pos = 6; - } elsif($plm_cmd_id eq '0271'){ - $plm_message .= sprintf("%20s: ",'Command1 Data').substr($plm_string,4,2)."\n"; - $plm_message .= sprintf("%20s: ",'Command2 Data').substr($plm_string,6,2)."\n"; - $plm_ack_pos = 8; - } elsif($plm_cmd_id eq '0272'){ - $plm_ack_pos = 4; - } elsif($plm_cmd_id eq '0273'){ - if(length($plm_string)>4) { - $plm_message .= sprintf("%20s: (",'PLM Config Flags').substr($plm_string,4,2).")\n"; - my $flags = hex(substr($plm_string,4,2)); - $plm_message .= sprintf("%20s: Automatic Linking ",'Bit 7').($flags&0b10000000?'Disabled':'Enabled')."\n"; - $plm_message .= sprintf("%20s: Monitor Mode ",'Bit 6').($flags&0b01000000?'Enabled':'Disabled')."\n"; - $plm_message .= sprintf("%20s: Automatic LED ",'Bit 5').($flags&0b00100000?'Disabled':'Enabled')."\n"; - $plm_message .= sprintf("%20s: Deadman Feature ",'Bit 4').($flags&0b00010000?'Disabled':'Enabled')."\n"; - $plm_message .= sprintf("%20s: ",'Spare 1').substr($plm_string,6,2)."\n"; - $plm_message .= sprintf("%20s: ",'Spare 2').substr($plm_string,8,2)."\n"; - } - $plm_ack_pos = 10; - } else { - $plm_message .= sprintf("%20s: (",'Undefined Cmd Data').substr($plm_string,4).")\n"; - $plm_ack_pos = 255; - } - - if(length($plm_string)>$plm_ack_pos) { - $plm_message .= sprintf("%20s: (",'PLM Response').substr($plm_string,$plm_ack_pos,2).") ".(substr($plm_string,$plm_ack_pos,2) eq '06' ? "ACK" : "NACK")."\n"; - } - $finished++; - } #if($FSM==) - } #while(!$abort) - return $plm_message; -} - - -=item plm_x10_decode(x10_string) - -Returns a string containing a decoded PLM X10 data packet - -=cut - -sub plm_x10_decode { - my ($x10_string) = @_; - $x10_string = lc($x10_string); - - my $x10_message = ''; - $x10_message .= sprintf("%24s: (",'X10 House Code').substr($x10_string,0,1).") ".uc($x10_house_codes{substr($x10_string,0,1)})."\n"; - if(substr($x10_string,2,1) == '8') { - $x10_message .= sprintf("%24s: (",'X10 Command').substr($x10_string,1,1).") ".$x10_commands{substr($x10_string,1,1)}."\n"; - } else { - $x10_message .= sprintf("%24s: (",'X10 Unit Code').substr($x10_string,1,1).") ".uc($x10_unit_codes{substr($x10_string,1,1)})."\n"; - } - return($x10_message); -} - -=item insteon_message_flags_decode(flags_string) - -Returns a string containing decoded Insteon message flags - -=cut - -sub insteon_message_flags_decode { - my ($flags_string) = @_; - $flags_string = lc($flags_string); - - my $flags_message = ''; - my %message_string = ( '4'=>'Broadcast Message', - '0'=>'Direct Message', - '1'=>'ACK of Direct Message', - '5'=>'NAK of Direct Message', - '6'=>'All-Link Broadcast Message', - '2'=>'All-Link Cleanup Direct Message', - '3'=>'ACK of All-Link Cleanup Direct Message', - '7'=>'NAK of All-Link Cleanup Direct Message'); - - my $flag_msg = hex(substr($flags_string,0,1))>>1; - my $flag_ext = hex(substr($flags_string,0,1))&0b0001; - $flags_message .= sprintf("%28s: (%03b) ",'Message Type',$flag_msg).$message_string{$flag_msg}."\n"; - $flags_message .= sprintf("%28s: (%01b) ",'Message Length',$flag_ext).($flag_ext?'Extended Length':'Standard Length')."\n"; - $flags_message .= sprintf("%28s: %d\n",'Hops Left',hex(substr($flags_string,1,1))>>2); - $flags_message .= sprintf("%28s: %d\n",'Max Hops',hex(substr($flags_string,1,1))&0b0011); - return($flags_message); -} - -=item insteon_decode(command_string) - -Returns a string containing a decoded Insteon message. Input -string should be the Insteon message starting with the -message flag byte. - -=cut - -sub insteon_decode { - my ($command_string) = @_; -#Mapping from message type bit field to acronyms used in -# the INSTEON Command Tables documentation -#100 4 - SB: Standard Broadcast - -#000 0 - SD or ED: Standard/Extended Direct -#001 1 - SDA or EDA: Standard/Extended Direct ACK -#101 5 - SDN or EDN: Standard/Extended Direct NACK - -#110 6 - SA: Standard All-Link Broadcast -#010 2 - SC: Standard Cleanup Direct -#011 3 - SCA: Standard Cleanup Direct ACK -#111 7 - SCN: Standard Cleanup Direct NACK - -#For SDA parsing 1st look for SDA command entry, if not found -#then lookup SD command entry for parsing information. - -#For SDN, EDN, SCN NACK responses, lookup coorespnding -#SD, ED, or SC entry for parsing, but always use the -#common NACK decoding for Cmd2 - -#Lookup SB, SD, ED, SA, and SC messages with just the -#Cmd1 entry appended at the key. If Cmd2 Flag == "Command" -#then repeat lookup appending both Cmd1 and Cmd2 for -#the key. If Cmd2 Flag != "Command" then use flag value -#to control how Cmd2 is displayed. If second lookup fails, -#simply print Cmd2 and indicate "not decoded". - - my $extended = hex(substr($command_string,0,1))&0b0001; - my $msg_type = (hex(substr($command_string,0,1))&0b1110)>>1; - my $cmd1 = substr($command_string,2,2); - my $cmd2 = substr($command_string,4,2); - my $data = ''; - $data = substr($command_string,6) if($extended); - - #Truncate $command_string to remove PLM ACK byte - $command_string = substr($command_string,0, ($extended ? 34 : 8)); - my $insteon_message=''; - if( $msg_type == 0) { - #SD/ED: Standard/Extended Direct - $insteon_message .= insteon_decode_cmd(($extended ? 'ED' : 'SD'), $cmd1, $cmd2, $extended, $data); - } elsif( $msg_type == 1 or $msg_type == 5) { - #SDA/EDA: Standard/Extended Direct ACK/NACK - $insteon_message .= insteon_decode_cmd(($extended ? 'EDA' : 'SDA'), $cmd1, $cmd2, $extended, $data); - } elsif( $msg_type == 6) { - #SA: Standard All-Link Broadcast - $insteon_message .= insteon_decode_cmd('SA', $cmd1, $cmd2, $extended, $data); - } elsif( $msg_type == 2) { - #SC: Standard Direct Cleanup - $insteon_message .= insteon_decode_cmd('SC', $cmd1, $cmd2, $extended, $data); - } elsif( $msg_type == 3 or $msg_type == 7) { - #SCA: Standard Direct Cleanup ACK/NACK - $insteon_message .= insteon_decode_cmd('SCA', $cmd1, $cmd2, $extended, $data); - } else { - $insteon_message .= sprintf("%28s: ",'')."Insteon message type not decoded\n"; - } - - return $insteon_message -} - - -sub insteon_decode_cmd { - my ($cmdLookup, $cmd1, $cmd2, $extended, $Data) = @_; - my $insteon_message=''; - my ($cmdDecoder1, $cmdDecoder2); - - #lookup 1st without using Cmd2 - $cmdDecoder1 = $insteonCmd{$cmdLookup.$cmd1}; - - if(!defined($cmdDecoder1)) { - #lookup failed, if this is an ACK/NACK retry w/ direct version - if( $cmdLookup eq 'SDA') { - $cmdDecoder1 = $insteonCmd{'SD'.$cmd1}; - } elsif( $cmdLookup eq 'EDA') { - $cmdDecoder1 = $insteonCmd{'ED'.$cmd1}; - } elsif( $cmdLookup eq 'SCA') { - $cmdDecoder1 = $insteonCmd{'SC'.$cmd1}; - } - if(!defined($cmdDecoder1)) { - #still not found so quit trying to decode - $insteon_message .= sprintf("%28s: ",'Cmd 1').$cmd1." Insteon command not decoded\n"; - $insteon_message .= sprintf("%28s: ",'Cmd 2').$cmd2."\n"; - $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); - return $insteon_message; - } - } - - if($cmdDecoder1->{'Cmd2Flag'} eq 'Command') { - #2nd lookup with Cmd2 - $cmdDecoder2 = $insteonCmd{$cmdLookup.$cmd1.$cmd2}; - if(!defined($cmdDecoder2)) { - #lookup failed, if this is an ACK/NACK retry w/ direct version - if( $cmdLookup eq 'SDA') { - $cmdDecoder2 = $insteonCmd{'SD'.$cmd1}; - } elsif( $cmdLookup eq 'EDA') { - $cmdDecoder2 = $insteonCmd{'ED'.$cmd1}; - } elsif( $cmdLookup eq 'SCA') { - $cmdDecoder2 = $insteonCmd{'SC'.$cmd1}; - } - } - if(!defined($cmdDecoder2)) { - #still not found so don't decode - $insteon_message .= sprintf("%28s: ",'Cmd 1').$cmd1." Insteon command not decoded\n"; - $insteon_message .= sprintf("%28s: ",'Cmd 2').$cmd2."\n"; - $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); - } else { - $insteon_message .= sprintf("%28s: (",'Cmd 1').$cmd1.") ".$cmdDecoder2->{'Cmd1Name'}."\n"; - $insteon_message .= sprintf("%28s: (",'Cmd 2').$cmd2.") ".$cmdDecoder2->{'Cmd2Name'}."\n"; - $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); - } - } elsif( $cmdDecoder1->{'Cmd2Flag'} eq 'Value') { - $insteon_message .= sprintf("%28s: (",'Cmd 1').$cmd1.") ".$cmdDecoder1->{'Cmd1Name'}."\n"; - $insteon_message .= sprintf("%28s: (",'Cmd 2').$cmd2.") ".$cmdDecoder1->{'Cmd2Name'}."\n"; - $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); - } elsif( $cmdDecoder1->{'Cmd2Flag'} eq 'NA') { - $insteon_message .= sprintf("%28s: (",'Cmd 1').$cmd1.") ".$cmdDecoder1->{'Cmd1Name'}."\n"; - $insteon_message .= sprintf("%28s: ",'Cmd 2').$cmd2."\n"; - $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); - } else { - $insteon_message .= "Parse database has undefined Cmd2Flag: ".$cmdDecoder1->{'Cmd2Flag'}; - } - - return $insteon_message; -} - - -#$plm_cmd is 2 byte hex cmd; $send_rec is 0 for send, 1, for rec; $is_extended is 1 if extended send -#returns expected byte length -sub insteon_cmd_len{ - my ($plm_cmd, $send_rec, $is_extended) = @_; - if ($is_extended && $plmcmdlen{uc($plm_cmd)} > 2) { - return $plmcmdlen{uc($plm_cmd)}->[($send_rec+2)]; - } else { - return $plmcmdlen{uc($plm_cmd)}->[$send_rec]; - } -} - - -=back - -=head1 SUPPORT - -You can find documentation for this module with the perldoc command. - - perldoc Insteon::MessageDecoder - -=head1 SEE ALSO - -L - -PLM command details can be found in the 2412S Developers Guide. This -document is not supplied by SmartHome but may be available through an -internet search. - -=head1 AUTHOR - -Michael Stovenour - -=head1 LICENSE AND COPYRIGHT - -Copyright 2012 Michael Stovenour - -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. - -=cut - -1; +package Insteon::MessageDecoder; + +use strict; + +=head1 NAME + +B - Static class for decoding Insteon PLM messages + +=head1 SYNOPSIS + + use Insteon::MessageDecoder; + my $decodedMessage = Insteon::MessageDecoder::plm_decode($plm_string); + +=head1 DESCRIPTION + +Insteon::MessageDecoder will decode Insteon PLM messages. Functions are +provided to decode the PLM envelope, X10 commands, Insteon flags, and +Insteon Cmd1/Cmd2 bytes. User data (D1-D14) of extended messages is not +decoded but will be displayed. + +=head1 EXAMPLE + + use Insteon::MessageDecoder; + my $plm_string; + + $plm_string = '02621f058c1f2e000100000000000000000000000000'; + print( "PLM Message: $plm_string\n"); + print( Insteon::MessageDecoder::plm_decode($plm_string)."\n"); + + $plm_string = '02511f058c1edc30112e000101000020201cfe3f0001000000'; + print( "PLM Message: $plm_string\n"); + print( Insteon::MessageDecoder::plm_decode($plm_string)."\n"); + +=head1 LIMITATIONS + +The message decoder is not perfect. It does not keep message state and +several of the Insteon ACK message formats can "only" be decoded relative +to the most recent command sent to the ACKing device. Some ACK messages +will be incorrectly decoded as part of another Insteon message. For +example in the ACK for a "Light Status Request", cmd1 is the ALDB serial +number. This serial number will be interpreted as another Insteon message +where the serial number matches the cmd1 value. Be aware of this and you +should still be able to intrepret the decided messages. + +Extended messages are not decoded and only display the D1-D14 hex data. +You will need to manually decode the extended data. Patches for decoding +one or more extended messages are welcome. + +=head1 BUGS + +There are probably many bugs. The decoder was designed by transcribing +Insteon documentation, Jonathan Dale's excellent command reference, and +reviewing many message board discussions. The decoder has not been +tested with all devices (of all firmware revs) or all PLM combinations +(of all firmware revs). There are bugs; please report them in the +misterhouse GitHub issues list. + +=head1 METHODS + +=over + +=cut + +#These constants are intentionally copied here from other Insteon modules +#This is so any changes to those structures do not introduce errors +#in the decoders. These constants should only be modified to extend +#the decoders or correct defects in the decoders. + +#PLM Serial Commands +my %plmcmd = ( + insteon_received => '0250', + insteon_ext_received => '0251', + x10_received => '0252', + all_link_complete => '0253', + plm_button_event => '0254', + user_plm_reset => '0255', + all_link_clean_failed => '0256', + all_link_record => '0257', + all_link_clean_status => '0258', + plm_info => '0260', + all_link_send => '0261', + insteon_send => '0262', +# insteon_ext_send => '0262', + x10_send => '0263', + all_link_start => '0264', + all_link_cancel => '0265', + set_host_device_cat => '0266', + plm_reset => '0267', + set_insteon_ack_cmd2 => '0268', + all_link_first_rec => '0269', + all_link_next_rec => '026a', + plm_set_config => '026b', + get_sender_all_link_rec => '026c', + plm_led_on => '026d', + plm_led_off => '026e', + all_link_manage_rec => '026f', + insteon_nak => '0270', + insteon_ack => '0271', + rf_sleep => '0272', + plm_get_config => '0273' +); + +#create a backwards lookup on hex code +my %plmcmd2string = reverse %plmcmd; + +my %plmcmdlen = ( + '0250' => [11, 11], + '0251' => [25, 25], + '0252' => [4, 4], + '0253' => [10, 10], + '0254' => [3, 3], + '0255' => [2, 2], + '0256' => [6, 6], + '0257' => [10, 10], + '0258' => [3, 3], + '0260' => [2, 9], + '0261' => [5, 6], + '0262' => [8, 9, 22, 23], # could get 9 or 23 (Standard or Extended Message received) + '0263' => [4, 5], + '0264' => [4, 5], + '0265' => [2, 3], + '0266' => [5, 6], + '0267' => [2, 3], + '0268' => [3, 4], + '0269' => [2, 3], + '026A' => [2, 3], + '026B' => [3, 4], + '026C' => [2, 3], + '026D' => [2, 3], + '026E' => [2, 3], + '026F' => [11, 12], + '0270' => [3, 4], + '0271' => [4, 5], + '0272' => [2, 3], + '0273' => [5, 6], + ); + + +#Mapping from message type bit field to acronyms used in +# the INSTEON Command Tables documentation +#100 4 - SB: Standard Broadcast + +#000 0 - SD or ED: Standard/Extended Direct +#001 1 - SDA or EDA: Standard/Extended Direct ACK +#101 5 - SDN or EDN: Standard/Extended Direct NACK + +#110 6 - SA: Standard All-Link Broadcast +#010 2 - SC: Standard Cleanup Direct +#011 3 - SCA: Standard Cleanup Direct ACK +#111 7 - SCN: Standard Cleanup Direct NACK + +#List below is maintained in an Excel spreadsheet. Make +#changes there and cut-n-paste list to here +#You should understand the parsing logic before attempting +#to modify this table! +my %insteonCmd = ( +'SD01' => {Cmd1Name=>'Assign to All-Link Group',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SB01' => {Cmd1Name=>'SET Button Pressed Respond',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD02' => {Cmd1Name=>'Delete from All-Link Group',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SB02' => {Cmd1Name=>'SET Button Pressed Controller',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD03' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD0300' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Product Data Request'}, +'SD0301' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'FxName Request'}, +'SD0302' => {Cmd1Name=>'Device Request',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Device Text String Request'}, +'ED03' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED0300' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Product Data Response'}, +'ED0301' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'FX Username Response'}, +'ED0302' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Device Text String Response'}, +'ED0303' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Set Device Text String'}, +'ED0304' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Set ALL-Link Command Alias'}, +'ED0305' => {Cmd1Name=>'Device Response',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Set ALL-Link Command Alias ED'}, +'SB03' => {Cmd1Name=>'Test Powerline Phase',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SB0300' => {Cmd1Name=>'Test Powerline Phase',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Phase A'}, +'SB0301' => {Cmd1Name=>'Test Powerline Phase',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Phase B'}, +'SB04' => {Cmd1Name=>'Heartbeat',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Battery Level'}, +'SA06' => {Cmd1Name=>'All-Link Cleanup Report',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Fail Count'}, +'SD09' => {Cmd1Name=>'Enter Linking Mode',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD0a' => {Cmd1Name=>'Enter Unlinking Mode',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD0d' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SDA0d' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SDA0d00' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'i1'}, +'SDA0d01' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'i2'}, +'SDA0d02' => {Cmd1Name=>'Get INSTEON Engine Version',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'i2CS'}, +'SD0f' => {Cmd1Name=>'Ping',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD10' => {Cmd1Name=>'ID Request',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD11' => {Cmd1Name=>'Light ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Level'}, +'SA11' => {Cmd1Name=>'ALL-Link Recall',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC11' => {Cmd1Name=>'ALL-Link Recall',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD12' => {Cmd1Name=>'Light ON Fast',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Level'}, +'SA12' => {Cmd1Name=>'ALL-Link Alias 2 High',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC12' => {Cmd1Name=>'ALL-Link Alias 2 High',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD13' => {Cmd1Name=>'Light OFF',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SA13' => {Cmd1Name=>'ALL-Link Alias 1 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC13' => {Cmd1Name=>'ALL-Link Alias 1 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD14' => {Cmd1Name=>'Light OFF Fast',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SA14' => {Cmd1Name=>'ALL-Link Alias 2 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC14' => {Cmd1Name=>'ALL-Link Alias 2 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD15' => {Cmd1Name=>'Light Brighten One Step',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SA15' => {Cmd1Name=>'ALL-Link Alias 3 High',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC15' => {Cmd1Name=>'ALL-Link Alias 3 High',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD16' => {Cmd1Name=>'Light Dim One Step',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SA16' => {Cmd1Name=>'ALL-Link Alias 3 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC16' => {Cmd1Name=>'ALL-Link Alias 3 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD17' => {Cmd1Name=>'Light Start Manual Change',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD1700' => {Cmd1Name=>'Light Start Manual Change',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Down'}, +'SD1701' => {Cmd1Name=>'Light Start Manual Change',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Up'}, +'SA17' => {Cmd1Name=>'ALL-Link Alias 4 High',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC17' => {Cmd1Name=>'ALL-Link Alias 4 High',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD18' => {Cmd1Name=>'Light Stop Manual Change',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SA18' => {Cmd1Name=>'ALL-Link Alias 4 Low',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC18' => {Cmd1Name=>'ALL-Link Alias 4 Low',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD19' => {Cmd1Name=>'Light Status Request',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD1900' => {Cmd1Name=>'Light Status Request',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'On Level'}, +'SD1901' => {Cmd1Name=>'Light Status Request',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'LED Bit Flags'}, +'SD1f' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD1f00' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Request Flags'}, +'SD1f01' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'All-Link Database Delta Number'}, +'SD1f02' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Signal-to-Noise'}, +'SDA1f' => {Cmd1Name=>'Get Operating Flags',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Config Flags'}, +'SD20' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD2000' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Program Lock On'}, +'SD2001' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Program Lock Off'}, +'SD2002' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Deveice Dependent'}, +'SD2003' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Deveice Dependent'}, +'SD2004' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Deveice Dependent'}, +'SD2005' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Deveice Dependent'}, +'SD2006' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Deveice Dependent'}, +'SD2007' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'Deveice Dependent'}, +'SD2008' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'Deveice Dependent'}, +'SD2009' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Deveice Dependent'}, +'SD200a' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Deveice Dependent'}, +'SD200b' => {Cmd1Name=>'Set Operating Flags',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Deveice Dependent'}, +'SD21' => {Cmd1Name=>'Light Instant Change',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'On Level'}, +'SA21' => {Cmd1Name=>'ALL-Link Alias 5',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SC21' => {Cmd1Name=>'ALL-Link Alias 5',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Group'}, +'SD22' => {Cmd1Name=>'Light Manually Turned Off',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD23' => {Cmd1Name=>'Light Manually Turned On',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD24' => {Cmd1Name=>'Reread Init Values(Deprecated)',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD25' => {Cmd1Name=>'Remote SET Button Tap',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD2501' => {Cmd1Name=>'Remote SET Button Tap',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'1 Tap'}, +'SD2502' => {Cmd1Name=>'Remote SET Button Tap',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'2 Taps'}, +'SD27' => {Cmd1Name=>'Light Set Status',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'On Level'}, +'SB27' => {Cmd1Name=>'Status Change',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, +'SD28' => {Cmd1Name=>'Set Address MSB(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'MSB'}, +'SD29' => {Cmd1Name=>'Poke One Byte(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, +'ED2a' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED2a00' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Transfer Failure'}, +'ED2a01' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Complete (1 byte)'}, +'ED2a02' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Complete (2 bytes)'}, +'ED2a03' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Complete (3 bytes)'}, +'ED2a04' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Complete (4 bytes)'}, +'ED2a05' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Complete (5 bytes)'}, +'ED2a06' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Complete (6 bytes)'}, +'ED2a07' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'Complete (7 bytes)'}, +'ED2a08' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'Complete (8 bytes)'}, +'ED2a09' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Complete (9 bytes)'}, +'ED2a0a' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Complete (10 bytes)'}, +'ED2a0b' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Complete (11 bytes)'}, +'ED2a0c' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Complete (12 bytes)'}, +'ED2a0d' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Complete (13 bytes)'}, +'ED2aff' => {Cmd1Name=>'Block Data Transfer',Cmd2Flag=>'Command',Cmd2Value=>'0xff',Cmd2Name=>'Request Block Data Transfer'}, +'SD2b' => {Cmd1Name=>'Peek One Byte(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'LSB of Address'}, +'SDA2b' => {Cmd1Name=>'Peek One Byte(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Peeked Byte'}, +'SD2c' => {Cmd1Name=>'Peek One Byte Internal(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'LSB of Address'}, +'SDA2c' => {Cmd1Name=>'Peek One Byte Internal(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Peeked Byte'}, +'SD2d' => {Cmd1Name=>'Poke One Byte Internal(Deprecated)',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, +'SD2e' => {Cmd1Name=>'Light ON at Ramp Rate',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Level and Rate'}, +'ED2e' => {Cmd1Name=>'Extended Set/Get',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED2e00' => {Cmd1Name=>'Extended Set/Get',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Command in D2'}, +'SD2f' => {Cmd1Name=>'Light OFF at Ramp Rate',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Ramp Rate'}, +'ED2f' => {Cmd1Name=>'Read/Write ALL-Link Database',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED2f00' => {Cmd1Name=>'Read/Write ALL-Link Database',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Command in D2'}, +'SD30' => {Cmd1Name=>'Beep',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Duration'}, +'ED30' => {Cmd1Name=>'Trigger ALL-Link Command',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED3000' => {Cmd1Name=>'Trigger ALL-Link Command',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Trigger Command'}, +'SD40' => {Cmd1Name=>'Sprinkler Valve On',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Valve Number'}, +'ED40' => {Cmd1Name=>'Set Sprinkler Program',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, +'SD41' => {Cmd1Name=>'Sprinkler Valve Off',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Valve Number'}, +'ED41' => {Cmd1Name=>'Sprinkler Get Program Response',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, +'SD42' => {Cmd1Name=>'Sprinkler Program ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, +'SD43' => {Cmd1Name=>'Sprinkler Program OFF',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Program Number'}, +'SD44' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD4400' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, +'SD4401' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM From RAM'}, +'SD4402' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Get Valve Status'}, +'SD4403' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Inhibit Command Acceptance'}, +'SD4404' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Resume Command Acceptance'}, +'SD4405' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Skip Forward'}, +'SD4406' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Skip Backwards'}, +'SD4407' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'Enable Pump on V8'}, +'SD4408' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'Disable Pump on V8'}, +'SD4409' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Broadcast ON'}, +'SD440a' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Broadcast OFF'}, +'SD440b' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Load RAM from EEPROM'}, +'SD440c' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Sensor ON'}, +'SD440d' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Sensor OFF'}, +'SD440e' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0e',Cmd2Name=>'Diagnostics ON'}, +'SD440f' => {Cmd1Name=>'Sprinkler Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0f',Cmd2Name=>'Diagnostics OFF'}, +'SD45' => {Cmd1Name=>'I/O Output ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Output Number'}, +'SD46' => {Cmd1Name=>'I/O Output OFF',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Output Number'}, +'SD47' => {Cmd1Name=>'I/O Alarm Data Request',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD48' => {Cmd1Name=>'I/O Write Output Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data'}, +'SDA48' => {Cmd1Name=>'I/O Write Output Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data Written'}, +'SD49' => {Cmd1Name=>'I/O Read Input Port',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SDA49' => {Cmd1Name=>'I/O Read Input Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Data Read'}, +'SD4a' => {Cmd1Name=>'Get Sensor Value',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Sensor Number'}, +'SDA4a' => {Cmd1Name=>'Get Sensor Value',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Sensor Value'}, +'SD4b' => {Cmd1Name=>'Set Sensor 1 Alarm Trigger OFF->ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Nominal Value'}, +'ED4b' => {Cmd1Name=>'I/O Set Sensor Nominal',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Sensor Number'}, +'SD4c' => {Cmd1Name=>'I/O Get Sensor Alarm Delta',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, +'ED4c' => {Cmd1Name=>'I/O Alarm Data Response',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED4c00' => {Cmd1Name=>'I/O Alarm Data Response',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Response'}, +'SD4d' => {Cmd1Name=>'I/O Write Configuration Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, +'SD4e' => {Cmd1Name=>'I/O Read Configuration Port',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'SD4e' => {Cmd1Name=>'I/O Read Configuration Port',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'I/O Port Config'}, +'SD4f' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD4f00' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, +'SD4f01' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM from RAM'}, +'SD4f02' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Status Request'}, +'SD4f03' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Read Analog once'}, +'SD4f04' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Read Analog Always'}, +'SD4f09' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'Enable status change message'}, +'SD4f0a' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Disable status change message'}, +'SD4f0b' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Load RAM from EEPROM'}, +'SD4f0c' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Sensor On'}, +'SD4f0d' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Sensor Off'}, +'SD4f0e' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0e',Cmd2Name=>'Diagnostics On'}, +'SD4f0f' => {Cmd1Name=>'I/O Module Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0f',Cmd2Name=>'Diagnostics Off'}, +'SD50' => {Cmd1Name=>'Pool Device ON',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Device Number'}, +'ED50' => {Cmd1Name=>'Pool Set Device Temperature',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'ED5000' => {Cmd1Name=>'Pool Set Device Temperature',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Set Temperature'}, +'ED5001' => {Cmd1Name=>'Pool Set Device Temperature',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Set Hysteresis'}, +'SD51' => {Cmd1Name=>'Pool Device OFF',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Device Number'}, +'SD52' => {Cmd1Name=>'Pool Temperature Up',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Increment Count'}, +'SD53' => {Cmd1Name=>'Pool Temperature Down',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Decrement Count'}, +'SD54' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD5400' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, +'SD5401' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM From RAM'}, +'SD5402' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Get Pool Mode'}, +'SD5403' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Get Ambient Temp'}, +'SD5404' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Get Water Temp'}, +'SD5405' => {Cmd1Name=>'Pool Control',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Get pH'}, +'SD58' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD5800' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Raise Door'}, +'SD5801' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Lower Door'}, +'SD5802' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Open Door'}, +'SD5803' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Close Door'}, +'SD5804' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Stop Door'}, +'SD5805' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Single Door Open'}, +'SD5806' => {Cmd1Name=>'Door Move',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Single Door Close'}, +'SD59' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD5900' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Raise Door'}, +'SD5901' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Lower Door'}, +'SD5902' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Open Door'}, +'SD5903' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Close Door'}, +'SD5904' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'Stop Door'}, +'SD5905' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'Single Door Open'}, +'SD5906' => {Cmd1Name=>'Door Status Report',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'Single Door Close'}, +'SD60' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD6000' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Open'}, +'SD6001' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Close'}, +'SD6002' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Stop'}, +'SD6003' => {Cmd1Name=>'Window Covering',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Program'}, +'SD61' => {Cmd1Name=>'Window Covering Position',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Position'}, +'SD68' => {Cmd1Name=>'Thermostat Temp Up',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Increment Count'}, +'ED68' => {Cmd1Name=>'Thermostat Zone Temp Up',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, +'SD69' => {Cmd1Name=>'Thermostat Temp Down',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Decrement Count'}, +'ED69' => {Cmd1Name=>'Thermostat Zone Temp Down',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, +'SD6a' => {Cmd1Name=>'Thermostat Get Zone Info',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, +'SDA6a' => {Cmd1Name=>'Thermostat Get Zone Info',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Requested Data'}, +'SD6b' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD6b00' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, +'SD6b01' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Load EEPROM from RAM'}, +'SD6b02' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Get Thermostat Mode'}, +'SD6b03' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Get ambient temperature'}, +'SD6b04' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x04',Cmd2Name=>'ON Heat'}, +'SD6b05' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x05',Cmd2Name=>'ON Cool'}, +'SD6b06' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x06',Cmd2Name=>'ON Auto'}, +'SD6b07' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x07',Cmd2Name=>'ON Fan'}, +'SD6b08' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x08',Cmd2Name=>'OFF Fan'}, +'SD6b09' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x09',Cmd2Name=>'OFF All'}, +'SD6b0a' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0a',Cmd2Name=>'Program Heat'}, +'SD6b0b' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0b',Cmd2Name=>'Program Cool'}, +'SD6b0c' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0c',Cmd2Name=>'Program Auto'}, +'SD6b0d' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0d',Cmd2Name=>'Get Equipment State'}, +'SD6b0e' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0e',Cmd2Name=>'Set Equipment State'}, +'SD6b0f' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x0f',Cmd2Name=>'Get Temperature Units'}, +'SD6b10' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x10',Cmd2Name=>'Set Fahrenheit'}, +'SD6b11' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x11',Cmd2Name=>'Set Celsius'}, +'SD6b12' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x12',Cmd2Name=>'Get Fan-On Speed'}, +'SD6b13' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x13',Cmd2Name=>'Set Fan-On Speed Low'}, +'SD6b14' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x14',Cmd2Name=>'Set Fan-On Speed Medium'}, +'SD6b15' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x15',Cmd2Name=>'Set Fan-On Speed High'}, +'SD6b16' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x16',Cmd2Name=>'Enable status change message'}, +'SD6b17' => {Cmd1Name=>'Thermostat Control',Cmd2Flag=>'Command',Cmd2Value=>'0x17',Cmd2Name=>'Disable status change message'}, +'SD6c' => {Cmd1Name=>'Thermostat Set Cool Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Setpoint Value'}, +'ED6c' => {Cmd1Name=>'Thermostat Set Zone Cool Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, +'SD6d' => {Cmd1Name=>'Thermostat Set Heat Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Setpoint Value'}, +'ED6d' => {Cmd1Name=>'Thermostat Set Zone Heat Setpoint',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Zone Number'}, +'SD6e' => {Cmd1Name=>'Thermostat Set or Read Mode',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, +'SD70' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SD7000' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Leak Detected'}, +'SD7001' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'No Leak Detected'}, +'SD7002' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Battery Low'}, +'SD7003' => {Cmd1Name=>'Leak Detector Announce',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Battery OK'}, +'SD81' => {Cmd1Name=>'Assign to Companion Group(Deprecated)',Cmd2Flag=>'NA',Cmd2Value=>'',Cmd2Name=>''}, +'EDf0' => {Cmd1Name=>'Read or Write Registers',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, +'SDf0' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'',Cmd2Name=>''}, +'SDf000' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x00',Cmd2Name=>'Load Initialization Values'}, +'SDf001' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x01',Cmd2Name=>'Write a Code Record'}, +'SDf002' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x02',Cmd2Name=>'Read a Code Record'}, +'SDf003' => {Cmd1Name=>'EZSnsRF Control',Cmd2Flag=>'Command',Cmd2Value=>'0x03',Cmd2Name=>'Get a Code Record'}, +'SDf1' => {Cmd1Name=>'Specific Code Record Read',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Record Number'}, +'EDf1' => {Cmd1Name=>'Response to Read Registers',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Bit Field'}, +'EDf1' => {Cmd1Name=>'Code Record Request Respon',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Record Number'}, +'EDf2' => {Cmd1Name=>'Specific Code Record Write',Cmd2Flag=>'Value',Cmd2Value=>'',Cmd2Name=>'Record Number'}, +); + + +#X10 PLM codes +my %x10_house_codes = ( + '6' => 'a', + 'e' => 'b', + '2' => 'c', + 'a' => 'd', + '1' => 'e', + '9' => 'f', + '5' => 'g', + 'd' => 'h', + '7' => 'i', + 'f' => 'j', + '3' => 'k', + 'b' => 'l', + '0' => 'm', + '8' => 'n', + '4' => 'o', + 'c' => 'p' +); + +my %x10_unit_codes = ( + '6' => '1', + 'e' => '2', + '2' => '3', + 'a' => '4', + '1' => '5', + '9' => '6', + '5' => '7', + 'd' => '8', + '7' => '9', + 'f' => 'a', + '3' => 'b', + 'b' => 'c', + '0' => 'd', + '8' => 'e', + '4' => 'f', + 'c' => 'g' +); + +my %x10_commands = ( + '2' => 'On(J)', + '3' => 'Off(K)', + '5' => 'Bright(L)', + '4' => 'Dim(M)', + 'a' => 'preset_dim1', + 'b' => 'preset_dim2', + '0' => 'all_units_off(P)', + '1' => 'all_lights_on(O)', + '6' => 'all_lights_off', + 'f' => 'status', + 'd' => 'status_on', + 'e' => 'status_off', + '9' => 'hail_ack', + '7' => 'ext_code', + 'c' => 'ext_data', + '8' => 'hail_request' +); + + +=item plm_decode(plm_string) + +Returns a string containing a decoded PLM data packet + +=cut + +sub plm_decode { + my ($plm_string) = @_; + $plm_string = lc($plm_string); + +#0262 1e5d8e 0f 0d00 +#0262 1e5d8e 0f 0d00 06 + +#FSM:0 - Look for PLM STX +#FSM:1 - Parse PLM command category +#FSM:2 - Parse command from PLM (50-58) +#FSM:3 - Parse command to PLM (60-73) and response + + my $plm_message = ''; + my $plm_cmd_id; + + my $FSM = 0; + my $abort = 0; + my $finished = 0; + while(!$abort and !$finished) { + if($FSM==0) { + #FSM:0 - Look for PLM STX + #Must start with STX or it is garbage + if(substr($plm_string,0,2) ne '02') { + $plm_message .= "Missing (02)STX: Invalid message\n"; + $abort++; + } else { + $FSM++; + } + } elsif($FSM==1) { + #FSM:1 - Parse PLM command category + #Must be at least 2 bytes (4 nibbles) or it is garbage + if(length($plm_string) < 4) { + $abort++; + } else { + #include the STX for historical reasons + $plm_cmd_id = substr($plm_string,0,4); + $plm_message .= sprintf("%20s: (","PLM Command").$plm_cmd_id.") ".$plmcmd2string{$plm_cmd_id}."\n"; + if(length($plm_string) < $plmcmdlen{uc($plm_cmd_id)}->[0] * 2) { + $plm_message .= " Message length too short for PLM command. Not parsed\n"; + $abort++; + } elsif(length($plm_string) > $plmcmdlen{uc($plm_cmd_id)}->[0] * 2 + and length($plm_string) < $plmcmdlen{uc($plm_cmd_id)}->[1] * 2) { + $plm_message .= " Message length too short for PLM command. Not parsed\n"; + $abort++; + } elsif(substr($plm_string,2,1) == '5') { + #commands from PLM are 50-58 + $FSM = 2; + } else { + $FSM = 3; + } + } + } elsif($FSM==2) { + #FSM:2 - Parse command from PLM (50-58) + if($plm_cmd_id eq '0250') { + $plm_message .= sprintf("%24s: ",'From Address').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; + $plm_message .= sprintf("%24s: ",'To Address').substr($plm_string,10,2).":".substr($plm_string,12,2).":".substr($plm_string,14,2)."\n"; + $plm_message .= sprintf("%24s: ",'Message Flags').substr($plm_string,16,2)."\n"; + $plm_message .= insteon_message_flags_decode(substr($plm_string,16,2)); + my $flag_ext = hex(substr($plm_string,16,1))&0b0001; + $plm_message .= sprintf("%24s: ",'Insteon Message').substr($plm_string,18,($flag_ext ? 32 : 4))."\n"; + $plm_message .= insteon_decode(substr($plm_string,16)); + } elsif($plm_cmd_id eq '0251'){ + $plm_message .= sprintf("%24s: ",'From Address').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; + $plm_message .= sprintf("%24s: ",'To Address').substr($plm_string,10,2).":".substr($plm_string,12,2).":".substr($plm_string,14,2)."\n"; + $plm_message .= sprintf("%24s: ",'Message Flags').substr($plm_string,16,2)."\n"; + $plm_message .= insteon_message_flags_decode(substr($plm_string,16,2)); + my $flag_ext = hex(substr($plm_string,16,1))&0b0001; + $plm_message .= sprintf("%24s: ",'Insteon Message').substr($plm_string,18,($flag_ext ? 32 : 4))."\n"; + $plm_message .= insteon_decode(substr($plm_string,16)); + } elsif($plm_cmd_id eq '0252'){ + $plm_message .= sprintf("%20s: ",'X10 Message').substr($plm_string,4,4)."\n"; + $plm_message .= plm_x10_decode(substr($plm_string,4,4)); + } elsif($plm_cmd_id eq '0253'){ + my @link_string = ('PLM is Responder', 'PLM is Controller', 'All-Link deleted'); + $plm_message .= sprintf("%20s: (",'Link Code').substr($plm_string,4,2).") ".$link_string[substr($plm_string,4,2)]."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,6,2)."\n"; + $plm_message .= sprintf("%20s: ",'Linked Device').substr($plm_string,8,2).":".substr($plm_string,10,2).":".substr($plm_string,12,2)."\n"; + $plm_message .= sprintf("%20s: ",'Device Category').substr($plm_string,14,2).":".substr($plm_string,16,2)."\n"; + $plm_message .= sprintf("%20s: ",'Firmware').substr($plm_string,18,2)."\n"; + } elsif($plm_cmd_id eq '0254'){ + my @buttons = ('SET Button ','Button 2 ','Button 3 '); + my @button_event = ('','','Tapped','Held 3 seconds','Released'); + $plm_message .= sprintf("%20s: (",'Button Event').substr($plm_string,4,2).") ".$buttons[substr($plm_string,4,1)].$button_event[substr($plm_string,5,1)]."\n"; + } elsif($plm_cmd_id eq '0255'){ + #Nothing else to do + } elsif($plm_cmd_id eq '0256'){ + $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,4,2)."\n"; + $plm_message .= sprintf("%20s: ",'Device').substr($plm_string,6,2).":".substr($plm_string,8,2).":".substr($plm_string,10,2)."\n"; + } elsif($plm_cmd_id eq '0257'){ + $plm_message .= sprintf("%20s: ",'All-Link Flags').substr($plm_string,4,2)."\n"; + my $flags = hex(substr($plm_string,4,2)); + $plm_message .= sprintf("%20s: Record is ",'Bit 7').($flags&0b10000000?'in use':'available')."\n"; + $plm_message .= sprintf("%20s: PLM is ",'Bit 6').($flags&0b01000000?'controller':'responder')."\n"; + $plm_message .= sprintf("%20s: ACK is ",'Bit 5').($flags&0b00100000?'required':'not required')."\n"; + $plm_message .= sprintf("%20s: Record has ",'Bit 1').($flags&0b00000001?'been used before':'not been used before')."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,6,2)."\n"; + $plm_message .= sprintf("%20s: ",'Linked Device').substr($plm_string,8,2).":".substr($plm_string,10,2).":".substr($plm_string,12,2)."\n"; +#XXXX $plm_message .= sprintf("%20s: ",'Link Data').substr($plm_string,14,6)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Command1').substr($plm_string,14,2)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Command2').substr($plm_string,16,2)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Data').substr($plm_string,18,2)."\n"; + #TODO: Find insteon information for link data decode + } elsif($plm_cmd_id eq '0258'){ + $plm_message .= sprintf("%20s: (",'Status Byte').substr($plm_string,4,2).") ".(substr($plm_string,4,2) eq '06' ? "ACK" : "NACK")."\n"; + } else { + $plm_message .= sprintf("%20s: (",'Undefined Cmd Data').substr($plm_string,4).")\n"; + } + $finished++; + } elsif($FSM==3) { + #FSM:3 - Parse command to PLM (60-73) and response + my $plm_ack_pos; + if($plm_cmd_id eq '0260') { + if(length($plm_string)>4) { + $plm_message .= sprintf("%20s: ",'PLM Device ID').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; + $plm_message .= sprintf("%20s: ",'Device Category').substr($plm_string,10,2).":".substr($plm_string,12,2)."\n"; + $plm_message .= sprintf("%20s: ",'Firmware').substr($plm_string,14,2)."\n"; + } + $plm_ack_pos = 16; + } elsif($plm_cmd_id eq '0261'){ + $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,4,2)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Command1').substr($plm_string,6,2)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Command2').substr($plm_string,8,2)."\n"; + $plm_ack_pos = 10; + #TODO: look up insteon information for all-link command1 / command2 decode + } elsif($plm_cmd_id eq '0262'){ + $plm_message .= sprintf("%24s: ",'To Address').substr($plm_string,4,2).":".substr($plm_string,6,2).":".substr($plm_string,8,2)."\n"; + $plm_message .= sprintf("%24s: ",'Message Flags').substr($plm_string,10,2)."\n"; + $plm_message .= insteon_message_flags_decode(substr($plm_string,10,2)); + my $flag_ext = hex(substr($plm_string,10,1))&0b0001; + $plm_message .= sprintf("%24s: ",'Insteon Message').substr($plm_string,12,($flag_ext ? 32 : 4))."\n"; + $plm_message .= insteon_decode(substr($plm_string,10)); + $plm_ack_pos = $flag_ext ? 44 : 16; + } elsif($plm_cmd_id eq '0263'){ + $plm_message .= sprintf("%20s: ",'X10 Message').substr($plm_string,4,4)."\n"; + $plm_message .= plm_x10_decode(substr($plm_string,4,4)); + $plm_ack_pos = 8; + } elsif($plm_cmd_id eq '0264'){ + my %link_string = ('00'=>'PLM is Responder', + '01'=>'PLM is Controller', + '03'=>'PLM is either Responder or Controller', + 'ff'=>'Delete All-Link'); + $plm_message .= sprintf("%20s: (",'Link Code').substr($plm_string,4,2).") ".$link_string{substr($plm_string,4,2)}."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,6,2)."\n"; + $plm_ack_pos = 8; + } elsif($plm_cmd_id eq '0265'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '0266'){ + $plm_message .= sprintf("%20s: ",'Device Category').substr($plm_string,4,2).":".substr($plm_string,6,2)."\n"; + $plm_message .= sprintf("%20s: ",'Firmware').substr($plm_string,8,2)."\n"; + $plm_ack_pos = 10; + } elsif($plm_cmd_id eq '0267'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '0268'){ + $plm_message .= sprintf("%20s: ",'Command2 Data').substr($plm_string,4,2)."\n"; + $plm_ack_pos = 6; + } elsif($plm_cmd_id eq '0269'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '026a'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '026b'){ + $plm_message .= sprintf("%20s: (",'PLM Config Flags').substr($plm_string,4,2).")\n"; + my $flags = hex(substr($plm_string,4,2)); + $plm_message .= sprintf("%20s: Automatic Linking ",'Bit 7').($flags&0b10000000?'Disabled':'Enabled')."\n"; + $plm_message .= sprintf("%20s: Monitor Mode ",'Bit 6').($flags&0b01000000?'Enabled':'Disabled')."\n"; + $plm_message .= sprintf("%20s: Automatic LED ",'Bit 5').($flags&0b00100000?'Disabled':'Enabled')."\n"; + $plm_message .= sprintf("%20s: Deadman Feature ",'Bit 4').($flags&0b00010000?'Disabled':'Enabled')."\n"; + $plm_ack_pos = 6; + } elsif($plm_cmd_id eq '026c'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '026d'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '026e'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '026f'){ + my %control_string = ('00'=>'Find All-Link Record', + '01'=>'Find Next All-Link Record', + '20'=>'Update/Add All-Link Record', + '40'=>'Update/Add Controller All-Link Record', + '41'=>'Update/Add Responder All-Link Record', + '80'=>'Delete All-Link Record'); + $plm_message .= sprintf("%20s: (",'Control code').substr($plm_string,4,2).") ".$control_string{substr($plm_string,4,2)}."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Flags').substr($plm_string,6,2)."\n"; + my $flags = hex(substr($plm_string,6,2)); + $plm_message .= sprintf("%20s: Record is ",'Bit 7').($flags&0b10000000?'in use':'available')."\n"; + $plm_message .= sprintf("%20s: PLM is ",'Bit 6').($flags&0b01000000?'controller':'responder')."\n"; + $plm_message .= sprintf("%20s: ACK is ",'Bit 5').($flags&0b00100000?'required':'not required')."\n"; + $plm_message .= sprintf("%20s: Record has ",'Bit 1').($flags&0b00000001?'been used before':'not been used before')."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Group').substr($plm_string,8,2)."\n"; + $plm_message .= sprintf("%20s: ",'Linked Device').substr($plm_string,10,2).":".substr($plm_string,12,2).":".substr($plm_string,14,2)."\n"; +# $plm_message .= sprintf("%20s: ",'Link Data').substr($plm_string,16,6)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Command1').substr($plm_string,16,2)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Command2').substr($plm_string,18,2)."\n"; + $plm_message .= sprintf("%20s: ",'All-Link Data').substr($plm_string,20,2)."\n"; + $plm_ack_pos = 22; + #TODO: Find insteon information for link data decode + } elsif($plm_cmd_id eq '0270'){ + $plm_message .= sprintf("%20s: ",'Command2 Data').substr($plm_string,4,2)."\n"; + $plm_ack_pos = 6; + } elsif($plm_cmd_id eq '0271'){ + $plm_message .= sprintf("%20s: ",'Command1 Data').substr($plm_string,4,2)."\n"; + $plm_message .= sprintf("%20s: ",'Command2 Data').substr($plm_string,6,2)."\n"; + $plm_ack_pos = 8; + } elsif($plm_cmd_id eq '0272'){ + $plm_ack_pos = 4; + } elsif($plm_cmd_id eq '0273'){ + if(length($plm_string)>4) { + $plm_message .= sprintf("%20s: (",'PLM Config Flags').substr($plm_string,4,2).")\n"; + my $flags = hex(substr($plm_string,4,2)); + $plm_message .= sprintf("%20s: Automatic Linking ",'Bit 7').($flags&0b10000000?'Disabled':'Enabled')."\n"; + $plm_message .= sprintf("%20s: Monitor Mode ",'Bit 6').($flags&0b01000000?'Enabled':'Disabled')."\n"; + $plm_message .= sprintf("%20s: Automatic LED ",'Bit 5').($flags&0b00100000?'Disabled':'Enabled')."\n"; + $plm_message .= sprintf("%20s: Deadman Feature ",'Bit 4').($flags&0b00010000?'Disabled':'Enabled')."\n"; + $plm_message .= sprintf("%20s: ",'Spare 1').substr($plm_string,6,2)."\n"; + $plm_message .= sprintf("%20s: ",'Spare 2').substr($plm_string,8,2)."\n"; + } + $plm_ack_pos = 10; + } else { + $plm_message .= sprintf("%20s: (",'Undefined Cmd Data').substr($plm_string,4).")\n"; + $plm_ack_pos = 255; + } + + if(length($plm_string)>$plm_ack_pos) { + $plm_message .= sprintf("%20s: (",'PLM Response').substr($plm_string,$plm_ack_pos,2).") ".(substr($plm_string,$plm_ack_pos,2) eq '06' ? "ACK" : "NACK")."\n"; + } + $finished++; + } #if($FSM==) + } #while(!$abort) + return $plm_message; +} + + +=item plm_x10_decode(x10_string) + +Returns a string containing a decoded PLM X10 data packet + +=cut + +sub plm_x10_decode { + my ($x10_string) = @_; + $x10_string = lc($x10_string); + + my $x10_message = ''; + $x10_message .= sprintf("%24s: (",'X10 House Code').substr($x10_string,0,1).") ".uc($x10_house_codes{substr($x10_string,0,1)})."\n"; + if(substr($x10_string,2,1) == '8') { + $x10_message .= sprintf("%24s: (",'X10 Command').substr($x10_string,1,1).") ".$x10_commands{substr($x10_string,1,1)}."\n"; + } else { + $x10_message .= sprintf("%24s: (",'X10 Unit Code').substr($x10_string,1,1).") ".uc($x10_unit_codes{substr($x10_string,1,1)})."\n"; + } + return($x10_message); +} + +=item insteon_message_flags_decode(flags_string) + +Returns a string containing decoded Insteon message flags + +=cut + +sub insteon_message_flags_decode { + my ($flags_string) = @_; + $flags_string = lc($flags_string); + + my $flags_message = ''; + my %message_string = ( '4'=>'Broadcast Message', + '0'=>'Direct Message', + '1'=>'ACK of Direct Message', + '5'=>'NAK of Direct Message', + '6'=>'All-Link Broadcast Message', + '2'=>'All-Link Cleanup Direct Message', + '3'=>'ACK of All-Link Cleanup Direct Message', + '7'=>'NAK of All-Link Cleanup Direct Message'); + + my $flag_msg = hex(substr($flags_string,0,1))>>1; + my $flag_ext = hex(substr($flags_string,0,1))&0b0001; + $flags_message .= sprintf("%28s: (%03b) ",'Message Type',$flag_msg).$message_string{$flag_msg}."\n"; + $flags_message .= sprintf("%28s: (%01b) ",'Message Length',$flag_ext).($flag_ext?'Extended Length':'Standard Length')."\n"; + $flags_message .= sprintf("%28s: %d\n",'Hops Left',hex(substr($flags_string,1,1))>>2); + $flags_message .= sprintf("%28s: %d\n",'Max Hops',hex(substr($flags_string,1,1))&0b0011); + return($flags_message); +} + +=item insteon_decode(command_string) + +Returns a string containing a decoded Insteon message. Input +string should be the Insteon message starting with the +message flag byte. + +=cut + +sub insteon_decode { + my ($command_string) = @_; +#Mapping from message type bit field to acronyms used in +# the INSTEON Command Tables documentation +#100 4 - SB: Standard Broadcast + +#000 0 - SD or ED: Standard/Extended Direct +#001 1 - SDA or EDA: Standard/Extended Direct ACK +#101 5 - SDN or EDN: Standard/Extended Direct NACK + +#110 6 - SA: Standard All-Link Broadcast +#010 2 - SC: Standard Cleanup Direct +#011 3 - SCA: Standard Cleanup Direct ACK +#111 7 - SCN: Standard Cleanup Direct NACK + +#For SDA parsing 1st look for SDA command entry, if not found +#then lookup SD command entry for parsing information. + +#For SDN, EDN, SCN NACK responses, lookup coorespnding +#SD, ED, or SC entry for parsing, but always use the +#common NACK decoding for Cmd2 + +#Lookup SB, SD, ED, SA, and SC messages with just the +#Cmd1 entry appended at the key. If Cmd2 Flag == "Command" +#then repeat lookup appending both Cmd1 and Cmd2 for +#the key. If Cmd2 Flag != "Command" then use flag value +#to control how Cmd2 is displayed. If second lookup fails, +#simply print Cmd2 and indicate "not decoded". + + my $extended = hex(substr($command_string,0,1))&0b0001; + my $msg_type = (hex(substr($command_string,0,1))&0b1110)>>1; + my $cmd1 = substr($command_string,2,2); + my $cmd2 = substr($command_string,4,2); + my $data = ''; + $data = substr($command_string,6) if($extended); + + #Truncate $command_string to remove PLM ACK byte + $command_string = substr($command_string,0, ($extended ? 34 : 8)); + my $insteon_message=''; + if( $msg_type == 0) { + #SD/ED: Standard/Extended Direct + $insteon_message .= insteon_decode_cmd(($extended ? 'ED' : 'SD'), $cmd1, $cmd2, $extended, $data); + } elsif( $msg_type == 1 or $msg_type == 5) { + #SDA/EDA: Standard/Extended Direct ACK/NACK + $insteon_message .= insteon_decode_cmd(($extended ? 'EDA' : 'SDA'), $cmd1, $cmd2, $extended, $data); + } elsif( $msg_type == 6) { + #SA: Standard All-Link Broadcast + $insteon_message .= insteon_decode_cmd('SA', $cmd1, $cmd2, $extended, $data); + } elsif( $msg_type == 2) { + #SC: Standard Direct Cleanup + $insteon_message .= insteon_decode_cmd('SC', $cmd1, $cmd2, $extended, $data); + } elsif( $msg_type == 3 or $msg_type == 7) { + #SCA: Standard Direct Cleanup ACK/NACK + $insteon_message .= insteon_decode_cmd('SCA', $cmd1, $cmd2, $extended, $data); + } else { + $insteon_message .= sprintf("%28s: ",'')."Insteon message type not decoded\n"; + } + + return $insteon_message +} + + +sub insteon_decode_cmd { + my ($cmdLookup, $cmd1, $cmd2, $extended, $Data) = @_; + my $insteon_message=''; + my ($cmdDecoder1, $cmdDecoder2); + + #lookup 1st without using Cmd2 + $cmdDecoder1 = $insteonCmd{$cmdLookup.$cmd1}; + + if(!defined($cmdDecoder1)) { + #lookup failed, if this is an ACK/NACK retry w/ direct version + if( $cmdLookup eq 'SDA') { + $cmdDecoder1 = $insteonCmd{'SD'.$cmd1}; + } elsif( $cmdLookup eq 'EDA') { + $cmdDecoder1 = $insteonCmd{'ED'.$cmd1}; + } elsif( $cmdLookup eq 'SCA') { + $cmdDecoder1 = $insteonCmd{'SC'.$cmd1}; + } + if(!defined($cmdDecoder1)) { + #still not found so quit trying to decode + $insteon_message .= sprintf("%28s: ",'Cmd 1').$cmd1." Insteon command not decoded\n"; + $insteon_message .= sprintf("%28s: ",'Cmd 2').$cmd2."\n"; + $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); + return $insteon_message; + } + } + + if($cmdDecoder1->{'Cmd2Flag'} eq 'Command') { + #2nd lookup with Cmd2 + $cmdDecoder2 = $insteonCmd{$cmdLookup.$cmd1.$cmd2}; + if(!defined($cmdDecoder2)) { + #lookup failed, if this is an ACK/NACK retry w/ direct version + if( $cmdLookup eq 'SDA') { + $cmdDecoder2 = $insteonCmd{'SD'.$cmd1}; + } elsif( $cmdLookup eq 'EDA') { + $cmdDecoder2 = $insteonCmd{'ED'.$cmd1}; + } elsif( $cmdLookup eq 'SCA') { + $cmdDecoder2 = $insteonCmd{'SC'.$cmd1}; + } + } + if(!defined($cmdDecoder2)) { + #still not found so don't decode + $insteon_message .= sprintf("%28s: ",'Cmd 1').$cmd1." Insteon command not decoded\n"; + $insteon_message .= sprintf("%28s: ",'Cmd 2').$cmd2."\n"; + $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); + } else { + $insteon_message .= sprintf("%28s: (",'Cmd 1').$cmd1.") ".$cmdDecoder2->{'Cmd1Name'}."\n"; + $insteon_message .= sprintf("%28s: (",'Cmd 2').$cmd2.") ".$cmdDecoder2->{'Cmd2Name'}."\n"; + $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); + } + } elsif( $cmdDecoder1->{'Cmd2Flag'} eq 'Value') { + $insteon_message .= sprintf("%28s: (",'Cmd 1').$cmd1.") ".$cmdDecoder1->{'Cmd1Name'}."\n"; + $insteon_message .= sprintf("%28s: (",'Cmd 2').$cmd2.") ".$cmdDecoder1->{'Cmd2Name'}."\n"; + $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); + } elsif( $cmdDecoder1->{'Cmd2Flag'} eq 'NA') { + $insteon_message .= sprintf("%28s: (",'Cmd 1').$cmd1.") ".$cmdDecoder1->{'Cmd1Name'}."\n"; + $insteon_message .= sprintf("%28s: ",'Cmd 2').$cmd2."\n"; + $insteon_message .= sprintf("%28s: ",'D1-D14').$Data."\n" if( $extended); + } else { + $insteon_message .= "Parse database has undefined Cmd2Flag: ".$cmdDecoder1->{'Cmd2Flag'}; + } + + return $insteon_message; +} + + +#$plm_cmd is 2 byte hex cmd; $send_rec is 0 for send, 1, for rec; $is_extended is 1 if extended send +#returns expected byte length +sub insteon_cmd_len{ + my ($plm_cmd, $send_rec, $is_extended) = @_; + if ($is_extended && $plmcmdlen{uc($plm_cmd)} > 2) { + return $plmcmdlen{uc($plm_cmd)}->[($send_rec+2)]; + } else { + return $plmcmdlen{uc($plm_cmd)}->[$send_rec]; + } +} + + +=back + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc Insteon::MessageDecoder + +=head1 SEE ALSO + +L + +PLM command details can be found in the 2412S Developers Guide. This +document is not supplied by SmartHome but may be available through an +internet search. + +=head1 AUTHOR + +Michael Stovenour + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012 Michael Stovenour + +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. + +=cut + +1; diff --git a/lib/Insteon/MessageDecoder_test.pl b/lib/Insteon/MessageDecoder_test.pl index 8e409bc9c..4487b54d4 100755 --- a/lib/Insteon/MessageDecoder_test.pl +++ b/lib/Insteon/MessageDecoder_test.pl @@ -1,89 +1,89 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl -w -use strict; -use lib ".."; -use Insteon::MessageDecoder; - - -#0250 1e5d8e 1edc30 2b 0d02 -plm_decode_print('02525700'); -plm_decode_print('02525a80'); -plm_decode_print('02530102aabbcc1a1bff'); -plm_decode_print('025402'); -plm_decode_print('02560105aabbcc'); -plm_decode_print('0257ff04aabbcc0badff'); -plm_decode_print('025806'); -plm_decode_print('025a15aabbcc'); -plm_decode_print('0260'); -plm_decode_print('02601edc3003159b06'); -plm_decode_print('026105eeff'); -plm_decode_print('026105eeff06'); -plm_decode_print('02635700'); -plm_decode_print('0263570006'); -plm_decode_print('02635a80'); -plm_decode_print('02635a8006'); -plm_decode_print('02640105'); -plm_decode_print('02640305'); -plm_decode_print('0264000506'); -plm_decode_print('0264ff05'); -plm_decode_print('0265'); -plm_decode_print('026515'); -plm_decode_print('026603159b'); -plm_decode_print('026603159b06'); -plm_decode_print('0267'); -plm_decode_print('026706'); -plm_decode_print('0268bb'); -plm_decode_print('0268bb06'); -plm_decode_print('0269'); -plm_decode_print('026915'); -plm_decode_print('026a'); -plm_decode_print('026a06'); -plm_decode_print('026bf0'); -plm_decode_print('026bf006'); -plm_decode_print('026b00'); -plm_decode_print('026c'); -plm_decode_print('026c06'); -plm_decode_print('026d'); -plm_decode_print('026d15'); -plm_decode_print('026e'); -plm_decode_print('026e06'); -plm_decode_print('026f20ff05aabbcc0badff'); -plm_decode_print('026f41ff05aabbcc0badff15'); -plm_decode_print('0270bb'); -plm_decode_print('0270bb06'); -plm_decode_print('02710bad'); -plm_decode_print('02710bad06'); -plm_decode_print('0272'); -plm_decode_print('027206'); -plm_decode_print('0273f00000'); -plm_decode_print('0273f0000006'); -plm_decode_print('0273000000'); -plm_decode_print('0274000000'); - -plm_decode_print('02621e5d8e0f0d00'); -plm_decode_print('02621e5d8e2f0d00'); -plm_decode_print('02621e5d8e4f0d00'); -plm_decode_print('02621e5d8e6f0d00'); -plm_decode_print('02621e5d8e8f0d00'); -plm_decode_print('02621e5d8eaf0d00'); -plm_decode_print('02621e5d8ecf0d00'); -plm_decode_print('02621e5d8eef0d00'); - -plm_decode_print('02621e5d8e0f0d0006'); -plm_decode_print('02621e5d8e0f0f00'); -plm_decode_print('02501e5d8e1edc302b0f00'); - -plm_decode_print('02622042d30f1f00'); -plm_decode_print('02501e5d8e1edc302b1f00'); - -plm_decode_print('02502042d3000005cb1100'); -plm_decode_print('02502042d3110105cb0600'); - -plm_decode_print('02621f058c1f2e000100000000000000000000000000'); -plm_decode_print('02511f058c1edc30112e000101000020201cfe3f0001000000'); - -sub plm_decode_print { - my ($plm_string) = @_; - print( "PLM Message: $plm_string\n"); - print( Insteon::MessageDecoder::plm_decode($plm_string)."\n"); -} +use strict; +use lib ".."; +use Insteon::MessageDecoder; + + +#0250 1e5d8e 1edc30 2b 0d02 +plm_decode_print('02525700'); +plm_decode_print('02525a80'); +plm_decode_print('02530102aabbcc1a1bff'); +plm_decode_print('025402'); +plm_decode_print('02560105aabbcc'); +plm_decode_print('0257ff04aabbcc0badff'); +plm_decode_print('025806'); +plm_decode_print('025a15aabbcc'); +plm_decode_print('0260'); +plm_decode_print('02601edc3003159b06'); +plm_decode_print('026105eeff'); +plm_decode_print('026105eeff06'); +plm_decode_print('02635700'); +plm_decode_print('0263570006'); +plm_decode_print('02635a80'); +plm_decode_print('02635a8006'); +plm_decode_print('02640105'); +plm_decode_print('02640305'); +plm_decode_print('0264000506'); +plm_decode_print('0264ff05'); +plm_decode_print('0265'); +plm_decode_print('026515'); +plm_decode_print('026603159b'); +plm_decode_print('026603159b06'); +plm_decode_print('0267'); +plm_decode_print('026706'); +plm_decode_print('0268bb'); +plm_decode_print('0268bb06'); +plm_decode_print('0269'); +plm_decode_print('026915'); +plm_decode_print('026a'); +plm_decode_print('026a06'); +plm_decode_print('026bf0'); +plm_decode_print('026bf006'); +plm_decode_print('026b00'); +plm_decode_print('026c'); +plm_decode_print('026c06'); +plm_decode_print('026d'); +plm_decode_print('026d15'); +plm_decode_print('026e'); +plm_decode_print('026e06'); +plm_decode_print('026f20ff05aabbcc0badff'); +plm_decode_print('026f41ff05aabbcc0badff15'); +plm_decode_print('0270bb'); +plm_decode_print('0270bb06'); +plm_decode_print('02710bad'); +plm_decode_print('02710bad06'); +plm_decode_print('0272'); +plm_decode_print('027206'); +plm_decode_print('0273f00000'); +plm_decode_print('0273f0000006'); +plm_decode_print('0273000000'); +plm_decode_print('0274000000'); + +plm_decode_print('02621e5d8e0f0d00'); +plm_decode_print('02621e5d8e2f0d00'); +plm_decode_print('02621e5d8e4f0d00'); +plm_decode_print('02621e5d8e6f0d00'); +plm_decode_print('02621e5d8e8f0d00'); +plm_decode_print('02621e5d8eaf0d00'); +plm_decode_print('02621e5d8ecf0d00'); +plm_decode_print('02621e5d8eef0d00'); + +plm_decode_print('02621e5d8e0f0d0006'); +plm_decode_print('02621e5d8e0f0f00'); +plm_decode_print('02501e5d8e1edc302b0f00'); + +plm_decode_print('02622042d30f1f00'); +plm_decode_print('02501e5d8e1edc302b1f00'); + +plm_decode_print('02502042d3000005cb1100'); +plm_decode_print('02502042d3110105cb0600'); + +plm_decode_print('02621f058c1f2e000100000000000000000000000000'); +plm_decode_print('02511f058c1edc30112e000101000020201cfe3f0001000000'); + +sub plm_decode_print { + my ($plm_string) = @_; + print( "PLM Message: $plm_string\n"); + print( Insteon::MessageDecoder::plm_decode($plm_string)."\n"); +} diff --git a/lib/Insteon/PLMTerminal.pl b/lib/Insteon/PLMTerminal.pl index e8eefb2b0..2af4de0cd 100755 --- a/lib/Insteon/PLMTerminal.pl +++ b/lib/Insteon/PLMTerminal.pl @@ -1,344 +1,344 @@ -#!/usr/bin/perl -w - -=head1 NAME - -B - A standalone way to test an Insteon PLM (2412/2413) - -=head1 SYNOPSIS - -PLMTerminal.pl [OPTIONS] serialport - - serialport - May be windows or linux serial port device - (e.g. COM1, /dev/ttyS0, /dev/ttyUSB0, etc) - - Options: - -nochecksum, -c Disable i2CS checksums - -h, -?, --help Brief help message - --man Full documentation - -=head1 DESCRIPTION - -PLMTerminal.pl is a simple serial terminal application designed to -interact with an Insteon PLM. This tool is handy for studying how -the PLM interacts with the Insteon network. It will display all -PLM messages received on the serial port and decode those messages -using the Insteon::MessageDecoder module. The tool will also -collect any key presses in the [0-9a-fA-F] range and interpret -those key presses as a hex string sent to the PLM after the user -presses enter. The string will be decoded before being sent to -the PLM. - -If the user types an extended Insteon message (PLM command 0x62), a -checksum will be automatically added to the D14 byte for compliance -with i2CS devices. This can be disabled with the --nochecksum option. - -Pressing Ctl-C will exit the program. - -=head1 EXAMPLE - - perl insteon_test.pl /dev/ttyS1 - - perl insteon_test.pl COM1 - -=head1 EXAMPLE PLM COMMANDS - -To get started try typing some of these PLM commands. The first -example 0x60 is a great command to verify that the PLM is working. -If you do not get a response then you may be using the wrong serial -port device or the PLM may not be working. In the examples replace -xxyyzz with the device ID from the device label. - - 0260 - Get PLM Info - 0262xxyyzz0f0d00 - Get Insteon Engine Version of device with zzyyzz id - 0262xxyyzz1f2e000100000000000000000000000000 - Get config request - -=head1 BUGS - -There are probably many bugs. The script was written and tested -with a 2413U PLM v1.7(9b) under Debian Woody linux (perl 5.10) and -under Windows ActiveState Perl (perl 5.12). It "should" work with -other PLMs but the decoder may not decode some messages correctly. -For help undestanding the decoded PLM messages see the -Insteon::MessageDecoder documentation. - -=head1 OPTIONS AND ARGUMENTS - -=over 8 - -=item B<-nochecksum, -c> - -Will not overwrite D14 with the user data checksum on extended messages. -Use this option if you are sending extended messages to non-i2CS devices -and the extended command needs the D14 byte. This should not be necessary. - -=item B<-h, -?, --help> - -Prints a brief help message and exits. - -=item B<--man> - -Prints the everything you ever wanted to know about the script and exits. - -=back - -=head1 SEE ALSO - -L - -PLM command details can be found in the 2412S Developers Guide. This -document is not supplied by SmartHome but may be available through an -internet search. - -=cut - -use strict; -use lib '..', '../site'; -use Insteon::MessageDecoder; -use Term::ReadKey; -use Getopt::Long; -use Pod::Usage; - -use constant { - RX_BLOCKTIME => 100, #block for 100ms - RX_TIMEOUT => 20, #100ms * 20 = 2 seconds -}; - -our $parms = getParameters(); - -my $device; -my $port; -$port = $parms->{'port'}; -if ($^O eq "MSWin32") { - print "Windows; opening Win32 serial port\n"; - require Win32::SerialPort; - die "$@\n" if ($@); - $port = 'COM1' unless $port; - $device = Win32::SerialPort->new($port) or die "Can't start $port\n"; -} -else { - print "Not Windows; opening linux serial port\n"; - require Device::SerialPort; - die "$@\n" if ($@); - $port = '/dev/ttyS0' unless $port; - $device = Device::SerialPort->new($port) or die "Can't start $port\n"; -} -print "Using port=$port\n"; - -$device->error_msg(1); # use built-in error messages -$device->user_msg(0); -$device->databits(8); -$device->baudrate(19200); -$device->parity("none"); -$device->stopbits(1); -$device->dtr_active(1); -$device->handshake("none"); -$device->read_char_time(0); # don't wait for each character -$device->read_const_time(RX_BLOCKTIME); # wait RX_BLOCKTIME (ms) per unfulfilled "read" call -$device->write_settings || die "Could not set up port\n"; -print "Done setting port parameters\n"; - -our $ctlc = 0; -$SIG{INT} = \&handler_ctlc; -sub handler_ctlc { - $SIG{INT} = \&handler_ctlc; - $ctlc++; -} - -print( "Ready to send command. Looking for messages. Use Ctl-C to quit.\n\n"); -my $RxMessage=''; -my $RxTimeout=RX_TIMEOUT; -my $TxMessage=''; -ReadMode(3); #set a consistent readmode for both linux and windows -$| = 1; -while(!$ctlc) { - #Read data from serial port - #Blocks for RX_BLOCKTIME (ms); set above - my ($count, $buffer) = $device->read(25); - for( my $i = 0; $i < $count; $i++) { - $RxMessage .= substr($buffer,$i,1); -# print("RXMessage=>".unpack( 'H*', $RxMessage)."\n"); - #Check to see if an entire message was received - if( plmValidMessage($RxMessage)) { - print "PLM=>".unpack( 'H*', $RxMessage)."\n"; - print Insteon::MessageDecoder::plm_decode(unpack( 'H*', $RxMessage))."\n"; - $RxMessage = ''; - $RxTimeout=RX_TIMEOUT; - } - } - - #Once message reception starts check for receive timeout. - #Will only occur if there is message corruption or if the - #PLM sends a message type that is not in the messageLength hash below. - $RxTimeout-- if($RxMessage ne ''); - if( $RxTimeout == 0) { - print("RX Timeout; command not parsed\n"); - print("Dumping: ".unpack('H*',$RxMessage)."\n"); - print Insteon::MessageDecoder::plm_decode(unpack( 'H*', $RxMessage))."\n"; - - $RxMessage = ''; - $RxTimeout=RX_TIMEOUT; - } - - #collect keypresses from user - #Ignore zero length messages (i.e. user just hits enter) - # but print a new line for visual separation - my $key; - while( defined ($key = ReadKey(-1))) { - if( $key eq "\n" or $key eq "\r" and $TxMessage ne '') { # enter - $TxMessage = insertChecksum($TxMessage) if( !$parms->{'nochecksum'}); - print "\nPLM<=".$TxMessage."\n"; - print Insteon::MessageDecoder::plm_decode($TxMessage)."\n"; - $device->write( pack( 'H*', $TxMessage)); - $TxMessage = ''; - } elsif( ($key =~ /[0-9a-fA-F]/)) { - $TxMessage .= $key; - print $key; - } elsif( $key eq "\n" or $key eq "\r") { - print "\n"; - } else { #else just drop the key - } - } -} #while(!$ctlc) - -print "Closing device port\n"; -$device->close || die "\nclose problem with $port\n"; -ReadMode(0); - -sub plmValidMessage { - my ($message) = @_; - - #Need at least 2 bytes to get started - return 0 if(length($message)<2); - -my %messageLength = ( - '50' => 11, - '51' => 25, - '52' => 4, - '53' => 10, - '54' => 3, - '55' => 2, - '56' => 13, - '57' => 10, - '58' => 3, - '60' => 9, - '61' => 6, - '62' => 23, # could get 9 or 23 (Standard or Extended Message received) - '63' => 5, - '64' => 5, - '65' => 3, - '66' => 6, - '67' => 3, - '68' => 4, - '69' => 3, - '6A' => 3, - '6B' => 4, - '6C' => 3, - '6D' => 3, - '6E' => 3, - '6F' => 12, - '70' => 4, - '71' => 5, - '72' => 3, - '73' => 6, - ); - - #look up hex code of message in %messageLength -# print("Looking up command=>".unpack('H*',substr($message,1,1))."\n"); - my $validLength = $messageLength{unpack('H*',substr($message,1,1))}; - - #override for '62' and standard message flag - if( ord(substr($message,1,1)) == 0x62) { - #need 6 bytes to check insteon message type (standard/extended) - return 0 if(length($message)<6); - #Use message flags to determine - $validLength = 9 if( !(ord(substr($message,5,1))&0b00010000)); - } - - #Will return false always if PLM message code is not in hash above - #Eventually the RX_TIMEOUT should declare end of message unless - #the bus is very busy. Multiple messages could get concatinated. -# print("\$validLength=$validLength; length(\$message)=".length($message)."\n"); - return -1 if( defined($validLength) and length($message) == $validLength); - return 0; -} - - -sub insertChecksum { - my ($message) = @_; - - #Only processes a PLM 0x62 Insteon send - # i.e. 0262 toaddr flags cmd1 cmd2 d1 ... d14 - #Must be a string of hex nibbles - # 1111111111222222222233333333334444 - # 01234567890123456789012345678901234567890123 - # i.e. '02622042d31f2e000107110000000000000000000000' - #Verify it is 0x62 extended message (leave others alone) - return $message if( substr($message,2,2) ne '62' or !(hex(substr($message,10,1))&0b0001)); - #Mask off D14 incase one was already set - $message = substr($message,0,42)."00"; - my $sum = 0; - $sum += hex($_) for (unpack('(A2)*', substr($message,12))); - $sum = (~$sum + 1) & 0xff; - $message = substr($message,0,42).unpack( 'H2', chr($sum)); - return $message -} - -sub getParameters { - - my $parms = {}; - - my $nochecksum = 0; -# my $port = ""; - my $help = 0; - my $man = 0; - - GetOptions ( - "nochecksum|c" => \$nochecksum, -# "port|p:s" => \$port, -# "verbose:i" => \$VERBOSE, -# "version|v" => sub {die( "\n$0: Version: $VERSION\n");}, - "help|h|?" => \$help, - "man" => \$man, - ); - - pod2usage(-verbose => 1) if ($help); - pod2usage(-verbose => 2) if ($man); - - if( !defined($ARGV[0])) { - print("Serial port must be specified (e.g. COM1 or /dev/ttyS0)\n\n"); - pod2usage(-verbose => 1); - } - - #Build the hash of command line parameters - $parms->{'nochecksum'} = $nochecksum; -# $parms->{'port'} = $port; - $parms->{'port'} = $ARGV[0]; - - return $parms; -} - -=head1 AUTHOR - -Michael Stovenour - -=head1 LICENSE AND COPYRIGHT - -Copyright 2012 Michael Stovenour - -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. - +#!/usr/bin/perl -w + +=head1 NAME + +B - A standalone way to test an Insteon PLM (2412/2413) + +=head1 SYNOPSIS + +PLMTerminal.pl [OPTIONS] serialport + + serialport + May be windows or linux serial port device + (e.g. COM1, /dev/ttyS0, /dev/ttyUSB0, etc) + + Options: + -nochecksum, -c Disable i2CS checksums + -h, -?, --help Brief help message + --man Full documentation + +=head1 DESCRIPTION + +PLMTerminal.pl is a simple serial terminal application designed to +interact with an Insteon PLM. This tool is handy for studying how +the PLM interacts with the Insteon network. It will display all +PLM messages received on the serial port and decode those messages +using the Insteon::MessageDecoder module. The tool will also +collect any key presses in the [0-9a-fA-F] range and interpret +those key presses as a hex string sent to the PLM after the user +presses enter. The string will be decoded before being sent to +the PLM. + +If the user types an extended Insteon message (PLM command 0x62), a +checksum will be automatically added to the D14 byte for compliance +with i2CS devices. This can be disabled with the --nochecksum option. + +Pressing Ctl-C will exit the program. + +=head1 EXAMPLE + + perl insteon_test.pl /dev/ttyS1 + + perl insteon_test.pl COM1 + +=head1 EXAMPLE PLM COMMANDS + +To get started try typing some of these PLM commands. The first +example 0x60 is a great command to verify that the PLM is working. +If you do not get a response then you may be using the wrong serial +port device or the PLM may not be working. In the examples replace +xxyyzz with the device ID from the device label. + + 0260 - Get PLM Info + 0262xxyyzz0f0d00 - Get Insteon Engine Version of device with zzyyzz id + 0262xxyyzz1f2e000100000000000000000000000000 - Get config request + +=head1 BUGS + +There are probably many bugs. The script was written and tested +with a 2413U PLM v1.7(9b) under Debian Woody linux (perl 5.10) and +under Windows ActiveState Perl (perl 5.12). It "should" work with +other PLMs but the decoder may not decode some messages correctly. +For help undestanding the decoded PLM messages see the +Insteon::MessageDecoder documentation. + +=head1 OPTIONS AND ARGUMENTS + +=over 8 + +=item B<-nochecksum, -c> + +Will not overwrite D14 with the user data checksum on extended messages. +Use this option if you are sending extended messages to non-i2CS devices +and the extended command needs the D14 byte. This should not be necessary. + +=item B<-h, -?, --help> + +Prints a brief help message and exits. + +=item B<--man> + +Prints the everything you ever wanted to know about the script and exits. + +=back + +=head1 SEE ALSO + +L + +PLM command details can be found in the 2412S Developers Guide. This +document is not supplied by SmartHome but may be available through an +internet search. + +=cut + +use strict; +use lib '..', '../site'; +use Insteon::MessageDecoder; +use Term::ReadKey; +use Getopt::Long; +use Pod::Usage; + +use constant { + RX_BLOCKTIME => 100, #block for 100ms + RX_TIMEOUT => 20, #100ms * 20 = 2 seconds +}; + +our $parms = getParameters(); + +my $device; +my $port; +$port = $parms->{'port'}; +if ($^O eq "MSWin32") { + print "Windows; opening Win32 serial port\n"; + require Win32::SerialPort; + die "$@\n" if ($@); + $port = 'COM1' unless $port; + $device = Win32::SerialPort->new($port) or die "Can't start $port\n"; +} +else { + print "Not Windows; opening linux serial port\n"; + require Device::SerialPort; + die "$@\n" if ($@); + $port = '/dev/ttyS0' unless $port; + $device = Device::SerialPort->new($port) or die "Can't start $port\n"; +} +print "Using port=$port\n"; + +$device->error_msg(1); # use built-in error messages +$device->user_msg(0); +$device->databits(8); +$device->baudrate(19200); +$device->parity("none"); +$device->stopbits(1); +$device->dtr_active(1); +$device->handshake("none"); +$device->read_char_time(0); # don't wait for each character +$device->read_const_time(RX_BLOCKTIME); # wait RX_BLOCKTIME (ms) per unfulfilled "read" call +$device->write_settings || die "Could not set up port\n"; +print "Done setting port parameters\n"; + +our $ctlc = 0; +$SIG{INT} = \&handler_ctlc; +sub handler_ctlc { + $SIG{INT} = \&handler_ctlc; + $ctlc++; +} + +print( "Ready to send command. Looking for messages. Use Ctl-C to quit.\n\n"); +my $RxMessage=''; +my $RxTimeout=RX_TIMEOUT; +my $TxMessage=''; +ReadMode(3); #set a consistent readmode for both linux and windows +$| = 1; +while(!$ctlc) { + #Read data from serial port + #Blocks for RX_BLOCKTIME (ms); set above + my ($count, $buffer) = $device->read(25); + for( my $i = 0; $i < $count; $i++) { + $RxMessage .= substr($buffer,$i,1); +# print("RXMessage=>".unpack( 'H*', $RxMessage)."\n"); + #Check to see if an entire message was received + if( plmValidMessage($RxMessage)) { + print "PLM=>".unpack( 'H*', $RxMessage)."\n"; + print Insteon::MessageDecoder::plm_decode(unpack( 'H*', $RxMessage))."\n"; + $RxMessage = ''; + $RxTimeout=RX_TIMEOUT; + } + } + + #Once message reception starts check for receive timeout. + #Will only occur if there is message corruption or if the + #PLM sends a message type that is not in the messageLength hash below. + $RxTimeout-- if($RxMessage ne ''); + if( $RxTimeout == 0) { + print("RX Timeout; command not parsed\n"); + print("Dumping: ".unpack('H*',$RxMessage)."\n"); + print Insteon::MessageDecoder::plm_decode(unpack( 'H*', $RxMessage))."\n"; + + $RxMessage = ''; + $RxTimeout=RX_TIMEOUT; + } + + #collect keypresses from user + #Ignore zero length messages (i.e. user just hits enter) + # but print a new line for visual separation + my $key; + while( defined ($key = ReadKey(-1))) { + if( $key eq "\n" or $key eq "\r" and $TxMessage ne '') { # enter + $TxMessage = insertChecksum($TxMessage) if( !$parms->{'nochecksum'}); + print "\nPLM<=".$TxMessage."\n"; + print Insteon::MessageDecoder::plm_decode($TxMessage)."\n"; + $device->write( pack( 'H*', $TxMessage)); + $TxMessage = ''; + } elsif( ($key =~ /[0-9a-fA-F]/)) { + $TxMessage .= $key; + print $key; + } elsif( $key eq "\n" or $key eq "\r") { + print "\n"; + } else { #else just drop the key + } + } +} #while(!$ctlc) + +print "Closing device port\n"; +$device->close || die "\nclose problem with $port\n"; +ReadMode(0); + +sub plmValidMessage { + my ($message) = @_; + + #Need at least 2 bytes to get started + return 0 if(length($message)<2); + +my %messageLength = ( + '50' => 11, + '51' => 25, + '52' => 4, + '53' => 10, + '54' => 3, + '55' => 2, + '56' => 13, + '57' => 10, + '58' => 3, + '60' => 9, + '61' => 6, + '62' => 23, # could get 9 or 23 (Standard or Extended Message received) + '63' => 5, + '64' => 5, + '65' => 3, + '66' => 6, + '67' => 3, + '68' => 4, + '69' => 3, + '6A' => 3, + '6B' => 4, + '6C' => 3, + '6D' => 3, + '6E' => 3, + '6F' => 12, + '70' => 4, + '71' => 5, + '72' => 3, + '73' => 6, + ); + + #look up hex code of message in %messageLength +# print("Looking up command=>".unpack('H*',substr($message,1,1))."\n"); + my $validLength = $messageLength{unpack('H*',substr($message,1,1))}; + + #override for '62' and standard message flag + if( ord(substr($message,1,1)) == 0x62) { + #need 6 bytes to check insteon message type (standard/extended) + return 0 if(length($message)<6); + #Use message flags to determine + $validLength = 9 if( !(ord(substr($message,5,1))&0b00010000)); + } + + #Will return false always if PLM message code is not in hash above + #Eventually the RX_TIMEOUT should declare end of message unless + #the bus is very busy. Multiple messages could get concatinated. +# print("\$validLength=$validLength; length(\$message)=".length($message)."\n"); + return -1 if( defined($validLength) and length($message) == $validLength); + return 0; +} + + +sub insertChecksum { + my ($message) = @_; + + #Only processes a PLM 0x62 Insteon send + # i.e. 0262 toaddr flags cmd1 cmd2 d1 ... d14 + #Must be a string of hex nibbles + # 1111111111222222222233333333334444 + # 01234567890123456789012345678901234567890123 + # i.e. '02622042d31f2e000107110000000000000000000000' + #Verify it is 0x62 extended message (leave others alone) + return $message if( substr($message,2,2) ne '62' or !(hex(substr($message,10,1))&0b0001)); + #Mask off D14 incase one was already set + $message = substr($message,0,42)."00"; + my $sum = 0; + $sum += hex($_) for (unpack('(A2)*', substr($message,12))); + $sum = (~$sum + 1) & 0xff; + $message = substr($message,0,42).unpack( 'H2', chr($sum)); + return $message +} + +sub getParameters { + + my $parms = {}; + + my $nochecksum = 0; +# my $port = ""; + my $help = 0; + my $man = 0; + + GetOptions ( + "nochecksum|c" => \$nochecksum, +# "port|p:s" => \$port, +# "verbose:i" => \$VERBOSE, +# "version|v" => sub {die( "\n$0: Version: $VERSION\n");}, + "help|h|?" => \$help, + "man" => \$man, + ); + + pod2usage(-verbose => 1) if ($help); + pod2usage(-verbose => 2) if ($man); + + if( !defined($ARGV[0])) { + print("Serial port must be specified (e.g. COM1 or /dev/ttyS0)\n\n"); + pod2usage(-verbose => 1); + } + + #Build the hash of command line parameters + $parms->{'nochecksum'} = $nochecksum; +# $parms->{'port'} = $port; + $parms->{'port'} = $ARGV[0]; + + return $parms; +} + +=head1 AUTHOR + +Michael Stovenour + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012 Michael Stovenour + +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. + =cut \ No newline at end of file diff --git a/lib/Insteon_PLM.pm b/lib/Insteon_PLM.pm index 2b91d37f2..ab2b7bc1c 100644 --- a/lib/Insteon_PLM.pm +++ b/lib/Insteon_PLM.pm @@ -1,990 +1,990 @@ -=head1 B - -=head2 SYNOPSIS - ----Example Code and Usage--- - -=head2 DESCRIPTION - -This is the base interface class for Insteon Power Line Modem (PLM) - -=head2 INHERITS - -L, -L - -=head2 METHODS - -=over - -=cut - -package Insteon_PLM; - -use strict; -use Insteon; -use Insteon::BaseInterface; -use Insteon::BaseInsteon; -use Insteon::AllLinkDatabase; -use Insteon::MessageDecoder; - -@Insteon_PLM::ISA = ('Serial_Item','Insteon::BaseInterface'); - - -my %prefix = ( -#PLM Serial Commands - insteon_received => '0250', - insteon_ext_received => '0251', - x10_received => '0252', - all_link_complete => '0253', - plm_button_event => '0254', - plm_user_reset => '0255', - all_link_clean_failed => '0256', - all_link_record => '0257', - all_link_clean_status => '0258', - plm_info => '0260', - all_link_send => '0261', - insteon_send => '0262', - insteon_ext_send => '0262', - all_link_direct_cleanup => '0262', - x10_send => '0263', - all_link_start => '0264', - all_link_cancel => '0265', - plm_reset => '0267', - all_link_first_rec => '0269', - all_link_next_rec => '026a', - plm_set_config => '026b', - plm_led_on => '026d', - plm_led_off => '026e', - all_link_manage_rec => '026f', - insteon_nak => '0270', - insteon_ack => '0271', - rf_sleep => '0272', - plm_get_config => '0273' -); - -=item C - -Creates a new serial port connection. - -=cut - -sub serial_startup { - my ($instance) = @_; - my $port = $::config_parms{$instance . "_serial_port"}; - my $speed = 19200; - - &::print_log("[Insteon_PLM] serial:$port:$speed"); - &::serial_port_create($instance, $port, $speed,'none','raw'); - -} - -=item C - -Instantiates a new object. - -=cut - -sub new { - my ($class, $port_name, $p_deviceid) = @_; - $port_name = 'Insteon_PLM' if !$port_name; - my $port = $::config_parms{$port_name . "_serial_port"}; - - my $self = new Insteon::BaseInterface(); - $$self{state} = ''; - $$self{said} = ''; - $$self{state_now} = ''; - $$self{port_name} = $port_name; - $$self{port} = $port; - $$self{last_command} = ''; - $$self{_prior_data_fragment} = ''; - bless $self, $class; - $self->restore_data('debug', 'corrupt_count_log'); - $$self{corrupt_count_log} = 0; - $$self{aldb} = new Insteon::ALDB_PLM($self); - - &Insteon::add($self); - - $self->device_id($p_deviceid) if defined $p_deviceid; - - $$self{xmit_delay} = $::config_parms{Insteon_PLM_xmit_delay}; - $$self{xmit_delay} = 0.25 unless defined $$self{xmit_delay}; # and $$self{xmit_delay} > 0.125; - &::print_log("[Insteon_PLM] setting default xmit delay to: $$self{xmit_delay}"); - $$self{xmit_x10_delay} = $::config_parms{Insteon_PLM_xmit_x10_delay}; - $$self{xmit_x10_delay} = 0.5 unless defined $$self{xmit_x10_delay} and $$self{xmit_x10_delay} > 0.5; - &::print_log("[Insteon_PLM] setting x10 xmit delay to: $$self{xmit_x10_delay}"); - $self->_clear_timeout('xmit'); - $self->_clear_timeout('command'); - - return $self; -} - -=item C - -Sets or gets the number of corrupt message that have arrived that could not be -associated with any device since the last time C was called. -These are generally instances in which the from device ID is corrupt. - -If type is set, to any value, will increment corrupt count by one. - -Returns: current corrupt count. - -=cut - -sub corrupt_count_log -{ - my ($self, $corrupt_count_log) = @_; - $$self{corrupt_count_log}++ if $corrupt_count_log; - return $$self{corrupt_count_log}; -} - -=item C - -Resets the retry, fail, outgoing, incoming, and corrupt message counters. - -=cut - -sub reset_message_stats -{ - my ($self) = @_; - $$self{corrupt_count_log} = 0; -} - -=item C - -This is called by mh on exit to save the cached ALDB of a device to persistant data. - -=cut - -sub restore_string -{ - my ($self) = @_; - my $restore_string = $self->SUPER::restore_string(); - if ($self->_aldb) { - $restore_string .= $self->_aldb->restore_string(); - } - return $restore_string; -} - -=item C - -Called once per loop. This checks for any data waiting on the serial port, if -data exists it is sent to C<_parse_data>. If there is no data waiting, then -this checks to see if the timers for any previous commands have expired, if they -have, it calls C. Else, this checks to see if there -is any timeout preventing a transmission right now, if there is no timeout it -calles C. - -=cut - -sub check_for_data { - - my ($self) = @_; - my $port_name = $$self{port_name}; - &::check_for_generic_serial_data($port_name) if $::Serial_Ports{$port_name}{object}; - my $data = $::Serial_Ports{$port_name}{data}; - # always check for data first; if it exists, then process; otherwise check if pending commands exist - if ($data) - { - # now, clear the serial port data so that any subsequent command processing doesn't result in an immediate filling/overwriting - if (length($$self{_data_fragment})) - { -# $main::Serial_Ports{$port_name}{data}=pack("H*",$$self{_data_fragment}); - # always clear the buffer since we're maintaining the fragment separately - $main::Serial_Ports{$port_name}{data} = ''; - } - else - { - $main::Serial_Ports{$port_name}{data} = ''; - } - - #lets turn this into Hex. I hate perl binary funcs - my $data = unpack "H*", $data; - - $self->_parse_data($data); - } - elsif (defined $self) - { - # if no data being received, then check if any timeouts have expired - if ($self->_check_timeout('command') == 1) - { - $self->_clear_timeout('command'); - if ($self->transmit_in_progress) { -# &::print_log("[Insteon_PLM] WARN: No acknowledgement from PLM to last command requires forced abort of current command." -# . " This may reflect a problem with your environment."); -# pop(@{$$self{command_stack2}}); # pop the active command off the queue - $self->retry_active_message(); - $self->process_queue(); - } - else - { - &::print_log("[Insteon_PLM] DEBUG2: PLM command timer expired but no transmission in place. Moving on...") if $self->debuglevel(2, 'insteon'); - $self->clear_active_message(); - $self->process_queue(); - } - } - elsif ($self->_check_timeout('xmit') == 1) - { - $self->_clear_timeout('xmit'); - if (!($self->transmit_in_progress)) - { - $self->process_queue(); - } - } - } -} - -=item C - -Used to send X10 messages, generates an X10 command and queues it. - -=cut - -sub set -{ - my ($self,$p_state,$p_setby,$p_response) = @_; - - my @x10_commands = &Insteon::X10Message::generate_commands($p_state, $p_setby); - foreach my $command (@x10_commands) - { - $self->queue_message(new Insteon::X10Message($command)); - } -} - -=item C - -Puts the PLM into linking mode as a responder. - -=cut - -sub complete_linking_as_responder -{ - my ($self, $group) = @_; - - # it is not clear that group should be anything as the group will be taken from the controller - $group = '01' unless $group; - # set up the PLM as the responder - my $cmd = '00'; # responder code - $cmd .= $group; # WARN - must be 2 digits and in hex!! - my $message = new Insteon::InsteonMessage('all_link_start', $self); - $message->interface_data($cmd); - $self->queue_message($message) -} - -=item C - -Causes MisterHouse to dump its cache of the PLM link table to the log. - -=cut - -sub log_alllink_table -{ - my ($self) = @_; - $self->_aldb->log_alllink_table if $self->_aldb; -} - -=item C - -Causes MisterHouse to scan the link table of the PLM only. - -=cut - -sub scan_link_table -{ - my ($self,$callback) = @_; - #$$self{links} = undef; # clear out the old - $$self{aldb} = new Insteon::ALDB_PLM($self); - $$self{_mem_activity} = 'scan'; - $$self{_mem_callback} = ($callback) ? $callback : undef; - $self->_aldb->get_first_alllink(); -} - -=item C - -Puts the PLM into linking mode as a controller, if p_group is specified the -controller will be added for this group, otherwise it will be for group 00. - -=cut - -sub initiate_linking_as_controller -{ - my ($self, $group, $success_callback, $failure_callback) = @_; - - $group = '00' unless $group; - # set up the PLM as the responder - my $cmd = '01'; # controller code - $cmd .= $group; # WARN - must be 2 digits and in hex!! - my $message = new Insteon::InsteonMessage('all_link_start', $self); - $message->interface_data($cmd); - $message->success_callback($success_callback); - $message->failure_callback($failure_callback); - $self->queue_message($message); -} - -=item C - -Puts the PLM into unlinking mode, if p_group is specified the PLM will try -to unlink any devices linked to that group that identify themselves with a set -button press. - -=cut - -sub initiate_unlinking_as_controller -{ - my ($self, $group) = @_; - - $group = 'FF' unless $group; - # set up the PLM as the responder - my $cmd = 'FF'; # controller code - $cmd .= $group; # WARN - must be 2 digits and in hex!! - my $message = new Insteon::InsteonMessage('all_link_start', $self); - $message->interface_data($cmd); - $self->queue_message($message); -} - -=item C - -Cancels any pending linking session that has not completed. - -=cut - -sub cancel_linking -{ - my ($self) = @_; - $self->queue_message(new Insteon::InsteonMessage('all_link_cancel', $self)); -} - -=item C<_aldb()> - -Returns the PLM's aldb object. - -=cut - -sub _aldb -{ - my ($self) = @_; - return $$self{aldb}; -} - -=item C<_send_cmd()> - -Causes a message to be sent to the serial port. - -=cut - -sub _send_cmd { - my ($self, $message, $cmd_timeout) = @_; - my $instance = $$self{port_name}; - if (!(ref $main::Serial_Ports{$instance}{object})) { - print "WARN: Insteon_PLM serial port not initialized!\n"; - return; - } - unshift(@{$$self{command_history}},$::Time); - $self->transmit_in_progress(1); - - my $command = $message->interface_data; - my $delay = $$self{xmit_delay}; - - # determine the delay from the point that the message was created to - # the point that it is queued - my $incurred_delay_time = $message->seconds_delayed; - - if ($message->isa('Insteon::X10Message')) { # is x10; so, be slow - &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " - . sprintf('%.2f',$incurred_delay_time) . " seconds") if $self->debuglevel(2, 'insteon'); - $command = $prefix{x10_send} . $command; - $delay = $$self{xmit_x10_delay}; - # clear command timeout so that we don't wait for an insteon ack before sending the next command - } else { - my $command_type = $message->command_type; - &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " - . sprintf('%.2f',$incurred_delay_time) . " seconds; starting hop-count: " - . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $message->setby->debuglevel(2, 'insteon'); - $command = $prefix{$command_type} . $command; - if ($command_type eq 'all_link_send' or $command_type eq 'insteon_send' or $command_type eq 'insteon_ext_send' or $command_type eq 'all_link_direct_cleanup') - { - $self->_set_timeout('command', $cmd_timeout); # a commmand needs to be PLM ack'd w/i 3 seconds or it gets dropped - } - } - my $is_extended = ($message->can('command_type') && $message->command_type eq "insteon_ext_send") ? 1 : 0; - if (length($command) != (Insteon::MessageDecoder::insteon_cmd_len(substr($command,0,4), 0, $is_extended)*2)){ - &::print_log( "[Insteon_PLM]: ERROR!! Command sent to PLM " . lc($command) - . " is of an incorrect length. Message not sent."); - $self->clear_active_message(); - } - else - { - my $debug_obj = $self; - $debug_obj = $message->setby if ($message->can('setby') && ref $message->setby); - &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $debug_obj->debuglevel(3, 'insteon'); - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $debug_obj->debuglevel(4, 'insteon'); - my $data = pack("H*",$command); - $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; - - - if ($delay) { - $self->_set_timeout('xmit',$delay * 1000); - } - $$self{'last_change'} = $main::Time; - } -} - -=item C<_parse_data()> - -A complex routine that parses data comming in from the serial port. In many cases -multiple messages or fragments of messages may arrive at once. This routine sorts -through the string of hexadecimal characters and determines what type of message -has arrived and its full content. Based on the type of message, it is then -passed off to lower level message handling routines. - -=cut - -sub _parse_data { - my ($self, $data) = @_; - my ($name, $val); - - # it is possible that a fragment exists from a previous attempt; so, if it exists, prepend it - if ($$self{_data_fragment}) - { - &::print_log("[Insteon_PLM] DEBUG3: Prepending prior data fragment: $$self{_data_fragment}") if $self->debuglevel(3, 'insteon'); - # maintain a copy of the parsed data fragment - $$self{_prior_data_fragment} = $$self{_data_fragment}; - # append if not a repeat - $data = $$self{_data_fragment} . $data unless $$self{_data_fragment} eq $data; - # and, clear it out - $$self{_data_fragment} = ''; - } - else - { - # clear the memory of any prior data fragment - $$self{_prior_data_fragment} = ''; - } - - &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $self->debuglevel(3, 'insteon'); - - # begin by pulling out any PLM ack/nacks - my $prev_cmd = ''; - my $pending_message = $self->active_message; - if ($pending_message) { - $prev_cmd = lc $pending_message->interface_data; - if ($pending_message->isa('Insteon::X10Message')) - { - $prev_cmd = $prefix{x10_send} . $prev_cmd; - } else { - my $command_type = $pending_message->command_type; - $prev_cmd = $prefix{$command_type} . $prev_cmd; - } - } - - my $residue_data = ''; - my $process_next_command = 1; - my $nack_count = 0; - my $entered_ack_loop; - my $previous_parsed_data; - if (defined $prev_cmd and $prev_cmd ne '') - { - my $ackcmd = $prev_cmd . '06'; - my $nackcmd = $prev_cmd . '15'; - my $badcmd = $prev_cmd . '0f'; - $previous_parsed_data = ''; - foreach my $parsed_data (split(/($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($badcmd)/,$data)) - { - #ignore blanks.. the split does odd things - next if $parsed_data eq ''; - next if $previous_parsed_data eq $parsed_data; # guard against repeats - $previous_parsed_data = $parsed_data; # and, now reinitialize - $entered_ack_loop = 1; - if ($parsed_data =~ /^($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($prefix{all_link_first_rec}15)|($prefix{all_link_next_rec}15)|($badcmd)$/) - { - my $debug_obj = $self; - $debug_obj = $self->active_message->setby if ($self->active_message->can('setby') && ref $self->active_message->setby); - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $debug_obj->debuglevel(4, 'insteon'); - my $ret_code = substr($parsed_data,length($parsed_data)-2,2); - my $record_type = substr($parsed_data,0,4); - my $message_data = substr($parsed_data,4,length($parsed_data)-4); - if ($ret_code eq '06') - { - if ($record_type eq $prefix{plm_info}) - { - $self->device_id(substr($message_data,0,6)); - $self->firmware(substr($message_data,10,2)); - $self->on_interface_info_received(); - } - elsif ($record_type eq $prefix{all_link_first_rec} - or $record_type eq $prefix{all_link_next_rec}) - { - $$self{_next_link_ok} = 1; - } - elsif ($record_type eq $prefix{all_link_start}) - { - if ($self->active_message->success_callback){ - package main; - eval ($self->active_message->success_callback); - &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->active_message->setby->debuglevel(1, 'insteon'); - package Insteon_PLM; - } - # clear the active message because we're done - $self->clear_active_message(); - } - else - { - my $debug_obj = $self; - $debug_obj = $self->active_message->setby if ($self->active_message->can('setby') && ref $self->active_message->setby); - &::print_log("[Insteon_PLM] DEBUG3: Received PLM acknowledge: " - . $pending_message->to_string) if $debug_obj->debuglevel(3, 'insteon'); - } - - # X10 messages don't ACK back on the powerline, so clear them if the PLM acknowledges - # AND if the current, pending message is the X10 message - if (($parsed_data =~ /$prefix{x10_send}\w{4}06/) && ($pending_message->isa('Insteon::X10Message'))) - { - $self->clear_active_message(); - } - - if ($record_type eq $prefix{all_link_manage_rec}) - { - # clear the active message because we're done - $self->clear_active_message(); - - my $callback; - if ($self->_aldb->{_success_callback}){ - $callback = $self->_aldb->{_success_callback}; - $self->_aldb->{_success_callback} = undef; - } elsif ($$self{_mem_callback}) - { - $callback = $pending_message->callback(); #$$self{_mem_callback}; - $$self{_mem_callback} = undef; - } - if ($callback){ - package main; - eval ($callback); - &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->active_message->setby->debuglevel(1, 'insteon'); - package Insteon_PLM; - } - } - } - elsif ($ret_code eq '15' or $ret_code eq '0f') - { #NAK or "bad" command received - $self->clear_active_message(); # regardless, we're not retrying as we'll just get the same - - if ($record_type eq $prefix{all_link_first_rec} - or $record_type eq $prefix{all_link_next_rec}) - { - # both of these conditions are ok as it just means - # we've reached the end of the memory - $$self{_next_link_ok} = 0; - $$self{_mem_activity} = undef; - if ($record_type eq $prefix{all_link_first_rec}) - { - $self->_aldb->health("empty"); - } - else - { - $self->_aldb->health("good"); - } - $self->_aldb->scandatetime(&main::get_tickcount); - &::print_log("[Insteon_PLM] " . $self->get_object_name - . " completed link memory scan: status: " . $self->_aldb->health()) - if $self->debuglevel(1, 'insteon'); - if ($$self{_mem_callback}) - { - my $callback = $$self{_mem_callback}; - $$self{_mem_callback} = undef; - package main; - eval ($callback); - &::print_log("[Insteon_PLM] WARN1: Error encountered during nack callback: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon_PLM; - } - } - elsif ($record_type eq $prefix{all_link_send}) - { - &::print_log("[Insteon_PLM] WARN: PLM memory does not contain link for: " - . $pending_message->to_string . $@) - } - elsif ($record_type eq $prefix{all_link_start}) - { - &::print_log("[Insteon_PLM] WARN: PLM unable to complete requested operation: " - . $pending_message->to_string . $@); - } - elsif ($record_type eq $prefix{all_link_manage_rec}) - { - # parse out the data - my $failed_cmd_code = substr($pending_message->interface_data(),0,2); - my $failed_cmd = 'unknown'; - if ($failed_cmd_code eq '40') - { - $failed_cmd = 'update/add controller record'; - } - elsif ($failed_cmd_code eq '41') - { - $failed_cmd = 'update/add responder record'; - } - elsif ($failed_cmd_code eq '80') - { - $failed_cmd = 'delete record'; - } - my $failed_group = substr($pending_message->interface_data(),4,2); - my $failed_deviceid = substr($pending_message->interface_data(),6,6); - &::print_log("[Insteon_PLM] WARN: PLM unable to complete requested " - . "PLM link table update ($failed_cmd) for " - . "group: $failed_group and deviceid: $failed_deviceid" ); - my $callback; - if ($self->_aldb->{_success_callback}){ - $callback = $self->_aldb->{_success_callback}; - $self->_aldb->{_success_callback} = undef; - } elsif ($$self{_mem_callback}) - { - $callback = $pending_message->callback(); #$$self{_mem_callback}; - $$self{_mem_callback} = undef; - } - if ($callback) - { - package main; - eval ($callback); - &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) - if $@ and $self->debuglevel(1, 'insteon'); - package Insteon_PLM; - } - # clear the active message because we're done - # $self->clear_active_message(); - } - else - { - &::print_log("[Insteon_PLM] WARN: received NACK from PLM for " - . $pending_message->to_string()); - } - } - else - { - # We have a problem (Usually we stepped on another X10 command) - &::print_log("[Insteon_PLM] ERROR: encountered $parsed_data. " - . $pending_message->to_string()); - $self->active_message->no_hop_increase(1); - $self->retry_active_message(); - #move it off the top of the stack and re-transmit later! - #TODO: We should keep track of an errored command and kill it if it fails twice. prevent an infinite loop here - } - } - else # no match occurred--which is the "leftovers" - { - # is $parsed_data an accidental anomoly? (there are other cases; but, this is a good start) - if ($parsed_data =~ /^($prefix{insteon_send}\w{12}06)|($prefix{insteon_send}\w{12}15)$/) - { - # first, parse the content to confirm that it could be a legitimate ACK - my $unknown_deviceid = substr($parsed_data,4,6); - my $unknown_msg_flags = substr($parsed_data,10,2); - my $unknown_command = substr($parsed_data,12,2); - my $unknown_data = substr($parsed_data,14,2); - my $unknown_obj = &Insteon::get_object($unknown_deviceid, '01'); - if ($unknown_obj) - { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $unknown_obj->debuglevel(4, 'insteon'); - &::print_log("[Insteon_PLM] WARN: encountered '$parsed_data' " - . "from " . $unknown_obj->get_object_name() - . " with command: $unknown_command, but expected '$ackcmd'."); - $residue_data .= $parsed_data; - } - else - { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - &::print_log("[Insteon_PLM] ERROR: encountered '$parsed_data' " - . "that does not match any known device ID (expected '$ackcmd')." - . " Discarding received data."); - #$residue_data .= $parsed_data; - } - $self->active_message->no_hop_increase(1); - } - else - { - $residue_data .= $parsed_data; - } - } - } #foreach - split across the incoming data - - $residue_data = $data unless $entered_ack_loop or $residue_data; - } - else - { - $residue_data = $data unless $residue_data; - } - - my $entered_rcv_loop = 0; - - $previous_parsed_data = ''; - - foreach my $parsed_data (split(/($prefix{x10_received}\w{4})|($prefix{insteon_received}\w{18})|($prefix{insteon_ext_received}\w{46})|($prefix{all_link_complete}\w{16})|($prefix{all_link_clean_failed}\w{8})|($prefix{all_link_record}\w{16})|($prefix{all_link_clean_status}\w{2})|($prefix{plm_button_event}\w{2})|($prefix{plm_user_reset})/,$residue_data)) - { - #ignore blanks.. the split does odd things - next if $parsed_data eq ''; - - if ($previous_parsed_data eq $parsed_data){ - # guard against repeats - ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $self->debuglevel(3, 'insteon'); - next; - } - $previous_parsed_data = $parsed_data; # and, now reinitialize - - $entered_rcv_loop = 1; - - my $parsed_prefix = substr($parsed_data,0,4); - my $message_length = length($parsed_data); - - my $message_data = substr($parsed_data,4,length($parsed_data)-4); - - if ($parsed_prefix eq $prefix{insteon_received} and ($message_length == 22)) - { #Insteon Standard Received - my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); - if (ref $find_obj) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4, 'insteon'); - } - else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - } - $self->on_standard_insteon_received($message_data); - } - elsif ($parsed_prefix eq $prefix{insteon_ext_received} and ($message_length == 50)) - { #Insteon Extended Received - my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); - if (ref $find_obj) { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4, 'insteon'); - } - else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - } - $self->on_extended_insteon_received($message_data); - } - elsif($parsed_prefix eq $prefix{x10_received} and ($message_length == 8)) - { #X10 Received - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - my $x10_message = new Insteon::X10Message($parsed_data); - my $x10_data = $x10_message->get_formatted_data(); - &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $self->debuglevel(3, 'insteon'); - &::process_serial_data($x10_data,undef,$self); - } - elsif ($parsed_prefix eq $prefix{all_link_complete} and ($message_length == 20)) - { #ALL-Linking Completed - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - my $link_address = substr($message_data,4,6); - &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $self->debuglevel(2, 'insteon'); - if ($self->active_message->success_callback){ - main::print_log("[Insteon::Insteon_PLM] DEBUG4: Now calling message success callback: " - . $self->active_message->success_callback) if $self->debuglevel(4, 'insteon'); - package main; - eval $self->active_message->success_callback; - ::print_log("[Insteon::Insteon_PLM] problem w/ success callback: $@") if $@; - package Insteon::BaseObject; - } - #Clear awaiting_ack flag - $self->active_message->setby->_process_command_stack(0); - $self->clear_active_message(); - } - elsif ($parsed_prefix eq $prefix{all_link_clean_failed} and ($message_length == 12)) - { #ALL-Link Cleanup Failure Report - if ($self->active_message){ - # extract out the pertinent parts of the message for display purposes - # bytes 0-1 - group; 2-7 device address - my $failure_group = substr($message_data,0,2); - my $failure_device = substr($message_data,2,6); - my $failed_object = &Insteon::get_object($failure_device,'01'); - if (ref $failed_object){ - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $failed_object->debuglevel(4, 'insteon'); - &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure from " . $failed_object->get_object_name - . " for all link group: $failure_group. Trying a direct cleanup.") if $failed_object->debuglevel(2, 'insteon'); - my $message = new Insteon::InsteonMessage('all_link_direct_cleanup', $failed_object, - $self->active_message->command, $failure_group); - push(@{$$failed_object{command_stack}}, $message); - $failed_object->_process_command_stack(); - } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - &::print_log("[Insteon_PLM] Received all-link cleanup failure from an unkown device id: " - . "$failure_device and for all link group: $failure_group. You may " - . "want to run delete orphans to remove this link from your PLM"); - } - } else { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure." - . " But there is no pending message.") if $self->debuglevel(2, 'insteon'); - } - - } - elsif ($parsed_prefix eq $prefix{all_link_record} and ($message_length == 20)) - { #ALL-Link Record Response - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $self->debuglevel(2, 'insteon'); - $self->_aldb->parse_alllink($message_data); - # before doing the next, make sure that the pending command - # (if it sitll exists) is pulled from the queue - $self->clear_active_message(); - - $self->_aldb->get_next_alllink(); - } - elsif ($parsed_prefix eq $prefix{plm_user_reset} and ($message_length == 4)) - { - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - main::print_log("[Insteon_PLM] Detected PLM user reset to factory defaults"); - } - elsif ($parsed_prefix eq $prefix{all_link_clean_status} and ($message_length == 6)) - { #ALL-Link Cleanup Status Report - &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); - my $cleanup_ack = substr($message_data,0,2); - if ($cleanup_ack eq '15') - { - &::print_log("[Insteon_PLM] WARN1: All-link cleanup failure for scene: " - . $self->active_message->setby->get_object_name . ". Retrying in 1 second.") - if $self->active_message->setby->debuglevel(1, 'insteon'); - $self->retry_active_message(); - # except that we should cause a bit of a delay to let things settle out - $self->_set_timeout('xmit', 1000); - $process_next_command = 0; - } - else - { - my $message_to_string = ($self->active_message) ? $self->active_message->to_string() : ""; - &::print_log("[Insteon_PLM] Received all-link cleanup success: $message_to_string") - if $self->active_message->setby->debuglevel(1, 'insteon'); - if (ref $self->active_message && ref $self->active_message->setby){ - my $object = $self->active_message->setby; - $object->is_acknowledged(1); - $object->_process_command_stack(); - } - $self->clear_active_message(); - } - } - elsif (substr($parsed_data,0,2) eq '15') - { # Indicates that the PLM can't receive more commands at the moment - # so, slow things down - if (!($nack_count)) - { - if ($self->active_message){ - my $nack_delay = ($::config_parms{Insteon_PLM_disable_throttling}) ? 0.3 : 1.0; - &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy. Resending command" - . " after delaying for $nack_delay second") if $self->debuglevel(3, 'insteon'); - $self->_set_timeout('xmit',$nack_delay * 1000); - $self->active_message->no_hop_increase(1); - $self->retry_active_message(); - $process_next_command = 0; - } else { - &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy." - . " No message to resend.") if $self->debuglevel(3, 'insteon'); - } - $nack_count++; - } - #Remove the leading NACK bytes and place whatever remains into fragment for next read - $parsed_data =~ s/^(15)*//; - if ($parsed_data ne ''){ - $$self{_data_fragment} .= $parsed_data; - ::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " - . $parsed_data) if( $self->debuglevel(3, 'insteon')); - } - } - else - { - # it's probably a fragment; so, handle it - # it it's the same as last time, then drop it as we can't recover - unless (($parsed_data eq $$self{_prior_data_fragment}) or ($parsed_data eq $$self{_data_fragment})) { - $$self{_data_fragment} .= $parsed_data; - main::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " - . $parsed_data) if( $self->debuglevel(3, 'insteon')); - } - } - } - - unless( $entered_rcv_loop or $$self{_data_fragment}) { - $$self{_data_fragment} = $residue_data; - main::print_log("[Insteon_PLM] DEBUG3: Saving residue data fragment: " - . $residue_data) if( $residue_data and $self->debuglevel(3, 'insteon')); - } - - if ($process_next_command) { - $self->process_queue(); - } - - return; -} - -=item C - -Dummy sub required to support the X10 integrtion, does nothing. - -=cut - -sub add_id_state { - # do nothing -} - -=item C - -Stores and returns the firmware version of the PLM. - -=cut - -sub firmware { - my ($self, $p_firmware) = @_; - $$self{firmware} = $p_firmware if defined $p_firmware; - return $$self{firmware}; -} - -=back - -=head2 INI PARAMETERS - -=over - -=item Insteon_PLM_serial_port - -Identifies the port on which the PLM is attached. Example: - - Insteon_PLM_serial_port=/dev/ttyS4 - -=item Insteon_PLM_xmit_delay - -Sets the minimum amount of seconds that must elapse between sending Insteon messages -to the PLM. Defaults to 0.25. - -=item Insteon_PLM_xmit_x10_delay - -Sets the minimum amount of seconds that must elapse between sending X10 messages -to the PLM. Defaults to 0.50. - -=item Insteon_PLM_disable_throttling - -Periodically, the PLM will report that it is too busy to accept a message from -MisterHouse. When this happens, MisterHouse will wait 1 second before trying -to send a message to the PLM. If this is set to 1, downgrades the delay to only -.3 seconds. Most of the issues which caused the PLM to overload have been handled -it is unlikely that you would need to set this. - -=back - -=head2 NOTES - -Special Thanks to: - -Brian Warren for significant testing and patches - -Bruce Winter - MH - -=head2 AUTHOR - -Jason Sharpee / jason@sharpee.com, Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour - -=head2 SEE ALSO - -For more information regarding the technical details of the PLM: -L - -=head2 LICENSE - -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. - -=cut - - -1; +=head1 B + +=head2 SYNOPSIS + +---Example Code and Usage--- + +=head2 DESCRIPTION + +This is the base interface class for Insteon Power Line Modem (PLM) + +=head2 INHERITS + +L, +L + +=head2 METHODS + +=over + +=cut + +package Insteon_PLM; + +use strict; +use Insteon; +use Insteon::BaseInterface; +use Insteon::BaseInsteon; +use Insteon::AllLinkDatabase; +use Insteon::MessageDecoder; + +@Insteon_PLM::ISA = ('Serial_Item','Insteon::BaseInterface'); + + +my %prefix = ( +#PLM Serial Commands + insteon_received => '0250', + insteon_ext_received => '0251', + x10_received => '0252', + all_link_complete => '0253', + plm_button_event => '0254', + plm_user_reset => '0255', + all_link_clean_failed => '0256', + all_link_record => '0257', + all_link_clean_status => '0258', + plm_info => '0260', + all_link_send => '0261', + insteon_send => '0262', + insteon_ext_send => '0262', + all_link_direct_cleanup => '0262', + x10_send => '0263', + all_link_start => '0264', + all_link_cancel => '0265', + plm_reset => '0267', + all_link_first_rec => '0269', + all_link_next_rec => '026a', + plm_set_config => '026b', + plm_led_on => '026d', + plm_led_off => '026e', + all_link_manage_rec => '026f', + insteon_nak => '0270', + insteon_ack => '0271', + rf_sleep => '0272', + plm_get_config => '0273' +); + +=item C + +Creates a new serial port connection. + +=cut + +sub serial_startup { + my ($instance) = @_; + my $port = $::config_parms{$instance . "_serial_port"}; + my $speed = 19200; + + &::print_log("[Insteon_PLM] serial:$port:$speed"); + &::serial_port_create($instance, $port, $speed,'none','raw'); + +} + +=item C + +Instantiates a new object. + +=cut + +sub new { + my ($class, $port_name, $p_deviceid) = @_; + $port_name = 'Insteon_PLM' if !$port_name; + my $port = $::config_parms{$port_name . "_serial_port"}; + + my $self = new Insteon::BaseInterface(); + $$self{state} = ''; + $$self{said} = ''; + $$self{state_now} = ''; + $$self{port_name} = $port_name; + $$self{port} = $port; + $$self{last_command} = ''; + $$self{_prior_data_fragment} = ''; + bless $self, $class; + $self->restore_data('debug', 'corrupt_count_log'); + $$self{corrupt_count_log} = 0; + $$self{aldb} = new Insteon::ALDB_PLM($self); + + &Insteon::add($self); + + $self->device_id($p_deviceid) if defined $p_deviceid; + + $$self{xmit_delay} = $::config_parms{Insteon_PLM_xmit_delay}; + $$self{xmit_delay} = 0.25 unless defined $$self{xmit_delay}; # and $$self{xmit_delay} > 0.125; + &::print_log("[Insteon_PLM] setting default xmit delay to: $$self{xmit_delay}"); + $$self{xmit_x10_delay} = $::config_parms{Insteon_PLM_xmit_x10_delay}; + $$self{xmit_x10_delay} = 0.5 unless defined $$self{xmit_x10_delay} and $$self{xmit_x10_delay} > 0.5; + &::print_log("[Insteon_PLM] setting x10 xmit delay to: $$self{xmit_x10_delay}"); + $self->_clear_timeout('xmit'); + $self->_clear_timeout('command'); + + return $self; +} + +=item C + +Sets or gets the number of corrupt message that have arrived that could not be +associated with any device since the last time C was called. +These are generally instances in which the from device ID is corrupt. + +If type is set, to any value, will increment corrupt count by one. + +Returns: current corrupt count. + +=cut + +sub corrupt_count_log +{ + my ($self, $corrupt_count_log) = @_; + $$self{corrupt_count_log}++ if $corrupt_count_log; + return $$self{corrupt_count_log}; +} + +=item C + +Resets the retry, fail, outgoing, incoming, and corrupt message counters. + +=cut + +sub reset_message_stats +{ + my ($self) = @_; + $$self{corrupt_count_log} = 0; +} + +=item C + +This is called by mh on exit to save the cached ALDB of a device to persistant data. + +=cut + +sub restore_string +{ + my ($self) = @_; + my $restore_string = $self->SUPER::restore_string(); + if ($self->_aldb) { + $restore_string .= $self->_aldb->restore_string(); + } + return $restore_string; +} + +=item C + +Called once per loop. This checks for any data waiting on the serial port, if +data exists it is sent to C<_parse_data>. If there is no data waiting, then +this checks to see if the timers for any previous commands have expired, if they +have, it calls C. Else, this checks to see if there +is any timeout preventing a transmission right now, if there is no timeout it +calles C. + +=cut + +sub check_for_data { + + my ($self) = @_; + my $port_name = $$self{port_name}; + &::check_for_generic_serial_data($port_name) if $::Serial_Ports{$port_name}{object}; + my $data = $::Serial_Ports{$port_name}{data}; + # always check for data first; if it exists, then process; otherwise check if pending commands exist + if ($data) + { + # now, clear the serial port data so that any subsequent command processing doesn't result in an immediate filling/overwriting + if (length($$self{_data_fragment})) + { +# $main::Serial_Ports{$port_name}{data}=pack("H*",$$self{_data_fragment}); + # always clear the buffer since we're maintaining the fragment separately + $main::Serial_Ports{$port_name}{data} = ''; + } + else + { + $main::Serial_Ports{$port_name}{data} = ''; + } + + #lets turn this into Hex. I hate perl binary funcs + my $data = unpack "H*", $data; + + $self->_parse_data($data); + } + elsif (defined $self) + { + # if no data being received, then check if any timeouts have expired + if ($self->_check_timeout('command') == 1) + { + $self->_clear_timeout('command'); + if ($self->transmit_in_progress) { +# &::print_log("[Insteon_PLM] WARN: No acknowledgement from PLM to last command requires forced abort of current command." +# . " This may reflect a problem with your environment."); +# pop(@{$$self{command_stack2}}); # pop the active command off the queue + $self->retry_active_message(); + $self->process_queue(); + } + else + { + &::print_log("[Insteon_PLM] DEBUG2: PLM command timer expired but no transmission in place. Moving on...") if $self->debuglevel(2, 'insteon'); + $self->clear_active_message(); + $self->process_queue(); + } + } + elsif ($self->_check_timeout('xmit') == 1) + { + $self->_clear_timeout('xmit'); + if (!($self->transmit_in_progress)) + { + $self->process_queue(); + } + } + } +} + +=item C + +Used to send X10 messages, generates an X10 command and queues it. + +=cut + +sub set +{ + my ($self,$p_state,$p_setby,$p_response) = @_; + + my @x10_commands = &Insteon::X10Message::generate_commands($p_state, $p_setby); + foreach my $command (@x10_commands) + { + $self->queue_message(new Insteon::X10Message($command)); + } +} + +=item C + +Puts the PLM into linking mode as a responder. + +=cut + +sub complete_linking_as_responder +{ + my ($self, $group) = @_; + + # it is not clear that group should be anything as the group will be taken from the controller + $group = '01' unless $group; + # set up the PLM as the responder + my $cmd = '00'; # responder code + $cmd .= $group; # WARN - must be 2 digits and in hex!! + my $message = new Insteon::InsteonMessage('all_link_start', $self); + $message->interface_data($cmd); + $self->queue_message($message) +} + +=item C + +Causes MisterHouse to dump its cache of the PLM link table to the log. + +=cut + +sub log_alllink_table +{ + my ($self) = @_; + $self->_aldb->log_alllink_table if $self->_aldb; +} + +=item C + +Causes MisterHouse to scan the link table of the PLM only. + +=cut + +sub scan_link_table +{ + my ($self,$callback) = @_; + #$$self{links} = undef; # clear out the old + $$self{aldb} = new Insteon::ALDB_PLM($self); + $$self{_mem_activity} = 'scan'; + $$self{_mem_callback} = ($callback) ? $callback : undef; + $self->_aldb->get_first_alllink(); +} + +=item C + +Puts the PLM into linking mode as a controller, if p_group is specified the +controller will be added for this group, otherwise it will be for group 00. + +=cut + +sub initiate_linking_as_controller +{ + my ($self, $group, $success_callback, $failure_callback) = @_; + + $group = '00' unless $group; + # set up the PLM as the responder + my $cmd = '01'; # controller code + $cmd .= $group; # WARN - must be 2 digits and in hex!! + my $message = new Insteon::InsteonMessage('all_link_start', $self); + $message->interface_data($cmd); + $message->success_callback($success_callback); + $message->failure_callback($failure_callback); + $self->queue_message($message); +} + +=item C + +Puts the PLM into unlinking mode, if p_group is specified the PLM will try +to unlink any devices linked to that group that identify themselves with a set +button press. + +=cut + +sub initiate_unlinking_as_controller +{ + my ($self, $group) = @_; + + $group = 'FF' unless $group; + # set up the PLM as the responder + my $cmd = 'FF'; # controller code + $cmd .= $group; # WARN - must be 2 digits and in hex!! + my $message = new Insteon::InsteonMessage('all_link_start', $self); + $message->interface_data($cmd); + $self->queue_message($message); +} + +=item C + +Cancels any pending linking session that has not completed. + +=cut + +sub cancel_linking +{ + my ($self) = @_; + $self->queue_message(new Insteon::InsteonMessage('all_link_cancel', $self)); +} + +=item C<_aldb()> + +Returns the PLM's aldb object. + +=cut + +sub _aldb +{ + my ($self) = @_; + return $$self{aldb}; +} + +=item C<_send_cmd()> + +Causes a message to be sent to the serial port. + +=cut + +sub _send_cmd { + my ($self, $message, $cmd_timeout) = @_; + my $instance = $$self{port_name}; + if (!(ref $main::Serial_Ports{$instance}{object})) { + print "WARN: Insteon_PLM serial port not initialized!\n"; + return; + } + unshift(@{$$self{command_history}},$::Time); + $self->transmit_in_progress(1); + + my $command = $message->interface_data; + my $delay = $$self{xmit_delay}; + + # determine the delay from the point that the message was created to + # the point that it is queued + my $incurred_delay_time = $message->seconds_delayed; + + if ($message->isa('Insteon::X10Message')) { # is x10; so, be slow + &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " + . sprintf('%.2f',$incurred_delay_time) . " seconds") if $self->debuglevel(2, 'insteon'); + $command = $prefix{x10_send} . $command; + $delay = $$self{xmit_x10_delay}; + # clear command timeout so that we don't wait for an insteon ack before sending the next command + } else { + my $command_type = $message->command_type; + &main::print_log("[Insteon_PLM] DEBUG2: Sending " . $message->to_string . " incurred delay of " + . sprintf('%.2f',$incurred_delay_time) . " seconds; starting hop-count: " + . ((ref $message->setby && $message->setby->isa('Insteon::BaseObject')) ? $message->setby->default_hop_count : "?")) if $message->setby->debuglevel(2, 'insteon'); + $command = $prefix{$command_type} . $command; + if ($command_type eq 'all_link_send' or $command_type eq 'insteon_send' or $command_type eq 'insteon_ext_send' or $command_type eq 'all_link_direct_cleanup') + { + $self->_set_timeout('command', $cmd_timeout); # a commmand needs to be PLM ack'd w/i 3 seconds or it gets dropped + } + } + my $is_extended = ($message->can('command_type') && $message->command_type eq "insteon_ext_send") ? 1 : 0; + if (length($command) != (Insteon::MessageDecoder::insteon_cmd_len(substr($command,0,4), 0, $is_extended)*2)){ + &::print_log( "[Insteon_PLM]: ERROR!! Command sent to PLM " . lc($command) + . " is of an incorrect length. Message not sent."); + $self->clear_active_message(); + } + else + { + my $debug_obj = $self; + $debug_obj = $message->setby if ($message->can('setby') && ref $message->setby); + &::print_log( "[Insteon_PLM] DEBUG3: Sending PLM raw data: ".lc($command)) if $debug_obj->debuglevel(3, 'insteon'); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($command)) if $debug_obj->debuglevel(4, 'insteon'); + my $data = pack("H*",$command); + $main::Serial_Ports{$instance}{object}->write($data) if $main::Serial_Ports{$instance}; + + + if ($delay) { + $self->_set_timeout('xmit',$delay * 1000); + } + $$self{'last_change'} = $main::Time; + } +} + +=item C<_parse_data()> + +A complex routine that parses data comming in from the serial port. In many cases +multiple messages or fragments of messages may arrive at once. This routine sorts +through the string of hexadecimal characters and determines what type of message +has arrived and its full content. Based on the type of message, it is then +passed off to lower level message handling routines. + +=cut + +sub _parse_data { + my ($self, $data) = @_; + my ($name, $val); + + # it is possible that a fragment exists from a previous attempt; so, if it exists, prepend it + if ($$self{_data_fragment}) + { + &::print_log("[Insteon_PLM] DEBUG3: Prepending prior data fragment: $$self{_data_fragment}") if $self->debuglevel(3, 'insteon'); + # maintain a copy of the parsed data fragment + $$self{_prior_data_fragment} = $$self{_data_fragment}; + # append if not a repeat + $data = $$self{_data_fragment} . $data unless $$self{_data_fragment} eq $data; + # and, clear it out + $$self{_data_fragment} = ''; + } + else + { + # clear the memory of any prior data fragment + $$self{_prior_data_fragment} = ''; + } + + &::print_log( "[Insteon_PLM] DEBUG3: Received PLM raw data: $data") if $self->debuglevel(3, 'insteon'); + + # begin by pulling out any PLM ack/nacks + my $prev_cmd = ''; + my $pending_message = $self->active_message; + if ($pending_message) { + $prev_cmd = lc $pending_message->interface_data; + if ($pending_message->isa('Insteon::X10Message')) + { + $prev_cmd = $prefix{x10_send} . $prev_cmd; + } else { + my $command_type = $pending_message->command_type; + $prev_cmd = $prefix{$command_type} . $prev_cmd; + } + } + + my $residue_data = ''; + my $process_next_command = 1; + my $nack_count = 0; + my $entered_ack_loop; + my $previous_parsed_data; + if (defined $prev_cmd and $prev_cmd ne '') + { + my $ackcmd = $prev_cmd . '06'; + my $nackcmd = $prev_cmd . '15'; + my $badcmd = $prev_cmd . '0f'; + $previous_parsed_data = ''; + foreach my $parsed_data (split(/($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($badcmd)/,$data)) + { + #ignore blanks.. the split does odd things + next if $parsed_data eq ''; + next if $previous_parsed_data eq $parsed_data; # guard against repeats + $previous_parsed_data = $parsed_data; # and, now reinitialize + $entered_ack_loop = 1; + if ($parsed_data =~ /^($ackcmd)|($nackcmd)|($prefix{plm_info}\w{12}06)|($prefix{plm_info}\w{12}15)|($prefix{all_link_first_rec}15)|($prefix{all_link_next_rec}15)|($badcmd)$/) + { + my $debug_obj = $self; + $debug_obj = $self->active_message->setby if ($self->active_message->can('setby') && ref $self->active_message->setby); + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $debug_obj->debuglevel(4, 'insteon'); + my $ret_code = substr($parsed_data,length($parsed_data)-2,2); + my $record_type = substr($parsed_data,0,4); + my $message_data = substr($parsed_data,4,length($parsed_data)-4); + if ($ret_code eq '06') + { + if ($record_type eq $prefix{plm_info}) + { + $self->device_id(substr($message_data,0,6)); + $self->firmware(substr($message_data,10,2)); + $self->on_interface_info_received(); + } + elsif ($record_type eq $prefix{all_link_first_rec} + or $record_type eq $prefix{all_link_next_rec}) + { + $$self{_next_link_ok} = 1; + } + elsif ($record_type eq $prefix{all_link_start}) + { + if ($self->active_message->success_callback){ + package main; + eval ($self->active_message->success_callback); + &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->active_message->setby->debuglevel(1, 'insteon'); + package Insteon_PLM; + } + # clear the active message because we're done + $self->clear_active_message(); + } + else + { + my $debug_obj = $self; + $debug_obj = $self->active_message->setby if ($self->active_message->can('setby') && ref $self->active_message->setby); + &::print_log("[Insteon_PLM] DEBUG3: Received PLM acknowledge: " + . $pending_message->to_string) if $debug_obj->debuglevel(3, 'insteon'); + } + + # X10 messages don't ACK back on the powerline, so clear them if the PLM acknowledges + # AND if the current, pending message is the X10 message + if (($parsed_data =~ /$prefix{x10_send}\w{4}06/) && ($pending_message->isa('Insteon::X10Message'))) + { + $self->clear_active_message(); + } + + if ($record_type eq $prefix{all_link_manage_rec}) + { + # clear the active message because we're done + $self->clear_active_message(); + + my $callback; + if ($self->_aldb->{_success_callback}){ + $callback = $self->_aldb->{_success_callback}; + $self->_aldb->{_success_callback} = undef; + } elsif ($$self{_mem_callback}) + { + $callback = $pending_message->callback(); #$$self{_mem_callback}; + $$self{_mem_callback} = undef; + } + if ($callback){ + package main; + eval ($callback); + &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->active_message->setby->debuglevel(1, 'insteon'); + package Insteon_PLM; + } + } + } + elsif ($ret_code eq '15' or $ret_code eq '0f') + { #NAK or "bad" command received + $self->clear_active_message(); # regardless, we're not retrying as we'll just get the same + + if ($record_type eq $prefix{all_link_first_rec} + or $record_type eq $prefix{all_link_next_rec}) + { + # both of these conditions are ok as it just means + # we've reached the end of the memory + $$self{_next_link_ok} = 0; + $$self{_mem_activity} = undef; + if ($record_type eq $prefix{all_link_first_rec}) + { + $self->_aldb->health("empty"); + } + else + { + $self->_aldb->health("good"); + } + $self->_aldb->scandatetime(&main::get_tickcount); + &::print_log("[Insteon_PLM] " . $self->get_object_name + . " completed link memory scan: status: " . $self->_aldb->health()) + if $self->debuglevel(1, 'insteon'); + if ($$self{_mem_callback}) + { + my $callback = $$self{_mem_callback}; + $$self{_mem_callback} = undef; + package main; + eval ($callback); + &::print_log("[Insteon_PLM] WARN1: Error encountered during nack callback: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon_PLM; + } + } + elsif ($record_type eq $prefix{all_link_send}) + { + &::print_log("[Insteon_PLM] WARN: PLM memory does not contain link for: " + . $pending_message->to_string . $@) + } + elsif ($record_type eq $prefix{all_link_start}) + { + &::print_log("[Insteon_PLM] WARN: PLM unable to complete requested operation: " + . $pending_message->to_string . $@); + } + elsif ($record_type eq $prefix{all_link_manage_rec}) + { + # parse out the data + my $failed_cmd_code = substr($pending_message->interface_data(),0,2); + my $failed_cmd = 'unknown'; + if ($failed_cmd_code eq '40') + { + $failed_cmd = 'update/add controller record'; + } + elsif ($failed_cmd_code eq '41') + { + $failed_cmd = 'update/add responder record'; + } + elsif ($failed_cmd_code eq '80') + { + $failed_cmd = 'delete record'; + } + my $failed_group = substr($pending_message->interface_data(),4,2); + my $failed_deviceid = substr($pending_message->interface_data(),6,6); + &::print_log("[Insteon_PLM] WARN: PLM unable to complete requested " + . "PLM link table update ($failed_cmd) for " + . "group: $failed_group and deviceid: $failed_deviceid" ); + my $callback; + if ($self->_aldb->{_success_callback}){ + $callback = $self->_aldb->{_success_callback}; + $self->_aldb->{_success_callback} = undef; + } elsif ($$self{_mem_callback}) + { + $callback = $pending_message->callback(); #$$self{_mem_callback}; + $$self{_mem_callback} = undef; + } + if ($callback) + { + package main; + eval ($callback); + &::print_log("[Insteon_PLM] WARN1: Error encountered during ack callback: " . $@) + if $@ and $self->debuglevel(1, 'insteon'); + package Insteon_PLM; + } + # clear the active message because we're done + # $self->clear_active_message(); + } + else + { + &::print_log("[Insteon_PLM] WARN: received NACK from PLM for " + . $pending_message->to_string()); + } + } + else + { + # We have a problem (Usually we stepped on another X10 command) + &::print_log("[Insteon_PLM] ERROR: encountered $parsed_data. " + . $pending_message->to_string()); + $self->active_message->no_hop_increase(1); + $self->retry_active_message(); + #move it off the top of the stack and re-transmit later! + #TODO: We should keep track of an errored command and kill it if it fails twice. prevent an infinite loop here + } + } + else # no match occurred--which is the "leftovers" + { + # is $parsed_data an accidental anomoly? (there are other cases; but, this is a good start) + if ($parsed_data =~ /^($prefix{insteon_send}\w{12}06)|($prefix{insteon_send}\w{12}15)$/) + { + # first, parse the content to confirm that it could be a legitimate ACK + my $unknown_deviceid = substr($parsed_data,4,6); + my $unknown_msg_flags = substr($parsed_data,10,2); + my $unknown_command = substr($parsed_data,12,2); + my $unknown_data = substr($parsed_data,14,2); + my $unknown_obj = &Insteon::get_object($unknown_deviceid, '01'); + if ($unknown_obj) + { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $unknown_obj->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] WARN: encountered '$parsed_data' " + . "from " . $unknown_obj->get_object_name() + . " with command: $unknown_command, but expected '$ackcmd'."); + $residue_data .= $parsed_data; + } + else + { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] ERROR: encountered '$parsed_data' " + . "that does not match any known device ID (expected '$ackcmd')." + . " Discarding received data."); + #$residue_data .= $parsed_data; + } + $self->active_message->no_hop_increase(1); + } + else + { + $residue_data .= $parsed_data; + } + } + } #foreach - split across the incoming data + + $residue_data = $data unless $entered_ack_loop or $residue_data; + } + else + { + $residue_data = $data unless $residue_data; + } + + my $entered_rcv_loop = 0; + + $previous_parsed_data = ''; + + foreach my $parsed_data (split(/($prefix{x10_received}\w{4})|($prefix{insteon_received}\w{18})|($prefix{insteon_ext_received}\w{46})|($prefix{all_link_complete}\w{16})|($prefix{all_link_clean_failed}\w{8})|($prefix{all_link_record}\w{16})|($prefix{all_link_clean_status}\w{2})|($prefix{plm_button_event}\w{2})|($prefix{plm_user_reset})/,$residue_data)) + { + #ignore blanks.. the split does odd things + next if $parsed_data eq ''; + + if ($previous_parsed_data eq $parsed_data){ + # guard against repeats + ::print_log("[Insteon_PLM] DEBUG3: Dropped duplicate message: $parsed_data") if $self->debuglevel(3, 'insteon'); + next; + } + $previous_parsed_data = $parsed_data; # and, now reinitialize + + $entered_rcv_loop = 1; + + my $parsed_prefix = substr($parsed_data,0,4); + my $message_length = length($parsed_data); + + my $message_data = substr($parsed_data,4,length($parsed_data)-4); + + if ($parsed_prefix eq $prefix{insteon_received} and ($message_length == 22)) + { #Insteon Standard Received + my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); + if (ref $find_obj) { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4, 'insteon'); + } + else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + } + $self->on_standard_insteon_received($message_data); + } + elsif ($parsed_prefix eq $prefix{insteon_ext_received} and ($message_length == 50)) + { #Insteon Extended Received + my $find_obj = Insteon::get_object(substr($parsed_data,4,6), '01'); + if (ref $find_obj) { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $find_obj->debuglevel(4, 'insteon'); + } + else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + } + $self->on_extended_insteon_received($message_data); + } + elsif($parsed_prefix eq $prefix{x10_received} and ($message_length == 8)) + { #X10 Received + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + my $x10_message = new Insteon::X10Message($parsed_data); + my $x10_data = $x10_message->get_formatted_data(); + &::print_log("[Insteon_PLM] DEBUG3: received x10 data: $x10_data") if $self->debuglevel(3, 'insteon'); + &::process_serial_data($x10_data,undef,$self); + } + elsif ($parsed_prefix eq $prefix{all_link_complete} and ($message_length == 20)) + { #ALL-Linking Completed + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + my $link_address = substr($message_data,4,6); + &::print_log("[Insteon_PLM] DEBUG2: ALL-Linking Completed with $link_address ($message_data)") if $self->debuglevel(2, 'insteon'); + if ($self->active_message->success_callback){ + main::print_log("[Insteon::Insteon_PLM] DEBUG4: Now calling message success callback: " + . $self->active_message->success_callback) if $self->debuglevel(4, 'insteon'); + package main; + eval $self->active_message->success_callback; + ::print_log("[Insteon::Insteon_PLM] problem w/ success callback: $@") if $@; + package Insteon::BaseObject; + } + #Clear awaiting_ack flag + $self->active_message->setby->_process_command_stack(0); + $self->clear_active_message(); + } + elsif ($parsed_prefix eq $prefix{all_link_clean_failed} and ($message_length == 12)) + { #ALL-Link Cleanup Failure Report + if ($self->active_message){ + # extract out the pertinent parts of the message for display purposes + # bytes 0-1 - group; 2-7 device address + my $failure_group = substr($message_data,0,2); + my $failure_device = substr($message_data,2,6); + my $failed_object = &Insteon::get_object($failure_device,'01'); + if (ref $failed_object){ + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $failed_object->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure from " . $failed_object->get_object_name + . " for all link group: $failure_group. Trying a direct cleanup.") if $failed_object->debuglevel(2, 'insteon'); + my $message = new Insteon::InsteonMessage('all_link_direct_cleanup', $failed_object, + $self->active_message->command, $failure_group); + push(@{$$failed_object{command_stack}}, $message); + $failed_object->_process_command_stack(); + } else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] Received all-link cleanup failure from an unkown device id: " + . "$failure_device and for all link group: $failure_group. You may " + . "want to run delete orphans to remove this link from your PLM"); + } + } else { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] DEBUG2: Received all-link cleanup failure." + . " But there is no pending message.") if $self->debuglevel(2, 'insteon'); + } + + } + elsif ($parsed_prefix eq $prefix{all_link_record} and ($message_length == 20)) + { #ALL-Link Record Response + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + &::print_log("[Insteon_PLM] DEBUG2: ALL-Link Record Response:$message_data") if $self->debuglevel(2, 'insteon'); + $self->_aldb->parse_alllink($message_data); + # before doing the next, make sure that the pending command + # (if it sitll exists) is pulled from the queue + $self->clear_active_message(); + + $self->_aldb->get_next_alllink(); + } + elsif ($parsed_prefix eq $prefix{plm_user_reset} and ($message_length == 4)) + { + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + main::print_log("[Insteon_PLM] Detected PLM user reset to factory defaults"); + } + elsif ($parsed_prefix eq $prefix{all_link_clean_status} and ($message_length == 6)) + { #ALL-Link Cleanup Status Report + &::print_log( "[Insteon_PLM] DEBUG4:\n".Insteon::MessageDecoder::plm_decode($parsed_data)) if $self->debuglevel(4, 'insteon'); + my $cleanup_ack = substr($message_data,0,2); + if ($cleanup_ack eq '15') + { + &::print_log("[Insteon_PLM] WARN1: All-link cleanup failure for scene: " + . $self->active_message->setby->get_object_name . ". Retrying in 1 second.") + if $self->active_message->setby->debuglevel(1, 'insteon'); + $self->retry_active_message(); + # except that we should cause a bit of a delay to let things settle out + $self->_set_timeout('xmit', 1000); + $process_next_command = 0; + } + else + { + my $message_to_string = ($self->active_message) ? $self->active_message->to_string() : ""; + &::print_log("[Insteon_PLM] Received all-link cleanup success: $message_to_string") + if $self->active_message->setby->debuglevel(1, 'insteon'); + if (ref $self->active_message && ref $self->active_message->setby){ + my $object = $self->active_message->setby; + $object->is_acknowledged(1); + $object->_process_command_stack(); + } + $self->clear_active_message(); + } + } + elsif (substr($parsed_data,0,2) eq '15') + { # Indicates that the PLM can't receive more commands at the moment + # so, slow things down + if (!($nack_count)) + { + if ($self->active_message){ + my $nack_delay = ($::config_parms{Insteon_PLM_disable_throttling}) ? 0.3 : 1.0; + &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy. Resending command" + . " after delaying for $nack_delay second") if $self->debuglevel(3, 'insteon'); + $self->_set_timeout('xmit',$nack_delay * 1000); + $self->active_message->no_hop_increase(1); + $self->retry_active_message(); + $process_next_command = 0; + } else { + &::print_log("[Insteon_PLM] DEBUG3: Interface extremely busy." + . " No message to resend.") if $self->debuglevel(3, 'insteon'); + } + $nack_count++; + } + #Remove the leading NACK bytes and place whatever remains into fragment for next read + $parsed_data =~ s/^(15)*//; + if ($parsed_data ne ''){ + $$self{_data_fragment} .= $parsed_data; + ::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " + . $parsed_data) if( $self->debuglevel(3, 'insteon')); + } + } + else + { + # it's probably a fragment; so, handle it + # it it's the same as last time, then drop it as we can't recover + unless (($parsed_data eq $$self{_prior_data_fragment}) or ($parsed_data eq $$self{_data_fragment})) { + $$self{_data_fragment} .= $parsed_data; + main::print_log("[Insteon_PLM] DEBUG3: Saving parsed data fragment: " + . $parsed_data) if( $self->debuglevel(3, 'insteon')); + } + } + } + + unless( $entered_rcv_loop or $$self{_data_fragment}) { + $$self{_data_fragment} = $residue_data; + main::print_log("[Insteon_PLM] DEBUG3: Saving residue data fragment: " + . $residue_data) if( $residue_data and $self->debuglevel(3, 'insteon')); + } + + if ($process_next_command) { + $self->process_queue(); + } + + return; +} + +=item C + +Dummy sub required to support the X10 integrtion, does nothing. + +=cut + +sub add_id_state { + # do nothing +} + +=item C + +Stores and returns the firmware version of the PLM. + +=cut + +sub firmware { + my ($self, $p_firmware) = @_; + $$self{firmware} = $p_firmware if defined $p_firmware; + return $$self{firmware}; +} + +=back + +=head2 INI PARAMETERS + +=over + +=item Insteon_PLM_serial_port + +Identifies the port on which the PLM is attached. Example: + + Insteon_PLM_serial_port=/dev/ttyS4 + +=item Insteon_PLM_xmit_delay + +Sets the minimum amount of seconds that must elapse between sending Insteon messages +to the PLM. Defaults to 0.25. + +=item Insteon_PLM_xmit_x10_delay + +Sets the minimum amount of seconds that must elapse between sending X10 messages +to the PLM. Defaults to 0.50. + +=item Insteon_PLM_disable_throttling + +Periodically, the PLM will report that it is too busy to accept a message from +MisterHouse. When this happens, MisterHouse will wait 1 second before trying +to send a message to the PLM. If this is set to 1, downgrades the delay to only +.3 seconds. Most of the issues which caused the PLM to overload have been handled +it is unlikely that you would need to set this. + +=back + +=head2 NOTES + +Special Thanks to: + +Brian Warren for significant testing and patches + +Bruce Winter - MH + +=head2 AUTHOR + +Jason Sharpee / jason@sharpee.com, Gregg Liming / gregg@limings.net, Kevin Robert Keegan, Michael Stovenour + +=head2 SEE ALSO + +For more information regarding the technical details of the PLM: +L + +=head2 LICENSE + +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. + +=cut + + +1; diff --git a/lib/Weather_davisvantageproii.pm b/lib/Weather_davisvantageproii.pm index 97e627492..5cc962589 100644 --- a/lib/Weather_davisvantageproii.pm +++ b/lib/Weather_davisvantageproii.pm @@ -1,580 +1,580 @@ -package Weather_davisvantageproii; - -# $Date$ -# $Revision$ - -use strict; -use Weather_Common; -#!#!#!# eval 'use Digest::mhCRC qw(crc16);'; -#!#!#!# if ($@) { -#!#!#!# die("Weather_davisvantageproii: Can't find the Digest::mhCRC package (mhCRC.pm). Please ensure that it is installed.\n$@"); -#!#!#!#} - -=begin comment -============================================================================= -Davis VantagePro 2 Weather Station Interface for MH -Version 0.1 - Alpha Test -12/4/2010 - -By: Brian Klier - -Modified from Scott Huskey's Davis Weather Monitor II Code. - -Note: You must enable this module by setting the following parameters in mh{.private}.ini. - Obviously you must point the port to the actual port to which the station is connected. - serial_davisvantageproii_port=COM3 - serial_davisvantageproii_baudrate=19200 - serial_davisvantageproii_datatype=raw - serial_davisvantageproii_module=Weather_davisvantageproii - -============================================================================= -=cut -our $wakeupCommand = chr(10); -our $loopCommand = join "", "LOOP", chr(255), chr(255), chr(49), chr(10); -our $DavisVP_port; -#our $lastRainReading = undef; -#our $lastRainReadingTime = undef; - -our ($barometric_trend, $next_rec, $barometric_press, $air_temp_inside, $humidity_inside, - $air_temp, $wind_speed, $wind_speed_10min_ave, $wind_direction, $relative_humidity, - $rain_rate, $uv, $solar, $rain_storm, $storm_date, $rain_day, $rain_month, $rain_year, - $day_ET, $month_ET, $year_ET, $alarms_inside, $alarms_rain, $alarms_outside, - $batt_xmit, $batt_cons, $forecast_icon, $forecast_rule, $sunrise, $sunset, $dew_point_inside, - $dew_point_outside, $crc, $crc_calc, $wptr, $data); - -sub startup{ - my ($instance)=@_; - $DavisVP_port = new Serial_Item(undef, undef, 'serial_davisvantageproii'); - &requestData; - &::MainLoop_pre_add_hook(\&Weather_davisvantageproii::update,1); - &::trigger_set('$New_Minute','&Weather_davisvantageproii::requestData','NoExpire','davisvantageproii data request') - unless &::trigger_get('davisvantageproii data request'); -} - -sub wake_up{ - my $self = shift @_; - - foreach (1..3) - { - $DavisVP_port->set($wakeupCommand); - my $str = said $DavisVP_port; - ###my ($cnt_in, $str) = $self->read(2); - - if ($str eq "\n\r" ) - { - &::print_log ("davisvantageproii: success on wakeup") if $::Debug{weather}; - return 1; - } - - &::print_log ("davisvantageproii: no wakeup response") if $::Debug{weather}; - - sleep 1; # As per page 5 of VantagePro Doc - } - - &::print_log ("davisvantageproii: could not wake up unit") if $::Debug{weather}; - return 1; # fail -} - -# called by trigger every minute -sub requestData { - &::print_log ("davisvantageproii: requesting new data from station") if $::Debug{weather}; - #!#!#!#! &wake_up; - $DavisVP_port->set($wakeupCommand); - $DavisVP_port->set($loopCommand); -} - -# called once per loop -sub update{ - return unless my $data = said $DavisVP_port; - my $remainder=&process($data, \%main::Weather); - $DavisVP_port->set_data($remainder) if $remainder ne ''; - &weather_updated; -} - -# Parse DavisVP datastream into array pointed at with $wptr so Mr House -# can use the information -# - -sub process{ - - my ($data, $wptr) = @_; - my @data = unpack('C*',$data); - - if ($::Debug{weather}) { - my $debugInfo='davisvantageproii: Read from Davis VantagePro II '; - for (@data) { - $debugInfo .= sprintf ("0x%x ",$_); - } - &::print_log($debugInfo); - } - - my $data = shift @_; - my $loo = substr $data,0,3; - my $ack = ord substr($data,0,1); - - if ($loo eq 'LOO') { - &::print_log ("davisvantageproii: found proper LOO header") if $::Debug{weather}; - } - else { - &::print_log ("davisvantageproii: proper LOO header not found") if $::Debug{weather}; - return ''; - } - - $barometric_trend = unpack("C", substr $data,3,1); - $next_rec = unpack("s", substr $data,5,2); - $barometric_press = unpack("s", substr $data,7,2) / 1000; - $air_temp_inside = unpack("s", substr $data,9,2) / 10; - if ($air_temp_inside eq '3276.7') {$air_temp_inside = undef}; - $humidity_inside = unpack("C", substr $data,11,1); - if ($humidity_inside eq '255') {$humidity_inside = undef}; - $air_temp = unpack("s", substr $data,12,2) / 10; - if ($air_temp eq '3276.7') {$air_temp = undef}; - $wind_speed = unpack("C", substr $data,14,1); - if ($wind_speed eq '255') {$wind_speed = undef}; - $wind_speed_10min_ave = unpack("C", substr $data,15,1); - if ($wind_speed_10min_ave eq '255') {$wind_speed_10min_ave = undef}; - $wind_direction = unpack("s", substr $data,16,2); - if ($wind_direction eq '32767') {$wind_direction = undef}; - - # Skip other temps for now... - - $relative_humidity = unpack("C", substr $data,33,1); - if ($relative_humidity eq '255') {$relative_humidity = undef}; - - # Skip other humidities for now... - - $rain_rate = unpack("s", substr $data,41,2) / 100; # Inches per hr - if ($rain_rate < 0) {$rain_rate = undef}; - $uv = unpack("C", substr $data,43,1); - if ($uv eq '255') {$uv = undef}; - $solar = unpack("s", substr $data,44,2); # watt/m**2 - if ($solar eq '32767') {$solar = undef}; - $rain_storm = unpack("s", substr $data,46,2) / 100; # Inches per storm - - $storm_date = unpack("s", substr $data,48,2); # Need to parse data (not sure what this is) - $rain_day = unpack("s", substr $data,50,2)/100; - $rain_month = unpack("s", substr $data,52,2)/100; - $rain_year = unpack("s", substr $data,54,2)/100; - - $day_ET = unpack("s", substr $data,56,2)/1000; - $month_ET = unpack("s", substr $data,58,2)/100; - $year_ET = unpack("s", substr $data,60,2)/100; - # Skip Soil/Leaf Wetness - - $alarms_inside = unpack("b8", substr $data,70,1); - $alarms_rain = unpack("b8", substr $data,70,1); - $alarms_outside = unpack("b8", substr $data,70,1); - # Skip extra alarms - -# $batt_xmit = unpack("C", substr $data,86,1) * 0.005859375; - $batt_xmit = unpack("C", substr $data,86,1); - $batt_cons = unpack("s", substr $data,87,2) * 0.005859375; - - $forecast_icon = unpack("C", substr $data,89,1); - $forecast_rule = unpack("C", substr $data,90,1); - - $sunrise = sprintf( "%04d", unpack("S", substr $data,91,2) ); - $sunrise =~ s/(\d{2})(\d{2})/$1:$2/; - - $sunset = sprintf( "%04d", unpack("S", substr $data,93,2) ); - $sunset =~ s/(\d{2})(\d{2})/$1:$2/; - - my $nl = ord substr $data,95,1; - my $cr = ord substr $data,96,1; - - $crc = unpack "%n", substr($data,97,2); - $crc_calc = CRC_CCITT(substr($data,0,98)); -# $crc_calc = CRC_CCITT(substr($data,0,96)); - - # $crc_calc = Digest::mhCRC::crc16(substr($data,0,96)); # MH version - # $crc_calc = CRC_CCITT($data); # 3rd party version - - ### MISTERHOUSE STUFF VVVVV - -#!#!#!# if (Digest::mhCRC::crc16(substr($data,0,96)) != $crc) { -#!#!#!# &::print_log ("davisvantageproii: wrong crc16, looking again for header") if $::Debug{weather}; -#!#!#!# next; -#!#!#!# } - -# I added this next part to throw out packets that don't have a CRC value at all - if ($crc eq '0') { - &::print_log ("davisvantageproii: wrong crc16, looking again for header") if $::Debug{weather}; - next; - } - - # remove the 99 bytes that we just processed, we'll use the remainder as our return value - $data=substr($data,99); -#!#!#!# last; -## ? } - - &::print_log ("davisvantageproii: found a header with the right checksum") if $::Debug{weather}; - - # calculate sea level pressure - my $barometer_sea=convert_local_barom_to_sea_in($barometric_press); - -# 3rd party simple dew point calculation -# $dew_point_inside = $air_temp - ( (100 - $relative_humidity)/5 ); -# $dew_point_outside = $air_temp_inside - ( (100 - $humidity_inside)/5 ); - - # these dewpoints will be in Celsius - if ($humidity_inside ne undef) {$dew_point_inside=&::convert_humidity_to_dewpoint($humidity_inside,&::convert_f2c($air_temp_inside))}; - if ($relative_humidity ne undef) {$dew_point_outside=&::convert_humidity_to_dewpoint($relative_humidity,&::convert_f2c($air_temp))}; - -# $rain_rate=undef; -# if (defined ($lastRainReadingTime)) { -# $rain_rate=($total_rain-$lastRainReading); # delta in inches -# my $time_delta=(time - $lastRainReadingTime); -# if ($time_delta != 0) { -# $rain_rate/=$time_delta; # rate in inches per second -# $rain_rate *= 3600; # rate in inches per hour -# if ($rain_rate < 0) { # if total rain was reset to zero, this could happen -# $rain_rate=0; -# } -# } -# } -# $lastRainReadingTime=time; -# $lastRainReading=$total_rain; - - if ($main::config_parms{weather_uom_temp} eq 'C') { - grep {$_=&::convert_f2c($_);} ( - $air_temp_inside, - $air_temp - ); - # remember, dewpoints are in Celsius by default - } elsif ($main::config_parms{weather_uom_temp} eq 'F') { - $dew_point_inside=&::convert_c2f($dew_point_inside); - if ($relative_humidity ne undef) { - $dew_point_outside=&::convert_c2f($dew_point_outside); - } -# grep {$_=&::convert_c2f($_);} ( -# $dew_point_inside, -# $dew_point_outside -# ); - } - if ($main::config_parms{weather_uom_baro} eq 'mb') { - grep {$_=&::convert_in2mb($_);} ( - $barometric_press, - $barometer_sea - ); - } - if ($main::config_parms{weather_uom_rain} eq 'mm') { - grep {$_=&::convert_in2mm($_);} ( - $rain_storm - ); - } - if ($main::config_parms{weather_uom_rain} eq 'mm/hr') { - grep {$_=&::convert_in2mm($_);} ( - $rain_rate - ); - $rain_rate=sprintf('%.0f',$rain_rate); # round to nearest mm/hr - } else { - $rain_rate=sprintf('%.2f',$rain_rate); # round to nearest 0.01 in/hr - } - if ($main::config_parms{weather_uom_wind} eq 'kph') { - grep {$_=&::convert_mile2km($_);} ( - $wind_speed - ); - } - if ($main::config_parms{weather_uom_wind} eq 'm/s') { - grep {$_=&::convert_mph2mps($_);} ( - $wind_speed - ); - } - - $$wptr{TempIndoor}=$air_temp_inside; - $$wptr{TempOutdoor}=$air_temp; - $$wptr{DewIndoor}=$dew_point_inside; - $$wptr{DewOutdoor}=$dew_point_outside; - $$wptr{WindAvgSpeed}=$wind_speed; - $$wptr{WindGustSpeed}=$wind_speed; - $$wptr{WindAvgDir}=$wind_direction; - $$wptr{WindGustDir}=$wind_direction; - $$wptr{Barom}=$barometric_press; - $$wptr{BaromSea}=$barometer_sea; - $$wptr{HumidIndoor}=$humidity_inside; - $$wptr{HumidOutdoor}=$relative_humidity; - $$wptr{RainTotal}=$rain_storm; - $$wptr{RainRate}=$rain_rate; - - if ($::Debug{weather}) { - foreach my $key qw( - TempIndoor - TempOutdoor - DewIndoor - DewOutdoor - WindAvgSpeed - WindAvgDir - Barom - BaromSea - HumidIndoor - HumidOutdoor - RainTotal - RainRate - ) { - &::print_log ("davisvantageproii: $key ".$$wptr{$key}); - } - } - -if ($::Debug{weather}) { - &::print_log ("uv: $uv"); - &::print_log ("solar: $solar"); - &::print_log ("batt_xmit: $batt_xmit"); - &::print_log ("batt_cons: $batt_cons"); - &::print_log ("sunrise: $sunrise"); - &::print_log ("sunset: $sunset"); - &::print_log ("crc: $crc"); - &::print_log ("crc_calc: $crc_calc"); - &::print_log ("barometric_trend: $barometric_trend"); - &::print_log ("alarms_inside: $alarms_inside"); - &::print_log ("alarms_rain: $alarms_rain"); - &::print_log ("alarms_outside: $alarms_outside"); - &::print_log ("forecast_icon: $forecast_icon"); - &::print_log ("forecast_rule: $forecast_rule"); -} - - &::weather_updated; - return $data; -} - -sub CRC_CCITT -{ - # Expects packed data... - my $data_str = shift @_; - - my @crc_table = crc_table(); - - my $tempcrc = 0; - my @lst = split //, $data_str; - foreach my $data (@lst) - { - my $data = unpack("c",$data); - - my $crc_prev = $tempcrc; - my $index = $tempcrc >> 8 ^ $data; - my $lhs = $crc_table[$index]; - my $rhs = ($tempcrc << 8) & 0xFFFF; - $tempcrc = $lhs ^ $rhs; - - #$data = unpack("H*",$data); - #printf("%X\t %s\t %X\t %X\t %X\t : %x \n", $crc_prev, $data, $index, $lhs, $rhs, $crc); - } - - return $tempcrc; -} - -# - - - - - - - - - - - - - - - - - - - -sub crc_table -{ - -my @crc_table = ( -0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, -0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, -0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, -0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, -0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, -0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, -0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4, -0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, -0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823, -0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, -0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12, -0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, -0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41, -0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, -0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70, -0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, -0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, -0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, -0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, -0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, -0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, -0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405, -0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, -0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, -0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, -0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3, -0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, -0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92, -0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, -0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1, -0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, -0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0); -} - -# all modules must return 1. don't remove the following line -1; - -=begin comment -============================================================================= -Contents of the LOOP packet. -Field Offset Size Explanation -"L" 0 1 -"O" 1 1 -"O" 2 1 -Spells out "LOO" for Rev B packets and "LOOP" for Rev A -packets. Identifies a LOOP packet -"P" (Rev A) -Bar Trend (Rev B) -3 1 Signed byte that indicates the current 3-hour barometer trend. It -is one of these values: --60 = Falling Rapidly = 196 (as an unsigned byte) --20 = Falling Slowly = 236 (as an unsigned byte) -0 = Steady -20 = Rising Slowly -60 = Rising Rapidly -80 = ASCII "P" = Rev A firmware, no trend info is available -Any other value means that the Vantage does not have the 3 -hours of bar data needed to determine the bar trend. -Packet Type 4 1 Has the value zero. In the future we may define new LOOP -packet formats and assign a different value to this field. -Page 21 of 52 -Field Offset Size Explanation -Next Record 5 2 Location in the archive memory where the next data packet will -be written. This can be monitored to detect when a new record is -created. -Barometer 7 2 Current Barometer. Units are (in Hg / 1000). The barometric -value should be between 20 inches and 32.5 inches in Vantage -Pro and between 20 inches and 32.5 inches in both Vantatge Pro -Vantage Pro2. Values outside these ranges will not be logged. -Inside Temperature 9 2 The value is sent as 10th of a degree in F. For example, 795 is -returned for 79.5°F. -Inside Humidity 11 1 This is the relative humidity in %, such as 50 is returned for 50%. -Outside Temperature 12 2 The value is sent as 10th of a degree in F. For example, 795 is -returned for 79.5°F. -Wind Speed 14 1 It is a byte unsigned value in mph. If the wind speed is dashed -because it lost synchronization with the radio or due to some -other reason, the wind speed is forced to be 0. -10 Min Avg Wind Speed 15 1 It is a byte unsigned value in mph. -Wind Direction 16 2 It is a two byte unsigned value from 1 to 360 degrees. (0° is no -wind data, 90° is East, 180° is South, 270° is West and 360° is -north) -Extra Temperatures 18 7 This field supports seven extra temperature stations. -Each byte is one extra temperature value in whole degrees F with -an offset of 90 degrees. For example, a value of 0 = -90°F ; a -value of 100 = 10°F ; and a value of 169 = 79°F. -Soil Temperatures 25 4 This field supports four soil temperature sensors, in the same -format as the Extra Temperature field above -Leaf Temperatures 29 4 This field supports four leaf temperature sensors, in the same -format as the Extra Temperature field above -Outside Humidity 33 1 This is the relative humitiy in %. -Extra Humidties 34 7 Relative humidity in % for extra seven humidity stations. -Rain Rate 41 2 This value is sent as number of rain clicks (0.2mm or 0.01in). -For example, 256 can represent 2.56 inches/hour. -UV 43 1 The unit is in UV index. -Solar Radiation 44 2 The unit is in watt/meter2. -Storm Rain 46 2 The storm is stored as 100th of an inch. -Start Date of current Storm 48 2 Bit 15 to bit 12 is the month, bit 11 to bit 7 is the day and bit 6 to -bit 0 is the year offseted by 2000. -Day Rain 50 2 This value is sent as number of rain clicks. (0.2mm or 0.01in) -Month Rain 52 2 This value is sent as number of rain clicks. (0.2mm or 0.01in) -Year Rain 54 2 This value is sent as number of rain clicks. (0.2mm or 0.01in) -Day ET 56 2 This value is sent as the 1000th of an inch. -Month ET 58 2 This value is sent as the 100th of an inch. -Year ET 60 2 This value is setnt as the 100th of an inch. -Soil Moistures 62 4 The unit is in centibar. It supports four soil sensors. -Leaf Wetnesses 66 4 This is a scale number from 0 to 15 with 0 meaning very dry and -15 meaning very wet. It supports four leaf sensors. -Inside Alarms 70 1 Currently active inside alarms. See the table below -Rain Alarms 71 1 Currently active rain alarms. See the table below -Outside Alarms 72 2 Currently active outside alarms. See the table below -Extra Temp/Hum Alarms 74 8 Currently active extra temp/hum alarms. See the table below -Soil & Leaf Alarms 82 4 Currently active soil/leaf alarms. See the table below -Transmitter Battery Status 86 1 -Console Battery Voltage 87 2 Voltage = ((Data * 300)/512)/100.0 -Forecast Icons 89 1 -Page 22 of 52 -Field Offset Size Explanation -Forecast Rule number 90 1 -Time of Sunrise 91 2 The time is stored as hour * 100 + min. -Time of Sunset 93 2 The time is stored as hour * 100 + min. -"\n" = 0x0A 95 1 -"\r" = 0x0D 96 1 -CRC 97 2 -Total Length 99 -Forecast Icons in LOOP packet -Field Byte Bit # -Forecast Icons 89 Bit maps for forecast icons on the console screen. -Rain 0 -Cloud 1 -Partly Cloudy 2 -Sun 3 -Snow 4 -Forecast Icon Values -Value Decimal Value Hex Segments Shown Forecast -8 0x08 Sun Mostly Clear -6 0x06 Partial Sun + Cloud Partially Cloudy -2 0x02 Cloud Mostly Cloudy -3 0x03 Cloud + Rain Mostly Cloudy, Rain within 12 hours -18 0x12 Cloud + Snow Mostly Cloudy, Snow within 12 hours -19 0x13 Cloud + Rain + Snow Mostly Cloudy, Rain or Snow within 12 hours -7 0x07 Partial Sun + Cloud + -Rain -Partially Cloudy, Rain within 12 hours -22 0x16 Partial Sun + Cloud + -Snow -Partially Cloudy, Snow within 12 hours -23 0x17 Partial Sun + Cloud + -Rain + Snow -Partially Cloudy, Rain or Snow within 12 hours -Currently active alarms in the LOOP packet -This table shows which alarms correspond to each bit in the LOOP alarm fields. Not all bits in -each field are used. The Outside Alarms field has been split into 2 1-byte sections. -Field Byte Bit # -Inside Alarms 70 Currently active inside alarms. -Falling bar trend alarm 0 -Rising bar trend alarm 1 -Low inside temp alarm 2 -High inside temp alarm 3 -Page 23 of 52 -Field Byte Bit # -Low inside hum alarm 4 -High inside hum alarm 5 -Time alarm 6 -Rain Alarms 71 Currently active rain alarms. -High rain rate alarm 0 -15 min rain alarm 1 Flash Flood alarm -24 hour rain alarm 2 -Storm total rain alarm 3 -Daily ET alarm 4 -Outside Alarms 72 Currently active outside alarms. -Low outside temp alarm 0 -High outside temp alarm 1 -Wind speed alarm 2 -10 min avg speed alarm 3 -Low dewpoint alarm 4 -High dewpoint alarm 5 -High heat alarm 6 -Low wind chill alarm 7 -Outside Alarms, byte 2 73 -High THSW alarm 0 -High solar rad alarm 1 -High UV alarm 2 -UV Dose alarm 3 -UV Dose alarm Enabled 4 It is set to 1 when a UV dose alarm threshold has been entered -AND the daily UV dose has been manually cleared. -Outside Humidity Alarms 74 1 Currently active outside humidity alarms. -Low Humidity alarm 2 -High Humidity alarm 3 -Extra Temp/Hum Alarms 75 - 81 7 Each byte contains four alarm bits (0 – 3) for a single extra -Temp/Hum station. Bits (4 – 7) are not used and reserved for -future use. -Use the temperature and humidity sensor numbers, as -described in Section XIII.4 to locate which byte contains the -appropriate alarm bits. In particular, the humidity and -temperature alarms for a single station will be found in -different bytes. -Low temp X alarm 0 -High temp X alarm 1 -Low hum X alarm 2 -High hum X alarm 3 -Soil & Leaf Alarms 82 - 85 4 Currently active soil/leaf alarms. -Low leaf wetness X alarm 0 -High leaf wetness X alarm 1 -Low soil moisture X alarm 2 -High soil moisture X alarm 3 -Low leaf temp X alarm 4 -High leaf temp X alarm 5 -Low soil temp X alarm 6 -High soil temp X alarm 7 -============================================================================= +package Weather_davisvantageproii; + +# $Date$ +# $Revision$ + +use strict; +use Weather_Common; +#!#!#!# eval 'use Digest::mhCRC qw(crc16);'; +#!#!#!# if ($@) { +#!#!#!# die("Weather_davisvantageproii: Can't find the Digest::mhCRC package (mhCRC.pm). Please ensure that it is installed.\n$@"); +#!#!#!#} + +=begin comment +============================================================================= +Davis VantagePro 2 Weather Station Interface for MH +Version 0.1 - Alpha Test +12/4/2010 + +By: Brian Klier + +Modified from Scott Huskey's Davis Weather Monitor II Code. + +Note: You must enable this module by setting the following parameters in mh{.private}.ini. + Obviously you must point the port to the actual port to which the station is connected. + serial_davisvantageproii_port=COM3 + serial_davisvantageproii_baudrate=19200 + serial_davisvantageproii_datatype=raw + serial_davisvantageproii_module=Weather_davisvantageproii + +============================================================================= +=cut +our $wakeupCommand = chr(10); +our $loopCommand = join "", "LOOP", chr(255), chr(255), chr(49), chr(10); +our $DavisVP_port; +#our $lastRainReading = undef; +#our $lastRainReadingTime = undef; + +our ($barometric_trend, $next_rec, $barometric_press, $air_temp_inside, $humidity_inside, + $air_temp, $wind_speed, $wind_speed_10min_ave, $wind_direction, $relative_humidity, + $rain_rate, $uv, $solar, $rain_storm, $storm_date, $rain_day, $rain_month, $rain_year, + $day_ET, $month_ET, $year_ET, $alarms_inside, $alarms_rain, $alarms_outside, + $batt_xmit, $batt_cons, $forecast_icon, $forecast_rule, $sunrise, $sunset, $dew_point_inside, + $dew_point_outside, $crc, $crc_calc, $wptr, $data); + +sub startup{ + my ($instance)=@_; + $DavisVP_port = new Serial_Item(undef, undef, 'serial_davisvantageproii'); + &requestData; + &::MainLoop_pre_add_hook(\&Weather_davisvantageproii::update,1); + &::trigger_set('$New_Minute','&Weather_davisvantageproii::requestData','NoExpire','davisvantageproii data request') + unless &::trigger_get('davisvantageproii data request'); +} + +sub wake_up{ + my $self = shift @_; + + foreach (1..3) + { + $DavisVP_port->set($wakeupCommand); + my $str = said $DavisVP_port; + ###my ($cnt_in, $str) = $self->read(2); + + if ($str eq "\n\r" ) + { + &::print_log ("davisvantageproii: success on wakeup") if $::Debug{weather}; + return 1; + } + + &::print_log ("davisvantageproii: no wakeup response") if $::Debug{weather}; + + sleep 1; # As per page 5 of VantagePro Doc + } + + &::print_log ("davisvantageproii: could not wake up unit") if $::Debug{weather}; + return 1; # fail +} + +# called by trigger every minute +sub requestData { + &::print_log ("davisvantageproii: requesting new data from station") if $::Debug{weather}; + #!#!#!#! &wake_up; + $DavisVP_port->set($wakeupCommand); + $DavisVP_port->set($loopCommand); +} + +# called once per loop +sub update{ + return unless my $data = said $DavisVP_port; + my $remainder=&process($data, \%main::Weather); + $DavisVP_port->set_data($remainder) if $remainder ne ''; + &weather_updated; +} + +# Parse DavisVP datastream into array pointed at with $wptr so Mr House +# can use the information +# + +sub process{ + + my ($data, $wptr) = @_; + my @data = unpack('C*',$data); + + if ($::Debug{weather}) { + my $debugInfo='davisvantageproii: Read from Davis VantagePro II '; + for (@data) { + $debugInfo .= sprintf ("0x%x ",$_); + } + &::print_log($debugInfo); + } + + my $data = shift @_; + my $loo = substr $data,0,3; + my $ack = ord substr($data,0,1); + + if ($loo eq 'LOO') { + &::print_log ("davisvantageproii: found proper LOO header") if $::Debug{weather}; + } + else { + &::print_log ("davisvantageproii: proper LOO header not found") if $::Debug{weather}; + return ''; + } + + $barometric_trend = unpack("C", substr $data,3,1); + $next_rec = unpack("s", substr $data,5,2); + $barometric_press = unpack("s", substr $data,7,2) / 1000; + $air_temp_inside = unpack("s", substr $data,9,2) / 10; + if ($air_temp_inside eq '3276.7') {$air_temp_inside = undef}; + $humidity_inside = unpack("C", substr $data,11,1); + if ($humidity_inside eq '255') {$humidity_inside = undef}; + $air_temp = unpack("s", substr $data,12,2) / 10; + if ($air_temp eq '3276.7') {$air_temp = undef}; + $wind_speed = unpack("C", substr $data,14,1); + if ($wind_speed eq '255') {$wind_speed = undef}; + $wind_speed_10min_ave = unpack("C", substr $data,15,1); + if ($wind_speed_10min_ave eq '255') {$wind_speed_10min_ave = undef}; + $wind_direction = unpack("s", substr $data,16,2); + if ($wind_direction eq '32767') {$wind_direction = undef}; + + # Skip other temps for now... + + $relative_humidity = unpack("C", substr $data,33,1); + if ($relative_humidity eq '255') {$relative_humidity = undef}; + + # Skip other humidities for now... + + $rain_rate = unpack("s", substr $data,41,2) / 100; # Inches per hr + if ($rain_rate < 0) {$rain_rate = undef}; + $uv = unpack("C", substr $data,43,1); + if ($uv eq '255') {$uv = undef}; + $solar = unpack("s", substr $data,44,2); # watt/m**2 + if ($solar eq '32767') {$solar = undef}; + $rain_storm = unpack("s", substr $data,46,2) / 100; # Inches per storm + + $storm_date = unpack("s", substr $data,48,2); # Need to parse data (not sure what this is) + $rain_day = unpack("s", substr $data,50,2)/100; + $rain_month = unpack("s", substr $data,52,2)/100; + $rain_year = unpack("s", substr $data,54,2)/100; + + $day_ET = unpack("s", substr $data,56,2)/1000; + $month_ET = unpack("s", substr $data,58,2)/100; + $year_ET = unpack("s", substr $data,60,2)/100; + # Skip Soil/Leaf Wetness + + $alarms_inside = unpack("b8", substr $data,70,1); + $alarms_rain = unpack("b8", substr $data,70,1); + $alarms_outside = unpack("b8", substr $data,70,1); + # Skip extra alarms + +# $batt_xmit = unpack("C", substr $data,86,1) * 0.005859375; + $batt_xmit = unpack("C", substr $data,86,1); + $batt_cons = unpack("s", substr $data,87,2) * 0.005859375; + + $forecast_icon = unpack("C", substr $data,89,1); + $forecast_rule = unpack("C", substr $data,90,1); + + $sunrise = sprintf( "%04d", unpack("S", substr $data,91,2) ); + $sunrise =~ s/(\d{2})(\d{2})/$1:$2/; + + $sunset = sprintf( "%04d", unpack("S", substr $data,93,2) ); + $sunset =~ s/(\d{2})(\d{2})/$1:$2/; + + my $nl = ord substr $data,95,1; + my $cr = ord substr $data,96,1; + + $crc = unpack "%n", substr($data,97,2); + $crc_calc = CRC_CCITT(substr($data,0,98)); +# $crc_calc = CRC_CCITT(substr($data,0,96)); + + # $crc_calc = Digest::mhCRC::crc16(substr($data,0,96)); # MH version + # $crc_calc = CRC_CCITT($data); # 3rd party version + + ### MISTERHOUSE STUFF VVVVV + +#!#!#!# if (Digest::mhCRC::crc16(substr($data,0,96)) != $crc) { +#!#!#!# &::print_log ("davisvantageproii: wrong crc16, looking again for header") if $::Debug{weather}; +#!#!#!# next; +#!#!#!# } + +# I added this next part to throw out packets that don't have a CRC value at all + if ($crc eq '0') { + &::print_log ("davisvantageproii: wrong crc16, looking again for header") if $::Debug{weather}; + next; + } + + # remove the 99 bytes that we just processed, we'll use the remainder as our return value + $data=substr($data,99); +#!#!#!# last; +## ? } + + &::print_log ("davisvantageproii: found a header with the right checksum") if $::Debug{weather}; + + # calculate sea level pressure + my $barometer_sea=convert_local_barom_to_sea_in($barometric_press); + +# 3rd party simple dew point calculation +# $dew_point_inside = $air_temp - ( (100 - $relative_humidity)/5 ); +# $dew_point_outside = $air_temp_inside - ( (100 - $humidity_inside)/5 ); + + # these dewpoints will be in Celsius + if ($humidity_inside ne undef) {$dew_point_inside=&::convert_humidity_to_dewpoint($humidity_inside,&::convert_f2c($air_temp_inside))}; + if ($relative_humidity ne undef) {$dew_point_outside=&::convert_humidity_to_dewpoint($relative_humidity,&::convert_f2c($air_temp))}; + +# $rain_rate=undef; +# if (defined ($lastRainReadingTime)) { +# $rain_rate=($total_rain-$lastRainReading); # delta in inches +# my $time_delta=(time - $lastRainReadingTime); +# if ($time_delta != 0) { +# $rain_rate/=$time_delta; # rate in inches per second +# $rain_rate *= 3600; # rate in inches per hour +# if ($rain_rate < 0) { # if total rain was reset to zero, this could happen +# $rain_rate=0; +# } +# } +# } +# $lastRainReadingTime=time; +# $lastRainReading=$total_rain; + + if ($main::config_parms{weather_uom_temp} eq 'C') { + grep {$_=&::convert_f2c($_);} ( + $air_temp_inside, + $air_temp + ); + # remember, dewpoints are in Celsius by default + } elsif ($main::config_parms{weather_uom_temp} eq 'F') { + $dew_point_inside=&::convert_c2f($dew_point_inside); + if ($relative_humidity ne undef) { + $dew_point_outside=&::convert_c2f($dew_point_outside); + } +# grep {$_=&::convert_c2f($_);} ( +# $dew_point_inside, +# $dew_point_outside +# ); + } + if ($main::config_parms{weather_uom_baro} eq 'mb') { + grep {$_=&::convert_in2mb($_);} ( + $barometric_press, + $barometer_sea + ); + } + if ($main::config_parms{weather_uom_rain} eq 'mm') { + grep {$_=&::convert_in2mm($_);} ( + $rain_storm + ); + } + if ($main::config_parms{weather_uom_rain} eq 'mm/hr') { + grep {$_=&::convert_in2mm($_);} ( + $rain_rate + ); + $rain_rate=sprintf('%.0f',$rain_rate); # round to nearest mm/hr + } else { + $rain_rate=sprintf('%.2f',$rain_rate); # round to nearest 0.01 in/hr + } + if ($main::config_parms{weather_uom_wind} eq 'kph') { + grep {$_=&::convert_mile2km($_);} ( + $wind_speed + ); + } + if ($main::config_parms{weather_uom_wind} eq 'm/s') { + grep {$_=&::convert_mph2mps($_);} ( + $wind_speed + ); + } + + $$wptr{TempIndoor}=$air_temp_inside; + $$wptr{TempOutdoor}=$air_temp; + $$wptr{DewIndoor}=$dew_point_inside; + $$wptr{DewOutdoor}=$dew_point_outside; + $$wptr{WindAvgSpeed}=$wind_speed; + $$wptr{WindGustSpeed}=$wind_speed; + $$wptr{WindAvgDir}=$wind_direction; + $$wptr{WindGustDir}=$wind_direction; + $$wptr{Barom}=$barometric_press; + $$wptr{BaromSea}=$barometer_sea; + $$wptr{HumidIndoor}=$humidity_inside; + $$wptr{HumidOutdoor}=$relative_humidity; + $$wptr{RainTotal}=$rain_storm; + $$wptr{RainRate}=$rain_rate; + + if ($::Debug{weather}) { + foreach my $key qw( + TempIndoor + TempOutdoor + DewIndoor + DewOutdoor + WindAvgSpeed + WindAvgDir + Barom + BaromSea + HumidIndoor + HumidOutdoor + RainTotal + RainRate + ) { + &::print_log ("davisvantageproii: $key ".$$wptr{$key}); + } + } + +if ($::Debug{weather}) { + &::print_log ("uv: $uv"); + &::print_log ("solar: $solar"); + &::print_log ("batt_xmit: $batt_xmit"); + &::print_log ("batt_cons: $batt_cons"); + &::print_log ("sunrise: $sunrise"); + &::print_log ("sunset: $sunset"); + &::print_log ("crc: $crc"); + &::print_log ("crc_calc: $crc_calc"); + &::print_log ("barometric_trend: $barometric_trend"); + &::print_log ("alarms_inside: $alarms_inside"); + &::print_log ("alarms_rain: $alarms_rain"); + &::print_log ("alarms_outside: $alarms_outside"); + &::print_log ("forecast_icon: $forecast_icon"); + &::print_log ("forecast_rule: $forecast_rule"); +} + + &::weather_updated; + return $data; +} + +sub CRC_CCITT +{ + # Expects packed data... + my $data_str = shift @_; + + my @crc_table = crc_table(); + + my $tempcrc = 0; + my @lst = split //, $data_str; + foreach my $data (@lst) + { + my $data = unpack("c",$data); + + my $crc_prev = $tempcrc; + my $index = $tempcrc >> 8 ^ $data; + my $lhs = $crc_table[$index]; + my $rhs = ($tempcrc << 8) & 0xFFFF; + $tempcrc = $lhs ^ $rhs; + + #$data = unpack("H*",$data); + #printf("%X\t %s\t %X\t %X\t %X\t : %x \n", $crc_prev, $data, $index, $lhs, $rhs, $crc); + } + + return $tempcrc; +} + +# - - - - - - - - - - - - - - - - - - - +sub crc_table +{ + +my @crc_table = ( +0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, +0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, +0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, +0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, +0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, +0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, +0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4, +0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, +0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823, +0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, +0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12, +0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, +0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41, +0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, +0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70, +0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, +0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, +0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, +0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, +0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, +0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, +0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405, +0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, +0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, +0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, +0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3, +0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, +0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92, +0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, +0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1, +0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, +0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0); +} + +# all modules must return 1. don't remove the following line +1; + +=begin comment +============================================================================= +Contents of the LOOP packet. +Field Offset Size Explanation +"L" 0 1 +"O" 1 1 +"O" 2 1 +Spells out "LOO" for Rev B packets and "LOOP" for Rev A +packets. Identifies a LOOP packet +"P" (Rev A) +Bar Trend (Rev B) +3 1 Signed byte that indicates the current 3-hour barometer trend. It +is one of these values: +-60 = Falling Rapidly = 196 (as an unsigned byte) +-20 = Falling Slowly = 236 (as an unsigned byte) +0 = Steady +20 = Rising Slowly +60 = Rising Rapidly +80 = ASCII "P" = Rev A firmware, no trend info is available +Any other value means that the Vantage does not have the 3 +hours of bar data needed to determine the bar trend. +Packet Type 4 1 Has the value zero. In the future we may define new LOOP +packet formats and assign a different value to this field. +Page 21 of 52 +Field Offset Size Explanation +Next Record 5 2 Location in the archive memory where the next data packet will +be written. This can be monitored to detect when a new record is +created. +Barometer 7 2 Current Barometer. Units are (in Hg / 1000). The barometric +value should be between 20 inches and 32.5 inches in Vantage +Pro and between 20 inches and 32.5 inches in both Vantatge Pro +Vantage Pro2. Values outside these ranges will not be logged. +Inside Temperature 9 2 The value is sent as 10th of a degree in F. For example, 795 is +returned for 79.5°F. +Inside Humidity 11 1 This is the relative humidity in %, such as 50 is returned for 50%. +Outside Temperature 12 2 The value is sent as 10th of a degree in F. For example, 795 is +returned for 79.5°F. +Wind Speed 14 1 It is a byte unsigned value in mph. If the wind speed is dashed +because it lost synchronization with the radio or due to some +other reason, the wind speed is forced to be 0. +10 Min Avg Wind Speed 15 1 It is a byte unsigned value in mph. +Wind Direction 16 2 It is a two byte unsigned value from 1 to 360 degrees. (0° is no +wind data, 90° is East, 180° is South, 270° is West and 360° is +north) +Extra Temperatures 18 7 This field supports seven extra temperature stations. +Each byte is one extra temperature value in whole degrees F with +an offset of 90 degrees. For example, a value of 0 = -90°F ; a +value of 100 = 10°F ; and a value of 169 = 79°F. +Soil Temperatures 25 4 This field supports four soil temperature sensors, in the same +format as the Extra Temperature field above +Leaf Temperatures 29 4 This field supports four leaf temperature sensors, in the same +format as the Extra Temperature field above +Outside Humidity 33 1 This is the relative humitiy in %. +Extra Humidties 34 7 Relative humidity in % for extra seven humidity stations. +Rain Rate 41 2 This value is sent as number of rain clicks (0.2mm or 0.01in). +For example, 256 can represent 2.56 inches/hour. +UV 43 1 The unit is in UV index. +Solar Radiation 44 2 The unit is in watt/meter2. +Storm Rain 46 2 The storm is stored as 100th of an inch. +Start Date of current Storm 48 2 Bit 15 to bit 12 is the month, bit 11 to bit 7 is the day and bit 6 to +bit 0 is the year offseted by 2000. +Day Rain 50 2 This value is sent as number of rain clicks. (0.2mm or 0.01in) +Month Rain 52 2 This value is sent as number of rain clicks. (0.2mm or 0.01in) +Year Rain 54 2 This value is sent as number of rain clicks. (0.2mm or 0.01in) +Day ET 56 2 This value is sent as the 1000th of an inch. +Month ET 58 2 This value is sent as the 100th of an inch. +Year ET 60 2 This value is setnt as the 100th of an inch. +Soil Moistures 62 4 The unit is in centibar. It supports four soil sensors. +Leaf Wetnesses 66 4 This is a scale number from 0 to 15 with 0 meaning very dry and +15 meaning very wet. It supports four leaf sensors. +Inside Alarms 70 1 Currently active inside alarms. See the table below +Rain Alarms 71 1 Currently active rain alarms. See the table below +Outside Alarms 72 2 Currently active outside alarms. See the table below +Extra Temp/Hum Alarms 74 8 Currently active extra temp/hum alarms. See the table below +Soil & Leaf Alarms 82 4 Currently active soil/leaf alarms. See the table below +Transmitter Battery Status 86 1 +Console Battery Voltage 87 2 Voltage = ((Data * 300)/512)/100.0 +Forecast Icons 89 1 +Page 22 of 52 +Field Offset Size Explanation +Forecast Rule number 90 1 +Time of Sunrise 91 2 The time is stored as hour * 100 + min. +Time of Sunset 93 2 The time is stored as hour * 100 + min. +"\n" = 0x0A 95 1 +"\r" = 0x0D 96 1 +CRC 97 2 +Total Length 99 +Forecast Icons in LOOP packet +Field Byte Bit # +Forecast Icons 89 Bit maps for forecast icons on the console screen. +Rain 0 +Cloud 1 +Partly Cloudy 2 +Sun 3 +Snow 4 +Forecast Icon Values +Value Decimal Value Hex Segments Shown Forecast +8 0x08 Sun Mostly Clear +6 0x06 Partial Sun + Cloud Partially Cloudy +2 0x02 Cloud Mostly Cloudy +3 0x03 Cloud + Rain Mostly Cloudy, Rain within 12 hours +18 0x12 Cloud + Snow Mostly Cloudy, Snow within 12 hours +19 0x13 Cloud + Rain + Snow Mostly Cloudy, Rain or Snow within 12 hours +7 0x07 Partial Sun + Cloud + +Rain +Partially Cloudy, Rain within 12 hours +22 0x16 Partial Sun + Cloud + +Snow +Partially Cloudy, Snow within 12 hours +23 0x17 Partial Sun + Cloud + +Rain + Snow +Partially Cloudy, Rain or Snow within 12 hours +Currently active alarms in the LOOP packet +This table shows which alarms correspond to each bit in the LOOP alarm fields. Not all bits in +each field are used. The Outside Alarms field has been split into 2 1-byte sections. +Field Byte Bit # +Inside Alarms 70 Currently active inside alarms. +Falling bar trend alarm 0 +Rising bar trend alarm 1 +Low inside temp alarm 2 +High inside temp alarm 3 +Page 23 of 52 +Field Byte Bit # +Low inside hum alarm 4 +High inside hum alarm 5 +Time alarm 6 +Rain Alarms 71 Currently active rain alarms. +High rain rate alarm 0 +15 min rain alarm 1 Flash Flood alarm +24 hour rain alarm 2 +Storm total rain alarm 3 +Daily ET alarm 4 +Outside Alarms 72 Currently active outside alarms. +Low outside temp alarm 0 +High outside temp alarm 1 +Wind speed alarm 2 +10 min avg speed alarm 3 +Low dewpoint alarm 4 +High dewpoint alarm 5 +High heat alarm 6 +Low wind chill alarm 7 +Outside Alarms, byte 2 73 +High THSW alarm 0 +High solar rad alarm 1 +High UV alarm 2 +UV Dose alarm 3 +UV Dose alarm Enabled 4 It is set to 1 when a UV dose alarm threshold has been entered +AND the daily UV dose has been manually cleared. +Outside Humidity Alarms 74 1 Currently active outside humidity alarms. +Low Humidity alarm 2 +High Humidity alarm 3 +Extra Temp/Hum Alarms 75 - 81 7 Each byte contains four alarm bits (0 – 3) for a single extra +Temp/Hum station. Bits (4 – 7) are not used and reserved for +future use. +Use the temperature and humidity sensor numbers, as +described in Section XIII.4 to locate which byte contains the +appropriate alarm bits. In particular, the humidity and +temperature alarms for a single station will be found in +different bytes. +Low temp X alarm 0 +High temp X alarm 1 +Low hum X alarm 2 +High hum X alarm 3 +Soil & Leaf Alarms 82 - 85 4 Currently active soil/leaf alarms. +Low leaf wetness X alarm 0 +High leaf wetness X alarm 1 +Low soil moisture X alarm 2 +High soil moisture X alarm 3 +Low leaf temp X alarm 4 +High leaf temp X alarm 5 +Low soil temp X alarm 6 +High soil temp X alarm 7 +============================================================================= =cut \ No newline at end of file diff --git a/lib/X10_RF_rfxsensor.pm b/lib/X10_RF_rfxsensor.pm index c2a7fbf64..5d9d9ebe4 100644 --- a/lib/X10_RF_rfxsensor.pm +++ b/lib/X10_RF_rfxsensor.pm @@ -1,161 +1,161 @@ -=begin comment - -X10_RF_rfxsensor.pm - -This module contains routines called by X10_RF.pm to determine if a -group of RF data bytes represents a valid rfxsensor command and -to decompose that data and set the state of the specific rfxsensor -specified by the command to the state specified by the command. - -The rfxsensor is a wireless temperature/humidity/pressure/low voltage sensor -that can have up to 8 one-wire sensors attached. The default address of the -first sensor (the built-in one) is 00f0, the second is 01f1, the third is -02f2, and so on. Here is the information on it: -http://www.rfxcom.com/documents/RFXSensor.pdf - -David Norwood - -=cut - -use strict; - -package X10_RF; - -use X10_RF; - -#------------------------------------------------------------------------------ - -# Subroutine: rf_is_rfxsensor -# Determine if represents a valid rfxsensor style command. - -sub rf_is_rfxsensor { - my(@bytes) = @_; - - my @rbytes; - for (my $i = 0; $i < 4; $i++) { - $rbytes[$i] = ord(pack("b8", unpack("b*", $bytes[$i]))); - } - - my $B0H = $rbytes[0] >> 4; - my $B0L = $rbytes[0] & 0x0F; - my $B1H = $rbytes[1] >> 4; - my $B1L = $rbytes[1] & 0x0F; - my $B2H = $rbytes[2] >> 4; - my $B2L = $rbytes[2] & 0x0F; - my $B3H = $rbytes[3] >> 4; - my $B3L = $rbytes[3] & 0x0F; - - # check if unit bytes are in the right format here - my $found = ($B0L == $B1L and (0xf - $B0H) == $B1H); - # check parity bits here - $found = $found && ($B3L == (0xf - (($B0H + $B0L + $B1H + $B1L + $B2H + $B2L + $B3H) & 0xf))); - return $found; -} - -#------------------------------------------------------------------------------ - -# Subroutine: rf_process_rfxsensor -# Given a valid rfxsensor style command in , set the state -# of rfxsensor specified by the command to the state specfied by -# the command. -# indicates the source of the request (w800) - -sub rf_process_rfxsensor { - my($module, @bytes) = @_; - - - my $lc_module = lc $module; - my @rbytes; - for (my $i = 0; $i < 4; $i++) { - $rbytes[$i] = ord(pack("b8", unpack("b*", $bytes[$i]))); - } - - my($cmd, $device_id, $id, $state); - my($measurement); - - # Unlike X-10 security devices, the rfxsensor's ID is 2 bytes long - $device_id = $rbytes[0] * 256 + $rbytes[1]; - $id = $rbytes[0]; - - if ($rbytes[3] & 0x10) { # device has no set temperature (set temp always 0x00) - use Switch; - switch ($rbytes[2]) { - case 0x01 {&::print_log("$module: rfxsensor: info: sensor addresses incremented")} - case 0x02 {&::print_log("$module: rfxsensor: info: battery low detected")} - case 0x03 {&::print_log("$module: rfxsensor: info: conversion not ready, 1 retry is done")} - case 0x81 {&::print_log("$module: rfxsensor: error: no 1-wire device connected")} - case 0x82 {&::print_log("$module: rfxsensor: error: 1-Wire ROM CRC error")} - case 0x83 {&::print_log("$module: rfxsensor: error: 1-Wire device connected is not a DS18B20 or DS2438")} - case 0x84 {&::print_log("$module: rfxsensor: error: no end of read signal received from 1-Wire device")} - case 0x85 {&::print_log("$module: rfxsensor: error: 1-Wire scratchpad CRC error")} - case 0x86 {&::print_log("$module: rfxsensor: error: temperature conversion not ready in time")} - case 0x87 {&::print_log("$module: rfxsensor: error: A/D conversion not ready in time")} - } - } - else { - $measurement = $rbytes[2] * 8 + ($rbytes[3] >> 5); - # let's assume these are standard type 3 rfxsensors and the zeroth sensor is a ds2438 temperature sensor - if ($id % 8 == 0) { - $measurement *= -1 if ($rbytes[2] & 0x80); # check for sign bit - $measurement *= .125; # this is celcius - if ($main::config_parms{weather_uom_temp} == 'F') { - $measurement = &::convert_c2f($measurement); - } - } - # and the 1st sensor is the ds2438 A/D converter with an attached humidity sensor, create a dummy object even if you don't have - # the humidity sensor so you don't get unmatched device errors - elsif ($id % 8 == 1) { - $measurement *= 10; # this is mVols from the A/D conversion - &::print_log("$module: rfxsensor: the A/D input is $measurement mVolts") if ($main::Debug{$lc_module}); - # get temperature - my $tmp_id = $id - 1; - $tmp_id = $tmp_id * 256 + ((0xf - ($tmp_id >> 4)) * 16) + ($tmp_id & 0xf); - $tmp_id = lc sprintf "%04x", $tmp_id; - my $temperature = &rf_get_RF_Item($module, 'rfxsensor', "unmatched device $tmp_id", $tmp_id); - if ($main::config_parms{weather_uom_temp} == 'F') { - $temperature = &::convert_f2c ($temperature); - } - # get supply voltage - my $tmp_id = $id + 1; - $tmp_id = $tmp_id * 256 + ((0xf - ($tmp_id >> 4)) * 16) + ($tmp_id & 0xf); - $tmp_id = lc sprintf "%04x", $tmp_id; - my $supply_voltage = &rf_get_RF_Item($module, 'rfxsensor', "unmatched device $tmp_id", $tmp_id); - if (defined $temperature and defined $supply_voltage and $supply_voltage) { - $measurement = ((($measurement / $supply_voltage) - 0.16) / 0.0062) / (1.0546 - 0.00216 * $temperature); # this is % relative humidity - } - else { - &::print_log("$module: rfxsensor: error: can't calculate relative humidity without temperature and supply voltage measurements"); - return undef; - } - } - # and the 2nd sensor is the ds2438 supply voltage, create a dummy object even if you don't care - elsif ($id % 8 == 2) { - $measurement *= 10; # this is mVols - } - # assume any other device ids are ds18b20 temperature sensors, since they are cheap - else { - $measurement *= -1 if $rbytes[2] & 0x80; # check for sign bit - $measurement = $measurement / 4 * .5; # this is celcius - if ($main::config_parms{weather_uom_temp} == 'F') { - $measurement = &::convert_c2f($measurement); - } - } - } - - my $item_id = lc sprintf "%04x", $device_id; - - # Set the state of any items or classes associated with this device. - &rf_set_RF_Item($module, 'rfxsensor', "unmatched device 0x$item_id", $item_id, undef, $measurement); - - return $measurement; -} - -1; - -=begin comment - -This module doesn't decode the "initialization" bytes sent by the rfxsensor. This wouldn't -add much functionality to this module because it wouldn't tell us how a DS2438 is being -used. - +=begin comment + +X10_RF_rfxsensor.pm + +This module contains routines called by X10_RF.pm to determine if a +group of RF data bytes represents a valid rfxsensor command and +to decompose that data and set the state of the specific rfxsensor +specified by the command to the state specified by the command. + +The rfxsensor is a wireless temperature/humidity/pressure/low voltage sensor +that can have up to 8 one-wire sensors attached. The default address of the +first sensor (the built-in one) is 00f0, the second is 01f1, the third is +02f2, and so on. Here is the information on it: +http://www.rfxcom.com/documents/RFXSensor.pdf + +David Norwood + +=cut + +use strict; + +package X10_RF; + +use X10_RF; + +#------------------------------------------------------------------------------ + +# Subroutine: rf_is_rfxsensor +# Determine if represents a valid rfxsensor style command. + +sub rf_is_rfxsensor { + my(@bytes) = @_; + + my @rbytes; + for (my $i = 0; $i < 4; $i++) { + $rbytes[$i] = ord(pack("b8", unpack("b*", $bytes[$i]))); + } + + my $B0H = $rbytes[0] >> 4; + my $B0L = $rbytes[0] & 0x0F; + my $B1H = $rbytes[1] >> 4; + my $B1L = $rbytes[1] & 0x0F; + my $B2H = $rbytes[2] >> 4; + my $B2L = $rbytes[2] & 0x0F; + my $B3H = $rbytes[3] >> 4; + my $B3L = $rbytes[3] & 0x0F; + + # check if unit bytes are in the right format here + my $found = ($B0L == $B1L and (0xf - $B0H) == $B1H); + # check parity bits here + $found = $found && ($B3L == (0xf - (($B0H + $B0L + $B1H + $B1L + $B2H + $B2L + $B3H) & 0xf))); + return $found; +} + +#------------------------------------------------------------------------------ + +# Subroutine: rf_process_rfxsensor +# Given a valid rfxsensor style command in , set the state +# of rfxsensor specified by the command to the state specfied by +# the command. +# indicates the source of the request (w800) + +sub rf_process_rfxsensor { + my($module, @bytes) = @_; + + + my $lc_module = lc $module; + my @rbytes; + for (my $i = 0; $i < 4; $i++) { + $rbytes[$i] = ord(pack("b8", unpack("b*", $bytes[$i]))); + } + + my($cmd, $device_id, $id, $state); + my($measurement); + + # Unlike X-10 security devices, the rfxsensor's ID is 2 bytes long + $device_id = $rbytes[0] * 256 + $rbytes[1]; + $id = $rbytes[0]; + + if ($rbytes[3] & 0x10) { # device has no set temperature (set temp always 0x00) + use Switch; + switch ($rbytes[2]) { + case 0x01 {&::print_log("$module: rfxsensor: info: sensor addresses incremented")} + case 0x02 {&::print_log("$module: rfxsensor: info: battery low detected")} + case 0x03 {&::print_log("$module: rfxsensor: info: conversion not ready, 1 retry is done")} + case 0x81 {&::print_log("$module: rfxsensor: error: no 1-wire device connected")} + case 0x82 {&::print_log("$module: rfxsensor: error: 1-Wire ROM CRC error")} + case 0x83 {&::print_log("$module: rfxsensor: error: 1-Wire device connected is not a DS18B20 or DS2438")} + case 0x84 {&::print_log("$module: rfxsensor: error: no end of read signal received from 1-Wire device")} + case 0x85 {&::print_log("$module: rfxsensor: error: 1-Wire scratchpad CRC error")} + case 0x86 {&::print_log("$module: rfxsensor: error: temperature conversion not ready in time")} + case 0x87 {&::print_log("$module: rfxsensor: error: A/D conversion not ready in time")} + } + } + else { + $measurement = $rbytes[2] * 8 + ($rbytes[3] >> 5); + # let's assume these are standard type 3 rfxsensors and the zeroth sensor is a ds2438 temperature sensor + if ($id % 8 == 0) { + $measurement *= -1 if ($rbytes[2] & 0x80); # check for sign bit + $measurement *= .125; # this is celcius + if ($main::config_parms{weather_uom_temp} == 'F') { + $measurement = &::convert_c2f($measurement); + } + } + # and the 1st sensor is the ds2438 A/D converter with an attached humidity sensor, create a dummy object even if you don't have + # the humidity sensor so you don't get unmatched device errors + elsif ($id % 8 == 1) { + $measurement *= 10; # this is mVols from the A/D conversion + &::print_log("$module: rfxsensor: the A/D input is $measurement mVolts") if ($main::Debug{$lc_module}); + # get temperature + my $tmp_id = $id - 1; + $tmp_id = $tmp_id * 256 + ((0xf - ($tmp_id >> 4)) * 16) + ($tmp_id & 0xf); + $tmp_id = lc sprintf "%04x", $tmp_id; + my $temperature = &rf_get_RF_Item($module, 'rfxsensor', "unmatched device $tmp_id", $tmp_id); + if ($main::config_parms{weather_uom_temp} == 'F') { + $temperature = &::convert_f2c ($temperature); + } + # get supply voltage + my $tmp_id = $id + 1; + $tmp_id = $tmp_id * 256 + ((0xf - ($tmp_id >> 4)) * 16) + ($tmp_id & 0xf); + $tmp_id = lc sprintf "%04x", $tmp_id; + my $supply_voltage = &rf_get_RF_Item($module, 'rfxsensor', "unmatched device $tmp_id", $tmp_id); + if (defined $temperature and defined $supply_voltage and $supply_voltage) { + $measurement = ((($measurement / $supply_voltage) - 0.16) / 0.0062) / (1.0546 - 0.00216 * $temperature); # this is % relative humidity + } + else { + &::print_log("$module: rfxsensor: error: can't calculate relative humidity without temperature and supply voltage measurements"); + return undef; + } + } + # and the 2nd sensor is the ds2438 supply voltage, create a dummy object even if you don't care + elsif ($id % 8 == 2) { + $measurement *= 10; # this is mVols + } + # assume any other device ids are ds18b20 temperature sensors, since they are cheap + else { + $measurement *= -1 if $rbytes[2] & 0x80; # check for sign bit + $measurement = $measurement / 4 * .5; # this is celcius + if ($main::config_parms{weather_uom_temp} == 'F') { + $measurement = &::convert_c2f($measurement); + } + } + } + + my $item_id = lc sprintf "%04x", $device_id; + + # Set the state of any items or classes associated with this device. + &rf_set_RF_Item($module, 'rfxsensor', "unmatched device 0x$item_id", $item_id, undef, $measurement); + + return $measurement; +} + +1; + +=begin comment + +This module doesn't decode the "initialization" bytes sent by the rfxsensor. This wouldn't +add much functionality to this module because it wouldn't tell us how a DS2438 is being +used. + =cut \ No newline at end of file diff --git a/lib/ajax.pm b/lib/ajax.pm index 77a65db7d..677150fc9 100644 --- a/lib/ajax.pm +++ b/lib/ajax.pm @@ -1,216 +1,216 @@ -=head1 B - -=head2 SYNOPSIS - -NONE - -=head2 DESCRIPTION - -A object to hold the update waiting information - -=head2 INHERITS - -NONE - -=head2 METHODS - -=over - -=cut - - -# Here are the ajax support functions -# - -use strict; -#use diagnostics; - -sub html_ajax_long_poll () { - my ($socket, $get_req, $get_arg) = @_; - - my $waiter = new ChangeWaiter ($socket, $get_arg); - - ChangeChecker::addWaiter ($waiter); - - #ChangeChecker::checkWaiters(); - - return 1; -} - -package ChangeWaiter; - -sub new { - my ($class, $socket, $sub) = @_; - my $self = {}; - - bless $self, $class; - - &main::print_log ("creating ChangeWaiter object for socket $socket and sub $sub") if $main::Debug{ajax}; - - $sub =~ s/^/&main::/; - ${$$self{waitingSocket}} = $socket; - ${$$self{expireTime}} = time() + 60; - ${$$self{changed}} = 0; # Flag if at least on state is changed - ${$$self{event}} = "&ChangeChecker::setWaiterToChanged ('$self')"; - ${$$self{sub}} = $sub; - - return $self; -} - -# Function to set the changed flag -sub flagStateChange { - (my $self) = @_; - - ${$$self{changed}} = 1; -} - -=item C - -Function to check for changes in the objects state on change a message is sent to the socket, the socket is closed and 1 is returned else 0 is returne - -=cut - -sub checkForUpdate { - my ($self) = @_; - - if (${$$self{expireTime}} < time()) { - &main::print_log ("checkForUpdate waiter for sub ${$$self{sub}} timed out, closing socket") if $main::Debug{ajax}; - ${$$self{waitingSocket}}->close; - return 1; - } - - my $xml = eval ${$$self{sub}}; - if ($@) { - &main::print_log ("checkForUpdate syntax error in sub ${$$self{sub}}\n\t$@") if $main::Debug{ajax}; - return 1; - } - - if ($xml) { - &main::print_log ("checkForUpdate sub ${$$self{sub}} returned $xml") if $main::Debug{ajax}; - &::print_socket_fork (${$$self{waitingSocket}}, $xml); - ${$$self{waitingSocket}}->close; - ${$$self{changed}} = 1; - } else { - ${$$self{changed}} = 0; - } - return ${$$self{changed}}; -} - -=back - -=head2 INI PARAMETERS - -NONE - -=head2 AUTHOR - -UNK - -=head2 SEE ALSO - -NONE - -=head2 LICENSE - -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 B - -=head2 SYNOPSIS - -NONE - -=head2 DESCRIPTION - -A class wich contains the checker for update waiter - -=head2 INHERITS - -NONE - -=head2 METHODS - -=over - -=item B - -=cut - -# A class wich contains the checker for update waiter -package ChangeChecker; - -my %waiters; -my $started; # True if running already - -sub startup { - return if $started++; - printf " - initializing state tracker ...\n"; - %waiters = (); - &main::print_log ("adding hook for checkWaiters") if $main::Debug{ajax}; - &::MainLoop_post_add_hook(\&ChangeChecker::checkWaiters, 1); -} - -sub addWaiter { - my ($waiter) = @_; - - $waiters{$waiter} = $waiter; - &main::print_log ("waiter '$waiter' added") if $main::Debug{ajax}; -} - -sub checkWaiters { - my ($class) = @_; - - foreach my $key (keys %waiters) { - if ($waiters{$key}->checkForUpdate) { - # waiter can be removed - delete $waiters{$key}; - &main::print_log ("waiter '$key' removed") if $main::Debug{ajax}; - } - } -} - -sub setWaiterToChanged { - my ($hash) = @_; - - #print "Accessing hash '$hash'\n"; - $waiters{$hash}->flagStateChange; -} - -1; - -=back - -=head2 INI PARAMETERS - -NONE - -=head2 AUTHOR - -UNK - -=head2 SEE ALSO - -NONE - -=head2 LICENSE - -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. - -=cut - +=head1 B + +=head2 SYNOPSIS + +NONE + +=head2 DESCRIPTION + +A object to hold the update waiting information + +=head2 INHERITS + +NONE + +=head2 METHODS + +=over + +=cut + + +# Here are the ajax support functions +# + +use strict; +#use diagnostics; + +sub html_ajax_long_poll () { + my ($socket, $get_req, $get_arg) = @_; + + my $waiter = new ChangeWaiter ($socket, $get_arg); + + ChangeChecker::addWaiter ($waiter); + + #ChangeChecker::checkWaiters(); + + return 1; +} + +package ChangeWaiter; + +sub new { + my ($class, $socket, $sub) = @_; + my $self = {}; + + bless $self, $class; + + &main::print_log ("creating ChangeWaiter object for socket $socket and sub $sub") if $main::Debug{ajax}; + + $sub =~ s/^/&main::/; + ${$$self{waitingSocket}} = $socket; + ${$$self{expireTime}} = time() + 60; + ${$$self{changed}} = 0; # Flag if at least on state is changed + ${$$self{event}} = "&ChangeChecker::setWaiterToChanged ('$self')"; + ${$$self{sub}} = $sub; + + return $self; +} + +# Function to set the changed flag +sub flagStateChange { + (my $self) = @_; + + ${$$self{changed}} = 1; +} + +=item C + +Function to check for changes in the objects state on change a message is sent to the socket, the socket is closed and 1 is returned else 0 is returne + +=cut + +sub checkForUpdate { + my ($self) = @_; + + if (${$$self{expireTime}} < time()) { + &main::print_log ("checkForUpdate waiter for sub ${$$self{sub}} timed out, closing socket") if $main::Debug{ajax}; + ${$$self{waitingSocket}}->close; + return 1; + } + + my $xml = eval ${$$self{sub}}; + if ($@) { + &main::print_log ("checkForUpdate syntax error in sub ${$$self{sub}}\n\t$@") if $main::Debug{ajax}; + return 1; + } + + if ($xml) { + &main::print_log ("checkForUpdate sub ${$$self{sub}} returned $xml") if $main::Debug{ajax}; + &::print_socket_fork (${$$self{waitingSocket}}, $xml); + ${$$self{waitingSocket}}->close; + ${$$self{changed}} = 1; + } else { + ${$$self{changed}} = 0; + } + return ${$$self{changed}}; +} + +=back + +=head2 INI PARAMETERS + +NONE + +=head2 AUTHOR + +UNK + +=head2 SEE ALSO + +NONE + +=head2 LICENSE + +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 B + +=head2 SYNOPSIS + +NONE + +=head2 DESCRIPTION + +A class wich contains the checker for update waiter + +=head2 INHERITS + +NONE + +=head2 METHODS + +=over + +=item B + +=cut + +# A class wich contains the checker for update waiter +package ChangeChecker; + +my %waiters; +my $started; # True if running already + +sub startup { + return if $started++; + printf " - initializing state tracker ...\n"; + %waiters = (); + &main::print_log ("adding hook for checkWaiters") if $main::Debug{ajax}; + &::MainLoop_post_add_hook(\&ChangeChecker::checkWaiters, 1); +} + +sub addWaiter { + my ($waiter) = @_; + + $waiters{$waiter} = $waiter; + &main::print_log ("waiter '$waiter' added") if $main::Debug{ajax}; +} + +sub checkWaiters { + my ($class) = @_; + + foreach my $key (keys %waiters) { + if ($waiters{$key}->checkForUpdate) { + # waiter can be removed + delete $waiters{$key}; + &main::print_log ("waiter '$key' removed") if $main::Debug{ajax}; + } + } +} + +sub setWaiterToChanged { + my ($hash) = @_; + + #print "Accessing hash '$hash'\n"; + $waiters{$hash}->flagStateChange; +} + +1; + +=back + +=head2 INI PARAMETERS + +NONE + +=head2 AUTHOR + +UNK + +=head2 SEE ALSO + +NONE + +=head2 LICENSE + +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. + +=cut + diff --git a/lib/site/ControlX10/CM11.pm.old b/lib/site/ControlX10/CM11.pm.old index 09483f752..320e9f99b 100644 --- a/lib/site/ControlX10/CM11.pm.old +++ b/lib/site/ControlX10/CM11.pm.old @@ -1,839 +1,839 @@ -package ControlX10::CM11; -#----------------------------------------------------------------------------- -# -# An X10 ActiveHome interface, used by Misterhouse ( http://misterhouse.net ) -# -# Uses the Windows or Posix SerialPort.pm functions by Bill Birthisel, -# available on CPAN -# -#----------------------------------------------------------------------------- -use strict; -use vars qw($VERSION $DEBUG @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $POWER_RESET); - -require Exporter; - -@ISA = qw(Exporter); -@EXPORT= qw( send_cm11 receive_cm11 read_cm11 dim_decode_cm11 ping_cm11); -@EXPORT_OK= qw(); -%EXPORT_TAGS = (FUNC => [qw( send_cm11 receive_cm11 - read_cm11 dim_decode_cm11 - ping_cm11 )]); - -Exporter::export_ok_tags('FUNC'); - -$EXPORT_TAGS{ALL} = \@EXPORT_OK; - -#### Package variable declarations #### - -($VERSION) = q$Revision: 2.22 $ =~ /: (\S+)/; # Note: cvs version reset when we moved to sourceforge -$DEBUG = 0; -my $Last_Dcode; - -sub send_cm11 { - return unless ( 2 == @_ ); - return ControlX10::CM11::send ( @_ ); -} - -sub receive_cm11 { - return unless ( 1 == @_ ); - return ControlX10::CM11::receive_buffer ( shift ); -} - -sub read_cm11 { - return unless ( 2 == @_ ); - return ControlX10::CM11::read ( @_ ); -} - -sub dim_decode_cm11 { - return unless ( 1 == @_ ); - return ControlX10::CM11::dim_level_decode ( shift ); -} - -sub ping_cm11 { - return unless ( 1 == @_ ); - return ControlX10::CM11::ping ( shift ); -} - - - # These tables are used in sending data -my %table_hcodes = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101 - I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100); -my %table_dcodes = qw(1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101 - 9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100 - A 1111 B 0011 C 1011 D 0000 E 1000 F 0100 G 1100); -my %table_fcodes = qw(J 0010 K 0011 M 0100 L 0101 O 0001 P 0000 - ALL_OFF 0000 ALL_ON 0001 ON 0010 OFF 0011 DIM 0100 BRIGHT 0101 - -10 0100 -20 0100 -30 0100 -40 0100 - -15 0100 -25 0100 -35 0100 -45 0100 -5 0100 - -50 0100 -60 0100 -70 0100 -80 0100 -90 0100 - -55 0100 -65 0100 -75 0100 -85 0100 -95 0100 -100 0100 - +10 0101 +20 0101 +30 0101 +40 0101 - +15 0101 +25 0101 +35 0101 +45 0101 +5 0101 - +50 0101 +60 0101 +70 0101 +80 0101 +90 0101 - +55 0101 +65 0101 +75 0101 +85 0101 +95 0101 +100 0101 - ALL_LIGHTS_OFF 0110 EXTENDED_CODE 0111 HAIL_REQUEST 1000 HAIL_ACK 1001 - PRESET_DIM1 1010 PRESET_DIM2 1011 EXTENDED_DATA 1100 - STATUS_ON 1101 STATUS_OFF 1110 STATUS 1111); - - - # These tables are used in receiving data -my %table_hcodes2 = qw(0110 A 1110 B 0010 C 1010 D 0001 E 1001 F 0101 G 1101 H - 0111 I 1111 J 0011 K 1011 L 0000 M 1000 N 0100 O 1100 P); -my %table_dcodes2 = qw(0110 1 1110 2 0010 3 1010 4 0001 5 1001 6 0101 7 1101 8 - 0111 9 1111 A 0011 B 1011 C 0000 D 1000 E 0100 F 1100 G); - # Yikes! L and M are swapped! If we fix it here, we also - # have to fix it elsewhere (maybe only in bin/mh, $f_code test) -my %table_fcodes2 = qw(0010 J 0011 K 0100 L 0101 M 0001 O 0000 P - 0111 Z 1010 PRESET_DIM1 1011 PRESET_DIM2 - 1101 STATUS_ON 1110 STATUS_OFF 1111 STATUS); - - -sub receive_buffer { - my ($serial_port) = @_; - - if (exists $main::Debug{x10}) { - $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; - } - - my $pc_ready = pack('C', 0xc3); - print "Bad cm11 pc_ready transmition\n" unless 1 == $serial_port->write($pc_ready); - - # Lets not wait for data (use no_block option), or we loop too long and mh slows way down - - # let the 0xc3 ack take hold ... emperically derived ... 1/2 misses at 20 ms - # - increase from 40 to 80, based on other CM11s. - select undef, undef, undef, 80 / 1000; - - my $data; - return undef unless $data = &read($serial_port, 1); - -# my $data = &read($serial_port); - - my @bytes = split //, $data; - - my $length = shift @bytes; - my $mask = shift @bytes; - - $length = unpack('C', $length); - $mask = unpack('B8', $mask); - my $data_h = unpack('H*', $data); - print "receive buffer length=$length, mask=$mask, data_h=$data_h.\n" if $DEBUG; - - my ($house, $function, $device, $i, $extended_count); - - undef $data; - foreach my $byte (@bytes) { - # Send extended data into MH as untranslated hex. - if ($extended_count) { - $data .= unpack('H*', $byte); - --$extended_count; - ++$i; - } - else { - my $bits = unpack('B8', $byte); - my $house_bits = substr($bits, 0, 4); - my $code_bits = substr($bits, 4, 4); - print "CM11 error, not a valid house code: $house_bits\n" unless $house = $table_hcodes2{$house_bits}; - if (substr($mask, -(++$i), 1)) { - print "CM11 error, not a valid function code: $code_bits at byte $i value $bits\n" unless $function = $table_fcodes2{$code_bits}; -# print "function=$house$function\n"; - - # Add device code back in, since this is not included in status :( - $function = $Last_Dcode . $function if $function =~ /^STATUS/; - # Handle Vehicle Interface RF Receiver extended code - assume length of 3 for extended - $extended_count = 3 if ($function eq 'Z'); -## 2.08, but 'Z' not numeric ## -## $extended_count = 3 if ($function == 'Z'); - - $data .= $house . $function; - print "CM11 db: data=$data\n" if $DEBUG; - } - else { - print "CM11 error, not a valid device code: $code_bits\n" unless $device = $table_dcodes2{$code_bits}; -# print "device=$house$device\n"; - $data .= $house . $device; - } - } -# print "byte=$byte, $bits\n"; - } - return $data; -} - -sub format_data { - my ($house_code) = @_; - - print "CM11 send data=$house_code\n" if $DEBUG; - - my ($house, $code, $house_bits, $header, $code_bits, $function, $dim_level); - my ($extended, $extended_string, $extended_checksum); - - ($house, $code) = $house_code =~ /(\S)(\S+)/; - $house = uc($house); - $code = uc($code); - - unless ($house_bits = $table_hcodes{$house}) { - print "CM11 error, invalid house code: $house. data=$house_code\n"; - return; - } - -# $code can be -# 1-9,A-G for Device code -# d_xyz. for Extended code xyz for device d -# xyz for Function codes, including +-## for bright/dim - - # Test for extended code - # - format is &P## where ## is the preset dim level - if (my($extended_data) = $code =~ /&P(\d+)/) { - unless (($extended_data >= 0) && ($extended_data < 65)) { - print "CM11 error, invalid extended code. code=$code\n"; - return; - } - $code_bits = '0111'; # Extended code - $function = '1'; # Extended transmitions are a function - $extended = '1'; - $dim_level = 0; # Dim level is not applicable to extended transmitions. - - # Hard codeded preset for now ... - - # This is not documented!! By looking at - # ActiveHome errata, it seems the device code is required - # - $Last_Dcode is a hack ... assume previous selected device. - my $extended_device = '0000' . $table_dcodes{$Last_Dcode}; - my $extended_code = '00110001'; # Type=3 => Control Modules Func=1 => Preset Receiver - - # Convert from bit to string - my $b3 = pack('B8', $extended_device); - my $b4 = pack('C1', $extended_data); - my $b5 = pack('B8', $extended_code); - $extended_string = $b3 . $b4 . $b5; - my $b3c = unpack('C', $b3); - my $b4c = unpack('C', $b4); - my $b5c = unpack('C', $b5); - $extended_checksum = $b3c + $b4c + $b5c; - if ($DEBUG) { - printf "CM11 ed=%d, b345=0x%0.2x,0x%0.2x,0x%0.2x ex=%s cs=0x%0.2x\n", - $extended_data, $b3c, $b4c, $b5c, $extended_string, - $extended_checksum; - } - } - # Test for device code - elsif ($code_bits = $table_dcodes{$code}) { - $function = '0'; - $extended = '0'; - $dim_level = 0; - $Last_Dcode = $code; # This is desperate :) - } - # Test for function code - elsif ($code_bits = $table_fcodes{$code}) { - $function = '1'; - $extended = '0'; - if ($code eq 'DIM' or $code eq 'M' or $code eq 'BRIGHT' or $code eq 'L') { - $dim_level = 34; # Lets default to 3 bight/dims to go full swing - } - elsif ($code =~ /^[+-]\d\d$/) { - $dim_level = abs($code); - } - else { - $dim_level = 0; - } - } - else { - print "CM11 error, invalid cm11 x10 code: $code\n"; - return; - } - - my $dim = int($dim_level * 22 / 100); # 22 levels = 100% - $header = substr(unpack('B8', pack('C', $dim)), 3); - - $header .= '1'; # Bit 2 is always set to a 1 to ensure synchronization - $header .= $function; # 0 for address, 1 for function - $header .= $extended; # 0 for standard, 1 for extended transmition - - # Convert from bit to string - my $b1 = pack('B8', $header); - my $b2 = pack('B8', $house_bits . $code_bits); - - # Calculate checksum - my $b1d = unpack('C', $b1); - my $b2d = unpack('C', $b2); - my $checksum = ($b1d + $b2d) & 0xff; - - my $data = $b1 . $b2; - - if ($extended) { - $data .= $extended_string; - $checksum = ($checksum + $extended_checksum) & 0xff; - } - - printf("CM11 dim=$dim header=$header hb=$house_bits cb=$code_bits " . - "bd=0x%0.2x,0x%0.2x checksum=0x%0.2x\n", - $b1d, $b2d, $checksum) if $DEBUG; - - return $data, $checksum; - -} - -sub send { - my ($serial_port, $house_code) = @_; - - if (exists $main::Debug{x10}) { - $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; - } - - my ($data_snd, $checksum) = &format_data($house_code); - return unless $data_snd; - - my $retry_cnt = 0; - RETRY: - print "CM11 send: ", unpack('H*', $data_snd), "\n" if $DEBUG; - - print "Bad cm11 data send transmition\n" unless length($data_snd) == $serial_port->write($data_snd); - - # Note: Skip the power fail check, because we the - # checksum might be the power fail flag (0xa5) - my $data_rcv = &read($serial_port, 0, 1); -# my $data_rcv; -# return unless $data_rcv = &read($serial_port, 0, 1); - - my $data_d = unpack('C', $data_rcv); - - # Unrelated incoming data ... process and re-start - # Note: Some checksums will be 0x5a or 0xa5 ... skip this test if so - if (($data_d == 0x5a or $data_d == 0xa5) and !($checksum == 0x5a or $checksum == 0xa5)) { - print "Data received while xmiting data ... will receive and retry\n"; - &receive_buffer($serial_port); - goto RETRY if $retry_cnt++ < 3; - } - - if ($checksum != $data_d) { - print "Bad checksum in cm11 send: cs1=$checksum cs2=$data_d. Will retry\n"; - goto RETRY if $retry_cnt++ < 3; - } - - print "CM11 ack\n" if $DEBUG; - my $pc_ok = pack('C', 0x00); - print "Bad cm11 acknowledge send transmition\n" unless 1 == $serial_port->write($pc_ok); - - return unless $data_rcv = &read($serial_port); - $data_d = unpack('C', $data_rcv); - - if ($data_d == 0x55) { - print "CM11 done\n" if $DEBUG; - } - # Unrelated incoming data ... process - elsif ($data_d == 0x5a or $data_d == 0xa5) { - print "Data received while xmiting data ... receive and retry\n"; - &receive_buffer($serial_port); - goto RETRY if $retry_cnt++ < 3; - } - - return $data_d; -} - -sub read { - my ($serial_port, $no_block, $no_power_fail_check) = @_; - my $data; - # Note ... for dim commands > 30, this will time out after 30*50=1.5 seconds - # No harm done, but we would rather not wait :) - my $tries = ($no_block) ? 1 : 30; - - if (exists $main::Debug{x10}) { - $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; - } - - while ($tries--) { - print "." if $DEBUG and !$no_block; - if ($data = $serial_port->input) { - my $data_d = unpack('C', $data); -# printf("rcv data=%s, %x.\n", $data, $data_d); -# my $pc_ready = pack('C', 0xc3); -# $serial_data = "$pc_ready"; -# print "serial1 out=$serial_data results=", $serial_port->write($serial_data), ".\n" if $DEBUG; - printf("\nCM11 data=%s hex=%0.2lx\n", $data_d, $data_d) if $DEBUG; - - # If we received the power-fail string (0xa5), reset with a blank macro command - # - Protocol.txt says to send macros string, but that did not work. - if ($data_d == 165 and !$no_power_fail_check) { - - print "\nCM11 power fail detected."; - &setClock($serial_port); - # We can use this to detect a power failure - $POWER_RESET = 1; # The user code will be responsible for reseting this - } - - return $data; - } - # If we do not do this, we may get endless error messages. - else { - $serial_port->reset_error; - } - - if ($tries) { - select undef, undef, undef, 50 / 1000; - } - } - - print "No data received from cm11\n" if ($DEBUG and !$no_block); - return undef; -} - -sub dim_level_decode { - my ($code) = @_; - - my %table_hcodes = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101 - I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100); - my %table_dcodes = qw(1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101 - 9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100 - A 1111 B 0011 C 1011 D 0000 E 1000 F 0100 G 1100); - - if (exists $main::Debug{x10}) { - $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; - } - - # Convert bit string to decimal - my $level_b = $table_hcodes{substr($code, 0, 1)} . $table_dcodes{substr($code, 1, 1)}; - my $level_d = unpack('C', pack('B8', $level_b)); - # Varies from 36 to 201, by 11, then to 210 as a max. - # 16 different values. Round to nearest 5%, max of 95. - my $level_p = int(100 * $level_d / 211); # Do not allow 100% ... not a valid state? - ## print "CM11 debug1: levelb=$level_b level_p=$level_p\n" if $DEBUG; - $level_p = $level_p - ($level_p % 5); - print "CM11 debug: dim_code=$code leveld=$level_d level_p=$level_p\n" if $DEBUG; - return $level_p; -} - - -sub reset_cm11 { - return unless ( 1 == @_ ) ; # requires port number to reset - &enable_RI ( @_ ); - &setClock ( @_ ); -} - - # This currently gets bad checksums :( - # On windows, it gives Parity Errors. -sub enable_RI { - my ($serial_port) = @_; - my $ri_on = 0xeb; - my $ack = 0x00; - my $done = 0x55; - my $checksum; - - # Send RI Enable code to CM11 - $serial_port->input; - $serial_port->write(pack('C',$ri_on)); - do { - $checksum = $serial_port->input; - } until $checksum; - - if ( $checksum ne pack('C',$ri_on) ) { - print "Checksum error in enabling RI: ", unpack('H2',$checksum),"\n"; - return $checksum; - } - - # Tell the CM11 to do it - $serial_port->write(pack('C',$ack)); - - do { - $checksum = $serial_port->input; - } until $checksum; - - if ( $checksum ne pack('C',$done) ) { - print "CM11 failed to properly acknowledge execution of RI_Enable\n"; - } - return $checksum; -} - -#55 to 48 timer download header (0x9b) -#47 to 40 Current time (seconds) -#39 to 32 Current time (minutes ranging from 0 to 119) -#31 to 23 Current time (hours/2, ranging from 0 to 11) -#23 to 16 Current year day (bits 0 to 7) -#15 Current year day (bit 8) -#14 to 8 Day mask (SMTWTFS) -#7 to 4 Monitored house code -#3 Reserved -#2 Battery timer clear flag -#1 Monitored status clear flag -#0 Timer purge flag - -sub setClock { - my ($serial_port) = @_; -# $DEBUG=1; - my ($Second, $Minute, $Hour, $Mday, $Month, $Year, $Wday, $Yday) = localtime time; - my $localtime = localtime time; - print "Reseting time with: $localtime\n" if $DEBUG; -# $Wday = 2 ** (7 - $Wday); -# if ($Yday > 255) { -# $Yday -= 256; -# $Wday *= 2; -# } - # Manipulate Minutes to be 0 - 119 (2 hours) and Hours to be 0 - 11 - $Minute += (($Hour % 2) * 60); - $Hour /= 2; - # Must do some weird packing of data for Yday and Wday fields. - my $Yday1 = $Yday % 256; # mantisa of Yday - my $Yday2 = ($Yday / 256) << 7; # Radius of Yday shifted over 7 bits - my $Dmask = 0x01 << $Wday; # Day mask of SMTWTFS - $Yday2 |= $Dmask; # OR the two fields together to get one - my $CodeF = 0x06 << 4; # Put "A" housecode in upper nibble - $CodeF |= 0x07; # Put 0b0111 in lower nibble (battery,monitor, & timer cleared) - my $power_reset = pack('C7', - 0x9b, - $Second, - $Minute, - $Hour, - $Yday1, - $Yday2, - $CodeF); -# $Wday, -# 0x03); # Not sure what is best here. x10d.c did this. - - my $results = $serial_port->write($power_reset); - select undef, undef, undef, 50 / 1000; - my $checksum = $serial_port->input; # Receive, but ignore, checksum - if ($DEBUG) { - printf "\npower_reset: %s %s %s %s %s %s %s\n", - unpack ('H2H2H2H2H2H2H2', $power_reset); - print " sent $results bytes\n"; - printf " checksum = %x\n", ord($checksum); - } - my $pc_ok = pack('C', 0x00); - print "Bad cm11 checksum acknowledge\n" unless 1 == $serial_port->write($pc_ok); -} - -sub ping { - my ($serial_port) = @_; - my $ri_on = 0xeb; - my $ack = 0x00; - my $done = 0x55; - my $checksum; - my $counter; - my $maxcounter = 10000; - - # Send RI Enable code to CM11 - $serial_port->write(pack('C',$ri_on)); - - $counter = 0; - do { - $checksum = $serial_port->input; - $counter++; - } until (($checksum) || ($counter == $maxcounter)); - - return 0 if ($counter == $maxcounter); - - print "cm11::ping - checksum: got: 0x", unpack('H2',$checksum),"\n" if $DEBUG; - - if (($checksum ne pack('C',$ri_on)) && $DEBUG) { - print "cm11::ping checksum: expected 0x",unpack('H2',pack('C',$ri_on)),". got: 0x", unpack('H2',$checksum),")\n"; - } - - # 0x5a is sent by the CM11 if it has data waiting. If we get this then the CM11 is obviously alive. - if ($checksum eq pack('C',0x5a)) { - print "cm11::ping - cm11 has data waiting!\n" if $DEBUG; - return 1; - } - - $serial_port->write(pack('C',$ack)); - - $counter = 0; - do { - $checksum = $serial_port->input; - $counter++; - } until (($checksum) || ($counter == $maxcounter)); - - print "cm11::ping - checksum: got: 0x", unpack('H2',$checksum),"\n" if $DEBUG; - - if (($checksum ne pack('C',$done)) && $DEBUG) { - print "cm11::ping - checksum: expected 0x",unpack('H2',pack('C',$done)),". got: 0x", unpack('H2',$checksum),")\n"; - } - - print "cm11::ping - counter=$counter\n" if $DEBUG; - return 1; -} - - -return 1; # for require -__END__ - -=pod - -=head1 NAME - -ControlX10::CM11 - Perl extension for X10 'ActiveHome' Controller - -=head1 SYNOPSIS - - use ControlX10::CM11; - - # $serial_port is an object created using Win32::SerialPort - # or Device::SerialPort depending on OS - # my $serial_port = setup_serial_port('COM10', 4800); - - $data = &ControlX10::CM11::receive_buffer($serial_port); - $data = &ControlX10::CM11::read($serial_port, $no_block); - $percent = &ControlX10::CM11::dim_level_decode('GE'); # 40% - - &ControlX10::CM11::send($serial_port, 'A1'); # Address device A1 - &ControlX10::CM11::send($serial_port, 'AJ'); # Turn device ON - # House Code 'A' present in both send() calls - - &ControlX10::CM11::send($serial_port, 'B'.'ALL_OFF'); - # Turns All lights on house code B off - -=head1 DESCRIPTION - -The CM11A is a bi-directional X10 controller that connects to a serial -port and transmits commands via AC power line to X10 devices. This -module translates human-readable commands (eg. 'A2', 'AJ') into the -Interface Communication Protocol accepted by the CM11A. - -=over 4 - -=item send command - -This transmits a two-byte message containing dim and house information -and either an address or a function. Checksum and acknowledge handshaking -is automatic. The command accepts a string parameter. The first character -in the string must be a I in the range [A..P] and the rest of -the string determines the type of message. Intervening whitespace is not -currently permitted between the I and the I. This -may change in the future. - - STRING ALTERNATE_STRING FUNCTION - 1..9 Unit Address - A..G Unit Address - J ON Turn Unit On - K OFF Turn Unit Off - L BRIGHT Brighten Last Light Programmed 5% - M DIM Dim Last Light Programmed 5% - O ALL_ON All Units On - P ALL_OFF All Units Off - -There are also functions without "shortcut" letter commands: - - ALL_LIGHTS_OFF EXTENDED_CODE EXTENDED_DATA - HAIL_REQUEST HAIL_ACK PRESET_DIM1 - PRESET_DIM2 STATUS_ON STATUS_OFF - STATUS - -Dim and Bright functions can also take a signed value in the -range [-95,-90,...,-10,-5,+5,+10,...,+90,+95]. - - ControlX10::CM11::send($serial_port,'A1'); # Address device A1 - ControlX10::CM11::send($serial_port,'AJ'); # Turn device ON - ControlX10::CM11::send($serial_port,'A-25'); # Dim to 25% - -=item send extended function - -Starting in version 2.04, extended commands may be sent to devices that -support the enhanced X10 protocol. If you have one of the newer (more -expensive) LM14A/PLM21 2 way X10 pro lamp modules, you can set it directly -to a specific brightness level using a Preset Dim extended code. - -The 64 extended X10 Preset Dim codes are commanded by appending C<&##> to -the unit address where C<##> is a number between 1 and 63. - - ControlX10::CM11::send($serial_port,'A5'); # Address A5 - ControlX10::CM11::send($serial_port,'A&P16'); # Dim to 25% - -A partial translation list for the most important levels: - - &P## % &P## % &P## % - 0 0 13 20 44 70 - 1 2 16 25 47 75 - 2 4 19 30 50 80 - 3 5 25 40 57 90 - 6 10 31 50 61 95 - 9 15 38 60 63 100 - -There is another set of Preset Dim commands that are used by some modules -(e.g. the RCS TX15 thermostat). These 32 non-extended Preset Dim codes can -be coded directly, using the following table: - - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PRESET_DIM1 - M N O P C D A B E F G H K L I J - - 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 PRESET_DIM2 - M N O P C D A B E F G H K L I J - -This usage, and the responses assigned to each command, are device specific. -For example, the following commands enable preset value 18: - - ControlX10::CM11::send($serial_port,'M4'); # Address thermostat - ControlX10::CM11::send($serial_port,'OPRESET_DIM2'); # Select preset 18 - - -Starting in version 2.07, incoming extended data is also processed. -The first character will be the I in the range [A..P]. -The next character will be I, indicating extended data. -The remaining data will be the extended data. - - -=item read - -This checks for an incoming transmission. It will return "" for no input. -It also tests for a received a "power fail" message (0xa5). If it detects -one, it automatically sends the command/data to reset the CM11 clock. If -the C<$no_block> parameter is FALSE (0, "", or undef), the B will retry -for up to a second at 50 millisecond intervals. With C<$no_block> TRUE, -the B checks one time for available data. - - $data = &ControlX10::CM11::read($serial_port, $no_block); - -=item receive_buffer - -This command handles the upload response to an "Interface Poll Signal" -message (0x5a) B from the CM11. The module sends "ready" (0xc3) and -receives up to 10 bytes. The first two bytes are size and description of -the remaining bytes. These are used to decode the data bytes, but are not -returned by the B function. Each of the data bytes is -decoded as if it was a B command from an external CM11 or equivalent -external source (such as an RF keypad). - - $data = &ControlX10::CM11::receive_buffer($serial_port); - # $data eq "A2AK" after an external device turned off A2 - -Multiple house and unit addresses can appear in a single buffer. - - if ($data eq "B1BKA2AJ") { - print "B1 off, A2 on\n"; - } - -=item dim_level_decode - -When the external command includes dim/bright information in addition to -the address and function, the B function converts that -data byte (as processed by the B command) into percent. - - $data = &ControlX10::CM11::receive_buffer($serial_port); - # $data eq "A2AMGE" after an external device dimmed A2 to 40% - $percent = &ControlX10::CM11::dim_level_decode("GE"); - # $percent == 40 - -A more complex C<$data> input is possible. - - if ($data eq "B1B3B5B7B9BLLE") { - print "House B Inputs 1,3,5,7,9 Brightened to 85%\n"; - } - -The conversion between text_data and percent makes more sense to the code -than to humans. The following table gives representative values. Others -may be received from a CM11 and will be properly decoded. - - Percent Text Percent Text - 0 M7 50 AA - 5 ED 55 I6 - 10 EC 60 NF - 15 C7 65 N2 - 20 KD 70 F6 - 25 K4 75 DB - 30 O7 80 D2 - 35 OA 85 LE - 40 G6 90 PB - 45 AF 95 P8 - -=back - -=head1 EXPORTS - -The B, B, B, and B -functions are exported by default starting with Version 2.09. -They are identical to the "fully-qualified" names and accept the same -parameters. The I tag C<:FUNC> is maintained for -compatibility (but deprecated). - - use ControlX10::CM11; - send_cm11($serial_port, 'A1'); # send() - address - send_cm11($serial_port, 'AJ'); # send() - function - $data = receive_cm11($serial_port); # receive_buffer() - $data = read_cm11($serial_port, $no_block); # read() - $percent = dim_decode_cm11('GE'); # dim_level_decode() - -=head1 AUTHORS - -Bruce Winter bruce@misterhouse.net http://misterhouse.net - -CPAN packaging by Bill Birthisel wcbirthisel@alum.mit.edu -http://members.aol.com/bbirthisel - -=head2 MAILING LISTS - -General information about the mailing lists is at: - - http://lists.sourceforge.net/mailman/listinfo/misterhouse-users - http://lists.sourceforge.net/mailman/listinfo/misterhouse-announce - -To post to this list, send your email to: - - misterhouse-users@lists.sourceforge.net - -If you ever want to unsubscribe or change your options (eg, switch to -or from digest mode, change your password, etc.), visit your -subscription page at: - - http://lists.sourceforge.net/mailman/options/misterhouse-users/$user_id - -=head1 SEE ALSO - -mh can be download from http://misterhouse.net - -Win32::SerialPort and Device::SerialPort from CPAN - -CM11A Protocol documentation available at http://www.x10.com - -perl(1). - -=head1 COPYRIGHT - -Copyright (C) 2000 Bruce Winter. All rights reserved. - -This module is free software; you can redistribute it and/or modify it -under the same terms as Perl itself. 30 January 2000. - -=cut - -# -# $Log: CM11.pm,v $ -# Revision 2.22 2004/09/25 20:01:20 winter -# *** empty log message *** -# -# Revision 2.21 2004/06/06 21:38:44 winter -# *** empty log message *** -# -# Revision 2.20 2003/12/22 00:25:06 winter -# - 2.86 release -# -# Revision 2.19 2003/11/23 20:26:02 winter -# - 2.84 release -# -# Revision 2.18 2003/03/09 19:34:42 winter -# - 2.79 release -# -# Revision 2.17 2002/11/10 01:59:57 winter -# - 2.73 release -# -# Revision 2.16 2002/03/02 02:36:51 winter -# - 2.65 release -# -# Revision 2.15 2001/02/04 20:31:31 winter -# - 2.43 release -# -# Revision 2.14 2001/01/20 17:47:50 winter -# - 2.41 release -# -# Revision 2.13 2000/10/01 23:29:41 winter -# - 2.29 release -# -# Revision 2.12 2000/08/19 01:25:09 winter -# - 2.27 release -# -# Revision 2.11 2000/04/22 00:11:15 winter -# - increase receive buffer delay from 40 to 80 -# -# Revision 2.10 2000/02/12 06:11:37 winter -# - commit lots of changes, in preperation for mh release 2.0 -# -# Revision 2.08 2000/01/29 20:07:01 winter -# - add $no_power_fail_check. -# -# +package ControlX10::CM11; +#----------------------------------------------------------------------------- +# +# An X10 ActiveHome interface, used by Misterhouse ( http://misterhouse.net ) +# +# Uses the Windows or Posix SerialPort.pm functions by Bill Birthisel, +# available on CPAN +# +#----------------------------------------------------------------------------- +use strict; +use vars qw($VERSION $DEBUG @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $POWER_RESET); + +require Exporter; + +@ISA = qw(Exporter); +@EXPORT= qw( send_cm11 receive_cm11 read_cm11 dim_decode_cm11 ping_cm11); +@EXPORT_OK= qw(); +%EXPORT_TAGS = (FUNC => [qw( send_cm11 receive_cm11 + read_cm11 dim_decode_cm11 + ping_cm11 )]); + +Exporter::export_ok_tags('FUNC'); + +$EXPORT_TAGS{ALL} = \@EXPORT_OK; + +#### Package variable declarations #### + +($VERSION) = q$Revision: 2.22 $ =~ /: (\S+)/; # Note: cvs version reset when we moved to sourceforge +$DEBUG = 0; +my $Last_Dcode; + +sub send_cm11 { + return unless ( 2 == @_ ); + return ControlX10::CM11::send ( @_ ); +} + +sub receive_cm11 { + return unless ( 1 == @_ ); + return ControlX10::CM11::receive_buffer ( shift ); +} + +sub read_cm11 { + return unless ( 2 == @_ ); + return ControlX10::CM11::read ( @_ ); +} + +sub dim_decode_cm11 { + return unless ( 1 == @_ ); + return ControlX10::CM11::dim_level_decode ( shift ); +} + +sub ping_cm11 { + return unless ( 1 == @_ ); + return ControlX10::CM11::ping ( shift ); +} + + + # These tables are used in sending data +my %table_hcodes = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101 + I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100); +my %table_dcodes = qw(1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101 + 9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100 + A 1111 B 0011 C 1011 D 0000 E 1000 F 0100 G 1100); +my %table_fcodes = qw(J 0010 K 0011 M 0100 L 0101 O 0001 P 0000 + ALL_OFF 0000 ALL_ON 0001 ON 0010 OFF 0011 DIM 0100 BRIGHT 0101 + -10 0100 -20 0100 -30 0100 -40 0100 + -15 0100 -25 0100 -35 0100 -45 0100 -5 0100 + -50 0100 -60 0100 -70 0100 -80 0100 -90 0100 + -55 0100 -65 0100 -75 0100 -85 0100 -95 0100 -100 0100 + +10 0101 +20 0101 +30 0101 +40 0101 + +15 0101 +25 0101 +35 0101 +45 0101 +5 0101 + +50 0101 +60 0101 +70 0101 +80 0101 +90 0101 + +55 0101 +65 0101 +75 0101 +85 0101 +95 0101 +100 0101 + ALL_LIGHTS_OFF 0110 EXTENDED_CODE 0111 HAIL_REQUEST 1000 HAIL_ACK 1001 + PRESET_DIM1 1010 PRESET_DIM2 1011 EXTENDED_DATA 1100 + STATUS_ON 1101 STATUS_OFF 1110 STATUS 1111); + + + # These tables are used in receiving data +my %table_hcodes2 = qw(0110 A 1110 B 0010 C 1010 D 0001 E 1001 F 0101 G 1101 H + 0111 I 1111 J 0011 K 1011 L 0000 M 1000 N 0100 O 1100 P); +my %table_dcodes2 = qw(0110 1 1110 2 0010 3 1010 4 0001 5 1001 6 0101 7 1101 8 + 0111 9 1111 A 0011 B 1011 C 0000 D 1000 E 0100 F 1100 G); + # Yikes! L and M are swapped! If we fix it here, we also + # have to fix it elsewhere (maybe only in bin/mh, $f_code test) +my %table_fcodes2 = qw(0010 J 0011 K 0100 L 0101 M 0001 O 0000 P + 0111 Z 1010 PRESET_DIM1 1011 PRESET_DIM2 + 1101 STATUS_ON 1110 STATUS_OFF 1111 STATUS); + + +sub receive_buffer { + my ($serial_port) = @_; + + if (exists $main::Debug{x10}) { + $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; + } + + my $pc_ready = pack('C', 0xc3); + print "Bad cm11 pc_ready transmition\n" unless 1 == $serial_port->write($pc_ready); + + # Lets not wait for data (use no_block option), or we loop too long and mh slows way down + + # let the 0xc3 ack take hold ... emperically derived ... 1/2 misses at 20 ms + # - increase from 40 to 80, based on other CM11s. + select undef, undef, undef, 80 / 1000; + + my $data; + return undef unless $data = &read($serial_port, 1); + +# my $data = &read($serial_port); + + my @bytes = split //, $data; + + my $length = shift @bytes; + my $mask = shift @bytes; + + $length = unpack('C', $length); + $mask = unpack('B8', $mask); + my $data_h = unpack('H*', $data); + print "receive buffer length=$length, mask=$mask, data_h=$data_h.\n" if $DEBUG; + + my ($house, $function, $device, $i, $extended_count); + + undef $data; + foreach my $byte (@bytes) { + # Send extended data into MH as untranslated hex. + if ($extended_count) { + $data .= unpack('H*', $byte); + --$extended_count; + ++$i; + } + else { + my $bits = unpack('B8', $byte); + my $house_bits = substr($bits, 0, 4); + my $code_bits = substr($bits, 4, 4); + print "CM11 error, not a valid house code: $house_bits\n" unless $house = $table_hcodes2{$house_bits}; + if (substr($mask, -(++$i), 1)) { + print "CM11 error, not a valid function code: $code_bits at byte $i value $bits\n" unless $function = $table_fcodes2{$code_bits}; +# print "function=$house$function\n"; + + # Add device code back in, since this is not included in status :( + $function = $Last_Dcode . $function if $function =~ /^STATUS/; + # Handle Vehicle Interface RF Receiver extended code - assume length of 3 for extended + $extended_count = 3 if ($function eq 'Z'); +## 2.08, but 'Z' not numeric ## +## $extended_count = 3 if ($function == 'Z'); + + $data .= $house . $function; + print "CM11 db: data=$data\n" if $DEBUG; + } + else { + print "CM11 error, not a valid device code: $code_bits\n" unless $device = $table_dcodes2{$code_bits}; +# print "device=$house$device\n"; + $data .= $house . $device; + } + } +# print "byte=$byte, $bits\n"; + } + return $data; +} + +sub format_data { + my ($house_code) = @_; + + print "CM11 send data=$house_code\n" if $DEBUG; + + my ($house, $code, $house_bits, $header, $code_bits, $function, $dim_level); + my ($extended, $extended_string, $extended_checksum); + + ($house, $code) = $house_code =~ /(\S)(\S+)/; + $house = uc($house); + $code = uc($code); + + unless ($house_bits = $table_hcodes{$house}) { + print "CM11 error, invalid house code: $house. data=$house_code\n"; + return; + } + +# $code can be +# 1-9,A-G for Device code +# d_xyz. for Extended code xyz for device d +# xyz for Function codes, including +-## for bright/dim + + # Test for extended code + # - format is &P## where ## is the preset dim level + if (my($extended_data) = $code =~ /&P(\d+)/) { + unless (($extended_data >= 0) && ($extended_data < 65)) { + print "CM11 error, invalid extended code. code=$code\n"; + return; + } + $code_bits = '0111'; # Extended code + $function = '1'; # Extended transmitions are a function + $extended = '1'; + $dim_level = 0; # Dim level is not applicable to extended transmitions. + + # Hard codeded preset for now ... + + # This is not documented!! By looking at + # ActiveHome errata, it seems the device code is required + # - $Last_Dcode is a hack ... assume previous selected device. + my $extended_device = '0000' . $table_dcodes{$Last_Dcode}; + my $extended_code = '00110001'; # Type=3 => Control Modules Func=1 => Preset Receiver + + # Convert from bit to string + my $b3 = pack('B8', $extended_device); + my $b4 = pack('C1', $extended_data); + my $b5 = pack('B8', $extended_code); + $extended_string = $b3 . $b4 . $b5; + my $b3c = unpack('C', $b3); + my $b4c = unpack('C', $b4); + my $b5c = unpack('C', $b5); + $extended_checksum = $b3c + $b4c + $b5c; + if ($DEBUG) { + printf "CM11 ed=%d, b345=0x%0.2x,0x%0.2x,0x%0.2x ex=%s cs=0x%0.2x\n", + $extended_data, $b3c, $b4c, $b5c, $extended_string, + $extended_checksum; + } + } + # Test for device code + elsif ($code_bits = $table_dcodes{$code}) { + $function = '0'; + $extended = '0'; + $dim_level = 0; + $Last_Dcode = $code; # This is desperate :) + } + # Test for function code + elsif ($code_bits = $table_fcodes{$code}) { + $function = '1'; + $extended = '0'; + if ($code eq 'DIM' or $code eq 'M' or $code eq 'BRIGHT' or $code eq 'L') { + $dim_level = 34; # Lets default to 3 bight/dims to go full swing + } + elsif ($code =~ /^[+-]\d\d$/) { + $dim_level = abs($code); + } + else { + $dim_level = 0; + } + } + else { + print "CM11 error, invalid cm11 x10 code: $code\n"; + return; + } + + my $dim = int($dim_level * 22 / 100); # 22 levels = 100% + $header = substr(unpack('B8', pack('C', $dim)), 3); + + $header .= '1'; # Bit 2 is always set to a 1 to ensure synchronization + $header .= $function; # 0 for address, 1 for function + $header .= $extended; # 0 for standard, 1 for extended transmition + + # Convert from bit to string + my $b1 = pack('B8', $header); + my $b2 = pack('B8', $house_bits . $code_bits); + + # Calculate checksum + my $b1d = unpack('C', $b1); + my $b2d = unpack('C', $b2); + my $checksum = ($b1d + $b2d) & 0xff; + + my $data = $b1 . $b2; + + if ($extended) { + $data .= $extended_string; + $checksum = ($checksum + $extended_checksum) & 0xff; + } + + printf("CM11 dim=$dim header=$header hb=$house_bits cb=$code_bits " . + "bd=0x%0.2x,0x%0.2x checksum=0x%0.2x\n", + $b1d, $b2d, $checksum) if $DEBUG; + + return $data, $checksum; + +} + +sub send { + my ($serial_port, $house_code) = @_; + + if (exists $main::Debug{x10}) { + $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; + } + + my ($data_snd, $checksum) = &format_data($house_code); + return unless $data_snd; + + my $retry_cnt = 0; + RETRY: + print "CM11 send: ", unpack('H*', $data_snd), "\n" if $DEBUG; + + print "Bad cm11 data send transmition\n" unless length($data_snd) == $serial_port->write($data_snd); + + # Note: Skip the power fail check, because we the + # checksum might be the power fail flag (0xa5) + my $data_rcv = &read($serial_port, 0, 1); +# my $data_rcv; +# return unless $data_rcv = &read($serial_port, 0, 1); + + my $data_d = unpack('C', $data_rcv); + + # Unrelated incoming data ... process and re-start + # Note: Some checksums will be 0x5a or 0xa5 ... skip this test if so + if (($data_d == 0x5a or $data_d == 0xa5) and !($checksum == 0x5a or $checksum == 0xa5)) { + print "Data received while xmiting data ... will receive and retry\n"; + &receive_buffer($serial_port); + goto RETRY if $retry_cnt++ < 3; + } + + if ($checksum != $data_d) { + print "Bad checksum in cm11 send: cs1=$checksum cs2=$data_d. Will retry\n"; + goto RETRY if $retry_cnt++ < 3; + } + + print "CM11 ack\n" if $DEBUG; + my $pc_ok = pack('C', 0x00); + print "Bad cm11 acknowledge send transmition\n" unless 1 == $serial_port->write($pc_ok); + + return unless $data_rcv = &read($serial_port); + $data_d = unpack('C', $data_rcv); + + if ($data_d == 0x55) { + print "CM11 done\n" if $DEBUG; + } + # Unrelated incoming data ... process + elsif ($data_d == 0x5a or $data_d == 0xa5) { + print "Data received while xmiting data ... receive and retry\n"; + &receive_buffer($serial_port); + goto RETRY if $retry_cnt++ < 3; + } + + return $data_d; +} + +sub read { + my ($serial_port, $no_block, $no_power_fail_check) = @_; + my $data; + # Note ... for dim commands > 30, this will time out after 30*50=1.5 seconds + # No harm done, but we would rather not wait :) + my $tries = ($no_block) ? 1 : 30; + + if (exists $main::Debug{x10}) { + $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; + } + + while ($tries--) { + print "." if $DEBUG and !$no_block; + if ($data = $serial_port->input) { + my $data_d = unpack('C', $data); +# printf("rcv data=%s, %x.\n", $data, $data_d); +# my $pc_ready = pack('C', 0xc3); +# $serial_data = "$pc_ready"; +# print "serial1 out=$serial_data results=", $serial_port->write($serial_data), ".\n" if $DEBUG; + printf("\nCM11 data=%s hex=%0.2lx\n", $data_d, $data_d) if $DEBUG; + + # If we received the power-fail string (0xa5), reset with a blank macro command + # - Protocol.txt says to send macros string, but that did not work. + if ($data_d == 165 and !$no_power_fail_check) { + + print "\nCM11 power fail detected."; + &setClock($serial_port); + # We can use this to detect a power failure + $POWER_RESET = 1; # The user code will be responsible for reseting this + } + + return $data; + } + # If we do not do this, we may get endless error messages. + else { + $serial_port->reset_error; + } + + if ($tries) { + select undef, undef, undef, 50 / 1000; + } + } + + print "No data received from cm11\n" if ($DEBUG and !$no_block); + return undef; +} + +sub dim_level_decode { + my ($code) = @_; + + my %table_hcodes = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101 + I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100); + my %table_dcodes = qw(1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101 + 9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100 + A 1111 B 0011 C 1011 D 0000 E 1000 F 0100 G 1100); + + if (exists $main::Debug{x10}) { + $DEBUG = ($main::Debug{x10} >= 1) ? 1 : 0; + } + + # Convert bit string to decimal + my $level_b = $table_hcodes{substr($code, 0, 1)} . $table_dcodes{substr($code, 1, 1)}; + my $level_d = unpack('C', pack('B8', $level_b)); + # Varies from 36 to 201, by 11, then to 210 as a max. + # 16 different values. Round to nearest 5%, max of 95. + my $level_p = int(100 * $level_d / 211); # Do not allow 100% ... not a valid state? + ## print "CM11 debug1: levelb=$level_b level_p=$level_p\n" if $DEBUG; + $level_p = $level_p - ($level_p % 5); + print "CM11 debug: dim_code=$code leveld=$level_d level_p=$level_p\n" if $DEBUG; + return $level_p; +} + + +sub reset_cm11 { + return unless ( 1 == @_ ) ; # requires port number to reset + &enable_RI ( @_ ); + &setClock ( @_ ); +} + + # This currently gets bad checksums :( + # On windows, it gives Parity Errors. +sub enable_RI { + my ($serial_port) = @_; + my $ri_on = 0xeb; + my $ack = 0x00; + my $done = 0x55; + my $checksum; + + # Send RI Enable code to CM11 + $serial_port->input; + $serial_port->write(pack('C',$ri_on)); + do { + $checksum = $serial_port->input; + } until $checksum; + + if ( $checksum ne pack('C',$ri_on) ) { + print "Checksum error in enabling RI: ", unpack('H2',$checksum),"\n"; + return $checksum; + } + + # Tell the CM11 to do it + $serial_port->write(pack('C',$ack)); + + do { + $checksum = $serial_port->input; + } until $checksum; + + if ( $checksum ne pack('C',$done) ) { + print "CM11 failed to properly acknowledge execution of RI_Enable\n"; + } + return $checksum; +} + +#55 to 48 timer download header (0x9b) +#47 to 40 Current time (seconds) +#39 to 32 Current time (minutes ranging from 0 to 119) +#31 to 23 Current time (hours/2, ranging from 0 to 11) +#23 to 16 Current year day (bits 0 to 7) +#15 Current year day (bit 8) +#14 to 8 Day mask (SMTWTFS) +#7 to 4 Monitored house code +#3 Reserved +#2 Battery timer clear flag +#1 Monitored status clear flag +#0 Timer purge flag + +sub setClock { + my ($serial_port) = @_; +# $DEBUG=1; + my ($Second, $Minute, $Hour, $Mday, $Month, $Year, $Wday, $Yday) = localtime time; + my $localtime = localtime time; + print "Reseting time with: $localtime\n" if $DEBUG; +# $Wday = 2 ** (7 - $Wday); +# if ($Yday > 255) { +# $Yday -= 256; +# $Wday *= 2; +# } + # Manipulate Minutes to be 0 - 119 (2 hours) and Hours to be 0 - 11 + $Minute += (($Hour % 2) * 60); + $Hour /= 2; + # Must do some weird packing of data for Yday and Wday fields. + my $Yday1 = $Yday % 256; # mantisa of Yday + my $Yday2 = ($Yday / 256) << 7; # Radius of Yday shifted over 7 bits + my $Dmask = 0x01 << $Wday; # Day mask of SMTWTFS + $Yday2 |= $Dmask; # OR the two fields together to get one + my $CodeF = 0x06 << 4; # Put "A" housecode in upper nibble + $CodeF |= 0x07; # Put 0b0111 in lower nibble (battery,monitor, & timer cleared) + my $power_reset = pack('C7', + 0x9b, + $Second, + $Minute, + $Hour, + $Yday1, + $Yday2, + $CodeF); +# $Wday, +# 0x03); # Not sure what is best here. x10d.c did this. + + my $results = $serial_port->write($power_reset); + select undef, undef, undef, 50 / 1000; + my $checksum = $serial_port->input; # Receive, but ignore, checksum + if ($DEBUG) { + printf "\npower_reset: %s %s %s %s %s %s %s\n", + unpack ('H2H2H2H2H2H2H2', $power_reset); + print " sent $results bytes\n"; + printf " checksum = %x\n", ord($checksum); + } + my $pc_ok = pack('C', 0x00); + print "Bad cm11 checksum acknowledge\n" unless 1 == $serial_port->write($pc_ok); +} + +sub ping { + my ($serial_port) = @_; + my $ri_on = 0xeb; + my $ack = 0x00; + my $done = 0x55; + my $checksum; + my $counter; + my $maxcounter = 10000; + + # Send RI Enable code to CM11 + $serial_port->write(pack('C',$ri_on)); + + $counter = 0; + do { + $checksum = $serial_port->input; + $counter++; + } until (($checksum) || ($counter == $maxcounter)); + + return 0 if ($counter == $maxcounter); + + print "cm11::ping - checksum: got: 0x", unpack('H2',$checksum),"\n" if $DEBUG; + + if (($checksum ne pack('C',$ri_on)) && $DEBUG) { + print "cm11::ping checksum: expected 0x",unpack('H2',pack('C',$ri_on)),". got: 0x", unpack('H2',$checksum),")\n"; + } + + # 0x5a is sent by the CM11 if it has data waiting. If we get this then the CM11 is obviously alive. + if ($checksum eq pack('C',0x5a)) { + print "cm11::ping - cm11 has data waiting!\n" if $DEBUG; + return 1; + } + + $serial_port->write(pack('C',$ack)); + + $counter = 0; + do { + $checksum = $serial_port->input; + $counter++; + } until (($checksum) || ($counter == $maxcounter)); + + print "cm11::ping - checksum: got: 0x", unpack('H2',$checksum),"\n" if $DEBUG; + + if (($checksum ne pack('C',$done)) && $DEBUG) { + print "cm11::ping - checksum: expected 0x",unpack('H2',pack('C',$done)),". got: 0x", unpack('H2',$checksum),")\n"; + } + + print "cm11::ping - counter=$counter\n" if $DEBUG; + return 1; +} + + +return 1; # for require +__END__ + +=pod + +=head1 NAME + +ControlX10::CM11 - Perl extension for X10 'ActiveHome' Controller + +=head1 SYNOPSIS + + use ControlX10::CM11; + + # $serial_port is an object created using Win32::SerialPort + # or Device::SerialPort depending on OS + # my $serial_port = setup_serial_port('COM10', 4800); + + $data = &ControlX10::CM11::receive_buffer($serial_port); + $data = &ControlX10::CM11::read($serial_port, $no_block); + $percent = &ControlX10::CM11::dim_level_decode('GE'); # 40% + + &ControlX10::CM11::send($serial_port, 'A1'); # Address device A1 + &ControlX10::CM11::send($serial_port, 'AJ'); # Turn device ON + # House Code 'A' present in both send() calls + + &ControlX10::CM11::send($serial_port, 'B'.'ALL_OFF'); + # Turns All lights on house code B off + +=head1 DESCRIPTION + +The CM11A is a bi-directional X10 controller that connects to a serial +port and transmits commands via AC power line to X10 devices. This +module translates human-readable commands (eg. 'A2', 'AJ') into the +Interface Communication Protocol accepted by the CM11A. + +=over 4 + +=item send command + +This transmits a two-byte message containing dim and house information +and either an address or a function. Checksum and acknowledge handshaking +is automatic. The command accepts a string parameter. The first character +in the string must be a I in the range [A..P] and the rest of +the string determines the type of message. Intervening whitespace is not +currently permitted between the I and the I. This +may change in the future. + + STRING ALTERNATE_STRING FUNCTION + 1..9 Unit Address + A..G Unit Address + J ON Turn Unit On + K OFF Turn Unit Off + L BRIGHT Brighten Last Light Programmed 5% + M DIM Dim Last Light Programmed 5% + O ALL_ON All Units On + P ALL_OFF All Units Off + +There are also functions without "shortcut" letter commands: + + ALL_LIGHTS_OFF EXTENDED_CODE EXTENDED_DATA + HAIL_REQUEST HAIL_ACK PRESET_DIM1 + PRESET_DIM2 STATUS_ON STATUS_OFF + STATUS + +Dim and Bright functions can also take a signed value in the +range [-95,-90,...,-10,-5,+5,+10,...,+90,+95]. + + ControlX10::CM11::send($serial_port,'A1'); # Address device A1 + ControlX10::CM11::send($serial_port,'AJ'); # Turn device ON + ControlX10::CM11::send($serial_port,'A-25'); # Dim to 25% + +=item send extended function + +Starting in version 2.04, extended commands may be sent to devices that +support the enhanced X10 protocol. If you have one of the newer (more +expensive) LM14A/PLM21 2 way X10 pro lamp modules, you can set it directly +to a specific brightness level using a Preset Dim extended code. + +The 64 extended X10 Preset Dim codes are commanded by appending C<&##> to +the unit address where C<##> is a number between 1 and 63. + + ControlX10::CM11::send($serial_port,'A5'); # Address A5 + ControlX10::CM11::send($serial_port,'A&P16'); # Dim to 25% + +A partial translation list for the most important levels: + + &P## % &P## % &P## % + 0 0 13 20 44 70 + 1 2 16 25 47 75 + 2 4 19 30 50 80 + 3 5 25 40 57 90 + 6 10 31 50 61 95 + 9 15 38 60 63 100 + +There is another set of Preset Dim commands that are used by some modules +(e.g. the RCS TX15 thermostat). These 32 non-extended Preset Dim codes can +be coded directly, using the following table: + + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PRESET_DIM1 + M N O P C D A B E F G H K L I J + + 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 PRESET_DIM2 + M N O P C D A B E F G H K L I J + +This usage, and the responses assigned to each command, are device specific. +For example, the following commands enable preset value 18: + + ControlX10::CM11::send($serial_port,'M4'); # Address thermostat + ControlX10::CM11::send($serial_port,'OPRESET_DIM2'); # Select preset 18 + + +Starting in version 2.07, incoming extended data is also processed. +The first character will be the I in the range [A..P]. +The next character will be I, indicating extended data. +The remaining data will be the extended data. + + +=item read + +This checks for an incoming transmission. It will return "" for no input. +It also tests for a received a "power fail" message (0xa5). If it detects +one, it automatically sends the command/data to reset the CM11 clock. If +the C<$no_block> parameter is FALSE (0, "", or undef), the B will retry +for up to a second at 50 millisecond intervals. With C<$no_block> TRUE, +the B checks one time for available data. + + $data = &ControlX10::CM11::read($serial_port, $no_block); + +=item receive_buffer + +This command handles the upload response to an "Interface Poll Signal" +message (0x5a) B from the CM11. The module sends "ready" (0xc3) and +receives up to 10 bytes. The first two bytes are size and description of +the remaining bytes. These are used to decode the data bytes, but are not +returned by the B function. Each of the data bytes is +decoded as if it was a B command from an external CM11 or equivalent +external source (such as an RF keypad). + + $data = &ControlX10::CM11::receive_buffer($serial_port); + # $data eq "A2AK" after an external device turned off A2 + +Multiple house and unit addresses can appear in a single buffer. + + if ($data eq "B1BKA2AJ") { + print "B1 off, A2 on\n"; + } + +=item dim_level_decode + +When the external command includes dim/bright information in addition to +the address and function, the B function converts that +data byte (as processed by the B command) into percent. + + $data = &ControlX10::CM11::receive_buffer($serial_port); + # $data eq "A2AMGE" after an external device dimmed A2 to 40% + $percent = &ControlX10::CM11::dim_level_decode("GE"); + # $percent == 40 + +A more complex C<$data> input is possible. + + if ($data eq "B1B3B5B7B9BLLE") { + print "House B Inputs 1,3,5,7,9 Brightened to 85%\n"; + } + +The conversion between text_data and percent makes more sense to the code +than to humans. The following table gives representative values. Others +may be received from a CM11 and will be properly decoded. + + Percent Text Percent Text + 0 M7 50 AA + 5 ED 55 I6 + 10 EC 60 NF + 15 C7 65 N2 + 20 KD 70 F6 + 25 K4 75 DB + 30 O7 80 D2 + 35 OA 85 LE + 40 G6 90 PB + 45 AF 95 P8 + +=back + +=head1 EXPORTS + +The B, B, B, and B +functions are exported by default starting with Version 2.09. +They are identical to the "fully-qualified" names and accept the same +parameters. The I tag C<:FUNC> is maintained for +compatibility (but deprecated). + + use ControlX10::CM11; + send_cm11($serial_port, 'A1'); # send() - address + send_cm11($serial_port, 'AJ'); # send() - function + $data = receive_cm11($serial_port); # receive_buffer() + $data = read_cm11($serial_port, $no_block); # read() + $percent = dim_decode_cm11('GE'); # dim_level_decode() + +=head1 AUTHORS + +Bruce Winter bruce@misterhouse.net http://misterhouse.net + +CPAN packaging by Bill Birthisel wcbirthisel@alum.mit.edu +http://members.aol.com/bbirthisel + +=head2 MAILING LISTS + +General information about the mailing lists is at: + + http://lists.sourceforge.net/mailman/listinfo/misterhouse-users + http://lists.sourceforge.net/mailman/listinfo/misterhouse-announce + +To post to this list, send your email to: + + misterhouse-users@lists.sourceforge.net + +If you ever want to unsubscribe or change your options (eg, switch to +or from digest mode, change your password, etc.), visit your +subscription page at: + + http://lists.sourceforge.net/mailman/options/misterhouse-users/$user_id + +=head1 SEE ALSO + +mh can be download from http://misterhouse.net + +Win32::SerialPort and Device::SerialPort from CPAN + +CM11A Protocol documentation available at http://www.x10.com + +perl(1). + +=head1 COPYRIGHT + +Copyright (C) 2000 Bruce Winter. All rights reserved. + +This module is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. 30 January 2000. + +=cut + +# +# $Log: CM11.pm,v $ +# Revision 2.22 2004/09/25 20:01:20 winter +# *** empty log message *** +# +# Revision 2.21 2004/06/06 21:38:44 winter +# *** empty log message *** +# +# Revision 2.20 2003/12/22 00:25:06 winter +# - 2.86 release +# +# Revision 2.19 2003/11/23 20:26:02 winter +# - 2.84 release +# +# Revision 2.18 2003/03/09 19:34:42 winter +# - 2.79 release +# +# Revision 2.17 2002/11/10 01:59:57 winter +# - 2.73 release +# +# Revision 2.16 2002/03/02 02:36:51 winter +# - 2.65 release +# +# Revision 2.15 2001/02/04 20:31:31 winter +# - 2.43 release +# +# Revision 2.14 2001/01/20 17:47:50 winter +# - 2.41 release +# +# Revision 2.13 2000/10/01 23:29:41 winter +# - 2.29 release +# +# Revision 2.12 2000/08/19 01:25:09 winter +# - 2.27 release +# +# Revision 2.11 2000/04/22 00:11:15 winter +# - increase receive buffer delay from 40 to 80 +# +# Revision 2.10 2000/02/12 06:11:37 winter +# - commit lots of changes, in preperation for mh release 2.0 +# +# Revision 2.08 2000/01/29 20:07:01 winter +# - add $no_power_fail_check. +# +# diff --git a/lib/site/MSN.pm b/lib/site/MSN.pm index 45a1309ba..997289a23 100644 --- a/lib/site/MSN.pm +++ b/lib/site/MSN.pm @@ -1,835 +1,835 @@ -#================================================ -package MSN; -#================================================ - -=head1 MSN v2.0 - -=cut - -use strict; -use warnings; - -# IO -use IO::Select; - -use MSN::Notification; -use MSN::SwitchBoard; -use MSN::Util; - -use constant CVER10 => '0x0409 winnt 5.0 i386 MSNMSGR 6.1.0203 MSMSGS '; -use constant VER => 'MSNP10 MSNP9 CVR0'; - -my $REVISION = '$Rev: 84 $'; -$REVISION =~ s/\$//g; -my $VER = 'MSNP10 MSNP9 CVR0'; - -# print out the version and checksum -my $strVERSION = "MSN 2.0 (01/21/2005) $REVISION"; -sub checksum { my $o = tell(DATA); seek DATA,0,0; local $/; my $t = unpack("%32C*",) % 65535;seek DATA,$o,0; return $t;}; -print $strVERSION . " - Checksum: " . checksum() . "-NS" . MSN::Notification::checksum() . "-SB" . MSN::SwitchBoard::checksum() . "\n\n"; - - -=head2 Methods - -=item -new - -Creates an instance of the MSN object used to communicate with MSN Servers. - -=cut - -sub new -{ - my $class = shift; - - my $self = - { - Host => 'messenger.hotmail.com', - Port => 1863, - Handle => '', - Password => '', - Debug => 0, - ServerError => 1, - Error => 1, - AutoloadError => 0, - CMDError => 0, - ShowTX => 0, - ShowRX => 0, - AutoReconnect => 1, - Select => new IO::Select(), - Notification => undef, - Connections => {}, - Connected => 0, - Status => 'NLN', - LastError => '', - MessageStyle => { Font => "MS Shell Dlg", - Effect => "", - Color => "000000", - CharacterSet => 0, - PitchFamily => 0 - }, - ClientID => 536870920, - ClientCaps => { }, - @_ - }; - bless( $self, $class ); - - return $self; -} - -sub DESTROY -{ - my $self = shift; - - # placeholder for possible destructor code -} - -sub AUTOLOAD -{ - my $self = shift; - - $self->error( "method $MSN::AUTOLOAD not defined" ) if( $self->{AutoloadError} ); -} - -sub toggle -{ - my $self = shift; - my $flag = shift || return $self->error( "Flag is missing" ); - - return $self->error( "Unknown flag (check spelling or case?)" ) if( $flag !~ /^Debug|ServerError|Error|AutoloadError|CMDError|ShowTX|ShowRX|AutoReconnect$/ ); - - $self->{$flag} = !$self->{$flag}; - - return 1; -} - -sub setKey -{ - my $self = shift; - my $key = shift || return $self->error( "Key is missing" ); - my $value = shift; - - if( $key =~ /^Debug|ServerError|Error|AutoloadError|CMDError|ShowTX|ShowRX|AutoReconnect$/ ) - { - return $self->error( "Invalid value for $key (should be 0 or 1)" ) if( $value != 1 && $value != 0 ); - $self->{$key} = $value; - } - elsif( $key =~ /^PingIncrement|NoPongMax$/ ) - { - return $self->error( "Invalid value for $key (should be greater than 0)" ) if( $value <= 0 ); - $self->{Notification}->{$key} = $value; - } - else - { - return $self->error( "Unknown key (check spelling or case?)" ); - } - - return 1; -} - -sub debug -{ - my $self = shift; - my $message = shift || ''; - - if( defined $self->{handler}->{Debug} ) - { - $self->call_event( $self, 'Debug', $message ); - } - elsif( $self->{Debug} ) - { - print( "$message\n" ); - } - - return 1; -} - -sub serverError -{ - my $self = shift; - my $message = shift || ''; - - if( defined $self->{handler}->{ServerError} ) - { - $self->call_event( $self, 'ServerError', $message ); - } - elsif( $self->{ServerError} ) - { - print( "SERVER ERROR : $message\n" ); - } - - return 0; -} - -sub error -{ - my $self = shift; - my $message = shift || ''; - - $self->{LastError} = $message; - - if( defined $self->{handler}->{Error} ) - { - $self->call_event( $self, 'Error', $message ); - } - elsif( $self->{Error} ) - { - print( "ERROR : $message\nCaller trace:\n" ); - - for( my $i=0; $i<20; $i++ ) - { - my ($package, $filename, $line, $subroutine, @more ) = caller($i); - last if( !defined $package ); - $filename =~ s/.*MSN/MSN/gi; - print( " $i: $subroutine ($filename, line $line)\n" ); - } - } - - return 0; -} - -sub cmdError -{ - my $self = shift; - my $message = shift || ''; - - print( "UNDEFINED CMD : $message\n" ) if( $self->{CMDError} ); - - return 0; -} - -sub getLastError -{ - my $self = shift; - - return $self->{LastError}; -} - - -=item -connect - -Connect to MSN. Call this after your object is created and your event handlers are set. - -=cut - -sub connect -{ - my $self = shift; - - $self->debug( "Connecting to $self->{Host}:$self->{Port} as $self->{Handle}/$self->{Password}" ); - - $self->{Notification} = new MSN::Notification( $self, $self->{Host}, $self->{Port}, $self->{Handle}, $self->{Password} ); - - if( $self->{Notification}->connect() ) - { - $self->{Connected} = time; - } -} - -=item -disconnect - -Disconnect from MSN. - -=cut - -sub disconnect -{ - my $self = shift; - - foreach my $convo (values %{$self->getConvoList()}) - { - $convo->leave(); - } - - $self->{Notification}->disconnect(); - - $self->{Connected} = 0; - - return 1; -} - -=item -isConnected() - -Checks if the connection is active. - -=cut - -sub isConnected -{ - my $self = shift; - - return $self->{Connected}; -} - -=item -uptime() - -Get the current uptime in seconds (since the last connection). - -=cut - -sub uptime -{ - my $self = shift; - - return ($self->{Connected}) ? (time - $self->{Connected}) : 0; -} - -#================================================ -# Set and Get methods -#================================================ - -=item -setName - -Set the display name. - -=cut - -sub setName -{ - my $self = shift; - - return $self->{Notification}->setName( @_ ); -} - -=item -setDisplayPicture($file) - -Set the display picture. This must be passed a png file and resets your status to NLN so that your Display picture gets sent out. - -=cut - -sub setDisplayPicture -{ - my $self = shift; - - return $self->{Notification}->setDisplayPicture( @_ ); -} - -=item -setMessageStyle(%hash) - -Set the default style information (font, effect, etc) for sending messages. - -=cut - -sub setMessageStyle -{ - my $self = shift; - - $self->{MessageStyle} = { (%{$self->{MessageStyle}}), @_ }; -} - -=item -getMessageStyle() - -Get the default style information (font, effect, etc) for sending messages. - -=cut - -sub getMessageStyle -{ - my $self = shift; - - return $self->{MessageStyle}; -} - -=item -setClientInfo(%hash) - -Set the client info. This is the client id and flags that make up the cid. - -=cut - -sub setClientInfo -{ - my $self = shift; - my %info = @_; - - $self->{ClientID} = MSN::Util::convertToCid( %info ); - - return 1; -} - -=item -getClientInfo() - -Get the client info. This is the client id and flags that make up the cid. - -=cut - -sub getClientInfo -{ - my $self = shift; - - return MSN::Util::convertFromCid( $self->{ClientID} ); -} - -=item -setClientCaps(%hash) - -Set the client caps. These are the x-clientcaps data. - -=cut - -sub setClientCaps -{ - my $self = shift; - my %caps = @_; - - $self->{ClientCaps} = \%caps; - - return 1; -} - -=item -getClientCaps() - -Get the client caps. These are the x-clientcaps data. - -=cut - -sub getClientCaps -{ - my $self = shift; - - return $self->{ClientCaps}; -} - -=item -setStatus - -Set the status. - -=cut - -sub setStatus -{ - my $self = shift; - - return $self->{Notification}->setStatus( @_ ); -} - -#================================================ -# Contact methods -#================================================ - -=item -blockContact($email) - -Puts $email on your block list. - -=cut - -sub blockContact -{ - my $self = shift; - - return $self->{Notification}->blockContact( @_ ); -} - -=item -unblockContact($email) - -Removes $email from your block list. - -=cut - -sub unblockContact -{ - my $self = shift; - - return $self->{Notification}->unblockContact( @_ ); -} - -=item -addContact($email) - -Puts $email on your contact list. This allows you to recieve status messages about this individual. - -=cut - -sub addContact -{ - my $self = shift; - - return $self->{Notification}->addContact( @_ ); -} - -=item -remContact($email) - -Removes $email from your contact list. - -=cut - -sub remContact -{ - my $self = shift; - - return $self->{Notification}->remContact( @_ ); -} - -=item -allowContact($email) - -Puts $email on your allow list. This is generaly automatic but there might be some cases where it is useful. -If you do not want to automatically allow contacts to see you online, you can set a handler for the "ContactAddingUs" -event and return 0. - -=cut - -sub allowContact -{ - my $self = shift; - - return $self->{Notification}->allowContact( @_ ); -} - -=item -disallowContact($email) - -Removes $email from your allow list. They will no longer be able to see you or talk to you. - -=cut - -sub disallowContact -{ - my $self = shift; - - return $self->{Notification}->disallowContact( @_ ); -} - -=item -getContactList($list) - -Expects $list to be one of FL, RL, AL, or BL. Returns the email addresses on said list. - -=cut - -sub getContactList -{ - my $self = shift; - - return $self->{Notification}->getContactList( @_ ); -} - -=item -getContact($email) - -Returns a hash containing all known info for this contact. - -=cut - -sub getContact -{ - my $self = shift; - - return $self->{Notification}->getContact( @_ ); -} - -=item -getContactName($email) - -Returns the friendly name used by this contact, if they are on your FL list. - -=cut - -sub getContactName -{ - my $self = shift; - - return $self->{Notification}->getContactName( @_ ); -} - -=item -getContactStatus($email) - -Returns the status of this contact, if they are on your FL list. - -=cut - -sub getContactStatus -{ - my $self = shift; - - return $self->{Notification}->getContactStatus( @_ ); -} - -=item -getContactClientInfo($email) - -Returns the client info of this contact, if they are on your FL list. - -=cut - -sub getContactClientInfo -{ - my $self = shift; - - return $self->{Notification}->getContactClientInfo( @_ ); -} - -#================================================ -# Other -#================================================ - -=item -findMember($email) - -Looks for a member in an active SwitchBoard and returns the SB or undef, if not found. - -=cut - -sub findMember -{ - my $self = shift; - my $email = shift || ''; - - foreach my $convo (values %{$self->getConvoList()}) - { - my $members = $convo->getMembers(); - - return $convo if( defined $members->{$email} ); - } - - return undef; -} - -=item -addEmoticon($shortcut, $filename) - -Adds an emoticon to your connection. This loads the file and prepares it. Anytime you use the text form $shortcut in an outgoing message it will be replaced with the appropriate emoticon. You can only use 5 different emoticons per message. - -=cut - -sub addEmoticon -{ - my $self = shift; - - return return $self->{Notification}->addEmoticon( @_ ); -} - -=item -broadcast($msg,%style) - -Broadcasts the message to all open conversations. - -=cut - -sub broadcast -{ - my $self = shift; - my @data = @_; # probably don't have to do this, but just to be safe - - foreach my $convo (values %{$self->getConvoList()}) - { - $convo->sendMessage( @data ); - } -} - -=item -call($email,$msg,%style) - -Calls the contact, starting a conversation with them. - -=cut - -sub call -{ - my $self = shift; - - print "db in call for $self\n"; - - $self->{Notification}->call( @_ ); -} - -sub isOnline -{ - my $self = shift; - -} - -=item -do_one_loop() - -Process a single cycle's worth of incoming and outgoing messages. This should be done at a regular intervals, preferably under a second. - -=cut - -sub do_one_loop -{ - my $self = shift; - - # return immediately if we are not connected - return if( !$self->{Connected} ); - - $self->{Notification}->ping( ); - - foreach my $convo (values %{$self->getConvoList()}) - { - $convo->p2pSendOne() if $convo->p2pWaiting; - } - - my @ready = $self->{Select}->can_read(.1); - foreach my $fh ( @ready ) - { - # get the filenumber for this filehandle - my $fn = $fh->fileno; - - # get the object assocatied with this filenumber - my $connection = $self->{Connections}->{$fn}; - - # DO WE NEED THIS CODE? if the connection is really dead, will it even be showing up in the list of filehandles that can be read from?? - # if the connection is dead, remove it from the select, delete it from the Connections list and output a warn - if( !$connection->{Socket}->connected() ) - { - $self->{Select}->remove( $fn ); - delete( $self->{Connections}->{fn} ); - warn "Killing dead socket"; - next; - } - - sysread( $fh, $connection->{buf}, 2048, length( $connection->{buf} || '' ) ); - - while( $connection->{buf} =~ s/^(.*?\n)// ) - { - $connection->{line}= $1; - my $incomingdata = $connection->{line}; - $incomingdata =~ s/[\r\n]//g; - - print( "($fn $connection->{Type}) RX: $incomingdata\n" ) if( $self->{ShowRX} ); - - my $result = $connection->dispatch( $incomingdata ); - last if( $result && $result eq "wait" ); - } - } -} - - -=item -setHandler($event, $handler) - -$event should be an event listed in the events section. These are called based on information sent by MSN, -receiving a message is an event, status changes are events, getting a call is an event, etc. - - $msn->setHandler( Connected => \&connected ); - - sub connected { - my $self = shift; - print "Yay we connected"; - } - -=cut - -sub setHandler -{ - my $self = shift; - my ($event, $handler) = @_; - - $self->{handler}->{$event} = $handler; -} - -=item -setHandlers( $event1 => $handler1, $event2 => $handler2) - -Expects a list of events and handlers. - - my $msn = new MSN; - $msn->setHandlers( Connected => \&connected, - Disconnected => \&disconnected ); - -=cut - -sub setHandlers -{ - my $self = shift; - my $handlers = { @_ }; - for my $event (keys %$handlers) - { - $self->setHandler( $event, $handlers->{$event} ); - } -} - -sub call_event -{ - my $self = shift; - my $receiver = shift; - my $event = shift; - - # get and run the handler if it is defined - my $function = $self->{handler}->{$event}; - return &$function( $receiver, @_ ) if( defined $function ); - - # get and run the default handler if it is defined - $function = $self->{handler}->{Default}; - return &$function( $receiver, $event, @_ ) if( defined $function ); - - return undef; -} - -=item -getNotification - -Returns the MSN::Notification object if you have a need to interact with it directly. - -=cut - -sub getNotification -{ - my $self = shift; - - return $self->{Notification}; -} - -=item -getConvoList - -Returns a hash of conversations (MSN::SwitchBoard objects) keyed by file number of the socket they are on. - -=cut - -sub getConvoList -{ - my $self = shift; - - my $convos = {}; - - foreach my $fn (keys %{$self->{Connections}} ) - { - if( $self->{Connections}->{$fn}->getType() eq 'SB' ) - { - $convos->{$fn} = $self->{Connections}->{$fn}; - } - } - - return $convos; -} - -=item -getConvo - -Returns a conversation (MSN::SwitchBoard object) found by socket number. - -=cut - -sub getConvo -{ - my $self = shift; - my $key = shift; - - if( defined $self->{Connections}->{$key} && $self->{Connections}->{$key}->getType() eq 'SB' ) - { - return $self->{Connections}->{$key}; - } - - return undef; -} - - -return 1; -__DATA__ +#================================================ +package MSN; +#================================================ + +=head1 MSN v2.0 + +=cut + +use strict; +use warnings; + +# IO +use IO::Select; + +use MSN::Notification; +use MSN::SwitchBoard; +use MSN::Util; + +use constant CVER10 => '0x0409 winnt 5.0 i386 MSNMSGR 6.1.0203 MSMSGS '; +use constant VER => 'MSNP10 MSNP9 CVR0'; + +my $REVISION = '$Rev: 84 $'; +$REVISION =~ s/\$//g; +my $VER = 'MSNP10 MSNP9 CVR0'; + +# print out the version and checksum +my $strVERSION = "MSN 2.0 (01/21/2005) $REVISION"; +sub checksum { my $o = tell(DATA); seek DATA,0,0; local $/; my $t = unpack("%32C*",) % 65535;seek DATA,$o,0; return $t;}; +print $strVERSION . " - Checksum: " . checksum() . "-NS" . MSN::Notification::checksum() . "-SB" . MSN::SwitchBoard::checksum() . "\n\n"; + + +=head2 Methods + +=item +new + +Creates an instance of the MSN object used to communicate with MSN Servers. + +=cut + +sub new +{ + my $class = shift; + + my $self = + { + Host => 'messenger.hotmail.com', + Port => 1863, + Handle => '', + Password => '', + Debug => 0, + ServerError => 1, + Error => 1, + AutoloadError => 0, + CMDError => 0, + ShowTX => 0, + ShowRX => 0, + AutoReconnect => 1, + Select => new IO::Select(), + Notification => undef, + Connections => {}, + Connected => 0, + Status => 'NLN', + LastError => '', + MessageStyle => { Font => "MS Shell Dlg", + Effect => "", + Color => "000000", + CharacterSet => 0, + PitchFamily => 0 + }, + ClientID => 536870920, + ClientCaps => { }, + @_ + }; + bless( $self, $class ); + + return $self; +} + +sub DESTROY +{ + my $self = shift; + + # placeholder for possible destructor code +} + +sub AUTOLOAD +{ + my $self = shift; + + $self->error( "method $MSN::AUTOLOAD not defined" ) if( $self->{AutoloadError} ); +} + +sub toggle +{ + my $self = shift; + my $flag = shift || return $self->error( "Flag is missing" ); + + return $self->error( "Unknown flag (check spelling or case?)" ) if( $flag !~ /^Debug|ServerError|Error|AutoloadError|CMDError|ShowTX|ShowRX|AutoReconnect$/ ); + + $self->{$flag} = !$self->{$flag}; + + return 1; +} + +sub setKey +{ + my $self = shift; + my $key = shift || return $self->error( "Key is missing" ); + my $value = shift; + + if( $key =~ /^Debug|ServerError|Error|AutoloadError|CMDError|ShowTX|ShowRX|AutoReconnect$/ ) + { + return $self->error( "Invalid value for $key (should be 0 or 1)" ) if( $value != 1 && $value != 0 ); + $self->{$key} = $value; + } + elsif( $key =~ /^PingIncrement|NoPongMax$/ ) + { + return $self->error( "Invalid value for $key (should be greater than 0)" ) if( $value <= 0 ); + $self->{Notification}->{$key} = $value; + } + else + { + return $self->error( "Unknown key (check spelling or case?)" ); + } + + return 1; +} + +sub debug +{ + my $self = shift; + my $message = shift || ''; + + if( defined $self->{handler}->{Debug} ) + { + $self->call_event( $self, 'Debug', $message ); + } + elsif( $self->{Debug} ) + { + print( "$message\n" ); + } + + return 1; +} + +sub serverError +{ + my $self = shift; + my $message = shift || ''; + + if( defined $self->{handler}->{ServerError} ) + { + $self->call_event( $self, 'ServerError', $message ); + } + elsif( $self->{ServerError} ) + { + print( "SERVER ERROR : $message\n" ); + } + + return 0; +} + +sub error +{ + my $self = shift; + my $message = shift || ''; + + $self->{LastError} = $message; + + if( defined $self->{handler}->{Error} ) + { + $self->call_event( $self, 'Error', $message ); + } + elsif( $self->{Error} ) + { + print( "ERROR : $message\nCaller trace:\n" ); + + for( my $i=0; $i<20; $i++ ) + { + my ($package, $filename, $line, $subroutine, @more ) = caller($i); + last if( !defined $package ); + $filename =~ s/.*MSN/MSN/gi; + print( " $i: $subroutine ($filename, line $line)\n" ); + } + } + + return 0; +} + +sub cmdError +{ + my $self = shift; + my $message = shift || ''; + + print( "UNDEFINED CMD : $message\n" ) if( $self->{CMDError} ); + + return 0; +} + +sub getLastError +{ + my $self = shift; + + return $self->{LastError}; +} + + +=item +connect + +Connect to MSN. Call this after your object is created and your event handlers are set. + +=cut + +sub connect +{ + my $self = shift; + + $self->debug( "Connecting to $self->{Host}:$self->{Port} as $self->{Handle}/$self->{Password}" ); + + $self->{Notification} = new MSN::Notification( $self, $self->{Host}, $self->{Port}, $self->{Handle}, $self->{Password} ); + + if( $self->{Notification}->connect() ) + { + $self->{Connected} = time; + } +} + +=item +disconnect + +Disconnect from MSN. + +=cut + +sub disconnect +{ + my $self = shift; + + foreach my $convo (values %{$self->getConvoList()}) + { + $convo->leave(); + } + + $self->{Notification}->disconnect(); + + $self->{Connected} = 0; + + return 1; +} + +=item +isConnected() + +Checks if the connection is active. + +=cut + +sub isConnected +{ + my $self = shift; + + return $self->{Connected}; +} + +=item +uptime() + +Get the current uptime in seconds (since the last connection). + +=cut + +sub uptime +{ + my $self = shift; + + return ($self->{Connected}) ? (time - $self->{Connected}) : 0; +} + +#================================================ +# Set and Get methods +#================================================ + +=item +setName + +Set the display name. + +=cut + +sub setName +{ + my $self = shift; + + return $self->{Notification}->setName( @_ ); +} + +=item +setDisplayPicture($file) + +Set the display picture. This must be passed a png file and resets your status to NLN so that your Display picture gets sent out. + +=cut + +sub setDisplayPicture +{ + my $self = shift; + + return $self->{Notification}->setDisplayPicture( @_ ); +} + +=item +setMessageStyle(%hash) + +Set the default style information (font, effect, etc) for sending messages. + +=cut + +sub setMessageStyle +{ + my $self = shift; + + $self->{MessageStyle} = { (%{$self->{MessageStyle}}), @_ }; +} + +=item +getMessageStyle() + +Get the default style information (font, effect, etc) for sending messages. + +=cut + +sub getMessageStyle +{ + my $self = shift; + + return $self->{MessageStyle}; +} + +=item +setClientInfo(%hash) + +Set the client info. This is the client id and flags that make up the cid. + +=cut + +sub setClientInfo +{ + my $self = shift; + my %info = @_; + + $self->{ClientID} = MSN::Util::convertToCid( %info ); + + return 1; +} + +=item +getClientInfo() + +Get the client info. This is the client id and flags that make up the cid. + +=cut + +sub getClientInfo +{ + my $self = shift; + + return MSN::Util::convertFromCid( $self->{ClientID} ); +} + +=item +setClientCaps(%hash) + +Set the client caps. These are the x-clientcaps data. + +=cut + +sub setClientCaps +{ + my $self = shift; + my %caps = @_; + + $self->{ClientCaps} = \%caps; + + return 1; +} + +=item +getClientCaps() + +Get the client caps. These are the x-clientcaps data. + +=cut + +sub getClientCaps +{ + my $self = shift; + + return $self->{ClientCaps}; +} + +=item +setStatus + +Set the status. + +=cut + +sub setStatus +{ + my $self = shift; + + return $self->{Notification}->setStatus( @_ ); +} + +#================================================ +# Contact methods +#================================================ + +=item +blockContact($email) + +Puts $email on your block list. + +=cut + +sub blockContact +{ + my $self = shift; + + return $self->{Notification}->blockContact( @_ ); +} + +=item +unblockContact($email) + +Removes $email from your block list. + +=cut + +sub unblockContact +{ + my $self = shift; + + return $self->{Notification}->unblockContact( @_ ); +} + +=item +addContact($email) + +Puts $email on your contact list. This allows you to recieve status messages about this individual. + +=cut + +sub addContact +{ + my $self = shift; + + return $self->{Notification}->addContact( @_ ); +} + +=item +remContact($email) + +Removes $email from your contact list. + +=cut + +sub remContact +{ + my $self = shift; + + return $self->{Notification}->remContact( @_ ); +} + +=item +allowContact($email) + +Puts $email on your allow list. This is generaly automatic but there might be some cases where it is useful. +If you do not want to automatically allow contacts to see you online, you can set a handler for the "ContactAddingUs" +event and return 0. + +=cut + +sub allowContact +{ + my $self = shift; + + return $self->{Notification}->allowContact( @_ ); +} + +=item +disallowContact($email) + +Removes $email from your allow list. They will no longer be able to see you or talk to you. + +=cut + +sub disallowContact +{ + my $self = shift; + + return $self->{Notification}->disallowContact( @_ ); +} + +=item +getContactList($list) + +Expects $list to be one of FL, RL, AL, or BL. Returns the email addresses on said list. + +=cut + +sub getContactList +{ + my $self = shift; + + return $self->{Notification}->getContactList( @_ ); +} + +=item +getContact($email) + +Returns a hash containing all known info for this contact. + +=cut + +sub getContact +{ + my $self = shift; + + return $self->{Notification}->getContact( @_ ); +} + +=item +getContactName($email) + +Returns the friendly name used by this contact, if they are on your FL list. + +=cut + +sub getContactName +{ + my $self = shift; + + return $self->{Notification}->getContactName( @_ ); +} + +=item +getContactStatus($email) + +Returns the status of this contact, if they are on your FL list. + +=cut + +sub getContactStatus +{ + my $self = shift; + + return $self->{Notification}->getContactStatus( @_ ); +} + +=item +getContactClientInfo($email) + +Returns the client info of this contact, if they are on your FL list. + +=cut + +sub getContactClientInfo +{ + my $self = shift; + + return $self->{Notification}->getContactClientInfo( @_ ); +} + +#================================================ +# Other +#================================================ + +=item +findMember($email) + +Looks for a member in an active SwitchBoard and returns the SB or undef, if not found. + +=cut + +sub findMember +{ + my $self = shift; + my $email = shift || ''; + + foreach my $convo (values %{$self->getConvoList()}) + { + my $members = $convo->getMembers(); + + return $convo if( defined $members->{$email} ); + } + + return undef; +} + +=item +addEmoticon($shortcut, $filename) + +Adds an emoticon to your connection. This loads the file and prepares it. Anytime you use the text form $shortcut in an outgoing message it will be replaced with the appropriate emoticon. You can only use 5 different emoticons per message. + +=cut + +sub addEmoticon +{ + my $self = shift; + + return return $self->{Notification}->addEmoticon( @_ ); +} + +=item +broadcast($msg,%style) + +Broadcasts the message to all open conversations. + +=cut + +sub broadcast +{ + my $self = shift; + my @data = @_; # probably don't have to do this, but just to be safe + + foreach my $convo (values %{$self->getConvoList()}) + { + $convo->sendMessage( @data ); + } +} + +=item +call($email,$msg,%style) + +Calls the contact, starting a conversation with them. + +=cut + +sub call +{ + my $self = shift; + + print "db in call for $self\n"; + + $self->{Notification}->call( @_ ); +} + +sub isOnline +{ + my $self = shift; + +} + +=item +do_one_loop() + +Process a single cycle's worth of incoming and outgoing messages. This should be done at a regular intervals, preferably under a second. + +=cut + +sub do_one_loop +{ + my $self = shift; + + # return immediately if we are not connected + return if( !$self->{Connected} ); + + $self->{Notification}->ping( ); + + foreach my $convo (values %{$self->getConvoList()}) + { + $convo->p2pSendOne() if $convo->p2pWaiting; + } + + my @ready = $self->{Select}->can_read(.1); + foreach my $fh ( @ready ) + { + # get the filenumber for this filehandle + my $fn = $fh->fileno; + + # get the object assocatied with this filenumber + my $connection = $self->{Connections}->{$fn}; + + # DO WE NEED THIS CODE? if the connection is really dead, will it even be showing up in the list of filehandles that can be read from?? + # if the connection is dead, remove it from the select, delete it from the Connections list and output a warn + if( !$connection->{Socket}->connected() ) + { + $self->{Select}->remove( $fn ); + delete( $self->{Connections}->{fn} ); + warn "Killing dead socket"; + next; + } + + sysread( $fh, $connection->{buf}, 2048, length( $connection->{buf} || '' ) ); + + while( $connection->{buf} =~ s/^(.*?\n)// ) + { + $connection->{line}= $1; + my $incomingdata = $connection->{line}; + $incomingdata =~ s/[\r\n]//g; + + print( "($fn $connection->{Type}) RX: $incomingdata\n" ) if( $self->{ShowRX} ); + + my $result = $connection->dispatch( $incomingdata ); + last if( $result && $result eq "wait" ); + } + } +} + + +=item +setHandler($event, $handler) + +$event should be an event listed in the events section. These are called based on information sent by MSN, +receiving a message is an event, status changes are events, getting a call is an event, etc. + + $msn->setHandler( Connected => \&connected ); + + sub connected { + my $self = shift; + print "Yay we connected"; + } + +=cut + +sub setHandler +{ + my $self = shift; + my ($event, $handler) = @_; + + $self->{handler}->{$event} = $handler; +} + +=item +setHandlers( $event1 => $handler1, $event2 => $handler2) + +Expects a list of events and handlers. + + my $msn = new MSN; + $msn->setHandlers( Connected => \&connected, + Disconnected => \&disconnected ); + +=cut + +sub setHandlers +{ + my $self = shift; + my $handlers = { @_ }; + for my $event (keys %$handlers) + { + $self->setHandler( $event, $handlers->{$event} ); + } +} + +sub call_event +{ + my $self = shift; + my $receiver = shift; + my $event = shift; + + # get and run the handler if it is defined + my $function = $self->{handler}->{$event}; + return &$function( $receiver, @_ ) if( defined $function ); + + # get and run the default handler if it is defined + $function = $self->{handler}->{Default}; + return &$function( $receiver, $event, @_ ) if( defined $function ); + + return undef; +} + +=item +getNotification + +Returns the MSN::Notification object if you have a need to interact with it directly. + +=cut + +sub getNotification +{ + my $self = shift; + + return $self->{Notification}; +} + +=item +getConvoList + +Returns a hash of conversations (MSN::SwitchBoard objects) keyed by file number of the socket they are on. + +=cut + +sub getConvoList +{ + my $self = shift; + + my $convos = {}; + + foreach my $fn (keys %{$self->{Connections}} ) + { + if( $self->{Connections}->{$fn}->getType() eq 'SB' ) + { + $convos->{$fn} = $self->{Connections}->{$fn}; + } + } + + return $convos; +} + +=item +getConvo + +Returns a conversation (MSN::SwitchBoard object) found by socket number. + +=cut + +sub getConvo +{ + my $self = shift; + my $key = shift; + + if( defined $self->{Connections}->{$key} && $self->{Connections}->{$key}->getType() eq 'SB' ) + { + return $self->{Connections}->{$key}; + } + + return undef; +} + + +return 1; +__DATA__ diff --git a/lib/site/Tie/Hash.pm.original b/lib/site/Tie/Hash.pm.original index 63a5ee708..282006984 100644 --- a/lib/site/Tie/Hash.pm.original +++ b/lib/site/Tie/Hash.pm.original @@ -1,243 +1,243 @@ -package Tie::Hash; - -our $VERSION = '1.00'; - -=head1 NAME - -Tie::Hash, Tie::StdHash, Tie::ExtraHash - base class definitions for tied hashes - -=head1 SYNOPSIS - - package NewHash; - require Tie::Hash; - - @ISA = (Tie::Hash); - - sub DELETE { ... } # Provides needed method - sub CLEAR { ... } # Overrides inherited method - - - package NewStdHash; - require Tie::Hash; - - @ISA = (Tie::StdHash); - - # All methods provided by default, define only those needing overrides - # Accessors access the storage in %{$_[0]}; - # TIEHANDLE should return a reference to the actual storage - sub DELETE { ... } - - package NewExtraHash; - require Tie::Hash; - - @ISA = (Tie::ExtraHash); - - # All methods provided by default, define only those needing overrides - # Accessors access the storage in %{$_[0][0]}; - # TIEHANDLE should return an array reference with the first element being - # the reference to the actual storage - sub DELETE { - $_[0][1]->('del', $_[0][0], $_[1]); # Call the report writer - delete $_[0][0]->{$_[1]}; # $_[0]->SUPER::DELETE($_[1]) } - - - package main; - - tie %new_hash, 'NewHash'; - tie %new_std_hash, 'NewStdHash'; - tie %new_extra_hash, 'NewExtraHash', - sub {warn "Doing \U$_[1]\E of $_[2].\n"}; - -=head1 DESCRIPTION - -This module provides some skeletal methods for hash-tying classes. See -L for a list of the functions required in order to tie a hash -to a package. The basic B package provides a C method, as well -as methods C, C and C. The B and -B packages -provide most methods for hashes described in L (the exceptions -are C and C). They cause tied hashes to behave exactly like standard hashes, -and allow for selective overwriting of methods. B grandfathers the -C method: it is used if C is not defined -in the case a class forgets to include a C method. - -For developers wishing to write their own tied hashes, the required methods -are briefly defined below. See the L section for more detailed -descriptive, as well as example code: - -=over 4 - -=item TIEHASH classname, LIST - -The method invoked by the command C. Associates a new -hash instance with the specified class. C would represent additional -arguments (along the lines of L and compatriots) needed to -complete the association. - -=item STORE this, key, value - -Store datum I into I for the tied hash I. - -=item FETCH this, key - -Retrieve the datum in I for the tied hash I. - -=item FIRSTKEY this - -Return the first key in the hash. - -=item NEXTKEY this, lastkey - -Return the next key in the hash. - -=item EXISTS this, key - -Verify that I exists with the tied hash I. - -The B implementation is a stub that simply croaks. - -=item DELETE this, key - -Delete the key I from the tied hash I. - -=item CLEAR this - -Clear all values from the tied hash I. - -=back - -=head1 Inheriting from B - -The accessor methods assume that the actual storage for the data in the tied -hash is in the hash referenced by C. Thus overwritten -C method should return a hash reference, and the remaining methods -should operate on the hash referenced by the first argument: - - package ReportHash; - our @ISA = 'Tie::StdHash'; - - sub TIEHASH { - my $storage = bless {}, shift; - warn "New ReportHash created, stored in $storage.\n"; - $storage - } - sub STORE { - warn "Storing data with key $_[1] at $_[0].\n"; - $_[0]{$_[1]} = $_[2] - } - - -=head1 Inheriting from B - -The accessor methods assume that the actual storage for the data in the tied -hash is in the hash referenced by C<(tied(%tiedhash))[0]>. Thus overwritten -C method should return an array reference with the first -element being a hash reference, and the remaining methods should operate on the -hash C<< %{ $_[0]->[0] } >>: - - package ReportHash; - our @ISA = 'Tie::StdHash'; - - sub TIEHASH { - my $storage = bless {}, shift; - warn "New ReportHash created, stored in $storage.\n"; - [$storage, @_] - } - sub STORE { - warn "Storing data with key $_[1] at $_[0].\n"; - $_[0][0]{$_[1]} = $_[2] - } - -The default C method stores "extra" arguments to tie() starting -from offset 1 in the array referenced by C; this is the -same storage algorithm as in TIEHASH subroutine above. Hence, a typical -package inheriting from B does not need to overwrite this -method. - -=head1 C and C - -The methods C and C are not defined in B, -B, or B. Tied hashes do not require -presense of these methods, but if defined, the methods will be called in -proper time, see L. - -If needed, these methods should be defined by the package inheriting from -B, B, or B. - -=head1 MORE INFORMATION - -The packages relating to various DBM-related implementations (F, -F, etc.) show examples of general tied hashes, as does the -L module. While these do not utilize B, they serve as -good working examples. - -=cut - -use Carp; -use warnings::register; - -sub new { - my $pkg = shift; - $pkg->TIEHASH(@_); -} - -# Grandfather "new" - -sub TIEHASH { - my $pkg = shift; - if (defined &{"${pkg}::new"}) { - warnings::warnif("WARNING: calling ${pkg}->new since ${pkg}->TIEHASH is missing"); - $pkg->new(@_); - } - else { - croak "$pkg doesn't define a TIEHASH method"; - } -} - -sub EXISTS { - my $pkg = ref $_[0]; - croak "$pkg doesn't define an EXISTS method"; -} - -sub CLEAR { - my $self = shift; - my $key = $self->FIRSTKEY(@_); - my @keys; - - while (defined $key) { - push @keys, $key; - $key = $self->NEXTKEY(@_, $key); - } - foreach $key (@keys) { - $self->DELETE(@_, $key); - } -} - -# The Tie::StdHash package implements standard perl hash behaviour. -# It exists to act as a base class for classes which only wish to -# alter some parts of their behaviour. - -package Tie::StdHash; -# @ISA = qw(Tie::Hash); # would inherit new() only - -sub TIEHASH { bless {}, $_[0] } -sub STORE { $_[0]->{$_[1]} = $_[2] } -sub FETCH { $_[0]->{$_[1]} } -sub FIRSTKEY { my $a = scalar keys %{$_[0]}; each %{$_[0]} } -sub NEXTKEY { each %{$_[0]} } -sub EXISTS { exists $_[0]->{$_[1]} } -sub DELETE { delete $_[0]->{$_[1]} } -sub CLEAR { %{$_[0]} = () } - -package Tie::ExtraHash; - -sub TIEHASH { my $p = shift; bless [{}, @_], $p } -sub STORE { $_[0][0]{$_[1]} = $_[2] } -sub FETCH { $_[0][0]{$_[1]} } -sub FIRSTKEY { my $a = scalar keys %{$_[0][0]}; each %{$_[0][0]} } -sub NEXTKEY { each %{$_[0][0]} } -sub EXISTS { exists $_[0][0]->{$_[1]} } -sub DELETE { delete $_[0][0]->{$_[1]} } -sub CLEAR { %{$_[0][0]} = () } - -1; +package Tie::Hash; + +our $VERSION = '1.00'; + +=head1 NAME + +Tie::Hash, Tie::StdHash, Tie::ExtraHash - base class definitions for tied hashes + +=head1 SYNOPSIS + + package NewHash; + require Tie::Hash; + + @ISA = (Tie::Hash); + + sub DELETE { ... } # Provides needed method + sub CLEAR { ... } # Overrides inherited method + + + package NewStdHash; + require Tie::Hash; + + @ISA = (Tie::StdHash); + + # All methods provided by default, define only those needing overrides + # Accessors access the storage in %{$_[0]}; + # TIEHANDLE should return a reference to the actual storage + sub DELETE { ... } + + package NewExtraHash; + require Tie::Hash; + + @ISA = (Tie::ExtraHash); + + # All methods provided by default, define only those needing overrides + # Accessors access the storage in %{$_[0][0]}; + # TIEHANDLE should return an array reference with the first element being + # the reference to the actual storage + sub DELETE { + $_[0][1]->('del', $_[0][0], $_[1]); # Call the report writer + delete $_[0][0]->{$_[1]}; # $_[0]->SUPER::DELETE($_[1]) } + + + package main; + + tie %new_hash, 'NewHash'; + tie %new_std_hash, 'NewStdHash'; + tie %new_extra_hash, 'NewExtraHash', + sub {warn "Doing \U$_[1]\E of $_[2].\n"}; + +=head1 DESCRIPTION + +This module provides some skeletal methods for hash-tying classes. See +L for a list of the functions required in order to tie a hash +to a package. The basic B package provides a C method, as well +as methods C, C and C. The B and +B packages +provide most methods for hashes described in L (the exceptions +are C and C). They cause tied hashes to behave exactly like standard hashes, +and allow for selective overwriting of methods. B grandfathers the +C method: it is used if C is not defined +in the case a class forgets to include a C method. + +For developers wishing to write their own tied hashes, the required methods +are briefly defined below. See the L section for more detailed +descriptive, as well as example code: + +=over 4 + +=item TIEHASH classname, LIST + +The method invoked by the command C. Associates a new +hash instance with the specified class. C would represent additional +arguments (along the lines of L and compatriots) needed to +complete the association. + +=item STORE this, key, value + +Store datum I into I for the tied hash I. + +=item FETCH this, key + +Retrieve the datum in I for the tied hash I. + +=item FIRSTKEY this + +Return the first key in the hash. + +=item NEXTKEY this, lastkey + +Return the next key in the hash. + +=item EXISTS this, key + +Verify that I exists with the tied hash I. + +The B implementation is a stub that simply croaks. + +=item DELETE this, key + +Delete the key I from the tied hash I. + +=item CLEAR this + +Clear all values from the tied hash I. + +=back + +=head1 Inheriting from B + +The accessor methods assume that the actual storage for the data in the tied +hash is in the hash referenced by C. Thus overwritten +C method should return a hash reference, and the remaining methods +should operate on the hash referenced by the first argument: + + package ReportHash; + our @ISA = 'Tie::StdHash'; + + sub TIEHASH { + my $storage = bless {}, shift; + warn "New ReportHash created, stored in $storage.\n"; + $storage + } + sub STORE { + warn "Storing data with key $_[1] at $_[0].\n"; + $_[0]{$_[1]} = $_[2] + } + + +=head1 Inheriting from B + +The accessor methods assume that the actual storage for the data in the tied +hash is in the hash referenced by C<(tied(%tiedhash))[0]>. Thus overwritten +C method should return an array reference with the first +element being a hash reference, and the remaining methods should operate on the +hash C<< %{ $_[0]->[0] } >>: + + package ReportHash; + our @ISA = 'Tie::StdHash'; + + sub TIEHASH { + my $storage = bless {}, shift; + warn "New ReportHash created, stored in $storage.\n"; + [$storage, @_] + } + sub STORE { + warn "Storing data with key $_[1] at $_[0].\n"; + $_[0][0]{$_[1]} = $_[2] + } + +The default C method stores "extra" arguments to tie() starting +from offset 1 in the array referenced by C; this is the +same storage algorithm as in TIEHASH subroutine above. Hence, a typical +package inheriting from B does not need to overwrite this +method. + +=head1 C and C + +The methods C and C are not defined in B, +B, or B. Tied hashes do not require +presense of these methods, but if defined, the methods will be called in +proper time, see L. + +If needed, these methods should be defined by the package inheriting from +B, B, or B. + +=head1 MORE INFORMATION + +The packages relating to various DBM-related implementations (F, +F, etc.) show examples of general tied hashes, as does the +L module. While these do not utilize B, they serve as +good working examples. + +=cut + +use Carp; +use warnings::register; + +sub new { + my $pkg = shift; + $pkg->TIEHASH(@_); +} + +# Grandfather "new" + +sub TIEHASH { + my $pkg = shift; + if (defined &{"${pkg}::new"}) { + warnings::warnif("WARNING: calling ${pkg}->new since ${pkg}->TIEHASH is missing"); + $pkg->new(@_); + } + else { + croak "$pkg doesn't define a TIEHASH method"; + } +} + +sub EXISTS { + my $pkg = ref $_[0]; + croak "$pkg doesn't define an EXISTS method"; +} + +sub CLEAR { + my $self = shift; + my $key = $self->FIRSTKEY(@_); + my @keys; + + while (defined $key) { + push @keys, $key; + $key = $self->NEXTKEY(@_, $key); + } + foreach $key (@keys) { + $self->DELETE(@_, $key); + } +} + +# The Tie::StdHash package implements standard perl hash behaviour. +# It exists to act as a base class for classes which only wish to +# alter some parts of their behaviour. + +package Tie::StdHash; +# @ISA = qw(Tie::Hash); # would inherit new() only + +sub TIEHASH { bless {}, $_[0] } +sub STORE { $_[0]->{$_[1]} = $_[2] } +sub FETCH { $_[0]->{$_[1]} } +sub FIRSTKEY { my $a = scalar keys %{$_[0]}; each %{$_[0]} } +sub NEXTKEY { each %{$_[0]} } +sub EXISTS { exists $_[0]->{$_[1]} } +sub DELETE { delete $_[0]->{$_[1]} } +sub CLEAR { %{$_[0]} = () } + +package Tie::ExtraHash; + +sub TIEHASH { my $p = shift; bless [{}, @_], $p } +sub STORE { $_[0][0]{$_[1]} = $_[2] } +sub FETCH { $_[0][0]{$_[1]} } +sub FIRSTKEY { my $a = scalar keys %{$_[0][0]}; each %{$_[0][0]} } +sub NEXTKEY { each %{$_[0][0]} } +sub EXISTS { exists $_[0][0]->{$_[1]} } +sub DELETE { delete $_[0][0]->{$_[1]} } +sub CLEAR { %{$_[0][0]} = () } + +1; diff --git a/lib/site/Tk/CursorControl.pm b/lib/site/Tk/CursorControl.pm index 3ca1c0dc0..2f38d68ae 100644 --- a/lib/site/Tk/CursorControl.pm +++ b/lib/site/Tk/CursorControl.pm @@ -1,914 +1,914 @@ -package Tk::CursorControl; - -require 5.005_62; -use Tk 800.015; -use Carp; -use strict; - -$Tk::CursorControl::VERSION = '0.4'; - -my $AlreadyInit = 0; -my $CurrentObject = 0; -my $Main; - -#Create Aliases to some public methods. -*jail = \&confine; -*free = \&release; -*Show = \&show; - -Construct Tk::Widget 'CursorControl'; - -sub new { - my ( $me, $parent ) = @_; - my $class = ref($me) || $me; - my $self = {}; - bless $self => $class; - - # provide access to class data - $self->{_Init} = \$AlreadyInit; - $self->{_CurrentObj} = \$CurrentObject; - - # set MainWindow reference in 'accessible' class data - $Main = $parent->MainWindow; - $parent->OnDestroy( sub { $self->DESTROY } ); - $self->{MAIN} = \$Main if ( defined $Main ); - - if ( ${ $self->{_Init} } == 0 ) { - ++${ $self->{_Init} }; - $self->_init; - ${ $self->{_CurrentObj} } = - $self; #store object in case user tries to create two! - return $self; - } - else { - ++${ $self->{_Init} }; # DESTROY will be called, so increment anyway - # These error messages are now suppressed - JD October 13, 2003 - # Thanks for the suggestion Ala. - ### carp "A $class object has ALREADY been created !"; - ### carp "The object returned is the original object for $class"; - # This means that either a module already called Tk::CursorControl on behalf - # of the user (i.e. via a 'use SomeModule' where the code within SomeModule - # creates a Tk::CursorControl object ---OR--- the programmer didn't read the - # documentation and tried to create two or more CursorControl objects for one - # MainWindow. - return ${ $self->{_CurrentObj} }; #return ORIGINALLY created object - } -} - -# For erroneous understanding of this Class! -sub _errmsg { croak "You cannot $_[1] a ", ref( $_[0] ); } - -########## Public NON-methods ########## -# Just in case someone treats this like a Tk widget -# Override geometry managers - -sub pack { $_[0]->_errmsg('pack') } -sub grid { $_[0]->_errmsg('grid') } -sub form { $_[0]->_errmsg('form') } -sub place { $_[0]->_errmsg('place') } -sub configure { $_[0]->_errmsg('configure') } -sub cget { $_[0]->_errmsg('cget') } - -########## Public Methods ########## -sub confine { - my ( $self, $widget ) = @_; - unless ( defined $widget ) { - carp "\$cursor->confine(\$widget)"; - return; - } - - #free the cursor if already confined elsewhere - $self->release if ( $self->{Confined} ); - - #does the widget exist? is it mapped? - return unless ( $self->_check($widget) ); - - if ( $self->{Type} eq 'win32' ) { - $self->_Win32confine($widget); - } - else { - - #Then $self->{Type} is the default 'unix' - $self->_Unixconfine($widget); - } -} - -sub release { - my $self = shift; - if ( $self->{Type} eq 'win32' ) { - $self->_Win32release; - } - else { - - #Then $self->{Type} is the default 'unix' - $self->_Unixrelease; - } -} - -sub hide { - my $self = shift; - my $w; - - foreach $w (@_) { - - #bind to Enter and Leave Events - if ( $self->{Type} eq 'win32' ) { - - #Showcursor is a system-wide API - so we want to ensure that the cursor doesn't - #disappear forever! - $self->_Win32saveBindings($w); - $self->_Win32setBindings($w); - $self->_Win32hidecursor; - } - else { - $self->_setOldCursor($w); - $w->configure( -cursor => - [ '@' . $self->{bitmapfile}, $self->{maskfile}, 'black', 'white' ] ); - } - } -} - -sub show { - my $self = shift; - - foreach my $w (@_) { - - #delete bindings for hide Events for specified widget - if ( $self->{Type} eq 'win32' ) { - $self->_Win32restoreBindings($w); - $self->_Win32showcursor; - } - else { - my $cursor = $self->_getOldCursor($w); - $w->configure( -cursor => $cursor ) if ($cursor); - } - } -} - -sub moveto { - -# Similar to the warpto sub. Instead - we always use the root window coordinates -# So, all we are interested in is the starting x,y coordinates and the ending -# x,y coordinates. Most of the code is a copy and paste from the warpto sub. - - my $self = shift; - my $w; - - #parse the time off the arguments...there has to be a better way! - my $movetime = 1000; #default to 1 second (1000ms) - if ( grep /time/, @_ ) { - my $i = 0; - my $timefound; - map { - if (/time/) { - splice @_, $i, 1; - $timefound = pop(@_); - $movetime = $timefound if ( $timefound =~ /\d+/ ); - } - $i++; - } @_; - } - - #minimum time allowed - $movetime = 25 if ( $movetime < 25 ); - -# Three ways of warping: -# 1. Pass a widget reference - default warp the cursor to the center. -# 2. Pass a widget reference and x,y value - warp the cursor to x,y of that widget. -# 3. Pass only an x,y coordinate (with no widget reference) then it is treated like -# a screen coordinate. - - my $finalx; - my $finaly; - my $startx = ${ $self->{MAIN} }->pointerx; - my $starty = ${ $self->{MAIN} }->pointery; - - my $argnum = scalar(@_); - my $ref = ref( $_[0] ); - if ( $ref and $ref =~ /^Tk/ ) { - $w = shift; - - # Does the widget exist and is it mapped? - return unless ( $self->_check($w) ); - - if ( $argnum == 1 ) { - - #assume only a widget reference passed - $self->release if ( $self->{Confined} ); - - #Get ROOT coordinates of the final position - $finalx = $w->rootx + ( $w->width / 2 ); - $finaly = $w->rooty + ( $w->height / 2 ); - - } - elsif ( $argnum == 3 ) { - - #assume a widget reference AND x,y value passed - my $x = shift; - my $y = shift; - - $self->release if ( $self->{Confined} ); - - #warp pointer to x,y coordinate relative to widgets NW corner - my $width = $w->width; - my $height = $w->height; - $x = 0 if ( $x < 0 ); - $x = $width - 1 if ( $x > $width ); - $y = 0 if ( $y < 0 ); - $y = $height - 1 if ( $y > $height ); - - $finalx = $w->rootx + $x; - $finaly = $w->rooty + $y; - } - } #end if $widget is passed - elsif ( $argnum == 2 ) { - - # Assume only an x,y value passed.. - - my $X = shift; - my $Y = shift; - - $self->release if ( $self->{Confined} ); - -# Sanity check - don't warp beyond the screen. The window managers won't let you -# anyways - but we might as well not try! - my $sw = ${ $self->{MAIN} }->screenwidth; - my $sh = ${ $self->{MAIN} }->screenheight; - - $X = 0 if ( $X < 0 ); - $Y = 0 if ( $Y < 0 ); - $X = $sw if ( $X > $sw ); - $Y = $sh if ( $Y > $sh ); - - $finalx = $X; - $finaly = $Y; - - } - - return unless ( defined $finalx and defined $finaly ); - - #finally "move" the cursor (based on time passed) - my $denom = $movetime / 25; - my $deltax = ( $finalx - $startx ) / $denom; - my $deltay = ( $finaly - $starty ) / $denom; - my $interx = $startx; - my $intery = $starty; - for ( my $i = 1 ; $i <= $denom ; $i++ ) { - $interx = $interx + $deltax; - $intery = $intery + $deltay; - $self->warpto( $interx, $intery ); - ${ $self->{MAIN} }->update; - - # Blocking is actually a good thing here. - ${ $self->{MAIN} }->after(25); - } - - # Now make sure we end up where we originally wanted ! - # Round off errors may have crept in. - $self->warpto( $finalx, $finaly ); - -} - -sub warpto { - - my $self = shift; - my $w; - -# Three ways of warping: -# 1. Pass a widget reference - default warp the cursor to the center. -# 2. Pass a widget reference and x,y value - warp the cursor to x,y of that widget. -# 3. Pass only an x,y coordinate (with no widget reference) then it is treated like -# a screen coordinate. - - my $argnum = scalar(@_); - my $ref = ref( $_[0] ); - if ( $ref and $ref =~ /^Tk/ ) { - $w = shift; - - # Does the widget exist and is it mapped? - return unless ( $self->_check($w) ); - - if ( $argnum == 1 ) { - - #assume only a widget reference passed - $self->release if ( $self->{Confined} ); - - #warp pointer to the center of the widget - my $x = ( $w->width / 2 ); - my $y = ( $w->height / 2 ); - - $w->eventGenerate( - "", - -when => 'head', - -x => $x, - -y => $y, - -warp => 1 - ); - } - elsif ( $argnum == 3 ) { - - #assume a widget reference AND x,y value passed - my $x = shift; - my $y = shift; - - $self->release if ( $self->{Confined} ); - - #warp pointer to x,y coordinate relative to widgets NW corner - my $width = $w->width; - my $height = $w->height; - $x = 0 if ( $x < 0 ); - $x = $width - 1 if ( $x > $width ); - $y = 0 if ( $y < 0 ); - $y = $height - 1 if ( $y > $height ); - - $w->eventGenerate( - "", - -when => 'head', - -x => $x, - -y => $y, - -warp => 1 - ); - } - } #end if $widget is passed - elsif ( $argnum == 2 ) { - -# Assume only an x,y value passed.. -# Warp to specific screen coordinates. There is no way to do this outright using -# eventGenerate...at least "I" couldn't find one. -# So we use the rootx and rooty of the MainWindow as our anchor position. -# and we might end up actually warping to negative values of x and y. - - my $X = shift; - my $Y = shift; - - $self->release if ( $self->{Confined} ); - - # works on windows even if iconified - check on unix. Maybe not needed!! - # return unless ( $self->_check(${$self->{MAIN}}) ); - -# Sanity check - don't warp beyond the screen. The window managers won't let you -# anyways - but we might as well not try! - my $sw = ${ $self->{MAIN} }->screenwidth; - my $sh = ${ $self->{MAIN} }->screenheight; - - $X = 0 if ( $X < 0 ); - $Y = 0 if ( $Y < 0 ); - $X = $sw if ( $X > $sw ); - $Y = $sh if ( $Y > $sh ); - - my $x = $X - ${ $self->{MAIN} }->rootx; - my $y = $Y - ${ $self->{MAIN} }->rooty; - - ${ $self->{MAIN} }->eventGenerate( - "", - -when => 'head', - -x => $x, - -y => $y, - -warp => 1 - ); - } - -} - -sub destroy { shift->DESTROY } - -############# Private methods ################## -sub _check { - my ( $self, $w ) = @_; - my $ok = 1; - unless ( $ok = Exists($w) ) { - carp "Widget $w: Does not exist!"; - } - unless ( $ok = $w->viewable ) { - carp "Widget $w: is not mapped"; - } - return $ok; -} - -sub _getposition { - - #return the top left corner and width/height of widget passed. - my ( $self, $w ) = @_; - my $x0 = $w->rootx; - my $y0 = $w->rooty; - my $width = $w->width; - my $height = $w->height; - - return ( $x0, $y0, $width, $height ); -} - -sub _getbbox { - - #return the absolute screen coordinates (i.e. bbox) - my ( $self, $w ) = @_; - my @c = $self->_getposition($w); - return ( $c[0], $c[1], $c[0] + $c[2], $c[1] + $c[3] ); -} - -sub _init { - my $self = shift; - - $self->{Type} = 'unix'; #default to 'perl based' cursor confine - - if ( $Tk::platform eq 'MSWin32' ) { - if ( eval "require Win32::API" ) { - - #Create API's - $self->{ClipCursor} = - Win32::API->new( 'user32', 'ClipCursor', ['P'], 'N' ); - $self->{ShowCursor} = - Win32::API->new( 'user32', 'ShowCursor', ['N'], 'N' ); - $self->{Type} = 'win32' if ( $self->{ClipCursor} && $self->{ShowCursor} ); - $self->{DisplayCount} = 0; - return if $self->{Type} eq 'win32'; - croak "Creating API objects failed for unknown reason"; - } - else { - croak "Please install Win32::API !!"; - } - } - else { - - #retrieve proper filenames for transparent cursor for *NIX.. - $self->{bitmapfile} = Tk->findINC('trans_cur.xbm'); - $self->{maskfile} = Tk->findINC('trans_cur.mask'); - croak "Files for tranparent cursor not found" - unless ( $self->{bitmapfile} && $self->{maskfile} ); - } -} - -####### On Unix Only ############# -sub _setOldCursor { - - #save the current cursor on a ->hide command - my ( $self, $w ) = @_; - my $oldcursor = $w->cget('-cursor') || 'left_ptr'; - $self->{OldCursor}{$w} = $oldcursor if ($oldcursor); -} - -sub _getOldCursor { - - #get saved cursor on a ->show command - my ( $self, $w ) = @_; - my $cursor = $self->{OldCursor}{$w}; - delete $self->{OldCursor}{$w}; - return ($cursor); -} - -sub _Unixconfine { - my ( $self, $w ) = @_; - my @coords = $self->_getposition($w); - -# Stop Class bindings from being triggered - as a Leave should NEVER occur -# Why? Because the cursor is 'supposed' to be confined to the widget! Since this -# event 'will' still occur we Tk->break it before the Class binding occurs. -# An example if we did not do this (and I know from testing) is: a Button relief rapidly -# changing between sunken and normal to causing massive flickering - -# Also - We cannot guarantee a Motion binding will get triggered for the passed widget. -# Instead we overwrite the Motion binding for the toplevel containg $widget. This virtually -# guarantees (fingers crossed) that a proper Motion binding exists. Of course the Leave -# event must stay with the passed $widget. - - my @bindtags = $w->bindtags; - $w->bindtags( [ @bindtags[ 1, 0, 2, 3 ] ] ); - - #Save current leave binding..if there is one - my $old_leave; - my $old_motion; - eval { $old_leave = $w->Tk::bind('') }; - eval { $old_motion = $w->toplevel->bind('') }; - ( defined $old_leave ) - ? ( $self->{OldLeave} = $old_leave ) - : ( $self->{OldLeave} = 0 ); - ( defined $old_motion ) - ? ( $self->{OldMotion} = $old_motion ) - : ( $self->{OldMotion} = 0 ); - - $w->Tk::bind( '', sub { $_[0]->break } ); - - $w->toplevel->bind( - '', - sub { - $self->{OldMotion}->Call if ( $self->{OldMotion} ); - $self->_warpToConfine( $w, @coords ); - } - ); - $self->{Confined} = $w; -} - -sub _Unixrelease { - my $self = shift; - - #Restore proper bindtag order for widget.. - return unless $self->{Confined}; - my @bindtags = $self->{Confined}->bindtags; - $self->{Confined}->bindtags( [ @bindtags[ 1, 0, 2, 3 ] ] ); - - ( $self->{OldLeave} ) - ? ( $self->{Confined}->Tk::bind( '', $self->{OldLeave} ) ) - : ( $self->{Confined}->Tk::bind( '', '' ) ); - ( $self->{OldMotion} ) - ? ( $self->{Confined}->toplevel->bind( '', $self->{OldMotion} ) ) - : ( $self->{Confined}->toplevel->bind( '', '' ) ); - - $self->{Confined} = 0; -} - -sub _warpToConfine { - my ( $self, $w, $x0, $y0, $wi, $he ) = @_; - - my $e = $w->XEvent; - my ( $x, $y ) = ( $e->x, $e->y ); - - my $warpneeded = 0; - - if ( $x <= 0 ) { - $x = 1; - $warpneeded = 1; - } - elsif ( $x >= $wi ) { - $x = $wi - 1; - $warpneeded = 1; - } - - if ( $y <= 0 ) { - $y = 1; - $warpneeded = 1; - } - elsif ( $y >= $he ) { - $y = $he - 1; - $warpneeded = 1; - } - - if ($warpneeded) { - $w->eventGenerate( - "", - -when => 'head', - -x => $x, - -y => $y, - -warp => 1 - ); - } -} - -########### On Win32 Only ############# -sub _Win32confine { - my ( $self, $w ) = @_; - my @coords = $self->_getbbox($w); - - my $rect = CORE::pack "L4", @coords; - - if ( defined $self->{ClipCursor} ) { - $self->{ClipCursor}->Call($rect); - $self->{Confined} = 1; - } -} - -sub _Win32release { - my $self = shift; - my $null = 0; - if ( defined $self->{ClipCursor} ) { - $self->{ClipCursor}->Call($null); - $self->{Confined} = 0; - } -} - -sub _Win32saveBindings { - my ( $self, $w ) = @_; - - # Save current Enter, Leave and Unmap bindings..if there are any - # Fully specify Tk::bind in case of a canvas widget. - my $old_leave; - my $old_enter; - my $old_unmap; - eval { $old_leave = $w->Tk::bind('') }; - eval { $old_enter = $w->Tk::bind('') }; - eval { $old_unmap = $w->Tk::bind('') }; - ( defined $old_leave ) - ? ( $self->{Win32Leave}{$w} = $old_leave ) - : ( $self->{Win32Leave}{$w} = 0 ); - ( defined $old_enter ) - ? ( $self->{Win32Enter}{$w} = $old_enter ) - : ( $self->{Win32Enter}{$w} = 0 ); - ( defined $old_unmap ) - ? ( $self->{Win32Unmap}{$w} = $old_unmap ) - : ( $self->{Win32Unmap}{$w} = 0 ); -} - -sub _Win32setBindings { - my ( $self, $w ) = @_; - $w->Tk::bind( - '', - sub { - $self->{Win32Enter}{$w}->Call if ( $self->{Win32Enter}{$w} ); - $self->_Win32hidecursor; - } - ); - $w->Tk::bind( - '', - sub { - $self->{Win32Leave}{$w}->Call if ( $self->{Win32Leave}{$w} ); - $self->_Win32showcursor; - } - ); - - #ensure cursor gets shown again if widget disappears.. - $w->Tk::bind( - '', - sub { - $self->{Win32Unmap}{$w}->Call if ( $self->{Win32Unmap}{$w} ); - $self->_Win32showcursor; - } - ); -} - -sub _Win32restoreBindings { - my ( $self, $w ) = @_; - ( $self->{Win32Enter}{$w} ) - ? ( $w->Tk::bind( '', $self->{Win32Enter}{$w} ) ) - : ( $w->Tk::bind( '', '' ) ); - ( $self->{Win32Leave}{$w} ) - ? ( $w->Tk::bind( '', $self->{Win32Leave}{$w} ) ) - : ( $w->Tk::bind( '', '' ) ); - ( $self->{Win32Unmap}{$w} ) - ? ( $w->Tk::bind( '', $self->{Win32Unmap}{$w} ) ) - : ( $w->Tk::bind( '', '' ) ); -} - -sub _Win32hidecursor { - my $self = shift; - $self->_Win32resetDisplayCount; - $self->{DisplayCount} = $self->{ShowCursor}->Call(0); -} - -sub _Win32showcursor { - my $self = shift; - $self->_Win32resetDisplayCount; - $self->{DisplayCount} = $self->{ShowCursor}->Call(1); -} - -sub _Win32resetDisplayCount { - my $self = shift; - - #Decrement display count to get ready for a hide. i.e. set count to 0. - if ( $self->{DisplayCount} > 0 ) { - my $count = $self->{DisplayCount}; - for ( my $i = $count ; $i > 0 ; $i-- ) { - $self->{DisplayCount} = $self->{ShowCursor}->Call(0); - } - } - - #Increment display count to get ready for a show. i.e. set count to -1. - elsif ( $self->{DisplayCount} < 0 ) { - my $count = $self->{DisplayCount}; - for ( my $i = $count ; $i < -1 ; $i++ ) { - $self->{DisplayCount} = $self->{ShowCursor}->Call(1); - } - } -} - -######################################## -sub DESTROY { - my $self = shift; - $self->release; - ##carp "DESTROY CALLED ON $self"; - --${ $self->{_Init} }; #decrement initialize value -} - -1; - -__END__ - -=head1 NAME - -Tk::CursorControl - Manipulate the mouse cursor programmatically - -=head1 SYNOPSIS - - use Tk::CursorControl; - $cursor = $main->CursorControl; - - # Lock the mouse cursor to $widget - $cursor->confine($widget); - - # Free the cursor - $cursor->release; - - # cursor disappears over $widget - $cursor->hide($widget); - - # show cursor again over $widget - $cursor->show($widget); - - # warp cursor to $widget (jump) - $cursor->warpto($widget); - - # move cursor to $widget - $cursor->moveto($widget); - -=head1 DESCRIPTION - -B is-B-a Tk::Widget. -Rather, it I Tk and encompasses a collection of methods -used to manipulate the cursor I<(aka pointer)> programmatically -from a Tk program. - -=head1 STANDARD OPTIONS - -B does I accept any standard options - -=head1 METHODS - -The following methods are available: - -=over 4 - -=item I<$cursor>-EB( $widget ) - -Confine the cursor to stay within the bounding box of $widget. - -=over 4 - -=item I<$cursor>-EB( $widget ) - -Alias for the B method. - -=back - -=item I<$cursor>-EB - -Release the cursor. Used to restore proper cursor functionality -after a confine. Note: I<$widget> does B need to be specified. - -=over 4 - -=item I<$cursor>-EB - -Alias for the B method. - -=back - -=item I<$cursor>-EB( @widgets ) - -Make cursor I over each widget in @widgets. - -=item I<$cursor>-EB( @widgets ) - -Make cursor I over each widget in @widgets. This is used after a B. -Bhow (capital S) can be used as well. - -=item I<$cursor>-EB( $widget I) - -Warp the cursor to the specified I<(?x,y?)> position in $widget. If the x,y values -are not specified, then the I
    of the widget is used as the target. - -OR - -=item I<$cursor>-EB( X,Y ) - -Warp the cursor to the specified I screen coordinate. - -=item I<$cursor>-EB( $widget I, -time=EI) - -Move the cursor to the specified I<(?x,y?)> position in $widget in I<-time> milliseconds. -If the x,y values are not specified, then the I
    of the widget is used as the -target. The -time value defaults to 1000ms (1 second) if not specified. The smaller the -time, the faster the cursor will move. The time given will not be exact. See bugs below. - -OR - -=item I<$cursor>-EB( X,Y, -time=EI) - -Move the cursor to the specified I screen coordinate in I<-time> milliseconds. -The -time value defaults to 1000ms (1 second) if not specified. The smaller the -time, the faster the cursor will move. The time given will not be exact. See bugs below. - -=back - -=head1 DEPENDENCIES - -B is required on Win32 systems. - -=head1 POSSIBLE USES - -Don't e-mail me to debate whether or not a program I warp or -hide a cursor. I will give you a few instances where "I think" a -module like this could come in handy. - -1. Confining a canvas item to remain within the Canvas boundaries -on a move. See the cursor demonstration in 'widget'. - -2. Giving the user some 'leeway' on clicking near an item. Say, -clicking on the picture of a thermometer, warps the cursor to a -Tk::Scale (right beside it) which actually controls that thermometer. - -3. Confining a window within another window (Tk::MDI should be -upgraded to 'use Tk::CursorControl') - -4. A step by step, show and tell session on 'How to use this GUI'. - -5. Make the cursor disappear for a keyboard only Tk::Canvas game. - -The key to using this module properly, is subtlety! Don't start making -the cursor warp all over the screen or making it disappear sporadically. -That is a misuse of the functionality. - -For some 'real world' applications which already have these types of -functionality, see any Multiple Document Interface (MDI); such as in -Excel or Word). Also have a look at the Win32 color chooser. The cursor -will be confined to the color palette while the button is pressed. Also, -try clicking on the gradient bar to the right of the palette. See what -happens to the mouse cursor?! -I'll bet you didn't even know that this existed until now. - -If you discover another good use for this module, I would definitely -like to hear about it ! I is the type of e-mail I would welcome. - -=head1 BUGS & IDIOSYNCRASIES - -B - -B only allows ONE object per MainWindow! If you try -to create more than one, B. -This will also be true if using a widget or module which already defines -a Tk::CursorControl object. - -B - -B internally generates EEnterE, -ELeaveE and EMotionE bindings for the I<$widget> -passed. Any user-defined bindings of the same type for I<$widget> -should I get executed. This feature has not been completely -tested. - -B - -This module makes heavy use of the ShowCursor and ClipCursor API's on -Win32. Be aware that when you change a cursor using the API, you -are doing so for your entire system. You, (the programmer) are -responsible for generating the show/hide and confine/release commands -in the proper order. - -For every hide - you I<*will*> want a show. For every confine - you -I<*should*> have a release. There are cautionary measures built-in -to ensure that the cursor doesn't disappear forever or get locked -within a widget. - -i.e. A B is automatically called if you try to confine -the cursor to two widgets at the same time. - -In other words, the last B always wins! - -B - -The methods for hiding and confining the cursor on Unix-based systems -is different than for Win32. - -A blank cursor is defined using the Tk::Widget configure method for -each widget passed. Two files have been provided for this purpose in -the installation - I and I. These files -must exist under a BFindINC> directory. - -Confining a cursor on *nix does I use any sort of API or Xlib -calls. Motion events are generated on the toplevel window to confine -the cursor to the proper widget. On slow systems, this will make the -cursor I like it is attached to the widget sides with a spring. -On faster systems, while still there, this I type action -is much less noticible. - -B - -The time parameter passed to a moveto method will not be exact. The -reason for this is because a crude L command is -used to I for a very short period. You will find that the -actual time taken for the cursor to stop is alway slightly B -than the time you specified. This time difference will be greater -on slower computers. The time error will also increase for higher -time values. - -B - -Warping the cursor will cause problems for users of absolute location -pointing devices (like graphics tablets). Users of graphics tablets -should B use this module. - -=head1 AUTHOR - -B . - -Copyright (c) 2002-2004 Jack Dunnigan. All rights reserved. - -This program is free software; you can redistribute it and/or -modify it under the same terms as Perl itself. - -My thanks to Tk gurus Steve Lidie and Slaven Rezic for their suggestions -and their patches. This is my first module on CPAN and I appreciate -their help. Thanks to Ala Qumsieh for utilizing the power of my module -in L. - -=cut - - - - +package Tk::CursorControl; + +require 5.005_62; +use Tk 800.015; +use Carp; +use strict; + +$Tk::CursorControl::VERSION = '0.4'; + +my $AlreadyInit = 0; +my $CurrentObject = 0; +my $Main; + +#Create Aliases to some public methods. +*jail = \&confine; +*free = \&release; +*Show = \&show; + +Construct Tk::Widget 'CursorControl'; + +sub new { + my ( $me, $parent ) = @_; + my $class = ref($me) || $me; + my $self = {}; + bless $self => $class; + + # provide access to class data + $self->{_Init} = \$AlreadyInit; + $self->{_CurrentObj} = \$CurrentObject; + + # set MainWindow reference in 'accessible' class data + $Main = $parent->MainWindow; + $parent->OnDestroy( sub { $self->DESTROY } ); + $self->{MAIN} = \$Main if ( defined $Main ); + + if ( ${ $self->{_Init} } == 0 ) { + ++${ $self->{_Init} }; + $self->_init; + ${ $self->{_CurrentObj} } = + $self; #store object in case user tries to create two! + return $self; + } + else { + ++${ $self->{_Init} }; # DESTROY will be called, so increment anyway + # These error messages are now suppressed - JD October 13, 2003 + # Thanks for the suggestion Ala. + ### carp "A $class object has ALREADY been created !"; + ### carp "The object returned is the original object for $class"; + # This means that either a module already called Tk::CursorControl on behalf + # of the user (i.e. via a 'use SomeModule' where the code within SomeModule + # creates a Tk::CursorControl object ---OR--- the programmer didn't read the + # documentation and tried to create two or more CursorControl objects for one + # MainWindow. + return ${ $self->{_CurrentObj} }; #return ORIGINALLY created object + } +} + +# For erroneous understanding of this Class! +sub _errmsg { croak "You cannot $_[1] a ", ref( $_[0] ); } + +########## Public NON-methods ########## +# Just in case someone treats this like a Tk widget +# Override geometry managers + +sub pack { $_[0]->_errmsg('pack') } +sub grid { $_[0]->_errmsg('grid') } +sub form { $_[0]->_errmsg('form') } +sub place { $_[0]->_errmsg('place') } +sub configure { $_[0]->_errmsg('configure') } +sub cget { $_[0]->_errmsg('cget') } + +########## Public Methods ########## +sub confine { + my ( $self, $widget ) = @_; + unless ( defined $widget ) { + carp "\$cursor->confine(\$widget)"; + return; + } + + #free the cursor if already confined elsewhere + $self->release if ( $self->{Confined} ); + + #does the widget exist? is it mapped? + return unless ( $self->_check($widget) ); + + if ( $self->{Type} eq 'win32' ) { + $self->_Win32confine($widget); + } + else { + + #Then $self->{Type} is the default 'unix' + $self->_Unixconfine($widget); + } +} + +sub release { + my $self = shift; + if ( $self->{Type} eq 'win32' ) { + $self->_Win32release; + } + else { + + #Then $self->{Type} is the default 'unix' + $self->_Unixrelease; + } +} + +sub hide { + my $self = shift; + my $w; + + foreach $w (@_) { + + #bind to Enter and Leave Events + if ( $self->{Type} eq 'win32' ) { + + #Showcursor is a system-wide API - so we want to ensure that the cursor doesn't + #disappear forever! + $self->_Win32saveBindings($w); + $self->_Win32setBindings($w); + $self->_Win32hidecursor; + } + else { + $self->_setOldCursor($w); + $w->configure( -cursor => + [ '@' . $self->{bitmapfile}, $self->{maskfile}, 'black', 'white' ] ); + } + } +} + +sub show { + my $self = shift; + + foreach my $w (@_) { + + #delete bindings for hide Events for specified widget + if ( $self->{Type} eq 'win32' ) { + $self->_Win32restoreBindings($w); + $self->_Win32showcursor; + } + else { + my $cursor = $self->_getOldCursor($w); + $w->configure( -cursor => $cursor ) if ($cursor); + } + } +} + +sub moveto { + +# Similar to the warpto sub. Instead - we always use the root window coordinates +# So, all we are interested in is the starting x,y coordinates and the ending +# x,y coordinates. Most of the code is a copy and paste from the warpto sub. + + my $self = shift; + my $w; + + #parse the time off the arguments...there has to be a better way! + my $movetime = 1000; #default to 1 second (1000ms) + if ( grep /time/, @_ ) { + my $i = 0; + my $timefound; + map { + if (/time/) { + splice @_, $i, 1; + $timefound = pop(@_); + $movetime = $timefound if ( $timefound =~ /\d+/ ); + } + $i++; + } @_; + } + + #minimum time allowed + $movetime = 25 if ( $movetime < 25 ); + +# Three ways of warping: +# 1. Pass a widget reference - default warp the cursor to the center. +# 2. Pass a widget reference and x,y value - warp the cursor to x,y of that widget. +# 3. Pass only an x,y coordinate (with no widget reference) then it is treated like +# a screen coordinate. + + my $finalx; + my $finaly; + my $startx = ${ $self->{MAIN} }->pointerx; + my $starty = ${ $self->{MAIN} }->pointery; + + my $argnum = scalar(@_); + my $ref = ref( $_[0] ); + if ( $ref and $ref =~ /^Tk/ ) { + $w = shift; + + # Does the widget exist and is it mapped? + return unless ( $self->_check($w) ); + + if ( $argnum == 1 ) { + + #assume only a widget reference passed + $self->release if ( $self->{Confined} ); + + #Get ROOT coordinates of the final position + $finalx = $w->rootx + ( $w->width / 2 ); + $finaly = $w->rooty + ( $w->height / 2 ); + + } + elsif ( $argnum == 3 ) { + + #assume a widget reference AND x,y value passed + my $x = shift; + my $y = shift; + + $self->release if ( $self->{Confined} ); + + #warp pointer to x,y coordinate relative to widgets NW corner + my $width = $w->width; + my $height = $w->height; + $x = 0 if ( $x < 0 ); + $x = $width - 1 if ( $x > $width ); + $y = 0 if ( $y < 0 ); + $y = $height - 1 if ( $y > $height ); + + $finalx = $w->rootx + $x; + $finaly = $w->rooty + $y; + } + } #end if $widget is passed + elsif ( $argnum == 2 ) { + + # Assume only an x,y value passed.. + + my $X = shift; + my $Y = shift; + + $self->release if ( $self->{Confined} ); + +# Sanity check - don't warp beyond the screen. The window managers won't let you +# anyways - but we might as well not try! + my $sw = ${ $self->{MAIN} }->screenwidth; + my $sh = ${ $self->{MAIN} }->screenheight; + + $X = 0 if ( $X < 0 ); + $Y = 0 if ( $Y < 0 ); + $X = $sw if ( $X > $sw ); + $Y = $sh if ( $Y > $sh ); + + $finalx = $X; + $finaly = $Y; + + } + + return unless ( defined $finalx and defined $finaly ); + + #finally "move" the cursor (based on time passed) + my $denom = $movetime / 25; + my $deltax = ( $finalx - $startx ) / $denom; + my $deltay = ( $finaly - $starty ) / $denom; + my $interx = $startx; + my $intery = $starty; + for ( my $i = 1 ; $i <= $denom ; $i++ ) { + $interx = $interx + $deltax; + $intery = $intery + $deltay; + $self->warpto( $interx, $intery ); + ${ $self->{MAIN} }->update; + + # Blocking is actually a good thing here. + ${ $self->{MAIN} }->after(25); + } + + # Now make sure we end up where we originally wanted ! + # Round off errors may have crept in. + $self->warpto( $finalx, $finaly ); + +} + +sub warpto { + + my $self = shift; + my $w; + +# Three ways of warping: +# 1. Pass a widget reference - default warp the cursor to the center. +# 2. Pass a widget reference and x,y value - warp the cursor to x,y of that widget. +# 3. Pass only an x,y coordinate (with no widget reference) then it is treated like +# a screen coordinate. + + my $argnum = scalar(@_); + my $ref = ref( $_[0] ); + if ( $ref and $ref =~ /^Tk/ ) { + $w = shift; + + # Does the widget exist and is it mapped? + return unless ( $self->_check($w) ); + + if ( $argnum == 1 ) { + + #assume only a widget reference passed + $self->release if ( $self->{Confined} ); + + #warp pointer to the center of the widget + my $x = ( $w->width / 2 ); + my $y = ( $w->height / 2 ); + + $w->eventGenerate( + "", + -when => 'head', + -x => $x, + -y => $y, + -warp => 1 + ); + } + elsif ( $argnum == 3 ) { + + #assume a widget reference AND x,y value passed + my $x = shift; + my $y = shift; + + $self->release if ( $self->{Confined} ); + + #warp pointer to x,y coordinate relative to widgets NW corner + my $width = $w->width; + my $height = $w->height; + $x = 0 if ( $x < 0 ); + $x = $width - 1 if ( $x > $width ); + $y = 0 if ( $y < 0 ); + $y = $height - 1 if ( $y > $height ); + + $w->eventGenerate( + "", + -when => 'head', + -x => $x, + -y => $y, + -warp => 1 + ); + } + } #end if $widget is passed + elsif ( $argnum == 2 ) { + +# Assume only an x,y value passed.. +# Warp to specific screen coordinates. There is no way to do this outright using +# eventGenerate...at least "I" couldn't find one. +# So we use the rootx and rooty of the MainWindow as our anchor position. +# and we might end up actually warping to negative values of x and y. + + my $X = shift; + my $Y = shift; + + $self->release if ( $self->{Confined} ); + + # works on windows even if iconified - check on unix. Maybe not needed!! + # return unless ( $self->_check(${$self->{MAIN}}) ); + +# Sanity check - don't warp beyond the screen. The window managers won't let you +# anyways - but we might as well not try! + my $sw = ${ $self->{MAIN} }->screenwidth; + my $sh = ${ $self->{MAIN} }->screenheight; + + $X = 0 if ( $X < 0 ); + $Y = 0 if ( $Y < 0 ); + $X = $sw if ( $X > $sw ); + $Y = $sh if ( $Y > $sh ); + + my $x = $X - ${ $self->{MAIN} }->rootx; + my $y = $Y - ${ $self->{MAIN} }->rooty; + + ${ $self->{MAIN} }->eventGenerate( + "", + -when => 'head', + -x => $x, + -y => $y, + -warp => 1 + ); + } + +} + +sub destroy { shift->DESTROY } + +############# Private methods ################## +sub _check { + my ( $self, $w ) = @_; + my $ok = 1; + unless ( $ok = Exists($w) ) { + carp "Widget $w: Does not exist!"; + } + unless ( $ok = $w->viewable ) { + carp "Widget $w: is not mapped"; + } + return $ok; +} + +sub _getposition { + + #return the top left corner and width/height of widget passed. + my ( $self, $w ) = @_; + my $x0 = $w->rootx; + my $y0 = $w->rooty; + my $width = $w->width; + my $height = $w->height; + + return ( $x0, $y0, $width, $height ); +} + +sub _getbbox { + + #return the absolute screen coordinates (i.e. bbox) + my ( $self, $w ) = @_; + my @c = $self->_getposition($w); + return ( $c[0], $c[1], $c[0] + $c[2], $c[1] + $c[3] ); +} + +sub _init { + my $self = shift; + + $self->{Type} = 'unix'; #default to 'perl based' cursor confine + + if ( $Tk::platform eq 'MSWin32' ) { + if ( eval "require Win32::API" ) { + + #Create API's + $self->{ClipCursor} = + Win32::API->new( 'user32', 'ClipCursor', ['P'], 'N' ); + $self->{ShowCursor} = + Win32::API->new( 'user32', 'ShowCursor', ['N'], 'N' ); + $self->{Type} = 'win32' if ( $self->{ClipCursor} && $self->{ShowCursor} ); + $self->{DisplayCount} = 0; + return if $self->{Type} eq 'win32'; + croak "Creating API objects failed for unknown reason"; + } + else { + croak "Please install Win32::API !!"; + } + } + else { + + #retrieve proper filenames for transparent cursor for *NIX.. + $self->{bitmapfile} = Tk->findINC('trans_cur.xbm'); + $self->{maskfile} = Tk->findINC('trans_cur.mask'); + croak "Files for tranparent cursor not found" + unless ( $self->{bitmapfile} && $self->{maskfile} ); + } +} + +####### On Unix Only ############# +sub _setOldCursor { + + #save the current cursor on a ->hide command + my ( $self, $w ) = @_; + my $oldcursor = $w->cget('-cursor') || 'left_ptr'; + $self->{OldCursor}{$w} = $oldcursor if ($oldcursor); +} + +sub _getOldCursor { + + #get saved cursor on a ->show command + my ( $self, $w ) = @_; + my $cursor = $self->{OldCursor}{$w}; + delete $self->{OldCursor}{$w}; + return ($cursor); +} + +sub _Unixconfine { + my ( $self, $w ) = @_; + my @coords = $self->_getposition($w); + +# Stop Class bindings from being triggered - as a Leave should NEVER occur +# Why? Because the cursor is 'supposed' to be confined to the widget! Since this +# event 'will' still occur we Tk->break it before the Class binding occurs. +# An example if we did not do this (and I know from testing) is: a Button relief rapidly +# changing between sunken and normal to causing massive flickering + +# Also - We cannot guarantee a Motion binding will get triggered for the passed widget. +# Instead we overwrite the Motion binding for the toplevel containg $widget. This virtually +# guarantees (fingers crossed) that a proper Motion binding exists. Of course the Leave +# event must stay with the passed $widget. + + my @bindtags = $w->bindtags; + $w->bindtags( [ @bindtags[ 1, 0, 2, 3 ] ] ); + + #Save current leave binding..if there is one + my $old_leave; + my $old_motion; + eval { $old_leave = $w->Tk::bind('') }; + eval { $old_motion = $w->toplevel->bind('') }; + ( defined $old_leave ) + ? ( $self->{OldLeave} = $old_leave ) + : ( $self->{OldLeave} = 0 ); + ( defined $old_motion ) + ? ( $self->{OldMotion} = $old_motion ) + : ( $self->{OldMotion} = 0 ); + + $w->Tk::bind( '', sub { $_[0]->break } ); + + $w->toplevel->bind( + '', + sub { + $self->{OldMotion}->Call if ( $self->{OldMotion} ); + $self->_warpToConfine( $w, @coords ); + } + ); + $self->{Confined} = $w; +} + +sub _Unixrelease { + my $self = shift; + + #Restore proper bindtag order for widget.. + return unless $self->{Confined}; + my @bindtags = $self->{Confined}->bindtags; + $self->{Confined}->bindtags( [ @bindtags[ 1, 0, 2, 3 ] ] ); + + ( $self->{OldLeave} ) + ? ( $self->{Confined}->Tk::bind( '', $self->{OldLeave} ) ) + : ( $self->{Confined}->Tk::bind( '', '' ) ); + ( $self->{OldMotion} ) + ? ( $self->{Confined}->toplevel->bind( '', $self->{OldMotion} ) ) + : ( $self->{Confined}->toplevel->bind( '', '' ) ); + + $self->{Confined} = 0; +} + +sub _warpToConfine { + my ( $self, $w, $x0, $y0, $wi, $he ) = @_; + + my $e = $w->XEvent; + my ( $x, $y ) = ( $e->x, $e->y ); + + my $warpneeded = 0; + + if ( $x <= 0 ) { + $x = 1; + $warpneeded = 1; + } + elsif ( $x >= $wi ) { + $x = $wi - 1; + $warpneeded = 1; + } + + if ( $y <= 0 ) { + $y = 1; + $warpneeded = 1; + } + elsif ( $y >= $he ) { + $y = $he - 1; + $warpneeded = 1; + } + + if ($warpneeded) { + $w->eventGenerate( + "", + -when => 'head', + -x => $x, + -y => $y, + -warp => 1 + ); + } +} + +########### On Win32 Only ############# +sub _Win32confine { + my ( $self, $w ) = @_; + my @coords = $self->_getbbox($w); + + my $rect = CORE::pack "L4", @coords; + + if ( defined $self->{ClipCursor} ) { + $self->{ClipCursor}->Call($rect); + $self->{Confined} = 1; + } +} + +sub _Win32release { + my $self = shift; + my $null = 0; + if ( defined $self->{ClipCursor} ) { + $self->{ClipCursor}->Call($null); + $self->{Confined} = 0; + } +} + +sub _Win32saveBindings { + my ( $self, $w ) = @_; + + # Save current Enter, Leave and Unmap bindings..if there are any + # Fully specify Tk::bind in case of a canvas widget. + my $old_leave; + my $old_enter; + my $old_unmap; + eval { $old_leave = $w->Tk::bind('') }; + eval { $old_enter = $w->Tk::bind('') }; + eval { $old_unmap = $w->Tk::bind('') }; + ( defined $old_leave ) + ? ( $self->{Win32Leave}{$w} = $old_leave ) + : ( $self->{Win32Leave}{$w} = 0 ); + ( defined $old_enter ) + ? ( $self->{Win32Enter}{$w} = $old_enter ) + : ( $self->{Win32Enter}{$w} = 0 ); + ( defined $old_unmap ) + ? ( $self->{Win32Unmap}{$w} = $old_unmap ) + : ( $self->{Win32Unmap}{$w} = 0 ); +} + +sub _Win32setBindings { + my ( $self, $w ) = @_; + $w->Tk::bind( + '', + sub { + $self->{Win32Enter}{$w}->Call if ( $self->{Win32Enter}{$w} ); + $self->_Win32hidecursor; + } + ); + $w->Tk::bind( + '', + sub { + $self->{Win32Leave}{$w}->Call if ( $self->{Win32Leave}{$w} ); + $self->_Win32showcursor; + } + ); + + #ensure cursor gets shown again if widget disappears.. + $w->Tk::bind( + '', + sub { + $self->{Win32Unmap}{$w}->Call if ( $self->{Win32Unmap}{$w} ); + $self->_Win32showcursor; + } + ); +} + +sub _Win32restoreBindings { + my ( $self, $w ) = @_; + ( $self->{Win32Enter}{$w} ) + ? ( $w->Tk::bind( '', $self->{Win32Enter}{$w} ) ) + : ( $w->Tk::bind( '', '' ) ); + ( $self->{Win32Leave}{$w} ) + ? ( $w->Tk::bind( '', $self->{Win32Leave}{$w} ) ) + : ( $w->Tk::bind( '', '' ) ); + ( $self->{Win32Unmap}{$w} ) + ? ( $w->Tk::bind( '', $self->{Win32Unmap}{$w} ) ) + : ( $w->Tk::bind( '', '' ) ); +} + +sub _Win32hidecursor { + my $self = shift; + $self->_Win32resetDisplayCount; + $self->{DisplayCount} = $self->{ShowCursor}->Call(0); +} + +sub _Win32showcursor { + my $self = shift; + $self->_Win32resetDisplayCount; + $self->{DisplayCount} = $self->{ShowCursor}->Call(1); +} + +sub _Win32resetDisplayCount { + my $self = shift; + + #Decrement display count to get ready for a hide. i.e. set count to 0. + if ( $self->{DisplayCount} > 0 ) { + my $count = $self->{DisplayCount}; + for ( my $i = $count ; $i > 0 ; $i-- ) { + $self->{DisplayCount} = $self->{ShowCursor}->Call(0); + } + } + + #Increment display count to get ready for a show. i.e. set count to -1. + elsif ( $self->{DisplayCount} < 0 ) { + my $count = $self->{DisplayCount}; + for ( my $i = $count ; $i < -1 ; $i++ ) { + $self->{DisplayCount} = $self->{ShowCursor}->Call(1); + } + } +} + +######################################## +sub DESTROY { + my $self = shift; + $self->release; + ##carp "DESTROY CALLED ON $self"; + --${ $self->{_Init} }; #decrement initialize value +} + +1; + +__END__ + +=head1 NAME + +Tk::CursorControl - Manipulate the mouse cursor programmatically + +=head1 SYNOPSIS + + use Tk::CursorControl; + $cursor = $main->CursorControl; + + # Lock the mouse cursor to $widget + $cursor->confine($widget); + + # Free the cursor + $cursor->release; + + # cursor disappears over $widget + $cursor->hide($widget); + + # show cursor again over $widget + $cursor->show($widget); + + # warp cursor to $widget (jump) + $cursor->warpto($widget); + + # move cursor to $widget + $cursor->moveto($widget); + +=head1 DESCRIPTION + +B is-B-a Tk::Widget. +Rather, it I Tk and encompasses a collection of methods +used to manipulate the cursor I<(aka pointer)> programmatically +from a Tk program. + +=head1 STANDARD OPTIONS + +B does I accept any standard options + +=head1 METHODS + +The following methods are available: + +=over 4 + +=item I<$cursor>-EB( $widget ) + +Confine the cursor to stay within the bounding box of $widget. + +=over 4 + +=item I<$cursor>-EB( $widget ) + +Alias for the B method. + +=back + +=item I<$cursor>-EB + +Release the cursor. Used to restore proper cursor functionality +after a confine. Note: I<$widget> does B need to be specified. + +=over 4 + +=item I<$cursor>-EB + +Alias for the B method. + +=back + +=item I<$cursor>-EB( @widgets ) + +Make cursor I over each widget in @widgets. + +=item I<$cursor>-EB( @widgets ) + +Make cursor I over each widget in @widgets. This is used after a B. +Bhow (capital S) can be used as well. + +=item I<$cursor>-EB( $widget I) + +Warp the cursor to the specified I<(?x,y?)> position in $widget. If the x,y values +are not specified, then the I
    of the widget is used as the target. + +OR + +=item I<$cursor>-EB( X,Y ) + +Warp the cursor to the specified I screen coordinate. + +=item I<$cursor>-EB( $widget I, -time=EI) + +Move the cursor to the specified I<(?x,y?)> position in $widget in I<-time> milliseconds. +If the x,y values are not specified, then the I
    of the widget is used as the +target. The -time value defaults to 1000ms (1 second) if not specified. The smaller the +time, the faster the cursor will move. The time given will not be exact. See bugs below. + +OR + +=item I<$cursor>-EB( X,Y, -time=EI) + +Move the cursor to the specified I screen coordinate in I<-time> milliseconds. +The -time value defaults to 1000ms (1 second) if not specified. The smaller the +time, the faster the cursor will move. The time given will not be exact. See bugs below. + +=back + +=head1 DEPENDENCIES + +B is required on Win32 systems. + +=head1 POSSIBLE USES + +Don't e-mail me to debate whether or not a program I warp or +hide a cursor. I will give you a few instances where "I think" a +module like this could come in handy. + +1. Confining a canvas item to remain within the Canvas boundaries +on a move. See the cursor demonstration in 'widget'. + +2. Giving the user some 'leeway' on clicking near an item. Say, +clicking on the picture of a thermometer, warps the cursor to a +Tk::Scale (right beside it) which actually controls that thermometer. + +3. Confining a window within another window (Tk::MDI should be +upgraded to 'use Tk::CursorControl') + +4. A step by step, show and tell session on 'How to use this GUI'. + +5. Make the cursor disappear for a keyboard only Tk::Canvas game. + +The key to using this module properly, is subtlety! Don't start making +the cursor warp all over the screen or making it disappear sporadically. +That is a misuse of the functionality. + +For some 'real world' applications which already have these types of +functionality, see any Multiple Document Interface (MDI); such as in +Excel or Word). Also have a look at the Win32 color chooser. The cursor +will be confined to the color palette while the button is pressed. Also, +try clicking on the gradient bar to the right of the palette. See what +happens to the mouse cursor?! +I'll bet you didn't even know that this existed until now. + +If you discover another good use for this module, I would definitely +like to hear about it ! I is the type of e-mail I would welcome. + +=head1 BUGS & IDIOSYNCRASIES + +B + +B only allows ONE object per MainWindow! If you try +to create more than one, B. +This will also be true if using a widget or module which already defines +a Tk::CursorControl object. + +B + +B internally generates EEnterE, +ELeaveE and EMotionE bindings for the I<$widget> +passed. Any user-defined bindings of the same type for I<$widget> +should I get executed. This feature has not been completely +tested. + +B + +This module makes heavy use of the ShowCursor and ClipCursor API's on +Win32. Be aware that when you change a cursor using the API, you +are doing so for your entire system. You, (the programmer) are +responsible for generating the show/hide and confine/release commands +in the proper order. + +For every hide - you I<*will*> want a show. For every confine - you +I<*should*> have a release. There are cautionary measures built-in +to ensure that the cursor doesn't disappear forever or get locked +within a widget. + +i.e. A B is automatically called if you try to confine +the cursor to two widgets at the same time. + +In other words, the last B always wins! + +B + +The methods for hiding and confining the cursor on Unix-based systems +is different than for Win32. + +A blank cursor is defined using the Tk::Widget configure method for +each widget passed. Two files have been provided for this purpose in +the installation - I and I. These files +must exist under a BFindINC> directory. + +Confining a cursor on *nix does I use any sort of API or Xlib +calls. Motion events are generated on the toplevel window to confine +the cursor to the proper widget. On slow systems, this will make the +cursor I like it is attached to the widget sides with a spring. +On faster systems, while still there, this I type action +is much less noticible. + +B + +The time parameter passed to a moveto method will not be exact. The +reason for this is because a crude L command is +used to I for a very short period. You will find that the +actual time taken for the cursor to stop is alway slightly B +than the time you specified. This time difference will be greater +on slower computers. The time error will also increase for higher +time values. + +B + +Warping the cursor will cause problems for users of absolute location +pointing devices (like graphics tablets). Users of graphics tablets +should B use this module. + +=head1 AUTHOR + +B . + +Copyright (c) 2002-2004 Jack Dunnigan. All rights reserved. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +My thanks to Tk gurus Steve Lidie and Slaven Rezic for their suggestions +and their patches. This is my first module on CPAN and I appreciate +their help. Thanks to Ala Qumsieh for utilizing the power of my module +in L. + +=cut + + + + diff --git a/lib/site/Tk/ToolBar.pm b/lib/site/Tk/ToolBar.pm index d3f1ddd3f..54687f35e 100644 --- a/lib/site/Tk/ToolBar.pm +++ b/lib/site/Tk/ToolBar.pm @@ -1,1096 +1,1096 @@ - -package Tk::ToolBar; - -use strict; -use Tk::Frame; -use Tk::Balloon; - -use base qw/Tk::Frame/; -use Tk::widgets qw(Frame); - -use Carp; -use POSIX qw/ceil/; - -Construct Tk::Widget 'ToolBar'; - -use vars qw/$VERSION/; -$VERSION = 0.09; - -my $edgeH = 24; -my $edgeW = 5; - -my $sepH = 24; -my $sepW = 3; - -my %sideToSticky = qw( - top n - right e - left w - bottom s - ); - -my $packIn = ''; -my @allWidgets = (); -my $floating = 0; -my %packIn; -my %containers; -my %isDummy; - -1; - -sub ClassInit { - my ($class, $mw) = @_; - $class->SUPER::ClassInit($mw); - - # load the images. - my $imageFile = Tk->findINC('ToolBar/tkIcons'); - - if (defined $imageFile) { - local *F; - open F, $imageFile; - - local $_; - - while () { - chomp; - my ($n, $d) = (split /:/)[0, 4]; - - $mw->Photo($n, -data => $d); - } - close F; - } else { - carp <SUPER::Populate($args); - $self->{MW} = $self->parent; - $self->{SIDE} = exists $args->{-side} ? delete $args->{-side} : 'top'; - $self->{STICKY} = exists $args->{-sticky} ? delete $args->{-sticky} : 'nsew'; - $self->{USECC} = exists $args->{-cursorcontrol} ? delete $args->{-cursorcontrol} : 1; - $self->{STYLE} = exists $args->{-mystyle} ? delete $args->{-mystyle} : 0; - $packIn = exists $args->{-in} ? delete $args->{-in} : ''; - - if ($packIn) { - unless ($packIn->isa('Tk::ToolBar')) { - croak "value of -packin '$packIn' is not a Tk::ToolBar object"; - } else { - $self->{SIDE} = $packIn->{SIDE}; - } - } - - unless ($self->{STICKY} =~ /$sideToSticky{$self->{SIDE}}/) { - croak "can't place '$self->{STICKY}' toolbar on '$self->{SIDE}' side"; - } - - $self->{CONTAINER} = $self->{MW}->Frame; - $self->_packSelf; - - my $edge = $self->{CONTAINER}->Frame(qw/ - -borderwidth 2 - -relief ridge - /); - - $self->{EDGE} = $edge; - - $self->_packEdge($edge, 1); - - $self->ConfigSpecs( - -movable => [qw/METHOD movable Movable 1/], - -close => [qw/PASSIVE close Close 15/], - -activebackground => [qw/METHOD activebackground ActiveBackground/, Tk::ACTIVE_BG], - -indicatorcolor => [qw/PASSIVE indicatorcolor IndicatorColor/, '#00C2F1'], - -indicatorrelief => [qw/PASSIVE indicatorrelief IndicatorRelief flat/], - -float => [qw/PASSIVE float Float 1/], - ); - - push @allWidgets => $self; - - $containers{$self->{CONTAINER}} = $self; - - $self->{BALLOON} = $self->{MW}->Balloon; - - # check for Tk::CursorControl - $self->{CC} = undef; - if ($self->{USECC}) { - local $^W = 0; # suppress message from Win32::API - eval "require Tk::CursorControl"; - unless ($@) { - # CC is installed. Use it. - $self->{CC} = $self->{MW}->CursorControl; - } - } -} - -sub activebackground { - my ($self, $c) = @_; - - return unless $c; # ignore falses. - - $self->{ACTIVE_BG} = $c; -} - -sub _packSelf { - my $self = shift; - - my $side = $self->{SIDE}; - my $fill = 'y'; - if ($side eq 'top' or $side eq 'bottom') { $fill = 'x' } - - if ($packIn && $packIn != $self) { - my $side = $packIn->{SIDE} =~ /top|bottom/ ? 'left' : 'top'; - - $self->{CONTAINER}->pack(-in => $packIn->{CONTAINER}, - -side => $side, - -anchor => ($fill eq 'x' ? 'w' : 'n'), - -expand => 0); - $self->{CONTAINER}->raise; - $packIn{$self->{CONTAINER}} = $packIn->{CONTAINER}; - } else { - # force a certain look! for now. - my $slave = ($self->{MW}->packSlaves)[0]; - - $self->configure(qw/-relief raised -borderwidth 1/); - $self->pack(-side => $side, -fill => $fill, - $slave ? (-before => $slave) : () - ); - - $self->{CONTAINER}->pack(-in => $self, - -anchor => ($fill eq 'x' ? 'w' : 'n'), - -expand => 0); - - $packIn{$self->{CONTAINER}} = $self; - } -} - -sub _packEdge { - my $self = shift; - my $e = shift; - my $w = shift; - - my $s = $self->{SIDE}; - - my ($pack, $pad, $nopad, $fill); - - if ($s eq 'top' or $s eq 'bottom') { - if ($w) { - $e->configure(-height => $edgeH, -width => $edgeW); - } else { - $e->configure(-height => $sepH, -width => $sepW); - } - $pack = 'left'; - $pad = '-padx'; - $nopad = '-pady'; - $fill = 'y'; - } else { - if ($w) { - $e->configure(-height => $edgeW, -width => $edgeH); - } else { - $e->configure(-height => $sepW, -width => $sepH); - } - - $pack = 'top'; - $pad = '-pady'; - $nopad = '-padx'; - $fill = 'x'; - } - - if (exists $self->{SEPARATORS}{$e}) { - $e->configure(-cursor => $pack eq 'left' ? 'sb_h_double_arrow' : 'sb_v_double_arrow'); - $self->{SEPARATORS}{$e}->pack(-side => $pack, - -fill => $fill); - } - - $e->pack(-side => $pack, $pad => 5, - $nopad => 0, -expand => 0); -} - -sub movable { - my ($self, $value) = @_; - - if (defined $value) { - $self->{ISMOVABLE} = $value; - my $e = $self->_edge; - - if ($value) { - $e->configure(qw/-cursor fleur/); - $self->afterIdle(sub {$self->_enableEdge()}); - } else { - $e->configure(-cursor => undef); - $self->_disableEdge($e); - } - } - - return $self->{ISMOVABLE}; -} - -sub _enableEdge { - my ($self) = @_; - - my $e = $self->_edge; - my $hilte = $self->{MW}->Frame(-bg => $self->cget('-indicatorcolor'), - -relief => $self->cget('-indicatorrelief')); - - my $dummy = $self->{MW}->Frame( - qw/ - -borderwidth 2 - -relief ridge - /); - - $self->{DUMMY} = $dummy; - - my $drag = 0; - #my $floating = 0; - my $clone; - - my @mwSize; # extent of mainwindow. - - $e->bind('<1>' => sub { - $self->{CC}->confine($self->{MW}) if defined $self->{CC}; - my $geom = $self->{MW}->geometry; - my ($rx, $ry) = ($self->{MW}->rootx, $self->{MW}->rooty); - - if ($geom =~ /(\d+)x(\d+)/) {#\+(\d+)\+(\d+)/) { -# @mwSize = ($3, $4, $1 + $3, $2 + $4); - @mwSize = ($rx, $ry, $1 + $rx, $2 + $ry); - } else { - @mwSize = (); - } - - if (!$self->{ISCLONE} && $self->{CLONE}) { - $self->{CLONE}->destroy; - $self->{CLONE} = $clone = undef; - @allWidgets = grep Tk::Exists, @allWidgets; - } - - }); - - $e->bind('' => sub { - my ($x, $y) = ($self->pointerx - $self->{MW}->rootx - ceil($e->width /2) - $e->x, - $self->pointery - $self->{MW}->rooty - ceil($e->height/2) - $e->y); - - my ($px, $py) = $self->pointerxy; - - $dummy = $self->{ISCLONE} ? $self->{CLONE}{DUMMY} : $self->{DUMMY}; - - unless ($drag or $floating) { - $drag = 1; - $dummy->raise; - my $noclone = $self->{ISCLONE} ? $self->{CLONE} : $self; - $noclone->packForget; - $noclone->{CONTAINER}->pack(-in => $dummy); - $noclone->{CONTAINER}->raise; - ref($_) eq 'Tk::Frame' && $_->raise for $noclone->{CONTAINER}->packSlaves; - } - $hilte->placeForget; - - if ($self->cget('-float') && - (@mwSize and - $px < $mwSize[0] or - $py < $mwSize[1] or - $px > $mwSize[2] or - $py > $mwSize[3])) { - - # we are outside .. switch to toplevel mode. - $dummy->placeForget; - $floating = 1; - - unless ($self->{CLONE} || $self->{ISCLONE}) { - # clone it. - my $clone = $self->{MW}->Toplevel(qw/-relief ridge -borderwidth 2/); - $clone->withdraw; - $clone->overrideredirect(1); - $self->_clone($clone); - $self->{CLONE} = $clone; - } - - $clone = $self->{ISCLONE} || $self->{CLONE}; - $clone->deiconify unless $clone->ismapped; - $clone->geometry("+$px+$py"); - - } else { - $self->{ISCLONE}->withdraw if $self->{CLONE} && $self->{ISCLONE}; - - $dummy->place('-x' => $x, '-y' => $y); - $floating = 0; - - if (my $newSide = $self->_whereAmI($x, $y)) { - # still inside main window. - # highlight the close edge. - $clone && $clone->ismapped && $clone->withdraw; - #$self->{ISCLONE}->withdraw if $self->{CLONE} && $self->{ISCLONE}; - - my ($op, $pp); - if ($newSide =~ /top/) { - $op = [qw/-height 5/]; - $pp = [qw/-relx 0 -relwidth 1 -y 0/]; - } elsif ($newSide =~ /bottom/) { - $op = [qw/-height 5/]; - $pp = [qw/-relx 0 -relwidth 1 -y -5 -rely 1/]; - } elsif ($newSide =~ /left/) { - $op = [qw/-width 5/]; - $pp = [qw/-x 0 -relheight 1 -y 0/]; - } elsif ($newSide =~ /right/) { - $op = [qw/-width 5/]; - $pp = [qw/-x -5 -relx 1 -relheight 1 -y 0/]; - } - - $hilte->configure(@$op); - $hilte->place(@$pp); - $hilte->raise; - } - } - }); - - $e->bind('' => sub { - my $noclone = $self->{ISCLONE} ? $self->{CLONE} : $self; - $noclone->{CC}->free($noclone->{MW}) if defined $noclone->{CC}; - return unless $drag; - - $drag = 0; - $dummy->placeForget; - - # forget everything if it's cloned. - return if $clone && $clone->ismapped; - - # destroy the clone. - #$clone->destroy; - - #return unless $self->_whereAmI(1); - $noclone->_whereAmI(1); - $hilte->placeForget; - - # repack everything now. - my $ec = $noclone->_edge; - my @allSlaves = grep {$_ ne $ec} $noclone->{CONTAINER}->packSlaves; - $_ ->packForget for $noclone, @allSlaves, $noclone->{CONTAINER}; - - $noclone->_packSelf; - $noclone->_packEdge($ec, 1); - $noclone->_packWidget($_) for @allSlaves; - }); -} - -sub _whereAmI { - my $self = shift; - - my $flag = 0; - my ($x, $y); - - if (@_ == 1) { - $flag = shift; - my $e = $self->_edge; - ($x, $y) = ($self->pointerx - $self->{MW}->rootx - ceil($e->width /2) - $e->x, - $self->pointery - $self->{MW}->rooty - ceil($e->height/2) - $e->y); - } else { - ($x, $y) = @_; - } - - my $x2 = $x + $self->{CONTAINER}->width; - my $y2 = $y + $self->{CONTAINER}->height; - - my $w = $self->{MW}->Width; - my $h = $self->{MW}->Height; - - # bound check - $x = 1 if $x <= 0; - $y = 1 if $y <= 0; - $x = $w - 1 if $x >= $w; - $y = $h - 1 if $y >= $h; - - $x2 = 0 if $x2 <= 0; - $y2 = 0 if $y2 <= 0; - $x2 = $w - 1 if $x2 >= $w; - $y2 = $h - 1 if $y2 >= $h; - - my $dx = 0; - my $dy = 0; - - my $close = $self->cget('-close'); - - if ($x < $close) { $dx = $x } - elsif ($w - $x2 < $close) { $dx = $x2 - $w } - - if ($y < $close) { $dy = $y } - elsif ($h - $y2 < $close) { $dy = $y2 - $h } - - $packIn = ''; - if ($dx || $dy) { - my $newSide; - if ($dx && $dy) { - # which is closer? - if (abs($dx) < abs($dy)) { - $newSide = $dx > 0 ? 'left' : 'right'; - } else { - $newSide = $dy > 0 ? 'top' : 'bottom'; - } - } elsif ($dx) { - $newSide = $dx > 0 ? 'left' : 'right'; - } else { - $newSide = $dy > 0 ? 'top' : 'bottom'; - } - - # make sure we're stickable on that side. - return undef unless $self->{STICKY} =~ /$sideToSticky{$newSide}/; - - $self->{SIDE} = $newSide if $flag; - return $newSide; - } elsif ($flag) { - # check for overlaps. - for my $w (@allWidgets) { - next if $w == $self; - - my $x1 = $w->x; - my $y1 = $w->y; - my $x2 = $x1 + $w->width; - my $y2 = $y1 + $w->height; - - if ($x > $x1 and $y > $y1 and $x < $x2 and $y < $y2) { - $packIn = $w; - last; - } - } - - $self->{SIDE} = $packIn->{SIDE} if $packIn; -# if ($packIn) { -# $self->{SIDE} = $packIn->{SIDE}; -# } else { -# return undef; -# } - } else { - return undef; - } - - return 1; -} - -sub _disableEdge { - my ($self, $e) = @_; - - $e->bind('' => undef); - $e->bind('' => undef); -} - -sub _edge { - $_[0]->{EDGE}; -} - -sub ToolButton { - my $self = shift; - my %args = @_; - - my $type = delete $args{-type} || 'Button'; - - unless ($type eq 'Button' or - $type eq 'Checkbutton' or - $type eq 'Menubutton' or - $type eq 'Radiobutton') { - - croak "toolbutton can be only 'Button', 'Menubutton', 'Checkbutton', or 'Radiobutton'"; - } - - my $m = delete $args{-tip} || ''; - my $x = delete $args{-accelerator} || ''; - - my $b = $self->{CONTAINER}->$type(%args, - $self->{STYLE} ? () : ( - -relief => 'flat', - -borderwidth => 1, - ), - ); - - $self->_createButtonBindings($b); - $self->_configureWidget ($b); - - push @{$self->{WIDGETS}} => $b; - $self->_packWidget($b); - - $self->{BALLOON}->attach($b, -balloonmsg => $m) if $m; - $self->{MW}->bind($x => [$b, 'invoke']) if $x; - - # change the bind tags. - #$b->bindtags([$b, ref($b), $b->toplevel, 'all']); - - return $b; -} - -sub ToolLabel { - my $self = shift; - - my $l = $self->{CONTAINER}->Label(@_); - - push @{$self->{WIDGETS}} => $l; - - $self->_packWidget($l); - - return $l; -} - -sub ToolEntry { - my $self = shift; - my %args = @_; - - my $m = delete $args{-tip} || ''; - my $l = $self->{CONTAINER}->Entry(%args, -width => 5); - - push @{$self->{WIDGETS}} => $l; - - $self->_packWidget($l); - $self->{BALLOON}->attach($b, -balloonmsg => $m) if $m; - - return $l; -} - -sub ToolLabEntry { - my $self = shift; - my %args = @_; - - require Tk::LabEntry; - my $m = delete $args{-tip} || ''; - my $l = $self->{CONTAINER}->LabEntry(%args, -width => 5); - - push @{$self->{WIDGETS}} => $l; - - $self->_packWidget($l); - $self->{BALLOON}->attach($b, -balloonmsg => $m) if $m; - - return $l; -} - -sub ToolOptionmenu { - my $self = shift; - my %args = @_; - - my $m = delete $args{-tip} || ''; - my $l = $self->{CONTAINER}->Optionmenu(%args); - - push @{$self->{WIDGETS}} => $l; - - $self->_packWidget($l); - $self->{BALLOON}->attach($b, -balloonmsg => $m) if $m; - - return $l; -} - -sub separator { - my $self = shift; - my %args = @_; - - my $move = 1; - $move = $args{-movable} if exists $args{-movable}; - my $just = $args{-space} || 0; - - my $f = $self->{CONTAINER}->Frame(-width => $just, -height => 0); - - my $sep = $self->{CONTAINER}->Frame(qw/ - -borderwidth 5 - -relief sunken - /); - - $isDummy{$f} = $self->{SIDE}; - - push @{$self->{WIDGETS}} => $sep; - $self->{SEPARATORS}{$sep} = $f; - $self->_packWidget($sep); - - $self->_createSeparatorBindings($sep) if $move; - - if ($just eq 'right' || $just eq 'bottom') { - # just figure out the good width. - } - - return 1; -} - -sub _packWidget { - my ($self, $b) = @_; - - return $self->_packEdge($b) if exists $self->{SEPARATORS}{$b}; - - my ($side, $pad, $nopad) = $self->{SIDE} =~ /^top$|^bottom$/ ? - qw/left -padx -pady/ : qw/top -pady -padx/; - - if (ref($b) eq 'Tk::LabEntry') { - $b->configure(-labelPack => [-side => $side]); - } - - my @extra; - if (exists $packIn{$b}) { - @extra = (-in => $packIn{$b}); - - # repack everything now. - my $top = $containers{$b}; - $top->{SIDE} = $self->{SIDE}; - - my $e = $top->_edge; - my @allSlaves = grep {$_ ne $e} $b->packSlaves; - $_ ->packForget for @allSlaves; - - $top->_packEdge($e, 1); - $top->_packWidget($_) for @allSlaves; - } - - if (exists $isDummy{$b}) { # swap width/height if we need to. - my ($w, $h); - - if ($side eq 'left' && $isDummy{$b} =~ /left|right/) { - $w = 0; - $h = $b->height; - } elsif ($side eq 'top' && $isDummy{$b} =~ /top|bottom/) { - $w = $b->width; - $h = 0; - } - - $b->configure(-width => $h, -height => $w) if defined $w; - $isDummy{$b} = $self->{SIDE}; - } - - $b->pack(-side => $side, $pad => 4, $nopad => 0, @extra); -} - -sub _packWidget_old { - my ($self, $b) = @_; - - return $self->_packEdge($b) if exists $self->{SEPARATORS}{$b}; - - my ($side, $pad, $nopad) = $self->{SIDE} =~ /^top$|^bottom$/ ? - qw/left -padx -pady/ : qw/top -pady -padx/; - - if (ref($b) eq 'Tk::LabEntry') { - $b->configure(-labelPack => [-side => $side]); - } - - my @extra; - if (exists $packIn{$b}) { - @extra = (-in => $packIn{$b}); - - # repack everything now. - my $top = $containers{$b}; - $top->{SIDE} = $self->{SIDE}; - - my $e = $top->_edge; - my @allSlaves = grep {$_ ne $e} $b->packSlaves; - $_ ->packForget for @allSlaves; - - $top->_packEdge($e, 1); - $top->_packWidget($_) for @allSlaves; - } - - $b->pack(-side => $side, $pad => 4, $nopad => 0, @extra); -} - -sub _configureWidget { - my ($self, $w) = @_; - - $w->configure(-activebackground => $self->{ACTIVE_BG}); -} - -sub _createButtonBindings { - my ($self, $b) = @_; - - my $bg = $b->cget('-bg'); - - $b->bind('' => [$b, 'configure', qw/-relief raised/]); - $b->bind('' => [$b, 'configure', qw/-relief flat/]); -} - -sub _createSeparatorBindings { - my ($self, $s) = @_; - - my ($ox, $oy); - - $s->bind('<1>' => sub { - $ox = $s->XEvent->x; - $oy = $s->XEvent->y; - }); - - $s->bind('' => sub { - my $x = $s->XEvent->x; - my $y = $s->XEvent->y; - - my $f = $self->{SEPARATORS}{$s}; - - if ($self->{SIDE} =~ /top|bottom/) { - my $dx = $x - $ox; - - my $w = $f->width + $dx; - $w = 0 if $w < 0; - - $f->GeometryRequest($w, $f->height); - } else { - my $dy = $y - $oy; - - my $h = $f->height + $dy; - $h = 0 if $h < 0; - - $f->GeometryRequest($f->width, $h); - } - }); -} - -sub Button { goto &ToolButton } -sub Label { goto &ToolLabel } -sub Entry { goto &ToolEntry } -sub LabEntry { goto &ToolLabEntry } -sub Optionmenu { goto &ToolOptionmenu } - -sub _clone { - my ($self, $top, $in) = @_; - - my $new = $top->ToolBar(qw/-side top -cursorcontrol/, $self->{USECC}, ($in ? (-in => $in, -movable => 0) : ())); - my $e = $self->_edge; - - my @allSlaves = grep {$_ ne $e} $self->{CONTAINER}->packSlaves; - for my $w (@allSlaves) { - my $t = ref $w; - $t =~ s/Tk:://; - - if ($t eq 'Frame' && exists $containers{$w}) { # embedded toolbar - my $obj = $containers{$w}; - $obj->_clone($top, $new); - } - - if ($t eq 'Frame' && exists $self->{SEPARATORS}{$w}) { # separator - $new->separator; - } - - my %c = map { $_->[0], $_->[4] || $_->[3] } grep {defined $_->[4] || $_->[3] } grep @$_ > 2, $w->configure; - delete $c{$_} for qw/-offset -class -tile -visual -colormap -labelPack/; - - if ($t =~ /.button/) { - $new->Button(-type => $t, - %c); - } else { - $new->$t(%c); - } - } - - $new ->{MW} = $self->{MW}; - $new ->{CLONE} = $self; - $new ->{ISCLONE} = $top; - $self->{ISCLONE} = 0; -} - -__END__ - -=pod - -=head1 NAME - -Tk::ToolBar - A toolbar widget for Perl/Tk - -=for category Tk Widget Classes - -=head1 SYNOPSIS - - use Tk; - use Tk::ToolBar; - - my $mw = new MainWindow; - my $tb = $mw->ToolBar(qw/-movable 1 -side top - -indicatorcolor blue/); - - $tb->ToolButton (-text => 'Button', - -tip => 'tool tip', - -command => sub { print "hi\n" }); - $tb->ToolLabel (-text => 'A Label'); - $tb->Label (-text => 'Another Label'); - $tb->ToolLabEntry(-label => 'A LabEntry', - -labelPack => [-side => "left", - -anchor => "w"]); - - my $tb2 = $mw->ToolBar; - $tb2->ToolButton(-image => 'navback22', - -tip => 'back', - -command => \&back); - $tb2->ToolButton(-image => 'navforward22', - -tip => 'forward', - -command => \&forward); - $tb2->separator; - $tb2->ToolButton(-image => 'navhome22', - -tip => 'home', - -command => \&home); - $tb2->ToolButton(-image => 'actreload22', - -tip => 'reload', - -command => \&reload); - - MainLoop; - -=head1 DESCRIPTION - -This module implements a dockable toolbar. It is in the same spirit as the -"short-cut" toolbars found in most major applications, such as most web browsers -and text editors (where you find the "back" or "save" and other shortcut buttons). - -Buttons of any type (regular, menu, check, radio) can be created inside this widget. -You can also create Label, Entry and LabEntry widgets. -Moreover, the ToolBar itself can be made dockable, such that it can be dragged to -any edge of your window. Dragging is done in "real-time" so that you can see the -contents of your ToolBar as you are dragging it. Furthermore, if you are close to -a stickable edge, a visual indicator will show up along that edge to guide you. -ToolBars can be made "floatable" such that if they are dragged beyond their -associated window, they will detach and float on the desktop. -Also, multiple ToolBars are embeddable inside each other. - -If you drag a ToolBar to within 15 pixels of an edge, it will stick to that -edge. If the ToolBar is further than 15 pixels away from an edge and still -inside the window, but you -release it over another ToolBar widget, then it will be embedded inside the -second ToolBar. You can "un-embed" an embedded ToolBar simply by dragging it -out. You can change the 15 pixel limit using the B<-close> option. - -Various icons are built into the Tk::ToolBar widget. Those icons can be used -as images for ToolButtons (see L). A demo program is bundled with -the module that should be available under the 'User Contributed Demonstrations' -when you run the B program. Run it to see a list of the available -images. - -Tk::ToolBar attempts to use Tk::CursorControl if it's already installed on -the system. You can further control this using the I<-cursorcontrol> option. -See L. - -The ToolBar is supposed to be created as a child of a Toplevel (MainWindow is -a Toplevel widget) or a Frame. You are free to experiment otherwise, -but expect the unexpected :-) - -=head1 WIDGET-SPECIFIC OPTIONS - -The ToolBar widget takes the following arguments: - -=over 4 - -=item B<-side> - -This option tells the ToolBar what edge to I stick to. Can be one of 'top', 'bottom', -'left' or 'right'. Defaults to 'top'. This option can be set only during object -creation. Default is 'top'. - -=item B<-movable> - -This option specifies whether the ToolBar is dockable or not. A dockable ToolBar -can be dragged around with the mouse to any edge of the window, subject to the -sticky constraints defined by I<-sticky>. Default is 1. - -=item B<-close> - -This option specifies, in pixels, how close we have to drag the ToolBar an edge for the -ToolBar to stick to it. Default is 15. - -=item B<-sticky> - -This option specifies which sides the toolbar is allowed to stick to. The value -must be a string of the following characters 'nsew'. A string of 'ns' means that -the ToolBar can only stick to the north (top) or south (bottom) sides. Defaults to -'nsew'. This option can be set only during object creation. - -=item B<-in> - -This option allows the toolbar to be embedded within another already instantiated -Tk::ToolBar object. The value must be a Tk::ToolBar object. This option can be set -only during object creation. - -=item B<-float> - -This option specifies whether the toolbar should "float" on the desktop if -dragged outside of the window. It defaults to 1. Note that this value is -ignored if I<-cursorcontrol> is set to 1. - -=item B<-cursorcontrol> - -This option specifies whether to use Tk::CursorControl to confine the cursor -during dragging. The value must be either 1 or 0. The default is 1 which -checks for Tk::CursorControl and uses it if present. - -=item B<-mystyle> - -This option indicates that you want to control how the ToolBar looks like -and not rely on Tk::ToolBar's own judgement. The value must be either -1 or 0. For now, the only thing this controls is the relief of ToolButtons -and the borderwidth. Defaults to 0. - -=item B<-indicatorcolor> - -This option controls the color of the visual indicator that tells you -whether you are close enough to an edge when dragging the ToolBar. -Defaults to some shade of blue and green (I like it :P). - -=item B<-indicatorrelief> - -This option controls the relief of the visual indicator that tells you -whether you are close enough to an edge when dragging the ToolBar. -Defaults to flat. - -=back - -=head1 WIDGET METHODS - -The following methods are used to create widgets that are placed inside -the ToolBar. Widgets are ordered in the same order they are created, left to right. - -For all widgets, except Labels, a tooltip can be specified via the B<-tip> option. -An image can be specified using the -image option for Button- and Label-based widgets. - -=over 4 - -=item I<$ToolBar>-EB(?-type => I,? I) - -=item I<$ToolBar>-EB
  • - -
    Date TimeVehicle Heading and SpeedLocationNew Location
    -
    - - - - -
    -

     

    -
    - - + + + + + + + +
    +

     

    +
    + + - - + + diff --git a/web/iphone/icons/LICENSE.TXT b/web/iphone/icons/LICENSE.TXT index 29719c080..922d4ab6f 100644 --- a/web/iphone/icons/LICENSE.TXT +++ b/web/iphone/icons/LICENSE.TXT @@ -1,7 +1,7 @@ -Diese Buttons sind ausschließlich zur privaten Nutzung freigegeben. -Bei kommerzieller Nutzung kontaktieren Sie bitte für eine Lizenz r(at)lf-klueber(dot)de - -This is only for using non commercial sites! For a commercial licence contact r(at)lf-klueber(dot)de - -Ralf -18.09.2008 +Diese Buttons sind ausschließlich zur privaten Nutzung freigegeben. +Bei kommerzieller Nutzung kontaktieren Sie bitte für eine Lizenz r(at)lf-klueber(dot)de + +This is only for using non commercial sites! For a commercial licence contact r(at)lf-klueber(dot)de + +Ralf +18.09.2008 diff --git a/web/lib/android.xsl b/web/lib/android.xsl index 7d6106399..6fd6dec09 100644 --- a/web/lib/android.xsl +++ b/web/lib/android.xsl @@ -1,13 +1,13 @@ - - + + - + - + @@ -28,10 +28,10 @@ - + - +
      @@ -63,7 +63,7 @@
    - +
  • @@ -80,7 +80,7 @@ - + @@ -111,7 +111,7 @@ - + @@ -131,5 +131,5 @@ - - + + diff --git a/web/lib/default.xsl b/web/lib/default.xsl index dca03344e..ea30c04e6 100755 --- a/web/lib/default.xsl +++ b/web/lib/default.xsl @@ -1,13 +1,13 @@ - - + + - + - + @@ -28,10 +28,10 @@ - + - +
      @@ -63,7 +63,7 @@
    - +
  • @@ -80,7 +80,7 @@ - + @@ -111,7 +111,7 @@ - + @@ -131,5 +131,5 @@ - - + + diff --git a/web/lib/pod.css b/web/lib/pod.css index 7f32e3b0f..276624913 100644 --- a/web/lib/pod.css +++ b/web/lib/pod.css @@ -130,16 +130,16 @@ dt { margin-top: 15px; } -a:link, a:active { - color: #36415c; +a:link, a:active { + color: #36415c; } - -a:visited { - color: #666666; + +a:visited { + color: #666666; } - -a:hover { - color: #888; + +a:hover { + color: #888; } pre { diff --git a/web/misc/actiontec_traffic.html b/web/misc/actiontec_traffic.html index dcc1ce167..8515752c9 100755 --- a/web/misc/actiontec_traffic.html +++ b/web/misc/actiontec_traffic.html @@ -1,112 +1,112 @@ - - - - - - -Internet Traffic Graphs for 1289 Old Topanga Canyon Rd - - - - - - - - - - - - - - - - - - -
    - - - - - -Internet Traffic Graphs - - -
    - - -

    - - - - - - - -
    - - - - - - - - - - - - - - - - - -
    6 hours 1 day 2 days 1 week
    1 month 2 months 6 months 1 year
    - - -
    -

    6hour


    - -

    - -
    -

    1day


    - -

    - -
    -

    2day


    - -

    - -
    -

    1week


    - -

    - -
    -

    1month


    - -

    - -
    -

    6month


    - -

    - -
    -

    1year


    - -

    - -

    -
  • - - - - - - - - - + + + + + + +Internet Traffic Graphs for 1289 Old Topanga Canyon Rd + + + + + + + + + + + + + + + + + + +
    + + + + + +Internet Traffic Graphs + + +
    + + +

    + + + + + + + +
    + + + + + + + + + + + + + + + + + +
    6 hours 1 day 2 days 1 week
    1 month 2 months 6 months 1 year
    + + +
    +

    6hour


    + +

    + +
    +

    1day


    + +

    + +
    +

    2day


    + +

    + +
    +

    1week


    + +

    + +
    +

    1month


    + +

    + +
    +

    6month


    + +

    + +
    +

    1year


    + +

    + +

    + + + + + + + + + + diff --git a/web/newclock/GlobeClock.html b/web/newclock/GlobeClock.html index 85ca36e3f..cd0522d5e 100644 --- a/web/newclock/GlobeClock.html +++ b/web/newclock/GlobeClock.html @@ -1,96 +1,96 @@ - -CME House - - - - - - - -
    - -

    -

    - - - - - - - - - - - - - - + +CME House + + + + + + + +
    + +

    +

    + + + + + + + + + + + + + + diff --git a/web/newclock/MapClock.html b/web/newclock/MapClock.html index 22c2bfccf..1737aa67e 100644 --- a/web/newclock/MapClock.html +++ b/web/newclock/MapClock.html @@ -1,95 +1,95 @@ - -Mr. House Clock - - - - - - - -
    -

    - - - - - - - - - - -

    - - - - + +Mr. House Clock + + + + + + + +

    +

    + + + + + + + + + + +

    + + + + diff --git a/web/newclock/OriginalLED.html b/web/newclock/OriginalLED.html index c2e0ccb4b..3573c8ae7 100644 --- a/web/newclock/OriginalLED.html +++ b/web/newclock/OriginalLED.html @@ -1,92 +1,92 @@ - -CME House - - - - - - - -

    - - - - - - - - - -
    - - - + +CME House + + + + + + + +
    + + + + + + + + + +
    + + + diff --git a/web/newclock/alarm.pl b/web/newclock/alarm.pl index 7899b7cdf..70b820b40 100644 --- a/web/newclock/alarm.pl +++ b/web/newclock/alarm.pl @@ -1,60 +1,60 @@ -#!/usr/bin/perl - - -use CGI; -my $query = new CGI ; -my $mode = $query->param("mode"); -my $hourOpt = $query->param("hourOpt"); -my $minOpt = $query->param("minOpt"); -my $ampm = $query->param("ampm"); - -my $alarmfile = "C:/mh/data/web/data_clock.txt"; - - -my $script_url = "alarm.pl"; - -print "Content-Type: text/html\n\n"; - -print ""; - - if ($mode ne "setdate") { - - print qq~ - - -
    - - -Current Alarm: $alarmfile OR $hourOpt:$minOpt $ampm - - - -AM -PM - - -\n
    - - -~; -} else { - - print "

    SET DATE TO $hourOpt:$minOpt $ampm"; - -&file_write($alarmfile, "$hourOpt:$minOpt $ampm"); - -} - print ""; - -1; +#!/usr/bin/perl + + +use CGI; +my $query = new CGI ; +my $mode = $query->param("mode"); +my $hourOpt = $query->param("hourOpt"); +my $minOpt = $query->param("minOpt"); +my $ampm = $query->param("ampm"); + +my $alarmfile = "C:/mh/data/web/data_clock.txt"; + + +my $script_url = "alarm.pl"; + +print "Content-Type: text/html\n\n"; + +print ""; + + if ($mode ne "setdate") { + + print qq~ + + +

    + + +Current Alarm: $alarmfile OR $hourOpt:$minOpt $ampm + + + +AM +PM + + +\n + + +~; +} else { + + print "

    SET DATE TO $hourOpt:$minOpt $ampm"; + +&file_write($alarmfile, "$hourOpt:$minOpt $ampm"); + +} + print ""; + +1; diff --git a/web/newclock/calendar.js b/web/newclock/calendar.js index 73dd5bddf..943e5ae69 100644 --- a/web/newclock/calendar.js +++ b/web/newclock/calendar.js @@ -1,176 +1,176 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/newclock/index.html b/web/newclock/index.html index 661e3d3f0..c3d657b89 100644 --- a/web/newclock/index.html +++ b/web/newclock/index.html @@ -1,217 +1,217 @@ - -CME House - - - - - - - -

    + +CME House + + + + + + + +
    - +
    - - -
    - + + + + - - - - +
    + + + -
    - - - -
    - - - - - - - - - -
    -
    -

    - - - - - - - + + + + -
    - - - - - + + + + - + - - -
    -
    + + +
    +
    - +
    - -
    -
    -
    - - + +
    + + + + diff --git a/web/newclock/index.pl b/web/newclock/index.pl index f73215d1e..4786ab1f3 100644 --- a/web/newclock/index.pl +++ b/web/newclock/index.pl @@ -1,170 +1,170 @@ -#!/usr/bin/perl - - -use CGI; -my $query = new CGI ; -my $mode = $query->param("mode"); -my $hourOpt = $query->param("hourOpt"); -my $minOpt = $query->param("minOpt"); -my $ampm = $query->param("ampm"); - -my $alarmfile = "C:/mh/data/web/data_clock.txt"; - -my $alarmtime = file_read $alarmfile; - -my $script_url = "/clock/index.pl"; - -print "Content-Type: text/html\n\n"; - -print ""; - - if ($mode ne "setdate") { - - print qq~ - - - -CME House - - - - - - - -
    - - - - - -
    Reload Page
    -
    - - - - - - - - - -
    -

    -
    -
    - - -Current Alarm: $alarmtime
    - - - -AM -PM - - -\n - - - -
    - -
    -
    -
    -
    - - -~; - -} else { - -print ""; -print ""; -print "

    SET DATE TO $hourOpt:$minOpt $ampm"; -print ""; - -&file_write($alarmfile, "$hourOpt:$minOpt $ampm"); - -} - - -1; - +#!/usr/bin/perl + + +use CGI; +my $query = new CGI ; +my $mode = $query->param("mode"); +my $hourOpt = $query->param("hourOpt"); +my $minOpt = $query->param("minOpt"); +my $ampm = $query->param("ampm"); + +my $alarmfile = "C:/mh/data/web/data_clock.txt"; + +my $alarmtime = file_read $alarmfile; + +my $script_url = "/clock/index.pl"; + +print "Content-Type: text/html\n\n"; + +print ""; + + if ($mode ne "setdate") { + + print qq~ + + + +CME House + + + + + + + +

    + + + + + +
    Reload Page
    +
    + + + + + + + + + +
    +

    +
    +
    + + +Current Alarm: $alarmtime
    + + + +AM +PM + + +\n + + + +
    + +
    +
    +
    +
    + + +~; + +} else { + +print ""; +print ""; +print "

    SET DATE TO $hourOpt:$minOpt $ampm"; +print ""; + +&file_write($alarmfile, "$hourOpt:$minOpt $ampm"); + +} + + +1; + diff --git a/web/robots.txt b/web/robots.txt index bb698c250..16972ebd1 100644 --- a/web/robots.txt +++ b/web/robots.txt @@ -1,4 +1,4 @@ -# Robots.txt. No need for robots to crawl mh webserver - -User-agent: * -Disallow: / +# Robots.txt. No need for robots to crawl mh webserver + +User-agent: * +Disallow: / diff --git a/web/test/ajax_example1.shtml b/web/test/ajax_example1.shtml index 988ed8cf8..8a9fef49a 100755 --- a/web/test/ajax_example1.shtml +++ b/web/test/ajax_example1.shtml @@ -1,111 +1,111 @@ - - -Ajax Example #1 - - - - - -

    Weather

    - -

    This is a very simple example of using Ajax technology with Misterhouse. It doesn't use -any special javascript library, but it does assume you are running a script that populates -the Misterhouse %Weather hash. This script gets its data from weather.sxml -every ten seconds.

    - - - - -
    -
    -
    -Outside Temperature: º - -
    -Outside Humidity: % -
    -Dew Point: º - -
    -Rain Total: - -
    -Rain Rate: - -
    -Wind Speed: - -
    -Wind Direction: º -
    -Wind Chill: º - -
    -Barometer: - -
    -Indoor Temperature: º - -
    -Indoor Humidity: % -
    -
    -
    -
    -
    - - - - + + +Ajax Example #1 + + + + + +

    Weather

    + +

    This is a very simple example of using Ajax technology with Misterhouse. It doesn't use +any special javascript library, but it does assume you are running a script that populates +the Misterhouse %Weather hash. This script gets its data from weather.sxml +every ten seconds.

    + + + + +
    +
    +
    +Outside Temperature: º + +
    +Outside Humidity: % +
    +Dew Point: º + +
    +Rain Total: + +
    +Rain Rate: + +
    +Wind Speed: + +
    +Wind Direction: º +
    +Wind Chill: º + +
    +Barometer: + +
    +Indoor Temperature: º + +
    +Indoor Humidity: % +
    +
    +
    +
    +
    + + + + diff --git a/web/test/ajax_example2.shtml b/web/test/ajax_example2.shtml index 131e1227a..f40469b72 100755 --- a/web/test/ajax_example2.shtml +++ b/web/test/ajax_example2.shtml @@ -1,115 +1,115 @@ - - -Ajax Example #2 - - - - - -

    Weather

    - -

    This is a very simple example of using Ajax technology with Misterhouse. It doesn't use -any special javascript library, but it does assume you are running a script that populates -the Misterhouse %Weather hash. This script gets its data from - -the Misterhouse XML server every ten seconds.

    - - - - -
    -
    -
    -Outside Temperature: º - -
    -Outside Humidity: % -
    -Dew Point: º - -
    -Rain Total: - -
    -Rain Rate: - -
    -Wind Speed: - -
    -Wind Direction: º -
    -Wind Chill: º - -
    -Barometer: - -
    -Indoor Temperature: º - -
    -Indoor Humidity: % -
    -
    -
    -
    -
    - - - - + + +Ajax Example #2 + + + + + +

    Weather

    + +

    This is a very simple example of using Ajax technology with Misterhouse. It doesn't use +any special javascript library, but it does assume you are running a script that populates +the Misterhouse %Weather hash. This script gets its data from + +the Misterhouse XML server every ten seconds.

    + + + + +
    +
    +
    +Outside Temperature: º + +
    +Outside Humidity: % +
    +Dew Point: º + +
    +Rain Total: + +
    +Rain Rate: + +
    +Wind Speed: + +
    +Wind Direction: º +
    +Wind Chill: º + +
    +Barometer: + +
    +Indoor Temperature: º + +
    +Indoor Humidity: % +
    +
    +
    +
    +
    + + + + diff --git a/web/test/ajax_example3.shtml b/web/test/ajax_example3.shtml index 0465d72d7..5d230970f 100755 --- a/web/test/ajax_example3.shtml +++ b/web/test/ajax_example3.shtml @@ -1,77 +1,77 @@ - - - -Ajax Example #3 - - - - - -

    Lights

    - -

    This is a very simple example of using Ajax technology with Misterhouse. It doesn't use -any special javascript library, but it does assume you have an All_Lights group defined in your -pl or mht files and that group contains one or more lights. This web page has been tested with -Insteon lights, but should work with other light items as well (X10, UPB, etc.) -This script gets its data from the Misterhouse XML server -every ten seconds.

    - -
    -
    - - - - - + + + +Ajax Example #3 + + + + + +

    Lights

    + +

    This is a very simple example of using Ajax technology with Misterhouse. It doesn't use +any special javascript library, but it does assume you have an All_Lights group defined in your +pl or mht files and that group contains one or more lights. This web page has been tested with +Insteon lights, but should work with other light items as well (X10, UPB, etc.) +This script gets its data from the Misterhouse XML server +every ten seconds.

    + +
    +
    + + + + + diff --git a/web/test/index.shtml b/web/test/index.shtml index 1e0fd6d41..364d5460e 100755 --- a/web/test/index.shtml +++ b/web/test/index.shtml @@ -1,12 +1,12 @@ - -MisterHouse web/test Directory - - - - -

    MisterHouse mh/test Directory

    - - - - - + +MisterHouse web/test Directory + + + + +

    MisterHouse mh/test Directory

    + + + + + From b599462058744206e9cb31edfe65def79357200c Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 18 Nov 2013 18:13:08 -0800 Subject: [PATCH 325/330] Correct two MH_Control Voice Commands The "re load" and "reload" statements are alternative phrasings and not two different commands and thus should be contained in braces not brackets. --- code/common/mh_control.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/common/mh_control.pl b/code/common/mh_control.pl index 211f6430f..20b40423a 100644 --- a/code/common/mh_control.pl +++ b/code/common/mh_control.pl @@ -7,12 +7,12 @@ #@ update docs. This script also defines and lets you set the various modes. # Reload MisterHouse -$v_reload_code = new Voice_Cmd("[Reload,re load] code"); +$v_reload_code = new Voice_Cmd("{Reload,re load} code"); $v_reload_code->set_info('Load mh.ini, icon, and/or code changes'); $v_reload_code->tie_event('push(@Nextpass_Actions, \&read_code)'); # noloop # Force reload MisterHouse -$v_reload_code2 = new Voice_Cmd("Force [Reload,re load] code"); +$v_reload_code2 = new Voice_Cmd("Force {Reload,re load} code"); $v_reload_code2->set_info('Force a code reload of all modules'); $v_reload_code2->tie_event('push(@Nextpass_Actions, # noloop \&read_code_forced)'); # noloop From 890d30bad0252db56bdcc69975a00e93cd2cff1e Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 18 Nov 2013 18:56:58 -0800 Subject: [PATCH 326/330] Insteon: Fix Error with DebugLevel in Message.pm --- lib/Insteon/Message.pm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Insteon/Message.pm b/lib/Insteon/Message.pm index 3274fe885..770f0e0a3 100644 --- a/lib/Insteon/Message.pm +++ b/lib/Insteon/Message.pm @@ -231,9 +231,12 @@ sub send if ($self->send_attempts > 0) { - &::print_log("[Insteon::BaseMessage] WARN: now resending " - . $self->to_string() . " after " . $self->send_attempts - . " attempts.") if $self->setby->debuglevel(1, 'insteon'); + if ((ref $self->setby && $self->setby->debuglevel(1, 'insteon')) || + ((!ref $self->setby) && ::debug{'insteon'})){ + ::print_log("[Insteon::BaseMessage] WARN: now resending " + . $self->to_string() . " after " . $self->send_attempts + . " attempts."); + } # revise default hop count to reflect retries if (ref $self->setby && $self->setby->isa('Insteon::BaseObject') && !defined($$self{no_hop_increase})) From 0e82f5af38092dfdd127928e5281666afb9248da Mon Sep 17 00:00:00 2001 From: hplato Date: Thu, 2 Jan 2014 22:02:33 -0700 Subject: [PATCH 327/330] new file: AD2USB.pm --- lib/AD2USB.pm | 1305 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1305 insertions(+) create mode 100755 lib/AD2USB.pm diff --git a/lib/AD2USB.pm b/lib/AD2USB.pm new file mode 100755 index 000000000..8b6ac8520 --- /dev/null +++ b/lib/AD2USB.pm @@ -0,0 +1,1305 @@ +# ########################################################################### +# Name: AD2USB Monitoring Module +# +# Description: +# Module that monitors a serial device for the AD2USB for known events and +# maintains the state of the Ademco system in memory. Module also sends +# instructions to the panel as requested. +# +# Author: Kirk Friedenberger (kfriedenberger@gmail.com) +# $Revision: $ +# $Date: $ +# +# Change log: +# - Added relay support (Wayne Gatlin, wayne@razorcla.ws) +# - Added 2-way zone expander support (Wayne Gatlin, wayne@razorcla.ws) +# - Completed Wireless support (Wayne Gatlin, wayne@razorcla.ws) +# - Added ser2sock support (Wayne Gatlin, wayne@razorcla.ws) +# - Added in child MH-Style objects (Door & Motion items) (H Plato, hplato@gmail.com) +############################################################################## +# Copyright Kirk Friedenberger (kfriedenberger@gmail.com), 2013, All rights reserved +############################################################################## +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +############################################################################### + +use Switch; + +package AD2USB; + +@AD2USB::ISA = ('Generic_Item'); + +my %CmdMsg; +my %CmdMsgRev; +my $Self; +my %ErrorCode; +my $IncompleteCmd; +my $connecttype; + +# Starting a new object {{{ +sub new { + my ($class) = @_; + my $self = {}; + $$self{panel_status} = 'Unknown'; + $$self{Log} = []; + $$self{ac_power} = 0; + $$self{battery_low} = 1; + $$self{chime} = 0; + + bless $self, $class; + + # load command hash + DefineCmdMsg(); + + + my @LogType = qw(AD2USB_part_log AD2USB_zone_log AD2USB_debug_log); + foreach (@LogType) { + if ( !exists $::config_parms{$_} ) { + $main::config_parms{$_} = 1; + &::print_log("Parameter $_ not defined in mh.private.ini, enabling by default"); + } + } + + if ( !exists $::config_parms{'AD2USB_ser2sock_recon'} ) { + $::config_parms{'AD2USB_ser2sock_recon'} = 10; + &::print_log("Parameter AD2USB_ser2sock_recon not defined in mh.private.ini, enabling by default"); + } + + &main::print_log("Starting ADEMCO panel interface module"); + $Self = $self; + + #Set all zones to ready + ChangeZones( 1, 100, "ready", "ready", 0); + ChangePartitions( 1, 1, "ready", 0); + $self->{keys_sent} = 0; + + return $self; +} + +#}}} +# serial port configuration {{{ +sub init { + + my ($serial_port) = @_; + $serial_port->error_msg(1); + $serial_port->databits(8); + $serial_port->parity("none"); + $serial_port->stopbits(1); + $serial_port->handshake('none'); + $serial_port->datatype('raw'); + $serial_port->dtr_active(1); + $serial_port->rts_active(0); + + select( undef, undef, undef, .100 ); # Sleep a bit + +} + +#}}} +# module startup / enabling serial port {{{ +sub serial_startup { + my $self = $Self; + my $port; my $BaudRate; my $ip; + if ($::config_parms{'AD2USB_serial_port'} and $::config_parms{'AD2USB_serial_port'} ne '/dev/none') { + $port = $::config_parms{'AD2USB_serial_port'}; + $BaudRate = ( defined $::config_parms{AD2USB_baudrate} ) ? $main::config_parms{AD2USB_baudrate} : 115200; + if ( &main::serial_port_create( 'AD2USB', $port, $BaudRate, 'none', 'raw' ) ) { + init( $::Serial_Ports{AD2USB}{object}, $port ); + &main::print_log(" AD2USB.pm initializing port $port at $BaudRate baud") if $main::config_parms{debug} eq 'AD2USB'; + &::MainLoop_pre_add_hook( \&AD2USB::check_for_data, 1 ) if $main::Serial_Ports{AD2USB}{object}; + $::Year_Month_Now = &::time_date_stamp( 10, time ); # Not yet set when we init. + LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", " ========= AD2USB.pm Serial Initialized =========" ); + $connecttype = 'serial'; + } + } elsif ($::config_parms{'AD2USB_ser2sock_ip'}) { + $recon_timer = new Timer; + $ip = $::config_parms{'AD2USB_ser2sock_ip'}; + $port = $::config_parms{'AD2USB_ser2sock_port'}; + &main::print_log(" AD2USB.pm initializing TCP session with $ip on port $port") if $main::config_parms{debug} eq 'AD2USB'; + $AD2USB_ser2sock = new Socket_Item(undef, undef, "$ip:$port", 'AD2USB', 'tcp', 'raw'); + $AD2USB_ser2sock_sender = new Socket_Item(undef, undef, "$ip:$port", 'AD2USB_SENDER', 'tcp', 'rawout'); + start $AD2USB_ser2sock; + start $AD2USB_ser2sock_sender; + &::MainLoop_pre_add_hook( \&AD2USB::check_for_data, 1 ); + $::Year_Month_Now = &::time_date_stamp( 10, time ); # Not yet set when we init. + LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", " ========= AD2USB.pm Socket Initialized =========" ); + $connecttype = 'tcp'; + } else { + warn "AD2USB.pm->startup AD2USB_serial_port or AD2USB_ser2sock_ip not defined in mh.ini file"; + } + } + +#}}} +# module startup; hack because of the startup error +sub startup { +} + + + +#}}} +# check for incoming data on serial port {{{ +sub check_for_data { + my $NewCmd; + + if ($connecttype eq 'serial') { + &main::check_for_generic_serial_data('AD2USB'); + $NewCmd = $main::Serial_Ports{'AD2USB'}{data}; + $main::Serial_Ports{'AD2USB'}{data} = ''; + } + + if ($connecttype eq 'tcp') { + if (active $AD2USB_ser2sock) { + $NewCmd = said $AD2USB_ser2sock; + } else { + # restart the TCP connection if its lost. + if (inactive $recon_timer) { + &main::print_log("Connection to AD2USB was lost, I will try to reconnect in $::config_parms{'AD2USB_ser2sock_recon'} seconds"); + # LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "AD2USB.pm ser2sock connection lost! Trying to reconnect." ); + set $recon_timer $::config_parms{'AD2USB_ser2sock_recon'}, sub { + start $AD2USB_ser2sock; + start $AD2USB_ser2sock_sender; + } + } + } + } + + $self=$Self; + # we need to buffer the information receive, because many command could be include in a single pass + $NewCmd = $IncompleteCmd . $NewCmd if $IncompleteCmd; + return if !$NewCmd; + $NewCmd =~ s/\r\n/#/g; # Replace newlines with # (use # as command delimiter) + #LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "TCP DATA - - $NewCmd" ); + my $Cmd = ''; # Build up a command string by iterating each character + foreach my $c ( split( //, $NewCmd ) ) { + if ( $c eq '#' ) { + if ($Cmd) { + &main::print_log($Cmd); + # This is a full command that was terminated by \r\n + my $status_type = GetStatusType($Cmd); + if ($status_type >= 10) { + # This is a panel message + if (($Cmd ne $self->{panel_status}) || ($status_type == 11)) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "NEW: $Cmd") if $main::config_parms{AD2USB_debug_log}; + CheckCmd($Cmd); + ResetAdemcoState(); + $self->{panel_status} = $Cmd; + } + else { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "DUPE: $Cmd") if $main::config_parms{AD2USB_debug_log}; + } + } + else { + # This is a relay or RF or zone expander message + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "NONPANEL: $Cmd") if $main::config_parms{AD2USB_debug_log}; + CheckCmd($Cmd); + ResetAdemcoState(); + #$self->{panel_status} = $Cmd; + } + $Cmd = ''; + } + } + else { + # Append this character to the current command + $Cmd .= $c; + } + + } + # Save partial command for next serial read + $IncompleteCmd = $Cmd; +} + +#}}} +# Validate the command and perform action {{{ + +sub CheckCmd { + my $CmdStr = shift; + my $status_type = GetStatusType($CmdStr); + my $self = $Self; + + switch ( $status_type ) { + + case -1 { # UNRECOGNIZED STATUS + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "UNKNOWN STATUS: $CmdStr" ) if $main::config_parms{AD2USB_debug_log}; + } + + case 0 { # Key send confirmation + if ($self->{keys_sent} == 0) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Key sent from ANOTHER panel." ) if $main::config_parms{AD2USB_debug_log}; + } + else { + $self->{keys_sent}--; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Key received ($self->{keys_sent} left)" ) if $main::config_parms{AD2USB_debug_log}; + } + + } + + case 10 { # FAULTS AVAILABLE +# &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Faults exist and are available to parse" ) if $main::config_parms{AD2USB_debug_log}; + cmd( $self, "ShowFaults" ); + } + + case 11 { # IN FAULT LOOP + my $status_codes = substr( $CmdStr, 1, 12 ); + my $fault = substr( $CmdStr, 23, 3 ); + $fault = substr($CmdStr, 67, 2); + $fault = "0$fault"; + my $panel_message = substr( $CmdStr, 61, 32); + + my $ZoneName = my $ZoneNum = $fault; + my $PartNum = "1"; + $ZoneName = $main::config_parms{"AD2USB_zone_${ZoneNum}"} if exists $main::config_parms{"AD2USB_zone_${ZoneNum}"}; + $ZoneNum =~ s/^0*//; + $fault = $ZoneNum; + + if (&MappedZones("00$ZoneNum")) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Zone $ZoneNum is mapped to a Relay or RF ID, skipping normal monitoring!") } + else { + + #Check if this is the new lowest fault number and reset the zones before it + if (int($fault) <= int($self->{zone_lowest_fault})) { + $self->{zone_lowest_fault} = $fault; + #Reset zones to ready before the lowest + $start = 1; + $end = $self->{zone_lowest_fault} - 1; + ChangeZones( $start, $end, "ready", "bypass", 1); + } + + #Check if this is a new highest fault number and reset zones after it + if (int($fault) > int($self->{zone_highest_fault})) { + $self->{zone_highest_fault} = $fault; + #Reset zones to ready after the highest + $start = $self->{zone_highest_fault} + 1;; + $end = 11; + ChangeZones( $start, $end, "ready", "bypass", 1); + } + + # Check if this zone was already faulted + if ($self->{zone_status}{"$fault"} eq "fault") { + + #Check if this fault is less than the last fault (and must now be the new lowest zone) + if (int($fault) <= int($self->{zone_last_num})) { + #This is the new lowest zone + $self->{zone_lowest_fault} = $fault; + #Reset zones to ready before the lowest + $start = 1; + $end = $self->{zone_lowest_fault} - 1; + ChangeZones( $start, $end, "ready", "bypass", 1); + } + + #Check if this fault is equal to the last fault (and must now be the only zone) + if (int($fault) == int($self->{zone_last_num})) { + #Reset zones to ready after the only one + $start = int($fault) + 1; + $end = 11; + ChangeZones( $start, $end, "ready", "bypass", 1); + } + + #Check if this fault is greater than the last fault and reset the zones between it and the prior one + if (int($fault) > int($self->{zone_last_num})) { + $start = (($self->{zone_last_num} == $fault) ? 1 : int($self->{zone_last_num}) + 1); + $end = $fault - 1; + ChangeZones( $start, $end, "ready", "bypass", 1); + } + } + + + $self->{zone_now_msg} = "$panel_message"; + $self->{zone_now_status} = "fault"; + $self->{zone_now_name} = "$ZoneName"; + $self->{zone_now_num} = "$ZoneNum"; + ChangeZones( int($ZoneNum), int($ZoneNum), "fault", "", 1); + } + $self->{partition_now_msg} = "$panel_message"; + $self->{partition_now_status} = "not ready"; + $self->{partition_now_num} = "$PartNum"; + ChangePartitions( int($PartNum), int($PartNum), "not ready", 1); + } + + case 12 { # IN BYPASS FLASH LOOP + my $status_codes = substr( $CmdStr, 1, 12 ); + my $fault = substr( $CmdStr, 23, 3 ); +$fault = substr($CmdStr, 67, 2); +$fault = "0$fault"; + my $panel_message = substr( $CmdStr, 61, 32); + + my $ZoneName = my $ZoneNum = $fault; + my $PartNum = "1"; + $ZoneName = $main::config_parms{"AD2USB_zone_${ZoneNum}"} if exists $main::config_parms{"AD2USB_zone_${ZoneNum}"}; + $ZoneNum =~ s/^0*//; + $fault = $ZoneNum; + + $self->{zone_now_msg} = "$panel_message"; + $self->{zone_now_status} = "bypass"; + $self->{zone_now_name} = "$ZoneName"; + $self->{zone_now_num} = "$ZoneNum"; + ChangeZones( int($ZoneNum), int($ZoneNum), "bypass", "", 1); + $self->{partition_now_msg} = "$panel_message"; + $self->{partition_now_status} = "not ready"; + $self->{partition_now_num} = "$PartNum"; + ChangePartitions( int($PartNum), int($PartNum), "not ready", 1); + + } + + case 13 { # NORMAL STATUS + + # Get three sections of the Ademco status message + my $status_codes = substr( $CmdStr, 1, 12 ); + my $fault = substr( $CmdStr, 23, 3 ); + my $panel_message = substr( $CmdStr, 61, 32); + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Key received ($self->{keys_sent} left)" ) if $main::config_parms{AD2USB_debug_log}; + + # READY + $data = 0; + if ( substr($status_codes,$data,1) == "1" ) { + my $start = 1; + my $end = 11; + if ( substr($status_codes,6,1) ne "1" ) { + # Reset all zones to ready if partition is ready and not bypassed + ChangeZones( $start, $end, "ready", "", 1); + } + else { + # If zones are bypassed, reset unbypassed zones to ready + for ($i = $start; $i <= $end; $i++) { + my $current_status = $self->{zone_status}{"$i"}; + if ($current_status eq "fault") { + ChangeZones($i, $i, "ready", "bypass", 1); + } + } + } + + my $PartName = my $PartNum = "1"; + + $PartName = $main::config_parms{"AD2USB_part_${PartNum}"} if exists $main::config_parms{"AD2USB_part_${PartNum}"}; + $self->{partition_now_msg} = "$panel_message"; + $self->{partition_now_num} = "$PartNum"; + $self->{partition_now_status} = "ready"; + ChangePartitions( int($PartNum), int($PartNum), "ready", 1); + $self->{zone_lowest_fault} = 999; + $self->{zone_highest_fault} = -1; + + # Reset state for fault checks + $self->{zone_last_status} = ""; + $self->{zone_last_num} = ""; + $self->{zone_last_name} = ""; + } + + # ARMED AWAY + $data = 1; + if ( substr($status_codes,$data,1) == "1" ) { + my $PartNum = my $PartName = "1"; + $PartName = $main::config_parms{"AD2USB_part_${PartNum}"} if exists $main::config_parms{"AD2USB_part_${PartNum}"}; + + my $mode = "ERROR"; + if (index($panel_message, "ALL SECURE")) { + $mode = "armed away"; + } + elsif (index($panel_message, "You may exit now")) { + $mode = "exit delay"; + } + elsif (index($panel_message, "or alarm occurs")) { + $mode = "entry delay"; + } + elsif (index($panel_message, "ZONE BYPASSED")) { + $mode = "armed away"; + } + + set $self "$mode"; + $self->{partition_now_msg} = "$panel_message"; + $self->{partition_now_status} = "$mode"; + $self->{partition_now_num} = "$PartNum"; + ChangePartitions( int($PartNum), int($PartNum), "$mode", 1); + + # Reset state for fault checks + $self->{zone_last_status} = ""; + $self->{zone_last_num} = ""; + $self->{zone_last_name} = ""; + } + + # ARMED HOME + $data = 2; + if ( substr($status_codes,$data,1) eq "1" ) { + my $PartNum = my $PartName = "1"; + + my $mode = "armed stay"; + $PartName = $main::config_parms{"AD2USB_part_${PartNum}"} if exists $main::config_parms{"AD2USB_part_${PartNum}"}; + $self->{partition_now_msg} = "$panel_message"; + $self->{partition_now_status} = "$mode"; + $self->{partition_now_num} = "$PartNum"; + ChangePartitions( int($PartNum), int($PartNum), "$mode", 1); + + # Reset state for fault checks + $self->{zone_last_status} = ""; + $self->{zone_last_num} = ""; + $self->{zone_last_name} = ""; + } + + # SKIP BACKLIGHT + $data = 3; + + # PROGRAMMING MODE + $data = 4; + if ( substr($status_codes,$data,1) eq "1" ) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Panel is in programming mode" ) if $main::config_parms{AD2USB_debug_log}; + + # Reset state for fault checks + $self->{zone_last_status} = ""; + $self->{zone_last_num} = ""; + $self->{zone_last_name} = ""; + } + + # SKIP BEEPS + $data = 5; + + # A ZONE OR ZONES ARE BYPASSED + $data = 6; + if ( substr($status_codes,$data,1) == "1" ) { + + # Reset zones to ready that haven't appeared in the bypass loop +# if ($self->{zone_last_status} eq "bypass") { +# if (int($fault) < int($self->{zone_now_num})) { +# $start = int($self->{zone_now_num}) + 1; +# $end = 12; +# } +# ChangeZones( $start, $end - 1, "ready", "", 1); +# $self->{zone_now_status} = ""; +# $self->{zone_now_num} = "0"; +# } + + # Reset state for fault checks + $self->{zone_last_status} = ""; + $self->{zone_last_num} = ""; + $self->{zone_last_name} = ""; + } + + # SKIP AC POWER + $data = 7; + + # SKIP CHIME MODE + $data = 8; + + # ALARM WAS TRIGGERED (Sticky until disarm) + $data = 9; + if ( substr($status_codes,$data,1) == "1" ) { + $EventName = "ALARM WAS TRIGGERED"; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "$EventName" ) if $main::config_parms{AD2USB_part_log}; + } + + # ALARM IS SOUNDING + $data = 10; + if ( substr($status_codes,$data,1) == "1" ) { + $EventName = "ALARM IS SOUNDING"; + + #TODO: figure out how to get a partition number + my $PartName = my $PartNum = "1"; + my $ZoneNum = $fault; + $ZoneName = $main::config_parms{"AD2USB_zone_$ZoneNum"} if exists $main::config_parms{"AD2USB_zone_$ZoneNum"}; + $PartName = $main::config_parms{"AD2USB_part_$PartName"} if exists $main::config_parms{"AD2USB_part_$PartName"}; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "$EventName - Zone $ZoneNum ($ZoneName)" ) if $main::config_parms{AD2USB_part_log}; + $ZoneNum =~ s/^0*//; + ChangeZones( int($ZoneNum), int($ZoneNum), "alarm", "", 1); + $self->{zone_now_msg} = "$panel_message"; + $self->{zone_now_status} = "alarm"; + $self->{zone_now_num} = "$ZoneNum"; + $self->{partition_now_msg} = "$panel_message"; + $self->{partition_now_status} = "alarm"; + $self->{partition_now_num} = "$PartNum"; + ChangePartitions( int($PartNum), int($PartNum), "alarm", 1); + } + + # SKIP BATTERY LOW + $data = 11; + } + + case 2 { # WIRELESS STATUS + my $ZoneLoop = ""; + my $MZoneLoop = ""; + # Parse raw status strings + my $rf_id = substr( $CmdStr, 5, 7 ); + my $rf_status = substr( $CmdStr, 13, 2 ); + my $lc = 0; + my $wnum = 0; + + # UNKNOWN + my $unknown_1 = 0; + $unknown_1 = 1 if (hex(substr($rf_status, 1, 1)) & 1) == 1; + # Parse for low battery signal + my $low_batt = 0; + $low_batt = 1 if (hex(substr($rf_status, 1, 1)) & 2) == 2; + # Parse for supervision flag + my $supervised = 0; + $supervised = 1 if (hex(substr($rf_status, 1, 1)) & 4) == 4; + # UNKNOWN + my $unknown_8 = 0; + $unknown_8 = 1 if (hex(substr($rf_status, 1, 1)) & 8) == 8; + + # Parse loop faults + my $loop_fault_1 = 0; + $loop_fault_1 = 1 if (hex(substr($rf_status, 0, 1)) & 8) == 8; + my $loop_fault_2 = 0; + $loop_fault_2 = 1 if (hex(substr($rf_status, 0, 1)) & 2) == 2; + my $loop_fault_3 = 0; + $loop_fault_3 = 1 if (hex(substr($rf_status, 0, 1)) & 1) == 1; + my $loop_fault_4 = 0; + $loop_fault_4 = 1 if (hex(substr($rf_status, 0, 1)) & 4) == 4; + + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "WIRELESS: rf_id($rf_id) status($rf_status) loop1($loop_fault_1) loop2($loop_fault_2) loop3($loop_fault_3) loop4($loop_fault_4)" ) if $main::config_parms{AD2USB_debug_log}; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "WIRELESS: rf_id($rf_id) status($rf_status) low_batt($low_batt) supervised($supervised)" ) if $main::config_parms{AD2USB_debug_log}; + + my $ZoneStatus = "ready"; + my $PartStatus = ""; + my @parsest; + my $sensortype; + + if (exists $main::config_parms{"AD2USB_wireless_$rf_id"}) { + # Assign zone + my @ParseNum = split(",", $main::config_parms{"AD2USB_wireless_$rf_id"}); + + # Assign status (zone and partition) + if ($low_batt == "1") { + $ZoneStatus = "low battery"; + } + + foreach $wnum(@ParseNum) { + if ($lc eq 0 or $lc eq 2 or $lc eq 4 or $lc eq 6) { + $ZoneNum = $wnum; + } + + if ($lc eq 1 or $lc eq 3 or $lc eq 5 or $lc eq 7) { + @parsest = split("", $wnum); + $sensortype = $parsest[0]; + $ZoneLoop = $parsest[1]; + $ZoneName = "Unknown"; + $ZoneName = $main::config_parms{"AD2USB_zone_$ZoneNum"} if exists $main::config_parms{"AD2USB_zone_$ZoneNum"}; + + if ($ZoneLoop eq "1") {$MZoneLoop = $loop_fault_1} + if ($ZoneLoop eq "2") {$MZoneLoop = $loop_fault_2} + if ($ZoneLoop eq "3") {$MZoneLoop = $loop_fault_3} + if ($ZoneLoop eq "4") {$MZoneLoop = $loop_fault_4} + + if ("$MZoneLoop" eq "1") { + $ZoneStatus = "fault"; + } elsif ("$MZoneLoop" eq 0) { + $ZoneStatus = "ready"; + } + + $self->{zone_now_msg} = "$CmdStr"; + $self->{zone_now_status} = "$ZoneStatus"; + $self->{zone_now_name} = "$ZoneName"; + $self->{zone_now_num} = "$ZoneNum"; + ChangeZones( int($ZoneNum), int($ZoneNum), "$ZoneStatus", "", 1); + if ($sensortype eq "k") { + $ZoneStatus = "ready"; + $self->{zone_now_msg} = "$CmdStr"; + $self->{zone_now_status} = "$ZoneStatus"; + $self->{zone_now_name} = "$ZoneName"; + $self->{zone_now_num} = "$ZoneNum"; + ChangeZones( int($ZoneNum), int($ZoneNum), "$ZoneStatus", "", 1); + } + } + $lc++ + } + } + + } + + case 3 { # EXPANDER STATUS + my $exp_id = substr( $CmdStr, 5, 2 ); + my $input_id = substr( $CmdStr, 8, 2 ); + my $status = substr( $CmdStr, 11, 2 ); + my $ZoneStatus; + my $PartStatus; + + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "EXPANDER: exp_id($exp_id) input($input_id) status($status)" ) if $main::config_parms{AD2USB_debug_log}; + + if (exists $main::config_parms{"AD2USB_expander_$exp_id$input_id"}) { + # Assign zone + $ZoneNum = $main::config_parms{"AD2USB_expander_$exp_id$input_id"}; + $ZoneName = "Unknown"; + $ZoneName = $main::config_parms{"AD2USB_zone_$ZoneNum"} if exists $main::config_parms{"AD2USB_zone_$ZoneNum"}; + # Assign status (zone and partition) + + + if ($status == 01) { + $ZoneStatus = "fault"; + $PartStatus = "not ready"; + } elsif ($status == 00) { + $ZoneStatus = "ready"; + $PartStatus = ""; + } + + $self->{zone_now_msg} = "$CmdStr"; + $self->{zone_now_status} = "$ZoneStatus"; + $self->{zone_now_name} = "$ZoneName"; + $self->{zone_now_num} = "$ZoneNum"; + ChangeZones( int($ZoneNum), int($ZoneNum), "$ZoneStatus", "", 1); + # if (($self->{partition_status}{int($PartNum)}) eq "ready") { #only change the partition status if the current status is "ready". We dont change if the system is armed. + # if ($PartStatus ne "") { + # $self->{partition_now_msg} = "$CmdStr"; + # $self->{partition_now_status} = "$PartStatus"; + # $self->{partition_now_num} = "$PartNum"; + # ChangePartitions( int($PartNum), int($PartNum), "$PartStatus", 1); + # } + # } + } + } + + + case 4 { # RELAY STATUS + my $rel_id = substr( $CmdStr, 5, 2 ); + my $rel_input_id = substr( $CmdStr, 8, 2 ); + my $rel_status = substr( $CmdStr, 11, 2 ); + my $PartName = my $PartNum = "1"; + my $ZoneStatus; + my $PartStatus; + + + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "RELAY: rel_id($rel_id) input($rel_input_id) status($rel_status)" ) if $main::config_parms{AD2USB_debug_log}; + + if (exists $main::config_parms{"AD2USB_relay_$rel_id$rel_input_id"}) { + # Assign zone + $ZoneNum = $main::config_parms{"AD2USB_relay_$rel_id$rel_input_id"}; + $ZoneName = "Unknown"; + $ZoneName = $main::config_parms{"AD2USB_zone_$ZoneNum"} if exists $main::config_parms{"AD2USB_zone_$ZoneNum"}; + # Assign status (zone and partition) + + + if ($rel_status == 01) { + $ZoneStatus = "fault"; + $PartStatus = "not ready"; + } elsif ($rel_status == 00) { + $ZoneStatus = "ready"; + $PartStatus = ""; + } + + $self->{zone_now_msg} = "$CmdStr"; + $self->{zone_now_status} = "$ZoneStatus"; + $self->{zone_now_name} = "$ZoneName"; + $self->{zone_now_num} = "$ZoneNum"; + ChangeZones( int($ZoneNum), int($ZoneNum), "$ZoneStatus", "", 1); + # if (($self->{partition_status}{int($PartNum)}) eq "ready") { #only change the partition status if the current status is "ready". We dont change if the system is armed. + # if ($PartStatus ne "") { + # $self->{partition_now_msg} = "$CmdStr"; + # $self->{partition_now_status} = "$PartStatus"; + # $self->{partition_now_num} = "$PartNum"; + # ChangePartitions( int($PartNum), int($PartNum), "$PartStatus", 1); + # } + # } + } + } + + else { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "SOMETHING SERIOUSLY WRONG - UNKNOWN COMMAND" ) if $main::config_parms{AD2USB_debug_log}; + } + } + + # NORMAL STATUS TYPE + # ALWAYS CHECK CHIME / AC POWER / BATTERY STATUS / BACKLIGHT / BEEPS + if ($status_type >= 10) { + + # PARSE codes + my $status_codes = substr( $CmdStr, 1, 12 ); + my $fault = substr( $CmdStr, 23, 3 ); + my $panel_message = substr( $CmdStr, 61, 32); + + # BACKLIGHT + $data = 3; + if ( substr($status_codes,$data,1) == "1" ) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Panel backlight is on" ) if $main::config_parms{AD2USB_debug_log}; + } + + # BEEPS + $data = 5; + if ( substr($status_codes,$data,1) != "0" ) { + $NumBeeps = substr($status_codes,$data,1); + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Panel beeped $NumBeeps times" ) if $main::config_parms{AD2USB_debug_log}; + } + + # AC POWER + $data = 7; + if ( substr($status_codes,$data,1) == "0" ) { + $$self{ac_power} = 0; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "AC Power has been lost" ); + } + else { + $$self{ac_power} = 1; + } + + # CHIME MODE + $data = 8; + if ( substr($status_codes,$data,1) == "0" ) { + $self->{chime} = 0; +# &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Chime is off" ) if $main::config_parms{AD2USB_debug_log}; + } + else { + $self->{chime} = 1; +# &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Chime is on" ) if $main::config_parms{AD2USB_debug_log}; + } + + # BATTERY LOW + $data = 11; + if ( substr($status_codes,$data,1) == "1" ) { + $self->{battery_low} = 1; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Panel is low on battery" ); + } + else { + $self->{battery_low} = 0; + } + + } + + return; + +} + +#}}} +# local logit call {{{ +sub LocalLogit { + my $file = shift; + my $str = shift; + &::logit( "$file", "$str" ); + my $Timestamp = &::time_date_stamp(16); + $str =~ s/ +/ /; + unshift @{ $Self->{Log} }, "$Timestamp: $str" if $str !~ /Temperature/; + pop @{ $Self->{Log} } if scalar( @{ $Self->{Log} } ) > 60; + +} + +#}}} +# Determine if the status string requires parsing {{{ +sub GetStatusType { + my $AdemcoStr = shift; + my $ll = length($AdemcoStr); + + if ($ll eq 94) { + my $substatus = substr($AdemcoStr, 61, 5); + if ( $substatus eq "FAULT" ) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Fault zones available: $AdemcoStr") if $main::config_parms{AD2USB_debug_log}; + return 11; + } + elsif ( $substatus eq "BYPAS" ) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Bypass zones available: $AdemcoStr") if $main::config_parms{AD2USB_debug_log}; + return 12; + } + elsif (index($AdemcoStr, "Hit *") >= 0) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Faults available: $AdemcoStr") if $main::config_parms{AD2USB_debug_log}; + return 10; + } + else { +# &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Standard status received: $AdemcoStr"); + return 13; + } + } + elsif (substr($AdemcoStr,0,5) eq "!RFX:") { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Wireless status received.") if $main::config_parms{AD2USB_debug_log}; + return 2; + } + elsif (substr($AdemcoStr,0,5) eq "!EXP:") { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Expander status received.") if $main::config_parms{AD2USB_debug_log}; + return 3; + } + elsif (substr($AdemcoStr,0,5) eq "!REL:") { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Relay status received.") if $main::config_parms{AD2USB_debug_log}; + return 4; + } + elsif ($AdemcoStr eq "!Sending...done") { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Command sent successfully.") if $main::config_parms{AD2USB_debug_log};; + return 0; + } + return -1; +} + +#}}} +# Change zone statuses for zone indices from start to end +sub ChangeZones { + my $start = @_[0]; + my $end = @_[1]; + my $new_status = @_[2]; + my $neq_status = @_[3]; + my $log = @_[4]; + + my $self = $Self; + for ($i = $start; $i <= $end; $i++) { + $current_status = $self->{zone_status}{"$i"}; + if (($current_status ne $new_status) && ($current_status ne $neq_status)) { + if (($main::config_parms{AD2USB_zone_log}) && ($log == 1)) { + my $ZoneNumPadded = $i; + $ZoneNumPadded = sprintf("%3d", $ZoneNumPadded); + $ZoneNumPadded =~ tr/ /0/; + $ZoneName = "Unknown"; + $ZoneName = $main::config_parms{"AD2USB_zone_$ZoneNumPadded"} if exists $main::config_parms{"AD2USB_zone_$ZoneNumPadded"}; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Zone $i ($ZoneName) changed from '$current_status' to '$new_status'" ) if $main::config_parms{AD2USB_zone_log}; + } + $self->{zone_status}{"$i"} = $new_status; + # Set child object status if it is registered to the zone + $$self{zone_object}{"$i"}->set($new_status) if defined $$self{zone_object}{"$i"}; + } + } +} + +#}}} +# Change partition statuses for partition indices from start to end +sub ChangePartitions { + my $start = @_[0]; + my $end = @_[1]; + my $new_status = @_[2]; + my $log = @_[3]; + + my $self = $Self; + for ($i = $start; $i <= $end; $i++) { + $current_status = $self->{partition_status}{"$i"}; + if ($current_status ne $new_status) { + if (($main::config_parms{AD2USB_part_log}) && ($log == 1)) { + $PartName = $main::config_parms{"AD2USB_part_$i"} if exists $main::config_parms{"AD2USB_part_$i"}; + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Partition $i ($PartName) changed from '$current_status' to '$new_status'" ) if $main::config_parms{AD2USB_part_log}; + } + $self->{partition_status}{"$i"} = $new_status; + } + } +} + + +# Reset Ademco state to simulate a "now" on some value ie: zone, temp etc. {{{ +sub ResetAdemcoState { + + my $self = $Self; + # store faults (fault and bypass) for next message parsing + if (($self->{zone_now_status} eq "fault") || ($self->{zone_now_status} eq "bypass")) { + $self->{zone_last_status} = $self->{zone_now_status}; + $self->{zone_last_num} = $self->{zone_now_num}; + $self->{zone_last_name} = $self->{zone_now_name}; + } + + # reset zone + if ( defined $self->{zone_now_num} ) { + my $ZoneNum = $self->{zone_now_num}; + $self->{zone_num}{$ZoneNum} = $self->{zone_now_num}; + $self->{zone_msg}{$ZoneNum} = $self->{zone_now_msg}; + $self->{zone_status}{$ZoneNum} = $self->{zone_now_status}; + $self->{zone_time}{$ZoneNum} = &::time_date_stamp( 17, time ); + undef $self->{zone_now_num}; + undef $self->{zone_now_name}; + undef $self->{zone_now_status}; + undef $self->{zone_now_msg}; + } + + # reset partition + if ( defined $self->{partition_now_num} ) { + my $PartNum = $self->{partition_now_num}; + $self->{partition}{$PartNum} = $self->{partition_now_num}; + $self->{partition_msg}{$PartNum} = $self->{partition_now_msg}; + $self->{partition_status}{$PartNum} = $self->{partition_now_status}; + $self->{partition_time}{$PartNum} = &::time_date_stamp( 17, time ); + undef $self->{partition_now_num}; + undef $self->{partition_now_msg}; + undef $self->{partition_now_status}; + } + + return; +} + +#}}} +# Define hash with Ademco commands {{{ +sub DefineCmdMsg { + my %OutputListco; + foreach my $key (keys(%::config_parms)) { + next if $key =~ /_MHINTERNAL_/; + next if $key !~ /^AD2USB_output_(\D+)_(\d+)$/; + if ($1 eq 'co') { + $OutputListco{"$::config_parms{$key}c"} = "$::config_parms{AD2USB_user_master_code}#70$2"; + $OutputListco{"$::config_parms{$key}o"} = "$::config_parms{AD2USB_user_master_code}#80$2"; + } + if ($1 eq 'oc') { + $OutputListco{"$::config_parms{$key}o"} = "$::config_parms{AD2USB_user_master_code}#80$2"; + $OutputListco{"$::config_parms{$key}c"} = "$::config_parms{AD2USB_user_master_code}#70$2"; + } + if ($1 eq 'o') { + $OutputListco{"$::config_parms{$key}o"} = "$::config_parms{AD2USB_user_master_code}#80$2"; + } + if ($1 eq 'c') { + $OutputListco{"$::config_parms{$key}c"} = "$::config_parms{AD2USB_user_master_code}#70$2"; + } + } + + my %ExpListc; + my $srpzonenum; + foreach my $key (keys(%::config_parms)) { + next if $key =~ /_MHINTERNAL_/; + next if $key !~ /^AD2USB_expander_(\d+)$/; + $srpzonenum = substr($::config_parms{$key}, 1); + $ExpListc{"exp$::config_parms{$key}c"} = "L$srpzonenum"."0"; + $ExpListc{"exp$::config_parms{$key}f"} = "L$srpzonenum"."1"; + $ExpListc{"exp$::config_parms{$key}p"} = "L$srpzonenum"."2"; + } + + %CmdMsg = ( + "Disarm" => "$::config_parms{AD2USB_user_master_code}1", + "ArmAway" => "$::config_parms{AD2USB_user_master_code}2", + "ArmStay" => "$::config_parms{AD2USB_user_master_code}3", + "ArmAwayMax" => "$::config_parms{AD2USB_user_master_code}4", + "Test" => "$::config_parms{AD2USB_user_master_code}5", + "Bypass" => "$::config_parms{AD2USB_user_master_code}6#", + "ArmStayInstant" => "$::config_parms{AD2USB_user_master_code}7", + "Code" => "$::config_parms{AD2USB_user_master_code}8", + "Chime" => "$::config_parms{AD2USB_user_master_code}9", + "ToggleVoice" => '#024', + "ShowFaults" => "*", + "AD2USBReboot" => "=", + "AD2USBConfigure" => "!" + ); + + my %newHash = (%OutputListco, %CmdMsg); + %CmdMsg = %newHash; + %newHash = (%ExpListc, %CmdMsg); + %CmdMsg = %newHash; + %CmdMsgRev = reverse %CmdMsg; + return; +} + +#}}} +# Define hash with all zone numbers and names {{{ +sub ZoneName { + #my $self = $Self; + my @Name = ["none"]; + + foreach my $key (keys(%::config_parms)) { + next if $key =~ /_MHINTERNAL_/; + next if $key !~ /^AD2USB_zone_(\d+)$/; + $Name[int($1)]=$::config_parms{$key}; + } + return @Name; +} + + +sub MappedZones { + foreach my $mkey (keys(%::config_parms)) { + next if $mkey =~ /_MHINTERNAL_/; + next if $mkey !~ /^AD2USB_(relay|wireless|expander)_(\d+)$/; + if ("@_" eq $::config_parms{$mkey}) { return 1 } + } + return 0; +} + +#}}} +# Sending command to ADEMCO panel {{{ +sub cmd { + + my ( $class, $cmd, $password ) = @_; + $cmd = $CmdMsg{$cmd}; + + $CmdName = ( exists $CmdMsgRev{$cmd} ) ? $CmdMsgRev{$cmd} : "unknown"; + $CmdStr = $cmd; + + # Exit if unknown command + if ( $CmdName =~ /^unknown/ ) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Invalid ADEMCO panel command : $CmdName ($cmd)"); + return; + } + + # Exit if password is wrong + if ( ($password ne $::config_parms{AD2USB_user_master_code}) && ($CmdName ne "ShowFaults" ) ) { + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", "Invalid password for command $CmdName ($password)"); + return; + } + + &LocalLogit( "$main::config_parms{data_dir}/logs/AD2USB.$main::Year_Month_Now.log", ">>> Sending to ADEMCO panel $CmdName ($cmd)" ) if $main::config_parms{AD2USB_debug_log}; + $class->{keys_sent} = $class->{keys_sent} + length($CmdStr); + if ($connecttype eq 'serial') { + $main::Serial_Ports{AD2USB}{object}->write("$CmdStr"); + } else { + set $AD2USB_ser2sock_sender "$CmdStr"; + } + return "Sending to ADEMCO panel: $CmdName ($cmd)"; + +} + +#}}} +# user call from MH {{{ + +sub zone_now { + return $_[0]->{zone_now_name} if defined $_[0]->{zone_now_name}; +} + +sub zone_msg { + return $_[0]->{zone_now_msg} if defined $_[0]->{zone_now_msg}; +} + +sub zone_now_restore { + return $_[0]->{zone_now_restore} if defined $_[0]->{zone_now_restore}; +} + +sub zone_now_tamper { + return $_[0]->{zone_now_tamper} if defined $_[0]->{zone_now_tamper}; +} + +sub zone_now_tamper_restore { + return $_[0]->{zone_now_tamper_restore} if defined $_[0]->{zone_now_tamper_restore}; +} + +sub zone_now_alarm { + return $_[0]->{zone_now_alarm} if defined $_[0]->{zone_now_alarm}; +} + +sub zone_now_alarm_restore { + return $_[0]->{zone_now_alarm_restore} if defined $_[0]->{zone_now_alarm_restore}; +} + +sub zone_now_fault { + return $_[0]->{zone_now_num} if defined $_[0]->{zone_now_num}; +} + +sub status_zone { + my ( $class, $zone ) = @_; + return $_[0]->{zone_status}{$zone} if defined $_[0]->{zone_status}{$zone}; +} + +sub zone_name { + my ( $class, $zone_num ) = @_; + $zone_num = sprintf "%03s", $zone_num; + my $ZoneName = $main::config_parms{"AD2USB_zone_$zone_num"} if exists $main::config_parms{"AD2USB_zone_$zone_num"}; + return $ZoneName if $ZoneName; + return $zone_num; +} + +sub partition_now { + my ( $class, $part ) = @_; + return $_[0]->{partition_now_num} if defined $_[0]->{partition_now_num}; +} + +sub partition_now_msg { + my ( $class, $part ) = @_; + return $_[0]->{partition_now_msg} if defined $_[0]->{partition_now_msg}; +} + +sub partition_name { + my ( $class, $part_num ) = @_; + my $PartName = $main::config_parms{"AD2USB_part_$part_num"} if exists $main::config_parms{"AD2USB_part_$part_num"}; + return $PartName if $PartName; + return $part_num; +} + +sub cmd_list { + foreach my $k ( sort keys %CmdMsg ) { + &::print_log("$k"); + } +} + +##Used to register a child object to the zone. Allows for MH-style Door & Motion sensors +sub register { + my ($class, $object, $zone_num ) = @_; + &::print_log("Registering Child Object $object->{zone_number} on zone $zone_num"); + $_[0]->{zone_object}{$zone_num} = $object; + } + + + +# MH-Style child objects +# These allow zones to behave like Door_Items and Motion Sensors +# to use, just create the item with the Master AD2USB object and the appropriate zone +# +# ie. +# $AD2USB = new AD2USB; +# $Front_door = new AD2USB_Door_Item($AD2USB,1); +# $Front_motion = new AD2USB_Motion_Item($AD2USB,2); + +package AD2USB_Door_Item; + +@AD2USB_Door_Item::ISA = ('Generic_Item'); + +sub new +{ + my ($class,$object,$zone) = @_; + + my $self={}; + bless $self,$class; + + $$self{m_write} = 0; + $$self{m_timerCheck} = new Timer() unless $$self{m_timerCheck}; + $$self{m_timerAlarm} = new Timer() unless $$self{m_timerAlarm}; + $$self{'alarm_action'} = ''; + $$self{last_open} = 0; + $$self{last_closed} = 0; + @{$$self{states}} = ('open','closed','check'); + $$self{zone_number} = $zone; + $$self{master_object} = $object; + $$self{item_type} = 'door'; + $object->register($self,$zone); + + return $self; + +} + +sub set +{ + my ($self,$p_state,$p_setby) = @_; + + if (ref $p_setby and $p_setby->can('get_set_by')) { + &::print_log("AD2USB_Door_Item($$self{object_name})::set($p_state, $p_setby): $$p_setby{object_name} was set by " . $p_setby->get_set_by) if $main::Debug{AD2USB}; + } else { + &::print_log("AD2USB_Door_Item($$self{object_name})::set($p_state, $p_setby)") if $main::Debug{AD2USB}; + } + + if ($p_state =~ /^fault/) { + $p_state = 'open'; + $$self{last_open} = $::Time; + + } elsif ($p_state =~ /^ready/) { + $p_state = 'closed'; + $$self{last_closed} = $::Time; + + # Other door sensors? + } elsif ($p_state eq 'on') { + $p_state = 'open'; + $$self{last_open} = $::Time; + + } elsif ($p_state eq 'off') { + $p_state = 'closed'; + $$self{last_closed} = $::Time; + + } else { + $p_state = 'check'; + } + + $self->SUPER::set($p_state,$p_setby); +} + +sub get_last_close_time { + my ($self) = @_; + return $$self{last_closed}; +} + +sub get_last_open_time { + my ($self) = @_; + return $$self{last_open}; +} + +sub get_child_item_type { + my ($self) = @_; + return $$self{item_type}; +} + +#Left in these methods to maintain compatibility. Since we're not tracking inactivity, these won't return proper results. + +sub set_alarm($$$) { + my ($self, $time, $action, $repeat_time) = @_; + $$self{'alarm_action'} = $action; + $$self{'alarm_time'} = $time; + $$self{'alarm_repeat_time'} = $repeat_time if defined $repeat_time; + &::print_log ("AD2USB_Door_Item:: set_alarm not supported"); + +} + +sub set_inactivity_alarm($$$) { + my ($self, $time, $action) = @_; + $$self{'inactivity_action'} = $action; + $$self{'inactivity_time'} = $time*3600; + &::print_log("AD2USB_Door_Item:: set_inactivity_alarm not supported"); + +} + + +package AD2USB_Motion_Item; +@AD2USB_Motion_Item::ISA = ('Generic_Item'); + +sub new +{ + my ($class,$object,$zone) = @_; + + my $self={}; + bless $self,$class; + + $$self{m_write} = 0; + $$self{m_timerCheck} = new Timer() unless $$self{m_timerCheck}; + $$self{m_timerAlarm} = new Timer() unless $$self{m_timerAlarm}; + $$self{'alarm_action'} = ''; + $$self{last_still} = 0; + $$self{last_motion} = 0; + @{$$self{states}} = ('motion','still','check'); + $$self{zone_number} = $zone; + $$self{master_object} = $object; + $$self{item_type} = 'motion'; + + $object->register($self,$zone); + + return $self; + +} + +sub set +{ + my ($self,$p_state,$p_setby) = @_; + + + if (ref $p_setby and $p_setby->can('get_set_by')) { + &::print_log("AD2USB_Motion_Item($$self{object_name})::set($p_state, $p_setby): $$p_setby{object_name} was set by " . $p_setby->get_set_by) if $main::Debug{AD2USB}; + } else { + &::print_log("AD2USB_Motion_Item($$self{object_name})::set($p_state, $p_setby)") if $main::Debug{AD2USB}; + } + + if ($p_state =~ /^fault/i) { + $p_state = 'motion'; + $$self{last_motion} = $::Time; + + } elsif ($p_state =~ /^ready/i) { + $p_state = 'still'; + $$self{last_still} = $::Time; + + } else { + $p_state = 'check'; + } + + $self->SUPER::set($p_state, $p_setby); +} + +sub get_last_still_time { + my ($self) = @_; + return $$self{last_still}; +} + +sub get_last_motion_time { + my ($self) = @_; + return $$self{last_motion}; +} + +sub get_child_item_type { + my ($self) = @_; + return $$self{item_type}; +} + +#Left in these methods to maintain compatibility. Since we're not tracking inactivity, these won't return proper results. +sub delay_off() +{ + my ($self,$p_time) = @_; + $$self{m_delay_off} = $p_time if defined $p_time; + &::print_log("AD2USB_Motion_Item:: delay_off not supported"); + return $$self{m_delay_off}; +} + +sub set_inactivity_alarm($$$) { + my ($self, $time, $action) = @_; + $$self{'inactivity_action'} = $action; + $$self{'inactivity_time'} = $time*3600; + $$self{m_timerCheck}->set($time*3600, $self); + &::print_log("AD2USB_Motion_Item:: set_inactivity_alarm not supported"); +} + +1; + +#}}} +#$Log:$ + +__END__ + From 361924383b10bc660c55ed1c1310eed76dfee477 Mon Sep 17 00:00:00 2001 From: hplato Date: Fri, 3 Jan 2014 17:12:19 -0700 Subject: [PATCH 328/330] modified: AD2USB.pm --- lib/AD2USB.pm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/AD2USB.pm b/lib/AD2USB.pm index 8b6ac8520..76577fb7e 100755 --- a/lib/AD2USB.pm +++ b/lib/AD2USB.pm @@ -1104,7 +1104,12 @@ sub register { $_[0]->{zone_object}{$zone_num} = $object; } - +sub get_child_object_name { + my ($class,$zone_num) = @_; + my $object = $_[0]->{zone_object}{$zone_num}; + return $object->get_object_name() if defined ($object); +} + # MH-Style child objects # These allow zones to behave like Door_Items and Motion Sensors @@ -1113,7 +1118,11 @@ sub register { # ie. # $AD2USB = new AD2USB; # $Front_door = new AD2USB_Door_Item($AD2USB,1); +# states include open, closed and check # $Front_motion = new AD2USB_Motion_Item($AD2USB,2); +# states include motion and still +# +# inactivity timers are not working...don't know if those are relevant for panel items. package AD2USB_Door_Item; From dedfc5cf011d374853dc8b18a106653874aafd7c Mon Sep 17 00:00:00 2001 From: hplato Date: Wed, 8 Jan 2014 20:33:06 -0700 Subject: [PATCH 329/330] modified: bin/ical2vsdb modified: code/common/organizer.pl modified: web/organizer/calendar.pl --- bin/ical2vsdb | 48 ++++++++++++++++++++++------------ code/common/organizer.pl | 55 ++++++++++++++++++++++++++++++++------- web/organizer/calendar.pl | 32 +++++++++++++---------- 3 files changed, 96 insertions(+), 39 deletions(-) diff --git a/bin/ical2vsdb b/bin/ical2vsdb index 591549c59..83d00dbdd 100755 --- a/bin/ical2vsdb +++ b/bin/ical2vsdb @@ -9,13 +9,18 @@ use strict; ## ## Date::Calc ## DateTime::TimeZone +## Crypt::SSLeay is required if using https ## ## All other libs should exist w/i the core mh lib/site dir structure ## ## Changelog 3.4 10-04-06: for some reason ical parser is adding an 'ical' level in the ## ical parse hash from a Darwin Calendar Server. Added a dcsfix option - +## +## v4.0 01-2014: Date bugfix, added https method, added control calendar, and made sync_dtstamp +## and dcsfix defaults. Added in nosync_dtstamp and nodcsfix option to override. +## NOTE: v4 requires the update 2014 vsdb calendar schema (adding a control attribute). +## Common module organizer 2014 and vsdb calendar.pl 1.6.0-3 required. use lib '../lib', '../lib/site'; use iCal::Parser; @@ -33,12 +38,11 @@ use vsLock; # verify todos (uninitialized string message?) # verify locking works as expected - my $progname = "ical2vsdb"; -my $progver = "v3.4 10-04-06"; +my $progver = "v4 01-2014"; my $DB = 0; -my $days_before = 180; # defaults to avoid large vsdb databases, can be overriden +my $days_before = -180; # defaults to avoid large vsdb databases, can be overriden my $days_after = 180; # my $config_file= ""; @@ -55,6 +59,7 @@ $config_file = $ARGV[0] if $ARGV[0]; &help if ((lc $config_file eq "-h") or (lc $config_file eq "--h") or ($config_file eq "")); print "iCal to vsDB Misterhouse import ($progname $progver) starting...\n"; +print "Debug Level $DB\n" if ($DB); &purge_icals if ($config_file eq "--purge-ical-info"); @@ -120,10 +125,12 @@ while (1) { print "done\n"; $process = 1; - } elsif ((lc $ical_data[$loop]->{method} eq "http") or (lc $ical_data[$loop]->{method} eq "webcal")) { - print "Fetching via LWP..."; + } elsif ((lc $ical_data[$loop]->{method} eq "http") or (lc $ical_data[$loop]->{method} eq "https") or (lc $ical_data[$loop]->{method} eq "webcal")) { my $ua = new LWP::UserAgent; - my $req = new HTTP::Request GET => "http://" . $calendar_loc; + my $method = "http"; + $method .= "s" if (lc $ical_data[$loop]->{method} eq "https"); + print "Fetching via LWP:$method..."; + my $req = new HTTP::Request GET => $method . "://" . $calendar_loc; if ($ical_data[$loop]->{username}) { $req->authorization_basic($ical_data[$loop]->{username},$ical_data[$loop]->{password}); } @@ -162,7 +169,7 @@ while (1) { if ($process) { - if ($ical_data[$loop]->{options}->{sync_dtstamp}) { + unless ($ical_data[$loop]->{options}->{nosync_dtstamp}) { print "Syncing DTSTAMP attributes from CREATED...\n"; $data =~ s/DTSTAMP:(.*)\n//g; $data =~ s/CREATED:(.*)\n/CREATED:$1\nDTSTAMP:$1\n/g; @@ -178,7 +185,7 @@ while (1) { # print "Debug: Hash=$ical_data[$loop]->{hash}\n" if (defined $ical_data[$loop]->{hash}); if (!(defined $ical_data[$loop]->{hash}) or ($ical_data[$loop]->{hash} ne $digest)) { - print "New Calendar entries. Processing iCal..."; + print "New Calendar entries. Processing iCal."; if ($ical_data[$loop]->{method} ne "dir") { eval {$parser->parse_strings($data); }; } @@ -299,14 +306,16 @@ sub parse_cal { my $opt_speak_todo = 0; my $opt_holiday = 0; my $opt_vacation = 0; + my $opt_control = 0; $opt_speak_cal = 1 if (exists $options->{speak_cal}); $opt_speak_todo = 1 if (exists $options->{speak_todo}); $opt_holiday = 1 if (exists $options->{holiday}); $opt_vacation = 1 if (exists $options->{vacation}); + $opt_control = 1 if (exists $options->{control}); + my ($opt_sourcename) = $options->{name} if (defined $options->{name}); - $cal = $cal->{ical} if (exists $options->{dcsfix}); - print "Fixing Darwin Calendar Server Settings...\n" if ($DB and (exists $options->{dcsfix})); + $cal = $cal->{ical} unless (exists $options->{nodcsfix}); my $calname = $cal->{cals}->[0]->{'X-WR-CALNAME'}; $calname = $opt_sourcename if $opt_sourcename; @@ -334,16 +343,17 @@ sub parse_cal { print "."; #give some progress while (my $uid = each %{$cal->{events}->{$year}->{$month}->{$day}}) { my $delta; - #is this event in range? if ($days_before or $days_after) { $delta = Delta_Days($lyear,$lmon,$lday, $year,$month,$day); } + print "EventID:$uid, before=$days_before after=$days_after delta=$delta\n" if ($DB>2); #if not then skip the item next if ((($days_before) and ($days_before > $delta)) or - (($days_after) and ($days_after < $delta))); - + (($days_after) and ($days_after < $delta))); + + print "Processing EventID:$uid\n" if ($DB >2); my $event_ref = $cal->{events}->{$year}->{$month}->{$day}->{$uid}; my $starttime = "12:00 am"; # starttime and endtime are initialized the same if all day my $endtime = "12:00 am"; @@ -380,6 +390,8 @@ sub parse_cal { $holiday = "on" if $opt_holiday; my $vacation = "off"; $vacation = "on" if $opt_vacation; + my $control = "off"; + $control = "on" if $opt_control; my $description = $event_ref->{DESCRIPTION} || ''; if ($description) { @@ -399,6 +411,7 @@ sub parse_cal { $out_cals[$count]->{DETAILS} = $description; $out_cals[$count]->{HOLIDAY} = $holiday; $out_cals[$count]->{VACATION} = $vacation; + $out_cals[$count]->{CONTROL} = $control; $out_cals[$count]->{SOURCE} = $source; $out_cals[$count]->{REMINDER} = $reminder; $out_cals[$count]->{ENDTIME} = $endtime; @@ -542,7 +555,8 @@ sub init { $ical_data[$count]->{options}->{$key} = $value; } $ical_data[$count]->{hash} = "none"; - + print "Applying second level ical parsing (set nodscfix if this breaks anything) ...\n" unless (exists $ical_data[$count]->{options}->{nodcsfix}); + my ($method, $loc) = split(/:\/\//,$url); if ($loc) { my ($username, $password, $uri) = $loc =~ /^(\S+):(\S+)@(\S+)/i; @@ -593,12 +607,14 @@ sub help { print "usage\t$progname CONFIGURATION_FILE OUTPUT_DIR\n"; print " or\t$progname --purge-ical-info OUTPUT_DIR\n"; + print "version $progver\n"; print "\n\n"; print " -- CONFIG FILE SYNTAX -- \n"; print "TYPEVALUEoption\n\n"; print "TYPE = cfg_version|ical|days_before|days_after|sleep\n"; print "VALUE = parameter value or ical location (http://, file://, dir://)\n"; - print "OPTION = comma delimited values (holiday,name=Joe User,speak_todos,vacation,sync_dtstamp)\n"; + print "OPTION = comma delimited values (holiday,name=Joe User,speak_todos,vacation,\n"; + print " control, nodcsfix, nosync_dtstamp)\n"; print "\n"; die; diff --git a/code/common/organizer.pl b/code/common/organizer.pl index 162fbd818..4fe66f6e6 100644 --- a/code/common/organizer.pl +++ b/code/common/organizer.pl @@ -1,18 +1,19 @@ # Category = Time -#@ This module is a significant update from 2.103, and has a few functions; +#@ This module is a significant update from v3, and has a few functions; #@
    #@
      #@
    • iCal2vsDB syncronization control. Imports iCal files (Apple iCal, #@ Mozilla Sunbird) into MH as standard calendars, or holiday/vacation #@ calendars. +#@
    • New in v3.1 now can control objects using a control calendar #@
    • Monitors the vsDB calendar and todo files and creates required events #@ to process these items (creates organizer_*.pl files in the code dir) #@
    • Implements an Organizer_Events class for manipulating events, holidays #@ and vacations. #@
    • Automatically updates vsDB 'databases' with the new required fields #@
      -#@ Minimum Requirements: calendar.pl 1.5.7-4 and tasks.pl 1.4.8-4 (Misterhouse v2.104) +#@ Minimum Requirements: calendar.pl 1.6.0-3 and tasks.pl 1.4.8-4 (Misterhouse v2.104) =begin comment @@ -37,22 +38,29 @@ url examples: http://server/path/to/icalfile.ics +https://server/path/to/icalfile.ics file://path/to/icalfile.ics http://user@pass:server/file.ics ical2vsdb__options = comma delimited list of ical processing options Options available: -sync_dtstamp some calendars (ie google) update the dtstamp field each time the calendar - is downloaded, such that it is processed each time. Setting this option - syncs the dtstamp field with created, ensuring that ical2vsdb only runs when - the calendar has changed. + speak_cal to speak calendar entries speak_todo to speak task entries holiday calendar entries should be treated as holiday time vacation calendar entries should be treated as vacation time name=XXXX set source name to XXX rather than parse it from inside the ical -dcsfix might be needed to parse calendars using the Darwin Calendar Server + +Changed in ical2vsdb 4 +nodcsfix most calendar servers (ie google) need a second level parse. Set this if the ical + isn't being processed +nosync_dtstamp some calendars (ie google) update the dtstamp field each time the calendar + is downloaded, such that it is processed each time. ical2vsdb syncs these fields + ensuring that ical2vsdb only runs when the calendar has changed. Set this if there is + too much processing not if non-google calendars aren't being processed +control For dedicated item control calendars. MH objects with the same name as the + event will be turned on during the event duration ie ical2vsdb_account1 = http://house/holical.ics @@ -67,6 +75,8 @@ iCal2vsDB uses iCal::Parser, which has several significant dependancies to operate correctly, these have been included in lib/site +for https access Crypt::SSLeay needs to be installed manually as well + =cut @@ -198,7 +208,7 @@ package main; if defined $main::config_parms{organizer_announce_priorday_times}; #Check to see if calendar and organizer databases need upgrade - my @_upd_cal = ( 'DATE','TIME','EVENT','CATEGORY','DETAILS','HOLIDAY','VACATION','SOURCE','REMINDER','ENDTIME' ); + my @_upd_cal = ( 'DATE','TIME','EVENT','CATEGORY','DETAILS','HOLIDAY','VACATION','SOURCE','REMINDER','ENDTIME','CONTROL' ); $calOk = &update_vsdb('Calendar',$_organizer_cal->name,@_upd_cal); my @_upd_todo = ( 'Complete','Description','DueDate','AssignedTo','Notes','SPEAK','SOURCE','REMINDER','STARTDATE','CATEGORY' ); @@ -309,12 +319,14 @@ package main; $data{reminder} = $objDB->FieldValue('REMINDER'); $data{category} = $objDB->FieldValue('CATEGORY'); $data{endtime} = $objDB->FieldValue('ENDTIME'); + $data{control} = $objDB->FieldValue('CONTROL'); $data{endtime} = (!($data{endtime}) && $data{time}) ? $data{time} : $data{endtime}; $data{allday} = ($data{time} eq $data{endtime}) ? 'Yes' : 'No'; $data{notes} = $objDB->FieldValue('DETAILS'); $data{startdt} = $data{date} . ' ' . (($data{time}) ? $data{time} : "12:00 am"); $data{enddt} = $data{date} . ' ' - . (($data{endtime} && $data{endtime} !~ /12:00 am/i) ? $data{time} : "11:59 pm"); + . (($data{endtime} && $data{endtime} !~ /12:00 am/i) ? $data{endtime} : "11:59 pm"); + #changed to notify an array of email addresses $data{name_count} = 0; @@ -474,6 +486,9 @@ sub generate_code { my $default_reminder = $main::config_parms{organizer_reminder}; $default_reminder = '15m' unless $default_reminder; $data{reminder} = $default_reminder unless $data{reminder} or $data{allday} =~ /^y/i; + +#print_log "organizerDB: data{type}=$data{type} data{control}=$data{control} data{desc}=$data{description}"; + my $task_flag = $main::config_parms{organizer_vc_category}; if (($task_flag) && ($data{type} eq 'task') && (($data{category} and ($data{category} =~ /^$task_flag/i)) or ($data{description} =~ /^$task_flag/i))) { @@ -494,6 +509,28 @@ sub generate_code { } return; } + + # control calendars turn item on and off. Note that this only tests if an object exists. A better way + # might be to check if on & off are valid states... + if (($data{type} eq 'event') and ($data{control} eq 'on')) { + my $obj = $data{description}; + #&main::print_log("organizerDB: found control event $obj starting $data{startdt} ending $data{enddt}"); + my $obj_test = ''; + my $obj_state = ''; + eval { $obj_test = &main::get_object_by_name($obj); $obj_state = state $obj_test }; + if ($obj_state) { + if (($data{startdt}) and ($data{enddt})) { + print MYCODE "if (time_now '$data{startdt}') { set \$$obj ON; }; #Control Event\n"; + print MYCODE "if (time_now '$data{enddt}') { set \$$obj OFF; }; #Control Event\n"; + } else { + &main::print_log("Organizer: Warning, invalid times for event object $obj. Ignoring Event on $data{startdt}"); + } + } else { + &main::print_log("Organizer: Warning, cannot determing state of event object $obj. This item might not exist. Ignoring Event on $data{startdt}"); + } + return; + } + if ($data{reminder} and !(time_greater_than($data{time_date}) or $data{reminder} eq 'none')) { my @reminders = split(/,/,$data{reminder}); for my $reminder_info (@reminders) { diff --git a/web/organizer/calendar.pl b/web/organizer/calendar.pl index c224a889c..5fc036184 100644 --- a/web/organizer/calendar.pl +++ b/web/organizer/calendar.pl @@ -15,6 +15,7 @@ # or indirectly caused by this software. # # Version History +# 1.6.0-3 - 01/03/14 - added support for control calendars (hide them) # 1.6.0-2 - 11/02/07 - minor bugfix to in icalsync name # 1.6.0-1 - 09/24/07 - Updated to organizer release 2.5.2 without admin login and ical customization # (1.6.0 added admin login & changed navigation) @@ -30,7 +31,7 @@ # 1.5.2 - 08/22/01 - added file locking # ---------------------------------------------------------------------------- -my $VERSION = "1.6.0-2"; +my $VERSION = "1.6.0-3"; BEGIN { # $SIG{__WARN__} = \&FatalError; @@ -338,18 +339,19 @@ sub PrintDay { } while (!$objMyDb->EOF) { - my $custcolor = ""; - $custcolor = " bgcolor='$dataHolidayColor' " if ($objMyDb->FieldValue("HOLIDAY") eq "on"); - $custcolor = " bgcolor='$dataVacationColor' " if ($objMyDb->FieldValue("VACATION") eq "on"); - $custcolor = " bgcolor='$dataMultipleColor' " if (($objMyDb->FieldValue("VACATION") eq "on") and ($objMyDb->FieldValue("HOLIDAY") eq "on")); # if a day is both vacation and holiday - - my $icon = $detailIcon; - my $source = $objMyDb->FieldValue("SOURCE"); - $icon = "images/ical_1.jpg" if ($source =~ /^ical=/); - my $link = ""; - print "" . $link . ""; - print "" . $objMyDb->FieldValue("TIME") . " "; - print "" . $link . "" . $objMyDb->FieldValue("EVENT") . " \n"; + unless ($objMyDb->FieldValue("CONTROL") eq "on") { #Don't display CONTROL calendars + my $custcolor = ""; + $custcolor = " bgcolor='$dataHolidayColor' " if ($objMyDb->FieldValue("HOLIDAY") eq "on"); + $custcolor = " bgcolor='$dataVacationColor' " if ($objMyDb->FieldValue("VACATION") eq "on"); + $custcolor = " bgcolor='$dataMultipleColor' " if (($objMyDb->FieldValue("VACATION") eq "on") and ($objMyDb->FieldValue("HOLIDAY") eq "on")); # if a day is both vacation and holiday + my $icon = $detailIcon; + my $source = $objMyDb->FieldValue("SOURCE"); + $icon = "images/ical_1.jpg" if ($source =~ /^ical=/); + my $link = ""; + print "" . $link . ""; + print "" . $objMyDb->FieldValue("TIME") . " "; + print "" . $link . "" . $objMyDb->FieldValue("EVENT") . " \n"; + } $objMyDb->MoveNext; } @@ -460,7 +462,7 @@ sub PrintMonth { if ($showDayDetails) { print ""; while (!$objMyDb->EOF) { - print "" .$objMyDb->FieldValue("EVENT") . "
      "; + print "" .$objMyDb->FieldValue("EVENT") . "
      " unless ($objMyDb->FieldValue("CONTROL") eq "on"); #Don't display CONTROL calendars; $objMyDb->MoveNext; } print "
      "; @@ -573,6 +575,8 @@ sub PrintCurrentRecord { } elsif ($fieldName eq "ENDTIME") { $endtime_entry = $objMyDB->FieldValue($fieldName); + } elsif ($fieldName eq "CONTROL") { + #Don't display CONTROL field. Do nothing } else { print "\n"; print "" . $fieldName . "\n"; From 1555d1357f54b63f8ab14ffad986e00a04f8ce65 Mon Sep 17 00:00:00 2001 From: hplato Date: Sat, 11 Jan 2014 11:52:57 -0700 Subject: [PATCH 330/330] Added Calendar v4, bug fixes, https and control calendar modified: bin/ical2vsdb modified: code/common/organizer.pl modified: web/organizer/calendar.pl --- bin/ical2vsdb | 1 + code/common/organizer.pl | 2 +- web/organizer/calendar.pl | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/ical2vsdb b/bin/ical2vsdb index 83d00dbdd..03e8efcfa 100755 --- a/bin/ical2vsdb +++ b/bin/ical2vsdb @@ -21,6 +21,7 @@ use strict; ## and dcsfix defaults. Added in nosync_dtstamp and nodcsfix option to override. ## NOTE: v4 requires the update 2014 vsdb calendar schema (adding a control attribute). ## Common module organizer 2014 and vsdb calendar.pl 1.6.0-3 required. +## use lib '../lib', '../lib/site'; use iCal::Parser; diff --git a/code/common/organizer.pl b/code/common/organizer.pl index 4fe66f6e6..61eac3806 100644 --- a/code/common/organizer.pl +++ b/code/common/organizer.pl @@ -1,6 +1,6 @@ # Category = Time -#@ This module is a significant update from v3, and has a few functions; +#@ This module is a significant update from MH v2, and has a few functions; #@
      #@
        #@
      • iCal2vsDB syncronization control. Imports iCal files (Apple iCal, diff --git a/web/organizer/calendar.pl b/web/organizer/calendar.pl index 5fc036184..0e1d262e6 100644 --- a/web/organizer/calendar.pl +++ b/web/organizer/calendar.pl @@ -15,7 +15,7 @@ # or indirectly caused by this software. # # Version History -# 1.6.0-3 - 01/03/14 - added support for control calendars (hide them) +# 1.6.0-3 - 01/04/14 - added support for control calendars (hide them) # 1.6.0-2 - 11/02/07 - minor bugfix to in icalsync name # 1.6.0-1 - 09/24/07 - Updated to organizer release 2.5.2 without admin login and ical customization # (1.6.0 added admin login & changed navigation)