From e8f1e9366db27183c480429c9c69a5d4a7e619e1 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Thu, 12 Jan 2017 15:24:06 -0500 Subject: [PATCH 01/78] Commit unmodifed web/bin/runit.pl For some reason (lineend differences?) web/bin/runit.pl shows up as modified in my local repository even though I have not touched. Hopefully committing will fix this issue. --- web/bin/runit.pl | 74 ++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/web/bin/runit.pl b/web/bin/runit.pl index f4883b063..dc823a603 100644 --- a/web/bin/runit.pl +++ b/web/bin/runit.pl @@ -1,38 +1,38 @@ -return &runit(@ARGV); -sub runit { - my ($cmd) = "@_"; - #print_log "-------------------- Original Command: $cmd"; - $cmd =~ s/_/ /g; - #print_log "-------------------- Manipulated Command: $cmd"; - # Look for exact command matches - if (&process_external_command($cmd, 1, 'android', 'speak')) { - print_log "-------------------- Exact Command Match $cmd"; - return &html_page('', 'done'); - } - - # added by Brian: STRIP out articles and then check for exact command match - $cmd =~ s/the //g; - $cmd =~ s/to //g; - $cmd =~ s/turn //g; - - $cmd =~ s/an //g; - $cmd =~ s/make //g; - $cmd =~ s/switch //g; - - if (&process_external_command($cmd, 1, 'android', 'speak')) { - print_log "-------------------- Exact Command Match removing articles $cmd"; - return &html_page('', 'done'); - } - - # Look for nearest fuzzy match - my $cmd1 = &phrase_match1($cmd); - print_log "-------------------- Fuzzy Command Match $cmd1"; - &process_external_command($cmd1, 1, 'android', 'speak'); - return &html_page('', 'done'); - -# # Added by Brian: Give up -# print_log "-------------------- No command found $cmd"; -# play('file' => 'c:\mh\sounds\log.wav'); -# return &html_page('', 'No command found'); - +return &runit(@ARGV); +sub runit { + my ($cmd) = "@_"; + #print_log "-------------------- Original Command: $cmd"; + $cmd =~ s/_/ /g; + #print_log "-------------------- Manipulated Command: $cmd"; + # Look for exact command matches + if (&process_external_command($cmd, 1, 'android', 'speak')) { + print_log "-------------------- Exact Command Match $cmd"; + return &html_page('', 'done'); + } + + # added by Brian: STRIP out articles and then check for exact command match + $cmd =~ s/the //g; + $cmd =~ s/to //g; + $cmd =~ s/turn //g; + + $cmd =~ s/an //g; + $cmd =~ s/make //g; + $cmd =~ s/switch //g; + + if (&process_external_command($cmd, 1, 'android', 'speak')) { + print_log "-------------------- Exact Command Match removing articles $cmd"; + return &html_page('', 'done'); + } + + # Look for nearest fuzzy match + my $cmd1 = &phrase_match1($cmd); + print_log "-------------------- Fuzzy Command Match $cmd1"; + &process_external_command($cmd1, 1, 'android', 'speak'); + return &html_page('', 'done'); + +# # Added by Brian: Give up +# print_log "-------------------- No command found $cmd"; +# play('file' => 'c:\mh\sounds\log.wav'); +# return &html_page('', 'No command found'); + } \ No newline at end of file From 378a11e2f93b1f795857fd5bc56ff9d6b634e8f2 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Thu, 12 Jan 2017 15:37:12 -0500 Subject: [PATCH 02/78] Remove unused internet_weather.pl parameters. Remove mh.ini parameters and variables not used anymore by code/common/internet_weather.pl after recent internet_weather.pl changes to accomodate NOAA website changes. --- bin/mh.ini | 13 ------------- code/common/internet_weather.pl | 18 +++++------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/bin/mh.ini b/bin/mh.ini index 6fec86287..a035e6d7d 100644 --- a/bin/mh.ini +++ b/bin/mh.ini @@ -2151,19 +2151,6 @@ weather_internet_elements = all @ Set to 0 to use Humidex, 1 to use Heat Index weather_use_heatindex=0 -@ This parameter is used by the common code file internet_weather.pl to download -@ weather data from the nearest NWS station. Use this link , click -@ on your state, and then click "Hourly Report" to locate the nearest supported city or station. - -nws_city= - -@ This parameter is used by the common code file internet_weather.pl to download -@ weather data from the nearest NWS weather zone. Use this link, click -@ on your state, and then click "Zone Forecast" to locate the nearest zone to your location, for example, -@ zone=SAN DIEGO COUNTY COASTAL AREAS - -zone= - @ This parameter is used by the common code file internet_weather_noaa.pl. @ Locate the NOAA station nearest to your location at http://www.weather.gov/data/current_obs/, @ for example, weather_noaa_station=KSAN diff --git a/code/common/internet_weather.pl b/code/common/internet_weather.pl index 3c8afde7b..6fb9ea1b6 100644 --- a/code/common/internet_weather.pl +++ b/code/common/internet_weather.pl @@ -4,13 +4,10 @@ # $Revision$ #@ Retrieves current weather conditions and forecasts using bin/get_weather (US only). -#@ You will need to set the city, zone, and state parms in your ini file. +#@ You will need to set the city and state parms in your ini file. #@ To verify your city, click here, #@ then click on your state, then click on "Hourly Reports". If your city -#@ is not listed in the report, pick the closest one. The zone is usually -#@ the same as your city, but not always. To verify your zone, -#@ hit the Back button and click on "Zone Forecast". Zone names precede each -#@ forecast and each is followed by a hyphen. +#@ is not listed in the report, pick the closest one. #@ To modify when this script is run (or to disable it), go to the #@ triggers page and modify the #@ 'get internet weather conditions' and 'get internet weather forecast' triggers. @@ -22,21 +19,21 @@ $v_get_internet_weather_data = new Voice_Cmd('[Get,Check,Mail,SMS] Internet weather data'); $v_get_internet_weather_data->set_info( - "Retrieves weather conditions and forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}" + "Retrieves weather conditions and forecasts for $config_parms{city}, $config_parms{state}" ); # Get the current weather data from the Internet $v_get_internet_weather_conditions = new Voice_Cmd('Get the Internet weather conditions'); $v_get_internet_weather_conditions->set_info( - "Retrieves current weather conditions for $config_parms{city}, $config_parms{state}, $config_parms{zone}" + "Retrieves current weather conditions for $config_parms{city}, $config_parms{state}" ); # Get the weather forecast from the Internet $v_get_internet_weather_forecast = new Voice_Cmd('Get the Internet weather forecast'); $v_get_internet_weather_forecast->set_info( - "Retrieves weather forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}" + "Retrieves weather forecasts for $config_parms{city}, $config_parms{state}" ); $v_show_internet_weather_forecast = @@ -58,8 +55,6 @@ "$config_parms{data_dir}/web/weather_conditions.txt"; $f_weather_conditions = new File_Item($weather_conditions_path); $f_weather_forecast = new File_Item($weather_forecast_path); -my $city = $config_parms{city}; -$city = $config_parms{nws_city} if defined $config_parms{nws_city}; $p_weather_data = new Process_Item; $p_weather_conditions = new Process_Item; $p_weather_forecast = new Process_Item; @@ -84,7 +79,6 @@ sub normalize_conditions { start $p_weather_data; $v_get_internet_weather_data->respond( "app=weather Weather data requested for $config_parms{city}, $config_parms{state}" - . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { @@ -101,7 +95,6 @@ sub normalize_conditions { start $p_weather_conditions; $v_get_internet_weather_conditions->respond( "app=weather Weather conditions requested for $config_parms{city}, $config_parms{state}" - . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { @@ -119,7 +112,6 @@ sub normalize_conditions { start $p_weather_forecast; $v_get_internet_weather_forecast->respond( "app=weather Weather forecast requested for $config_parms{city}, $config_parms{state}" - . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { From 44804cb52b6087e37d20603be4d41986d93cb0bd Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Tue, 4 Apr 2017 18:46:54 -0400 Subject: [PATCH 03/78] Saving files before refreshing line endings --- code/common/vr_match.pl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/code/common/vr_match.pl b/code/common/vr_match.pl index bf2b7df4c..e24785194 100644 --- a/code/common/vr_match.pl +++ b/code/common/vr_match.pl @@ -1,16 +1,16 @@ -sub phrase_match1 { - my ($phrase) = @_; - my (%list1); - my $d_min1 = 999; - my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; - my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); - for my $phrase2 (sort @phrases) { - my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); -# my $brianlendist = abs(length($phrase)-length($phrase2)); -# $d = $brianlendist + $d; -# print_log "---------------- $phrase --- $phrase2 --- $d"; - push @{$list1{$d}}, $phrase2 if $d <= $d_min1; - $d_min1 = $d if $d < $d_min1; - } - return ${$list1{$d_min1}}[0]; +sub phrase_match1 { + my ($phrase) = @_; + my (%list1); + my $d_min1 = 999; + my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); + for my $phrase2 (sort @phrases) { + my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); +# my $brianlendist = abs(length($phrase)-length($phrase2)); +# $d = $brianlendist + $d; +# print_log "---------------- $phrase --- $phrase2 --- $d"; + push @{$list1{$d}}, $phrase2 if $d <= $d_min1; + $d_min1 = $d if $d < $d_min1; + } + return ${$list1{$d_min1}}[0]; } \ No newline at end of file From c1f72c818f00cb1bbcc70ce235a35f157238f3a5 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Tue, 4 Apr 2017 21:55:15 -0400 Subject: [PATCH 04/78] Saving files before refreshing line endings --- code/common/vr_match.pl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/code/common/vr_match.pl b/code/common/vr_match.pl index bf2b7df4c..e24785194 100644 --- a/code/common/vr_match.pl +++ b/code/common/vr_match.pl @@ -1,16 +1,16 @@ -sub phrase_match1 { - my ($phrase) = @_; - my (%list1); - my $d_min1 = 999; - my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; - my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); - for my $phrase2 (sort @phrases) { - my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); -# my $brianlendist = abs(length($phrase)-length($phrase2)); -# $d = $brianlendist + $d; -# print_log "---------------- $phrase --- $phrase2 --- $d"; - push @{$list1{$d}}, $phrase2 if $d <= $d_min1; - $d_min1 = $d if $d < $d_min1; - } - return ${$list1{$d_min1}}[0]; +sub phrase_match1 { + my ($phrase) = @_; + my (%list1); + my $d_min1 = 999; + my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); + for my $phrase2 (sort @phrases) { + my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); +# my $brianlendist = abs(length($phrase)-length($phrase2)); +# $d = $brianlendist + $d; +# print_log "---------------- $phrase --- $phrase2 --- $d"; + push @{$list1{$d}}, $phrase2 if $d <= $d_min1; + $d_min1 = $d if $d < $d_min1; + } + return ${$list1{$d_min1}}[0]; } \ No newline at end of file From d8ca655576263ed14faa03a6d06c5369f31f707a Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Tue, 4 Apr 2017 22:06:52 -0400 Subject: [PATCH 05/78] Saving files before refreshing line endings --- code/common/vr_match.pl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/code/common/vr_match.pl b/code/common/vr_match.pl index bf2b7df4c..e24785194 100644 --- a/code/common/vr_match.pl +++ b/code/common/vr_match.pl @@ -1,16 +1,16 @@ -sub phrase_match1 { - my ($phrase) = @_; - my (%list1); - my $d_min1 = 999; - my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; - my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); - for my $phrase2 (sort @phrases) { - my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); -# my $brianlendist = abs(length($phrase)-length($phrase2)); -# $d = $brianlendist + $d; -# print_log "---------------- $phrase --- $phrase2 --- $d"; - push @{$list1{$d}}, $phrase2 if $d <= $d_min1; - $d_min1 = $d if $d < $d_min1; - } - return ${$list1{$d_min1}}[0]; +sub phrase_match1 { + my ($phrase) = @_; + my (%list1); + my $d_min1 = 999; + my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); + for my $phrase2 (sort @phrases) { + my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); +# my $brianlendist = abs(length($phrase)-length($phrase2)); +# $d = $brianlendist + $d; +# print_log "---------------- $phrase --- $phrase2 --- $d"; + push @{$list1{$d}}, $phrase2 if $d <= $d_min1; + $d_min1 = $d if $d < $d_min1; + } + return ${$list1{$d_min1}}[0]; } \ No newline at end of file From 58726a5d6202598cae6def11d3b25ac15f2a2235 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Tue, 4 Apr 2017 22:19:56 -0400 Subject: [PATCH 06/78] Added debug prints to troubleshoot group creation --- lib/Group.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Group.pm b/lib/Group.pm index 5b422f8ee..3eee7a28b 100644 --- a/lib/Group.pm +++ b/lib/Group.pm @@ -60,6 +60,7 @@ sub new { &add( $self, @items ) if @items; $self->{logger_enable} = $main::config_parms{object_logger_group} if ( defined $main::config_parms{object_logger_group} ); bless $self, $class; +&::print_log("new group: " . $self->get_object_name() . "\n"); return $self; } @@ -80,6 +81,7 @@ sub add { # This allows us to monitor changed members for my $ref (@items) { $ref->tie_items( $self, undef, 'member changed' ); +&::print_log("add to " . $self . "(" . $self->get_object_name() . "): $ref" . $ref->get_object_name() . "xxx\n"); if ( $ref->isa('X10_Item') ) { if ( can_dim($ref) ) { From 2072c5ca00e474c5b0b8e48138bf2099e31971eb Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Tue, 4 Apr 2017 22:26:39 -0400 Subject: [PATCH 07/78] Allow overwrite of _on_set_message data --- lib/xPL_Items.pm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/xPL_Items.pm b/lib/xPL_Items.pm index e73777177..1ac49d4ec 100644 --- a/lib/xPL_Items.pm +++ b/lib/xPL_Items.pm @@ -833,6 +833,13 @@ sub device_name { sub on_set_message { my ( $self, @data ) = @_; + + # Delete any existing _on_set_message data so the user can + # fully overwrite with a call to the on_set_message() method. + # Otherwise we will add user data on top of the default + # _on_set_message data that is added in the constructor. + delete $$self{_on_set_message}; + while (@data) { my $section = shift @data; my $ptr = shift @data; From 4120ec34fe07cb2489a6f8b9b9a7bbf44952dacd Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Tue, 4 Apr 2017 22:34:21 -0400 Subject: [PATCH 08/78] Fix Owfs_Item _ToServer calls Use correct buffer lengths in calls to _ToServer in Owfs_Item. --- lib/Owfs_Item.pm | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/Owfs_Item.pm b/lib/Owfs_Item.pm index b84071104..cf5ccce5f 100644 --- a/lib/Owfs_Item.pm +++ b/lib/Owfs_Item.pm @@ -357,8 +357,13 @@ sub set { #$value .= ' '; my $value_length = length($value); - my $payload = pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); - $self->_ToServer( $path, $token, $value, $set_by, length($payload) + 1, $msg_write, $value_length, 0, $payload ); + my $payload = + pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); + $self->_ToServer( + $path, $token, $value, $set_by, + length($payload), $msg_write, $value_length, 0, + $payload + ); } # This method is called to schedule a read command be sent to the owserver for the object. @@ -367,7 +372,8 @@ sub get { return if ( !defined $self->{path} ); my $path = $self->{path} . $token; &main::print_log("Owfs_Item::get path: $path") if $main::Debug{owfs}; - $self->_ToServer( $path, $token, 0, 0, length($path) + 1, $msg_read, $default_block, 0, $path ); + $self->_ToServer( $path, $token, 0, 0, length($path), + $msg_read, $default_block, 0, $path ); } # This method is called to schedule a directory command be sent to the owserver for the object. @@ -378,7 +384,8 @@ sub _dir { # new msg_dirall method -- single packet &main::print_log("Owfs_Item::dir path: $path") if $main::Debug{owfs}; $self->{dir_path} = $path; - $self->_ToServer( $path, 0, 0, 0, length($path) + 1, $msg_dirall, $default_block, 0, $path ); + $self->_ToServer( $path, 0, 0, 0, length($path), + $msg_dirall, $default_block, 0, $path ); } # This method is called to schedule a write command be sent to the owserver for the object. @@ -397,8 +404,13 @@ sub _set_root { #$value .= ' '; my $value_length = length($value); - my $payload = pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); - $self->_ToServer( $path, $token, $value, $set_by, length($payload) + 1, $msg_write, $value_length, 0, $payload ); + my $payload = + pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); + $self->_ToServer( + $path, $token, $value, $set_by, + length($payload), $msg_write, $value_length, 0, + $payload + ); } # This method is called to schedule a read command be sent to the owserver for the object. @@ -410,7 +422,8 @@ sub _get_root { return if ( !defined $root ); my $path = $self->{root} . $token; &main::print_log("Owfs_Item::_get_root path: $path") if $main::Debug{owfs}; - $self->_ToServer( $path, $token, 0, 0, length($path) + 1, $msg_read, $default_block, 0, $path ); + $self->_ToServer( $path, $token, 0, 0, length($path), + $msg_read, $default_block, 0, $path ); } # This method is used to search the one-wire tree for the specific object as defined @@ -497,12 +510,20 @@ sub _chomp_plus { # This method is a direct port from the OWNet.pm module from owfs. This is the lower layer interface # to the owserver socket port. sub _ToServer { - my ( $self, $path, $token, $value, $set_by, $payload_length, $msg_type, $size, $offset, $payload_data ) = @_; - my $f = "N6Z$payload_length"; - - #$f .= 'Z'.$payload_length if ( $payload_length > 0 ) ; - my $message = pack( $f, $self->{VER}, $payload_length, $msg_type, $self->{SG} | $self->{PERSIST}, $size, $offset, $payload_data ); - &main::print_log("Owfs_Item::_ToServer path: $path payload_length: $payload_length payload_data: $payload_data message: $message") if $main::Debug{owfs}; + my ( + $self, $path, $token, $value, + $set_by, $payload_length, $msg_type, $size, + $offset, $payload_data + ) = @_; + my $f = "N6A$payload_length"; + + my $message = pack( $f, + $self->{VER}, $payload_length, $msg_type, + $self->{SG} | $self->{PERSIST}, + $size, $offset, $payload_data ); + &main::print_log( + "Owfs_Item::_ToServer path: $path payload_length: $payload_length payload_data: $payload_data message: $message" + ) if $main::Debug{owfs}; my $hashref = { msg_type => $msg_type, self => $self, From e7c33d66553d62c1ba4a3bd7034af6546667815a Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Sat, 4 Nov 2017 01:54:10 -0400 Subject: [PATCH 09/78] Add support for the AoG Smart Home Provider Add support for the Actions on Google Smart Home Provider so the Google Assistant (on the Google Home and other devices like smartphones) can interact with MisterHouse. An Actions on Google Smart Home Provider implementation requires four things: 1. An OAuth server to the user can link a MisterHouse account to the Actions on Google Smart Home Provider from the Google Home app. 2. A handler for a "sync" request from the Google Assistant. This tells the Google Assistant what MisterHouse devices are available for control. 3. A handler for a "query" request from the Google Assistant. The Assistant sends "query" requests when it needs to know the state of a device. 4. A handler for an "execute" request from the Google Assistant. The Assistant sends "execute" requests when the user requests an action to be performed, like turning on or off a light, dimming or brightening a light, etc. See https://developers.google.com/actions/smarthome/ for details. This initial commit adds supports to MisterHouse for all the four pieces described above. The devices that are to be exposed to the Actions on Google Smart Home Provider must be specified via AOGSMARTHOME_ITEMS and AOGSMARTHOME_ITEM items (which are new) in a .mht file (or via user code). This implementation adds a hook to the MisterHouse HTTP server (lib/http_server.pl) to process two specific HTTP requests from the Actions on Google Smart Home Provider, one for the OAuth flow, and the other for the sync, query, and execute commands. The code that handles these two HTTP requests lives in the new file lib/http_server_aog.pl. Everything else lives in the new file lib/AoGSmartHome_Items.pm. The commit also makes some small cleanups to lib/http_server.pl that are not related to the Actions on Google Smart Home Provider implementation. Specifically: * Use the m// and s// operators with delimiters other than '/' so heavy escaping of '/' is not necessary, e.g. m%/path/to/file% instead of m/\/path\/to\/file/. This makes the regular expressions easier to read, especially when the code handles HTTP requests that contain paths. * Match $Http{'Content-Type'} against "application/json" using a regular expression as "if (lc($Http{'Content-Type'}) eq lc('application/json') )" breaks when Content-Type is "application/json;charset=UTF-8", which the Actions on Google Smart Home Provider is sending. --- bin/mh | 8 +- lib/AoGSmartHome_Items.pm | 922 ++++++++++++++++++++++++++++++++++++++ lib/http_server.pl | 145 +++--- lib/http_server_aog.pl | 313 +++++++++++++ lib/read_table_A.pl | 22 + 5 files changed, 1337 insertions(+), 73 deletions(-) create mode 100644 lib/AoGSmartHome_Items.pm create mode 100644 lib/http_server_aog.pl diff --git a/bin/mh b/bin/mh index 6f46a9c1c..3567514f4 100755 --- a/bin/mh +++ b/bin/mh @@ -797,6 +797,10 @@ sub setup { } else { require 'json_server.pl'; + + # Actions on Google HTTP helper + require 'http_server_aog.pl'; + http_server_aog_startup(); } if ($OS_win) { @@ -2686,7 +2690,9 @@ sub check_for_socket_data { } else { # 1500 is ethernet packet size - my $from_saddr = recv( $sock, $data, 1500, 0 ); + # FIXME. Breaks when receiving more than 1500. Who's at fault? + # peloy@chapus.net + my $from_saddr = recv( $sock, $data, 4500, 0 ); # Store udp from_* data if ( $Socket_Ports{$port_name}{protocol} diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm new file mode 100644 index 000000000..10e47aa59 --- /dev/null +++ b/lib/AoGSmartHome_Items.pm @@ -0,0 +1,922 @@ + +=head1 B + +=head2 DESCRIPTION + +This module provides support for the Actions on Google Smart Home +provider. + +=head2 CONFIGURATION + +The AoGSmartHome_Items object holds the configured Misterhouse +objects that are presented to the Actions on Google Smart Home +provider. + +=head2 mh.private.ini Configuration + +# All options + + aog_enable = 1 # Enable the module + aog_auth_path = /oauth # OAuth URI + aog_fulfillment_url = /aog # Fulfillment URI + aog_client_id = # OAuth client ID + aog_oauth_token_file = xxxxxx # OAuth token file + aog_project_id = xxxxxxx # Google project ID + aog_uuid_start = x # UUID start + aog_agentuserid = xxxxx # Agent User ID (optional but recommended) + +=head2 Defining the Primary Object + +The object can be defined in the user code or in a .mht file. + +In mht: + +AOGSMARTHOME_ITEMS, + +ie: + + AOGSMARTHOME_ITEMS, AoGSmartHomeItems + +Or in user code: + + = new AoGSmartHome_Items(); + +ie: + + $AoGSmartHomeItems = new AoGSmartHome_Items(); + +=head2 NOTES + +The most important part of the configuration is mapping the objects/code +you want to present to the Actions on Google Smart Home Provider. This +allows the user to map pretty much anything in MisterHouse to the +Actions on Google Smart Home Provider. + + AOGSMARTHOME_ITEM, , , , , + , + + - This is the only required parameter. If you are +good with the defaults, you can add an object like: + +# In MHT + + AOGSMARTHOME_ITEM, AoGSmartHomeItems, light1 + +# or in user code + + $AoGSmartHomeItems->add('$light1'); + + - This defaults to using the without the $. If want to change the name you say to the +Echo/GH to control the object, you can define it here. You can also make +aliases for objects so it's easier to remember. + + - This defaults to 'set' which +works for most objects. You can also put a code reference or +'run_voice_cmd'. + + - If you want to set an object to +something other than 'on' when you say 'on' to the Echo/GH, you can define +it here. Defaults to 'on'. + + - If you want to set an object to +something other than 'off' when you say 'off' to the Echo/GH, you can +define it here. Defaults to 'off'. + + - If your object uses a custom sub to +get the state, define it here. Defaults to 'state' which works for most +objects. + + +The dim % is the actual number you say to Alexa, so if you say "Alexa,Set +Light 1 to 75 %" then the dim % value will be 75. + +The module supports 300 devices which is the max supported by the Echo + +=head2 Complete Examples + +MHT examples: + + AOGSMARTHOME_ITEMS, AoGSmartHomeItems + AOGSMARTHOME_ITEM, AoGSmartHomeItems, light1 light1, set, on, off, state # these are the defaults + AOGSMARTHOME_ITEM, AoGSmartHomeItems, light1 # same as the line above + AOGSMARTHOME_ITEM, AoGSmartHomeItems, light3, Test_Light_3 # if you want to change the name you say + AOGSMARTHOME_ITEM, AoGSmartHomeItems, testsub, Test_Sub, \&testsub +# "!" will be replaced with the action ( on/off/ ), so if you say "turn on test voice" then the module will run run_voice_cmd("test voice on") + AOGSMARTHOME_ITEM, AoGSmartHomeItems, test_voice_!, Test_Voice, run_voice_cmd + +User code examples: + + $AoGSmartHomeItems = new AoGSmartHome_Items(); + $AoGSmartHomeItems->add('$light1','light1','set','on','off','state'); # This is the same as $AoGSmartHomeItems->add('$light1') + +To change the name of an object to a more natural name that you would say to the Echo/GH: + + $AoGSmartHomeItems->add('$GarageHall_light_front','Garage_Hall_light'); + +To map a voice command, '!' is replaced by the Echo/GH command (on/off/dim%). +My actual voice command in MH is "set night mode on", so I configure it like: + + $AoGSmartHomeItems->add('set night mode !','NightMode','run_voice_cmd'); + + If I say "Alexa, Turn on Night Mode", run_voice_cmd("set night mode on") is run in MH. + +To configure a user code sub: +The actual name (argument 1) can be anything. +A code ref must be used. +When the sub is run 2 arguments are passed to it: Argument 1 is (state or set) Argument 2 is: (on/off/). + +# Mht file + + AOGSMARTHOME_ITEM, AoGSmartHomeItems, testsub, Test_Sub, &testsub + +# User Code + + $AoGSmartHomeItems->add('testsub','Test_Sub',\&testsub); # say "Alexa, Turn on Test Sub", &testsub('set','on') is run in MH. + + +# I have an Insteon thermostat, the Insteon object name is $thermostat and I configured it like: + + AOGSMARTHOME_ITEM, AoGSmartHomeItems, thermostat, Heat, heat_setpoint, on, off, get_heat_sp + +# say "Alexa, Set Heat to 73", $thermostat->heat_setpoint("73") is run in MH. + + AOGSMARTHOME_ITEM, AoGSmartHomeItems, thermostat, Cool, cool_setpoint, on, off, get_cool_sp + +In order to be able to say things like "Alexa, set thermostat up by 2", a sub must be created in user code +When the above is said to the Echo, it first gets the current state, then subtracts or adds the amount that was said. + + sub temperature { + my ($type, $state) = @_; + + # $type is state or set + # $state is the number, on, off, etc + + # we are changing heat and cool so just return a static number, we just need the diff + # because the Echo will add or subtact the amount that was said to it. + # so if we say "set thermostat up by 2", 52 will be returned in $state + if ($type eq 'state') { return 50; } + + return '' unless ($state =~ /\d+/); Make sure we have a number + return '' if ($state > 65); # Dont allow changes over 15 + return '' if ($state < 35); # Dont allow changes over 15 + my ( $heatsp, $coolsp ); + $state = ($state - 50); # subtract the amount we return above to get the actual amount to change. + $coolsp = ((state $thermo_setpoint_c) + $state); + $heatsp = ((state $thermo_setpoint_h) + $state); + # The Insteon thermostat has an issue when setting both heat and cool at the same time, so the timer is a work around. + $alexa_temp_timer = new Timer; + $thermostat->cool_setpoint($coolsp); + set $alexa_temp_timer '7', sub { $thermostat->heat_setpoint($heatsp) } + } + +# Map our new temperature sub in the .mht file so the Echo/Google Home can discover it + + AOGSMARTHOME_ITEM, AoGSmartHomeItems, thermostat, thermostat, &temperature + +I have a script that I use to control my AV equipment and I can run it via +ssh, so I made a voice command in MH: + + $v_set_tv_mode = new Voice_Cmd("set tv mode [on,off,hbo,netflix,roku,directtv,xbmc,wii]"); + $p_set_tv_mode = new Process_Item; + if (my $state = said $v_set_tv_mode) { + set $p_set_tv_mode "/usr/bin/ssh wayne\@192.168.1.10 \"sudo /usr/local/HomeAVControl/bin/input_change $state\""; + start $p_set_tv_mode; + } + +I added the following to my .mht file: + + AOGSMARTHOME_ITEM, AoGSmartHomeItems, set_tv_mode_!, DirectTv, run_voice_cmd, directtv, directtv + AOGSMARTHOME_ITEM, AoGSmartHomeItems, set_tv_mode_!, Roku, run_voice_cmd, roku, roku + AOGSMARTHOME_ITEM, AoGSmartHomeItems, set_tv_mode_!, xbmc, run_voice_cmd, xbmc, xbmc + AOGSMARTHOME_ITEM, AoGSmartHomeItems, set_tv_mode_!, wii, run_voice_cmd, wii, wii + AOGSMARTHOME_ITEM, AoGSmartHomeItems, set_tv_mode_!, Hbo, run_voice_cmd, hbo, hbo + AOGSMARTHOME_ITEM, AoGSmartHomeItems, set_tv_mode_!, Netflix, run_voice_cmd, netflix, netflix + +=head2 INHERITS + +L + +Storable + +=head2 METHODS + +=over + +=cut + +package AoGSmartHome_Items; + +@AoGSmartHome_Items::ISA = ('Generic_Item'); + +use Data::Dumper; +use Storable; +use Time::HiRes qw(gettimeofday tv_interval); + +sub get_set_state { + my ( $self, $uuid, $action, $state ) = @_; + + my $name = $self->{'uuids'}->{$uuid}->{'name'}; + my $realname = $self->{'uuids'}->{$uuid}->{'realname'}; + my $sub = $self->{'uuids'}->{$uuid}->{'sub'}; + my $statesub = $self->{'uuids'}->{$uuid}->{'statesub'}; + + # ??? + $state = $self->{'uuids'}->{$uuid}->{ lc($state) } if $self->{'uuids'}->{$uuid}->{ lc($state) }; + + print STDERR "[AoGSmartHome] Debug: get_set_state(uuid='$uuid', action='$action', state='$state', name='$name' realname='$realname' sub='$sub')\n" + if $main::Debug{'aog'} > 2; + + if ( $sub =~ /^voice[_-]cmd:\s*(.+)\s*$/ ) { + my $voice_cmd = $1; + + if ( $action eq 'set' ) { + $voice_cmd =~ s/[#!]/$state/; + + print STDERR "[AoGSmartHome] Debug: running voice command \'$voice_cmd\'\n" + if $main::Debug{'aog'}; + &main::run_voice_cmd("$voice_cmd"); + + return; + } + elsif ( $action eq 'get' ) { + + # FIXME -- "get" on voice command? Hhhmmm + return qq["on":true,"bri":254]; + } + } + elsif ( ref $sub eq 'CODE' ) { + if ( $action eq 'set' ) { + print "[AoGSmartHome] Debug: running sub $sub(set, $state)\n" if $main::Debug{'aog'}; + &{$sub}( 'set', $state ); + return; + } + elsif ( $action eq 'get' ) { + my $debug = "[AoGSmartHome] Debug: get_state running sub: $sub( state, $state ) - "; + my $state = &{$sub}('state'); + if ( $state =~ /\d+/ ) { + $state = ( &roundoff( ( $state * 2.54 ) ) ); + my $return = qq["on":true,"bri":$state]; + print STDERR "$debug returning - $return\n" if $main::Debug{'aog'}; + return $return; + } + + return qq["on":true,"bri":254]; + } + } + else { + # + # Treat as a MisterHouse object, using $sub as the 'set' function. + # + + my $mh_object = ::get_object_by_name($realname); + return undef if !defined $mh_object; + + if ( $action eq 'get' ) { + my $cstate = $mh_object->$statesub(); + $cstate =~ s/\%//; + my $type = $mh_object->get_type(); + my $debug = "[AoGSmartHome] Debug: get state -- actual object state: $cstate, object type: $type, "; + + if ( $type =~ /X10/i ) { + $cstate = 'on' if $cstate =~ /\d+/ || $cstate =~ /dim/ || $cstate =~ /bright/; + $debug .= "determined state: $cstate, "; + } + + $debug .= "$debug returning $cstate\n"; + + print STDERR $debug if $main::Debug{'aog'} > 2; + + return $cstate; + } + elsif ( $action eq 'set' ) { + if ( ($mh_object->isa('Insteon::DimmableLight') + || $mh_object->can('state_level') ) && $state =~ /\d+/ ) { + $state = $state . '%'; + } + + print STDERR "[AoGSmartHome] Debug: setting object ($realname) to state ($state)\n" + if $main::Debug{'aog'}; + + $mh_object->$sub( $state, 'AoGSmartHome' ); + + return; + } + } +} + +sub roundoff { + my $num = shift; + my $roundto = shift || 1; + + return int( $num / $roundto + 0.5 ) * $roundto; +} + +sub new { + my ($class) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + + my $file = $::config_parms{'data_dir'} . '/aogsmarthome_temp.saved_id'; + if ( -e $file ) { + my $restoredhash = retrieve($file); + $self->{idmap} = $restoredhash->{idmap}; + + if ( $main::Debug{'aog'} ) { + print STDERR "[AoGSmartHome] Debug: dumping persistent IDMAP:\n"; + print STDERR Dumper $self->{idmap}; + print STDERR "[AoGSmartHome] Debug: done.\n"; + } + } + + return $self; +} + +=item C + +Presents MisterHouse objects, subs, or voice coommands to the Actions on +Google Smart Home API. + +add('', '', +'', +'', +'', +'' +'); + +=cut + +sub add { + my ( $self, $realname, $name, $sub, $on, $off, $statesub, $dev_properties ) = @_; + my ($type, $room); # AoG Smart Home Provider device properties + + if ( !$name ) { + $name = $realname; + $name =~ s/\$//g; + $name =~ s/_/ /g; # Otherwise the Google Assistant witll say + # "kitchen-underscore-light" instead of + # "kitchen light". + $name =~ s/#//g; + $name =~ s/\\//g; + $name =~ s/&//g; + } + + if ($dev_properties) { + foreach (split /\s*:\s*/, $dev_properties) { + my ($parm, $value) = split /\s*=\s*/; + + if ($parm eq 'type') { + $type = $value; + } elsif ($parm eq 'room') { + $room = $value; + } else { + &main::print_log("[AoGSmartHome] Invalid device property '$parm'; ignoring."); + } + } + } + + my $uuid = $self->uuid($realname); + + $self->{'uuids'}->{$uuid}->{'realname'} = $realname; + $self->{'uuids'}->{$uuid}->{'name'} = $name; + $self->{'uuids'}->{$uuid}->{'sub'} = $sub || 'set'; + $self->{'uuids'}->{$uuid}->{'on'} = lc($on) || 'on'; + $self->{'uuids'}->{$uuid}->{'off'} = lc($off) || 'off'; + $self->{'uuids'}->{$uuid}->{'statesub'} = $statesub || 'state'; + $self->{'uuids'}->{$uuid}->{'type'} = lc($type) || 'light'; + $self->{'uuids'}->{$uuid}->{'room'} = $room if $room; +} + +=item C + +Generates an action.devices.SYNC fulfillment response. + +=cut + +sub sync { + my ( $self, $body ) = @_; + + my $response = <{'requestId'}", + "payload": { +EOF + + if (defined $::config_parms{'aog_agentuserid'}) { + $response .= <{'uuids'} } ) { + my $type = $self->{'uuids'}->{$uuid}->{'type'}; + + if ( $type eq 'light' ) { + $response .= <{'uuids'}->{$uuid}->{'realname'}); + if ($mh_object->isa('Insteon::DimmableLight') + || $mh_object->can('state_level') ) { + $response .= <{'uuids'}->{$uuid}->{'name'}" + }, + "willReportState": false, +EOF + + if (exists $self->{'uuids'}->{$uuid}->{'room'}) { + $response .= <{'uuids'}->{$uuid}->{'room'}", +EOF + } + + $response =~ s/,$//; # Remove extra ',' + + $response .= <{'uuids'}->{$uuid}->{'name'}" + }, + "willReportState": false, +EOF + + if (exists $self->{'uuids'}->{$uuid}->{'room'}) { + $response .= <{'uuids'}->{$uuid}->{'room'}", +EOF + } + + $response =~ s/,$//; # Remove extra ',' + + $response .= <{'uuids'}->{$uuid}->{'name'}" + }, + "willReportState": false, + "attributes": { + "sceneReversible": false + } + }, +EOF + } + } + + $response =~ s/,$//; # Remove extra ',' + + $response .= < + +Generates an action.devices.QUERY fulfillment response. + +=cut + +sub query { + my ( $self, $body ) = @_; + + my $response = <{'requestId'}", + "payload": { + "devices": { +EOF + + foreach my $device ( @{ $body->{'inputs'}->[0]->{'payload'}->{'devices'} } ) { + if ( !exists $self->{'uuids'}->{ $device->{'id'} } ) { + $response .= <{'id'}": { + "errorCode": "deviceNotFound" + }, +EOF + next; + } + + if ( $self->{'uuids'}->{ $device->{'id'} }->{'type'} eq 'scene' ) { + $response .= <{'id'}": { + "online": true + }, +EOF + next; + } + + # + # The device is a light. + # + + my $devstate = get_set_state( $self, $device->{'id'}, 'get' ); + if ( !defined $devstate ) { + $response .= <{'id'}": { + "errorCode": "deviceNotFound" + }, +EOF + next; + } + + $response .= <{'id'}": { +EOF + + my $mh_object = ::get_object_by_name($self->{'uuids'}->{$device->{'id'} }->{'realname'}); + if ($mh_object->isa('Insteon::DimmableLight') + || $mh_object->can('state_level') ) { + + # INSTEON devices return "on" or "off". The AoG "Brightness" trait + # expects needs "100" or "0", so we adjust here accordingly. + if ($devstate eq 'on') { + $devstate = 100; + } elsif ($devstate eq 'off') { + $devstate = 0; + } + + $response .= <{'execution'}->[0]->{'params'}->{'on'} eq "true" ? 1 : 0; + + foreach my $device ( @{ $command->{'devices'} } ) { + $desired_states{ $device->{'id'} } = $turn_on ? 'on' : 'off'; + + get_set_state( $self, $device->{'id'}, 'set', $turn_on ? 'on' : 'off' ); + } + + my @successes_on; + my @successes_off; + + # + # Second, check the state of each device. + # + # for (my $t0 = [gettimeofday()]; tv_interval($t0) < 5.0;) { + # foreach my $devid (keys %desired_states) { + # my $devstate = get_set_state($self, $devid, 'get'); + # if ($devstate eq $desired_states{$devid}) { + # if ($desired_states{$devid} eq "on") { + # push @successes_on, $devid; + # } else { + # push @successes_off, $devid; + # } + # + # delete $desired_states{$devid}; + # } + # } + # } + + # + # Now generate the response... + # + + if (@successes_on) { + $response .= qq( {\n "ids": [); + + foreach my $devid (@successes_on) { + $response .= qq["$devid",]; + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= "],\n"; + + $response .= <{'execution'}->[0]->{'params'}->{'brightness'}; + + foreach my $device ( @{ $command->{'devices'} } ) { + $desired_states{ $device->{'id'} } = $brightness; + + get_set_state( $self, $device->{'id'}, 'set', $brightness); + } + + if ( keys %desired_states ) { + $response .= qq( {\n "ids": [); + + foreach my $devid ( keys %desired_states ) { + $response .= qq["$devid",]; + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= "],\n"; + + $response .= <{'devices'} } ) { + + # Just a marker to generate output later + $scene_ids{ $device->{'id'} } = undef; + + get_set_state( $self, $device->{'id'}, 'set' ); + } + + # + # Now generate the response... + # + + $response .= qq( {\n); + $response .= qq( "ids": [); + + foreach my $devid ( keys %scene_ids ) { + $response .= qq["$devid",]; + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= "],\n"; + + $response .= < + +Generates an action.devices.EXECUTE fulfillment response. + +=cut + +# +# Implement the action.devices.EXECUTE fulfillment hook. +# +# Our strategy consists of sending all the commands at once, and then +# coming back to check if the command was successful. +# +sub execute { + my ( $self, $body ) = @_; + my %desired_states; + + my $response = <{'requestId'}", + "payload": { + "commands": [ +EOF + + # + # First, send the commands to all the devices specified in the request. + # + foreach my $command ( @{ $body->{'inputs'}->[0]->{'payload'}->{'commands'} } ) { + my $execution_command = $command->{'execution'}->[0]->{'command'}; + + if ( $execution_command eq "action.devices.commands.OnOff" ) { + $response .= execute_OnOff( $self, $command ); + } + elsif ( $execution_command eq "action.devices.commands.BrightnessAbsolute" ) { + $response .= execute_BrightnessAbsolute( $self, $command ); + } + elsif ( $execution_command eq "action.devices.commands.ActivateScene" ) { + $response .= execute_ActivateScene( $self, $command ); + } + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= <{'idmap'}->{objects}->{$name} + if exists $self->{'idmap'}->{objects}->{$name}; + + my $highid; + my $missing; + my $count = $::config_parms{'aog_uuid_start'} || 1; + + foreach my $object ( keys %{ $self->{idmap}->{objects} } ) { + my $currentid = $self->{idmap}->{objects}->{$object}; + $highid = $currentid if ( $currentid > $highid ); + $missing = $count unless ( $self->{'idmap'}->{ids}->{$count} ); # We have a number that has no value + $count++; + } + $highid++; + + $highid = $missing if defined $missing; # Reuse numbers for deleted objects to keep the count from growning for ever. + + $self->{'idmap'}->{objects}->{$name} = $highid; + $self->{'idmap'}->{ids}->{$highid} = $name; + + my $idmap->{'idmap'} = $self->{'idmap'}; + + my $file = $::config_parms{'data_dir'} . '/aogsmarthome_temp.saved_id'; + store $idmap, $file; + + return $highid; + + # use Data::UUID; + # $ug = Data::UUID->new; + # $uuid = $ug->to_string( ( $ug->create_from_name(NameSpace_DNS, $name) ) ); + # $uuid =~ s/\D//g; + # $uuid =~ s/-//g; + # $uuid = (substr $uuid, 0, 9); + # return lc($uuid); +} + +1; + +=back + +=head2 NOTES + +=head2 AUTHOR + +Eloy Paris based heavily on AlexaBridge.pm by Wayne Gatlin + +=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 + diff --git a/lib/http_server.pl b/lib/http_server.pl index fcfe54b6a..806738f4c 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -10,8 +10,7 @@ use AlexaBridge; require 'http_utils.pl'; - -#use Data::Dumper; +use Data::Dumper; #$main::Debug{http} = 4; #no warnings 'uninitialized'; # These seem to always show up. Dang, will not work with 5.0 @@ -328,7 +327,7 @@ sub http_process_request { $Authorized = &password_check( $Cookies{password}, 'http', 'crypted' ); } - my ( $req_typ, $get_req, $get_arg ) = $header =~ m|^(GET\|POST\|PUT) (\/[^ \?]*)\??(\S+)? HTTP|; + my ( $req_typ, $get_req, $get_arg ) = $header =~ m%^(GET|POST|PUT) (/[^ \?]*)\??(\S+)? HTTP%; $get_arg = '' unless defined $get_arg; $HTTP_REQ_TYPE = $req_typ; @@ -362,13 +361,20 @@ sub http_process_request { print "http POST in loop\n" if $main::Debug{http}; $get_arg .= "&" if ( $get_arg ne '' ); $get_arg .= $buf; - - } elsif ( ( lc($Http{'Content-Type'}) eq lc('application/json') ) && ( $HTTP_BODY =~ /^\{/ ) ) { - print "[http_server.pl]: posting json data\n" if $main::Debug{http}; - + + # Sample "Content-Type" header that has been seen: + # + # Content-Type: application/json;charset=UTF-8 + # + # For this reason we use a regular expresion here instead of + # checking for "application/json" using the "eq" operator. + # + } elsif ($Http{'Content-Type'} =~ m%^application/json%i && $HTTP_BODY =~ /^\{/) { + print "[http_server.pl]: posting json data\n" if $main::Debug{http}; } else { &main::print_log("[http_server.pl]: Warning, invalid argument string detected ($buf)\n"); } + print "http POST get_arg=$get_arg\n" if $main::Debug{http}; # shutdown($socket->fileno(), 0); # "how": 0=no more receives, 1=sends, 2=both @@ -450,12 +456,11 @@ sub http_process_request { $ENV{HTTP_QUERY_STRING} = $get_arg; # Prompt for password (SET_PASSWORD) and allow for UNSET_PASSWORD - if ( $get_req =~ /SET_PASSWORD$/ ) { - my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); - + if ( $get_req =~ m%^/(UN)?SET_PASSWORD$% ) { if ( $config_parms{password_menu} eq 'html' ) { + my ($mode) = $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//; my $html = &html_authorized; - if ( $get_req =~ /^\/UNSET_PASSWORD$/ ) { + if ( $get_req =~ m%^/UNSET_PASSWORD$% ) { $Authorized = 0; $Cookie .= "Set-Cookie: password=xyz ; ; path=/;\n"; $html .= ""; @@ -473,7 +478,7 @@ sub http_process_request { # $html .= &html_reload_link('/', 'Refresh Main Page'); # Does not force reload? my ( $name, $name_short ) = &net_domain_name('http'); - if ( $Authorized and $get_req =~ /\/SET_PASSWORD$/ ) { + if ( $Authorized and $get_req =~ m%^/SET_PASSWORD$% ) { &print_log("Password was just accepted for User [$Authorized] browser $name"); # Speak calls cause problems with speak hooks, like in the audrey code @@ -494,31 +499,22 @@ sub http_process_request { } # Process the html password form - elsif ( $get_req =~ /\/SET_PASSWORD_FORM$/ ) { - my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); + elsif ( $get_req =~ m%^/SET_PASSWORD_FORM$% ) { my ($password) = $get_arg =~ /password=(\S+)/; - my ($html); my ( $name, $name_short ) = &net_domain_name('http'); my ( $user, $password_crypted ) = &password_check2($password); $Authorized = $user if $password_crypted; - $html .= &html_authorized; - #$html .= "REMOVEME = get_arg = " . $get_arg . "
\n"; - $html .= "
Refresh: Main Page\n"; - # $html .= &html_reload_link('/', 'Refresh Main Page'); + my $html = &html_authorized; + $html .= "
\n"; + $html .= "Refresh: Main Page\n"; $html .= &html_password(''); + if ($password_crypted) { - $Cookie .= "Set-Cookie: password=$password_crypted; ; path=/\n" - if $password_crypted; + $Cookie .= "Set-Cookie: password=$password_crypted; ; path=/\n"; - # Refresh the main page $html .= "$user password accepted"; - - # $html = $Http{Referer}; # &html_page will use referer if only a url is given - $html =~ s/\/SET_PASSWORD.*//; &print_log("Password was just accepted for User [$user] browser $name"); - - # &speak("app=admin $user password accepted for $name_short"); } else { $Authorized = 0; @@ -527,8 +523,6 @@ sub http_process_request { $Cookies{password_was_not_valid}++; # So we can monitor from user code &print_log("Password was just NOT set; $name"); &play( file => 'unauthorized' ); # Defined in event_sounds.pl - - # &speak("app=admin Password NOT set by $name_short"); } print $socket &html_page( undef, $html ); @@ -556,6 +550,19 @@ sub http_process_request { return; } + # Handle Actions on Google Smart Home Provider fulfillment requests + if ($::config_parms{'aog_enable'} ) { + my $aog_response = process_http_aog($get_req, $req_typ, $HTTP_BODY, $socket, %Http); + if ($aog_response) { + # Request was handled by the AoG HTTP helper; send response back + # to the client and return. + print $socket $aog_response if $aog_response; + &http_delete_headers($client_number,$requestnum); + + return; + } + }; + # See if the request was for a file if ( &test_for_file( $socket, $get_req, $get_arg, undef, undef, $client_number, $requestnum ) ) { } @@ -824,35 +831,31 @@ sub http_process_request { return ( $leave_socket_open_passes, &http_close_socket ); } +# +# Generate the HTML markup for the login form. +# sub html_password { my ($menu) = @_; $menu = $config_parms{password_menu} unless $menu; - my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); - - # return $html_unauthorized unless $Authorized; my $html; if ( $menu eq 'html' ) { - $html = qq[\n] - unless ( lc $mode eq "ia7" ); - - # $html .= qq[\n]; - $html .= qq[
\n]; + $html = < + Password: + + - # $html .= qq[
\n]; ... get not secure from browser history list!! - # $html .= qq[

Password:

\n
\n]; - $html .= qq[Password:\n]; - $html .= qq[\n\n]; - $html .= - qq[

This form is used for logging into MisterHouse.
For administration please see the documentation of set_password

\n]; - - # } +

This form is used for logging into MisterHouse.
+For administration please see the documentation of +set_password

+EOF } else { - $html = qq[HTTP/1.0 401 Unauthorized\n]; - $html .= qq[Server: MisterHouse\n]; - $html .= qq[Content-type: text/html\n]; - $html .= qq[WWW-Authenticate: Basic realm="mh_control"\n]; + $html = qq[HTTP/1.0 401 Unauthorized\n]; + $html .= qq[Server: MisterHouse\n]; + $html .= qq[Content-type: text/html\n]; + $html .= qq[WWW-Authenticate: Basic realm="mh_control"\n]; } return $html; } @@ -860,16 +863,16 @@ sub html_password { sub html_authorized { my $html = "Status: "; if ($Authorized) { - $html .= ""; + $html .= ""; $html .= "Logged In as $Authorized"; $html .= ""; - $html .= "
"; + $html .= "
\n"; } else { - $html .= ""; + $html .= ""; $html .= "Not Logged In"; $html .= ""; - $html .= "
"; + $html .= "
\n"; } return $html; } @@ -1950,8 +1953,8 @@ sub html_file { my $whoisit = &net_domain_name('http'); &print_log("$whoisit made an unauthorized request for $file"); - # return &html_page("", &html_unauthorized("Not authorized to run perl .pl file: $file")); - return &html_unauthorized("Not authorized to run perl .pl file: $file"); + return &html_page("", &html_unauthorized("Not authorized to run perl .pl file: $file")); + #return &html_unauthorized("Not authorized to run perl .pl file: $file"); } @ARGV = ''; # Have to clear previous args @@ -2266,34 +2269,32 @@ sub html_page { unless $script =~ / script /i; $html = $script . "\n"; } -$html .= " + $html .= < $style $title - $body -"; +EOF + $html =~ s/\n/\n\r/g; # Bill S. says this is required to be standards compiliant - $html =~ s/\n/\n\r/g; # Bill S. says this is required to be standards compiliant - - $html_head = "HTTP/1.1 200 OK\r\n"; - $html_head .= "Server: MisterHouse\r\n"; - $html_head .= "Connection: close\r\n" if &http_close_socket(%HttpHeader); - $html_head .= "Content-type: text/html\r\n"; - $html_head .= "Content-Length: " . ( length $html ) . "\r\n"; - $html_head .= "Date: " . time2str(time) . "\r\n"; - $html_head .= "Cache-Control: no-cache\r\n"; - $html_head .= $Cookie . "\r\n" if $Cookie; - $html_head .= $frame . "\r\n" if $frame; - $html_head .= "\r\n"; - - return $html_head.$html; + $html_head = "HTTP/1.1 200 OK\r\n"; + $html_head .= "Server: MisterHouse\r\n"; + $html_head .= "Connection: close\r\n" if &http_close_socket(%HttpHeader); + $html_head .= "Content-type: text/html\r\n"; + $html_head .= "Content-Length: " . ( length $html ) . "\r\n"; + $html_head .= "Date: " . time2str(time) . "\r\n"; + $html_head .= "Cache-Control: no-cache\r\n"; + $html_head .= $Cookie . "\r\n" if $Cookie; + $html_head .= $frame . "\r\n" if $frame; + $html_head .= "\r\n"; + return $html_head . $html; } sub http_redirect { diff --git a/lib/http_server_aog.pl b/lib/http_server_aog.pl new file mode 100644 index 000000000..b05dac854 --- /dev/null +++ b/lib/http_server_aog.pl @@ -0,0 +1,313 @@ + +=head1 B + +=head2 SYNOPSIS + +HTTP support for the Actions on Google Smart Home provider. Called via the +web server. Examples: + + http://localhost:8080/oauth + +=head2 DESCRIPTION + +Generate json for mh objects, groups, categories, and variables + +TODO + + add request types for speak, print, and error logs + add the truncate option to packages, vars, and other requests + add more info to subs request + +=head2 INHERITS + +B + +=head2 METHODS + +=over + +=item B + +=cut + +use Config; +use MIME::Base64; +use JSON qw(decode_json); +use Storable; +use constant RANDBITS => $Config{randbits}; +use constant RAND_MAX => 2**RANDBITS; + +# Cache of OAuth authentication tokens. Persistent tokens are stored +# in $::config_parms{'aog_oauth_tokens_file'} and read on startup. +my $oauth_tokens; + +sub http_server_aog_startup { + &main::print_log("Actions on Google Smart Home Provider HTTP server helper startup:") + if $main::Debug{'aog'}; + + $::config_parms{'aog_auth_path'} = 'oauth' + if !defined $::config_parms{'aog_auth_path'}; + &main::print_log("aog_auth_path = $::config_parms{'aog_auth_path'}") + if $main::Debug{'aog'}; + + my $oauth_tokens_file = $config_parms{'aog_oauth_tokens_file'}; + + $::config_parms{'aog_oauth_tokens_file'} = "$config_parms{data_dir}/.aog_tokens" + if !defined $::config_parms{'aog_oauth_tokens_file'}; + &main::print_log("aog_oauth_tokens_file = $::config_parms{'aog_oauth_tokens_file'}") + if $main::Debug{'aog'}; + + if ( -e $::config_parms{'aog_oauth_tokens_file'} ) { + $oauth_tokens = retrieve( $::config_parms{'aog_oauth_tokens_file'} ); + } + if ( $main::Debug{'aog'} ) { + print "[AoGSmartHome] Debug: Dumping \$oauth_tokens...\n"; + print Dumper $oauth_tokens; + print "Done.\n"; + } +} + +# +# Receives an HTTP error response string and generates the HTTP +# header and HTML page to return to the HTTP client. +# +# Sample HTTP error responses: +# +# "400 Bad Request", "408 Request Timeout", "500 Internal Server Error". +# +# https://en.wikipedia.org/wiki/List_of_HTTP_status_codes +# +# Other parts of the AoG HTTP server helper call this when some error +# condition is detected, like a missing HTTP argument. +# +sub http_error($) { + my ($http_response) = @_; + + $style = $main::config_parms{ 'html_style' . $Http{format} } + if $main::config_parms{ 'html_style' . $Http{format} } + and !defined $style; + + my $html_body = < + +$style +$http_response + + +

Bad Request

+ +

Your browser made a request that this server does not understand.

+ + +EOF + + my $html_head = "HTTP/1.1 $http_response\r\n"; + $html_head .= "Server: MisterHouse\r\n"; + $html_head .= "Content-Length: " . length($html_body) . "\r\n"; + $html_head .= "Date: @{[time2str(time)]}\r\n"; + $html_head .= "\r\n"; + + return $html_head . $html_body; +} + +sub process_http_aog { + my ( $uri, $request_type, $body, $socket, %Http ) = @_; + my $html; + + if ( $::config_parms{'aog_enable'} + && !scalar list_objects_by_type('AoGSmartHome_Items') ) + { + print "[AoGSmartHome] AoG is enabled but there are no AoG items. Disabling AoG!\n"; + $::config_parms{'aog_enable'} = 0; + return 0; + } + + if ( $uri eq $::config_parms{'aog_auth_path'} ) { + print "[AoGSmartHome] Debug: Processing OAuth request.\n" if $main::Debug{'aog'}; + + if ( $request_type eq 'POST' ) { + print "[AoGSmartHome] Debug: Processing HTTP POST.\n" if $main::Debug{'aog'}; + + if ( !exists $HTTP_ARGV{'password'} ) { + &main::print_log("[AoGSmartHome] missing 'password' argument in HTTP POST"); + + return http_error("400 Bad Request"); + } + + $Authorized = password_check( $HTTP_ARGV{'password'}, 'http' ); + if ( !$Authorized ) { + $html = "

Login failed.

\n"; + } + } + + if ( !exists $HTTP_ARGV{'client_id'} ) { + &main::print_log("[AoGSmartHome] client_id parameter missing from OAuth request."); + return http_error("400 Bad Request"); + } + + if ( $HTTP_ARGV{'client_id'} ne $::config_parms{'aog_client_id'} ) { + &main::print_log( + "[AoGSmartHome] Received client_id \'$HTTP_ARGV{'client_id'}\' does not match our client_id \'$::config_parms{'aog_client_id'}\'."); + return http_error("400 Bad Request"); + } + + if ( !exists $HTTP_ARGV{'state'} ) { + &main::print_log("[AoGSmartHome] state parameter missing from OAuth request."); + return http_error("400 Bad Request"); + } + + if ( !exists $HTTP_ARGV{'redirect_uri'} ) { + &main::print_log("[AoGSmartHome] redirect_uri parameter missing from OAuth request."); + return http_error("400 Bad Request"); + } + + # Verify "redirect_uri" value + if ( $HTTP_ARGV{'redirect_uri'} !~ m%https://oauth-redirect.googleusercontent.com/r/$::config_parms{'project_id'}% ) { + &main::print_log("[AoGSmartHome] invalid redirect_uri (should be \"https://oauth-redirect.googleusercontent.com/r/$::config_parms{'project_id'}\""); + return http_error("400 Bad Request"); + } + + if ( !exists $HTTP_ARGV{'response_type'} ) { + &main::print_log("[AoGSmartHome] response_type parameter missing from OAuth request."); + return http_error("400 Bad Request"); + } + + if ( $HTTP_ARGV{'response_type'} ne 'token' ) { + &main::print_log( + "[AoGSmartHome] Invalid response_type \'$HTTP_ARGV{'response_type'}\' in OAuth request; must be 'token' for OAuth 2.0 implicit flow."); + return http_error("400 Bad Request"); + } + + if ( !$Authorized ) { + # + # User is not authenticated (authorized). Present a login form. + # + + $html .= < + Password: + + + + + + + + +

This form is used for logging into MisterHouse.

+EOF + + return html_page( 'MisterHouse Actions on Google Login', $html ); + } + + # + # User is authenticated. + # + + my $token; + + foreach my $t ( keys %{$oauth_tokens} ) { + if ( $oauth_tokens->{$t} eq $Authorized ) { + print "[AoGSmartHome] Debug: found token '$t' for user '$Authorized'\n" + if $main::Debug{'aog'}; + $token = $t; + last; + } + } + + if ( !$token ) { + + # We didn't find an existing token for the authenticated user; + # generate a new token (making sure token is unique). + while (1) { + $token = encode_base64( int rand(RAND_MAX), '' ); + + if ( !exists $oauth_tokens->{$token} ) { + $oauth_tokens->{$token} = $Authorized; + last; + } + } + + print "[AoGSmartHome] Debug: token for user '$Authorized' did not exist; generated token '$token'\n" + if $main::Debug{'aog'}; + + store $oauth_tokens, $::config_parms{'aog_oauth_tokens_file'}; + } + + return http_redirect("$HTTP_ARGV{'redirect_uri'}#access_token=$token&token_type=bearer&state=$HTTP_ARGV{'state'}"); + } + elsif ( $uri eq $::config_parms{'aog_fulfillment_url'} ) { + print "[AoGSmartHome] Debug: Processing fulfillment request.\n" if $main::Debug{'aog'}; + + if ( !$Http{Authorization} || $Http{Authorization} !~ /Bearer (\S+)/ ) { + return http_error("401 Unauthorized"); + } + + my $received_token = $1; + + if ( exists $oauth_tokens->{$received_token} ) { + print "[AoGSmartHome] Debug: fulfillment request has correct token '$received_token' for user '$oauth_tokens->{$received_token}'\n" + if $main::Debug{'aog'}; + } + else { + &main::print_log("[AoGSmartHome] Incorrect token '$received_token' in fulfillment request!"); + + print "[AoGSmartHome] Debug: Incorrect token '$received_token' in fulfillment request!\n" + if $main::Debug{'aog'}; + + return http_error("401 Unauthorized"); + } + + # + # See here for reference on what Google will send to us: + # + # https://developers.google.com/actions/smarthome/create-app#build_fulfillment + # + + my $aog_items_objname = ( &list_objects_by_type('AoGSmartHome_Items') )[0]; + my $aog_items = get_object_by_name($aog_items_objname); + + my $body = decode_json($body); + + if ( $body->{'inputs'}->[0]->{'intent'} eq 'action.devices.SYNC' ) { + return $aog_items->sync($body); + } + elsif ( $body->{'inputs'}->[0]->{'intent'} eq 'action.devices.QUERY' ) { + return $aog_items->query($body); + } + elsif ( $body->{'inputs'}->[0]->{'intent'} eq 'action.devices.EXECUTE' ) { + return $aog_items->execute($body); + } + else { + # Bad boy + return http_error("400 Bad Request"); + } + } +} + +1; # Make "require" happy + +=back + +=head2 INI PARAMETERS + +NONE + +=head2 AUTHOR + +Eloy Paris + +=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/read_table_A.pl b/lib/read_table_A.pl index 059b251bb..b2e54046b 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1815,6 +1815,28 @@ sub read_table_A { } #-------------- End Alexa Objects ---------------- + #-------------- AoGSmartHome Objects ----------------- + elsif ( $type eq "AOGSMARTHOME_ITEMS" ) { + require 'AoGSmartHome_Items.pm'; + ($name) = @item_info; + $object = "AoGSmartHome_Items()"; + } + elsif ( $type eq "AOGSMARTHOME_ITEM" ) { + my ($parent, $realname, $name, $sub, $on, $off, $statesub, @other) = @item_info; + $sub =~ s/&/\\&/ if $sub =~ /^&/; + $sub =~ s/\\// if $sub =~ /^\\\\&/; + $realname =~ s/_/ /g if $sub =~ /run_voice_cmd/; + $realname = "\$$realname" if $realname && $sub !~ /&|run_voice_cmd/; + $sub = qq|'$sub'| if $sub !~ /&/; + my $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if (!$packages{AoGSmartHome_Items}++ ) { # first time for this object type? + $code .= "use AoGSmartHome_Items;\n"; + } + $code .= sprintf "\$%-35s -> add('$realname','$name',$sub,'$on','$off','$statesub',$other);\n", $parent; + $object = ''; + } + #-------------- End AoGSmartHome Objects ---------------- + elsif ( $type =~ /PLCBUS_.*/ ) { #<,PLCBUS_Scene,Address,Name,Groups,Default|Scenes># require PLCBUS; From a9f634bd7ea2c5ea367995849d6abaa11b6a4890 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Sat, 4 Nov 2017 02:23:36 -0400 Subject: [PATCH 10/78] Do not try to get device status before responding Do not try to get the status of a device before sending the response to the EXECUTE command -- it does not work because we (apparently) have to run the main loop to get device status update, so this does not seem possible to do from the HTTP server. Glad to be proven wrong, but for now just removing this code that does not work. --- lib/AoGSmartHome_Items.pm | 141 ++++++-------------------------------- 1 file changed, 20 insertions(+), 121 deletions(-) diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm index 10e47aa59..4943e91df 100644 --- a/lib/AoGSmartHome_Items.pm +++ b/lib/AoGSmartHome_Items.pm @@ -625,141 +625,52 @@ EOF sub execute_OnOff { my ( $self, $command ) = @_; - my %desired_states; - my $response = ''; + + my $response = ' { + "ids": ['; my $turn_on = $command->{'execution'}->[0]->{'params'}->{'on'} eq "true" ? 1 : 0; foreach my $device ( @{ $command->{'devices'} } ) { - $desired_states{ $device->{'id'} } = $turn_on ? 'on' : 'off'; - get_set_state( $self, $device->{'id'}, 'set', $turn_on ? 'on' : 'off' ); + $response .= qq["$device->{'id'}",]; } - my @successes_on; - my @successes_off; - - # - # Second, check the state of each device. - # - # for (my $t0 = [gettimeofday()]; tv_interval($t0) < 5.0;) { - # foreach my $devid (keys %desired_states) { - # my $devstate = get_set_state($self, $devid, 'get'); - # if ($devstate eq $desired_states{$devid}) { - # if ($desired_states{$devid} eq "on") { - # push @successes_on, $devid; - # } else { - # push @successes_off, $devid; - # } - # - # delete $desired_states{$devid}; - # } - # } - # } - - # - # Now generate the response... - # - - if (@successes_on) { - $response .= qq( {\n "ids": [); - - foreach my $devid (@successes_on) { - $response .= qq["$devid",]; - } - - # Remove extra ',' at the end - $response =~ s/,$//; - - $response .= "],\n"; - - $response .= <{'execution'}->[0]->{'params'}->{'brightness'}; foreach my $device ( @{ $command->{'devices'} } ) { - $desired_states{ $device->{'id'} } = $brightness; - get_set_state( $self, $device->{'id'}, 'set', $brightness); + $response .= qq["$device->{'id'}",]; } - if ( keys %desired_states ) { - $response .= qq( {\n "ids": [); - - foreach my $devid ( keys %desired_states ) { - $response .= qq["$devid",]; - } - - # Remove extra ',' at the end - $response =~ s/,$//; + # Remove extra ',' at the end + $response =~ s/,$//; - $response .= "],\n"; + $response .= "],\n"; - $response .= <{'devices'} } ) { - # Just a marker to generate output later - $scene_ids{ $device->{'id'} } = undef; + my $response = ' { + "ids": ['; + foreach my $device ( @{ $command->{'devices'} } ) { get_set_state( $self, $device->{'id'}, 'set' ); - } - - # - # Now generate the response... - # - - $response .= qq( {\n); - $response .= qq( "ids": [); - - foreach my $devid ( keys %scene_ids ) { - $response .= qq["$devid",]; + $response .= qq["$device->{'id'}",]; } # Remove extra ',' at the end From 04c8e05e566fb9c714408b1c5bb081e6caa3da15 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Sat, 4 Nov 2017 09:34:24 -0400 Subject: [PATCH 11/78] Unconditionally send "on" state in QUERY response Send an "on" state unconditionally for all devices of type "Light". Before we were not sending the "on" state for devices that are dimmable, which is incorrect because a dimmable light is either on, or off. This caused the Google Assistant command "what lights are on?" to fail for dimmable lights. A response to a QUERY command now looks like this: { "requestId": "18352987651883269961", "payload": { "devices": { "11": { "on": false, "online": true }, "6": { "on": false, "brightness": 0, "online": true }, [...] --- lib/AoGSmartHome_Items.pm | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm index 4943e91df..b362c301b 100644 --- a/lib/AoGSmartHome_Items.pm +++ b/lib/AoGSmartHome_Items.pm @@ -577,6 +577,18 @@ EOF "$device->{'id'}": { EOF + # Check whether the device is on so we can populate the "on" state + # for the "OnOff" trait. A device is also "on" if the brightness level + # is non-zero. Note that all lights have the "OnOff" trait so we + # unconditionally send the "on" state. + my $on = $devstate eq "on" || $devstate > 0 ? 'true' : 'false'; + + $response .= <{'uuids'}->{$device->{'id'} }->{'realname'}); if ($mh_object->isa('Insteon::DimmableLight') || $mh_object->can('state_level') ) { @@ -591,12 +603,6 @@ EOF $response .= < Date: Wed, 15 Nov 2017 22:41:44 -0500 Subject: [PATCH 12/78] Split get_set_state into get_state and set_state Split get_set_state into get_state and set_state + minor clean ups. --- lib/AoGSmartHome_Items.pm | 140 ++++++++++++++++++++------------------ lib/read_table_A.pl | 9 ++- 2 files changed, 79 insertions(+), 70 deletions(-) diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm index b362c301b..0ed67fef5 100644 --- a/lib/AoGSmartHome_Items.pm +++ b/lib/AoGSmartHome_Items.pm @@ -213,10 +213,9 @@ package AoGSmartHome_Items; use Data::Dumper; use Storable; -use Time::HiRes qw(gettimeofday tv_interval); -sub get_set_state { - my ( $self, $uuid, $action, $state ) = @_; +sub set_state { + my ( $self, $uuid, $state ) = @_; my $name = $self->{'uuids'}->{$uuid}->{'name'}; my $realname = $self->{'uuids'}->{$uuid}->{'realname'}; @@ -226,45 +225,27 @@ sub get_set_state { # ??? $state = $self->{'uuids'}->{$uuid}->{ lc($state) } if $self->{'uuids'}->{$uuid}->{ lc($state) }; - print STDERR "[AoGSmartHome] Debug: get_set_state(uuid='$uuid', action='$action', state='$state', name='$name' realname='$realname' sub='$sub')\n" + print STDERR "[AoGSmartHome] Debug: set_state(uuid='$uuid', state='$state', name='$name' realname='$realname' sub='$sub')\n" if $main::Debug{'aog'} > 2; if ( $sub =~ /^voice[_-]cmd:\s*(.+)\s*$/ ) { my $voice_cmd = $1; - if ( $action eq 'set' ) { - $voice_cmd =~ s/[#!]/$state/; + $voice_cmd =~ s/[#!]/$state/; - print STDERR "[AoGSmartHome] Debug: running voice command \'$voice_cmd\'\n" - if $main::Debug{'aog'}; - &main::run_voice_cmd("$voice_cmd"); + print STDERR "[AoGSmartHome] Debug: running voice command \'$voice_cmd\'\n" + if $main::Debug{'aog'}; + &main::run_voice_cmd("$voice_cmd"); - return; - } - elsif ( $action eq 'get' ) { - - # FIXME -- "get" on voice command? Hhhmmm - return qq["on":true,"bri":254]; - } + return; } elsif ( ref $sub eq 'CODE' ) { - if ( $action eq 'set' ) { - print "[AoGSmartHome] Debug: running sub $sub(set, $state)\n" if $main::Debug{'aog'}; - &{$sub}( 'set', $state ); - return; - } - elsif ( $action eq 'get' ) { - my $debug = "[AoGSmartHome] Debug: get_state running sub: $sub( state, $state ) - "; - my $state = &{$sub}('state'); - if ( $state =~ /\d+/ ) { - $state = ( &roundoff( ( $state * 2.54 ) ) ); - my $return = qq["on":true,"bri":$state]; - print STDERR "$debug returning - $return\n" if $main::Debug{'aog'}; - return $return; - } - - return qq["on":true,"bri":254]; - } + my $mh_object = ::get_object_by_name($realname); + return undef if !defined $mh_object; + + print STDERR "[AoGSmartHome] Debug: running sub $sub(set, $state)\n" if $main::Debug{'aog'}; + &{$sub}($mh_object, $state, 'AoGSmartHome'); + return; } else { # @@ -274,44 +255,73 @@ sub get_set_state { my $mh_object = ::get_object_by_name($realname); return undef if !defined $mh_object; - if ( $action eq 'get' ) { - my $cstate = $mh_object->$statesub(); - $cstate =~ s/\%//; - my $type = $mh_object->get_type(); - my $debug = "[AoGSmartHome] Debug: get state -- actual object state: $cstate, object type: $type, "; + if ( ($mh_object->isa('Insteon::DimmableLight') + || $mh_object->can('state_level') ) && $state =~ /\d+/ ) { + $state = $state . '%'; + } - if ( $type =~ /X10/i ) { - $cstate = 'on' if $cstate =~ /\d+/ || $cstate =~ /dim/ || $cstate =~ /bright/; - $debug .= "determined state: $cstate, "; - } + print STDERR "[AoGSmartHome] Debug: setting object $realname to state '$state'\n" + if $main::Debug{'aog'}; - $debug .= "$debug returning $cstate\n"; + $mh_object->$sub( $state, 'AoGSmartHome' ); - print STDERR $debug if $main::Debug{'aog'} > 2; + return; + } +} - return $cstate; - } - elsif ( $action eq 'set' ) { - if ( ($mh_object->isa('Insteon::DimmableLight') - || $mh_object->can('state_level') ) && $state =~ /\d+/ ) { - $state = $state . '%'; - } +sub get_state { + my ( $self, $uuid, $state ) = @_; - print STDERR "[AoGSmartHome] Debug: setting object ($realname) to state ($state)\n" - if $main::Debug{'aog'}; + my $name = $self->{'uuids'}->{$uuid}->{'name'}; + my $realname = $self->{'uuids'}->{$uuid}->{'realname'}; + my $sub = $self->{'uuids'}->{$uuid}->{'sub'}; + my $statesub = $self->{'uuids'}->{$uuid}->{'statesub'}; - $mh_object->$sub( $state, 'AoGSmartHome' ); + # ??? + $state = $self->{'uuids'}->{$uuid}->{ lc($state) } if $self->{'uuids'}->{$uuid}->{ lc($state) }; - return; - } + print STDERR "[AoGSmartHome] Debug: get_state(uuid='$uuid', state='$state', name='$name' realname='$realname' sub='$sub')\n" + if $main::Debug{'aog'} > 2; + + if ( $sub =~ /^voice[_-]cmd:\s*(.+)\s*$/ ) { + my $voice_cmd = $1; + + # FIXME -- "get" on voice command? Hhhmmm + return qq["on":true,"bri":254]; } -} + elsif ( ref $statesub eq 'CODE' ) { + my $mh_object = ::get_object_by_name($realname); + return undef if !defined $mh_object; + + my $debug = "[AoGSmartHome] Debug: get_state() running sub: $statesub('$realname') - "; + my $state = &{$statesub}($mh_object); + print STDERR "$debug returning - $state\n" if $main::Debug{'aog'}; + return $state; + } + else { + # + # Treat as a MisterHouse object, using $statesub as the 'state' function. + # + + my $mh_object = ::get_object_by_name($realname); + return undef if !defined $mh_object; -sub roundoff { - my $num = shift; - my $roundto = shift || 1; + my $cstate = $mh_object->$statesub(); + $cstate =~ s/\%//; + my $type = $mh_object->get_type(); + my $debug = "[AoGSmartHome] Debug: get state() -- actual object state: $cstate, object type: $type, "; - return int( $num / $roundto + 0.5 ) * $roundto; + if ( $type =~ /X10/i ) { + $cstate = 'on' if $cstate =~ /\d+/ || $cstate =~ /dim/ || $cstate =~ /bright/; + $debug .= "determined state: $cstate, "; + } + + $debug .= "$debug returning $cstate\n"; + + print STDERR $debug if $main::Debug{'aog'} > 2; + + return $cstate; + } } sub new { @@ -563,7 +573,7 @@ EOF # The device is a light. # - my $devstate = get_set_state( $self, $device->{'id'}, 'get' ); + my $devstate = get_state( $self, $device->{'id'} ); if ( !defined $devstate ) { $response .= <{'id'}": { @@ -638,7 +648,7 @@ sub execute_OnOff { my $turn_on = $command->{'execution'}->[0]->{'params'}->{'on'} eq "true" ? 1 : 0; foreach my $device ( @{ $command->{'devices'} } ) { - get_set_state( $self, $device->{'id'}, 'set', $turn_on ? 'on' : 'off' ); + set_state( $self, $device->{'id'}, $turn_on ? 'on' : 'off' ); $response .= qq["$device->{'id'}",]; } @@ -664,7 +674,7 @@ sub execute_BrightnessAbsolute { my $brightness = $command->{'execution'}->[0]->{'params'}->{'brightness'}; foreach my $device ( @{ $command->{'devices'} } ) { - get_set_state( $self, $device->{'id'}, 'set', $brightness); + set_state( $self, $device->{'id'}, $brightness); $response .= qq["$device->{'id'}",]; } @@ -689,7 +699,7 @@ sub execute_ActivateScene { "ids": ['; foreach my $device ( @{ $command->{'devices'} } ) { - get_set_state( $self, $device->{'id'}, 'set' ); + set_state( $self, $device->{'id'}); $response .= qq["$device->{'id'}",]; } diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index b2e54046b..ea16b8c53 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1823,11 +1823,10 @@ sub read_table_A { } elsif ( $type eq "AOGSMARTHOME_ITEM" ) { my ($parent, $realname, $name, $sub, $on, $off, $statesub, @other) = @item_info; - $sub =~ s/&/\\&/ if $sub =~ /^&/; - $sub =~ s/\\// if $sub =~ /^\\\\&/; - $realname =~ s/_/ /g if $sub =~ /run_voice_cmd/; - $realname = "\$$realname" if $realname && $sub !~ /&|run_voice_cmd/; - $sub = qq|'$sub'| if $sub !~ /&/; + $sub =~ s%^&%\\&%; # "&my_subroutine" -> "\&my_subroutine" + $sub =~ s%^\\\\&%\\&%; # "\\&my_subroutine" -> "\&my_subroutine" + $sub = "'$sub'" if $sub !~ /&/; + $realname = "\$$realname" if $realname; my $other = join ', ', ( map { "'$_'" } @other ); # Quote data if (!$packages{AoGSmartHome_Items}++ ) { # first time for this object type? $code .= "use AoGSmartHome_Items;\n"; From 221bf25489aeecf914f7da996fe6dee7c34cd7dc Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 15 Nov 2017 22:45:07 -0500 Subject: [PATCH 13/78] Change read size from 1500 bytes to 65535 1500 bytes is the default Ethernet MTU but there can be larger MTUs, e.g. the loopback interface's MTU is 65535 bytes in a modern Linux distribution. Change the read size when reading data received over HTTP from 1500 bytes to 65535. --- bin/mh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bin/mh b/bin/mh index 3567514f4..b067b319e 100755 --- a/bin/mh +++ b/bin/mh @@ -2689,10 +2689,14 @@ sub check_for_socket_data { $data = <$sock>; } else { - # 1500 is ethernet packet size - # FIXME. Breaks when receiving more than 1500. Who's at fault? - # peloy@chapus.net - my $from_saddr = recv( $sock, $data, 4500, 0 ); + # We used to read 1500 bytes here because that is the default + # MTU for Ethernet. However, larger MTUs can be in use. In fact, + # the MTU of the loopback interface is set in a modern Linux + # distribution at 65535 bytes. Reading only 1500 can break + # things when the traffic comes to the MisterHouse HTTP server + # via the loopback interface and in packets larger than 1500 + # bytes. + my $from_saddr = recv( $sock, $data, 65535, 0 ); # Store udp from_* data if ( $Socket_Ports{$port_name}{protocol} From 67eac51964aaf12f62dd666e9b4ad493cb104e79 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Sun, 19 Nov 2017 11:22:54 -0500 Subject: [PATCH 14/78] Check for mandatory .ini params. Re-org debug msgs Check for mandatory .ini file parameters and disable AoG integration if one or more are missing. Re-organize debug messages. --- lib/AoGSmartHome_Items.pm | 3 ++- lib/http_server_aog.pl | 42 ++++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm index 0ed67fef5..c1b70c3de 100644 --- a/lib/AoGSmartHome_Items.pm +++ b/lib/AoGSmartHome_Items.pm @@ -366,7 +366,7 @@ sub add { if ( !$name ) { $name = $realname; $name =~ s/\$//g; - $name =~ s/_/ /g; # Otherwise the Google Assistant witll say + $name =~ s/_/ /g; # Otherwise the Google Assistant will say # "kitchen-underscore-light" instead of # "kitchen light". $name =~ s/#//g; @@ -396,6 +396,7 @@ sub add { $self->{'uuids'}->{$uuid}->{'on'} = lc($on) || 'on'; $self->{'uuids'}->{$uuid}->{'off'} = lc($off) || 'off'; $self->{'uuids'}->{$uuid}->{'statesub'} = $statesub || 'state'; + # If no device type is provided we default to 'light' $self->{'uuids'}->{$uuid}->{'type'} = lc($type) || 'light'; $self->{'uuids'}->{$uuid}->{'room'} = $room if $room; } diff --git a/lib/http_server_aog.pl b/lib/http_server_aog.pl index b05dac854..cfcde2cde 100644 --- a/lib/http_server_aog.pl +++ b/lib/http_server_aog.pl @@ -42,28 +42,42 @@ =head2 METHODS my $oauth_tokens; sub http_server_aog_startup { - &main::print_log("Actions on Google Smart Home Provider HTTP server helper startup:") - if $main::Debug{'aog'}; - - $::config_parms{'aog_auth_path'} = 'oauth' - if !defined $::config_parms{'aog_auth_path'}; - &main::print_log("aog_auth_path = $::config_parms{'aog_auth_path'}") - if $main::Debug{'aog'}; + if ( !$::config_parms{'aog_enable'}) { + &main::print_log("[AoGSmartHome] AoG is disabled."); + return; + } else { + &main::print_log("\n[AoGSmartHome] AoG is enabled; will look for AoG requests via HTTP."); + } - my $oauth_tokens_file = $config_parms{'aog_oauth_tokens_file'}; + # We don't want defaults for these important parameters so we disable + # AoG integration if one or more are missing. + if ( !defined $::config_parms{'aog_auth_path'} + || !defined $::config_parms{'aog_fulfillment_url'} + || !defined $::config_parms{'aog_client_id'} + || !defined $::config_parms{'aog_project_id'} ) + { + print STDERR "[AoGSmartHome] AoG is enabled but one or more .ini file parameters are missing; disabling AoG!\n"; + print STDERR "[AoGSmartHome] Required .ini file parameters: aog_auth_path aog_fulfillment_url aog_client_id aog_project_id\n"; + $::config_parms{'aog_enable'} = 0; + return; + } $::config_parms{'aog_oauth_tokens_file'} = "$config_parms{data_dir}/.aog_tokens" if !defined $::config_parms{'aog_oauth_tokens_file'}; - &main::print_log("aog_oauth_tokens_file = $::config_parms{'aog_oauth_tokens_file'}") - if $main::Debug{'aog'}; if ( -e $::config_parms{'aog_oauth_tokens_file'} ) { $oauth_tokens = retrieve( $::config_parms{'aog_oauth_tokens_file'} ); } + if ( $main::Debug{'aog'} ) { - print "[AoGSmartHome] Debug: Dumping \$oauth_tokens...\n"; - print Dumper $oauth_tokens; - print "Done.\n"; + print STDERR < Date: Mon, 4 Dec 2017 17:30:02 -0700 Subject: [PATCH 15/78] v1.1.01 - v2.2.0 firmware and Rhythm detection --- lib/Nanoleaf_Aurora.pm | 180 +++++++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 87 deletions(-) diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index 149ebb5ae..5bd680310 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -1,9 +1,6 @@ package Nanoleaf_Aurora; -#todo if poll queue exceeds 3, then just empty the queue -#print and purge the command queue - -# v1.0.15 +# v1.1.01 #if any effect is changed, by definition the static child should be set to off. #cmd data returns, need to check by command @@ -34,10 +31,10 @@ use IO::Socket::INET; # the location URL and tokens are stored in the mh.ini file # Firmware supported -# 1.4.38 or earlier - no +# 2.2.0 - needs v1.1 +# 1.5.0 to 2.1.3 - yes # 1.4.39 - pass the option api=beta -# 1.5.0 - yes -# 1.5.1 - yes +# 1.4.38 or earlier - no # Nanoleaf_Aurora Objects # @@ -84,12 +81,17 @@ our %rest; $rest{info} = ""; $rest{effects} = "effects"; $rest{auth} = "new"; -$rest{on} = "state/on"; -$rest{off} = "state/on"; -$rest{set_effect} = "effects/select"; +#$rest{on} = "state/on"; +#$rest{off} = "state/on"; +$rest{on} = "state"; +$rest{off} = "state"; +#$rest{set_effect} = "effects/select"; +$rest{set_effect} = "effects"; $rest{set_static} = "effects"; -$rest{brightness} = "state/brightness"; -$rest{brightness2} = "state/brightness"; +#$rest{brightness} = "state/brightness"; +#$rest{brightness2} = "state/brightness"; +$rest{brightness} = "state"; +$rest{brightness2} = "state"; $rest{get_static} = "effects"; $rest{identify} = "identify"; @@ -100,8 +102,10 @@ $opts{on} = "-response_code -json -put '{\"on\":true}'"; $opts{off} = "-response_code -json -put '{\"on\":false}'"; $opts{set_effect} = "-response_code -json -put '{\"select\":"; $opts{set_static} = "-response_code -json -put '{\"write\":{\"command\":\"display\",\"version\":\"1.0\",\"animType\":\"static\",\"animData\":"; -$opts{brightness} = "-response_code -json -put '{\"value\":"; -$opts{brightness2} = "-response_code -json -put '{\"increment\":"; +#$opts{brightness} = "-response_code -json -put '{\"value\":"; +#$opts{brightness2} = "-response_code -json -put '{\"increment\":"; +$opts{brightness} = "-response_code -json -put '{\"brightness\":{\"value\":"; +$opts{brightness2} = "-response_code -json -put '{\"brightness\":{\"increment\":"; $opts{get_static} = "-response_code -json -put '{\"write\":{\"command\":\"request\",\"version\":\"1.0\",\"animName\":\"*Static*\"}}'"; $opts{identify} = "-response_code -json -put '{}'"; @@ -125,7 +129,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.0.15"; + $self->{module_version} = "v1.1.01"; $self->{ssdp_timeout} = 4000; $self->{last_static} = ""; @@ -163,12 +167,12 @@ sub new { unlink "$::config_parms{data_dir}/Auroroa_cmd_" . $self->{name} . ".data"; $self->{cmd_process} = new Process_Item; $self->{cmd_process}->set_output( $self->{cmd_data_file} ); - $self->{generate_voice_cmds} = 0; - &::MainLoop_post_add_hook( \&Nanoleaf_Aurora::process_check, 0, $self ); - &::Reload_post_add_hook( \&Nanoleaf_Aurora::generate_voice_commands, 1, $self ); - $self->get_data(); $self->{init} = 0; $self->{init_data} = 0; + $self->{init_v_cmd} = 0; + &::MainLoop_post_add_hook( \&Nanoleaf_Aurora::process_check, 0, $self ); + &::Reload_post_add_hook( \&Nanoleaf_Aurora::generate_voice_commands, 1, $self ); + $self->get_data(); #push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); push( @{ $$self{states} }, 'off'); for my $i (1..99) { push @{ $$self{states} }, "$i%"; } @@ -258,7 +262,8 @@ sub process_check { if ( $self->{poll_process}->done_now() ) { - shift @{ $self->{poll_queue} }; #remove the poll since they are expendable. + #shift @{ $self->{poll_queue} }; #remove the poll since they are expendable. + @{ $self->{poll_queue} } = (); #clear the queue since process is done. my $com_status = "online"; main::print_log( "[Aurora:" . $self->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed" ) if ( $self->{debug} ); @@ -314,6 +319,12 @@ sub process_check { else { $self->{data}->{panels} = $data->{panelLayout}->{layout}->{numPanels}; $self->{data}->{panel_size} = $data->{panelLayout}->{layout}->{sideLength}; + $self->{data}->{rhythm} = 0; + if (defined $data->{rhythm}->{rhythmConnected} and $data->{rhythm}->{rhythmConnected} eq "true") { + $self->{data}->{rhythm} = 1; + } + $self->{data}->{panels} = $self->{data}->{panels} - $self->{data}->{rhythm}; #Rhythm module counts as a panel + for ( my $i = 0; $i < $self->{data}->{panels}; $i++ ) { $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{x} = @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{x}; @@ -339,18 +350,6 @@ sub process_check { } } -#polls are expendable and will always trigger on the timer, so don't keep a queue -# if ( scalar @{ $self->{poll_queue} } ) { -# my $cmd_string = shift @{ $self->{poll_queue} }; -# my ( $mode, $cmd ) = split /\|/, $cmd_string; -# $self->{poll_process}->set($cmd); -# $self->{poll_process}->start(); -# $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; -# $self->{poll_process_mode} = $mode; -# main::print_log( "[Aurora:" . $self->{name} . "] Poll Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd" ) -# if ( $self->{debug} ); -# -# } if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { main::print_log "[Aurora:" @@ -441,8 +440,7 @@ sub process_check { } else { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful, file data is " . $file_data ); - main::print_log( "[Aurora:" . $self->{name} . "] Retrying command..." ); + main::print_log( "[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful, retrying..." ); if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { main::print_log( "[Aurora:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); shift @{ $self->{cmd_queue} }; @@ -490,20 +488,8 @@ sub _get_JSON_data { push @{ $self->{poll_queue} }, "$mode|$cmd"; } else { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Clearing Polling Queue." ); - @{ $self->{poll_queue} } = (); #Polls are disposable - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne "offline" ) { - main::print_log "[Aurora:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } + #the queue has grown past the max, so it might be down. Since polls are expendable, just don't do anything + #when the aurora is back it will process the backlog, and as soon as a poll is processed, the queue is cleared. } } } @@ -729,6 +715,14 @@ sub print_info { main::print_log( "[Aurora:" . $self->{name} . "] Manufacturer: " . $self->{data}->{info}->{manufacturer} ); main::print_log( "[Aurora:" . $self->{name} . "] Model: " . $self->{data}->{info}->{model} ); main::print_log( "[Aurora:" . $self->{name} . "] Firmware: " . $self->{data}->{info}->{firmwareVersion} ); + if ($self->{data}->{rhythm}) { + main::print_log( "[Aurora:" . $self->{name} . "] Rhythm Hardware: " . $self->{data}->{info}->{rhythm}->{hardwareVersion} ); + main::print_log( "[Aurora:" . $self->{name} . "] Rhythm Firmware: " . $self->{data}->{info}->{rhythm}->{firmwareVersion} ); + } else { + main::print_log( "[Aurora:" . $self->{name} . "] Rhythm Module: Not Present"); + } + main::print_log( "[Aurora:" . $self->{name} . "] Firmware: " . $self->{data}->{info}->{firmwareVersion} ); + main::print_log( "[Aurora:" . $self->{name} . "] Connected Panels: " . $self->{data}->{panels} ); main::print_log( "[Aurora:" . $self->{name} . "] Panel Size: " . $self->{data}->{panel_size} ); main::print_log( "[Aurora:" . $self->{name} . "] API Path: " . $self->{api_path} ); @@ -1022,11 +1016,11 @@ sub set { $self->_push_JSON_data($mode); } elsif ( $mode =~ /^(\d+)/ ) { - my $params = $opts{brightness} . $1 . '}' . "'"; + my $params = $opts{brightness} . $1 . '}}' . "'"; $self->_push_JSON_data( 'brightness', $params ); } elsif ( $mode =~ /^([-+]\d+)/ ) { - my $params = $opts{brightness2} . $1 . '}' . "'"; + my $params = $opts{brightness2} . $1 . '}}' . "'"; $self->_push_JSON_data( 'brightness2', $params ); } else { @@ -1066,6 +1060,16 @@ sub check_static { return ('1'); } +sub is_rhythm_effect { + my ( $self) = @_; + my $return = 0; + print "DB \$self->{data}->{info}->{rhythm}->{rhythmActive} = $self->{data}->{info}->{rhythm}->{rhythmActive}\n"; + print "DB \$self->{data}->{info}->{rhythm}->{rhythmMode} = $self->{data}->{info}->{rhythm}->{rhythmMode}\n"; + $return = 1 if ($self->{data}->{info}->{rhythm}->{rhythmActive}); + + return $return; +} + sub print_static { my ($self) = @_; @@ -1104,42 +1108,43 @@ sub identify { sub generate_voice_commands { my ($self) = @_; - unless ($self->{generate_voice_cmds}) { - my $object_string; - $self->{generate_voice_cmds} = 1; - my $object_name = $self->get_object_name; - &main::print_log("Generating Voice commands for Nanoleaf Aurora Controller $object_name"); - - my $voice_cmds = $self->get_voice_cmds(); - my $i = 1; - foreach my $cmd ( keys %$voice_cmds ) { - - #get object name to use as part of variable in voice command - my $object_name_v = $object_name . '_' . $i . '_v'; - $object_string .= "use vars '${object_name}_${i}_v';\n"; - - #Convert object name into readable voice command words - my $command = $object_name; - $command =~ s/^\$//; - $command =~ tr/_/ /; - - #Initialize the voice command with all of the possible device commands - $object_string .= $object_name . "_" . $i . "_v = new Voice_Cmd '$command $cmd';\n"; - - #Tie the proper routine to each voice command - $object_string .= $object_name . "_" . $i . "_v -> tie_event('" . $voice_cmds->{$cmd} . "');\n\n"; #, '$command $cmd');\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', 'Nanoleaf_Aurora', 'Controller_commands' ); - $i++; - } - - #Evaluate the resulting object generating string - package main; - eval $object_string; - print "Error in nanoleaf_aurora_item_commands: $@\n" if $@; - - package Nanoleaf_Aurora; + + if ($self->{init_v_cmd} == 0) { + my $object_string; + my $object_name = $self->get_object_name; + $self->{init_v_cmd} = 1; + &main::print_log("Generating Voice commands for Nanoleaf Aurora Controller $object_name"); + + my $voice_cmds = $self->get_voice_cmds(); + my $i = 1; + foreach my $cmd ( keys %$voice_cmds ) { + + #get object name to use as part of variable in voice command + my $object_name_v = $object_name . '_' . $i . '_v'; + $object_string .= "use vars '${object_name}_${i}_v';\n"; + + #Convert object name into readable voice command words + my $command = $object_name; + $command =~ s/^\$//; + $command =~ tr/_/ /; + + #Initialize the voice command with all of the possible device commands + $object_string .= $object_name . "_" . $i . "_v = new Voice_Cmd '$command $cmd';\n"; + + #Tie the proper routine to each voice command + $object_string .= $object_name . "_" . $i . "_v -> tie_event('" . $voice_cmds->{$cmd} . "');\n\n"; #, '$command $cmd');\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', 'Nanoleaf_Aurora', 'Controller_commands' ); + $i++; + } + + #Evaluate the resulting object generating string + package main; + eval $object_string; + print "Error in nanoleaf_aurora_item_commands: $@\n" if $@; + + package Nanoleaf_Aurora; } } @@ -1327,4 +1332,5 @@ sub set { # v1.0.12 - get_effects method to get array of available effects # v1.0.13 - ability to print and purge the command queue in case a network error prevents clearing, empty poll queue if max reached # v1.0.14 - commands now queue properly -# v1.0.15 - only load voice commands at startup \ No newline at end of file +# v1.0.15 - fixed polling +# v1.1.01 - firmware v2.2.0 and rhythm module \ No newline at end of file From ac9fda45493f8e3c28a7377ab38ccf17d8c544e8 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Mon, 18 Dec 2017 19:17:37 -0500 Subject: [PATCH 16/78] Add Experimental Support For INSTEON Thermostat Add "experimental" support a "thermostat" device type that currently only works with the INSTEON thermostat ("experimental" because I don't have an INSTEON thermostat so I cannot test). --- lib/AoGSmartHome_Items.pm | 169 +++++++++++++++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 13 deletions(-) diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm index c1b70c3de..08221d4c3 100644 --- a/lib/AoGSmartHome_Items.pm +++ b/lib/AoGSmartHome_Items.pm @@ -380,10 +380,18 @@ sub add { if ($parm eq 'type') { $type = $value; + + # Check that the type is a supported one + if ($type ne 'light' && $type ne 'scene' && $type ne 'switch' + && $type ne 'outlet'&& $type ne 'thermostat') { + &main::print_log("[AoGSmartHome] Invalid device type '$type'; ignoring AoG item."); + return; + } } elsif ($parm eq 'room') { $room = $value; } else { - &main::print_log("[AoGSmartHome] Invalid device property '$parm'; ignoring."); + &main::print_log("[AoGSmartHome] Invalid device property '$parm'; ignoring AoG item."); + return; } } } @@ -472,11 +480,19 @@ EOF }, EOF } - elsif ( $type eq 'switch' ) { + elsif ( $type eq 'switch' || $type eq 'outlet' ) { + # + # action.devices.types.SWITCH and action.devices.types.OUTLET + # are basically the same type of device; as far as I can + # tell the only difference is the icon they get in the apps. + # + + $type = uc $type; + $response .= <{'uuids'}->{$uuid}->{'realname'}); + if (!$mh_object->isa('Insteon::Thermostat') ) { + &main::print_log("[AoGSmartHome] '$self->{'uuids'}->{$uuid}->{'realname'} is an unsupported thermostat; ignoring AoG item."); + next; + } + + $response .= <{'uuids'}->{$uuid}->{'name'}" + }, + "willReportState": false, + "attributes": { + "availableThermostatModes": "off,heat,cool,on", + "thermostatTemperatureUnit": "F" + }, +EOF + + if (exists $self->{'uuids'}->{$uuid}->{'room'}) { + $response .= <{'uuids'}->{$uuid}->{'room'}", +EOF + } + + $response =~ s/,$//; # Remove extra ',' + + $response .= <{'inputs'}->[0]->{'payload'}->{'devices'} } ) { - if ( !exists $self->{'uuids'}->{ $device->{'id'} } ) { + my $uuid = $device->{'id'}; # Makes things easier below... + + if ( !exists $self->{'uuids'}->{$uuid} ) { $response .= <{'id'}": { + "$uuid": { "errorCode": "deviceNotFound" }, EOF next; } - if ( $self->{'uuids'}->{ $device->{'id'} }->{'type'} eq 'scene' ) { + if ( $self->{'uuids'}->{$uuid}->{'type'} eq 'scene' ) { $response .= <{'id'}": { + "$uuid": { "online": true }, EOF next; } + elsif ( $self->{'uuids'}->{$uuid}->{'type'} eq 'thermostat' ) { + my $mh_object = ::get_object_by_name($self->{'uuids'}->{$uuid}->{'realname'}); + if ($mh_object->isa('Insteon::Thermostat') ) { + my $mode = $mh_object->get_mode(); + + my $temp_setpoint; + if ($mode eq 'cool') { + $temp_setpoint = $mh_object->get_cool_sp(); + } else { + $temp_setpoint = $mh_object->get_heat_sp(); + } + + my $temp_ambient = $mh_object->get_temp(); + + $response .= <{'id'} ); + my $devstate = get_state( $self, $uuid ); if ( !defined $devstate ) { $response .= <{'id'}": { + "$uuid": { "errorCode": "deviceNotFound" }, EOF @@ -585,7 +667,7 @@ EOF } $response .= <{'id'}": { + "$uuid": { EOF # Check whether the device is on so we can populate the "on" state @@ -600,7 +682,7 @@ EOF # If the device is dimmable we provided the "Brightness" trait, so we # have to supply the "brightness" state. - my $mh_object = ::get_object_by_name($self->{'uuids'}->{$device->{'id'} }->{'realname'}); + my $mh_object = ::get_object_by_name($self->{'uuids'}->{$uuid}->{'realname'}); if ($mh_object->isa('Insteon::DimmableLight') || $mh_object->can('state_level') ) { @@ -694,7 +776,6 @@ EOF sub execute_ActivateScene { my ( $self, $command ) = @_; - my $response = ''; my $response = ' { "ids": ['; @@ -717,6 +798,65 @@ EOF return $response; } +sub execute_ThermostatX { + my ( $self, $command, $exec_command ) = @_; + + my $response; + + my $execution_command = $command->{'execution'}->[0]->{'command'}; + + foreach my $device ( @{ $command->{'devices'} } ) { + my $realname = $self->{'uuids'}->{$device->{'id'} }->{'realname'}; + + my $mh_object = ::get_object_by_name($realname); + return undef if !defined $mh_object; + + if ($mh_object->isa('Insteon::Thermostat') ) { + if ( $execution_command =~ /TemperatureSetpoint/ ) { + my $setpoint = $command->{'execution'}->[0]->{'params'}->{'thermostatTemperatureSetpoint'}; + if ($mh_object->get_mode() eq 'cool') { + $mh_object->cool_setpoint($setpoint); + } else { + $mh_object->heat_setpoint($setpoint); + } + } elsif ( $execution_command =~ /ThermostatSetMode/ ) { + my $mode = $command->{'execution'}->[0]->{'params'}->{'thermostatMode'}; + $mh_object->mode($mode); + } + + my $mode = $mh_object->get_mode(); + + my $temp_setpoint; + if ($mode eq 'cool') { + $temp_setpoint = $mh_object->get_cool_sp(); + } else { + $temp_setpoint = $mh_object->get_heat_sp(); + } + + my $temp_ambient = $mh_object->get_temp(); + + $response .= " {"; + $response .= <{'id'}"], + "status": "SUCCESS", + "states": { + "thermostatMode": "$mode", + "thermostatTemperatureSetpoint": "$temp_setpoint", + "thermostatTemperatureAmbient": "$temp_ambient", + } + }, +EOF + } + # No "else" -- unsupported thermostats are not included in + # "sync" response + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + return $response; +} + =item C Generates an action.devices.EXECUTE fulfillment response. @@ -755,6 +895,9 @@ EOF elsif ( $execution_command eq "action.devices.commands.ActivateScene" ) { $response .= execute_ActivateScene( $self, $command ); } + elsif ( $execution_command =~ /^action\.devices\.commands\.Thermostat.+$/ ) { + $response .= execute_ThermostatX( $self, $command ); + } } # Remove extra ',' at the end From 0e270cefdf424e57b1be4082b1a2462d76f1e88b Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Mon, 18 Dec 2017 19:43:48 -0500 Subject: [PATCH 17/78] Add Descriptions For New .mht file AoG Items Add descriptions for new .mht file Actions on Google items. This way the new AoG items show up nicely in the web interface @ Home > Mr. House Home > Setup MrHouse > Edit Items. Thanks to Wayne Gatlin for letting me know about this feature, which I did not know existed. --- lib/read_table_A.pl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index ea16b8c53..e2cfa6e75 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1817,11 +1817,13 @@ sub read_table_A { #-------------- AoGSmartHome Objects ----------------- elsif ( $type eq "AOGSMARTHOME_ITEMS" ) { + ## require 'AoGSmartHome_Items.pm'; ($name) = @item_info; $object = "AoGSmartHome_Items()"; } elsif ( $type eq "AOGSMARTHOME_ITEM" ) { + ## my ($parent, $realname, $name, $sub, $on, $off, $statesub, @other) = @item_info; $sub =~ s%^&%\\&%; # "&my_subroutine" -> "\&my_subroutine" $sub =~ s%^\\\\&%\\&%; # "\\&my_subroutine" -> "\&my_subroutine" From 6207fc7cdfef43c63bb51350f9ff35a906fd4560 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 7 Jul 2018 13:30:00 -0600 Subject: [PATCH 18/78] v1.3 - better error recovery if bad WU data.. --- code/common/calc_eto.pl | 136 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 11 deletions(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 8c60a90c1..550de60b7 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -1,8 +1,11 @@ # Category = Irrigation -# June 2016 -# v1.2 -# - Added in minimum time calculation +# June 2018 +# v1.3 +# - added check if wudata returns null data +# - Email has clearer information on start times and run length. +# - if run after sunrise, then use the sunset times and max 2 cycles +# - write predicted daily rain to the RRD. #@ This module allows MisterHouse to calculate daily EvapoTranspiration based on a #@ Data feed from Weatherunderground (WU). To use it you need to sign up for a weatherundeground key @@ -10,7 +13,7 @@ #@ A location is also required. Best is a lat/long pair. #@ By default wuData is written to $Data_Dir/wuData and the eto logs are written to $Data_Dir/eto #@ -#@ The ET programs can be automatically uploaded to an OpenSprinkler. (need v1.1 of the lib) +#@ The ET programs can be automatically uploaded to an OpenSprinkler. (need >= v1.1 of the lib) ########################################################################################################### ## Credits ## @@ -49,8 +52,6 @@ #TODO # - the safefloat and safeint subs are from python. don't know if they're needed -# - if run after sunrise, then use the sunset times and max 2 cycles (line 677) -# - populate yesterday's rain value in the RRD #VERIFY # - line 430 sub getConditionsData chkcond array isn't checked yet @@ -59,6 +60,28 @@ # - line 610 read in multiple water times for overall aggregate # - line 711 when multiple times are scheduled, only one entry was written to the logs. +#WU Data elements mapping (useful if we want to look to another provider) +#$hist = $wuData->{history}->{dailysummary}[0]; +#$wuData->{history}->{observations} +#$wuData->{history}->{observations}->[$period]->{date}->{hour} +#$wuData->{history}->{observations}->[$period]->{conds} + +#$tzone = $data->{current_observation}->{local_tz_long}; +#$mm = $data->[$day]->{qpf_allday}->{mm}; +#$cor = $data->[$day]->{pop}; +#$rHour = safe_int( $data->{'sunrise'}->{'hour'}, 6 ); +#$rMin = safe_int( $data->{'sunrise'}->{'minute'} ); +#$sHour = safe_int( $data->{'sunset'}->{'hour'}, 18 ); +#$sMin = safe_int( $data->{'sunset'}->{'minute'} ); +#$conditions->{ $current->{weather} } +#$current->{wind_kph} ), 10 ); +#$cTemp = safe_float( $current->{temp_c}, 20 ); +#$cmm = safe_float( $current->{precip_today_metric} ); +#$predicted->{avewind}->{kph} +#$pLowTemp = safe_float( $predicted->{low}->{celsius} ); +#$pCoR = safe_float( $predicted->{pop} ); +#$pmm = safe_float( $predicted->{qpf_allday}->{mm} ); + use eto; use LWP::UserAgent; use HTTP::Request::Common; @@ -72,6 +95,7 @@ use Date::Calc qw(Day_of_Year); my $debug = 0; my $msg_string; +my $rrd = 0; $p_wu_forecast = new Process_Item qq[get_url --quiet "http://api.wunderground.com/api/$config_parms{wu_key}/astronomy/yesterday/conditions/forecast/q/$config_parms{eto_location}.json" "$config_parms{data_dir}/wuData/wu_data.json"]; @@ -96,7 +120,7 @@ if ( $Startup or $Reload ) { $eto_ready = 1; - print_log "[calc_eto] v1.2 Startup. Checking Configuration..."; + print_log "[calc_eto] v1.3 Startup. Checking Configuration..."; mkdir "$eto_data_dir" unless ( -d "$eto_data_dir" ); mkdir "$eto_data_dir/ET" unless ( -d "$eto_data_dir/ET" ); mkdir "$eto_data_dir/logs" unless ( -d "$eto_data_dir/logs" ); @@ -128,12 +152,24 @@ print_log "[calc_eto] ERROR! wu key undefined!!"; $eto_ready = 0; } + if ( defined $config_parms{eto_rrd} ) { + if ($config_parms{eto_rrd} eq "metric") { + print_log "[calc_eto] Will write daily rain to RRD (mms)"; + $rrd = "m"; + } elsif ($config_parms{eto_rrd} eq "in") { + print_log "[calc_eto] Will write daily rain to RRD (inches)"; + $rrd = "i"; + } else { + print_log "[calc_eto] Unknown RRD option $config_parms{eto_rrd}"; + $rrd = 0; + } + } if ( defined $config_parms{eto_irrigation} ) { print_log "[calc_eto] $config_parms{eto_irrigation} set as programmable irrigation system"; } else { print_log "[calc_eto] WARNING! no sprinkler system defined!"; - } + } if ($eto_ready) { print_log "[calc_eto] Configuration good. ETo Calcuations Ready"; print_log "[calc_eto] Will email results to $config_parms{eto_email}" if ( defined $config_parms{eto_email} ); @@ -811,6 +847,12 @@ sub writeResults { my @startTime = (-1) x 4; my @availTimes = ( $sun->{rise} - sum(@runTime) / 60, $sun->{rise} + 60, $sun->{set} - sum(@runTime) / 60, $sun->{set} + 60 ); + #if the current time is after $sun->{rise} then add two more options to $sun->{set} + if (time_greater_than($Time_Sunrise)) { + print_log "[calc_eto] It's after sunrise, so run extra programs at night"; + @availTimes = ($sun->{set} - sum(@runTime) / 60, $sun->{set} + 60, $sun->{set} + 120, $sun->{set} - (sum(@runTime) / 60) - 60 ); + } + print "[times=$times, sun->{rise}=" . $sun->{rise} . " sum=" . sum(@runTime) / 60 . "]\n"; # if ($debug); for ( my $i = 0; $i < $times; $i++ ) { @@ -904,6 +946,44 @@ sub calc_eto_runtimes { return $rt; } +sub detailSchedule { + my ($stime) = @_; + my ($times, $lengths) = $stime =~ /\[\[(.*)\],\[(.*)\]\]/; + my $msg = ""; + + foreach my $time (split /,/, $times) { + next if ($time == -1); + my $station_id = 1; + $time = $time * 60; #add in seconds + foreach my $station (split /,/, $lengths) { + my $run_hour = 0; + if ($station > 3600) { + $run_hour = int($station / 3600); + $station = int($station % 3600); + } + my $run_min = int($station / 60); + my $run_sec = int($station % 60); + $msg .= "[calc_eto] : " . formatTime($time) . " : Station:" .sprintf("%2s",$station_id) . " Run Time:" .sprintf("%02d:%02d:%02d",$run_hour,$run_min,$run_sec) . "\n"; + $station_id++; + $time += $run_sec + ($run_min * 60) + ($run_hour * 3600); + } + } + return ($msg); + + sub formatTime { + my ($t) = @_; + my $hour = int($t / 3600); + my $min = int(($t % 3600) / 60); + my $sec = int(($t % 3600) % 60); + my $ampm = "AM"; + if ($hour > 12) { + $ampm = "PM"; + $hour = $hour - 12; + } + return(sprintf("%2s:%02d:%02d",$hour,$min,$sec) . " $ampm"); + } +} + sub main_calc_eto { my ( $datadir, $loc, $wuData ) = @_; @@ -992,6 +1072,18 @@ sub main_calc_eto { my $tmean = ( $tmin + $tmax ) / 2; my $alt = safe_float( $wuData->{current_observation}->{display_location}->{elevation} ); my $tdew = safe_float( $hist->{meandewptm} ); + if ($hist->{date}->{year} == undef || $hist->{date}->{mon} == undef || $hist->{date}->{mday} == undef) { + #problem with the data + my $msg = "[calc_eto] ERROR: Bad Data received from Provider. A date field is empty"; + print_log $msg; + my $msg2 = "[calc_eto] ERROR: Undefined Parameter: Year=[$hist->{date}->{year}] Month=[$hist->{date}->{mon}] Day=[$hist->{date}->{mday}]"; + print_log $msg2; + if ( defined $config_parms{eto_email} ) { + print_log "[calc_eto] Emailing Error"; + net_mail_send( to => $config_parms{eto_email}, subject => "EvapoTranspiration Failed to retrieve data", text => $msg . "\n" . $msg2 ); + } + return "[[-1,-1,-1,-1],[0]]"; + } my $doy = Day_of_Year( $hist->{date}->{year}, $hist->{date}->{mon}, $hist->{date}->{mday} ); my $sun_hours = sun_block( $wuData, $sun->{rise}, $sun->{set}, $conditions ); my $rh_min = safe_float( $hist->{minhumidity} ); @@ -1130,9 +1222,31 @@ sub main_calc_eto { #Write the WU data to a file. This can be used for the MH weather data and save an api call writewuData( $wuData, $noWater, $wuDataPath ); - $msg = "[calc_eto] RESULTS Calculated Schedule: $rtime"; - print_log $msg; - $msg_string .= $msg . "\n"; + + #write to the RRD if it's enabled + if ($rrd != 0) { + $msg = '[calc_eto] Writing predicted rain to RRD :'; + + if ($rrd eq "m") { + $msg .= $wuData->{current_observation}->{precip_today_metric} . " mm"; + $Weather{RainTotal} = $wuData->{current_observation}->{precip_today_metric}; + } else { + $msg .= $wuData->{current_observation}->{precip_today_in} . " in"; + $Weather{RainTotal} = $wuData->{current_observation}->{precip_today_in}; + } + print_log $msg; + $msg_string .= $msg . "\n"; + + } + + #$msg = "[calc_eto] RESULTS Calculated Schedule: $rtime"; + #print_log $msg; + #$msg_string .= $msg . "\n"; + my ($rtime2) = detailSchedule($rtime); + foreach my $detail (split /\n/,$rtime2) { + print_log $detail; + } + $msg_string .= $msg . $rtime2; if ( defined $config_parms{eto_email} ) { print_log "[calc_eto] Emailing results"; net_mail_send( to => $config_parms{eto_email}, subject => "EvapoTranspiration Results for $Time_Now", text => $msg_string ); From 56e7c6bc2c2cda8adff525b97118bdc36e3b1dfb Mon Sep 17 00:00:00 2001 From: hplato Date: Sat, 7 Jul 2018 14:16:36 -0600 Subject: [PATCH 19/78] fixed up some formatting, prevented side scrolling --- web/ia7/index.shtml | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/web/ia7/index.shtml b/web/ia7/index.shtml index a0e210ea0..d06a2ae6f 100644 --- a/web/ia7/index.shtml +++ b/web/ia7/index.shtml @@ -30,10 +30,7 @@ - - - - + @@ -44,7 +41,7 @@ white-space: nowrap; text-align: left; padding-left: 15px; - padding-right: 15px; + padding-right: 15px; } @media (min-width: 768px) { .top-buffer { @@ -279,8 +276,8 @@ #loader { position: absolute; - left: 50%; - top: 70%; + right: 45%; + top: 60%; z-index: 1; width: 150px; height: 150px; @@ -303,6 +300,12 @@ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + + @media (max-width: 500px) { + #loader { + right: 35%; + } + } .btn-purple { color: #fff; @@ -333,7 +336,12 @@ background-color: #834087; border-color: #834087; } - + + html, body { + max-width: 100%; + overflow-x: hidden; + } + @@ -357,10 +365,11 @@ - -

+ + +
From 64c00cdd83a98da037317e6abaae899a0446f9d1 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 7 Jul 2018 14:24:28 -0600 Subject: [PATCH 20/78] fixed unlink typo --- lib/Nanoleaf_Aurora.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index 5bd680310..e6ccc5b1a 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -164,7 +164,7 @@ sub new { $self->{poll_process}->set_output( $self->{poll_data_file} ); @{ $self->{cmd_queue} } = (); $self->{cmd_data_file} = "$::config_parms{data_dir}/Aurora_cmd_" . $self->{name} . ".data"; - unlink "$::config_parms{data_dir}/Auroroa_cmd_" . $self->{name} . ".data"; + unlink "$::config_parms{data_dir}/Aurora_cmd_" . $self->{name} . ".data"; $self->{cmd_process} = new Process_Item; $self->{cmd_process}->set_output( $self->{cmd_data_file} ); $self->{init} = 0; @@ -1333,4 +1333,4 @@ sub set { # v1.0.13 - ability to print and purge the command queue in case a network error prevents clearing, empty poll queue if max reached # v1.0.14 - commands now queue properly # v1.0.15 - fixed polling -# v1.1.01 - firmware v2.2.0 and rhythm module \ No newline at end of file +# v1.1.01 - firmware v2.2.0 and rhythm module From 87ee7f2b2db6115489c9cb443460428ccda2edd6 Mon Sep 17 00:00:00 2001 From: hplato Date: Sat, 7 Jul 2018 16:55:58 -0600 Subject: [PATCH 21/78] IA7 v2.0.680 - fixed up something went wrong opacity --- web/ia7/include/javascript.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 5b7e33208..ee7568ac4 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.670"; +var ia7_ver = "v2.0.680"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -1952,12 +1952,22 @@ var something_went_wrong = function(module,text,fadeout) { if ((json_store.ia7_config.prefs.show_errors !== undefined) && json_store.ia7_config.prefs.show_errors == "yes") { + // if a module SWW is already displayed, don't display another duplicate message + var found = 0; + $('.sww-background').each(function (){ + if ($(this).attr('module') == module) { + console.log("INFO: Something went Wrong module "+module+" already displayed"); + found = 1; + } + }); + if (found) return; + console.log("creating SWW window for "+module); var type = "dark"; var mobile = ""; if ($(window).width() <= 768) { // override the responsive mobile top-buffer mobile = "mobile-alert"; } - var html = "
"; + var html = "
"; //if fadeout is -1 then don't display data-dismiss? if (fadeout == undefined || (fadeout !== undefined && fadeout !== 0)) html += ""; html += "
"; @@ -2208,8 +2218,11 @@ var get_notifications = function(time) { }; -var ajax_req_error = function(xhr, status, error, module) { +var ajax_req_error = function(xhr, status, error, module, modal) { //ignore abort messages, not a communication issue + //close any open modals if an error encountered (unless the ajax error is through a modal interface) + if (modal == undefined) modal = false; + if (modal !== true) $('.modal').modal('hide'); if (status == "abort" && error == "abort") return; var message = "Unknown ajax request error"; if (xhr == undefined || xhr.responseText == undefined || xhr.responseText == "") { From 18470d060e6b5345bc06545d2e1afe5b3a3664f3 Mon Sep 17 00:00:00 2001 From: hplato Date: Tue, 10 Jul 2018 18:32:15 -0600 Subject: [PATCH 22/78] IA7 v2.0.690 - fixed login issue --- lib/handy_utilities.pl | 352 ++++++++++++++++++++++++---------- lib/json_server.pl | 80 +++++--- web/ia7/house/whatsnew.shtml | 25 ++- web/ia7/include/javascript.js | 31 +-- 4 files changed, 344 insertions(+), 144 deletions(-) diff --git a/lib/handy_utilities.pl b/lib/handy_utilities.pl index c836e7c8e..0b4a96710 100644 --- a/lib/handy_utilities.pl +++ b/lib/handy_utilities.pl @@ -1603,128 +1603,268 @@ sub main::Groups { -# Modify and get user to group relationships - if ($user and $group) { - if ($function eq 'add') { #Add user to group and add the group if needed - $UserGroups->{user}->{$user}->{$group} = 1; - $UserGroups->{group}->{$group}->{status} = 1; #Add the group - store $UserGroups, $file; - return 1; - } elsif ($function eq 'remove') { #Remove a user from a group - delete $UserGroups->{user}->{$user}->{$group}; - store $UserGroups, $file; - return 1; - } elsif ( ($UserGroups) and ($function eq 'memberof') ) { #Check is user is a member of a specific group - foreach my $ugroup ( keys %{$UserGroups->{user}->{$user}} ) { - return 1 if $ugroup eq $group; - } - return 0; - } - } elsif ( $UserGroups and $user and ($function eq 'get') ) { #Get all of the groups for a specific user - foreach my $ugroup ( keys %{$UserGroups->{user}->{$user}} ) { - push (@ugroups, $ugroup); - } - return \@ugroups; - -# Modify and get group permissions - } elsif ( $UserGroups and $group and $cmd and $function and $cmdperm and $type and ( defined($line) ) ) { - if ($function eq 'add') { #Add an allowed/denied CMD for a group on a specific line +# If the group file is empty, add user/group admin by default so we dont crash +unless ( ref($UserGroups) eq 'HASH' ) { + $UserGroups->{user}->{admin}->{status} = 1; + $UserGroups->{group}->{admin}->{status} = 1; + $UserGroups->{user}->{admin}->{group}->{admin} = 1; + push @{ $UserGroups->{user}->{admin}->{grouplist} }, 'admin'; + $UserGroups->{user}->{admin}->{defaultperm}->{_global_} = 'allow'; + store $UserGroups, $file; +} + + +# Modify and get user to group relationships + if ( $user and $group and ($function eq 'add') ) { #Add user to group and add the group if needed + $UserGroups->{user}->{$user}->{status} = 1 unless ( defined($UserGroups->{user}->{$user}->{status}) ); #Add the user if needed + $UserGroups->{group}->{$group}->{status} = 1 unless ( defined($UserGroups->{group}->{$group}->{status}) ); #Add the group if needed + unless ( $UserGroups->{user}->{$user}->{group}->{$group} ) { + $UserGroups->{user}->{$user}->{group}->{$group} = 1; #Add the user to the group + if ( defined($line) ) { #Add the group to the user on a specific line if line is defined + my $cmdindex = 0; + if ( $UserGroups->{user}->{$user}->{grouplist} ) { + $cmdindex = scalar(@{ $UserGroups->{user}->{$user}->{grouplist} }); + } + return 0 if $line > $cmdindex; + splice(@{ $UserGroups->{user}->{$user}->{grouplist} }, $line, 1); + ${ $UserGroups->{user}->{$user}->{grouplist} }[$line] = $group; + } else { + push @{ $UserGroups->{user}->{$user}->{grouplist} }, $group; #Add the user to the group to the bottom of the list + } + } + store $UserGroups, $file; + return 1; + } elsif ( $user and ($function eq 'add') ) { #Add a new user + unless ( defined($UserGroups->{user}->{$user}->{status}) ) { + $UserGroups->{user}->{$user}->{status} = 1; + $UserGroups->{user}->{$user}->{password} = ""; + $UserGroups->{user}->{$user}->{password} = $type if (defined $type and $type ne ""); + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and $user and ($function eq 'disable') ) { #Disable user + if ( $user eq 'admin' ) { return 0 } # Disallow admin from being disabled + if ( defined($UserGroups->{user}->{$user}->{status}) && ( not $UserGroups->{user}->{$user}->{status} == 0 ) ) { + $UserGroups->{user}->{$user}->{status} = 0; + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and $user and ($function eq 'enable') ) { #Enable user + if ( defined($UserGroups->{user}->{$user}->{status}) && ( not $UserGroups->{user}->{$user}->{status} == 1 ) ) { + $UserGroups->{user}->{$user}->{status} = 1; + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and $user and ($function eq 'getpw') ) { #Return stored password + if ( defined($UserGroups->{user}->{$user}->{password}) ) { +print "PWD = $UserGroups->{user}->{$user}->{password}\n"; + return ($UserGroups->{user}->{$user}->{password}); + } + return ""; + } elsif ( $UserGroups and $user and $group and ($function eq 'remove') ) { #Remove a user from a group + if ( ( $user eq 'admin' ) && ( $group eq 'admin' ) ) { return 0 } # Disallow admin from being removed from admin group + delete $UserGroups->{user}->{$user}->{group}->{$group}; + my $lc = -1; + foreach my $ugroup ( @{ $UserGroups->{user}->{$user}->{grouplist} } ) { + $lc++; + if ( $ugroup eq $group ) { splice @{ $UserGroups->{user}->{$user}->{grouplist} }, $lc, 1 } ## Delete group from user group array + } + store $UserGroups, $file; + return 1; + } elsif ( $UserGroups and $user and ($function eq 'delete') ) { #Delete a user + if ( $user eq 'admin' ) { return 0 } # Disallow admin from being deleted + if ( defined($UserGroups->{user}->{$user}->{status}) ) { + delete $UserGroups->{user}->{$user}; + return 1; + } + return 0; + } elsif ( $UserGroups and $user and $group and ($function eq 'memberof') ) { #Check is user is a member of a specific group + return 1 if $UserGroups->{user}->{$user}->{group}->{$group}; + return 0; + } elsif ( $UserGroups and $user and ($function eq 'get') ) { #Get all of the groups for a specific user + return \@{ $UserGroups->{user}->{$user}->{grouplist} }; + } elsif ( $UserGroups and $user and $cmdperm and $type and ($function eq 'defaultperm') ) { #Set the default permission for a user and specific ACL type + if ( $user eq 'admin' ) { return 0 } # Disallow admin user from being modified + $UserGroups->{user}->{$user}->{defaultperm}->{$type} = $cmdperm; + store $UserGroups, $file; + return 1; + } elsif ( $UserGroups and $user and $cmdperm and ($function eq 'defaultperm') ) { #Set the default global (all types) permission for a user + if ( $user eq 'admin' ) { return 0 } # Disallow admin user from being modified + $UserGroups->{user}->{$user}->{defaultperm}->{_global_} = $cmdperm; + store $UserGroups, $file; + return 1; + } elsif ( $UserGroups and $user and $type and $cmd and ($function eq 'check') ) { #Check a CMD to see if its allowed for the user (requires ACL type) + return 'deny' unless $UserGroups->{user}->{$user}->{status}; #User is disabled + foreach my $ugroup ( @{ $UserGroups->{user}->{$user}->{grouplist} } ) { + next if ( $UserGroups->{group}->{$ugroup}->{status} == 0 ); #Group is disabled, skip it + foreach my $cmdarray ( @{ $UserGroups->{group}->{$ugroup}->{acl}->{$type} } ) { + my $matchcmd = ${ $cmdarray }[0]; + my $permission = ${ $cmdarray }[1]; + + if ( $cmd =~ /$matchcmd/ ) { + return $permission; + } elsif ( $matchcmd eq 'all' ) { + return $permission; + } + } + } + return $UserGroups->{user}->{$user}->{defaultperm}->{$type} if ( $UserGroups->{user}->{$user}->{defaultperm}->{$type} ); #Return user default permission for cmd type if defined + return $UserGroups->{user}->{$user}->{defaultperm}->{_global_} if ( $UserGroups->{user}->{$user}->{defaultperm}->{_global_} ); #Return user default permission if defined + return 'deny'; #Return deny by default + + } elsif ( $UserGroups and $user and $type and ($function eq 'fullacl') ) { #Get the full user ACL for a specific ACL type (returns array) + my @aclarray; + foreach my $ugroup ( @{ $UserGroups->{user}->{$user}->{grouplist} } ) { + next if ( $UserGroups->{group}->{$ugroup}->{status} == 0 ); #Group is disabled, skip it + foreach my $cmdarray ( @{ $UserGroups->{group}->{$ugroup}->{acl}->{$type} } ) { + my $matchcmd = ${ $cmdarray }[0]; + my $permission = ${ $cmdarray }[1]; + my $index = push @aclarray, $matchcmd; + ${ $aclarray[$index] }[0] = $permission + } + } + return \@aclarray; + } elsif ( $UserGroups and $user and ($function eq 'fullacl') ) { #Get the full user ACL for all ACL types (returns hash of arrays) + my $aclhash; + foreach my $ugroup ( @{ $UserGroups->{user}->{$user}->{grouplist} } ) { + next if ( $UserGroups->{group}->{$ugroup}->{status} == 0 ); #Group is disabled, skip it + foreach my $type ( keys %{$UserGroups->{group}->{$ugroup}->{acl}} ) { + foreach my $cmdarray ( @{ $UserGroups->{group}->{$ugroup}->{acl}->{$type} } ) { + my $matchcmd = ${ $cmdarray }[0]; + my $permission = ${ $cmdarray }[1]; + my $index = push @{ $aclhash->{$type} }, $matchcmd; + ${ ${ $aclhash->{$type} }[$index] }[0] = $permission + } + } + } + return \$aclhash; + + # Modify and get group permissions + } elsif ( $UserGroups and $group and $cmd and ($function eq 'add') and $cmdperm and $type) { #Add an allowed/denied CMD for a group my $cmdindex = 0; - if ( $UserGroups->{group}->{$group}->{$type} ) { - $cmdindex = scalar(@{ $UserGroups->{group}->{$group}->{$type} }); + if ( $UserGroups->{group}->{$group}->{acl}->{$type} ) { + $cmdindex = scalar(@{ $UserGroups->{group}->{$group}->{acl}->{$type} }); + } + my $lc = -1; + foreach my $cmdarray ( @{ $UserGroups->{group}->{$group}->{acl}->{$type} } ) { + $lc++; + my $matchcmd = ${ $cmdarray }[0]; + my $permission = ${ $cmdarray }[1]; + if ( $matchcmd eq $cmd ) { + if ( defined($line) ) { + splice @{ $UserGroups->{group}->{$group}->{acl}->{$type} }, $lc, 1; #A specific line number was specified, so delete the matching cmd + $cmdindex--; + last; + } + if ( $permission eq $cmdperm ) { return 0 } #Duplicate, dont add anything + ${ ${ $UserGroups->{group}->{$group}->{acl}->{$type} }[$lc] }[1] = $cmdperm; #update perm only + store $UserGroups, $file; + return 1; + } } - return 0 if $line > $cmdindex; - splice(@{ $UserGroups->{group}->{$group}->{$type} }, $line, 1, undef); - ${ ${ $UserGroups->{group}->{$group}->{$type} }[$line] }[0] = $cmd; - ${ ${ $UserGroups->{group}->{$group}->{$type} }[$line] }[1] = $cmdperm; + if ( defined($line) ) { #A specific line number was specified + $line = $cmdindex if $line > $cmdindex; + splice(@{ $UserGroups->{group}->{$group}->{acl}->{$type} }, $line, 0, undef); #Insert a new line on the specified line number + ${ ${ $UserGroups->{group}->{$group}->{acl}->{$type} }[$line] }[0] = $cmd; + ${ ${ $UserGroups->{group}->{$group}->{acl}->{$type} }[$line] }[1] = $cmdperm; + store $UserGroups, $file; + return 1; + } + ${ ${ $UserGroups->{group}->{$group}->{acl}->{$type} }[$cmdindex] }[0] = $cmd; + ${ ${ $UserGroups->{group}->{$group}->{acl}->{$type} }[$cmdindex] }[1] = $cmdperm; store $UserGroups, $file; return 1; - } - } elsif ( $UserGroups and $group and $cmd and $function and $cmdperm and $type) { - if ($function eq 'add') { #Add an allowed/denied CMD for a group - my $cmdindex = 0; - if ( $UserGroups->{group}->{$group}->{$type} ) { - $cmdindex = scalar(@{ $UserGroups->{group}->{$group}->{$type} }); - } - ${ ${ $UserGroups->{group}->{$group}->{$type} }[$cmdindex] }[0] = $cmd; - ${ ${ $UserGroups->{group}->{$group}->{$type} }[$cmdindex] }[1] = $cmdperm; - store $UserGroups, $file; - return 1; - } - } elsif ( $UserGroups and $group and $cmd and $type and $function ) { - if ($function eq 'check') { #Check a CMD to see if its allowed for the group - foreach my $cmdarray ( @{ $UserGroups->{group}->{$group}->{$type} } ) { - my $matchcmd = ${ $cmdarray }[0]; - my $permission = ${ $cmdarray }[1]; - - if ( $cmd =~ /$matchcmd/ ) { - return $permission; - } elsif ( $matchcmd eq 'all' ) { - return $permission; - } - } - return $UserGroups->{group}->{$group}->{defaultperm}->{$type} if ( $UserGroups->{group}->{$group}->{defaultperm}->{$type} ); #Return group default permission for cmd type if defined - return $UserGroups->{group}->{$group}->{defaultperm}->{_global_} if ( $UserGroups->{group}->{$group}->{defaultperm}->{_global_} ); #Return group default permission if defined - return 'deny'; #Return deny by default - } elsif ($function eq 'removebyname') { + } elsif ( $UserGroups and $group and $type and ($function eq 'removeall') ) { #Remove all ACL lines for an ACL type + if ( defined($UserGroups->{group}->{$group}->{acl}->{$type}) ) { + delete $UserGroups->{group}->{$group}->{acl}->{$type}; + delete $UserGroups->{group}->{$group}->{acl} unless ( keys %{ $UserGroups->{group}->{$group}->{acl} } ); + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and $group and $cmd and $type and ($function eq 'removebyname') ) { #Remove an ACL line by the CMD my $c = 0; - my $deleted = 0; - foreach my $cmdarray ( @{ $UserGroups->{group}->{$group}->{$type} } ) { - if ( ${ $cmdarray }[0] eq $cmd ) { # Delete matching cmd - splice @{ $UserGroups->{group}->{$group}->{$type} }, $c, 1; - $deleted++ - } + my $deleted = 0; + foreach my $cmdarray ( @{ $UserGroups->{group}->{$group}->{acl}->{$type} } ) { + if ( ${ $cmdarray }[0] eq $cmd ) { # Delete matching cmd + splice @{ $UserGroups->{group}->{$group}->{acl}->{$type} }, $c, 1; + $deleted++ + } $c++; } store $UserGroups, $file if $deleted; return $deleted; - } elsif ($function eq 'removebyindex') { - splice @{ $UserGroups->{group}->{$group}->{$type} }, $cmd, 1; - store $UserGroups, $file; - return 1; - } - } elsif ( $UserGroups and $group and ($function eq 'defaultperm') and $cmdperm and $type) { - $UserGroups->{group}->{$group}->{defaultperm}->{$type} = $cmdperm; - store $UserGroups, $file; - return 1; - } elsif ( $UserGroups and $group and ($function eq 'defaultperm') and $cmdperm ) { - $UserGroups->{group}->{$group}->{defaultperm}->{_global_} = $cmdperm; - store $UserGroups, $file; - return 1; - } elsif ( $UserGroups and $group ) { - if ($function eq 'get') { #Get all users in a group - foreach my $cuser ( keys %{$UserGroups->{user}} ) { - foreach my $ugroup ( keys %{$UserGroups->{user}->{$cuser}} ) { - push (@users, $cuser) if $ugroup eq $group; - } - } - return \@users; - } elsif ($function eq 'getgroupdet') { - return \$UserGroups->{group}->{$group}; - } elsif ($function eq 'add') { #Add a new group - $UserGroups->{group}->{$group}->{status} = 1; #Add the group - store $UserGroups, $file; - return 1; - } elsif ($function eq 'delete') { #Delete a group + } elsif ( $UserGroups and $group and $type and ($function eq 'removebyindex') and defined($line) ) { #Remove an ACL line by line number + my $cmdindex = 0; + if ( $UserGroups->{group}->{$group}->{acl}->{$type} ) { + $cmdindex = scalar(@{ $UserGroups->{group}->{$group}->{acl}->{$type} }); + } + return 0 if $line > $cmdindex; + + splice @{ $UserGroups->{group}->{$group}->{acl}->{$type} }, $cmd, 1; + store $UserGroups, $file; + return 1; + } elsif ( $UserGroups and $group and ($function eq 'get') ) { #Get all users in a group (returns an array) + my @users; + foreach my $cuser ( keys %{$UserGroups->{user}} ) { + push (@users, $cuser) if $UserGroups->{user}->{$cuser}->{group}->{$group}; + } + return \@users; + } elsif ( $UserGroups and $group and ($function eq 'getgroupdet') ) { + return \$UserGroups->{group}->{$group}; + } elsif ( $UserGroups and $group and ($function eq 'add') ) { #Add a new group + unless ( defined($UserGroups->{group}->{$group}->{status}) ) { + $UserGroups->{group}->{$group}->{status} = 1; #Add the group + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and $group and ($function eq 'delete') ) { #Delete a group + if ( $group eq 'admin' ) { return 0 } # Disallow admin group from being deleted delete $UserGroups->{group}->{$group}; #Delete group from group hash foreach my $cuser ( keys %{$UserGroups->{user}} ) { - foreach my $ugroup ( keys %{$UserGroups->{user}->{$cuser}} ) { - delete $UserGroups->{user}->{$user}->{$group} if $ugroup eq $group; #Delete group from users + foreach my $ugroup ( keys %{$UserGroups->{user}->{$cuser}->{group}} ) { + if ( $ugroup eq $group ) { + delete $UserGroups->{user}->{$cuser}->{group}->{$group}; #Delete group from users + my $lc = -1; + foreach my $usrgroup ( @{ $UserGroups->{user}->{$cuser}->{grouplist} } ) { + $lc++; + if ( $usrgroup eq $group ) { splice @{ $UserGroups->{user}->{$cuser}->{grouplist} }, $lc, 1 } ## Delete group from user group array + } + } } } store $UserGroups, $file; return 1; + } elsif ( $UserGroups and $group and ($function eq 'disable') ) { #Disable a group + if ( $group eq 'admin' ) { return 0 } # Disallow admin from being disabled + if ( defined($UserGroups->{group}->{$group}->{status}) && ( not $UserGroups->{group}->{$group}->{status} == 0 ) ) { + $UserGroups->{group}->{$group}->{status} = 0; + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and $group and ($function eq 'enable') ) { #Enable a group + if ( defined($UserGroups->{group}->{$group}->{status}) && ( not $UserGroups->{group}->{$group}->{status} == 1 ) ) { + $UserGroups->{group}->{$group}->{status} = 1; + store $UserGroups, $file; + return 1; + } + return 0; + } elsif ( $UserGroups and ($function eq 'getgroupnames') ) { #Get all the group names (returns array) + my @ugroups; + foreach my $ugroup ( keys %{$UserGroups->{group}} ) { + push (@ugroups, $ugroup); + } + return \@ugroups; + } elsif ( $UserGroups and ($function eq 'getgroupdet') ) { #Get all groups and their details (allowedd cmd, etc) + return \$UserGroups->{group}; + } elsif ( $UserGroups and ($function eq 'getall') ) { #Get entire hash with user and group details, for caching in MH because storable reads from a file. + return \$UserGroups; } - } elsif ( $UserGroups and ($function eq 'getgroupnames') ) { - foreach my $ugroup ( keys %{$UserGroups->{group}} ) { - push (@ugroups, $ugroup); - } - return \@ugroups; - } elsif ( $UserGroups and ($function eq 'getgroupdet') ) { #Get all groups and their details (allowedd cmd, etc) - return \$UserGroups->{group}; - } elsif ( $UserGroups and ($function eq 'getall') ) { #Get entire hash with user and group details, for caching in MH because storable reads from a file. - return \$UserGroups; - } return 0; } diff --git a/lib/json_server.pl b/lib/json_server.pl index 9cc0420ed..fa3e1c1e5 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -48,6 +48,7 @@ =head2 METHODS use IO::Compress::Gzip qw(gzip); use vars qw(%json_table); use File::Copy; +use Digest::MD5 qw(md5 md5_hex); my %json_cache; my @json_notifications = (); #noloop my $web_counter; #noloop; @@ -266,10 +267,12 @@ sub json_put { &Groups('add',$body->{name},$members); } if ($body->{pk} eq 'add_user') { - &Groups('add','',$body->{name}); - print("add_user &Groups('add','',$body->{name})\n"); + my $passwd = ""; + $passwd = $body->{password}; + $passwd = md5_hex($passwd) unless ($body->{md5} eq "true"); + &Groups('add','',$body->{name},$passwd); + print("add_user &Groups('add','',$body->{name},$passwd), [$body->{md5}]\n"); - #Add create password function foreach my $group (@{$body->{groups}}) { &Groups('add',$group,$body->{name}); print("add_user &Groups('add',$group,$body->{name})\n"); @@ -881,26 +884,59 @@ sub json_get { if ( $path[0] eq 'security' ) { #check if $Authorized - my $ref; - my $users; - my $found = 0; - if ($args{user} && $args{user}[0] ne "") { - $ref->{user} = &Groups('get','',$args{user}[0]); - #$json_data{security}{users} = - $found = 1; - } - if ($args{group} && $args{group}[0] ne "") { - $ref->{group} = &Groups('get',$args{group}[0]); - #$json_data{security}{groups} = - $found = 1; - } - if (!$found) { - $ref = ${&Groups('getall')}; + if (defined $path[1] and $path[1] eq 'authorize') { + print "IN AUTHORIZE\n"; + my $status = ""; + if ($args{user} && $args{user}[0] eq "") { + $status = "Empty Username"; + } elsif ($args{password} && $args{password}[0] eq "") { + $status = "Empty Password"; + } else { + my $password = &Groups('getpw','',$args{user}[0]); + my $time_seed = &main::time_date_stamp('18',$Time); + my $time_seedY = &main::time_date_stamp('18',$Time - 86400); + my $time_seedT = &main::time_date_stamp('18',$Time + 86400); + + #if time is between 11:55 and midnight then also check tomorrow + #if time is between midnight and 00:05 then also check yesterday + print "PW=$password, time_seed=$time_seed, $time_seedY, $time_seedT\n"; + my $pwdcheck = md5_hex($password . $time_seed); + print "PWC=$pwdcheck\n"; + + if (lc $args{password}[0] eq lc $pwdcheck) { + $status = "success"; + } else { + $status = "fail"; + } + } + $json_data{security}->{authorize} = $status; + } else { + my $ref; + my $users; + my $found = 0; + if ($args{user} && $args{user}[0] ne "") { + $ref->{user} = &Groups('get','',$args{user}[0]); + #$json_data{security}{users} = + $found = 1; + } + if ($args{group} && $args{group}[0] ne "") { + $ref->{group} = &Groups('get',$args{group}[0]); + #$json_data{security}{groups} = + $found = 1; + } + if (!$found) { + $ref = ${&Groups('getall')}; + } + #ref->{acl} = ${&Groups('fullacl')}; + print Dumper $ref; + $json_data{security} = $ref if (defined $ref); + #$json_data{security} = ${$ref} if (defined ${$ref}); + #print Dumper $json_data{security}; + } } - print Dumper $ref; - $json_data{security} = $ref if (defined $ref); - #$json_data{security} = ${$ref} if (defined ${$ref}); - #print Dumper $json_data{security}; + + if ( $path[0] eq 'authorize' ) { + # /json/security/authorize?$user=admin&password=MD5HASH } # List subroutines diff --git a/web/ia7/house/whatsnew.shtml b/web/ia7/house/whatsnew.shtml index 6f4dcc58e..e717af24e 100644 --- a/web/ia7/house/whatsnew.shtml +++ b/web/ia7/house/whatsnew.shtml @@ -3,6 +3,7 @@
+

@@ -21,19 +22,33 @@
-
+
    -
  • Faster Loading -
  • Zoneminder +
  • Faster Loading on pages +
  • Zoneminder will now generate a modal if an event is detected. Requires external utility zmeventserver
- +
+ +
+
+
    +
  • Users and Groups can be created from the GUI +
  • MH can be used as an authentication source /json/security/authorize?username=&password=MD5HASHYYYYDDMM +
+
+
+

v5.0

diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index ee7568ac4..719fc8088 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.680"; +var ia7_ver = "v2.0.690"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -98,6 +98,8 @@ function HashtoURL(URLHash) { return location.path + "#" + pairs.join('&'); } +var MD5 = function(s){function L(k,d){return(k<>>(32-d))}function K(G,k){var I,d,F,H,x;F=(G&2147483648);H=(k&2147483648);I=(G&1073741824);d=(k&1073741824);x=(G&1073741823)+(k&1073741823);if(I&d){return(x^2147483648^F^H)}if(I|d){if(x&1073741824){return(x^3221225472^F^H)}else{return(x^1073741824^F^H)}}else{return(x^F^H)}}function r(d,F,k){return(d&F)|((~d)&k)}function q(d,F,k){return(d&k)|(F&(~k))}function p(d,F,k){return(d^F^k)}function n(d,F,k){return(F^(d|(~k)))}function u(G,F,aa,Z,k,H,I){G=K(G,K(K(r(F,aa,Z),k),I));return K(L(G,H),F)}function f(G,F,aa,Z,k,H,I){G=K(G,K(K(q(F,aa,Z),k),I));return K(L(G,H),F)}function D(G,F,aa,Z,k,H,I){G=K(G,K(K(p(F,aa,Z),k),I));return K(L(G,H),F)}function t(G,F,aa,Z,k,H,I){G=K(G,K(K(n(F,aa,Z),k),I));return K(L(G,H),F)}function e(G){var Z;var F=G.length;var x=F+8;var k=(x-(x%64))/64;var I=(k+1)*16;var aa=Array(I-1);var d=0;var H=0;while(H>>29;return aa}function B(x){var k="",F="",G,d;for(d=0;d<=3;d++){G=(x>>>(d*8))&255;F="0"+G.toString(16);k=k+F.substr(F.length-2,2)}return k}function J(k){k=k.replace(/rn/g,"n");var d="";for(var F=0;F127)&&(x<2048)){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128)}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128)}}}return d}var C=Array();var P,h,E,v,g,Y,X,W,V;var S=7,Q=12,N=17,M=22;var A=5,z=9,y=14,w=20;var o=4,m=11,l=16,j=23;var U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;P

Object Log

"); $('#control').find('.modal-body').append(object_log_header); for (var i = 0; i < json_store.ia7_config.prefs.state_log_entries; i++) { + if (json_store.objects[entity].state_log == undefined) continue; if (json_store.objects[entity].state_log[i] == undefined) continue; var slog = json_store.objects[entity].state_log[i].split("set_by="); $('#control').find('.obj_log').append(slog[0]+"
"); @@ -4074,8 +4080,10 @@ var create_develop_item_modal = function(colid,col_parent) { type: 'post', contentType: 'application/json', data: JSON.stringify(data), - success: function( data, status, error ){ - console.log("data="+data+" status="+status+" error="+error); + currentUser: {user: current_user}, + success: function( data, status, error){ + var user = this.currentUser.user; + console.log("data="+data+" status="+status+" error="+error+" user="+user); //throw up red warning if the response isn't good from MH if (data.status !== undefined || data.status == "error") { var message = "Unknown server error"; @@ -4089,17 +4097,18 @@ var create_develop_item_modal = function(colid,col_parent) { $('.btn-dev-apply').addClass('disabled'); dev_changes = 0; } - data[700].user = current_user; + json_store.collections[700].user = user; }, error: function( xhr, status, error ){ var message = "Unknown ajax request error"; + var user = this.currentUser.user; var data = JSON.parse(xhr.responseText); if (data !== undefined && data.text !== undefined) message = data.text; console.log("status="+status); console.log("error="+error); $(".modal-header").append($("

 Failure: "+message+"

")); $(".write-status").delay(4000).fadeOut("slow", function () { $(this).remove(); }); - data[700].user = current_user; + json_store.collections[700].user = user; } }); }); From e0a551bdc6c0c7783ac9acbf2af18ac495d11bf6 Mon Sep 17 00:00:00 2001 From: hplato Date: Sun, 15 Jul 2018 21:42:56 -0600 Subject: [PATCH 23/78] v1.0 --- bin/get_tcp | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 bin/get_tcp diff --git a/bin/get_tcp b/bin/get_tcp new file mode 100644 index 000000000..22a2d074d --- /dev/null +++ b/bin/get_tcp @@ -0,0 +1,124 @@ +#!/usr/bin/env perl +# -*- Perl -*- + +use strict; +use IO::Socket; + +# Similar to get_url, open a socket and then get the data. Useful to spawn off as a process_item to avoid pauses + +my ( $Pgm_Path, $Pgm_Name ); + +BEGIN { + ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; + ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works +} + +my ( %config_parms, %parms ); + +use Getopt::Long; + +if ( + !&GetOptions( \%parms, 'h', 'help', 'quiet', 'timeout=s', 'rn') + or !@ARGV + or $parms{h} + or $parms{help} + ) +{ + + print < $host, +PeerPort => $port, +Timeout => $timeout, +Proto => "tcp") or $response = "get_tcp_error: opening socket: $!.\n"; +#print "error $host:$port\n" if ($tcp->connected()); + +unless ($response) { + $error = 0; + print "Sending data to $location " unless $parms{quiet}; + print "into $file" unless ($parms{quiet} or !$file); + print "..." unless $parms{quiet}; + + $tcp->send($data) or $response = "get_tcp_error: Couldn't send: $!"; + + unless ($response) { + $tcp->recv($response, 1024); + print " data retrieved\n" unless $parms{quiet}; + } else { + $error = 1; + } +} + if ($file) { + # print $data; + unless ( $file eq '/dev/null' ) { + if ($response) { + open( OUT, ">$file" ) + or die "get_tcp_error: could not open file '$file' for output: $!\n"; + binmode OUT; + print OUT $response; + close OUT; + } + else { + print " empty data response\n"; + } + } + } else { + print $response; + } + + +$tcp->close() unless ($error); + + From 3969e334be235673f0cc481cd7f63aed52beaaf0 Mon Sep 17 00:00:00 2001 From: hplato Date: Sun, 15 Jul 2018 21:43:25 -0600 Subject: [PATCH 24/78] v1.0 --- lib/Yeelight.pm | 884 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 884 insertions(+) create mode 100644 lib/Yeelight.pm diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm new file mode 100644 index 000000000..e8cf71992 --- /dev/null +++ b/lib/Yeelight.pm @@ -0,0 +1,884 @@ +package Yeelight; + +# v1.0 + +#if any effect is changed, by definition the static child should be set to off. +#cmd data returns, need to check by command +#NB: if any effect gets set, or another static is active, then other statics should be set to off. + +#effects, turn static off and clear out static_check + +#TODO + +use strict; +use warnings; + +use LWP::UserAgent; +use HTTP::Request::Common qw(POST); +use JSON::XS; +use Data::Dumper; +use Socket; +use IO::Select; +use IO::Socket::INET; + +# To set up, first pair with mobile app -- the Yeelight needs to be set up initially with the app +# to get it's wifi information. +# if problems with ios, use the android app if you can. +# MAKE SURE TO SELECT A 2.4Ghz WIRELESS NETWORK +# TURN ON LOCAL CONTROL + +# Firmware supported +# stripe : 44 + +# Yeelight Objects +# +# $Yeelight = new Yeelight('1'); +# $Yeelight_effects = new Yeelight_Effects($aurora); +# $Yeelight_comm = new Yeelight_Comm($aurora); + +# MH.INI settings +# If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used +# instead of object definitions +# +# Yeelight__location = +# Yeelight__poll = +# Yeelight__options = +# +# for example +#Yeelight_1_location = 10.10.0.20 +#Yeelight_1_token = EfgrIHH887EHhftotNNSD818erhNWHR0 +#Yeelight_1_options = 'api=beta' + +# OPTIONS +# current options that can be passed are; +# - api= +# - debug= +# - loglevel= + +# Notes +# +# The Yeelight needs to be specified as an IP address, since the module uses SSDP scan to determine +# what features are supported + +# Issues + +# + +@Yeelight::ISA = ('Generic_Item'); + +# -------------------- START OF SUBROUTINES -------------------- +# -------------------------------------------------------------- + +our %method; +$method{info} = "\"get_prop\""; #power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","flow_params","music_on","name","bg_power","bg_flowing","bg_flow_params","bg_ct","bg_lmode","bg_bright","bg_rgb","bg_hue","bg_sat","nl_br"]}\r\n); +$method{on} = "\"set_power\""; +$method{off} = "\"set_power\""; +$method{brightness} = "\"set_bright\""; +$method{rgb} = "\"set_rgb\""; +$method{hsv} = "\"set_hsv\""; +$method{ct} = "\"set_ct_abx\""; + +my %param_array; +@{$param_array{info}} = ("power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","flow_params","music_on","name","bg_power","bg_flowing","bg_flow_params","bg_ct","bg_lmode","bg_bright","bg_rgb","bg_hue","bg_sat","nl_br"); +@{$param_array{brightness}} = ("smooth",500); +@{$param_array{on}} = ("on","smooth",500); +@{$param_array{off}} = ("on","smooth",500); + +our %active_yeelights = (); + +sub new { + my ( $class, $id, $location, $poll, $options ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $self->{id} = "1"; + $self->{id} = $id if ((defined $id) and ($id)); + + $self->{name} = "1"; + $self->{name} = $id if ((defined $id) and ($id)); + + $self->{data} = undef; + $self->{child_object} = undef; + $self->{config}->{poll_seconds} = 10; + $self->{config}->{poll_seconds} = $::config_parms{ "yeelight_" . $self->{name} . "_poll" } + if ( defined $::config_parms{ "yeelight_" . $self->{name} . "_poll" } ); + $self->{config}->{poll_seconds} = $poll if ($poll); + $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{updating} = 0; + $self->{data}->{retry} = 0; + $self->{status} = ""; + $self->{module_version} = "v1.0"; + $self->{ssdp_timeout} = 1000; + $self->{last_static} = ""; + $self->{host} = $location; + $self->{port} = 55443; + + if ($location =~ m/:/) { + ($self->{host}, $self->{port}) = $location =~ /(.*):(.*)/; + } + + $options = "" unless ( defined $options ); + $options = $::config_parms{ "yeelight_" . $self->{name} . "_options" } if ( $::config_parms{ "yeelight_" . $self->{name} . "_options" } ); + + $self->{debug} = 4; + ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) if ( $options =~ m/debug\=/i ); + $self->{debug} = 0 if ( $self->{debug} < 0 ); + + $self->{loglevel} = 5; + ( $self->{loglevel} ) = ( $options =~ /loglevel\=(\d+)/i ) if ($options =~ m/loglevel\=/i ); + + $self->{poll_data_timestamp} = 0; + $self->{max_poll_queue} = 3; + $self->{max_cmd_queue} = 5; + $self->{cmd_process_retry_limit} = 6; + + @{ $self->{poll_queue} } = (); + $self->{poll_data_file} = "$::config_parms{data_dir}/Yeelight_poll_" . $self->{name} . ".data"; + unlink "$::config_parms{data_dir}/Yeelight_poll_" . $self->{name} . ".data"; + $self->{poll_process} = new Process_Item; + $self->{poll_process}->set_output( $self->{poll_data_file} ); + @{ $self->{cmd_queue} } = (); + $self->{cmd_data_file} = "$::config_parms{data_dir}/Yeelight_cmd_" . $self->{name} . ".data"; + unlink "$::config_parms{data_dir}/Yeelight_cmd_" . $self->{name} . ".data"; + $self->{cmd_process} = new Process_Item; + $self->{cmd_process}->set_output( $self->{cmd_data_file} ); + $self->{init} = 0; + $self->{init_data} = 0; + $self->{init_v_cmd} = 0; + &::MainLoop_post_add_hook( \&Yeelight::process_check, 0, $self ); + &::Reload_post_add_hook( \&Yeelight::generate_voice_commands, 1, $self ); + $self->get_data(); + #push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); + push( @{ $$self{states} }, 'off'); + for my $i (1..99) { push @{ $$self{states} }, "$i%"; } + push( @{ $$self{states} }, 'on'); + $self->{timer} = new Timer; + $self->start_timer; + return $self; +} + +sub start_timer { + my ($self) = @_; + unless ( defined $self->{timer} ) { + $self->{timer} = new Timer; #HP: why do timers get undefined?? + } + if ( defined $self->{timer} ) { + $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &Yeelight::get_data($self) }, -1 ); + } + else { + main::print_log( "[Yeelight:" . $self->{name} . "] Warning, start_timer called but timer undefined" ); + } +} + +sub get_data { + my ($self) = @_; + + main::print_log( "[Yeelight:" . $self->{name} . "] get_data initiated" ) if ( $self->{debug} ); + + #Check that we have data + + if ( $self->{init} == 0 ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Contacting Yeelight for configuration details..." ); + $self->get_ssdp_data($self->{ssdp_timeout}); + } + + if ( $self->{data}->{info}->{Location} ) { + + if ( ( defined $self->{data}->{info}->{model} ) and ( $self->{init} == 0 ) ) { + main::print_log( "[Yeelight:" . $self->{name} . "] " . $self->{module_version} . " Configuration Loaded" ); + $active_yeelights{ $self->{host} } = 1; + $self->print_info(); + $self->{init} = 1; + } + $self->poll(); + + } + else { + main::print_log( "[Yeelight:" . $self->{name} . "] WARNING, Did not poll: location: $self->{location}" ) if ( $self->{debug} ); + } +} + + +sub poll { + my ($self) = @_; + + main::print_log( "[Yeelight:" . $self->{name} . "] Background Polling initiated" ) if ( $self->{debug} ); + $self->_get_TCP_data('info'); + + return ('1'); +} + +sub process_check { + my ($self) = @_; + + return unless ( defined $self->{poll_process} ); + + if ( $self->{poll_process}->done_now() ) { + + #shift @{ $self->{poll_queue} }; #remove the poll since they are expendable. + @{ $self->{poll_queue} } = (); #clear the queue since process is done. + + my $com_status = "online"; + main::print_log( "[Yeelight:" . $self->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed" ) if ( $self->{debug} ); + + my $file_data = &main::file_read( $self->{poll_data_file} ); + + return unless ($file_data); #if there is no data, then don't process + if ( $file_data =~ m/^get_tcp_error:/i ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Data retrieval error: $file_data" ); + return; + } + + # Clean out the characters before and after the json since the parser can crash + print "debug: file_data=$file_data\n" if ( $self->{debug} > 2); + my ($json_data) = $file_data =~ /({.*})/; + print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); + unless ( ($file_data) and ($json_data) ) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! bad data returned by poll" ); + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); + return; + } + my $data; + eval { $data = JSON::XS->new->decode($json_data); }; + + # catch crashes: + if ($@) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; + } + else { + if ( keys %{$data} ) { + + my $index = 0; + foreach my $item (@{$param_array{info}}) { + $self->{data}->{info}->{$item} = $data->{result}[$index] unless ($data->{result}[$index] eq ""); + $index++; + } + + $self->process_data(); + } + else { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! Returned data not structured! Not processing..." ); + $com_status = "offline"; + } + } + + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } + + return unless ( defined $self->{cmd_process} ); + if ( $self->{cmd_process}->done_now() ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Background Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); + $self->get_data(); #poll since the command is done to get a new state + + my $file_data = &main::file_read( $self->{cmd_data_file} ); + my $com_status = "online"; + + if ($file_data) { + + #for some reason get_url adds garbage to the output. Clean out the characters before and after the json + print "debug: file_data=$file_data\n" if ( $self->{debug} > 2); + my ($json_data) = $file_data =~ /({.*})/; + print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); + my $data; + eval { $data = JSON::XS->new->decode($json_data); }; + + # catch crashes: + if ($@) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; + } + else { +shift @{ $self->{cmd_queue} }; +print "***RESULTS***"; +print Dumper $data; + if ($data->{results} eq 'ok') { + shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful + $self->{cmd_process_retry} = 0; + $com_status = "online"; + + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] Last command failed! Going to retry" ); + } + $self->poll; + } + + if ( scalar @{ $self->{cmd_queue} } ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue found" ); + my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) + if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); + } + + } + else { + + main::print_log( "[Yeelight:" . $self->{name} . "] WARNING Issued command was unsuccessful, retrying..." ); + if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); + shift @{ $self->{cmd_queue} }; + $self->{cmd_process_retry} = 0; + $com_status = "offline"; + } + else { + $self->{cmd_process_retry}++; + } + } + + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } +} + +#polling process. Keep it separate from commands +sub _get_TCP_data { + my ( $self, $mode, $params ) = @_; + #{"id":1,"method":"get_prop","params":["power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","flow_params","music_on","name","bg_power","bg_flowing","bg_flow_params","bg_ct","bg_lmode","bg_bright","bg_rgb","bg_hue","bg_sat","nl_br"]}\r\n); + my $cmdline = "{\"id\":" . $self->{id} . ",\"method\":" . $method{$mode} . ",\"params\":["; + foreach my $item (@{$param_array{$mode}}) { + $cmdline .= '"' . $item . '",'; + } + chop($cmdline); + $cmdline .= "]}"; + my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; + if ( $self->{poll_process}->done() ) { + $self->{poll_process}->set($cmd); + $self->{poll_process}->start(); + $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{poll_process_mode} = $mode; + main::print_log( "[Yeelight:" . $self->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd" ) if ( $self->{debug} ); + } + else { + if ( scalar @{ $self->{poll_queue} } < $self->{max_poll_queue} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Queue is " . scalar @{ $self->{poll_queue} } . ". Queing command $mode, $cmd" ) + if ( $self->{debug} ); + push @{ $self->{poll_queue} }, "$mode|$cmd"; + } + else { + #the queue has grown past the max, so it might be down. Since polls are expendable, just don't do anything + #when the aurora is back it will process the backlog, and as soon as a poll is processed, the queue is cleared. + } + } +} + +#command process +sub _push_TCP_data { + my ( $self, $mode, @params ) = @_; + + my $cmdline = "{\"id\":" . $self->{id} . ",\"method\":" . $method{$mode} . ",\"params\":["; + foreach my $item (@params) { + $cmdline .= '"' . $item . '",'; + } + chop($cmdline); + $cmdline .= "]}"; + my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; + +print "***cmd=$cmd\n"; + + if ( $self->{cmd_process}->done() ) { + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{cmd_process_mode} = $mode; + push @{ $self->{cmd_queue} }, "$cmd"; + + main::print_log( "[Yeelight:" . $self->{name} . "] Backgrounding " . $self->{cmd_process}->pid() . " command $mode, $cmd" ) if ( $self->{debug} ); + } + else { + if ( scalar @{ $self->{cmd_queue} } < $self->{max_cmd_queue} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Queue is " . scalar @{ $self->{cmd_queue} } . ". Queing command $mode, $cmd" ) + if ( $self->{debug} ); + push @{ $self->{cmd_queue} }, "$cmd"; + } + else { + main::print_log( "[Yeelight:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne "offline" ) { + main::print_log "[Yeelight:" + . $self->{name} + . "] Communication Tracking object found. Updating from " + . $self->{child_object}->{comm}->state() + . " to offline..." + if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } + } + } +} + +sub get_ssdp_data { + my ( $self, $id, $timeout ) = @_; + + #return a location that isn't in the $active_auroras hash + + my ( $data ) = scan_ssdp_data($timeout); + if (defined $data and defined $data->{$self->{host}}) { + &main::print_log( "[Yeelight:" . $self->{name} . "] SSDP scan found device $self->{host}!"); +#TODO Cleanupthere probably is a better way to copy in a hash + $self->{data}->{info} = $data->{$self->{host}}; + #foreach my $key (keys %{$data->{$self->{host}}}) { + # print "**key = $key, $data->{$self->{host}}->{$key}\n"; + # $self->{data}->{info}->{$key} = $data->{$self->{host}}->{$key}; + #} + } else { + &main::print_log( "[Yeelight:" . $self->{name} . "] Warning, SSDP did not locate yeelight $self->{host}. Retrying."); + } + return; +} + +sub scan_ssdp_data { + my ($timeout) = @_; + $timeout = 500 unless ($timeout); + my %yl = (); + + my $CAST = '239.255.255.250'; + my $PORT = 1982; + ################################################################################ + my $msg =<add($sock); + + my $data; + my $i; + my $count = 0; + &main::print_log( "[Yeelight] Discovering >" ); + while ($i++ < $timeout) { + select undef, undef, undef, .1; + + my @ready = $sel->can_read(2); + last unless scalar @ready; + + recv($sock,$data, 65536,0); + $count++; + &main::print_log( "[Yeelight] Receiving $count ($i) "); + my ($location) = $data =~ /Location:\syeelight:\/\/(.*)/; + $location =~ s/[^a-zA-Z0-9\:\.\/]*//g; + if ($location) { + my ($host, $port) = $location =~ /(.*):(.*)/; + $yl{$host}->{host} = $host; + $yl{$host}->{port} = $port; + #Go through the rest of the data + foreach my $line (split(/\n/,$data)) { + my ($field, $value) = $line =~ /(.*)\:\s+(.*)/; + next if (!defined $field or $field =~ m/^Location:/); + next unless ($value); + $value =~ s/[^a-zA-Z 0-9\:\.\/]*//g; + if ($field eq "support") { + @{$yl{$host}->{features}} = split(/ /,$value); + } else { + $yl{$host}->{$field} = $value; + } + } + $yl{$host}->{name} = "" unless (defined $yl{$host}->{name}); + } + } + return \%yl; + } + + +sub register { + my ( $self, $object, $type ) = @_; + my $keys = ""; + + #allow for multiple static entries + if ( lc $type eq 'static' ) { + if ( defined $self->{child_object}->{static} ) { + $keys = keys %{ $self->{child_object}->{static} }; + } + else { + $keys = 0; + } + $self->{child_object}->{static}->{$keys} = $object; + } + else { + $self->{child_object}->{$type} = $object; + } + $type .= " (" . $keys . ")" if ( $keys ne "" ); + &main::print_log( "[Yeelight:" . $self->{name} . "] Registered $type child object" ); + +} + +sub stop_timer { + my ($self) = @_; + + if ( defined $self->{timer} ) { + $self->{timer}->stop() if ( $self->{timer}->active() ); + } + else { + main::print_log( "[Yeelight:" . $self->{name} . "] Warning, stop_timer called but timer undefined" ); + } +} + +sub print_info { + my ($self) = @_; + my $name = $self->{data}->{info}->{name}; + $name = "Not Set" if ($self->{data}->{info}->{name} eq ""); + + main::print_log( "[Yeelight:" . $self->{name} . "] Name: " . $name ); + main::print_log( "[Yeelight:" . $self->{name} . "] Model: " . $self->{data}->{info}->{model} ); + main::print_log( "[Yeelight:" . $self->{name} . "] Firmware: " . $self->{data}->{info}->{fw_ver} ); + + + main::print_log( "[Yeelight:" . $self->{name} . "] MH Module version: " . $self->{module_version} ); + main::print_log( "[Yeelight:" . $self->{name} . "] *** DEBUG MODE ENABLED ***") if ( $self->{debug} ); + + main::print_log( "[Yeelight:" . $self->{name} . "] -- Current Settings --" ); + + main::print_log( "[Yeelight:" . $self->{name} . "] State:\t\t " . $self->{data}->{info}->{power} ); + if ($self->{data}->{info}->{color_mode} == 1) { + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t rgb mode"); + } elsif ($self->{data}->{info}->{color_mode} == 2) { + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t color temperature mode"); + } elsif ($self->{data}->{info}->{color_mode} == 3) { + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t hsv mode"); + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t Unknown mode: " . $self->{data}->{info}->{color_mode}); + } + main::print_log( "[Yeelight:" . $self->{name} . "] Brightness:\t " . $self->{data}->{info}->{bright} ); + #rgb = red * 65536 + green * 256 + blue + my ($r_red, $r_green, $r_blue) = $self->get_rgb(); + main::print_log( "[Yeelight:" . $self->{name} . "] RGB:\t\t " . $self->{data}->{info}->{rgb} ); + main::print_log( "[Yeelight:" . $self->{name} . "] \tRed: " . $r_red ); + main::print_log( "[Yeelight:" . $self->{name} . "] \tGreen: " . $r_green ); + main::print_log( "[Yeelight:" . $self->{name} . "] \tBlue: " . $r_blue ); + + main::print_log( "[Yeelight:" . $self->{name} . "] Hue:\t\t " . $self->{data}->{info}->{hue} ); + main::print_log( "[Yeelight:" . $self->{name} . "] Saturation:\t " . $self->{data}->{info}->{sat} ); + main::print_log( "[Yeelight:" . $self->{name} . "] Color Temp:\t " . $self->{data}->{info}->{ct} ); + main::print_log( "[Yeelight:" . $self->{name} . "] -- Enabled Features --" ); + + foreach my $feature ( @{ $self->{data}->{info}->{features} } ) { + main::print_log( "[Yeelight:" . $self->{name} . "] - $feature" ); + } + +} + +sub process_data { + my ($self) = @_; + + + # Main core of processing + # set state of self for state + # for any registered child selfs, update their state if + + main::print_log( "[Yeelight:" . $self->{name} . "] Processing Data..." ) if ( $self->{debug} ); + + if ( ( !$self->{init_data} ) and ( defined $self->{data}->{info} ) ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Init: Setting startup values" ); + + foreach my $key ( keys %{$self->{data}->{info}} ) { + $self->{previous}->{info}->{$key} = $self->{data}->{info}->{$key}; + } + + if ( ( $self->{data}->{info}->{power} eq 'on') and ( $self->{data}->{info}->{bright} != 100 ) ) { + $self->set( $self->{data}->{info}->{bright}, 'poll' ); + } + else { + $self->set( $self->{data}->{info}->{power}, 'poll' ); + } + $self->{init_data} = 1; + } + + if ( $self->{previous}->{info}->{fw_ver} ne $self->{data}->{info}->{fw_ver} ) { + main::print_log( + "[Yeelight:" . $self->{name} . "] Firmware changed from $self->{previous}->{info}->{fw_ver} to $self->{data}->{info}->{fw_ver}" ); + main::print_log( "[Yeelight:" . $self->{name} . "] This really isn't a regular operation. Should check Yeelight to confirm" ); + $self->{previous}->{info}->{fw_ver} = $self->{data}->{info}->{fw_ver}; + } + + if ( $self->{previous}->{info}->{name} ne $self->{data}->{info}->{name} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Device Name changed from $self->{previous}->{info}->{name} to $self->{data}->{info}->{name}" ); + $self->{previous}->{info}->{name} = $self->{data}->{info}->{name}; + } + + if ( $self->{previous}->{info}->{power} ne $self->{data}->{info}->{power} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] State changed from $self->{previous}->{info}->{power} to $self->{data}->{info}->{power}" ) if ( $self->{loglevel} ); + $self->{previous}->{info}->{power} = $self->{data}->{info}->{power}; + + #if on and brightness not 100 set brightness else set on or off + if ( ( $self->{data}->{info}->{power} eq "on" ) and ( $self->{data}->{info}->{bright} != 100 ) ) { + $self->set( $self->{data}->{info}->{bright}, 'poll' ); + } + else { + $self->set( $self->{data}->{info}->{power}, 'poll' ); + } + } + + if ( $self->{previous}->{info}->{bright} != $self->{data}->{info}->{bright} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Brightness changed from $self->{previous}->{info}->{state}->{brightness}->{value} to $self->{data}->{info}->{state}->{brightness}->{value}" ) if ( $self->{loglevel} ); + $self->{previous}->{info}->{bright} = $self->{data}->{info}->{bright}; + $self->set( $self->{data}->{info}->{bright}, 'poll' ); + } +#TODO convert the rest + + if ( $self->{previous}->{info}{color_mode} != $self->{data}->{info}->{color_mode} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] State Color Mode changed from $self->{previous}->{info}->{color_mode} to $self->{data}->{info}->{color_mode}" ) if ( $self->{loglevel} ); + $self->{previous}->{info}->{color_mode} = $self->{data}->{info}->{color_mode}; + } + +} + +sub print_command_queue { + my ($self) = @_; + main::print_log( "Yeelight:" . $self->{name} . "] ------------------------------------------------------------------" ); + my $commands = scalar @{ $self->{cmd_queue} }; + my $name = "$commands commands"; + $name = "empty" if ($commands == 0); + main::print_log( "Yeelight:" . $self->{name} . "] Current Command Queue: $name" ); + for my $i ( 1 .. $commands ) { + main::print_log( "Yeelight:" . $self->{name} . "] Command $i: " . @{ $self->{cmd_queue} }[$i - 1] ); + } + main::print_log( "Yeelight:" . $self->{name} . "] ------------------------------------------------------------------" ); + +} + +sub purge_command_queue { + my ($self) = @_; + my $commands = scalar @{ $self->{cmd_queue} }; + main::print_log( "Aurora:" . $self->{name} . "] Purging Command Queue of $commands commands" ); + @{ $self->{cmd_queue} } = (); +} + +#------------ +# User access methods + +sub get_debug { + my ($self) = @_; + return $self->{debug}; +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $p_state .= "%" if ( defined $p_state and $p_state =~ m/\d+(?!%)/ ); + main::print_log( "[Yeelight:" . $self->{name} . "] DB super::set, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); + $self->SUPER::set($p_state); + $self->start_timer; + + } + else { + main::print_log( "[Yeelight:" . $self->{name} . "] DB set_mode, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); + my $mode = lc $p_state; + if ( ( $mode eq "on" ) or ( $mode eq "off" ) ) { + $self->_push_TCP_data($mode, @{$param_array{$mode}}); + } + elsif ( $mode =~ /^(\d+)/ ) { + my @params = @{$param_array{$mode}}; + unshift @params, $1; + $self->_push_TCP_data( 'brightness', @params ); + } + elsif ( $mode =~ /^([-+]\d+)/ ) { + my @params = @{$param_array{$mode}}; + unshift @params, $self->{info}->{bright} + $1; + $self->_push_TCP_data( 'brightness', @params ); + } + else { + main::print_log( "Yeelight:" . $self->{name} . "] Error, unknown set state $p_state" ); + return ('0'); + } + return ('1'); + } +} + +sub get_rgb { + my ($self) = @_; + my $red = int($self->{data}->{info}->{rgb} / 65536); + my $green = int(($self->{data}->{info}->{rgb} - ($red * 65536)) / 256); + my $blue = $self->{data}->{info}->{rgb} - ($red * 65536) - ($green * 256); + return ($red, $green, $blue); +} + +sub set_hsv { + my ( $self, $h, $s, $v ) = @_; +} + +sub set_rgb { + my ( $self, $r, $g, $b ) = @_; + my ( $cred, $cgreen, $cblue) = $self->get_rbg(); + $r = $cred unless ($r); + $g = $cgreen unless ($g); + $b = $cblue unless ($b); + my $value = ($r * 65536) + ($g * 265) + $b; +#TODO $self->_push_TCP_data( 'brightness', $params ); +} + +sub set_ct { + my ( $self, $ct ) = @_; +} + +sub generate_voice_commands { + my ($self) = @_; + + if ($self->{init_v_cmd} == 0) { + my $object_string; + my $object_name = $self->get_object_name; + $self->{init_v_cmd} = 1; + &main::print_log("Generating Voice commands for Nanoleaf Aurora Controller $object_name"); + + my $voice_cmds = $self->get_voice_cmds(); + my $i = 1; + foreach my $cmd ( keys %$voice_cmds ) { + + #get object name to use as part of variable in voice command + my $object_name_v = $object_name . '_' . $i . '_v'; + $object_string .= "use vars '${object_name}_${i}_v';\n"; + + #Convert object name into readable voice command words + my $command = $object_name; + $command =~ s/^\$//; + $command =~ tr/_/ /; + + #Initialize the voice command with all of the possible device commands + $object_string .= $object_name . "_" . $i . "_v = new Voice_Cmd '$command $cmd';\n"; + + #Tie the proper routine to each voice command + $object_string .= $object_name . "_" . $i . "_v -> tie_event('" . $voice_cmds->{$cmd} . "');\n\n"; #, '$command $cmd');\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', 'Nanoleaf_Aurora', 'Controller_commands' ); + $i++; + } + + #Evaluate the resulting object generating string + package main; + eval $object_string; + print "Error in nanoleaf_Yeelight_item_commands: $@\n" if $@; + + package Nanoleaf_Aurora; + } +} + +sub get_voice_cmds { + my ($self) = @_; + my %voice_cmds = ( + 'Print Command Queue to print log' => $self->get_object_name . '->print_command_queue', + 'Purge Command Queue' => $self->get_object_name . '->purge_command_queue' + + ); + + return \%voice_cmds; +} + + + +package Yeelight_Red; + +@Nanoleaf_Yeelight_Static::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $static_string ) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + @{ $$self{states} } = ( 'on', 'off' ); + + $$self{master_object} = $object; + $$self{loop} = 0; + $$self{string} = $static_string if ( defined $static_string ); + $object->register( $self, 'rgb-red' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + #if ON then backup current configuration and set the new one. + if ( lc $p_state eq 'on' ) { + $$self{previous_effect} = $$self{master_object}->get_effect(); + $$self{master_object}->set_static( $$self{string} ); + + #if OFF then check if current is same as static, and if so restore the old one. + } + elsif ( lc $p_state eq 'off' ) { + $$self{master_object}->check_static( $$self{string}, $$self{previous_effect} ); + } + else { + main::print_log("[Aurora Static] Error. Unknown set mode $p_state"); + } + } +} + + + +package Yeelight_Comm; + +@Nanoleaf_Yeelight_Comm::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + + $$self{master_object} = $object; + push( @{ $$self{states} }, 'online', 'offline' ); + $object->register( $self, 'comm' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } +} + +1; + +# Version History +# v1.0.0 - initial module +# v1.0.1 - initial static support +# v1.0.2 - multiple auroras, brightness +# v1.0.3 - working static. turn on, overrides effect, turning off will restore previous effect +# v1.0.4 - Voice Commands +# v1.0.5 - working multi static +# v1.0.6 - better processing +# v1.0.7 - ability to specify API as an option +# v1.0.8 - initial v1.5.0 API v1 support +# v1.0.9 - use config_parms (mh.ini) instead of dedicated config file +# v1.0.10 - Updated to work with other versions of perl, typo with mh.ini +# v1.0.11 - cosmetic fixes for undefined variables +# v1.0.12 - get_effects method to get array of available effects +# v1.0.13 - ability to print and purge the command queue in case a network error prevents clearing, empty poll queue if max reached +# v1.0.14 - commands now queue properly +# v1.0.15 - fixed polling +# v1.1.01 - firmware v2.2.0 and rhythm module \ No newline at end of file From 022784f63d1063264580a7acb0beac4d885e5ba9 Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 16 Jul 2018 18:44:04 -0600 Subject: [PATCH 25/78] v1.1.03 - fixed a few typos --- lib/Nanoleaf_Aurora.pm | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index e6ccc5b1a..757e01c4a 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -1,6 +1,6 @@ package Nanoleaf_Aurora; -# v1.1.01 +# v1.1.03 #if any effect is changed, by definition the static child should be set to off. #cmd data returns, need to check by command @@ -129,7 +129,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.1.01"; + $self->{module_version} = "v1.1.03"; $self->{ssdp_timeout} = 4000; $self->{last_static} = ""; @@ -721,7 +721,6 @@ sub print_info { } else { main::print_log( "[Aurora:" . $self->{name} . "] Rhythm Module: Not Present"); } - main::print_log( "[Aurora:" . $self->{name} . "] Firmware: " . $self->{data}->{info}->{firmwareVersion} ); main::print_log( "[Aurora:" . $self->{name} . "] Connected Panels: " . $self->{data}->{panels} ); main::print_log( "[Aurora:" . $self->{name} . "] Panel Size: " . $self->{data}->{panel_size} ); @@ -763,9 +762,9 @@ sub print_info { main::print_log( "[Aurora:" . $self->{name} . "] Color Temp:\t " - . $self->{data}->{info}->{state}->{brightness}->{value} . "\t[" - . $self->{data}->{info}->{state}->{brightness}->{min} . "-" - . $self->{data}->{info}->{state}->{brightness}->{max} + . $self->{data}->{info}->{state}->{ct}->{value} . "\t[" + . $self->{data}->{info}->{state}->{ct}->{min} . "-" + . $self->{data}->{info}->{state}->{ct}->{max} . "]" ); main::print_log( "[Aurora:" . $self->{name} . "] -- Active Effects --" ); if ( defined $self->{data}->{info}->{effects}->{list} ) { @@ -1063,8 +1062,6 @@ sub check_static { sub is_rhythm_effect { my ( $self) = @_; my $return = 0; - print "DB \$self->{data}->{info}->{rhythm}->{rhythmActive} = $self->{data}->{info}->{rhythm}->{rhythmActive}\n"; - print "DB \$self->{data}->{info}->{rhythm}->{rhythmMode} = $self->{data}->{info}->{rhythm}->{rhythmMode}\n"; $return = 1 if ($self->{data}->{info}->{rhythm}->{rhythmActive}); return $return; @@ -1334,3 +1331,4 @@ sub set { # v1.0.14 - commands now queue properly # v1.0.15 - fixed polling # v1.1.01 - firmware v2.2.0 and rhythm module +# v1.1.03 - fixed a few typos \ No newline at end of file From 760fda5a67494bc6a154ceafa2306ebe65959909 Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 16 Jul 2018 18:47:50 -0600 Subject: [PATCH 26/78] v1.0.1 - basic and color control --- lib/Yeelight.pm | 438 +++++++++++++++++++++++++++++++----------------- 1 file changed, 281 insertions(+), 157 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index e8cf71992..64f5e1e74 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,6 +1,6 @@ package Yeelight; -# v1.0 +# v1.0.1 #if any effect is changed, by definition the static child should be set to off. #cmd data returns, need to check by command @@ -32,28 +32,19 @@ use IO::Socket::INET; # Yeelight Objects # -# $Yeelight = new Yeelight('1'); -# $Yeelight_effects = new Yeelight_Effects($aurora); -# $Yeelight_comm = new Yeelight_Comm($aurora); - -# MH.INI settings -# If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used -# instead of object definitions -# -# Yeelight__location = -# Yeelight__poll = -# Yeelight__options = -# -# for example -#Yeelight_1_location = 10.10.0.20 -#Yeelight_1_token = EfgrIHH887EHhftotNNSD818erhNWHR0 -#Yeelight_1_options = 'api=beta' - +# $yeelight = new Yeelight('1'); +# $yeelight_comm = new Yeelight_Comm($yeelight); +# $yeelight_ct = new Yeelight_Colortemp($yeelight); +# $yeelight_rgb = new Yeelight_RGB($yeelight); +# $yeelight_red = new Yeelight_red($yeelight); +# $yeelight_blue = new Yeelight_blue($yeelight); +# $yeelight_green = new Yeelight_green($yeelight); +# individual color options are provided to allow for web slider control. +# yeelight_rgb the set value is 'red, green, blue' +#ie $yeelight_rgb->set('255,10,32'); +# yeelight_ct has some presets, $yeelight_ct->set_warm # OPTIONS -# current options that can be passed are; -# - api= -# - debug= -# - loglevel= + # Notes # @@ -79,15 +70,16 @@ $method{hsv} = "\"set_hsv\""; $method{ct} = "\"set_ct_abx\""; my %param_array; -@{$param_array{info}} = ("power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","flow_params","music_on","name","bg_power","bg_flowing","bg_flow_params","bg_ct","bg_lmode","bg_bright","bg_rgb","bg_hue","bg_sat","nl_br"); -@{$param_array{brightness}} = ("smooth",500); -@{$param_array{on}} = ("on","smooth",500); -@{$param_array{off}} = ("on","smooth",500); +@{$param_array{info}} = ('"power"','"bright"','"ct"','"rgb"','"hue"','"sat"','"color_mode"','"flowing"','"delayoff"','"flow_params"','"music_on"','"name"','"bg_power"','"bg_flowing"','"bg_flow_params"','"bg_ct"','"bg_lmode"','"bg_bright"','"bg_rgb"','"bg_hue"','"bg_sat"','"nl_br"'); +@{$param_array{bright}} = ('"smooth"',500); +@{$param_array{on}} = ('"on"','"smooth"',500); +@{$param_array{off}} = ('"off"','"smooth"',500); +@{$param_array{rgb}} = ('"smooth"',500); our %active_yeelights = (); sub new { - my ( $class, $id, $location, $poll, $options ) = @_; + my ( $class, $id, $location, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; $self->{id} = "1"; @@ -98,15 +90,14 @@ sub new { $self->{data} = undef; $self->{child_object} = undef; - $self->{config}->{poll_seconds} = 10; - $self->{config}->{poll_seconds} = $::config_parms{ "yeelight_" . $self->{name} . "_poll" } - if ( defined $::config_parms{ "yeelight_" . $self->{name} . "_poll" } ); - $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); +# $self->{config}->{poll_seconds} = 10; +# $self->{config}->{poll_seconds} = $::config_parms{ "yeelight_" . $self->{name} . "_poll" } +# if ( defined $::config_parms{ "yeelight_" . $self->{name} . "_poll" } ); +# $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.0"; + $self->{module_version} = "v1.0.1"; $self->{ssdp_timeout} = 1000; $self->{last_static} = ""; $self->{host} = $location; @@ -144,28 +135,84 @@ sub new { $self->{init} = 0; $self->{init_data} = 0; $self->{init_v_cmd} = 0; + $self->{data_socket} = new Socket_Item(undef, undef, "$self->{host}:$self->{port}", "yeelight" . $self->{id}, 'tcp', 'raw'); + $self->{recon_timer} = new Timer; + $self->{reconnect_time} = 10; &::MainLoop_post_add_hook( \&Yeelight::process_check, 0, $self ); + &::MainLoop_post_add_hook( \&Yeelight::check_for_socket_data, 0, $self ); &::Reload_post_add_hook( \&Yeelight::generate_voice_commands, 1, $self ); - $self->get_data(); #push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); push( @{ $$self{states} }, 'off'); for my $i (1..99) { push @{ $$self{states} }, "$i%"; } push( @{ $$self{states} }, 'on'); - $self->{timer} = new Timer; - $self->start_timer; + $self->{timer} = new Timer; + $self->get_data(); return $self; } -sub start_timer { +sub check_for_socket_data { my ($self) = @_; - unless ( defined $self->{timer} ) { - $self->{timer} = new Timer; #HP: why do timers get undefined?? - } - if ( defined $self->{timer} ) { - $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &Yeelight::get_data($self) }, -1 ); + +# if ($Socket_Items{$instance}{'socket'}->active) { +# $NewCmd = $Socket_Items{$instance}{'socket'}->said; +# } else { +# # restart the TCP connection if its lost. +# if ($Socket_Items{$instance}{recon_timer}->inactive) { +# ::print_log("Connection to $instance instance of AD2 was lost, I will try to reconnect in $$self{reconnect_time} seconds"); +# # ::logit("AD2.pm ser2sock connection lost! Trying to reconnect." ); +# $Socket_Items{$instance}{recon_timer}->set($$self{reconnect_time}, sub { +# $Socket_Items{$instance}{'socket'}->start; +# }); +# } +# } +# } + my $com_status = "online"; + if ($self->{data_socket}->active) { + my $rec_data = $self->{data_socket}->said; + return if (!defined $rec_data or $rec_data eq ""); + $rec_data =~ s/\r\n//g; + print "debug: rec_data=$rec_data\n" if ( $self->{debug} > 2); + my ($json_data) = $rec_data =~ /({.*})/; + print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); + unless ( ($rec_data) and ($json_data) ) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! bad data returned by socket" ); + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! received data is [$rec_data]. json data is [$json_data]" ); + $com_status = "offline"; + } else { + my $data; + main::print_log( "[Yeelight:" . $self->{name} . "] Data Received [$rec_data]" ); + + eval { $data = JSON::XS->new->decode($json_data); }; + + # catch crashes: + if ($@) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! JSON data parser crashed! $@\n" ); + } else { + if ($data->{method} eq "props") { + foreach my $key (keys %{$data->{params}}) { + $self->{data}->{info}->{$key} = $data->{params}->{$key}; + } + $self->process_data(); + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR. Expected method props, recieved $data-{method}" ); + } + } + } + + } else { + if ($self->{init}) { + main::print_log( "[Yeelight:" . $self->{name} . "] Lost connection to Yeelight. Trying to connect again in $self->{reconnect} seconds" ); + $self->{recon_timer}->set($self->{reconnect_time}, sub {$self->{data_socket}->start;}); + $com_status = "offline"; + } } - else { - main::print_log( "[Yeelight:" . $self->{name} . "] Warning, start_timer called but timer undefined" ); + + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } } } @@ -184,29 +231,22 @@ sub get_data { if ( $self->{data}->{info}->{Location} ) { if ( ( defined $self->{data}->{info}->{model} ) and ( $self->{init} == 0 ) ) { - main::print_log( "[Yeelight:" . $self->{name} . "] " . $self->{module_version} . " Configuration Loaded" ); + main::print_log( "[Yeelight:" . $self->{name} . "] " . $self->{module_version} . " Configuration Loaded. Starting Socket listener..." ); $active_yeelights{ $self->{host} } = 1; $self->print_info(); $self->{init} = 1; + $self->process_data(); + $self->{data_socket}->start(); } - $self->poll(); } else { - main::print_log( "[Yeelight:" . $self->{name} . "] WARNING, Did not poll: location: $self->{location}" ) if ( $self->{debug} ); + main::print_log( "[Yeelight:" . $self->{name} . "] WARNING, Did not find Yeelight data, retrying..." ); + $self->{timer}->set( 10, sub { &Yeelight::get_data($self) }); + } } - -sub poll { - my ($self) = @_; - - main::print_log( "[Yeelight:" . $self->{name} . "] Background Polling initiated" ) if ( $self->{debug} ); - $self->_get_TCP_data('info'); - - return ('1'); -} - sub process_check { my ($self) = @_; @@ -233,7 +273,7 @@ sub process_check { my ($json_data) = $file_data =~ /({.*})/; print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); unless ( ($file_data) and ($json_data) ) { - main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! bad data returned by poll" ); + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! bad data returned by query" ); main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); return; } @@ -274,7 +314,6 @@ sub process_check { return unless ( defined $self->{cmd_process} ); if ( $self->{cmd_process}->done_now() ) { main::print_log( "[Yeelight:" . $self->{name} . "] Background Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); - $self->get_data(); #poll since the command is done to get a new state my $file_data = &main::file_read( $self->{cmd_data_file} ); my $com_status = "online"; @@ -294,10 +333,8 @@ sub process_check { $com_status = "offline"; } else { -shift @{ $self->{cmd_queue} }; -print "***RESULTS***"; -print Dumper $data; - if ($data->{results} eq 'ok') { + + if ($data->{result}[0] eq 'ok') { shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful $self->{cmd_process_retry} = 0; $com_status = "online"; @@ -305,14 +342,13 @@ print Dumper $data; } else { main::print_log( "[Yeelight:" . $self->{name} . "] Last command failed! Going to retry" ); } - $self->poll; } if ( scalar @{ $self->{cmd_queue} } ) { main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue found" ); my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. $self->{cmd_process}->set($cmd); - $self->{cmd_process}->start(); +#TODO eval_with_timer $self->{cmd_process}->start(),3; #wait a few seconds before trying again main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); } @@ -342,34 +378,23 @@ print Dumper $data; } } -#polling process. Keep it separate from commands +#TODO ADD VOICE COMMAND +#query subroutine. Used for voice command to refresh state if things get out of sync sub _get_TCP_data { my ( $self, $mode, $params ) = @_; #{"id":1,"method":"get_prop","params":["power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","flow_params","music_on","name","bg_power","bg_flowing","bg_flow_params","bg_ct","bg_lmode","bg_bright","bg_rgb","bg_hue","bg_sat","nl_br"]}\r\n); my $cmdline = "{\"id\":" . $self->{id} . ",\"method\":" . $method{$mode} . ",\"params\":["; - foreach my $item (@{$param_array{$mode}}) { - $cmdline .= '"' . $item . '",'; - } - chop($cmdline); + $cmdline .= join(',', @{$param_array{$mode}}); $cmdline .= "]}"; - my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; + my $cmd = "";# "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; if ( $self->{poll_process}->done() ) { $self->{poll_process}->set($cmd); $self->{poll_process}->start(); $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; $self->{poll_process_mode} = $mode; main::print_log( "[Yeelight:" . $self->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd" ) if ( $self->{debug} ); - } - else { - if ( scalar @{ $self->{poll_queue} } < $self->{max_poll_queue} ) { - main::print_log( "[Yeelight:" . $self->{name} . "] Queue is " . scalar @{ $self->{poll_queue} } . ". Queing command $mode, $cmd" ) - if ( $self->{debug} ); - push @{ $self->{poll_queue} }, "$mode|$cmd"; - } - else { - #the queue has grown past the max, so it might be down. Since polls are expendable, just don't do anything - #when the aurora is back it will process the backlog, and as soon as a poll is processed, the queue is cleared. - } + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] Query request already in progress" ) } } @@ -377,16 +402,11 @@ sub _get_TCP_data { sub _push_TCP_data { my ( $self, $mode, @params ) = @_; - my $cmdline = "{\"id\":" . $self->{id} . ",\"method\":" . $method{$mode} . ",\"params\":["; - foreach my $item (@params) { - $cmdline .= '"' . $item . '",'; - } - chop($cmdline); - $cmdline .= "]}"; + my $cmdline = "{ \"id\":" . $self->{id} . ", \"method\":" . $method{$mode} . ", \"params\":["; + $cmdline .= join(',',@params); + $cmdline .= "] }"; my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; -print "***cmd=$cmd\n"; - if ( $self->{cmd_process}->done() ) { $self->{cmd_process}->set($cmd); $self->{cmd_process}->start(); @@ -423,17 +443,11 @@ print "***cmd=$cmd\n"; sub get_ssdp_data { my ( $self, $id, $timeout ) = @_; - #return a location that isn't in the $active_auroras hash - my ( $data ) = scan_ssdp_data($timeout); if (defined $data and defined $data->{$self->{host}}) { &main::print_log( "[Yeelight:" . $self->{name} . "] SSDP scan found device $self->{host}!"); -#TODO Cleanupthere probably is a better way to copy in a hash $self->{data}->{info} = $data->{$self->{host}}; - #foreach my $key (keys %{$data->{$self->{host}}}) { - # print "**key = $key, $data->{$self->{host}}->{$key}\n"; - # $self->{data}->{info}->{$key} = $data->{$self->{host}}->{$key}; - #} + } else { &main::print_log( "[Yeelight:" . $self->{name} . "] Warning, SSDP did not locate yeelight $self->{host}. Retrying."); } @@ -529,17 +543,6 @@ sub register { } -sub stop_timer { - my ($self) = @_; - - if ( defined $self->{timer} ) { - $self->{timer}->stop() if ( $self->{timer}->active() ); - } - else { - main::print_log( "[Yeelight:" . $self->{name} . "] Warning, stop_timer called but timer undefined" ); - } -} - sub print_info { my ($self) = @_; my $name = $self->{data}->{info}->{name}; @@ -607,6 +610,7 @@ sub process_data { else { $self->set( $self->{data}->{info}->{power}, 'poll' ); } +#TODO, add in color & ct $self->{init_data} = 1; } @@ -642,11 +646,44 @@ sub process_data { } #TODO convert the rest - if ( $self->{previous}->{info}{color_mode} != $self->{data}->{info}->{color_mode} ) { + if ( $self->{previous}->{info}->{color_mode} != $self->{data}->{info}->{color_mode} ) { main::print_log( "[Yeelight:" . $self->{name} . "] State Color Mode changed from $self->{previous}->{info}->{color_mode} to $self->{data}->{info}->{color_mode}" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{color_mode} = $self->{data}->{info}->{color_mode}; } + if ( $self->{previous}->{info}->{rgb} != $self->{data}->{info}->{rgb} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] RGB value changed from $self->{previous}->{info}->{rgb} to $self->{data}->{info}->{rgb}" ) if ( $self->{loglevel} ); + $self->{previous}->{info}->{rgb} = $self->{data}->{info}->{rgb}; + my ($red, $green, $blue) = $self->get_rgb($self->{data}->{info}->{rgb}); + + if ( defined $self->{child_object}->{rgb} ) { + main::print_log "[Yeelight:" . $self->{name} . "] RGB Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{rgb}->set("$red, $green, $blue", 'poll' ); + } + if ( defined $self->{child_object}->{rgb_red} ) { + if ($self->{child_object}->{rgb_red}->state != $red) { + main::print_log "[Yeelight:" . $self->{name} . "] RGB Red Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{rgb_red}->set($red, 'poll' ); + } + } + if ( defined $self->{child_object}->{rgb_green} ) { + if ($self->{child_object}->{rgb_green}->state != $green) { + main::print_log "[Yeelight:" . $self->{name} . "] RGB Green Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{rgb_green}->set($green, 'poll' ); + } + } + if ( defined $self->{child_object}->{rgb_blue} ) { + if ($self->{child_object}->{rgb_blue}->state != $blue) { + main::print_log "[Yeelight:" . $self->{name} . "] RGB Blue Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{rgb_blue}->set($blue, 'poll' ); + } + } + + + } + + + } sub print_command_queue { @@ -666,7 +703,7 @@ sub print_command_queue { sub purge_command_queue { my ($self) = @_; my $commands = scalar @{ $self->{cmd_queue} }; - main::print_log( "Aurora:" . $self->{name} . "] Purging Command Queue of $commands commands" ); + main::print_log( "Yeelight:" . $self->{name} . "] Purging Command Queue of $commands commands" ); @{ $self->{cmd_queue} } = (); } @@ -678,14 +715,20 @@ sub get_debug { return $self->{debug}; } +sub query_yeelight { + my ($self) = @_; + + main::print_log( "[Yeelight:" . $self->{name} . "] Querying Yeelight for status" ); + $self->_get_TCP_data('info'); +} + sub set { my ( $self, $p_state, $p_setby ) = @_; if ( $p_setby eq 'poll' ) { - $p_state .= "%" if ( defined $p_state and $p_state =~ m/\d+(?!%)/ ); + $p_state .= "%" if ($p_state =~ m/\d+(?!%)/ ); main::print_log( "[Yeelight:" . $self->{name} . "] DB super::set, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); $self->SUPER::set($p_state); - $self->start_timer; } else { @@ -695,13 +738,16 @@ sub set { $self->_push_TCP_data($mode, @{$param_array{$mode}}); } elsif ( $mode =~ /^(\d+)/ ) { - my @params = @{$param_array{$mode}}; + my @params = @{$param_array{"bright"}}; unshift @params, $1; $self->_push_TCP_data( 'brightness', @params ); } elsif ( $mode =~ /^([-+]\d+)/ ) { my @params = @{$param_array{$mode}}; - unshift @params, $self->{info}->{bright} + $1; + my $value = $self->{info}->{bright} + $1; + $value = 0 if ($value < 0); + $value = 100 if ($value > 100); + unshift @params, $value; $self->_push_TCP_data( 'brightness', @params ); } else { @@ -726,12 +772,14 @@ sub set_hsv { sub set_rgb { my ( $self, $r, $g, $b ) = @_; - my ( $cred, $cgreen, $cblue) = $self->get_rbg(); + my ( $cred, $cgreen, $cblue) = $self->get_rgb(); $r = $cred unless ($r); $g = $cgreen unless ($g); $b = $cblue unless ($b); - my $value = ($r * 65536) + ($g * 265) + $b; -#TODO $self->_push_TCP_data( 'brightness', $params ); + my $value = ($r * 65536) + ($g * 256) + $b; + my @params = @{$param_array{rgb}}; + unshift @params, $value; + $self->_push_TCP_data( 'rgb', @params ); } sub set_ct { @@ -745,7 +793,7 @@ sub generate_voice_commands { my $object_string; my $object_name = $self->get_object_name; $self->{init_v_cmd} = 1; - &main::print_log("Generating Voice commands for Nanoleaf Aurora Controller $object_name"); + &main::print_log("Generating Voice commands for Yeelight $object_name"); my $voice_cmds = $self->get_voice_cmds(); my $i = 1; @@ -767,16 +815,16 @@ sub generate_voice_commands { $object_string .= $object_name . "_" . $i . "_v -> tie_event('" . $voice_cmds->{$cmd} . "');\n\n"; #, '$command $cmd');\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', 'Nanoleaf_Aurora', 'Controller_commands' ); + $object_string .= ::store_object_data( $object_name_v, 'Voice_Cmd', 'Yeelight', 'Controller_commands' ); $i++; } #Evaluate the resulting object generating string package main; eval $object_string; - print "Error in nanoleaf_Yeelight_item_commands: $@\n" if $@; + print "Error in Yeelight_item_commands: $@\n" if $@; - package Nanoleaf_Aurora; + package Yeelight; } } @@ -784,8 +832,9 @@ sub get_voice_cmds { my ($self) = @_; my %voice_cmds = ( 'Print Command Queue to print log' => $self->get_object_name . '->print_command_queue', - 'Purge Command Queue' => $self->get_object_name . '->purge_command_queue' - + 'Purge Command Queue' => $self->get_object_name . '->purge_command_queue', + 'Force Yeelight Status query' => $self->get_object_name . '->query_yeelight' + ); return \%voice_cmds; @@ -795,19 +844,17 @@ sub get_voice_cmds { package Yeelight_Red; -@Nanoleaf_Yeelight_Static::ISA = ('Generic_Item'); +@Yeelight_Red::ISA = ('Generic_Item'); sub new { - my ( $class, $object, $static_string ) = @_; + my ( $class, $object) = @_; my $self = new Generic_Item(); bless $self, $class; - @{ $$self{states} } = ( 'on', 'off' ); + for my $i (0..255) { push @{ $$self{states} }, "$i"; } $$self{master_object} = $object; - $$self{loop} = 0; - $$self{string} = $static_string if ( defined $static_string ); - $object->register( $self, 'rgb-red' ); + $object->register( $self, 'rgb_red' ); return $self; } @@ -819,27 +866,119 @@ sub set { $self->SUPER::set($p_state); } else { - #if ON then backup current configuration and set the new one. - if ( lc $p_state eq 'on' ) { - $$self{previous_effect} = $$self{master_object}->get_effect(); - $$self{master_object}->set_static( $$self{string} ); - - #if OFF then check if current is same as static, and if so restore the old one. + if ( $p_state >= 0 and $p_state <= 255 ) { + my ($r, $g, $b) = $$self{master_object}->get_rgb(); + $$self{master_object}->set_rgb($p_state, $g, $b) + + } else { + main::print_log("[Yeelight RGB Red] Error. Unknown set mode $p_state"); } - elsif ( lc $p_state eq 'off' ) { - $$self{master_object}->check_static( $$self{string}, $$self{previous_effect} ); + } +} + +package Yeelight_Green; + +@Yeelight_Green::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + for my $i (0..255) { push @{ $$self{states} }, "$i"; } + + $$self{master_object} = $object; + $object->register( $self, 'rgb_green' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( $p_state >= 0 and $p_state <= 255 ) { + my ($r, $g, $b) = $$self{master_object}->get_rgb(); + $$self{master_object}->set_rgb($r, $p_state, $b) + + } else { + main::print_log("[Yeelight RGB Green] Error. Unknown set mode $p_state"); } - else { - main::print_log("[Aurora Static] Error. Unknown set mode $p_state"); + } +} + +package Yeelight_Blue; + +@Yeelight_Blue::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + for my $i (0..255) { push @{ $$self{states} }, "$i"; } + + $$self{master_object} = $object; + $object->register( $self, 'rgb_blue' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( $p_state >= 0 and $p_state <= 255 ) { + my ($r, $g, $b) = $$self{master_object}->get_rgb(); + $$self{master_object}->set_rgb($r, $g, $p_state) + + } else { + main::print_log("[Yeelight RGB Blue] Error. Unknown set mode $p_state"); } } } +package Yeelight_RGB; +@Yeelight_RGB::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + + $$self{master_object} = $object; + $object->register( $self, 'rgb' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + my ($r, $g, $b) = split($p_state,','); + if (( $r >= 0 and $r <= 255 ) and ( $g >= 0 and $g <= 255 ) and( $b >= 0 and $b <= 255 )) { + $$self{master_object}->set_rgb($r, $g, $b) + } else { + main::print_log("[Yeelight RGB] Error. Unknown set mode $p_state"); + } + } +} package Yeelight_Comm; -@Nanoleaf_Yeelight_Comm::ISA = ('Generic_Item'); +@Yeelight_Comm::ISA = ('Generic_Item'); sub new { my ( $class, $object ) = @_; @@ -865,20 +1004,5 @@ sub set { 1; # Version History -# v1.0.0 - initial module -# v1.0.1 - initial static support -# v1.0.2 - multiple auroras, brightness -# v1.0.3 - working static. turn on, overrides effect, turning off will restore previous effect -# v1.0.4 - Voice Commands -# v1.0.5 - working multi static -# v1.0.6 - better processing -# v1.0.7 - ability to specify API as an option -# v1.0.8 - initial v1.5.0 API v1 support -# v1.0.9 - use config_parms (mh.ini) instead of dedicated config file -# v1.0.10 - Updated to work with other versions of perl, typo with mh.ini -# v1.0.11 - cosmetic fixes for undefined variables -# v1.0.12 - get_effects method to get array of available effects -# v1.0.13 - ability to print and purge the command queue in case a network error prevents clearing, empty poll queue if max reached -# v1.0.14 - commands now queue properly -# v1.0.15 - fixed polling -# v1.1.01 - firmware v2.2.0 and rhythm module \ No newline at end of file +# v1.0.0 - initial module +# v1.0.1 - color support From c5e5d2109d760907a1d284afd334cba24733582c Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Wed, 15 Aug 2018 11:08:51 +0200 Subject: [PATCH 27/78] Make the self-test code more explicit so people are aware it is running --- code/test/test_mh.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/test/test_mh.pl b/code/test/test_mh.pl index e1fcc4ad1..fbedbef0c 100644 --- a/code/test/test_mh.pl +++ b/code/test/test_mh.pl @@ -16,7 +16,7 @@ } sub shutdown { - print_log "Stopping self-test, exit..."; + print_log "Stopping self-test code in code/test/test_mh.pl, going to exit Misterhouse now..."; run_voice_cmd("Exit Mister House"); } From e9a6724119bdd6515f4c96a7e0b6984e0408a92f Mon Sep 17 00:00:00 2001 From: H Plato Date: Tue, 21 Aug 2018 20:58:54 -0600 Subject: [PATCH 28/78] v1.2 - color, brightness controls --- lib/Yeelight.pm | 385 +++++++++++++++++++++--------------------------- 1 file changed, 171 insertions(+), 214 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index 64f5e1e74..1794e612e 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,14 +1,12 @@ package Yeelight; -# v1.0.1 - -#if any effect is changed, by definition the static child should be set to off. -#cmd data returns, need to check by command -#NB: if any effect gets set, or another static is active, then other statics should be set to off. - -#effects, turn static off and clear out static_check +# v1.2 #TODO +#- brightness changes change state to on and off +#- test queuing fast commands +#TODO check query data + use strict; use warnings; @@ -28,21 +26,19 @@ use IO::Socket::INET; # TURN ON LOCAL CONTROL # Firmware supported -# stripe : 44 +# led strip (stripe) : 44 # Yeelight Objects # -# $yeelight = new Yeelight('1'); +# $yeelight = new Yeelight('10.10.1.1'); # $yeelight_comm = new Yeelight_Comm($yeelight); # $yeelight_ct = new Yeelight_Colortemp($yeelight); # $yeelight_rgb = new Yeelight_RGB($yeelight); -# $yeelight_red = new Yeelight_red($yeelight); -# $yeelight_blue = new Yeelight_blue($yeelight); -# $yeelight_green = new Yeelight_green($yeelight); -# individual color options are provided to allow for web slider control. + # yeelight_rgb the set value is 'red, green, blue' -#ie $yeelight_rgb->set('255,10,32'); -# yeelight_ct has some presets, $yeelight_ct->set_warm +# ie $yeelight_rgb->set('255,10,32'); + + # OPTIONS @@ -75,11 +71,12 @@ my %param_array; @{$param_array{on}} = ('"on"','"smooth"',500); @{$param_array{off}} = ('"off"','"smooth"',500); @{$param_array{rgb}} = ('"smooth"',500); +@{$param_array{ct}} = ('"smooth"',500); our %active_yeelights = (); sub new { - my ( $class, $id, $location, $options ) = @_; + my ( $class, $location, $id, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; $self->{id} = "1"; @@ -90,18 +87,16 @@ sub new { $self->{data} = undef; $self->{child_object} = undef; -# $self->{config}->{poll_seconds} = 10; -# $self->{config}->{poll_seconds} = $::config_parms{ "yeelight_" . $self->{name} . "_poll" } -# if ( defined $::config_parms{ "yeelight_" . $self->{name} . "_poll" } ); -# $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.0.1"; + $self->{module_version} = "v1.2.0"; $self->{ssdp_timeout} = 1000; - $self->{last_static} = ""; + $self->{socket_connected} = 0; $self->{host} = $location; $self->{port} = 55443; + $self->{brightness_state_delay} = 1; if ($location =~ m/:/) { ($self->{host}, $self->{port}) = $location =~ /(.*):(.*)/; @@ -110,7 +105,7 @@ sub new { $options = "" unless ( defined $options ); $options = $::config_parms{ "yeelight_" . $self->{name} . "_options" } if ( $::config_parms{ "yeelight_" . $self->{name} . "_options" } ); - $self->{debug} = 4; + $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) if ( $options =~ m/debug\=/i ); $self->{debug} = 0 if ( $self->{debug} < 0 ); @@ -143,7 +138,7 @@ sub new { &::Reload_post_add_hook( \&Yeelight::generate_voice_commands, 1, $self ); #push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); push( @{ $$self{states} }, 'off'); - for my $i (1..99) { push @{ $$self{states} }, "$i%"; } + for my $i (0..100) { push @{ $$self{states} }, "$i%"; } push( @{ $$self{states} }, 'on'); $self->{timer} = new Timer; $self->get_data(); @@ -153,22 +148,14 @@ sub new { sub check_for_socket_data { my ($self) = @_; -# if ($Socket_Items{$instance}{'socket'}->active) { +# Other objects use a socket in $Socket_Items? # $NewCmd = $Socket_Items{$instance}{'socket'}->said; -# } else { -# # restart the TCP connection if its lost. -# if ($Socket_Items{$instance}{recon_timer}->inactive) { -# ::print_log("Connection to $instance instance of AD2 was lost, I will try to reconnect in $$self{reconnect_time} seconds"); -# # ::logit("AD2.pm ser2sock connection lost! Trying to reconnect." ); -# $Socket_Items{$instance}{recon_timer}->set($$self{reconnect_time}, sub { -# $Socket_Items{$instance}{'socket'}->start; -# }); -# } -# } -# } - my $com_status = "online"; + + my $com_status = ""; if ($self->{data_socket}->active) { my $rec_data = $self->{data_socket}->said; + $self->{socket_connected} = 1; + $com_status = "online"; return if (!defined $rec_data or $rec_data eq ""); $rec_data =~ s/\r\n//g; print "debug: rec_data=$rec_data\n" if ( $self->{debug} > 2); @@ -200,16 +187,16 @@ sub check_for_socket_data { } } else { - if ($self->{init}) { + if (($self->{init}) and ($self->{socket_connected})) { main::print_log( "[Yeelight:" . $self->{name} . "] Lost connection to Yeelight. Trying to connect again in $self->{reconnect} seconds" ); $self->{recon_timer}->set($self->{reconnect_time}, sub {$self->{data_socket}->start;}); $com_status = "offline"; } } - if ( defined $self->{child_object}->{comm} ) { + if (( defined $self->{child_object}->{comm} ) and ($self->{socket_connected})) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] A Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; $self->{child_object}->{comm}->set( $com_status, 'poll' ); } @@ -218,6 +205,7 @@ sub check_for_socket_data { sub get_data { my ($self) = @_; + my $com_status = "online"; main::print_log( "[Yeelight:" . $self->{name} . "] get_data initiated" ) if ( $self->{debug} ); @@ -243,8 +231,17 @@ sub get_data { else { main::print_log( "[Yeelight:" . $self->{name} . "] WARNING, Did not find Yeelight data, retrying..." ); $self->{timer}->set( 10, sub { &Yeelight::get_data($self) }); - + $com_status = "offline"; } + + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Yeelight:" . $self->{name} . "] 0 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } sub process_check { @@ -254,7 +251,6 @@ sub process_check { if ( $self->{poll_process}->done_now() ) { - #shift @{ $self->{poll_queue} }; #remove the poll since they are expendable. @{ $self->{poll_queue} } = (); #clear the queue since process is done. my $com_status = "online"; @@ -304,7 +300,7 @@ sub process_check { if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] 1 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; $self->{child_object}->{comm}->set( $com_status, 'poll' ); } @@ -345,10 +341,13 @@ sub process_check { } if ( scalar @{ $self->{cmd_queue} } ) { - main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue found" ); + main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue found" ); + main::print_log( "[Yeelight:" . $self->{name} . "] " .join (", ",@{ $self->{cmd_queue} }) ); + + print join(", ", @{ $self->{cmd_queue} }); my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. $self->{cmd_process}->set($cmd); -#TODO eval_with_timer $self->{cmd_process}->start(),3; #wait a few seconds before trying again + main::eval_with_timer "$self->{cmd_process}->start()",1; #wait a few seconds before trying again main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); } @@ -370,7 +369,7 @@ sub process_check { if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] 2 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; $self->{child_object}->{comm}->set( $com_status, 'poll' ); } @@ -379,6 +378,7 @@ sub process_check { } #TODO ADD VOICE COMMAND + #query subroutine. Used for voice command to refresh state if things get out of sync sub _get_TCP_data { my ( $self, $mode, $params ) = @_; @@ -386,7 +386,7 @@ sub _get_TCP_data { my $cmdline = "{\"id\":" . $self->{id} . ",\"method\":" . $method{$mode} . ",\"params\":["; $cmdline .= join(',', @{$param_array{$mode}}); $cmdline .= "]}"; - my $cmd = "";# "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; + my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; if ( $self->{poll_process}->done() ) { $self->{poll_process}->set($cmd); $self->{poll_process}->start(); @@ -426,12 +426,7 @@ sub _push_TCP_data { main::print_log( "[Yeelight:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne "offline" ) { - main::print_log "[Yeelight:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] 3 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); $self->{status} = "offline"; $self->{child_object}->{comm}->set( "offline", 'poll' ); } @@ -523,22 +518,16 @@ SSDP sub register { my ( $self, $object, $type ) = @_; - my $keys = ""; - #allow for multiple static entries - if ( lc $type eq 'static' ) { - if ( defined $self->{child_object}->{static} ) { - $keys = keys %{ $self->{child_object}->{static} }; - } - else { - $keys = 0; - } - $self->{child_object}->{static}->{$keys} = $object; - } - else { - $self->{child_object}->{$type} = $object; - } - $type .= " (" . $keys . ")" if ( $keys ne "" ); + $self->{child_object}->{$type} = $object; + my ($red, $green, $blue) = $self->get_rgb($self->{data}->{info}->{rgb}); + + if (lc $type eq "rgb") { + $self->{child_object}->{rgb}->set("$red, $green, $blue", 'poll' ) if (defined $red); + } elsif (lc $type eq "ct") { + $self->{child_object}->{ct}->set($self->{data}->{info}->{ct}, 'poll' ) if (defined $self->{data}->{info}->{ct}); + } + &main::print_log( "[Yeelight:" . $self->{name} . "] Registered $type child object" ); } @@ -560,21 +549,21 @@ sub print_info { main::print_log( "[Yeelight:" . $self->{name} . "] State:\t\t " . $self->{data}->{info}->{power} ); if ($self->{data}->{info}->{color_mode} == 1) { - main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t rgb mode"); + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t rgb mode"); } elsif ($self->{data}->{info}->{color_mode} == 2) { - main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t color temperature mode"); + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t color temperature mode"); } elsif ($self->{data}->{info}->{color_mode} == 3) { - main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t hsv mode"); + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t hsv mode"); } else { - main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t Unknown mode: " . $self->{data}->{info}->{color_mode}); + main::print_log( "[Yeelight:" . $self->{name} . "] Color Mode:\t Unknown mode: " . $self->{data}->{info}->{color_mode}); } main::print_log( "[Yeelight:" . $self->{name} . "] Brightness:\t " . $self->{data}->{info}->{bright} ); #rgb = red * 65536 + green * 256 + blue my ($r_red, $r_green, $r_blue) = $self->get_rgb(); - main::print_log( "[Yeelight:" . $self->{name} . "] RGB:\t\t " . $self->{data}->{info}->{rgb} ); - main::print_log( "[Yeelight:" . $self->{name} . "] \tRed: " . $r_red ); - main::print_log( "[Yeelight:" . $self->{name} . "] \tGreen: " . $r_green ); - main::print_log( "[Yeelight:" . $self->{name} . "] \tBlue: " . $r_blue ); + main::print_log( "[Yeelight:" . $self->{name} . "] RGB:\t\t " . $self->{data}->{info}->{rgb} ); + main::print_log( "[Yeelight:" . $self->{name} . "] \t Red: " . $r_red ); + main::print_log( "[Yeelight:" . $self->{name} . "] \t Green:" . $r_green ); + main::print_log( "[Yeelight:" . $self->{name} . "] \t Blue: " . $r_blue ); main::print_log( "[Yeelight:" . $self->{name} . "] Hue:\t\t " . $self->{data}->{info}->{hue} ); main::print_log( "[Yeelight:" . $self->{name} . "] Saturation:\t " . $self->{data}->{info}->{sat} ); @@ -593,7 +582,7 @@ sub process_data { # Main core of processing # set state of self for state - # for any registered child selfs, update their state if + # for any registered child selfs, update their state if changed main::print_log( "[Yeelight:" . $self->{name} . "] Processing Data..." ) if ( $self->{debug} ); @@ -610,7 +599,7 @@ sub process_data { else { $self->set( $self->{data}->{info}->{power}, 'poll' ); } -#TODO, add in color & ct + $self->{init_data} = 1; } @@ -640,12 +629,12 @@ sub process_data { } if ( $self->{previous}->{info}->{bright} != $self->{data}->{info}->{bright} ) { - main::print_log( "[Yeelight:" . $self->{name} . "] Brightness changed from $self->{previous}->{info}->{state}->{brightness}->{value} to $self->{data}->{info}->{state}->{brightness}->{value}" ) if ( $self->{loglevel} ); + main::print_log( "[Yeelight:" . $self->{name} . "] Brightness changed from $self->{previous}->{info}->{bright} to $self->{data}->{info}->{brightn}" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{bright} = $self->{data}->{info}->{bright}; $self->set( $self->{data}->{info}->{bright}, 'poll' ); } -#TODO convert the rest +#TODO Colormode child object / method if ( $self->{previous}->{info}->{color_mode} != $self->{data}->{info}->{color_mode} ) { main::print_log( "[Yeelight:" . $self->{name} . "] State Color Mode changed from $self->{previous}->{info}->{color_mode} to $self->{data}->{info}->{color_mode}" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{color_mode} = $self->{data}->{info}->{color_mode}; @@ -654,35 +643,24 @@ sub process_data { if ( $self->{previous}->{info}->{rgb} != $self->{data}->{info}->{rgb} ) { main::print_log( "[Yeelight:" . $self->{name} . "] RGB value changed from $self->{previous}->{info}->{rgb} to $self->{data}->{info}->{rgb}" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{rgb} = $self->{data}->{info}->{rgb}; + $self->{set_time} = $main::Time; #since we want updates if the color changes my ($red, $green, $blue) = $self->get_rgb($self->{data}->{info}->{rgb}); if ( defined $self->{child_object}->{rgb} ) { main::print_log "[Yeelight:" . $self->{name} . "] RGB Child object found. Updating..." if ( $self->{loglevel} ); $self->{child_object}->{rgb}->set("$red, $green, $blue", 'poll' ); } - if ( defined $self->{child_object}->{rgb_red} ) { - if ($self->{child_object}->{rgb_red}->state != $red) { - main::print_log "[Yeelight:" . $self->{name} . "] RGB Red Child object found. Updating..." if ( $self->{loglevel} ); - $self->{child_object}->{rgb_red}->set($red, 'poll' ); - } - } - if ( defined $self->{child_object}->{rgb_green} ) { - if ($self->{child_object}->{rgb_green}->state != $green) { - main::print_log "[Yeelight:" . $self->{name} . "] RGB Green Child object found. Updating..." if ( $self->{loglevel} ); - $self->{child_object}->{rgb_green}->set($green, 'poll' ); - } - } - if ( defined $self->{child_object}->{rgb_blue} ) { - if ($self->{child_object}->{rgb_blue}->state != $blue) { - main::print_log "[Yeelight:" . $self->{name} . "] RGB Blue Child object found. Updating..." if ( $self->{loglevel} ); - $self->{child_object}->{rgb_blue}->set($blue, 'poll' ); - } - } - - } - + if ( $self->{previous}->{info}->{ct} != $self->{data}->{info}->{ct} ) { + main::print_log( "[Yeelight:" . $self->{name} . "] Color Temperature value changed from $self->{previous}->{info}->{ct} to $self->{data}->{info}->{ct}" ) if ( $self->{loglevel} ); + $self->{previous}->{info}->{ct} = $self->{data}->{info}->{ct}; + + if ( defined $self->{child_object}->{ct} ) { + main::print_log "[Yeelight:" . $self->{name} . "] Color Temperature Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{ct}->set($self->{data}->{info}->{ct}, 'poll' ); + } + } } @@ -725,30 +703,58 @@ sub query_yeelight { sub set { my ( $self, $p_state, $p_setby ) = @_; + $p_setby = "" unless (defined $p_setby); + if ( $p_setby eq 'poll' ) { $p_state .= "%" if ($p_state =~ m/\d+(?!%)/ ); main::print_log( "[Yeelight:" . $self->{name} . "] DB super::set, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); $self->SUPER::set($p_state); } + elsif ($p_setby eq 'rgb') { + main::print_log( "[Yeelight:" . $self->{name} . "] DB super::set, in rgb set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); + my ($r, $g, $b) = split(/,/, $p_state); + $self->set_rgb($r,$g,$b); + } else { main::print_log( "[Yeelight:" . $self->{name} . "] DB set_mode, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); my $mode = lc $p_state; - if ( ( $mode eq "on" ) or ( $mode eq "off" ) ) { - $self->_push_TCP_data($mode, @{$param_array{$mode}}); - } - elsif ( $mode =~ /^(\d+)/ ) { - my @params = @{$param_array{"bright"}}; - unshift @params, $1; - $self->_push_TCP_data( 'brightness', @params ); + if ( $mode =~ /^(\d+)/ ) { + if (($self->{data}->{info}->{power} eq "on") and ($1 == 0)) { + $self->set("off"); + } elsif (($self->{data}->{info}->{power} eq "off") and ($1 > 0)) { + $self->set("on"); + main::print_log( "Yeelight:" . $self->{name} . "] Brightness change, delayed state change to $mode" ) if ( $self->{debug} ); + my $object_name = $self->get_object_name; + my $cmd_string = $object_name . '->set("' . $mode .'");'; + main::eval_with_timer $cmd_string, $self->{brightness_state_delay}; + } else { + my @params = @{$param_array{"bright"}}; + unshift @params, $1; + $self->_push_TCP_data( 'brightness', @params ); + } } elsif ( $mode =~ /^([-+]\d+)/ ) { - my @params = @{$param_array{$mode}}; my $value = $self->{info}->{bright} + $1; - $value = 0 if ($value < 0); - $value = 100 if ($value > 100); - unshift @params, $value; - $self->_push_TCP_data( 'brightness', @params ); + if (($self->{data}->{info}->{power} eq "on") and ($value <= 0)) { + $self->set("off"); + } elsif (($self->{data}->{info}->{power} eq "off") and ($value > 0)) { + $self->set("on"); + main::print_log( "Yeelight:" . $self->{name} . "] Brightness change, delayed state change to $mode" ) if ( $self->{debug} ); + my $object_name = $self->get_object_name; + my $cmd_string = $object_name . '->set("' . $mode .'");'; + main::eval_with_timer $cmd_string, $self->{brightness_state_delay}; + } else { + + my @params = @{$param_array{$mode}}; + $value = 0 if ($value < 0); + $value = 100 if ($value > 100); + unshift @params, $value; + $self->_push_TCP_data( 'brightness', @params ); + } + } + elsif ( ( $mode eq "on" ) or ( $mode eq "off" ) ) { + $self->_push_TCP_data($mode, @{$param_array{$mode}}); } else { main::print_log( "Yeelight:" . $self->{name} . "] Error, unknown set state $p_state" ); @@ -760,30 +766,52 @@ sub set { sub get_rgb { my ($self) = @_; - my $red = int($self->{data}->{info}->{rgb} / 65536); - my $green = int(($self->{data}->{info}->{rgb} - ($red * 65536)) / 256); - my $blue = $self->{data}->{info}->{rgb} - ($red * 65536) - ($green * 256); - return ($red, $green, $blue); -} - -sub set_hsv { - my ( $self, $h, $s, $v ) = @_; + if (defined $self->{data}->{info}->{rgb}) { + my $red = int($self->{data}->{info}->{rgb} / 65536); + my $green = int(($self->{data}->{info}->{rgb} - ($red * 65536)) / 256); + my $blue = $self->{data}->{info}->{rgb} - ($red * 65536) - ($green * 256); + return ($red, $green, $blue); + } else { + return (undef, undef, undef); + } } sub set_rgb { my ( $self, $r, $g, $b ) = @_; - my ( $cred, $cgreen, $cblue) = $self->get_rgb(); - $r = $cred unless ($r); - $g = $cgreen unless ($g); - $b = $cblue unless ($b); - my $value = ($r * 65536) + ($g * 256) + $b; - my @params = @{$param_array{rgb}}; - unshift @params, $value; - $self->_push_TCP_data( 'rgb', @params ); + + if (($r >= 0) and ($r <= 255) and ($g >= 0) and ($g <= 255) and ($b >= 0) and ($b <=255)) { + my ( $cred, $cgreen, $cblue) = $self->get_rgb(); + $r = $cred unless ($r); + $g = $cgreen unless ($g); + $b = $cblue unless ($b); + my $value = ($r * 65536) + ($g * 256) + $b; + my @params = @{$param_array{rgb}}; + unshift @params, $value; + $self->_push_TCP_data( 'rgb', @params ); + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR, RGB value out of range (0-255). Red=$r, Green=$g, Blue=$b" ); + } +} + +sub get_ct { + my ( $self) = @_; + + return ($self->{data}->{info}->{ct}); } sub set_ct { my ( $self, $ct ) = @_; + if ( $self->{data}->{info}->{power} ne 'on') { + main::print_log( "[Yeelight:" . $self->{name} . "] Yeelight needs to be on to set color temperature! Command not sent" ); + } else { + if (($ct >= 1700) and ($ct <= 6500)) { + my @params = @{$param_array{ct}}; + unshift @params, $ct; + $self->_push_TCP_data( 'ct', @params ); + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR: Color temperature $ct out of range (1700 - 6500)!" ); + } + } } sub generate_voice_commands { @@ -840,55 +868,18 @@ sub get_voice_cmds { return \%voice_cmds; } +package Yeelight_RGB; - -package Yeelight_Red; - -@Yeelight_Red::ISA = ('Generic_Item'); - -sub new { - my ( $class, $object) = @_; - - my $self = new Generic_Item(); - bless $self, $class; - for my $i (0..255) { push @{ $$self{states} }, "$i"; } - - $$self{master_object} = $object; - $object->register( $self, 'rgb_red' ); - return $self; - -} - -sub set { - my ( $self, $p_state, $p_setby ) = @_; - - if ( $p_setby eq 'poll' ) { - $self->SUPER::set($p_state); - } - else { - if ( $p_state >= 0 and $p_state <= 255 ) { - my ($r, $g, $b) = $$self{master_object}->get_rgb(); - $$self{master_object}->set_rgb($p_state, $g, $b) - - } else { - main::print_log("[Yeelight RGB Red] Error. Unknown set mode $p_state"); - } - } -} - -package Yeelight_Green; - -@Yeelight_Green::ISA = ('Generic_Item'); +@Yeelight_RGB::ISA = ('Generic_Item'); sub new { my ( $class, $object) = @_; my $self = new Generic_Item(); bless $self, $class; - for my $i (0..255) { push @{ $$self{states} }, "$i"; } $$self{master_object} = $object; - $object->register( $self, 'rgb_green' ); + $object->register( $self, 'rgb' ); return $self; } @@ -900,29 +891,28 @@ sub set { $self->SUPER::set($p_state); } else { - if ( $p_state >= 0 and $p_state <= 255 ) { - my ($r, $g, $b) = $$self{master_object}->get_rgb(); - $$self{master_object}->set_rgb($r, $p_state, $b) - + my ($r, $g, $b) = split($p_state,','); + if (( $r >= 0 and $r <= 255 ) and ( $g >= 0 and $g <= 255 ) and( $b >= 0 and $b <= 255 )) { + $$self{master_object}->set_rgb($r, $g, $b) } else { - main::print_log("[Yeelight RGB Green] Error. Unknown set mode $p_state"); + main::print_log("[Yeelight RGB] Error. Unknown set mode $p_state"); } } } -package Yeelight_Blue; +package Yeelight_ColorTemp; -@Yeelight_Blue::ISA = ('Generic_Item'); +@Yeelight_ColorTemp::ISA = ('Generic_Item'); sub new { my ( $class, $object) = @_; my $self = new Generic_Item(); bless $self, $class; - for my $i (0..255) { push @{ $$self{states} }, "$i"; } + for my $i (1700..6500) { push @{ $$self{states} }, "$i"; } $$self{master_object} = $object; - $object->register( $self, 'rgb_blue' ); + $object->register( $self, 'ct' ); return $self; } @@ -934,44 +924,11 @@ sub set { $self->SUPER::set($p_state); } else { - if ( $p_state >= 0 and $p_state <= 255 ) { - my ($r, $g, $b) = $$self{master_object}->get_rgb(); - $$self{master_object}->set_rgb($r, $g, $p_state) + if ( $p_state >= 1700 and $p_state <= 6500 ) { + $$self{master_object}->set_ct($p_state) } else { - main::print_log("[Yeelight RGB Blue] Error. Unknown set mode $p_state"); - } - } -} - -package Yeelight_RGB; - -@Yeelight_RGB::ISA = ('Generic_Item'); - -sub new { - my ( $class, $object) = @_; - - my $self = new Generic_Item(); - bless $self, $class; - - $$self{master_object} = $object; - $object->register( $self, 'rgb' ); - return $self; - -} - -sub set { - my ( $self, $p_state, $p_setby ) = @_; - - if ( $p_setby eq 'poll' ) { - $self->SUPER::set($p_state); - } - else { - my ($r, $g, $b) = split($p_state,','); - if (( $r >= 0 and $r <= 255 ) and ( $g >= 0 and $g <= 255 ) and( $b >= 0 and $b <= 255 )) { - $$self{master_object}->set_rgb($r, $g, $b) - } else { - main::print_log("[Yeelight RGB] Error. Unknown set mode $p_state"); + main::print_log("[Yeelight Color Temp] Error. State out of range (1700-6500): $p_state "); } } } From 672f17b68409dc24ab647716c6dae9c5b6db2ead Mon Sep 17 00:00:00 2001 From: H Plato Date: Wed, 22 Aug 2018 12:07:26 -0600 Subject: [PATCH 29/78] v1.3.1 - minor formatting changes --- code/common/calc_eto.pl | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 550de60b7..ab000f0d4 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -95,7 +95,7 @@ use Date::Calc qw(Day_of_Year); my $debug = 0; my $msg_string; -my $rrd = 0; +my $rrd = ""; $p_wu_forecast = new Process_Item qq[get_url --quiet "http://api.wunderground.com/api/$config_parms{wu_key}/astronomy/yesterday/conditions/forecast/q/$config_parms{eto_location}.json" "$config_parms{data_dir}/wuData/wu_data.json"]; @@ -120,7 +120,7 @@ if ( $Startup or $Reload ) { $eto_ready = 1; - print_log "[calc_eto] v1.3 Startup. Checking Configuration..."; + print_log "[calc_eto] v1.3.1 Startup. Checking Configuration..."; mkdir "$eto_data_dir" unless ( -d "$eto_data_dir" ); mkdir "$eto_data_dir/ET" unless ( -d "$eto_data_dir/ET" ); mkdir "$eto_data_dir/logs" unless ( -d "$eto_data_dir/logs" ); @@ -157,12 +157,12 @@ print_log "[calc_eto] Will write daily rain to RRD (mms)"; $rrd = "m"; } elsif ($config_parms{eto_rrd} eq "in") { - print_log "[calc_eto] Will write daily rain to RRD (inches)"; - $rrd = "i"; + print_log "[calc_eto] Inches to RRD not supported yet"; + $rrd = ""; } else { print_log "[calc_eto] Unknown RRD option $config_parms{eto_rrd}"; - $rrd = 0; - } + $rrd = ""; + } } if ( defined $config_parms{eto_irrigation} ) { print_log "[calc_eto] $config_parms{eto_irrigation} set as programmable irrigation system"; @@ -956,6 +956,7 @@ sub detailSchedule { my $station_id = 1; $time = $time * 60; #add in seconds foreach my $station (split /,/, $lengths) { + next if ($station == 0); my $run_hour = 0; if ($station > 3600) { $run_hour = int($station / 3600); @@ -1169,6 +1170,15 @@ sub main_calc_eto { print_log $msg; $msg_string .= $msg . "\n"; + #write to the RRD if it's enabled + if ($rrd ne "") { + $msg = '[calc_eto] Writing fallen and forecast rain to RRD: ' . round( $todayRain, 4 ) . " mm"; + $Weather{RainTotal} = round( $todayRain, 4 ); + print_log $msg; + $msg_string .= $msg . "\n"; + + } + # Binary watering determination based on 3 criteria: 1)Currently raining 2)Wind>8kph~5mph 3)Temp<4.5C ~ 40F if ($noWater) { $msg = "[calc_eto] RESULTS We will not water because: $whyNot"; @@ -1223,22 +1233,6 @@ sub main_calc_eto { #Write the WU data to a file. This can be used for the MH weather data and save an api call writewuData( $wuData, $noWater, $wuDataPath ); - #write to the RRD if it's enabled - if ($rrd != 0) { - $msg = '[calc_eto] Writing predicted rain to RRD :'; - - if ($rrd eq "m") { - $msg .= $wuData->{current_observation}->{precip_today_metric} . " mm"; - $Weather{RainTotal} = $wuData->{current_observation}->{precip_today_metric}; - } else { - $msg .= $wuData->{current_observation}->{precip_today_in} . " in"; - $Weather{RainTotal} = $wuData->{current_observation}->{precip_today_in}; - } - print_log $msg; - $msg_string .= $msg . "\n"; - - } - #$msg = "[calc_eto] RESULTS Calculated Schedule: $rtime"; #print_log $msg; #$msg_string .= $msg . "\n"; @@ -1246,7 +1240,7 @@ sub main_calc_eto { foreach my $detail (split /\n/,$rtime2) { print_log $detail; } - $msg_string .= $msg . $rtime2; + $msg_string .= $msg . "\n" . $rtime2; if ( defined $config_parms{eto_email} ) { print_log "[calc_eto] Emailing results"; net_mail_send( to => $config_parms{eto_email}, subject => "EvapoTranspiration Results for $Time_Now", text => $msg_string ); From 5083640b4735d450079f458600febc605e255df6 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 22 Aug 2018 22:58:30 -0400 Subject: [PATCH 30/78] Revert "Fix Owfs_Item _ToServer calls" This reverts commit 4120ec34fe07cb2489a6f8b9b9a7bbf44952dacd. --- lib/Owfs_Item.pm | 47 +++++++++++++---------------------------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/lib/Owfs_Item.pm b/lib/Owfs_Item.pm index cf5ccce5f..b84071104 100644 --- a/lib/Owfs_Item.pm +++ b/lib/Owfs_Item.pm @@ -357,13 +357,8 @@ sub set { #$value .= ' '; my $value_length = length($value); - my $payload = - pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); - $self->_ToServer( - $path, $token, $value, $set_by, - length($payload), $msg_write, $value_length, 0, - $payload - ); + my $payload = pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); + $self->_ToServer( $path, $token, $value, $set_by, length($payload) + 1, $msg_write, $value_length, 0, $payload ); } # This method is called to schedule a read command be sent to the owserver for the object. @@ -372,8 +367,7 @@ sub get { return if ( !defined $self->{path} ); my $path = $self->{path} . $token; &main::print_log("Owfs_Item::get path: $path") if $main::Debug{owfs}; - $self->_ToServer( $path, $token, 0, 0, length($path), - $msg_read, $default_block, 0, $path ); + $self->_ToServer( $path, $token, 0, 0, length($path) + 1, $msg_read, $default_block, 0, $path ); } # This method is called to schedule a directory command be sent to the owserver for the object. @@ -384,8 +378,7 @@ sub _dir { # new msg_dirall method -- single packet &main::print_log("Owfs_Item::dir path: $path") if $main::Debug{owfs}; $self->{dir_path} = $path; - $self->_ToServer( $path, 0, 0, 0, length($path), - $msg_dirall, $default_block, 0, $path ); + $self->_ToServer( $path, 0, 0, 0, length($path) + 1, $msg_dirall, $default_block, 0, $path ); } # This method is called to schedule a write command be sent to the owserver for the object. @@ -404,13 +397,8 @@ sub _set_root { #$value .= ' '; my $value_length = length($value); - my $payload = - pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); - $self->_ToServer( - $path, $token, $value, $set_by, - length($payload), $msg_write, $value_length, 0, - $payload - ); + my $payload = pack( 'Z' . $path_length . 'A' . $value_length, $path, $value ); + $self->_ToServer( $path, $token, $value, $set_by, length($payload) + 1, $msg_write, $value_length, 0, $payload ); } # This method is called to schedule a read command be sent to the owserver for the object. @@ -422,8 +410,7 @@ sub _get_root { return if ( !defined $root ); my $path = $self->{root} . $token; &main::print_log("Owfs_Item::_get_root path: $path") if $main::Debug{owfs}; - $self->_ToServer( $path, $token, 0, 0, length($path), - $msg_read, $default_block, 0, $path ); + $self->_ToServer( $path, $token, 0, 0, length($path) + 1, $msg_read, $default_block, 0, $path ); } # This method is used to search the one-wire tree for the specific object as defined @@ -510,20 +497,12 @@ sub _chomp_plus { # This method is a direct port from the OWNet.pm module from owfs. This is the lower layer interface # to the owserver socket port. sub _ToServer { - my ( - $self, $path, $token, $value, - $set_by, $payload_length, $msg_type, $size, - $offset, $payload_data - ) = @_; - my $f = "N6A$payload_length"; - - my $message = pack( $f, - $self->{VER}, $payload_length, $msg_type, - $self->{SG} | $self->{PERSIST}, - $size, $offset, $payload_data ); - &main::print_log( - "Owfs_Item::_ToServer path: $path payload_length: $payload_length payload_data: $payload_data message: $message" - ) if $main::Debug{owfs}; + my ( $self, $path, $token, $value, $set_by, $payload_length, $msg_type, $size, $offset, $payload_data ) = @_; + my $f = "N6Z$payload_length"; + + #$f .= 'Z'.$payload_length if ( $payload_length > 0 ) ; + my $message = pack( $f, $self->{VER}, $payload_length, $msg_type, $self->{SG} | $self->{PERSIST}, $size, $offset, $payload_data ); + &main::print_log("Owfs_Item::_ToServer path: $path payload_length: $payload_length payload_data: $payload_data message: $message") if $main::Debug{owfs}; my $hashref = { msg_type => $msg_type, self => $self, From 1ae899efd92b877a445af821f84ab6c3f5415148 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 22 Aug 2018 22:59:21 -0400 Subject: [PATCH 31/78] Revert "Remove unused internet_weather.pl parameters." This reverts commit 378a11e2f93b1f795857fd5bc56ff9d6b634e8f2. --- bin/mh.ini | 13 +++++++++++++ code/common/internet_weather.pl | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/bin/mh.ini b/bin/mh.ini index a035e6d7d..6fec86287 100644 --- a/bin/mh.ini +++ b/bin/mh.ini @@ -2151,6 +2151,19 @@ weather_internet_elements = all @ Set to 0 to use Humidex, 1 to use Heat Index weather_use_heatindex=0 +@ This parameter is used by the common code file internet_weather.pl to download +@ weather data from the nearest NWS station. Use this link , click +@ on your state, and then click "Hourly Report" to locate the nearest supported city or station. + +nws_city= + +@ This parameter is used by the common code file internet_weather.pl to download +@ weather data from the nearest NWS weather zone. Use this link, click +@ on your state, and then click "Zone Forecast" to locate the nearest zone to your location, for example, +@ zone=SAN DIEGO COUNTY COASTAL AREAS + +zone= + @ This parameter is used by the common code file internet_weather_noaa.pl. @ Locate the NOAA station nearest to your location at http://www.weather.gov/data/current_obs/, @ for example, weather_noaa_station=KSAN diff --git a/code/common/internet_weather.pl b/code/common/internet_weather.pl index 41f4f6e70..0e066d35f 100644 --- a/code/common/internet_weather.pl +++ b/code/common/internet_weather.pl @@ -4,10 +4,13 @@ # $Revision$ #@ Retrieves current weather conditions and forecasts using bin/get_weather (US only). -#@ You will need to set the city and state parms in your ini file. +#@ You will need to set the city, zone, and state parms in your ini file. #@ To verify your city, click here, #@ then click on your state, then click on "Hourly Reports". If your city -#@ is not listed in the report, pick the closest one. +#@ is not listed in the report, pick the closest one. The zone is usually +#@ the same as your city, but not always. To verify your zone, +#@ hit the Back button and click on "Zone Forecast". Zone names precede each +#@ forecast and each is followed by a hyphen. #@ To modify when this script is run (or to disable it), go to the #@ triggers page and modify the #@ 'get internet weather conditions' and 'get internet weather forecast' triggers. @@ -19,21 +22,21 @@ $v_get_internet_weather_data = new Voice_Cmd('[Get,Check,Mail,SMS] Internet weather data'); $v_get_internet_weather_data->set_info( - "Retrieves weather conditions and forecasts for $config_parms{city}, $config_parms{state}" + "Retrieves weather conditions and forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}" ); # Get the current weather data from the Internet $v_get_internet_weather_conditions = new Voice_Cmd('Get the Internet weather conditions'); $v_get_internet_weather_conditions->set_info( - "Retrieves current weather conditions for $config_parms{city}, $config_parms{state}" + "Retrieves current weather conditions for $config_parms{city}, $config_parms{state}, $config_parms{zone}" ); # Get the weather forecast from the Internet $v_get_internet_weather_forecast = new Voice_Cmd('Get the Internet weather forecast'); $v_get_internet_weather_forecast->set_info( - "Retrieves weather forecasts for $config_parms{city}, $config_parms{state}" + "Retrieves weather forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}" ); $v_show_internet_weather_forecast = @@ -52,6 +55,8 @@ my $weather_conditions_path = "$config_parms{data_dir}/web/weather_conditions.txt"; $f_weather_conditions = new File_Item($weather_conditions_path); $f_weather_forecast = new File_Item($weather_forecast_path); +my $city = $config_parms{city}; +$city = $config_parms{nws_city} if defined $config_parms{nws_city}; $p_weather_data = new Process_Item; $p_weather_conditions = new Process_Item; $p_weather_forecast = new Process_Item; @@ -76,6 +81,7 @@ sub normalize_conditions { start $p_weather_data; $v_get_internet_weather_data->respond( "app=weather Weather data requested for $config_parms{city}, $config_parms{state}" + . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { @@ -90,6 +96,7 @@ sub normalize_conditions { start $p_weather_conditions; $v_get_internet_weather_conditions->respond( "app=weather Weather conditions requested for $config_parms{city}, $config_parms{state}" + . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { @@ -104,6 +111,7 @@ sub normalize_conditions { start $p_weather_forecast; $v_get_internet_weather_forecast->respond( "app=weather Weather forecast requested for $config_parms{city}, $config_parms{state}" + . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { From e7d3215c4450232af37f1db814beea4d2d568dc0 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 22 Aug 2018 23:01:36 -0400 Subject: [PATCH 32/78] Revert "Added debug prints to troubleshoot group creation" This reverts commit 58726a5d6202598cae6def11d3b25ac15f2a2235. --- lib/Group.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Group.pm b/lib/Group.pm index 3eee7a28b..5b422f8ee 100644 --- a/lib/Group.pm +++ b/lib/Group.pm @@ -60,7 +60,6 @@ sub new { &add( $self, @items ) if @items; $self->{logger_enable} = $main::config_parms{object_logger_group} if ( defined $main::config_parms{object_logger_group} ); bless $self, $class; -&::print_log("new group: " . $self->get_object_name() . "\n"); return $self; } @@ -81,7 +80,6 @@ sub add { # This allows us to monitor changed members for my $ref (@items) { $ref->tie_items( $self, undef, 'member changed' ); -&::print_log("add to " . $self . "(" . $self->get_object_name() . "): $ref" . $ref->get_object_name() . "xxx\n"); if ( $ref->isa('X10_Item') ) { if ( can_dim($ref) ) { From 1d185cf537b17a1b6e0923d272456945c9e64904 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 22 Aug 2018 23:02:11 -0400 Subject: [PATCH 33/78] Revert "Allow overwrite of _on_set_message data" This reverts commit 2072c5ca00e474c5b0b8e48138bf2099e31971eb. --- lib/xPL_Items.pm | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/xPL_Items.pm b/lib/xPL_Items.pm index 1ac49d4ec..e73777177 100644 --- a/lib/xPL_Items.pm +++ b/lib/xPL_Items.pm @@ -833,13 +833,6 @@ sub device_name { sub on_set_message { my ( $self, @data ) = @_; - - # Delete any existing _on_set_message data so the user can - # fully overwrite with a call to the on_set_message() method. - # Otherwise we will add user data on top of the default - # _on_set_message data that is added in the constructor. - delete $$self{_on_set_message}; - while (@data) { my $section = shift @data; my $ptr = shift @data; From f4600c6d2970b7f709ac841f846c32e927d13a3d Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 22 Aug 2018 23:12:35 -0400 Subject: [PATCH 34/78] Revert local change unrelated to work performed on this branch --- code/common/vr_match.pl | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/code/common/vr_match.pl b/code/common/vr_match.pl index e17115f04..cdf51878f 100644 --- a/code/common/vr_match.pl +++ b/code/common/vr_match.pl @@ -1,16 +1,17 @@ sub phrase_match1 { my ($phrase) = @_; my (%list1); - my $d_min1 = 999; - my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; - my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); - for my $phrase2 (sort @phrases) { - my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); -# my $brianlendist = abs(length($phrase)-length($phrase2)); -# $d = $brianlendist + $d; -# print_log "---------------- $phrase --- $phrase2 --- $d"; - push @{$list1{$d}}, $phrase2 if $d <= $d_min1; - $d_min1 = $d if $d < $d_min1; + my $d_min1 = 999; + my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + my @phrases = &Voice_Cmd::voice_items( 'mh', 'no_category' ); + for my $phrase2 ( sort @phrases ) { + my $d = pdistance( $phrase, $phrase2, $set1, \&distance, { -cost => [ 0.5, 0, 4 ], -mode => 'set' } ); + + # my $brianlendist = abs(length($phrase)-length($phrase2)); + # $d = $brianlendist + $d; + # print_log "---------------- $phrase --- $phrase2 --- $d"; + push @{ $list1{$d} }, $phrase2 if $d <= $d_min1; + $d_min1 = $d if $d < $d_min1; } - return ${$list1{$d_min1}}[0]; + return ${ $list1{$d_min1} }[0]; } From fe0d9fd5f93fd6d618d76c3addefe7937c162149 Mon Sep 17 00:00:00 2001 From: Eloy Paris Date: Wed, 22 Aug 2018 23:20:18 -0400 Subject: [PATCH 35/78] Revert local change unrelated to work on this branch --- code/common/internet_weather.pl | 53 +++++++++++---------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/code/common/internet_weather.pl b/code/common/internet_weather.pl index 0e066d35f..4d032e6ed 100644 --- a/code/common/internet_weather.pl +++ b/code/common/internet_weather.pl @@ -3,7 +3,7 @@ # $Date$ # $Revision$ -#@ Retrieves current weather conditions and forecasts using bin/get_weather (US only). +#@ Retrieves current weather conditions and forecasts using bin/get_weather (US only). (MH5 Updated) #@ You will need to set the city, zone, and state parms in your ini file. #@ To verify your city, click here, #@ then click on your state, then click on "Hourly Reports". If your city @@ -19,30 +19,19 @@ #noloop=start # Get both the current weather data and forecast from the Internet -$v_get_internet_weather_data = - new Voice_Cmd('[Get,Check,Mail,SMS] Internet weather data'); -$v_get_internet_weather_data->set_info( - "Retrieves weather conditions and forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}" -); +$v_get_internet_weather_data = new Voice_Cmd('[Get,Check,Mail,SMS] Internet weather data'); +$v_get_internet_weather_data->set_info("Retrieves weather conditions and forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}"); # Get the current weather data from the Internet -$v_get_internet_weather_conditions = - new Voice_Cmd('Get the Internet weather conditions'); -$v_get_internet_weather_conditions->set_info( - "Retrieves current weather conditions for $config_parms{city}, $config_parms{state}, $config_parms{zone}" -); +$v_get_internet_weather_conditions = new Voice_Cmd('Get the Internet weather conditions'); +$v_get_internet_weather_conditions->set_info("Retrieves current weather conditions for $config_parms{city}, $config_parms{state}, $config_parms{zone}"); # Get the weather forecast from the Internet -$v_get_internet_weather_forecast = - new Voice_Cmd('Get the Internet weather forecast'); -$v_get_internet_weather_forecast->set_info( - "Retrieves weather forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}" -); - -$v_show_internet_weather_forecast = - new Voice_Cmd( '[Read Internet,What is the] weather forecast', 0 ); -$v_show_internet_weather_forecast->set_info( - 'Read previously downloaded weather forecast'); +$v_get_internet_weather_forecast = new Voice_Cmd('Get the Internet weather forecast'); +$v_get_internet_weather_forecast->set_info("Retrieves weather forecasts for $config_parms{city}, $config_parms{state}, $config_parms{zone}"); + +$v_show_internet_weather_forecast = new Voice_Cmd( '[Read Internet,What is the] weather forecast', 0 ); +$v_show_internet_weather_forecast->set_info('Read previously downloaded weather forecast'); $v_show_internet_weather_forecast->set_authority('anyone'); $v_show_internet_weather_conditions = new Voice_Cmd( '[Read Internet,What are the] weather conditions', 0 ); @@ -56,8 +45,8 @@ $f_weather_conditions = new File_Item($weather_conditions_path); $f_weather_forecast = new File_Item($weather_forecast_path); my $city = $config_parms{city}; -$city = $config_parms{nws_city} if defined $config_parms{nws_city}; -$p_weather_data = new Process_Item; +$city = $config_parms{nws_city} if defined $config_parms{nws_city}; +$p_weather_data = new Process_Item; $p_weather_conditions = new Process_Item; $p_weather_forecast = new Process_Item; $Weather_Common::weather_module_enabled = 1; @@ -79,10 +68,8 @@ sub normalize_conditions { if (&net_connect_check) { set $p_weather_data qq|get_weather -state $config_parms{state} -city "$config_parms{city}" -nws_rwr_zone $config_parms{nws_rwr_zone}|; start $p_weather_data; - $v_get_internet_weather_data->respond( - "app=weather Weather data requested for $config_parms{city}, $config_parms{state}" - . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) - ); + $v_get_internet_weather_data->respond( "app=weather Weather data requested for $config_parms{city}, $config_parms{state}" + . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { $v_get_internet_weather_data->respond("app=weather You must be connected to the Internet get weather data"); @@ -94,10 +81,8 @@ sub normalize_conditions { set $p_weather_conditions qq|get_weather -state $config_parms{state} -city "$config_parms{city}" -data conditions -nws_rwr_zone $config_parms{nws_rwr_zone}|; start $p_weather_conditions; - $v_get_internet_weather_conditions->respond( - "app=weather Weather conditions requested for $config_parms{city}, $config_parms{state}" - . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) - ); + $v_get_internet_weather_conditions->respond( "app=weather Weather conditions requested for $config_parms{city}, $config_parms{state}" + . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { $v_get_internet_weather_conditions->respond("app=weather You must be connected to the Internet get weather data"); @@ -109,10 +94,8 @@ sub normalize_conditions { set $p_weather_forecast qq|get_weather -state $config_parms{state} -city "$config_parms{city}" -data forecast|; print_log "get_weather -state $config_parms{state} -city $config_parms{city} -data forecast"; start $p_weather_forecast; - $v_get_internet_weather_forecast->respond( - "app=weather Weather forecast requested for $config_parms{city}, $config_parms{state}" - . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) - ); + $v_get_internet_weather_forecast->respond( "app=weather Weather forecast requested for $config_parms{city}, $config_parms{state}" + . ( ( $config_parms{zone} ) ? " Zone $config_parms{zone}" : '' ) ); } else { $v_get_internet_weather_forecast->respond("app=weather You must be connected to the Internet get weather data"); From d9e33e2d50821bc2eeeb161f33a95c96f6d7bbcf Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 23 Aug 2018 15:00:51 -0600 Subject: [PATCH 36/78] v1.2.1 - cleaned up some logging --- lib/Yeelight.pm | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index 1794e612e..a502f4772 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,11 +1,10 @@ package Yeelight; -# v1.2 +# v1.2.1 #TODO -#- brightness changes change state to on and off #- test queuing fast commands -#TODO check query data +#- check query data use strict; @@ -167,7 +166,7 @@ sub check_for_socket_data { $com_status = "offline"; } else { my $data; - main::print_log( "[Yeelight:" . $self->{name} . "] Data Received [$rec_data]" ); + main::print_log( "[Yeelight:" . $self->{name} . "] Data Received [$rec_data]" ) if ( $self->{debug} ); eval { $data = JSON::XS->new->decode($json_data); }; @@ -196,7 +195,7 @@ sub check_for_socket_data { if (( defined $self->{child_object}->{comm} ) and ($self->{socket_connected})) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] A Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; $self->{child_object}->{comm}->set( $com_status, 'poll' ); } @@ -300,7 +299,7 @@ sub process_check { if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] 1 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; $self->{child_object}->{comm}->set( $com_status, 'poll' ); } @@ -369,9 +368,11 @@ sub process_check { if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] 2 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); + if ($self->{child_object}->{comm}->state() ne $com_status) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } } } } @@ -426,7 +427,7 @@ sub _push_TCP_data { main::print_log( "[Yeelight:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne "offline" ) { - main::print_log "[Yeelight:" . $self->{name} . "] 3 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); $self->{status} = "offline"; $self->{child_object}->{comm}->set( "offline", 'poll' ); } @@ -963,3 +964,5 @@ sub set { # Version History # v1.0.0 - initial module # v1.0.1 - color support +# v1.2 - RGB changes +# v1.2.1 - minor fixes From 8f33804322e272a9cc0a419e8c1c173a2191aa63 Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 23 Aug 2018 15:03:26 -0600 Subject: [PATCH 37/78] v3.0.0 - motion objects and non-pausing process_item model --- lib/raZberry.pm | 711 +++++++++++++++++++++++++++----------------- lib/read_table_A.pl | 25 +- 2 files changed, 470 insertions(+), 266 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 93b92b9f7..e424c8a38 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,4 +1,13 @@ -=head1 B v2.2.1 +=head1 B v3.0 + +#test command setup +#command queue +#check hw checks, is_failed, ping, update_dev +#actually get the battery devices to get the proper value. +# - test all battery devices +#for those we can queue the two commands together. +#or set a time in the queue so that it will get executed properly, eval_with_timer + =head2 SYNOPSIS @@ -31,6 +40,8 @@ RAZBERRY_LOCK, device_id, name, group, controller_name, options RAZBERRY_THERMOSTAT, device_id, name, group, controller_name, options RAZBERRY_TEMP_SENSOR, device_id, name, group, controller_name, options RAZBERRY_BINARY_SENSOR, device_id, name, group, controller_name, options +RAZBERRY_MOTION, device_id, name, group, controller_name, options +RAZBERRY_BRIGHTNESS, device_id, name, group, controller_name, options RAZBERRY_GENERIC, device_id, name, group, controller_name, options * Note GENERIC requires the full device ID, ie 2-0-48-1 @@ -62,7 +73,7 @@ Devices need to first included inside the razberry zwave network using the inclu The Razberry is polled on a regular basis in order to update local objects. By default, the razberry is polled every 5 seconds. Push relies on the razberry to execute a httpget at state change. -raZberry will still check in every 10 minutes just to be safe +raZberry will still check in every 10 minutes just to ensure there is state syncing if pushes are missed. Update for local control use the 'niffler' plug in. This saves forcing a local device status every poll. @@ -77,8 +88,8 @@ razberry controller. =head2 RaZberry v2 AUTHENTICATION -No authentication required with v2.0.0. It _should_ also work with v1.7.4. -For later versions, Z_Way has introduced authentication. raZberry v2.0 supports this via two methods: +No authentication required with fw v2.0.0. It _should_ also work with fw v1.7.4. +For later versions, Z_Way has introduced authentication. raZberry v2.0+ supports this via two methods: 1: Enable anonymous authentication: - Create a room named devices, and assign all ZWay devices to that room @@ -92,7 +103,7 @@ Then in the controller definition, provide the username and password: $razberry_controller = new raZberry('10.0.1.1',10,"user=user,password=pwd"); -=head2 v2 PUSH or POLL. Only tested in version raZberry 2.3.5 +=head2 v2 PUSH or POLL. Only tested in version raZberry 2.3.5, 2.3.7 Using the HTTPGet automation module, this will 'push' a status change to MH rather than the constant polling. Use the following URL for updating: http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%,X) where X is the instance. If ommitted, assume instance 1. @@ -110,12 +121,9 @@ raZberry_password =head2 BUGS - -=head2 OTHER -http calls can cause pauses. There are a few possible options around this; -- push output to a file and then read the file. This is generally how other modules work. - -Changelog moved to bottom of file. +-controller failover doesn't work due to the zwave lifeline association can only be set to one device. + A secondary controller can operate devices, but the secondary will not be updated when it's state changes + It can be triggered to get device updates, but that adds more complexity. =over @@ -134,7 +142,7 @@ use HTTP::Request::Common qw(POST); use HTTP::Cookies; use JSON qw(decode_json); -#use Data::Dumper; +use Data::Dumper; @raZberry::ISA = ('Generic_Item'); @@ -174,7 +182,7 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v2.2.1 Controller Initializing..."); + &main::print_log("[raZberry]: v3.0.0 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; $self->{config}->{poll_seconds} = 5; @@ -195,7 +203,7 @@ sub new { $self->{host} = $host; $self->{port} = 8083; $self->{port} = $port if ($port); - $self->{debug} = 0; + $self->{debug} = 5; ( $self->{debug} ) = ( $options =~ /debug=(\d+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); $self->{debug} = $main::Debug{razberry} if ( defined $main::Debug{razberry} ); $self->{lastupdate} = undef; @@ -232,7 +240,8 @@ sub new { &main::print_log("[raZberry:" . $self->{host} . "]: Poll method selected"); } } - + + $self->{cookie_string} = ""; if ( $self->{username} ) { $self->{cookie_jar} = HTTP::Cookies->new( {} ); $self->login; @@ -243,19 +252,31 @@ sub new { $self->{controllers}->{failover_time} = 0; $self->{controllers}->{failover_threshold} = 120; - $self->get_controllerdata; $self->{timer} = new Timer; - $self->poll; - if (defined $self->{data}->{devices}) { - &main::print_log("[raZberry:" . $self->{host} . "]: Devices:\t\t\t" . (keys %{ $self->{data}->{devices} })); - } else { - &main::print_log("[raZberry:" . $self->{host} . "]: No Devices found on controller!"); - } + + $self->{poll_data_file} = "$::config_parms{data_dir}/raZberry_poll_" . $self->{host} . ".data"; + unlink "$::config_parms{data_dir}/raZberrry_poll_" . $self->{host} . ".data"; + $self->{poll_process} = new Process_Item; + $self->{poll_process}->set_output( $self->{poll_data_file} ); + @{ $self->{cmd_queue} } = (); + $self->{cmd_data_file} = "$::config_parms{data_dir}/raZberry_cmd_" . $self->{host} . ".data"; + unlink "$::config_parms{data_dir}/raZberry_cmd_" . $self->{host} . ".data"; + $self->{cmd_process} = new Process_Item; + $self->{cmd_process}->set_output( $self->{cmd_data_file} ); + $self->{max_cmd_queue} = 4; + + $self->{com_warning} = 0; + $self->{com_threshold} = 4; + $self->{com_poll_interval} = undef; + + &::MainLoop_post_add_hook( \&raZberry::process_check, 0, $self ); + &main::print_log("[raZberry:" . $self->{host} . "]: Instance:\t\t" . $self->{instance}); - $self->start_timer; $self->{generate_voice_cmds} = 0; &::Reload_post_add_hook( \&raZberry::generate_voice_commands, 0, $self ); - &main::print_log("[raZberry:" . $self->{host} . "]: Controller Initialization Complete"); + + $self->get_controllerdata; + return $self; } @@ -279,9 +300,7 @@ sub login { my $responseObj = $ua->request($request); $self->{cookie_jar}->extract_cookies($responseObj); $self->{cookie_jar}->save; - - #print Dumper $self->{cookie_jar}; - #print $json . "\n"; + #print $responseObj->content . "\n--------------------\n"; if ( $responseObj->code > 400 ) { $self->{login_success} = 0; @@ -291,28 +310,24 @@ sub login { else { &main::print_log("[raZberry:" . $self->{host} . "]: Successful authentication."); $self->{login_success} = 1; + #print Dumper $self->{cookie_jar}; + #print $json . "\n"; + $self->{cookie_string} = $self->{cookie_jar}->as_string(); + $self->{cookie_string} =~ s/^Set-Cookie3: //; #strip out the cookie header that http::cookies returns + $self->{cookie_string} =~ s/\n//; #strip out the \n that http::cookies returns + #print "***** [$self->{cookie_string}]\n"; } } sub get_controllerdata { my ($self) = @_; - my ( $isSuccessResponse1, $controller_data ) = _get_JSON_data( $self, 'controller' ); - if ($isSuccessResponse1) { + _get_JSON_data( $self, 'controller' ); - #print Dumper $controller_data; - $self->{controller_data} = $controller_data->{controller}->{data}; - &main::print_log("[raZberry:" . $self->{host} . "]: Controller found"); - &main::print_log("[raZberry:" . $self->{host} . "]: Chip version:\t\t" . $self->{controller_data}->{ZWaveChip}->{value} ); - &main::print_log("[raZberry:" . $self->{host} . "]: Software version:\t" . $self->{controller_data}->{softwareRevisionVersion}->{value} ); - &main::print_log("[raZberry:" . $self->{host} . "]: API version:\t\t" . $self->{controller_data}->{APIVersion}->{value} ); - &main::print_log("[raZberry:" . $self->{host} . "]: SDK version:\t\t" . $self->{controller_data}->{SDK}->{value} ); - } - else { - &main::print_log( "[raZberry:" . $self->{host} . "]: Problem getting controller data" ); - $self->controller_failover; - } } +#-------------- Secondary controllers don't quite work properly, leaving code in in case a method +#-------------- to move the lifeline becomes available in the future + sub add_backup_controller { my ($self,$object) = @_; @@ -376,6 +391,193 @@ sub controller_failback { } +sub process_check { + my ($self) = @_; + my @process_data = (); + my $com_status; + #In order to process multiple queues (one for poll, one for command), push the returned text into an array and then process the array + #The Command queue might have waiting commands so check the queue and pop one off + +#if process is done and an error returned on poll, then increment warning. If on push mode, then change to 10 seconds. If +#successful and on push mode, then change time + + return unless ( ( defined $self->{poll_process} ) and ( defined $self->{cmd_process} ) ); + +#check if data comes back unauthenticated + + if ( $self->{poll_process}->done_now() ) { + + $self->start_timer; #data has come in, so start the timer. + + $com_status = "online"; + main::print_log( "[raZerry:" . $self->{host} . "] Background poll " . $self->{poll_process_mode} . " process completed" ) if ( $self->{debug} ); + + my $file_data = &main::file_read( $self->{poll_data_file} ); + exit unless ($file_data); #if there is no data, then don't process + +# print "debug: file_data=$file_data\n" if ( $self->{debug} > 2); + my ($json_data) = $file_data =~ /(\{.*\})/s; + +# print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); + unless ( ($file_data) and ($json_data) ) { + main::print_log( "[raZberry:" . $self->{host} . "] ERROR! bad data returned by poll" ); + main::print_log( "[raZberry:" . $self->{host} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); + $com_status = "offline"; + } else { + push @process_data, $json_data; + } + } + if ( $self->{cmd_process}->done_now() ) { + + $com_status = "online"; + main::print_log( "[raZerry:" . $self->{host} . "] Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); + + my $file_data = &main::file_read( $self->{cmd_data_file} ); + exit unless ($file_data); #if there is no data, then don't process + + if ($self->{cmd_process_mode} eq "usercode") { + #normally usercode just returns null + if ($file_data ne "null") { + main::print_log( "[raZberry:" . $self->{host} . "] WARNING, unexpected return data from usercode: ($file_data)" ); + } + } else { + + # print "debug: file_data=$file_data\n" if ( $self->{debug} > 2); + my ($json_data) = $file_data =~ /(\{.*\})/s; + + # print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); + unless ( ($file_data) and ($json_data) ) { + main::print_log( "[raZberry:" . $self->{host} . "] ERROR! bad data returned by poll" ); + main::print_log( "[raZberry:" . $self->{host} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); + $com_status = "offline"; + } else { + push @process_data, $json_data; + shift @{ $self->{cmd_queue} }; + if (scalar @{ $self->{cmd_queue} }) { + main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); + my ($mode, $get_cmd) = ${ $self->{cmd_queue} }[0]; + $self->{cmd_process}->set($get_cmd); + $self->{cmd_process}->start(); + $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{cmd_process_mode} = $mode; + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command" . $self->{cmd_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); + } + } + } + } + + foreach my $rec_data (@process_data) { + my $data; + eval { $data = JSON::XS->new->decode($rec_data); }; + # catch crashes: + if ($@) { + main::print_log( "[raZberry:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; + } + else { + if ((defined $data->{controller}->{data}) and (!defined $self->{controller_data})) { + $self->{controller_data} = $data->{controller}->{data}; + &main::print_log("[raZberry:" . $self->{host} . "]: Controller found"); + &main::print_log("[raZberry:" . $self->{host} . "]: Chip version:\t\t" . $self->{controller_data}->{ZWaveChip}->{value} ); + &main::print_log("[raZberry:" . $self->{host} . "]: Software version:\t" . $self->{controller_data}->{softwareRevisionVersion}->{value} ); + &main::print_log("[raZberry:" . $self->{host} . "]: API version:\t\t" . $self->{controller_data}->{APIVersion}->{value} ); + &main::print_log("[raZberry:" . $self->{host} . "]: SDK version:\t\t" . $self->{controller_data}->{SDK}->{value} ); + &main::print_log("[raZberry:" . $self->{host} . "]: Controller Initialization Complete"); + } + + $self->{lastupdate} = $data->{data}->{updateTime}; + foreach my $item ( @{ $data->{data}->{devices} } ) { + next if ($item->{id} =~ m/_Int$/); #ignore some funny 2.3.5 devices + next if ($item->{id} =~ m/^MobileAppSupport/); + next if ($item->{id} =~ m/^BatteryPolling_/); + + &main::print_log("[raZberry:" . $self->{host} . "]: Found:" . $item->{id} . " with level " . $item->{metrics}->{level} . " and updated " . $item->{updateTime} . "." ) if ( $self->{debug} ); + &main::print_log("[raZberry:" . $self->{host} . "]: WARNING: device " . $item->{id} . " level is undefined") if ( ( !defined $item->{metrics}->{level} ) or ( lc $item->{metrics}->{level} eq "undefined" ) ); + my ($id) = ( split /_/, $item->{id} )[-1]; #always just get the last element + print "id=$id\n" if ( $self->{debug} > 1 ); + + my $battery_dev = 0; + $battery_dev = 1 if ( $id =~ m/-0-128$/ ); + my $voltage_dev = 0; + $voltage_dev = 1 if ( $id =~ m/-0-50-\d$/ ); + + if ($battery_dev) { #for a battery, set a different object + $self->{data}->{devices}->{$id}->{battery_level} = $item->{metrics}->{level}; + } + elsif ($voltage_dev) { + &main::print_log("[raZberry:" . $self->{host} . "]: Voltage Device found"); + } + else { + $self->{data}->{devices}->{$id}->{level} = $item->{metrics}->{level}; + } + $self->{data}->{devices}->{$id}->{updateTime} = $item->{updateTime}; + $self->{data}->{devices}->{$id}->{devicetype} = $item->{deviceType}; + $self->{data}->{devices}->{$id}->{location} = $item->{location}; + $self->{data}->{devices}->{$id}->{title} = $item->{metrics}->{title}; + $self->{data}->{devices}->{$id}->{icon} = $item->{metrics}->{icon}; + + #thermostat data items + $self->{data}->{devices}->{$id}->{units} = $item->{metrics}->{scaleTitle} if ( defined $item->{metrics}->{scaleTitle} ); + $self->{data}->{devices}->{$id}->{temp_min} = $item->{metrics}->{min} if ( defined $item->{metrics}->{min} ); + $self->{data}->{devices}->{$id}->{temp_max} = $item->{metrics}->{max} if ( defined $item->{metrics}->{max} ); + + $self->{status} = "online"; + + if ( defined $self->{child_object}->{$id} ) { + if ($battery_dev) { + &main::print_log("[raZberry:" . $self->{host} . "]: Child object detected: Battery Level:[" + . $item->{metrics}->{level} + . "] Child Level:[" + . $self->{child_object}->{$id}->battery_level() + . "]" ) + if ( $self->{debug} > 1 ); + my $data; + $data->{battery_level} = $item->{metrics}->{level}; + $self->{child_object}->{$id}->update_data( $data ); #be able to push other data to objects for actions + } + else { + &main::print_log("[raZberry:" . $self->{host} . "]: Child object detected: Controller Level:[" + . $item->{metrics}->{level} + . "] Child Level:[" + . $self->{child_object}->{$id}->level() + . "]" ) + if ( $self->{debug} > 1 ); + $self->{child_object}->{$id}->set( $item->{metrics}->{level}, 'poll' ) + if ( ( $self->{child_object}->{$id}->level() ne $item->{metrics}->{level} ) + and !( $id =~ m/-0-128$/ ) ); + $self->{child_object}->{$id}->update_data( $self->{data}->{devices}->{$id} ); #be able to push other data to objects for actions + } + } + } + } + } + if ( defined $self->{child_object}->{comm} ) { + #if an offline status is received, do a few more polls. for push, the raZberry is polled every 10 minutes, + #so sometimes a false positive can be created if that moment throws an error 500 + if ($com_status eq "online") { + $self->{com_warning} = 0; + $self->{config}->{poll_seconds} = $self->{com_poll_interval} if (defined $self->{com_poll_interval}); + $self->{com_poll_interval} = undef; + $self->stop_timer; + $self->start_timer; + } else { + main::print_log("[RaZberry:" . $self->{host} . "] WARNING. Recevied bad data from raZberry. Temporarily Increasing poll rate to confirm if device is offline.") if ($self->{com_warning} == 0); + $self->{com_warning}++; + $self->{com_poll_interval} = $self->{config}->{poll_seconds}; + $self->{config}->{poll_seconds} = 10 unless ($self->{config}->{poll_seconds} <= 10); + $self->stop_timer; + $self->start_timer; + } + if ( $self->{status} ne $com_status ) { + $self->{status} = $com_status; + if (($self->{child_object}->{comm}->state() ne $com_status) or (($self->{com_warning} > $self->{com_threshold}) and ($com_status eq "offline"))) { + main::print_log("[RaZberry:" . $self->{host} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "...") if ( $self->{loglevel} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } +} + sub poll { my ( $self, $option ) = @_; @@ -397,106 +599,23 @@ sub poll { #$self->ping_dev($dev); } - my ( $isSuccessResponse1, $devices ) = _get_JSON_data( $self, 'devices', $cmd ); + _get_JSON_data( $self, 'devices', $cmd ); - # print Dumper $devices if ( $self->{debug} > 1 ); - if ($isSuccessResponse1) { - $self->{lastupdate} = $devices->{data}->{updateTime}; - foreach my $item ( @{ $devices->{data}->{devices} } ) { - next if ($item->{id} =~ m/_Int$/); #ignore some funny 2.3.5 devices - next if ($item->{id} =~ m/^MobileAppSupport/); - next if ($item->{id} =~ m/^BatteryPolling_/); - - &main::print_log("[raZberry:" . $self->{host} . "]: Found:" . $item->{id} . " with level " . $item->{metrics}->{level} . " and updated " . $item->{updateTime} . "." ) if ( $self->{debug} ); - &main::print_log("[raZberry:" . $self->{host} . "]: WARNING: device " . $item->{id} . " level is undefined") if ( ( !defined $item->{metrics}->{level} ) or ( lc $item->{metrics}->{level} eq "undefined" ) ); - my ($id) = ( split /_/, $item->{id} )[-1]; #always just get the last element - print "id=$id\n" if ( $self->{debug} > 1 ); - - my $battery_dev = 0; - $battery_dev = 1 if ( $id =~ m/-0-128$/ ); - my $voltage_dev = 0; - $voltage_dev = 1 if ( $id =~ m/-0-50-\d$/ ); - - if ($battery_dev) { #for a battery, set a different object - $self->{data}->{devices}->{$id}->{battery_level} = $item->{metrics}->{level}; - } - elsif ($voltage_dev) { - &main::print_log("[raZberry:" . $self->{host} . "]: Voltage Device found"); - } - else { - $self->{data}->{devices}->{$id}->{level} = $item->{metrics}->{level}; - } - $self->{data}->{devices}->{$id}->{updateTime} = $item->{updateTime}; - $self->{data}->{devices}->{$id}->{devicetype} = $item->{deviceType}; - $self->{data}->{devices}->{$id}->{location} = $item->{location}; - $self->{data}->{devices}->{$id}->{title} = $item->{metrics}->{title}; - $self->{data}->{devices}->{$id}->{icon} = $item->{metrics}->{icon}; - - #thermostat data items - $self->{data}->{devices}->{$id}->{units} = $item->{metrics}->{scaleTitle} - if ( defined $item->{metrics}->{scaleTitle} ); - $self->{data}->{devices}->{$id}->{temp_min} = $item->{metrics}->{min} - if ( defined $item->{metrics}->{min} ); - $self->{data}->{devices}->{$id}->{temp_max} = $item->{metrics}->{max} - if ( defined $item->{metrics}->{max} ); - - $self->{status} = "online"; - - if ( defined $self->{child_object}->{$id} ) { - if ($battery_dev) { - &main::print_log("[raZberry:" . $self->{host} . "]: Child object detected: Battery Level:[" - . $item->{metrics}->{level} - . "] Child Level:[" - . $self->{child_object}->{$id}->battery_level() - . "]" ) - if ( $self->{debug} > 1 ); - my $data; - $data->{battery_level} = $item->{metrics}->{level}; - $self->{child_object}->{$id}->update_data( $data ); #be able to push other data to objects for actions - } - else { - &main::print_log("[raZberry:" . $self->{host} . "]: Child object detected: Controller Level:[" - . $item->{metrics}->{level} - . "] Child Level:[" - . $self->{child_object}->{$id}->level() - . "]" ) - if ( $self->{debug} > 1 ); - $self->{child_object}->{$id}->set( $item->{metrics}->{level}, 'poll' ) - if ( ( $self->{child_object}->{$id}->level() ne $item->{metrics}->{level} ) - and !( $id =~ m/-0-128$/ ) ); - $self->{child_object}->{$id}->update_data( $self->{data}->{devices}->{$id} ); #be able to push other data to objects for actions - } - } - } - } - else { - &main::print_log("[raZberry:" . $self->{host} . "]: Problem retrieving data from controller" ); - $self->{data}->{retry}++; - return ('0'); - } return ('1'); } sub set_dev { my ( $self, $device, $mode ) = @_; - &main::print_log("[raZberry:" . $self->{host} . "]: set_dev Setting $device to $mode") - if ( $self->{debug} ); + &main::print_log("[raZberry:" . $self->{host} . "]: set_dev Setting $device to $mode") if ( $self->{debug} ); my $cmd; my ( $action, $lvl ) = ( split /=/, $mode )[ 0, 1 ]; if ( defined $rest{$action} ) { $cmd = "/$zway_vdev" . "_" . $device . "/$rest{$action}"; $cmd .= "$lvl" if $lvl; - &main::print_log("[raZberry:" . $self->{host} . "]: sending command $cmd") - if ( $self->{debug} > 1 ); - my ( $isSuccessResponse1, $status ) = _get_JSON_data( $self, 'devices', $cmd ); - unless ($isSuccessResponse1) { - &main::print_log( "[raZberry]: Problem retrieving data from " . $self->{host} ); - return ('0'); - } - - # print Dumper $status if ( $self->{debug} > 1 ); + &main::print_log("[raZberry:" . $self->{host} . "]: sending command $cmd") if ( $self->{debug} > 1 ); + _get_JSON_data( $self, 'devices', $cmd ); } } @@ -561,96 +680,54 @@ sub update_dev { sub _get_JSON_data { my ( $self, $mode, $cmd ) = @_; - unless ( $self->{updating} ) { - - $self->{updating} = 1; - my $ua = new LWP::UserAgent( keep_alive => 1 ); - $ua->timeout( $self->{timeout} ); - $ua->cookie_jar( $self->{cookie_jar} ) if ( $self->{username} ); - my $host = $self->{host}; - my $port = $self->{port}; - my $params = ""; - $params = $cmd if ($cmd); - my $method = "ZAutomation/api/v1"; - $method = "ZWaveAPI/Run" - if ( ( $mode eq "force_update" ) - or ( $mode eq "ping" ) - or ( $mode eq "isfailed" ) - or ( $mode eq "usercode" ) - or ( $mode eq "usercode_data" ) ); - $method = "ZWaveAPI" if ( $mode eq "controller" ); - &main::print_log("[raZberry:" . $self->{host} . "]: contacting http://$host:$port/$method/$rest{$mode}$params") if ( $self->{debug} ); - - my $request = HTTP::Request->new( GET => "http://$host:$port/$method/$rest{$mode}$params" ); - $request->content_type("application/x-www-form-urlencoded"); - - #if unauthenticated, then try another login attempt. - my $connect_req = 0; - my $responseObj; - my $responseCode; - do { - $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" if ( $self->{debug} > 1 ); - $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if ( $self->{debug} > 1 ); - if ( ( $responseCode == 401 ) and ( !$connect_req ) ) { - &main::print_log("[raZberry:" . $self->{host} . "]: ReAuthenticating..."); - $self->login; - $connect_req = 1; - } - else { - $connect_req = 2; - } - } until ( $connect_req == 2 ); - - my $isSuccessResponse = $responseCode < 400; - $self->{updating} = 0; - if ( !$isSuccessResponse ) { - &main::print_log("[raZberry:" . $self->{host} . "]: Warning, failed to get data. Response code $responseCode"); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} eq "online" ) { - $self->{status} = "offline"; - main::print_log "[raZberry]: Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{child_object}->{comm}->set( "offline", 'poll' ); - $self->controller_failover($cmd); - } - } - return ('0'); - } - $self->controller_failback; - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} eq "offline" ) { - $self->{status} = "online"; - main::print_log "[raZberry]: Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to online..." - if ( $self->{loglevel} ); - $self->{child_object}->{comm}->set( "online", 'poll' ); + my $cookie = ""; + $cookie = $self->{cookie_string} if ( $self->{cookie_string} ); + my $host = $self->{host}; + my $port = $self->{port}; + my $params = ""; + $params = $cmd if ($cmd); + $cmd = "" unless (defined $cmd); + my $method = "ZAutomation/api/v1"; + $method = "ZWaveAPI/Run" + if ( ( $mode eq "force_update" ) + or ( $mode eq "ping" ) + or ( $mode eq "isfailed" ) + or ( $mode eq "usercode" ) + or ( $mode eq "usercode_data" ) ); + $method = "ZWaveAPI" if ( $mode eq "controller" ); + &main::print_log("[raZberry:" . $self->{host} . "]: contacting http://$host:$port/$method/$rest{$mode}$params") if ( $self->{debug} ); + my $get_params = "-ua "; + $get_params .= "-cookies " . "'" . $cookie . "' " if ($cookie ne ""); + my $get_cmd = "get_url $get_params " . '"http://' . "$host:$port/$method/$rest{$mode}$params" . '"'; + + if (( $cmd eq "") or ($cmd =~ m/^\?since=/)) { + + $self->{poll_process}->stop() unless ($self->{poll_process}->done() ); + $self->{poll_process}->set($get_cmd); + $self->{poll_process}->start(); + $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{poll_process_mode} = $mode; + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Poll" . $self->{poll_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); + } else { + if ($self->{cmd_process}->done() ) {; + $self->{cmd_process}->set($get_cmd); + $self->{cmd_process}->start(); + $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{cmd_process_mode} = $mode; + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command" . $self->{poll_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); + } else { + main::print_log( "[raZberry:" . $self->{host} . "] Queing Command" . $self->{poll_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); + push @{ $self->{cmd_queue} }, [$mode,$get_cmd]; + if (scalar @{ $self->{cmd_queue} } < $self->{max_poll_queue} ) { + main::print_log( "[raZberry:" . $self->{host} . "] Max Queue Length ($self->{max_poll_queue}) reached! Discarding all queued commands!" ); + @{ $self->{cmd_queue} } = (); } - } - return ('1') - if ( ( $mode eq "force_update" ) - or ( $mode eq "ping" ) - or ( $mode eq "usercode" ) ); #these come backs as nulls which crashes JSON::XS, so just return. - return ( $responseObj->content ) if ( $mode eq "isfailed" ); - - # my $response = JSON::XS->new->decode( $responseObj->content ); - my $response; - eval { - $response = decode_json( $responseObj->content ); #HP, wrap this in eval to prevent MH crashes - }; - if ($@) { - &main::print_log("[raZberry:" . $self->{host} . "]: WARNING: decode_json failed for returned data"); - return ( "0", "" ); - } - return ( $isSuccessResponse, $response ) - - } - else { - &main::print_log("[raZberry:" . $self->{host} . "]: Warning, not fetching data due to operation in progress"); - return ('0'); + } } + + # return ( $isSuccessResponse, $response ), need different responses for force_update, ping and usercode + return ("1", ""); + } sub stop_timer { @@ -686,7 +763,6 @@ sub get_dev_status { if ( defined $self->{data}->{devices}->{$id} ) { return $self->{data}->{devices}->{$id}->{level}; - } else { @@ -703,7 +779,6 @@ sub register { $self->{child_object}->{'comm'} = $object; } else { - #TODO my $type = $object->{type}; $type = "Digital " . $type if ( ( defined $options ) and ( $options =~ m/digital/ ) ); @@ -764,7 +839,7 @@ sub main::razberry_push { } } else { - &main::print_log("[raZberry]: ERROR, child object id $id not found!"); + &main::print_log("[raZberry]: ERROR, child object id $id not found! (level is $level)"); } } @@ -778,9 +853,8 @@ sub main::razberry_push { $raz_push_obj->{$instance}->{status} = "online"; main::print_log "[raZberry]: Successful push request, updating communication object from " . $raz_push_obj->{$instance}->{child_object}->{comm}->state() . " to online..."; $raz_push_obj->{$instance}->{child_object}->{comm}->set( "online", 'push' ); - } else {main::print_log ("[razberry]: status is [" . $raz_push_obj->{$instance}->{status} ."]"); } - } else {main::print_log ("[razberry]: no comm child??"); } - + } + } return ""; } @@ -1185,28 +1259,36 @@ sub update_data { } sub battery_check { - my ($self) = @_; + my ($self, $report) = @_; unless ( $self->{battery} ) { main::print_log("[raZberry_blind] ERROR, battery option not defined on this object"); return; } - if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ) { - $$self{master_object}->poll("full"); - if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ) { - main::print_log("[raZberry_blind] INFO Battery level currently undefined"); - return; + $report = 0 unless (defined $report); + if ($report) { + &main::print_log( "[raZberry_blind] INFO Battery currently at " . $self->{battery_level} . "%" ); + if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { + $self->{battery_alert} = 1; + &main::speak("Warning, Zwave blind battery has less than 30% charge"); } - } - main::print_log( "[raZberry_blind] INFO Battery currently at " . $self->{battery_level} . "%" ); - if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { - $self->{battery_alert} = 1; - main::speak("Warning, Zwave blind battery has less than 30% charge"); - } - else { - $self->{battery_alert} = 0; + else { + $self->{battery_alert} = 0; + } + return $self->{battery_level}; + } else { + + my $cmd; + my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5D128%5B.Get()"; + &main::print_log("[raZberry]: Getting Battery Details") if ( $self->{debug} ); + &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); + &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); + main::eval_with_timer( sub { &raZberry_lock::battery_check($self,1) }, 10 ); + } return $self->{battery_level}; + } sub _battery_timer { @@ -1329,23 +1411,32 @@ sub update_data { } sub battery_check { - my ($self) = @_; - if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ) { - $$self{master_object}->poll("full"); - if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ) { - main::print_log("[raZberry_lock] INFO Battery level currently undefined"); - return; + my ($self,$report) = @_; + #issue the get command, and then check the result about 10 seconds later + $report = 0 unless (defined $report); + if ($report) { + &main::print_log( "[raZberry_lock] INFO Battery currently at " . $self->{battery_level} . "%" ); + if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { + $self->{battery_alert} = 1; + &main::speak("Warning, Zwave lock battery has less than 30% charge"); } - } - &main::print_log( "[raZberry_lock] INFO Battery currently at " . $self->{battery_level} . "%" ); - if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { - $self->{battery_alert} = 1; - &main::speak("Warning, Zwave lock battery has less than 30% charge"); - } - else { - $self->{battery_alert} = 0; + else { + $self->{battery_alert} = 0; + } + return $self->{battery_level}; + } else { + + my $cmd; + my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5D128%5B.Get()"; + &main::print_log("[raZberry]: Getting Battery Details") if ( $self->{debug} ); + &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); + &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); + main::eval_with_timer( sub { &raZberry_lock::battery_check($self,1) }, 10 ); + } return $self->{battery_level}; + } sub enable_user { @@ -1770,23 +1861,31 @@ sub update_data { } sub battery_check { - my ($self) = @_; - if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ) { - $$self{master_object}->poll("full"); - if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ) { - main::print_log("[raZberry_battery] INFO Battery level currently undefined"); - return; + my ($self, $report) = @_; + $report = 0 unless (defined $report); + if ($report) { + &main::print_log( "[raZberry_battery] INFO Battery currently at " . $self->{battery_level} . "%" ); + if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { + $self->{battery_alert} = 1; + &main::speak("Warning, Zwave lock battery has less than 30% charge"); } - } - main::print_log( "[raZberry_battery] INFO Battery currently at " . $self->{battery_level} . "%" ); - if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { - $self->{battery_alert} = 1; - main::speak("Warning, Zwave battery has less than 30% charge"); - } - else { - $self->{battery_alert} = 0; + else { + $self->{battery_alert} = 0; + } + return $self->{battery_level}; + } else { + + my $cmd; + my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5D128%5B.Get()"; + &main::print_log("[raZberry]: Getting Battery Details") if ( $self->{debug} ); + &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); + &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); + main::eval_with_timer( sub { &raZberry_lock::battery_check($self,1) }, 10 ); + } return $self->{battery_level}; + } package raZberry_voltage; @@ -1907,7 +2006,89 @@ sub update_data { my ( $self, $data ) = @_; } +package raZberry_motion; +@raZberry_openclose::ISA = ('raZberry_binary_sensor'); + +sub new { + my ( $class, $object, $devid, $options ) = @_; + + my $self = $class->SUPER::new( $object, $devid, $options ); + + @{$$self{states}} = ('motion','still'); + return $self; +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { + $self->{level} = $p_state; + my $n_state; + if ( $p_state eq "on" ) { + $n_state = "motion"; + } + else { + $n_state = "still"; + } + main::print_log( "[raZberry]: Setting motion value to $n_state. Level is " . $self->{level} ) + if ( $self->{debug} ); + $self->SUPER::set($n_state); + } + else { + main::print_log("[raZberry]: ERROR Can not set state $p_state for motion"); + } +} + +package raZberry_brightness; +@raZberry_generic::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $devid, $options ) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + + $$self{master_object} = $object; + $$self{type} = "Brightness"; + $devid = $devid . "-49-3" if ( $devid =~ m/^\d+$/ ); + $$self{devid} = $devid; + $object->register( $self, $devid, $options ); + + $self->{level} = ""; + $self->{debug} = $object->{debug}; + return $self; + +} + +sub level { + my ($self) = @_; + + return ( $self->{level} ); +} + +sub ping { + my ($self) = @_; + + $$self{master_object}->ping_dev( $$self{devid} ); +} + +sub isfailed { + my ($self) = @_; + + $$self{master_object}->isfailed_dev( $$self{devid} ); +} + +#08/19/18 03:15:35 PM [raZberry]: ERROR, child object id 18-0-48-1 not found! +#08/19/18 03:16:23 PM [raZberry]: ERROR, child object id 18-0-49-3 not found! +#08/19/18 03:16:23 PM [raZberry]: ERROR, child object id 18-0-37 not found! + +# ZWayVDev_zway_18-0-113-8-1-A =head2 CHANGELOG +v3.0 +- added 3 10 second check on push mode status pull +- use process_item to prevent pauses +- added motion sensor. Motion/Still and Brightness + v2.2.1 - fixed thermostat to check for sub device @@ -1948,4 +2129,4 @@ v1.2 - added a check to see if the device is 'dead'. If dead it will attempt a ping for X attempts a Y seconds apart. -=cut \ No newline at end of file +=cut diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 510f596f7..3f5747a58 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1642,7 +1642,30 @@ sub read_table_A { $object = "raZberry_voltage(\$" . $controller . ",'$devid')"; } } - + elsif ( $type eq "RAZBERRY_MOTION" ) { + ## + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if ($other) { + $object = "raZberry_motion(\$" . $controller . ",'$devid','$other')"; + } + else { + $object = "raZberry_motion(\$" . $controller . ",'$devid')"; + } + } + elsif ( $type eq "RAZBERRY_BRIGHTNESS" ) { + ## + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if ($other) { + $object = "raZberry_brightness(\$" . $controller . ",'$devid','$other')"; + } + else { + $object = "raZberry_brightness(\$" . $controller . ",'$devid')"; + } + } #-------------- End of RaZberry Objects ----------------- # -[ MySensors ]------------------------------------------------------ From cde04abc7a261605da7c6a53f200ccc4e183beb0 Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 23 Aug 2018 15:14:07 -0600 Subject: [PATCH 38/78] v2.0.750 - color slider control --- lib/http_server.pl | 9 ++- lib/json_server.pl | 48 ++++++++--- web/ia7/include/javascript.js | 89 +++++++++++++++++---- web/ia7/include/jquery.longclick-1.0.min.js | 27 ++++++- web/ia7/include/tables.css | 4 +- web/ia7/index.shtml | 20 +++-- 6 files changed, 157 insertions(+), 40 deletions(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index ce64ee87e..6a6f35875 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -684,6 +684,11 @@ sub http_process_request { # print "Error, no SET argument: $header\n" unless $get_arg; + #allow setby to be passed in URL. Objects can then take a setby argument if there is an alternative action. + #used for RGB. Downside is that the true setby (web) would be lost. + my $get_arg_setby = ""; + ($get_arg_setby) = $get_arg =~ /select_setby=(\S+)/; + $get_arg =~ s/select_setby=(\S+)// if ($get_arg_setby); # Change select_item=$item&select_state=abc to $item=abc $get_arg =~ s/select_item=(\S+)\&&select_state=/$1=/; @@ -758,10 +763,10 @@ sub http_process_request { # Can be a scalar or a object $state =~ tr/\"/\'/; # So we can use "" to quote it - + $get_arg_setby = "web [$client_ip_address]" unless $get_arg_setby; # my $eval_cmd = qq[($item and ref($item) and UNIVERSAL::isa($item, 'Generic_Item')) ? my $eval_cmd = qq[($item and ref($item) ne '' and ref($item) ne 'SCALAR' and $item->can('set')) ? - ($item->set("$state", "web [$client_ip_address]")) : ($item = "$state")]; + ($item->set("$state", "$get_arg_setby")) : ($item = "$state")]; print "SET eval: $eval_cmd\n" if $main::Debug{http}; eval $eval_cmd; print "SET eval error. cmd=$eval_cmd error=$@\n" if $@; diff --git a/lib/json_server.pl b/lib/json_server.pl index fa3e1c1e5..79b747630 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -883,34 +883,52 @@ sub json_get { } if ( $path[0] eq 'security' ) { - #check if $Authorized if (defined $path[1] and $path[1] eq 'authorize') { - print "IN AUTHORIZE\n"; + # Passwords are stored as MD5 hashes in the user data file + # Take that MD5, then take the current date (in YYYYDDMM format) and then calculate + # an authorization MD5 value. Adding in the current date means that the lifespan of a compromised + # password token is at most 1 day. my $status = ""; if ($args{user} && $args{user}[0] eq "") { - $status = "Empty Username"; + $status = "fail"; + &main::print_log("json_server.pl: ERROR, authorize attempt with no username"); } elsif ($args{password} && $args{password}[0] eq "") { - $status = "Empty Password"; + $status = "fail"; + &main::print_log("json_server.pl: ERROR, authorize attempt with no password"); + } else { my $password = &Groups('getpw','',$args{user}[0]); my $time_seed = &main::time_date_stamp('18',$Time); - my $time_seedY = &main::time_date_stamp('18',$Time - 86400); - my $time_seedT = &main::time_date_stamp('18',$Time + 86400); - + #to account for clock drift, check today and tomorrow values around midnight #if time is between 11:55 and midnight then also check tomorrow #if time is between midnight and 00:05 then also check yesterday - print "PW=$password, time_seed=$time_seed, $time_seedY, $time_seedT\n"; + if (time_greater_than("11:55 PM")) { + my $time_seedT = &main::time_date_stamp('18',$Time + 86400); + my $pwdcheck1 = md5_hex($password . $time_seedT); + $status = "success" if (lc $args{password}[0] eq lc $pwdcheck1); + } + if (time_less_than("00:05 AM")) { + my $time_seedY = &main::time_date_stamp('18',$Time - 86400); + my $pwdcheck2 = md5_hex($password . $time_seedY); + $status = "success" if (lc $args{password}[0] eq lc $pwdcheck2); + } + #print "PW=$password, time_seed=$time_seed"; my $pwdcheck = md5_hex($password . $time_seed); - print "PWC=$pwdcheck\n"; + #print "PWC=$pwdcheck\n"; - if (lc $args{password}[0] eq lc $pwdcheck) { + if ($status eq "" and (lc $args{password}[0] eq lc $pwdcheck)) { $status = "success"; + &main::print_log("json_server.pl: INFO, user $args{user}[0] successfully authenticated"); + } else { $status = "fail"; + &main::print_log("json_server.pl: WARNING, user $args{user}[0] authentication attempt failed"); + } } $json_data{security}->{authorize} = $status; } else { + #check if $Authorized my $ref; my $users; my $found = 0; @@ -1508,7 +1526,7 @@ sub json_object_detail { my %json_complete_object; my @f = qw( category filename measurement rf_id set_by members state states state_log type label sort_order groups hidden parents schedule logger_status - idle_time text html seconds_remaining fp_location fp_icons fp_icon_set img link level); + idle_time text html seconds_remaining fp_location fp_icons fp_icon_set img link level rgb); # Build list of fields based on those requested. foreach my $f ( sort @f ) { @@ -1551,6 +1569,14 @@ sub json_object_detail { $value = $a if ( defined $a and $a ne "" ); #don't return a null value } + elsif ( $f eq 'rgb' ) { + my ($a,$b,$c) = $object->$method; + + $value = "$a,$b,$c" if (( defined $a and $a ne "" ) #don't return a null value + and ( defined $b and $b ne "" ) + and ( defined $c and $c ne "" )); + } + #if ( $f eq 'hidden' ) { # my $a = $object->$method; # if ($a == 1 or $a eq "1") { diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 719fc8088..9ab5c1211 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.690"; +var ia7_ver = "v2.0.750"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -1058,7 +1058,7 @@ var loadList = function() { var button_text = ''; var button_html = ''; var entity_arr = []; - URLHash.fields = "category,label,sort_order,members,state,states,state_log,hidden,type,text,schedule,logger_status,link"; + URLHash.fields = "category,label,sort_order,members,state,states,state_log,hidden,type,text,schedule,logger_status,link,rgb"; $.ajax({ type: "GET", url: "/json/"+HashtoJSONArgs(URLHash), @@ -1171,6 +1171,13 @@ var loadList = function() { if (json_store.ia7_config.prefs.always_double_buttons == "yes") { if (name.length < 30) dbl_btn = "
"; } + var btn_rgb = ""; + if (json_store.objects[entity].rgb !== undefined) { + //TODO fix + btn_rgb = ''; + btn_rgb += ''; + + } // direct control item, differentiate the button var btn_direct = ""; if (json_store.ia7_config.objects !== undefined && json_store.ia7_config.objects[entity] !== undefined) { @@ -1180,7 +1187,7 @@ var loadList = function() { } button_html = "
"; + button_html += name+btn_rgb+dbl_btn+""+json_store.objects[entity].state+"
"; entity_arr.push(button_html); } }//entity each loop @@ -1418,7 +1425,7 @@ var sortArrayByArray = function (listArray, sortArray){ //Used to dynamically update the state of objects var updateList = function(path) { var URLHash = URLToHash(); - URLHash.fields = "state,state_log,schedule,logger_status,type"; + URLHash.fields = "state,state_log,schedule,logger_status,type,rgb"; URLHash.long_poll = 'true'; URLHash.time = json_store.meta.time; if (updateSocket !== undefined && updateSocket.readyState != 4){ @@ -1447,8 +1454,12 @@ var updateList = function(path) { } else { color = getButtonColor(json.data[entity].state); } - $('button[entity="'+entity+'"]').find('.pull-right').text( - json.data[entity].state); + var btn_rgb = ""; + if (json.data[entity].rgb !== undefined) { + $('button[entity="'+entity+'"]').find('.object-color').css("color",'rgb('+json.data[entity].rgb+')'); + console.log("changing color to "+json.data[entity].rgb); + } + $('button[entity="'+entity+'"]').find('.object-state').text(json.data[entity].state); $('button[entity="'+entity+'"]').removeClass("btn-default"); $('button[entity="'+entity+'"]').removeClass("btn-success"); $('button[entity="'+entity+'"]').removeClass("btn-warning"); @@ -1489,7 +1500,7 @@ var updateItem = function(item,link,time) { time = ""; } var path_str = "/objects" // override, for now, would be good to add voice_cmds - var arg_str = "fields=state,states,label,state_log,schedule,logger_status&long_poll=true&items="+item+"&time="+time; + var arg_str = "fields=state,states,label,state_log,schedule,logger_status,rgb&long_poll=true&items="+item+"&time="+time; $.ajax({ type: "GET", url: "/LONG_POLL?json('GET','"+path_str+"','"+arg_str+"')", @@ -1501,7 +1512,7 @@ var updateItem = function(item,link,time) { JSONStore(json); requestTime = json_store.meta.time; var color = getButtonColor(json.data[item].state); - $('button[entity="'+item+'"]').find('.pull-right').text( + $('button[entity="'+item+'"]').find('.object-state').text( json.data[item].state); $('button[entity="'+item+'"]').removeClass("btn-default"); $('button[entity="'+item+'"]').removeClass("btn-success"); @@ -1567,7 +1578,7 @@ var updateStaticPage = function(link,time) { if ($(this).attr('entity') != '' && json.data[$(this).attr('entity')] != undefined ) { //need an entity item for this to work. entity = $(this).attr('entity'); var color = getButtonColor(json.data[entity].state); - $('button[entity="'+entity+'"]').find('.pull-right').text(json.data[entity].state); + $('button[entity="'+entity+'"]').find('.object-state').text(json.data[entity].state); $('button[entity="'+entity+'"]').removeClass("btn-default"); $('button[entity="'+entity+'"]').removeClass("btn-success"); $('button[entity="'+entity+'"]').removeClass("btn-warning"); @@ -1744,7 +1755,7 @@ var loadCollection = function(collection_keys) { if (name.length < 30) dbl_btn = "
"; var button_html = "
"; + button_html += name+dbl_btn+""+json_store.objects[item].state+"
"; button_html = "
" + button_html + "
"; entity_arr.push(button_html); items += item+","; @@ -2395,8 +2406,9 @@ var graph_rrd = function(start,group,time) { var data_timeout = 0; var refresh = 60; //refresh data every 60 seconds by default - if (!$('#rrd-graph').is(':visible')) { + if (!$('#rrd-graph').is(':visible')) { //if (URLHash.path == path){ $('#loader').show(); + console.log("showing loader "+URLHash.path+" : "+path+" : "+$('#top-graph').length); } if (json_store.ia7_config.prefs.rrd_refresh !== undefined) refresh = json_store.ia7_config.prefs.rrd_refresh; @@ -3569,7 +3581,11 @@ var create_state_modal = function(entity) { var modal_state = json_store.objects[entity].state; - $('#control').find('.object-title').html(name + " - " + json_store.objects[entity].state + ""); + var title = name + " - " + json_store.objects[entity].state + ""; + if (json_store.objects[entity].rgb !== undefined) { + title += ' '; + } + $('#control').find('.object-title').html(title); $('#control').find('.control-dialog').attr("entity", entity); var modal_states = json_store.objects[entity].states; // HP need to have at least 2 states to be a controllable object... @@ -3639,8 +3655,7 @@ var create_state_modal = function(entity) { } var slider_data = sliderDetails(modal_states); $('#control').find('.states').append("
"); - var val = $(".object-state").text().replace(/\%/,''); - + var val = $(".modal-object-state").text().replace(/\%/,''); var position = slider_data.values.indexOf(val); if (val == "on") position = slider_data.max; if (val == "off") position = slider_data.min; @@ -3659,7 +3674,7 @@ var create_state_modal = function(entity) { } else { if (slider_data.pct) sliderstate += "%"; } - $('#control').find('.object-state').text(sliderstate); + $('#control').find('.modal-object-state').text(sliderstate); }); $( "#slider" ).on( "slidechange", function(event, ui) { @@ -3678,7 +3693,49 @@ var create_state_modal = function(entity) { $(".get-status").delay(4000).fadeOut("slow", function () { $(this).remove(); }); }); }); - + if (json_store.objects[entity].rgb !== undefined) { + console.log("Insert RGB Slider Here"); + $('#control').find('.states').append("
"); + $('#control').find('.states').append("
"); + $('#control').find('.states').append("
"); + + $('#sliderR' ).slider({ + min: 0, + max: 255, + value: json_store.objects[entity].rgb.split(',')[0] + }); + $('#sliderG' ).slider({ + min: 0, + max: 255, + value: json_store.objects[entity].rgb.split(',')[1] + }); + $('#sliderB' ).slider({ + min: 0, + max: 255, + value: json_store.objects[entity].rgb.split(',')[2] + }); + $( ".rgb-slider" ).on( "slide", function(event, ui) { + var sliderstate; + if ($(this).hasClass("red-handle")) { + sliderstate = ui.value+","+$('#sliderG').slider("value")+","+$('#sliderB').slider("value"); + } else if ($(this).hasClass("green-handle")) { + sliderstate = $('#sliderR').slider("value")+","+ui.value+","+$('#sliderB').slider("value"); + } else if ($(this).hasClass("blue-handle")) { + sliderstate = $('#sliderR').slider("value")+","+$('#sliderG').slider("value")+","+ui.value; + } + $('.object-color').css("color","rgb("+sliderstate+")"); + }); + $( ".rgb-slider" ).on( "slidechange", function(event, ui) { + var sliderstate = $('#sliderR').slider("value")+","+$('#sliderG').slider("value")+","+$('#sliderB').slider("value"); + var rgb_url= '/SET;none?select_item='+$(this).parents('.control-dialog').attr("entity")+'&select_state='+sliderstate+'&select_setby=rgb'; + console.log("rgb_url="+rgb_url); + $.get(rgb_url).fail(function() { + $(".modal-header").append($("

 Failure: Could not send command to Misterhouse

")); + $(".get-status").delay(4000).fadeOut("slow", function () { $(this).remove(); }); + }); + }); + + } } if (slider_active) { advanced_html = "
"+advanced_html; //this is clunky but showing advanced states is kinda ugly anyways diff --git a/web/ia7/include/jquery.longclick-1.0.min.js b/web/ia7/include/jquery.longclick-1.0.min.js index d12dd595c..c8f53b376 100644 --- a/web/ia7/include/jquery.longclick-1.0.min.js +++ b/web/ia7/include/jquery.longclick-1.0.min.js @@ -1 +1,26 @@ -(function(a){var b={NS:"jquery.longclick-",delay:700};a.fn.mayTriggerLongClicks=function(c){var d=a.extend(b,c);var f;var e;return a(this).on("mousedown touchstart",function(){e=false;f=setTimeout(function(g){e=true;a(g).trigger("longClick")},d.delay,this)}).on("mouseup touchend",function(){clearTimeout(f)}).on("click",function(g){if(e){g.stopImmediatePropagation()}})}})(jQuery); \ No newline at end of file +(function(a) { + var b = { + NS: "jquery.longclick-", + delay: 700 + }; + a.fn.mayTriggerLongClicks = function(c) { + var d = a.extend(b, c); + var f; + var e; + return a(this).on("mousedown touchstart", function() { + e = false; + f = setTimeout(function(g) { + e = true; + a(g).trigger("longClick") + }, d.delay, this) + }).on("mouseup touchend", function() { + clearTimeout(f) + }).on("touchmove", function() { + clearTimeout(f) + }).on("click", function(g) { + if (e) { + g.stopImmediatePropagation() + } + }) + } +})(jQuery); \ No newline at end of file diff --git a/web/ia7/include/tables.css b/web/ia7/include/tables.css index 8b28a5add..eb0a96873 100644 --- a/web/ia7/include/tables.css +++ b/web/ia7/include/tables.css @@ -1,7 +1,5 @@ .table-curved { border-collapse: separate; -} -.table-curved { border: solid #ccc 1px; border-radius: 6px; border-left:0px; @@ -149,4 +147,4 @@ width: 320px; } } - } \ No newline at end of file + } diff --git a/web/ia7/index.shtml b/web/ia7/index.shtml index d06a2ae6f..688392f80 100644 --- a/web/ia7/index.shtml +++ b/web/ia7/index.shtml @@ -233,7 +233,6 @@ .sched-dropdown-menu { margin-left: 14px !important; } - .brightness-slider { margin-left: 16px; margin-right: 16px; @@ -249,30 +248,37 @@ width: 32px; margin-left: -16px } + .red-handle .ui-slider-handle { + background: red; + } + .green-handle .ui-slider-handle { + background: green; + } + .blue-handle .ui-slider-handle { + background: blue; + } .popover { min-width: 200px; - } - + } .mh-page-link { padding-left: 30px; } - .mh-wi-text { font-size: 15px; margin-top: 13px; } - .mh-wi-icon { margin-top: 2px; } - #option_collection { margin-top: 5px; } - .whatsnew-panel { margin-right: 15px; } + .fa-rgb-border { + text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000; + } #loader { position: absolute; From 5df404ddd2484dc9e52f6019acc3fe87005216db Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 23 Aug 2018 18:15:51 -0600 Subject: [PATCH 39/78] fixed class definitions --- lib/raZberry.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index e424c8a38..66218db44 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -203,7 +203,7 @@ sub new { $self->{host} = $host; $self->{port} = 8083; $self->{port} = $port if ($port); - $self->{debug} = 5; + $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug=(\d+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); $self->{debug} = $main::Debug{razberry} if ( defined $main::Debug{razberry} ); $self->{lastupdate} = undef; @@ -2007,7 +2007,7 @@ sub update_data { } package raZberry_motion; -@raZberry_openclose::ISA = ('raZberry_binary_sensor'); +@raZberry_motion::ISA = ('raZberry_binary_sensor'); sub new { my ( $class, $object, $devid, $options ) = @_; @@ -2040,7 +2040,7 @@ sub set { } package raZberry_brightness; -@raZberry_generic::ISA = ('Generic_Item'); +@raZberry_brightness::ISA = ('Generic_Item'); sub new { my ( $class, $object, $devid, $options ) = @_; From bc45b3f58c448c3b570a1ade43569d2c3240532f Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 23 Aug 2018 20:57:03 -0600 Subject: [PATCH 40/78] v3.0.1 - fixed push timers --- lib/raZberry.pm | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 66218db44..10e7cca52 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,4 +1,4 @@ -=head1 B v3.0 +=head1 B v3.0.1 #test command setup #command queue @@ -182,7 +182,7 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v3.0.0 Controller Initializing..."); + &main::print_log("[raZberry]: v3.0.1 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; $self->{config}->{poll_seconds} = 5; @@ -394,7 +394,8 @@ sub controller_failback { sub process_check { my ($self) = @_; my @process_data = (); - my $com_status; + my $com_status = $self->{status}; + my $processed_data = 0; #In order to process multiple queues (one for poll, one for command), push the returned text into an array and then process the array #The Command queue might have waiting commands so check the queue and pop one off @@ -406,8 +407,6 @@ sub process_check { #check if data comes back unauthenticated if ( $self->{poll_process}->done_now() ) { - - $self->start_timer; #data has come in, so start the timer. $com_status = "online"; main::print_log( "[raZerry:" . $self->{host} . "] Background poll " . $self->{poll_process_mode} . " process completed" ) if ( $self->{debug} ); @@ -428,7 +427,7 @@ sub process_check { } } if ( $self->{cmd_process}->done_now() ) { - + print "**** in process done_now\n"; $com_status = "online"; main::print_log( "[raZerry:" . $self->{host} . "] Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); @@ -475,6 +474,7 @@ sub process_check { $com_status = "offline"; } else { + $processed_data = 1; if ((defined $data->{controller}->{data}) and (!defined $self->{controller_data})) { $self->{controller_data} = $data->{controller}->{data}; &main::print_log("[raZberry:" . $self->{host} . "]: Controller found"); @@ -483,6 +483,8 @@ sub process_check { &main::print_log("[raZberry:" . $self->{host} . "]: API version:\t\t" . $self->{controller_data}->{APIVersion}->{value} ); &main::print_log("[raZberry:" . $self->{host} . "]: SDK version:\t\t" . $self->{controller_data}->{SDK}->{value} ); &main::print_log("[raZberry:" . $self->{host} . "]: Controller Initialization Complete"); + $self->poll(); #get the first set of data + $self->start_timer; #data has come in, so start the timer. } $self->{lastupdate} = $data->{data}->{updateTime}; @@ -520,7 +522,7 @@ sub process_check { $self->{data}->{devices}->{$id}->{units} = $item->{metrics}->{scaleTitle} if ( defined $item->{metrics}->{scaleTitle} ); $self->{data}->{devices}->{$id}->{temp_min} = $item->{metrics}->{min} if ( defined $item->{metrics}->{min} ); $self->{data}->{devices}->{$id}->{temp_max} = $item->{metrics}->{max} if ( defined $item->{metrics}->{max} ); - + $com_status = "online"; $self->{status} = "online"; if ( defined $self->{child_object}->{$id} ) { @@ -551,21 +553,22 @@ sub process_check { } } } - if ( defined $self->{child_object}->{comm} ) { + if (( defined $self->{child_object}->{comm} ) and ($processed_data)) { #if an offline status is received, do a few more polls. for push, the raZberry is polled every 10 minutes, #so sometimes a false positive can be created if that moment throws an error 500 if ($com_status eq "online") { $self->{com_warning} = 0; - $self->{config}->{poll_seconds} = $self->{com_poll_interval} if (defined $self->{com_poll_interval}); - $self->{com_poll_interval} = undef; - $self->stop_timer; - $self->start_timer; - } else { + if (defined $self->{com_poll_interval}) { + $self->{config}->{poll_seconds} = $self->{com_poll_interval}; + $self->{com_poll_interval} = undef; + $self->start_timer; + } + } elsif ($com_status eq "offline") { main::print_log("[RaZberry:" . $self->{host} . "] WARNING. Recevied bad data from raZberry. Temporarily Increasing poll rate to confirm if device is offline.") if ($self->{com_warning} == 0); $self->{com_warning}++; $self->{com_poll_interval} = $self->{config}->{poll_seconds}; $self->{config}->{poll_seconds} = 10 unless ($self->{config}->{poll_seconds} <= 10); - $self->stop_timer; + #$self->stop_timer; $self->start_timer; } if ( $self->{status} ne $com_status ) { @@ -732,13 +735,13 @@ sub _get_JSON_data { sub stop_timer { my ($self) = @_; - + print "**** in stop timer\n\n"; $self->{timer}->stop; } sub start_timer { my ($self) = @_; - + print "**** in start timer\n\n"; $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &raZberry::poll($self) }, -1 ); } @@ -1797,8 +1800,7 @@ sub set { else { $n_state = "closed"; } - main::print_log( "[raZberry]: Setting openclose value to $n_state. Level is " . $self->{level} ) - if ( $self->{debug} ); + main::print_log( "[raZberry]: Setting openclose value to $n_state. Level is " . $self->{level} ) if ( $self->{debug} ); $self->SUPER::set($n_state); } else { @@ -2014,7 +2016,7 @@ sub new { my $self = $class->SUPER::new( $object, $devid, $options ); - @{$$self{states}} = ('motion','still'); + #@{$$self{states}} = ('motion','still'); return $self; } @@ -2030,8 +2032,7 @@ sub set { else { $n_state = "still"; } - main::print_log( "[raZberry]: Setting motion value to $n_state. Level is " . $self->{level} ) - if ( $self->{debug} ); + main::print_log( "[raZberry]: Setting motion value to $n_state. Level is " . $self->{level} ) if ( $self->{debug} ); $self->SUPER::set($n_state); } else { @@ -2050,7 +2051,7 @@ sub new { $$self{master_object} = $object; $$self{type} = "Brightness"; - $devid = $devid . "-49-3" if ( $devid =~ m/^\d+$/ ); + $devid = $devid . "-0-49-3" if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; $object->register( $self, $devid, $options ); @@ -2078,6 +2079,10 @@ sub isfailed { $$self{master_object}->isfailed_dev( $$self{devid} ); } +sub update_data { + my ( $self, $data ) = @_; +} + #08/19/18 03:15:35 PM [raZberry]: ERROR, child object id 18-0-48-1 not found! #08/19/18 03:16:23 PM [raZberry]: ERROR, child object id 18-0-49-3 not found! #08/19/18 03:16:23 PM [raZberry]: ERROR, child object id 18-0-37 not found! From 59f910af6eebab7a53d5c42adeff851a2912a3a4 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 24 Aug 2018 15:34:38 -0600 Subject: [PATCH 41/78] v3.0.2 - more process item testing --- lib/raZberry.pm | 162 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 108 insertions(+), 54 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 10e7cca52..d2a5fb77d 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,4 +1,4 @@ -=head1 B v3.0.1 +=head1 B v3.0.2 #test command setup #command queue @@ -182,12 +182,25 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v3.0.1 Controller Initializing..."); + &main::print_log("[raZberry]: v3.0.2 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; + + #-------- These are config_parm items $self->{config}->{poll_seconds} = 5; $self->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} if ( defined $main::config_parms{raZberry_poll_seconds} ); - $self->{push} = 0; + $self->{timeout} = 2; + $self->{timeout} = $main::config_parms{raZberry_timeout} if ( defined $main::config_parms{raZberry_timeout} ); + $self->{username} = ""; + $self->{username} = $main::config_parms{raZberry_user} if ( defined $main::config_parms{raZberry_user} ); + $self->{password} = $main::config_parms{raZberry_password} if ( defined $main::config_parms{raZberry_password} ); + $self->{max_cmd_queue} = 4; + $self->{com_threshold} = 4; + $self->{command_timeout} = 60; + $self->{command_timeout_limit} = 3; + + + $self->{push} = 0; if ( ( defined $poll ) and ( lc $poll eq 'push' ) ) { $self->{push} = 1; @@ -197,26 +210,22 @@ sub new { $self->{config}->{poll_seconds} = $poll if ( ( defined $poll ) && ($poll)); #ensure a number $self->{config}->{poll_seconds} = 1 if ( ( defined $self->{config}->{poll_seconds} ) && ( $self->{config}->{poll_seconds} < 1 )); } + $self->{updating} = 0; $self->{data}->{retry} = 0; my ( $host, $port ) = ( split /:/, $addr )[ 0, 1 ]; - $self->{host} = $host; - $self->{port} = 8083; - $self->{port} = $port if ($port); - $self->{debug} = 0; - ( $self->{debug} ) = ( $options =~ /debug=(\d+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); - $self->{debug} = $main::Debug{razberry} if ( defined $main::Debug{razberry} ); - $self->{lastupdate} = undef; - $self->{timeout} = 2; - $self->{timeout} = $main::config_parms{raZberry_timeout} if ( defined $main::config_parms{raZberry_timeout} ); - $self->{status} = ""; - $self->{controller_data} = (); + $self->{host} = $host; + $self->{port} = 8083; + $self->{port} = $port if ($port); + $self->{debug} = 0; + ( $self->{debug} ) = ( $options =~ /debug=(\d+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); + $self->{debug} = $main::Debug{razberry} if ( defined $main::Debug{razberry} ); + $self->{lastupdate} = undef; + $self->{status} = ""; + $self->{controller_data} = (); &main::print_log("[raZberry:" . $self->{host} . "]: options are $options") if ( ( $self->{debug} ) and ( defined $options ) ); - $self->{username} = ""; $options =~ s/username\=/user\=/i if ( defined $options ); - $self->{username} = $main::config_parms{raZberry_user} if ( defined $main::config_parms{raZberry_user} ); - $self->{password} = $main::config_parms{raZberry_password} if ( defined $main::config_parms{raZberry_password} ); ( $self->{username} ) = ( $options =~ /user\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/user\=/i ) ); ( $self->{password} ) = ( $options =~ /password\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/password\=/i ) ); @@ -240,13 +249,17 @@ sub new { &main::print_log("[raZberry:" . $self->{host} . "]: Poll method selected"); } } + &main::print_log("[raZberry:" . $self->{host} . "]: Instance:\t\t" . $self->{instance}); $self->{cookie_string} = ""; if ( $self->{username} ) { $self->{cookie_jar} = HTTP::Cookies->new( {} ); $self->login; + } else { + $self->{login_success} = 1; } - + $self->{login_attempt} = 0; + ${$self->{controllers}->{objects}}[0] = $self; $self->{controllers}->{backup} = 0; $self->{controllers}->{failover_time} = 0; @@ -263,15 +276,12 @@ sub new { unlink "$::config_parms{data_dir}/raZberry_cmd_" . $self->{host} . ".data"; $self->{cmd_process} = new Process_Item; $self->{cmd_process}->set_output( $self->{cmd_data_file} ); - $self->{max_cmd_queue} = 4; - + $self->{com_warning} = 0; - $self->{com_threshold} = 4; $self->{com_poll_interval} = undef; &::MainLoop_post_add_hook( \&raZberry::process_check, 0, $self ); - &main::print_log("[raZberry:" . $self->{host} . "]: Instance:\t\t" . $self->{instance}); $self->{generate_voice_cmds} = 0; &::Reload_post_add_hook( \&raZberry::generate_voice_commands, 0, $self ); @@ -306,6 +316,8 @@ sub login { $self->{login_success} = 0; &main::print_log("[raZberry:" . $self->{host} . "]: Error attempting to authenticate to $host"); &main::print_log("[raZberry:" . $self->{host} . "]: Code is " . $responseObj->code . " and content is " . $responseObj->content ); + $self->{login_success} = 0; + $self->{login_attempt} = $main::Time; } else { &main::print_log("[raZberry:" . $self->{host} . "]: Successful authentication."); @@ -316,6 +328,7 @@ sub login { $self->{cookie_string} =~ s/^Set-Cookie3: //; #strip out the cookie header that http::cookies returns $self->{cookie_string} =~ s/\n//; #strip out the \n that http::cookies returns #print "***** [$self->{cookie_string}]\n"; + $self->{login_attempt} = 0; } } @@ -405,20 +418,34 @@ sub process_check { return unless ( ( defined $self->{poll_process} ) and ( defined $self->{cmd_process} ) ); #check if data comes back unauthenticated + if (($self->{login_success} == 0) and ($self->{login_attempt})) { + if ($main::Time > ($self->{login_attempt} + 30)) { #retry log in every 30 seconds + main::print_log( "[raZerry:" . $self->{host} . "] Attempting to re-authenticate" ); + $self->login; + } + } if ( $self->{poll_process}->done_now() ) { $com_status = "online"; + $processed_data = 1; main::print_log( "[raZerry:" . $self->{host} . "] Background poll " . $self->{poll_process_mode} . " process completed" ) if ( $self->{debug} ); my $file_data = &main::file_read( $self->{poll_data_file} ); exit unless ($file_data); #if there is no data, then don't process + if ($file_data =~m/\"401 Unauthorized\",\"error\"\:\"Not logged in\"/) { + $self->{login_success} = 0; + $self->{login_attempt} = $main::Time - 30; + return + } + # print "debug: file_data=$file_data\n" if ( $self->{debug} > 2); my ($json_data) = $file_data =~ /(\{.*\})/s; # print "debug: json_data=$json_data\n" if ( $self->{debug} > 2); unless ( ($file_data) and ($json_data) ) { + $json_data = "" unless ($json_data); main::print_log( "[raZberry:" . $self->{host} . "] ERROR! bad data returned by poll" ); main::print_log( "[raZberry:" . $self->{host} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); $com_status = "offline"; @@ -427,13 +454,20 @@ sub process_check { } } if ( $self->{cmd_process}->done_now() ) { - print "**** in process done_now\n"; $com_status = "online"; + $processed_data = 2; + main::print_log( "[raZerry:" . $self->{host} . "] Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); my $file_data = &main::file_read( $self->{cmd_data_file} ); exit unless ($file_data); #if there is no data, then don't process + if ($file_data =~m/\"401 Unauthorized\",\"error\"\:\"Not logged in\"/) { + $self->{login_success} = 0; + $self->{login_attempt} = $main::Time - 30; + return + } + if ($self->{cmd_process_mode} eq "usercode") { #normally usercode just returns null if ($file_data ne "null") { @@ -449,24 +483,41 @@ sub process_check { main::print_log( "[raZberry:" . $self->{host} . "] ERROR! bad data returned by poll" ); main::print_log( "[raZberry:" . $self->{host} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); $com_status = "offline"; + #update the retry on the failed item. + $ {$self->{cmd_queue}}[0][3]++; } else { push @process_data, $json_data; - shift @{ $self->{cmd_queue} }; - if (scalar @{ $self->{cmd_queue} }) { - main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); - my ($mode, $get_cmd) = ${ $self->{cmd_queue} }[0]; - $self->{cmd_process}->set($get_cmd); - $self->{cmd_process}->start(); - $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; - $self->{cmd_process_mode} = $mode; - main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command" . $self->{cmd_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); - } + shift @{ $self->{cmd_queue} }; #successfully processed to remove item from the queue } } } + +#check for any queued data that needs to be processed $self->{command_timeout} + if (scalar @{ $self->{cmd_queue} }) { + my ($mode, $get_cmd, $time, $retry) = ${ $self->{cmd_queue} }[0]; + #if there is a retry, then execute at request time + (retry * 5 seconds) + #discard the command if 60 seconds after the request time + #if the item is queued then wait until at least a second after the request time + #discard the item if it's been retried $self->{command_timeout_limit} times + if ($retry > $self->{command_timeout_limit}) { + main::print_log( "[raZberry:" . $self->{host} . "] ERROR: Abandoning command $get_cmd due to $retry retry attempts" ); + shift @{ $self->{cmd_queue}}; + } elsif ($main::Time > ($time + 60)) { + main::print_log( "[raZberry:" . $self->{host} . "] ERROR: $get_cmd request older than a minute. Abandoning request" ); + shift @{ $self->{cmd_queue}}; + } elsif ($main::Time > ($time + 1 + ($retry * 5))) { + main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); + $self->{cmd_process}->set($get_cmd); + $self->{cmd_process}->start(); + $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{cmd_process_mode} = $mode; + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command (" . $self->{cmd_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); + } + } foreach my $rec_data (@process_data) { my $data; + eval { $data = JSON::XS->new->decode($rec_data); }; # catch crashes: if ($@) { @@ -474,7 +525,6 @@ sub process_check { $com_status = "offline"; } else { - $processed_data = 1; if ((defined $data->{controller}->{data}) and (!defined $self->{controller_data})) { $self->{controller_data} = $data->{controller}->{data}; &main::print_log("[raZberry:" . $self->{host} . "]: Controller found"); @@ -559,22 +609,28 @@ sub process_check { if ($com_status eq "online") { $self->{com_warning} = 0; if (defined $self->{com_poll_interval}) { + main::print_log("[RaZberry:" . $self->{host} . "] Valid Data Received. Changing poll rate to $self->{com_poll_interval}."); $self->{config}->{poll_seconds} = $self->{com_poll_interval}; $self->{com_poll_interval} = undef; + $self->stop_timer; $self->start_timer; } } elsif ($com_status eq "offline") { - main::print_log("[RaZberry:" . $self->{host} . "] WARNING. Recevied bad data from raZberry. Temporarily Increasing poll rate to confirm if device is offline.") if ($self->{com_warning} == 0); $self->{com_warning}++; - $self->{com_poll_interval} = $self->{config}->{poll_seconds}; - $self->{config}->{poll_seconds} = 10 unless ($self->{config}->{poll_seconds} <= 10); - #$self->stop_timer; - $self->start_timer; + if (!defined $self->{com_poll_interval} ) { + main::print_log("[RaZberry:" . $self->{host} . "] WARNING. Recevied bad data from raZberry. Temporarily Increasing poll rate to confirm if device is offline."); + $self->{com_poll_interval} = $self->{config}->{poll_seconds}; + $self->{config}->{poll_seconds} = 10 unless ($self->{config}->{poll_seconds} <= 10); + $self->stop_timer; + $self->start_timer; + } } if ( $self->{status} ne $com_status ) { - $self->{status} = $com_status; - if (($self->{child_object}->{comm}->state() ne $com_status) or (($self->{com_warning} > $self->{com_threshold}) and ($com_status eq "offline"))) { - main::print_log("[RaZberry:" . $self->{host} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "...") if ( $self->{loglevel} ); + if ((($self->{child_object}->{comm}->state() eq "offline") and ($com_status eq "online")) or + (($self->{child_object}->{comm}->state() eq "online") and ($self->{com_warning} > $self->{com_threshold}) and ($com_status eq "offline")) or + (($self->{child_object}->{comm}->state() eq "online") and ($com_status eq "offline") and ($processed_data ==2))) { + $self->{status} = $com_status; #when $com_status was offline, it immediately triggered. + main::print_log("[RaZberry:" . $self->{host} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..."); $self->{child_object}->{comm}->set( $com_status, 'poll' ); } } @@ -589,7 +645,7 @@ sub poll { my $cmd = ""; $cmd = "?since=" . $self->{lastupdate} if ( defined $self->{lastupdate} ); $cmd = "" if ( lc $option eq "full" ); - &main::print_log("[raZberry:" . $self->{host} . "]: cmd=$cmd") if ( $self->{debug} > 1 ); + &main::print_log("[raZberry:" . $self->{host} . "]: cmd=$cmd option=$option last_updated=$self->{lastupdate}") if ( $self->{debug} > 1 ); for my $dev ( keys %{ $self->{data}->{force_update} } ) { &main::print_log("[raZberry:" . $self->{host} . "]: Forcing update to device $dev to account for local changes") if ( $self->{debug} ); @@ -704,26 +760,26 @@ sub _get_JSON_data { my $get_cmd = "get_url $get_params " . '"http://' . "$host:$port/$method/$rest{$mode}$params" . '"'; if (( $cmd eq "") or ($cmd =~ m/^\?since=/)) { - $self->{poll_process}->stop() unless ($self->{poll_process}->done() ); $self->{poll_process}->set($get_cmd); $self->{poll_process}->start(); $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; $self->{poll_process_mode} = $mode; - main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Poll" . $self->{poll_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Poll (" . $self->{poll_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); } else { if ($self->{cmd_process}->done() ) {; $self->{cmd_process}->set($get_cmd); $self->{cmd_process}->start(); $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; $self->{cmd_process_mode} = $mode; - main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command" . $self->{poll_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command (" . $self->{poll_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); } else { - main::print_log( "[raZberry:" . $self->{host} . "] Queing Command" . $self->{poll_process}->pid() . " command $mode, $get_cmd" ) if ( $self->{debug} ); - push @{ $self->{cmd_queue} }, [$mode,$get_cmd]; + main::print_log( "[raZberry:" . $self->{host} . "] Queing Command (" . $self->{poll_process}->pid() . ") command $mode, $get_cmd, time " . $main::Time ) if ( $self->{debug} ); if (scalar @{ $self->{cmd_queue} } < $self->{max_poll_queue} ) { - main::print_log( "[raZberry:" . $self->{host} . "] Max Queue Length ($self->{max_poll_queue}) reached! Discarding all queued commands!" ); - @{ $self->{cmd_queue} } = (); + push @{ $self->{cmd_queue} }, [$mode,$get_cmd,$main::Time,0]; + } else { + main::print_log( "[raZberry:" . $self->{host} . "] Max Queue Length ($self->{max_poll_queue}) reached! Discarding queued command" ); + #@{ $self->{cmd_queue} } = (); } } } @@ -735,13 +791,11 @@ sub _get_JSON_data { sub stop_timer { my ($self) = @_; - print "**** in stop timer\n\n"; $self->{timer}->stop; } sub start_timer { my ($self) = @_; - print "**** in start timer\n\n"; $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &raZberry::poll($self) }, -1 ); } @@ -785,7 +839,7 @@ sub register { my $type = $object->{type}; $type = "Digital " . $type if ( ( defined $options ) and ( $options =~ m/digital/ ) ); - &main::print_log("[raZberry:" . $self->{host} . "]: Registering " . $type . " Device ID $dev to controller " ); + &main::print_log("[raZberry:" . $self->{host} . "]: Registering " . $type . " Device ID $dev" ); $self->{child_object}->{$dev} = $object; $self->{lastupdate} = 0; if ( defined $options ) { @@ -806,7 +860,7 @@ sub deregister { return unless (defined $self->{child_object}->{$dev}); my $type = $self->{child_object}->{$dev}->{type}; - &main::print_log("[raZberry:" . $self->{host} . "]: Deregistering " . $type . " Device ID $dev on controller" ); + &main::print_log("[raZberry:" . $self->{host} . "]: Deregistering " . $type . " Device ID $dev" ); delete $self->{child_object}->{$dev}; delete $self->{data}->{force_update}->{$dev} if (defined $self->{data}->{force_update}->{$dev}); delete $self->{data}->{ping}->{$dev} if (defined $self->{data}->{ping}->{$dev}); From 79a797ab370605c8608ba470072d217834e26309 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 24 Aug 2018 16:30:34 -0600 Subject: [PATCH 42/78] v3.0.3 - command queue works --- lib/raZberry.pm | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index d2a5fb77d..17260a53a 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,4 +1,4 @@ -=head1 B v3.0.2 +=head1 B v3.0.3 #test command setup #command queue @@ -182,7 +182,7 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v3.0.2 Controller Initializing..."); + &main::print_log("[raZberry]: v3.0.3 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; @@ -217,7 +217,7 @@ sub new { $self->{host} = $host; $self->{port} = 8083; $self->{port} = $port if ($port); - $self->{debug} = 0; + $self->{debug} = 5; ( $self->{debug} ) = ( $options =~ /debug=(\d+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); $self->{debug} = $main::Debug{razberry} if ( defined $main::Debug{razberry} ); $self->{lastupdate} = undef; @@ -472,7 +472,12 @@ sub process_check { #normally usercode just returns null if ($file_data ne "null") { main::print_log( "[raZberry:" . $self->{host} . "] WARNING, unexpected return data from usercode: ($file_data)" ); + $ {$self->{cmd_queue}}[0][3]++; + + } else { + shift @{ $self->{cmd_queue} }; #successfully processed to remove item from the queue } + } else { # print "debug: file_data=$file_data\n" if ( $self->{debug} > 2); @@ -488,24 +493,28 @@ sub process_check { } else { push @process_data, $json_data; shift @{ $self->{cmd_queue} }; #successfully processed to remove item from the queue +print "*** 2 Array length is " . scalar @{ $self->{cmd_queue} } . "\n"; + } } } - + #check for any queued data that needs to be processed $self->{command_timeout} - if (scalar @{ $self->{cmd_queue} }) { - my ($mode, $get_cmd, $time, $retry) = ${ $self->{cmd_queue} }[0]; + if ((scalar @{ $self->{cmd_queue} }) and ($self->{cmd_process}->done() )) { + my ($mode, $get_cmd, $time, $retry) = @ { ${ $self->{cmd_queue} }[0] }; #if there is a retry, then execute at request time + (retry * 5 seconds) #discard the command if 60 seconds after the request time #if the item is queued then wait until at least a second after the request time #discard the item if it's been retried $self->{command_timeout_limit} times +print "*** mode=$mode, get_cmd=$get_cmd, time=$time, retry=$retry\n"; +print "*** Array length is " . scalar @{ $self->{cmd_queue} } . ": status=" . $self->{cmd_process}->done() . "\n"; if ($retry > $self->{command_timeout_limit}) { main::print_log( "[raZberry:" . $self->{host} . "] ERROR: Abandoning command $get_cmd due to $retry retry attempts" ); shift @{ $self->{cmd_queue}}; } elsif ($main::Time > ($time + 60)) { main::print_log( "[raZberry:" . $self->{host} . "] ERROR: $get_cmd request older than a minute. Abandoning request" ); shift @{ $self->{cmd_queue}}; - } elsif ($main::Time > ($time + 1 + ($retry * 5))) { + } elsif (($main::Time > ($time + 1 + ($retry * 5)) and ($self->{cmd_process}->done() ) )) { main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); $self->{cmd_process}->set($get_cmd); $self->{cmd_process}->start(); @@ -767,18 +776,18 @@ sub _get_JSON_data { $self->{poll_process_mode} = $mode; main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Poll (" . $self->{poll_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); } else { - if ($self->{cmd_process}->done() ) {; + if (($self->{cmd_process}->done() ) and (scalar @{ $self->{cmd_queue} } == 0)) {; $self->{cmd_process}->set($get_cmd); $self->{cmd_process}->start(); $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; $self->{cmd_process_mode} = $mode; - main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command (" . $self->{poll_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); + main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command (" . $self->{cmd_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); } else { - main::print_log( "[raZberry:" . $self->{host} . "] Queing Command (" . $self->{poll_process}->pid() . ") command $mode, $get_cmd, time " . $main::Time ) if ( $self->{debug} ); - if (scalar @{ $self->{cmd_queue} } < $self->{max_poll_queue} ) { + main::print_log( "[raZberry:" . $self->{host} . "] Queing Command command $mode, $get_cmd, time " . $main::Time ) if ( $self->{debug} ); + if (scalar @{ $self->{cmd_queue} } <= $self->{max_cmd_queue} ) { push @{ $self->{cmd_queue} }, [$mode,$get_cmd,$main::Time,0]; } else { - main::print_log( "[raZberry:" . $self->{host} . "] Max Queue Length ($self->{max_poll_queue}) reached! Discarding queued command" ); + main::print_log( "[raZberry:" . $self->{host} . "] Max Queue Length ($self->{max_cmd_queue}) reached! Discarding queued command" ); #@{ $self->{cmd_queue} } = (); } } @@ -1337,7 +1346,7 @@ sub battery_check { my $cmd; my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; - $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5D128%5B.Get()"; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5B128%5D.Get()"; &main::print_log("[raZberry]: Getting Battery Details") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); @@ -1485,7 +1494,7 @@ sub battery_check { my $cmd; my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; - $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5D128%5B.Get()"; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5B128%5D.Get()"; &main::print_log("[raZberry]: Getting Battery Details") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); @@ -1933,7 +1942,7 @@ sub battery_check { my $cmd; my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; - $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5D128%5B.Get()"; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5B128%5D.Get()"; &main::print_log("[raZberry]: Getting Battery Details") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); From b05fb9f38098564827ce03c94273b5f6ec8e4e15 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 25 Aug 2018 12:03:03 -0600 Subject: [PATCH 43/78] v3.0.4 - fixed command queing --- bin/get_url | 7 ++++-- lib/raZberry.pm | 57 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/bin/get_url b/bin/get_url index 3a62ca279..93643f7c7 100755 --- a/bin/get_url +++ b/bin/get_url @@ -19,7 +19,7 @@ use Getopt::Long; #print "get_url: @ARGV\n"; if ( !&GetOptions( \%parms, 'h', 'help', 'quiet', 'cookies=s', 'cookie_file_in=s', 'cookie_file_out=s', 'post=s', 'header=s', 'userid=s', 'password=s', 'ua', - 'put=s', 'json', 'response_code' ) + 'put=s', 'timeout=s', 'json', 'response_code' ) or !@ARGV or $parms{h} or $parms{help} @@ -32,7 +32,7 @@ if ( Usage: - $Pgm_Name [-quiet] [-cookies 'cookiestr'] [-post 'poststr'] [-header header_file] url [local_file] + $Pgm_Name [-quiet] [-cookies 'cookiestr'] [-post 'poststr'] [-header header_file] [-timeout X] url [local_file] -quiet: no output on stdout @@ -59,6 +59,7 @@ Usage: -response_code: STDOUT only: Prepend output with RESPONSECODE: \n + -timeout: XX : number of seconds to wait for command to complete If local_file is specified, data is stored there. If local_file = /dev/null, data is not returned. @@ -113,6 +114,8 @@ sub use_ua { if $config_parms{proxy}; $ua->timeout(30); # Time out after 30 seconds + $ua->timeout($parms{timeout} ) if $parms{timeout}; + $ua->env_proxy(); $ua->agent( $config_parms{get_url_ua} ) if $config_parms{get_url_ua}; diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 17260a53a..8209c97e8 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,4 +1,4 @@ -=head1 B v3.0.3 +=head1 B v3.0.4 #test command setup #command queue @@ -114,10 +114,14 @@ Only one razberry controller can be the push source, due to only a single contro =head2 MH.INI CONFIG PARAMS -raZberry_timeout -raZberry_poll_seconds -raZberry_user -raZberry_password +raZberry_timeout HTTP request timeout (default 5) +raZberry_poll_seconds Number of seconds to poll the raZberry +raZberry_user Authentication username +raZberry_password Authentication password +raZberry_max_cmd_queue Maximum number of commands to queue up (default 6) +raZberry_com_threshold Number of failed polls before controller marked offline (default 4) +raZberry_command_timeout Number of seconds after a command is issued before it is abandoned (default 60) +raZberry_command_timeout_limit Maximum number of retries for a command before abandoned =head2 BUGS @@ -189,15 +193,22 @@ sub new { #-------- These are config_parm items $self->{config}->{poll_seconds} = 5; $self->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} if ( defined $main::config_parms{raZberry_poll_seconds} ); - $self->{timeout} = 2; + $self->{timeout} = 5; $self->{timeout} = $main::config_parms{raZberry_timeout} if ( defined $main::config_parms{raZberry_timeout} ); $self->{username} = ""; $self->{username} = $main::config_parms{raZberry_user} if ( defined $main::config_parms{raZberry_user} ); $self->{password} = $main::config_parms{raZberry_password} if ( defined $main::config_parms{raZberry_password} ); - $self->{max_cmd_queue} = 4; + $self->{max_cmd_queue} = 6; + $self->{max_cmd_queue} = $main::config_parms{raZberry_max_cmd_queue} if ( defined $main::config_parms{raZberry_max_cmd_queue} );; + $self->{com_threshold} = 4; + $self->{com_threshold} = $main::config_parms{raZberry_com_threshold} if ( defined $main::config_parms{raZberry_com_threshold} );; + $self->{command_timeout} = 60; + $self->{command_timeout} = $main::config_parms{raZberry_command_timeout} if ( defined $main::config_parms{raZberry_command_timeout} );; + $self->{command_timeout_limit} = 3; + $self->{command_timeout_limit} = $main::config_parms{raZberry_command_timeout_limit} if ( defined $main::config_parms{raZberry_command_timeout_limit} );; $self->{push} = 0; @@ -217,7 +228,7 @@ sub new { $self->{host} = $host; $self->{port} = 8083; $self->{port} = $port if ($port); - $self->{debug} = 5; + $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug=(\d+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); $self->{debug} = $main::Debug{razberry} if ( defined $main::Debug{razberry} ); $self->{lastupdate} = undef; @@ -493,7 +504,6 @@ sub process_check { } else { push @process_data, $json_data; shift @{ $self->{cmd_queue} }; #successfully processed to remove item from the queue -print "*** 2 Array length is " . scalar @{ $self->{cmd_queue} } . "\n"; } } @@ -502,20 +512,24 @@ print "*** 2 Array length is " . scalar @{ $self->{cmd_queue} } . "\n"; #check for any queued data that needs to be processed $self->{command_timeout} if ((scalar @{ $self->{cmd_queue} }) and ($self->{cmd_process}->done() )) { my ($mode, $get_cmd, $time, $retry) = @ { ${ $self->{cmd_queue} }[0] }; + #print "**** mode=$mode, get_cmd=$get_cmd\n"; + #print "*** time=$time, time_diff=" . ($main::Time - $time) ." timeout=" .$self->{command_timeout} . " retry=$retry\n"; #if there is a retry, then execute at request time + (retry * 5 seconds) #discard the command if 60 seconds after the request time #if the item is queued then wait until at least a second after the request time #discard the item if it's been retried $self->{command_timeout_limit} times -print "*** mode=$mode, get_cmd=$get_cmd, time=$time, retry=$retry\n"; -print "*** Array length is " . scalar @{ $self->{cmd_queue} } . ": status=" . $self->{cmd_process}->done() . "\n"; if ($retry > $self->{command_timeout_limit}) { main::print_log( "[raZberry:" . $self->{host} . "] ERROR: Abandoning command $get_cmd due to $retry retry attempts" ); shift @{ $self->{cmd_queue}}; - } elsif ($main::Time > ($time + 60)) { - main::print_log( "[raZberry:" . $self->{host} . "] ERROR: $get_cmd request older than a minute. Abandoning request" ); + } elsif (($main::Time - $time) > $self->{command_timeout}) { + main::print_log( "[raZberry:" . $self->{host} . "] ERROR: $get_cmd request older than " . $self->{command_timeout} . " seconds. Abandoning request" ); shift @{ $self->{cmd_queue}}; } elsif (($main::Time > ($time + 1 + ($retry * 5)) and ($self->{cmd_process}->done() ) )) { - main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); + if ($retry == 0) { + main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); + } else { + main::print_log( "[raZberry:" . $self->{host} . "] Retrying previous command" ); + } $self->{cmd_process}->set($get_cmd); $self->{cmd_process}->start(); $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; @@ -765,6 +779,7 @@ sub _get_JSON_data { $method = "ZWaveAPI" if ( $mode eq "controller" ); &main::print_log("[raZberry:" . $self->{host} . "]: contacting http://$host:$port/$method/$rest{$mode}$params") if ( $self->{debug} ); my $get_params = "-ua "; + $get_params .= "-timeout " . $self->{timeout} . " "; $get_params .= "-cookies " . "'" . $cookie . "' " if ($cookie ne ""); my $get_cmd = "get_url $get_params " . '"http://' . "$host:$port/$method/$rest{$mode}$params" . '"'; @@ -781,6 +796,7 @@ sub _get_JSON_data { $self->{cmd_process}->start(); $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; $self->{cmd_process_mode} = $mode; + push @{ $self->{cmd_queue} }, [$mode,$get_cmd,$main::Time,0]; main::print_log( "[raZberry:" . $self->{host} . "] Backgrounding Command (" . $self->{cmd_process}->pid() . ") command $mode, $get_cmd" ) if ( $self->{debug} ); } else { main::print_log( "[raZberry:" . $self->{host} . "] Queing Command command $mode, $get_cmd, time " . $main::Time ) if ( $self->{debug} ); @@ -1330,7 +1346,10 @@ sub battery_check { main::print_log("[raZberry_blind] ERROR, battery option not defined on this object"); return; } - + if (!defined $self->{battery_level}) { + &main::print_log( "[raZberry_lock] WARNING Battery level undefined. Try again later" ); + return undef; + } $report = 0 unless (defined $report); if ($report) { &main::print_log( "[raZberry_blind] INFO Battery currently at " . $self->{battery_level} . "%" ); @@ -1480,6 +1499,10 @@ sub battery_check { my ($self,$report) = @_; #issue the get command, and then check the result about 10 seconds later $report = 0 unless (defined $report); + if (!defined $self->{battery_level}) { + &main::print_log( "[raZberry_lock] WARNING Battery level undefined. Try again later" ); + return undef; + } if ($report) { &main::print_log( "[raZberry_lock] INFO Battery currently at " . $self->{battery_level} . "%" ); if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { @@ -1928,6 +1951,10 @@ sub update_data { sub battery_check { my ($self, $report) = @_; $report = 0 unless (defined $report); + if (!defined $self->{battery_level}) { + &main::print_log( "[raZberry_lock] WARNING Battery level undefined. Try again later" ); + return undef; + } if ($report) { &main::print_log( "[raZberry_battery] INFO Battery currently at " . $self->{battery_level} . "%" ); if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { From df6a7cbbc7647ff4084220f8bf0103197821c9a9 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 25 Aug 2018 12:19:18 -0600 Subject: [PATCH 44/78] v1.2.3 - better command queue processing --- bin/get_tcp | 124 ------------------------------------------------ lib/Yeelight.pm | 119 +++++++++++++++++++++++----------------------- 2 files changed, 59 insertions(+), 184 deletions(-) delete mode 100644 bin/get_tcp diff --git a/bin/get_tcp b/bin/get_tcp deleted file mode 100644 index 22a2d074d..000000000 --- a/bin/get_tcp +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env perl -# -*- Perl -*- - -use strict; -use IO::Socket; - -# Similar to get_url, open a socket and then get the data. Useful to spawn off as a process_item to avoid pauses - -my ( $Pgm_Path, $Pgm_Name ); - -BEGIN { - ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; - ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works -} - -my ( %config_parms, %parms ); - -use Getopt::Long; - -if ( - !&GetOptions( \%parms, 'h', 'help', 'quiet', 'timeout=s', 'rn') - or !@ARGV - or $parms{h} - or $parms{help} - ) -{ - - print < $host, -PeerPort => $port, -Timeout => $timeout, -Proto => "tcp") or $response = "get_tcp_error: opening socket: $!.\n"; -#print "error $host:$port\n" if ($tcp->connected()); - -unless ($response) { - $error = 0; - print "Sending data to $location " unless $parms{quiet}; - print "into $file" unless ($parms{quiet} or !$file); - print "..." unless $parms{quiet}; - - $tcp->send($data) or $response = "get_tcp_error: Couldn't send: $!"; - - unless ($response) { - $tcp->recv($response, 1024); - print " data retrieved\n" unless $parms{quiet}; - } else { - $error = 1; - } -} - if ($file) { - # print $data; - unless ( $file eq '/dev/null' ) { - if ($response) { - open( OUT, ">$file" ) - or die "get_tcp_error: could not open file '$file' for output: $!\n"; - binmode OUT; - print OUT $response; - close OUT; - } - else { - print " empty data response\n"; - } - } - } else { - print $response; - } - - -$tcp->close() unless ($error); - - diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index a502f4772..766af91b0 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,11 +1,12 @@ package Yeelight; -# v1.2.1 +# v1.2.3 #TODO #- test queuing fast commands #- check query data - +#- test socket reconnection +#- retry time delay, should be based off process_item start not the original request time. use strict; use warnings; @@ -90,12 +91,13 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.2.0"; + $self->{module_version} = "v1.2.3"; $self->{ssdp_timeout} = 1000; $self->{socket_connected} = 0; $self->{host} = $location; $self->{port} = 55443; $self->{brightness_state_delay} = 1; + $self->{command_timeout_limit} = 3; if ($location =~ m/:/) { ($self->{host}, $self->{port}) = $location =~ /(.*):(.*)/; @@ -115,6 +117,7 @@ sub new { $self->{max_poll_queue} = 3; $self->{max_cmd_queue} = 5; $self->{cmd_process_retry_limit} = 6; + $self->{command_timeout} = 60; @{ $self->{poll_queue} } = (); $self->{poll_data_file} = "$::config_parms{data_dir}/Yeelight_poll_" . $self->{name} . ".data"; @@ -195,9 +198,11 @@ sub check_for_socket_data { if (( defined $self->{child_object}->{comm} ) and ($self->{socket_connected})) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); + if ($self->{child_object}->{comm}->state() ne $com_status) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } } } } @@ -235,9 +240,11 @@ sub get_data { if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] 0 Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); + if ($self->{child_object}->{comm}->state() ne $com_status) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } } } @@ -245,6 +252,7 @@ sub get_data { sub process_check { my ($self) = @_; + my $com_status = $self->{status}; return unless ( defined $self->{poll_process} ); @@ -252,7 +260,7 @@ sub process_check { @{ $self->{poll_queue} } = (); #clear the queue since process is done. - my $com_status = "online"; + $com_status = "online"; main::print_log( "[Yeelight:" . $self->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed" ) if ( $self->{debug} ); my $file_data = &main::file_read( $self->{poll_data_file} ); @@ -297,21 +305,15 @@ sub process_check { } } - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne $com_status ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); - $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); - } - } } return unless ( defined $self->{cmd_process} ); + if ( $self->{cmd_process}->done_now() ) { main::print_log( "[Yeelight:" . $self->{name} . "] Background Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); my $file_data = &main::file_read( $self->{cmd_data_file} ); - my $com_status = "online"; + $com_status = "online"; if ($file_data) { @@ -325,54 +327,51 @@ sub process_check { # catch crashes: if ($@) { main::print_log( "[Yeelight:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + ${ $self->{cmd_queue} }[0][2]++; $com_status = "offline"; } else { if ($data->{result}[0] eq 'ok') { shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful - $self->{cmd_process_retry} = 0; $com_status = "online"; } else { - main::print_log( "[Yeelight:" . $self->{name} . "] Last command failed! Going to retry" ); + main::print_log( "[Yeelight:" . $self->{name} . "] Last command failed with code ." .$data->{result}[0] . "! Going to retry" ); + ${ $self->{cmd_queue} }[0][2]++; + $com_status = "offline"; } } - - if ( scalar @{ $self->{cmd_queue} } ) { - main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue found" ); - main::print_log( "[Yeelight:" . $self->{name} . "] " .join (", ",@{ $self->{cmd_queue} }) ); - - print join(", ", @{ $self->{cmd_queue} }); - my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. - $self->{cmd_process}->set($cmd); - main::eval_with_timer "$self->{cmd_process}->start()",1; #wait a few seconds before trying again - main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) - if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); - } - } - else { + } - main::print_log( "[Yeelight:" . $self->{name} . "] WARNING Issued command was unsuccessful, retrying..." ); - if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { - main::print_log( "[Yeelight:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); - shift @{ $self->{cmd_queue} }; - $self->{cmd_process_retry} = 0; - $com_status = "offline"; - } - else { - $self->{cmd_process_retry}++; - } + if (( scalar @{ $self->{cmd_queue} } ) and ($self->{cmd_process}->done())) { + my ($cmd, $time, $retry) = @ { ${ $self->{cmd_queue} }[0] }; + #print "*** cmd=$cmd, time=$time, retry=$retry\n"; + if ($retry > $self->{command_timeout_limit}) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR: Abandoning command $cmd due to $retry retry attempts" ); + shift @{ $self->{cmd_queue}}; + } elsif (($main::Time - $time) > $self->{command_timeout}) { + main::print_log( "[Yeelight:" . $self->{name} . "] ERROR: $cmd request older than " . $self->{command_timeout} ." seconds. Abandoning request" ); + shift @{ $self->{cmd_queue}}; + } elsif ($main::Time > ($time + 1 + ($retry * 5)) and ($self->{cmd_process}->done() )) { #the original time isn't a great base for deep queued commands + if ($retry == 0) { + main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue found, processing next item" ); + } else { + main::print_log( "[Yeelight:" . $self->{name} . "] Retrying previous command. Attempt number $retry" ); + } + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + main::print_log( "[Yeelight:" . $self->{name} . "] Command Queue (" . $self->{cmd_process}->pid() . ") cmd=$cmd" ) if ( $self->{debug} ); } - - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne $com_status ) { - $self->{status} = $com_status; - if ($self->{child_object}->{comm}->state() ne $com_status) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); - $self->{child_object}->{comm}->set( $com_status, 'poll' ); - } + } + + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + $self->{status} = $com_status; + if ($self->{child_object}->{comm}->state() ne $com_status) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); } } } @@ -413,23 +412,24 @@ sub _push_TCP_data { $self->{cmd_process}->start(); $self->{cmd_process_pid}->{ $self->{cmd_process}->pid() } = $mode; #capture the type of information requested in order to parse; $self->{cmd_process_mode} = $mode; - push @{ $self->{cmd_queue} }, "$cmd"; + push @{ $self->{cmd_queue} }, [$cmd,$main::Time,0]; - main::print_log( "[Yeelight:" . $self->{name} . "] Backgrounding " . $self->{cmd_process}->pid() . " command $mode, $cmd" ) if ( $self->{debug} ); + main::print_log( "[Yeelight:" . $self->{name} . "] Backgrounding (" . $self->{cmd_process}->pid() . ") command $mode, $cmd" ) if ( $self->{debug} ); } else { if ( scalar @{ $self->{cmd_queue} } < $self->{max_cmd_queue} ) { - main::print_log( "[Yeelight:" . $self->{name} . "] Queue is " . scalar @{ $self->{cmd_queue} } . ". Queing command $mode, $cmd" ) - if ( $self->{debug} ); - push @{ $self->{cmd_queue} }, "$cmd"; + main::print_log( "[Yeelight:" . $self->{name} . "] Queue is " . scalar @{ $self->{cmd_queue} } . ". Queing command $mode, $cmd" ) if ( $self->{debug} ); + push @{ $self->{cmd_queue} }, [$cmd,$main::Time,0]; } else { main::print_log( "[Yeelight:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne "offline" ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); + if ($self->{child_object}->{comm}->state() ne "offline" ) { + main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } } } } @@ -630,7 +630,7 @@ sub process_data { } if ( $self->{previous}->{info}->{bright} != $self->{data}->{info}->{bright} ) { - main::print_log( "[Yeelight:" . $self->{name} . "] Brightness changed from $self->{previous}->{info}->{bright} to $self->{data}->{info}->{brightn}" ) if ( $self->{loglevel} ); + main::print_log( "[Yeelight:" . $self->{name} . "] Brightness changed from $self->{previous}->{info}->{bright} to $self->{data}->{info}->{bright}" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{bright} = $self->{data}->{info}->{bright}; $self->set( $self->{data}->{info}->{bright}, 'poll' ); } @@ -964,5 +964,4 @@ sub set { # Version History # v1.0.0 - initial module # v1.0.1 - color support -# v1.2 - RGB changes -# v1.2.1 - minor fixes +# v1.2.1 - command retry logic From ae5150f532d81c25002928c0689be2aad2334328 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 25 Aug 2018 12:40:03 -0600 Subject: [PATCH 45/78] process_item helper for issuing tcp commands --- bin/get_tcp | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100755 bin/get_tcp diff --git a/bin/get_tcp b/bin/get_tcp new file mode 100755 index 000000000..22a2d074d --- /dev/null +++ b/bin/get_tcp @@ -0,0 +1,124 @@ +#!/usr/bin/env perl +# -*- Perl -*- + +use strict; +use IO::Socket; + +# Similar to get_url, open a socket and then get the data. Useful to spawn off as a process_item to avoid pauses + +my ( $Pgm_Path, $Pgm_Name ); + +BEGIN { + ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; + ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works +} + +my ( %config_parms, %parms ); + +use Getopt::Long; + +if ( + !&GetOptions( \%parms, 'h', 'help', 'quiet', 'timeout=s', 'rn') + or !@ARGV + or $parms{h} + or $parms{help} + ) +{ + + print < $host, +PeerPort => $port, +Timeout => $timeout, +Proto => "tcp") or $response = "get_tcp_error: opening socket: $!.\n"; +#print "error $host:$port\n" if ($tcp->connected()); + +unless ($response) { + $error = 0; + print "Sending data to $location " unless $parms{quiet}; + print "into $file" unless ($parms{quiet} or !$file); + print "..." unless $parms{quiet}; + + $tcp->send($data) or $response = "get_tcp_error: Couldn't send: $!"; + + unless ($response) { + $tcp->recv($response, 1024); + print " data retrieved\n" unless $parms{quiet}; + } else { + $error = 1; + } +} + if ($file) { + # print $data; + unless ( $file eq '/dev/null' ) { + if ($response) { + open( OUT, ">$file" ) + or die "get_tcp_error: could not open file '$file' for output: $!\n"; + binmode OUT; + print OUT $response; + close OUT; + } + else { + print " empty data response\n"; + } + } + } else { + print $response; + } + + +$tcp->close() unless ($error); + + From 3c3058f222f1efb60f9b708a3ce2de2ef4719b72 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 25 Aug 2018 12:48:44 -0600 Subject: [PATCH 46/78] fixed display when either grass or shrubs are being watered --- code/common/calc_eto.pl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index ab000f0d4..3460e3c38 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -956,7 +956,6 @@ sub detailSchedule { my $station_id = 1; $time = $time * 60; #add in seconds foreach my $station (split /,/, $lengths) { - next if ($station == 0); my $run_hour = 0; if ($station > 3600) { $run_hour = int($station / 3600); @@ -964,7 +963,7 @@ sub detailSchedule { } my $run_min = int($station / 60); my $run_sec = int($station % 60); - $msg .= "[calc_eto] : " . formatTime($time) . " : Station:" .sprintf("%2s",$station_id) . " Run Time:" .sprintf("%02d:%02d:%02d",$run_hour,$run_min,$run_sec) . "\n"; + $msg .= "[calc_eto] : " . formatTime($time) . " : Station:" .sprintf("%2s",$station_id) . " Run Time:" .sprintf("%02d:%02d:%02d",$run_hour,$run_min,$run_sec) . "\n" unless ($station == 0); $station_id++; $time += $run_sec + ($run_min * 60) + ($run_hour * 3600); } From 46acad706f078e04639facad01204c46ce020c7f Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 25 Aug 2018 16:12:05 -0600 Subject: [PATCH 47/78] static pages need an update to work with v2.0.750 --- web/ia7/house/modes.shtml | 8 ++++---- web/ia7/house/sample.shtml | 4 ++-- web/ia7/index.shtml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/ia7/house/modes.shtml b/web/ia7/house/modes.shtml index 376b763e6..b250f4e31 100644 --- a/web/ia7/house/modes.shtml +++ b/web/ia7/house/modes.shtml @@ -4,7 +4,7 @@
-
@@ -12,14 +12,14 @@
-
-
@@ -29,7 +29,7 @@
-
diff --git a/web/ia7/house/sample.shtml b/web/ia7/house/sample.shtml index 4e4fa57e8..2632e508a 100644 --- a/web/ia7/house/sample.shtml +++ b/web/ia7/house/sample.shtml @@ -15,7 +15,7 @@
-
@@ -23,7 +23,7 @@
-
diff --git a/web/ia7/index.shtml b/web/ia7/index.shtml index 688392f80..f22597ec5 100644 --- a/web/ia7/index.shtml +++ b/web/ia7/index.shtml @@ -360,7 +360,7 @@ From 43574131a5b378a646b637f298657c35abf2afcf Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 25 Aug 2018 16:30:26 -0600 Subject: [PATCH 48/78] v2.0.751 - fixed RRD display --- web/ia7/include/javascript.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 9ab5c1211..55035d5e4 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.750"; +var ia7_ver = "v2.0.751"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -1512,6 +1512,7 @@ var updateItem = function(item,link,time) { JSONStore(json); requestTime = json_store.meta.time; var color = getButtonColor(json.data[item].state); +//TODO object-state to all buttons! $('button[entity="'+item+'"]').find('.object-state').text( json.data[item].state); $('button[entity="'+item+'"]').removeClass("btn-default"); @@ -2405,10 +2406,10 @@ var graph_rrd = function(start,group,time) { var new_data = 1; var data_timeout = 0; var refresh = 60; //refresh data every 60 seconds by default - +//TODO Changepage, unless rrd-graph visible then stop refresh counter? if (!$('#rrd-graph').is(':visible')) { //if (URLHash.path == path){ $('#loader').show(); - console.log("showing loader "+URLHash.path+" : "+path+" : "+$('#top-graph').length); + console.log("showing loader "+URLHash.path+" : : "+$('#top-graph').length); } if (json_store.ia7_config.prefs.rrd_refresh !== undefined) refresh = json_store.ia7_config.prefs.rrd_refresh; From dff63bdeb9b5efd467d1908d0f2acf0d6c06aed7 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 2 Sep 2018 11:28:21 -0600 Subject: [PATCH 49/78] v2.0.800 - fixed voice command color change --- web/ia7/include/javascript.js | 55 +++++++++++------------------------ 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 55035d5e4..272331a70 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.751"; +var ia7_ver = "v2.0.800"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -534,7 +534,7 @@ function loadPrefs (config_name){ //show ia7 prefs, args ia7_prefs, ia7_rrd_pref contentType: 'application/json', data: JSON.stringify(json_store.ia7_config), success: function( data, status, error ){ - console.log("data="+data+" status="+status+" error="+error); + //console.log("data="+data+" status="+status+" error="+error); //throw up red warning if the response isn't good from MH $('#lastResponse').modal({ show: true @@ -561,8 +561,6 @@ function loadPrefs (config_name){ //show ia7 prefs, args ia7_prefs, ia7_rrd_pref config_modal_loop = setTimeout(function(){ $('#lastResponse').modal('hide'); }, 3000); - console.log("status="+status); - console.log("error="+error); $(".modal-header").append($("

 Failure: "+message+"

")); $(".write-status").delay(4000).fadeOut("slow", function () { $(this).remove(); }); } @@ -605,7 +603,6 @@ function security (){ } } for (var i in data){ - console.log("sec="+i); if (i == "group") { html += ""+ i + ""; //if data[i].length > 0 otherwise print no group data found @@ -644,7 +641,6 @@ function security (){ row++; var groups=""; for (var p in data.user[j]) { - console.log(p + ":" + data.user[j][p] + "
"); groups += group_pos[p]+","; } groups = groups.slice(0,-1); @@ -750,7 +746,6 @@ function security (){ } }); $('.security-submit').off('click').on('click', function(){ - console.log("submit"); var data = {}; var name = '#new_group'; var pk = 'add_group'; @@ -766,7 +761,6 @@ function security (){ data.name = $(entity_name).val(); if (data.name == '') { $(entity_name).css('border-color', 'red'); - console.log('return1'+entity_name); return } else { $(entity_name).css('border-color', ''); @@ -781,7 +775,6 @@ function security (){ } else { var groups = $('#usr_groups_'+row).editable('getValue').user_new; data.groups = []; - console.log("g="+JSON.stringify(groups)); for (var i in groups) { data.groups.push(group_data[groups[i]-1].text); } @@ -795,7 +788,6 @@ function security (){ data.submit = "true"; MD5(data.password); } - console.log("row="+row+" data="+JSON.stringify(data)); $.ajax({ url: "/json/security", type: 'post', @@ -1173,10 +1165,8 @@ var loadList = function() { } var btn_rgb = ""; if (json_store.objects[entity].rgb !== undefined) { - //TODO fix btn_rgb = ''; - btn_rgb += ''; - + btn_rgb += ''; } // direct control item, differentiate the button var btn_direct = ""; @@ -1387,6 +1377,7 @@ var sliderDetails = function (states) { var pct = 0; var slider_array = []; for(var i = 0; i < states.length; i++) { +//TODO gives tostring error on null object items sensor_garage_motion_off var val = states[i].toString(); if(val.indexOf('%') != -1) pct=1; val = val.replace(/\%/g,''); @@ -1447,7 +1438,7 @@ var updateList = function(path) { // This is not an entity, skip it if (json.data[entity] === undefined ) continue; if (json.data[entity].type === undefined) continue; - + if ($('button[entity="'+entity+'"]').hasClass('btn-voice-cmd')) continue; //don't change color for voice commands var color; if (json.data[entity].state === undefined) { color = "default"; @@ -1457,7 +1448,6 @@ var updateList = function(path) { var btn_rgb = ""; if (json.data[entity].rgb !== undefined) { $('button[entity="'+entity+'"]').find('.object-color').css("color",'rgb('+json.data[entity].rgb+')'); - console.log("changing color to "+json.data[entity].rgb); } $('button[entity="'+entity+'"]').find('.object-state').text(json.data[entity].state); $('button[entity="'+entity+'"]').removeClass("btn-default"); @@ -1512,9 +1502,7 @@ var updateItem = function(item,link,time) { JSONStore(json); requestTime = json_store.meta.time; var color = getButtonColor(json.data[item].state); -//TODO object-state to all buttons! - $('button[entity="'+item+'"]').find('.object-state').text( - json.data[item].state); + $('button[entity="'+item+'"]').find('.object-state').text(json.data[item].state); $('button[entity="'+item+'"]').removeClass("btn-default"); $('button[entity="'+item+'"]').removeClass("btn-success"); $('button[entity="'+item+'"]').removeClass("btn-warning"); @@ -1878,7 +1866,6 @@ var loadCollection = function(collection_keys) { }); // test multiple items at some point - console.log("items="+items); updateItem(items); } loadModule('developer'); @@ -1977,7 +1964,6 @@ var something_went_wrong = function(module,text,fadeout) { } }); if (found) return; - console.log("creating SWW window for "+module); var type = "dark"; var mobile = ""; if ($(window).width() <= 768) { // override the responsive mobile top-buffer @@ -2160,14 +2146,13 @@ var get_wi_icon = function (conditions,rain,snow,night) { var get_notifications = function(time) { if (updateSocketN !== undefined && updateSocketN.readyState != 4){ // Only allow one update thread to run at once - console.log ("Notify aborted "+updateSocketN.readyState); + //console.log ("Notify aborted "+updateSocketN.readyState); updateSocketN.abort(); } if (time === undefined) time = new Date().getTime(); //this triggers on failure. var arg_str = "long_poll=true&time="+time; var path_str = "/notifications"; var requestTime; - console.log("in get_notifications"); updateSocketN = $.ajax({ type: "GET", url: "/LONG_POLL?json('GET','"+path_str+"','"+arg_str+"')", @@ -2265,9 +2250,7 @@ var ajax_req_success = function(module) { var errors = 0; for(var i in req_errors) { errors += req_errors[i]; - console.log("success reconnect "+i+" "+req_errors[i]+" "+errors); } - console.log("In success reconnect for "+module+ ":"+errors); // if (errors === 0) { //$("#mh_title").css("color", "black"); @@ -2406,11 +2389,14 @@ var graph_rrd = function(start,group,time) { var new_data = 1; var data_timeout = 0; var refresh = 60; //refresh data every 60 seconds by default -//TODO Changepage, unless rrd-graph visible then stop refresh counter? - if (!$('#rrd-graph').is(':visible')) { //if (URLHash.path == path){ + if (!$('#rrd-graph').is(':visible')) { //If not on an RRD page then stop the timer, otherwise show the loader + //console.log("checking loader "+URLHash.path+" : x : "+$('#top-graph').length); + if (URLHash.path === undefined || URLHash.path.substring(0,4) !== "/rrd") { + clearTimeout(rrd_refresh_loop); + return; + } $('#loader').show(); - console.log("showing loader "+URLHash.path+" : : "+$('#top-graph').length); - } + } if (json_store.ia7_config.prefs.rrd_refresh !== undefined) refresh = json_store.ia7_config.prefs.rrd_refresh; @@ -3695,7 +3681,6 @@ var create_state_modal = function(entity) { }); }); if (json_store.objects[entity].rgb !== undefined) { - console.log("Insert RGB Slider Here"); $('#control').find('.states').append("
"); $('#control').find('.states').append("
"); $('#control').find('.states').append("
"); @@ -3729,7 +3714,6 @@ var create_state_modal = function(entity) { $( ".rgb-slider" ).on( "slidechange", function(event, ui) { var sliderstate = $('#sliderR').slider("value")+","+$('#sliderG').slider("value")+","+$('#sliderB').slider("value"); var rgb_url= '/SET;none?select_item='+$(this).parents('.control-dialog').attr("entity")+'&select_state='+sliderstate+'&select_setby=rgb'; - console.log("rgb_url="+rgb_url); $.get(rgb_url).fail(function() { $(".modal-header").append($("

 Failure: Could not send command to Misterhouse

")); $(".get-status").delay(4000).fadeOut("slow", function () { $(this).remove(); }); @@ -3888,7 +3872,6 @@ var create_state_modal = function(entity) { contentType: 'application/json', data: JSON.stringify(data), success: function( data, status, error ){ - console.log("data="+data+" status="+status+" error="+error); //throw up red warning if the response isn't good from MH if (data.status !== undefined || data.status == "error") { var message = "Unknown server error"; @@ -3909,8 +3892,6 @@ var create_state_modal = function(entity) { var message = "Unknown ajax request error"; var data = JSON.parse(xhr.responseText); if (data !== undefined && data.text !== undefined) message = data.text; - console.log("status="+status); - console.log("error="+error); $(".modal-header").append($("

 Failure: "+message+"

")); $(".write-status").delay(4000).fadeOut("slow", function () { $(this).remove(); }); } @@ -4093,7 +4074,6 @@ var create_develop_item_modal = function(colid,col_parent) { if (!(prop == "mode")) { // loop through properties if ($('#col_'+prop).val() !== '') json_store.collections[colid][prop] = $('#col_'+prop).val(); - console.log("prop="+prop+" val="+$('#col_'+prop).val()); } } } @@ -4440,7 +4420,6 @@ var trigger = function() { contentType: 'application/json', data: JSON.stringify(data), success: function( data, status, error ){ - console.log("trigger success data="+data+" status="+status+" error="+error); if (data.status !== undefined || data.status == "error") { console.log("error!"); } else { @@ -4790,14 +4769,14 @@ function getScript(source, callback) { var loadModule = function(name,callback) { if (modules[name].loaded == 1) { - console.log("Module "+name+" already loaded"); + //console.log("Module "+name+" already loaded"); return 0; } //loop through all modules if all if (modules[name].script !== undefined ) { for (var i = 0, len = modules[name].script.length; i < len; i++) { - console.log("loading script "+name+" "+modules[name].script[i]); + //console.log("loading script "+name+" "+modules[name].script[i]); modules[name].loaded = 1; if (modules[name].callback !== undefined && (i + 1) == (len)) callback = modules[name].callback; getScript("/ia7/include/"+modules[name].script[i], callback); @@ -4806,7 +4785,7 @@ var loadModule = function(name,callback) { if (modules[name].css !== undefined ) { for (var i = 0, len = modules[name].css.length; i < len; i++) { modules[name].loaded = 1; - console.log("loading css "+name+" "+modules[name].css[i]); + //console.log("loading css "+name+" "+modules[name].css[i]); var fileref = document.createElement("link") fileref.setAttribute("rel", "stylesheet") fileref.setAttribute("type", "text/css") From 941c9f0ab7d73e6a7f1380cdd55c117267d7844f Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 2 Sep 2018 11:29:03 -0600 Subject: [PATCH 50/78] remove some debug statements --- code/common/calc_eto.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 3460e3c38..02b084df0 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -770,7 +770,7 @@ sub writeResults { #HP TODO This will determine if a 2nd, 3rd or 4th time is required. $times = 1 if ( ( $aET / $minRunmm ) > 1 ); #if the minium threshold is met, then run at least once. $times = int( max( min( $aET / $maxRunmm, 4 ), $times ) ); # int(.999999) = 0 - print "[calc_eto] DB: times=$times aET=$aET minRunm=$minRunmm maxRunm=$maxRunmm\n"; #if ($debug); + print "[calc_eto] DB: times=$times aET=$aET minRunm=$minRunmm maxRunm=$maxRunmm\n" if ($debug); print "E: aET[$x] = $aET (" . $aET / $maxRunmm . ") // mm/Day\n" if ($debug); print "E: times = $times (max " . max( min( $aET / $maxRunmm, 4 ), $times ) . "/min " @@ -853,7 +853,7 @@ sub writeResults { @availTimes = ($sun->{set} - sum(@runTime) / 60, $sun->{set} + 60, $sun->{set} + 120, $sun->{set} - (sum(@runTime) / 60) - 60 ); } - print "[times=$times, sun->{rise}=" . $sun->{rise} . " sum=" . sum(@runTime) / 60 . "]\n"; # if ($debug); + print "[times=$times, sun->{rise}=" . $sun->{rise} . " sum=" . sum(@runTime) / 60 . "]\n" if ($debug); for ( my $i = 0; $i < $times; $i++ ) { $startTime[$i] = int( $availTimes[$i] ); From 58c095dd59c10fbc7190d90e9068e296ab2b7c59 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 2 Sep 2018 11:29:58 -0600 Subject: [PATCH 51/78] fixed queue printing --- lib/raZberry.pm | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 8209c97e8..3ba2c35e6 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -186,7 +186,7 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v3.0.3 Controller Initializing..."); + &main::print_log("[raZberry]: v3.0.4 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; @@ -524,11 +524,11 @@ sub process_check { } elsif (($main::Time - $time) > $self->{command_timeout}) { main::print_log( "[raZberry:" . $self->{host} . "] ERROR: $get_cmd request older than " . $self->{command_timeout} . " seconds. Abandoning request" ); shift @{ $self->{cmd_queue}}; - } elsif (($main::Time > ($time + 1 + ($retry * 5)) and ($self->{cmd_process}->done() ) )) { + } elsif (($main::Time > ($time + 1 + ($retry * 5)) and ($self->{cmd_process}->done() ) )) {#the original time isn't a great base for deep queued commands if ($retry == 0) { main::print_log( "[raZberry:" . $self->{host} . "] Command Queue found, processing next item" ); } else { - main::print_log( "[raZberry:" . $self->{host} . "] Retrying previous command" ); + main::print_log( "[raZberry:" . $self->{host} . "] Retrying previous command. Attempt number $retry" ); } $self->{cmd_process}->set($get_cmd); $self->{cmd_process}->start(); @@ -951,7 +951,12 @@ sub print_command_queue { $name = "empty" if ($commands == 0); main::print_log( "[raZberry:" . $self->{host} . "]: Current Command Queue: $name" ); for my $i ( 1 .. $commands ) { - main::print_log( "[raZberry:" . $self->{host} . "]: Command $i: " . @{ $self->{cmd_queue} }[$i - 1] ); + my ($mode, $cmd, $time, $retry) = @ { ${ $self->{cmd_queue} }[$i - 1] }; + main::print_log( "[raZberry:" . $self->{host} . "]: Command $i Mode: " . $mode ); + main::print_log( "[raZberry:" . $self->{host} . "]: Command $i Cmd: " . $cmd ); + main::print_log( "[raZberry:" . $self->{host} . "]: Command $i Time: " . $time ); + main::print_log( "[raZberry:" . $self->{host} . "]: Command $i Retry: " . $retry ); + } } main::print_log( "[raZberry:" . $self->{host} . "]: ------------------------------------------------------------------" ); From 3182e5aafb24c63d78bca44fe8eb395ce52732e7 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 2 Sep 2018 12:33:40 -0600 Subject: [PATCH 52/78] v1.2.6 - command queue, and off to brightness fixes --- lib/Yeelight.pm | 146 ++++++++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 62 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index 766af91b0..14ffc6d02 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,55 +1,72 @@ -package Yeelight; - -# v1.2.3 - -#TODO -#- test queuing fast commands -#- check query data -#- test socket reconnection -#- retry time delay, should be based off process_item start not the original request time. - -use strict; -use warnings; - -use LWP::UserAgent; -use HTTP::Request::Common qw(POST); -use JSON::XS; -use Data::Dumper; -use Socket; -use IO::Select; -use IO::Socket::INET; +=head1 B v1.2.6 +=head2 Initial Setup # To set up, first pair with mobile app -- the Yeelight needs to be set up initially with the app # to get it's wifi information. # if problems with ios, use the android app if you can. # MAKE SURE TO SELECT A 2.4Ghz WIRELESS NETWORK # TURN ON LOCAL CONTROL -# Firmware supported +=head2 Firmware supported # led strip (stripe) : 44 -# Yeelight Objects -# -# $yeelight = new Yeelight('10.10.1.1'); -# $yeelight_comm = new Yeelight_Comm($yeelight); -# $yeelight_ct = new Yeelight_Colortemp($yeelight); -# $yeelight_rgb = new Yeelight_RGB($yeelight); +=head2 Yeelight Objects + +$yeelight = new Yeelight('10.10.1.1'); +$yeelight_comm = new Yeelight_Comm($yeelight); +$yeelight_ct = new Yeelight_Colortemp($yeelight); +$yeelight_rgb = new Yeelight_RGB($yeelight); + + yeelight_rgb the set value is 'red, green, blue' + ie $yeelight_rgb->set('255,10,32'); + + + +=head2 MH.INI CONFIG PARAMS + +yeelight_timeout HTTP request timeout (default 5) +yeelight_poll_seconds Number of seconds to poll the raZberry +yeelight_user Authentication username +yeelight_password Authentication password +yeelight_max_cmd_queue Maximum number of commands to queue up (default 6) +yeelight_com_threshold Number of failed polls before controller marked offline (default 4) +yeelight_command_timeout Number of seconds after a command is issued before it is abandoned (default 60) +yeelight_command_timeout_limit Maximum number of retries for a command before abandoned -# yeelight_rgb the set value is 'red, green, blue' -# ie $yeelight_rgb->set('255,10,32'); +=head2 Notes -# OPTIONS +The Yeelight needs to be specified as an IP address, since the module uses SSDP scan to determine +what features are supported +=head2 Issues +- retry time delay, should be based off process_item start not the original request time. -# Notes -# -# The Yeelight needs to be specified as an IP address, since the module uses SSDP scan to determine -# what features are supported +=head2 TODO +- test queuing fast commands +- check query data +- test socket reconnection +- test multi from state on +- test multi from state off +- comm tracker went offline when commands dropped +- 09/02/18 11:54:33 AM [Yeelight:1] WARNING. Queue has grown past 8. Command get_tcp -rn -quiet 192.168.0.173:55443 '{ "id":1, "method":"set_bright", "params":[90,"smooth",500] }' discarded. +- 09/02/18 11:54:33 AM [Yeelight:1] Communication Tracking object found. Updating from online to offline... -# Issues +=cut + +package Yeelight; + +use strict; +use warnings; + +use LWP::UserAgent; +use HTTP::Request::Common qw(POST); +use JSON::XS; +use Data::Dumper; +use Socket; +use IO::Select; +use IO::Socket::INET; -# @Yeelight::ISA = ('Generic_Item'); @@ -91,7 +108,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.2.3"; + $self->{module_version} = "v1.2.6"; $self->{ssdp_timeout} = 1000; $self->{socket_connected} = 0; $self->{host} = $location; @@ -106,7 +123,7 @@ sub new { $options = "" unless ( defined $options ); $options = $::config_parms{ "yeelight_" . $self->{name} . "_options" } if ( $::config_parms{ "yeelight_" . $self->{name} . "_options" } ); - $self->{debug} = 0; + $self->{debug} = 5; ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) if ( $options =~ m/debug\=/i ); $self->{debug} = 0 if ( $self->{debug} < 0 ); @@ -115,7 +132,7 @@ sub new { $self->{poll_data_timestamp} = 0; $self->{max_poll_queue} = 3; - $self->{max_cmd_queue} = 5; + $self->{max_cmd_queue} = 8; $self->{cmd_process_retry_limit} = 6; $self->{command_timeout} = 60; @@ -422,16 +439,16 @@ sub _push_TCP_data { push @{ $self->{cmd_queue} }, [$cmd,$main::Time,0]; } else { - main::print_log( "[Yeelight:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne "offline" ) { - $self->{status} = "offline"; - if ($self->{child_object}->{comm}->state() ne "offline" ) { - main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } - } + main::print_log( "[Yeelight:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command $cmd discarded." ); +# if ( defined $self->{child_object}->{comm} ) { +# if ( $self->{status} ne "offline" ) { +# $self->{status} = "offline"; +# if ($self->{child_object}->{comm}->state() ne "offline" ) { +# main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); +# $self->{child_object}->{comm}->set( "offline", 'poll' ); +# } +# } +# } } } } @@ -673,7 +690,10 @@ sub print_command_queue { $name = "empty" if ($commands == 0); main::print_log( "Yeelight:" . $self->{name} . "] Current Command Queue: $name" ); for my $i ( 1 .. $commands ) { - main::print_log( "Yeelight:" . $self->{name} . "] Command $i: " . @{ $self->{cmd_queue} }[$i - 1] ); + my ($cmd, $time, $retry) = @ { ${ $self->{cmd_queue} }[$i - 1] }; + main::print_log( "Yeelight:" . $self->{name} . "] Command $i cmd: " . $cmd ); + main::print_log( "Yeelight:" . $self->{name} . "] Command $i time: " . $time ); + main::print_log( "Yeelight:" . $self->{name} . "] Command $i retry: " . $retry ); } main::print_log( "Yeelight:" . $self->{name} . "] ------------------------------------------------------------------" ); @@ -721,19 +741,21 @@ sub set { main::print_log( "[Yeelight:" . $self->{name} . "] DB set_mode, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); my $mode = lc $p_state; if ( $mode =~ /^(\d+)/ ) { + main::print_log( "[Yeelight:" . $self->{name} . "] DB power = $self->{data}->{info}->{power} \$1 = $1 " ) if ( $self->{debug} ); + if (($self->{data}->{info}->{power} eq "on") and ($1 == 0)) { $self->set("off"); } elsif (($self->{data}->{info}->{power} eq "off") and ($1 > 0)) { $self->set("on"); - main::print_log( "Yeelight:" . $self->{name} . "] Brightness change, delayed state change to $mode" ) if ( $self->{debug} ); - my $object_name = $self->get_object_name; - my $cmd_string = $object_name . '->set("' . $mode .'");'; - main::eval_with_timer $cmd_string, $self->{brightness_state_delay}; - } else { + #main::print_log( "Yeelight:" . $self->{name} . "] Brightness change, delayed state change to $mode" ) if ( $self->{debug} ); + #my $object_name = $self->get_object_name; + #my $cmd_string = $object_name . '->set("' . $mode .'");'; + #main::eval_with_timer $cmd_string, $self->{brightness_state_delay}; + } #else { my @params = @{$param_array{"bright"}}; unshift @params, $1; $self->_push_TCP_data( 'brightness', @params ); - } + #} } elsif ( $mode =~ /^([-+]\d+)/ ) { my $value = $self->{info}->{bright} + $1; @@ -741,18 +763,18 @@ sub set { $self->set("off"); } elsif (($self->{data}->{info}->{power} eq "off") and ($value > 0)) { $self->set("on"); - main::print_log( "Yeelight:" . $self->{name} . "] Brightness change, delayed state change to $mode" ) if ( $self->{debug} ); - my $object_name = $self->get_object_name; - my $cmd_string = $object_name . '->set("' . $mode .'");'; - main::eval_with_timer $cmd_string, $self->{brightness_state_delay}; - } else { + #main::print_log( "Yeelight:" . $self->{name} . "] Brightness change, delayed state change to $mode" ) if ( $self->{debug} ); + #my $object_name = $self->get_object_name; + #my $cmd_string = $object_name . '->set("' . $mode .'");'; + #main::eval_with_timer $cmd_string, $self->{brightness_state_delay}; + } #else { my @params = @{$param_array{$mode}}; $value = 0 if ($value < 0); $value = 100 if ($value > 100); unshift @params, $value; $self->_push_TCP_data( 'brightness', @params ); - } + #} } elsif ( ( $mode eq "on" ) or ( $mode eq "off" ) ) { $self->_push_TCP_data($mode, @{$param_array{$mode}}); From 7ece140150be9fe486f745477f1b1cce652a2e1c Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 3 Sep 2018 08:45:32 -0600 Subject: [PATCH 53/78] added in some more runtime details --- code/common/calc_eto.pl | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 02b084df0..36233b14c 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -950,12 +950,13 @@ sub detailSchedule { my ($stime) = @_; my ($times, $lengths) = $stime =~ /\[\[(.*)\],\[(.*)\]\]/; my $msg = ""; - + my $total_time = 0; foreach my $time (split /,/, $times) { next if ($time == -1); my $station_id = 1; $time = $time * 60; #add in seconds foreach my $station (split /,/, $lengths) { + $total_time += $station; my $run_hour = 0; if ($station > 3600) { $run_hour = int($station / 3600); @@ -966,7 +967,17 @@ sub detailSchedule { $msg .= "[calc_eto] : " . formatTime($time) . " : Station:" .sprintf("%2s",$station_id) . " Run Time:" .sprintf("%02d:%02d:%02d",$run_hour,$run_min,$run_sec) . "\n" unless ($station == 0); $station_id++; $time += $run_sec + ($run_min * 60) + ($run_hour * 3600); - } + } + if ($total_time > 0) { + my $t_hours = 0; + if ($total_time > 3600) { + $t_hours = int($total_time / 3600); + $total_time = int($total_time % 3600); + } + my $t_min = int($total_time / 60); + my $t_sec = int($total_time % 60); + $msg .= "[calc_eto] : Total Run Time: " . .sprintf("%02d:%02d:%02d",$t_hours,$t_min,$t_sec) . "\n"; + } } return ($msg); @@ -1213,7 +1224,12 @@ sub main_calc_eto { } - $msg = "[calc_eto] RESULTS sunrise & sunset in minutes from midnight local time: " . $sun->{rise} . ' ' . $sun->{set}; + my $sr_hour = int($sun->{rise} / 60); + my $sr_min = int($sun->{rise} % 60); + my $ss_hour = int($sun->{set} / 60); + my $ss_min = int($sun->{set} % 60); + + $msg = "[calc_eto] RESULTS sunrise & sunset from midnight local time: $sr_hour:$sr_min (" . $sun->{rise} . ") $ss_hour:$ss_min (" . $sun->{set} . ")"; print_log $msg; $msg_string .= $msg . "\n"; From e419cad1733ac70c8848f28df8509ef5eb82e374 Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 3 Sep 2018 10:34:47 -0600 Subject: [PATCH 54/78] fixed typo --- code/common/calc_eto.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 36233b14c..6eab03e5e 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -976,7 +976,7 @@ sub detailSchedule { } my $t_min = int($total_time / 60); my $t_sec = int($total_time % 60); - $msg .= "[calc_eto] : Total Run Time: " . .sprintf("%02d:%02d:%02d",$t_hours,$t_min,$t_sec) . "\n"; + $msg .= "[calc_eto] : Total Run Time: " . sprintf("%02d:%02d:%02d",$t_hours,$t_min,$t_sec) . "\n"; } } return ($msg); From 35f90d0d671545d213968d480109220485db1185 Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 3 Sep 2018 17:41:57 -0600 Subject: [PATCH 55/78] ia7 speaker icon --- web/ia7/graphics/fp_speaker_danger_128.png | Bin 0 -> 65859 bytes web/ia7/graphics/fp_speaker_danger_32.png | Bin 0 -> 4217 bytes web/ia7/graphics/fp_speaker_danger_48.png | Bin 0 -> 9365 bytes web/ia7/graphics/fp_speaker_danger_64.png | Bin 0 -> 16561 bytes web/ia7/graphics/fp_speaker_default_128.png | Bin 0 -> 65859 bytes web/ia7/graphics/fp_speaker_default_32.png | Bin 0 -> 4217 bytes web/ia7/graphics/fp_speaker_default_48.png | Bin 0 -> 9365 bytes web/ia7/graphics/fp_speaker_default_64.png | Bin 0 -> 16561 bytes web/ia7/graphics/fp_speaker_info_128.png | Bin 0 -> 65859 bytes web/ia7/graphics/fp_speaker_info_32.png | Bin 0 -> 4217 bytes web/ia7/graphics/fp_speaker_info_48.png | Bin 0 -> 9365 bytes web/ia7/graphics/fp_speaker_info_64.png | Bin 0 -> 16561 bytes web/ia7/graphics/fp_speaker_success_128.png | Bin 0 -> 65859 bytes web/ia7/graphics/fp_speaker_success_32.png | Bin 0 -> 4217 bytes web/ia7/graphics/fp_speaker_success_48.png | Bin 0 -> 9365 bytes web/ia7/graphics/fp_speaker_success_64.png | Bin 0 -> 16561 bytes web/ia7/graphics/fp_speaker_warning_128.png | Bin 0 -> 65859 bytes web/ia7/graphics/fp_speaker_warning_32.png | Bin 0 -> 4217 bytes web/ia7/graphics/fp_speaker_warning_48.png | Bin 0 -> 9365 bytes web/ia7/graphics/fp_speaker_warning_64.png | Bin 0 -> 16561 bytes 20 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 web/ia7/graphics/fp_speaker_danger_128.png create mode 100644 web/ia7/graphics/fp_speaker_danger_32.png create mode 100644 web/ia7/graphics/fp_speaker_danger_48.png create mode 100644 web/ia7/graphics/fp_speaker_danger_64.png create mode 100644 web/ia7/graphics/fp_speaker_default_128.png create mode 100644 web/ia7/graphics/fp_speaker_default_32.png create mode 100644 web/ia7/graphics/fp_speaker_default_48.png create mode 100644 web/ia7/graphics/fp_speaker_default_64.png create mode 100644 web/ia7/graphics/fp_speaker_info_128.png create mode 100644 web/ia7/graphics/fp_speaker_info_32.png create mode 100644 web/ia7/graphics/fp_speaker_info_48.png create mode 100644 web/ia7/graphics/fp_speaker_info_64.png create mode 100644 web/ia7/graphics/fp_speaker_success_128.png create mode 100644 web/ia7/graphics/fp_speaker_success_32.png create mode 100644 web/ia7/graphics/fp_speaker_success_48.png create mode 100644 web/ia7/graphics/fp_speaker_success_64.png create mode 100644 web/ia7/graphics/fp_speaker_warning_128.png create mode 100644 web/ia7/graphics/fp_speaker_warning_32.png create mode 100644 web/ia7/graphics/fp_speaker_warning_48.png create mode 100644 web/ia7/graphics/fp_speaker_warning_64.png diff --git a/web/ia7/graphics/fp_speaker_danger_128.png b/web/ia7/graphics/fp_speaker_danger_128.png new file mode 100644 index 0000000000000000000000000000000000000000..a0876ec6201ab16381e347e5959f7d9dc6e211e8 GIT binary patch literal 65859 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�c0EZd~EDA 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 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_danger_48.png b/web/ia7/graphics/fp_speaker_danger_48.png new file mode 100644 index 0000000000000000000000000000000000000000..7f594d2dbd32c6dd22c77389b1f3460e2738fea8 GIT binary patch literal 9365 zcmeHNeQ;FO760A$wO`p}^RbW+f}sQwz8XfYgMf}uRDuYC9R(c~ojUFKg)?ZUT{`Fnt=)9%Z?oH9W@y0I2M-JJ+r*n#%xcKy04O%R6sMIf<6|W z8GRF+Bf*Ux5@mawN1wc@uJvR_rx6-PEkJGMu)$x z|F4nyrrOU5M9jRZI=8D#nJzH`e-Vno4UiB%J+9wU)6j6%l{hORq4Tn8dq!hwmCR&w z;>F~D+m}SV1W%^2XZFsTJpOERCXe(|cpE#M&uYwmy8?ymRd^b)U>9~Xw+FTo3dxKp zc+AL3PfUsy!+B9(rhq02}_M6n>jcmO}5`g5F$^$^do;44S`~idx z9fc7ZiWpd~7w#+P!!x}EymBh?=gvp>_LtDQc~3TM=Az3`3uQ@PM;&(3E$lq4WK|*B zVb>^QG9&(7Xju^E}A8 zav_`redzz9m9$C-o6b+z1K}=T?e#SNwX6F`8hZMXA%KW&G@JUggSn>A?3>CnDx)Rv z?qzqO;^CFBIW>|yf#64b(C_OcNOWn1@GM;bId&YUc6|Unl7KdG5;T_v`{e0RcqfAO zZQya**N0zBVLGC@tDHHzUJnG)Y#~F1L|~@CR!OprN$njs-i7Zxa0{G;E@CeVc`dU2 z?@8 z%c=d}>F+GRL`Y2XFHo39ftg@FikoKMx#B_0{N)`M@F}8VE>F{l5@po}o3{k+1vg>F zmJLuo--X6a|3sXOSn*8446>deklVYX6%O<+t>7f;dqOcC+C46)nFitw5l7pD+E zFZawX=M^D$`JJd(w}Qe@(R+AF>a%!bd{m2;7hXY6T?e?qV6V6WWslqk{_$>{_^4e3 zgpkHpWBm0MnS8}N!X!&-RY6si$I2y5g~VY*QPgtkOnrvI(aKP=ZY}bwCs@YQhEqBb zwt|$}SYDY`qM><^yX0mR%_~Fi+dI(m(|VLWwGQ6uQn=+zZX{Y75%{>-^pW<;YPi2w zkilPUY!m@cloE7MvXnFd#4(U@IWOt^lgpoSYbgaWy{d)4*{c-scXgrv2mI*y;|plr`57HwIMgx%JlTtI?T3h&dz=lr6OxPO;kRfXYe~{FdV0{e;}FIBRAdJ;bK7Bnegg9lFJQjJ=3464Z(rDE`qR6B1$r2zSLHPq+-K z=@1jpd;1})wE4*dheGM3WJA1DI7q$-HzeJPBTWF2`k;eEpXiXjfJyLU7lC|7@g$6U z>+&mYGDL@pFCLMOqL@0KNVy6)F?(VGosj-hSJvyQQh@qeJ zM7vnYj3pe5A`znnO80J7T3PGNW~WUm2SzxIP~Csf{@gYsDpsNN>IvqW#_INBATgd? z>TFP=0Eu7_@y>82f8r~!88W43EG5k#B1$-*a%U^8!qvl7hBwut^X1nNo^IU-6gaQC z3hpUh6O4tfSrs&ljuYtGv>5|sxrntjf#-e~A@pqf2ijlS1NY;Pz&n-pPs%igKiQ4$1N~X~nwSXN z@RVel0AgN6x-U+PNM}CE29EAR+ox#V@E7zobPf|j%w&>=akPn|&r=BczQC!MU%^*f z-i2%Ry_k6YY}0ebj@RMq7sbIVlP4(;ndjg!flXkv7Sj({8$`T)r?=3ct$UTqoP;0= z;#^PTR0jrIf^f{41})D)qY@b0_a6F>b`yD`(RC8RPd`WC?E~oVFfieX2QYq7HMATJ zi9jbhH@}RY{cWZf9nLGl1Be^JUo2~EK4`@%>H=N1!uHOrem6%dUZ^rhw)Z2qU3LYg z{Q4fK_Ca)Qd=dVG$A|iAdGOqKFUszl4@Dz0ChofbBu@P1x9F<?)ICE>(~Q_Mjlk1Ek-rPwfhvtYC7Ql676;zu7 zPVRj0$RTvUz7zi1mb88;9LfAp&>uJA&t(HI`i&$2V$#5ViL>o09QSC*8?j%Cne}O$ zXh-Zss%1z3?mvaTUArJfrX#$6AA%n?A^Np&Dnorzqr{VJQKzkGhf5Cbv$k97*+-u4 zArf0EbNwNYOt|2urr3-c#VdZlf3?@Ok0V|{ z+j==2IUO>yeIu(8=Ytd{_xQu;dOgO2k1eflZc70?*K^b>kRr67lD?5d&b5-Ht+TWI z>m1BWr`0hzsT3*NUv*NKZi?Z1-lI6@IpJ-t`8Gc>o#C9Uu(w1Ck|}F#@J!Q~r&Pw4 zIh&s)-#lOZFMvjAE%6~I6%VE6ihK++C^V+;F(g0&G5RVK=!l~4Si*>Hz2RuXtJE`N z!y3jr>(3O2tS?8?IP}StEzl%amCSAB5>qBpVCYGnwM9(NBL*}2=<{BCf<;?n#vpxi z<^T6ju4(d#?3qYF#T4Bm(jX#}kUETuBcV7|iGzuzD}f#gkB3K?X~Zu5xHJP7Zw7w0 Xr@Z-w7gv5DIM!UheDStLYd8E4vavTO literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_danger_64.png b/web/ia7/graphics/fp_speaker_danger_64.png new file mode 100644 index 0000000000000000000000000000000000000000..cae87873e0f5ddca5b0857fcb92628dce6763963 GIT binary patch literal 16561 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 D=?W)q literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_default_128.png b/web/ia7/graphics/fp_speaker_default_128.png new file mode 100644 index 0000000000000000000000000000000000000000..1e80842734aaf2de2dbac1136359d8cef43658ef GIT binary patch literal 65859 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 V?{>_W+C!(u5xu)T)^)(>{|EFy4ITgh literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_default_32.png b/web/ia7/graphics/fp_speaker_default_32.png new file mode 100644 index 0000000000000000000000000000000000000000..174464b4c079613a82080b0da090c5020077f514 GIT binary patch literal 4217 zcmd^CeNa?Y6#p%-3(J=aM36X!gXuV;1hec|0gWt{n3CiBS2{R2j#h99; zRLB68sc9dQi8GBgP1!=FH4RXRp(cK!5LN>5&F->n=RCNtee5nD>VLkO`_6s$-t#-Z zd+)jDo|n6D!J`4b<9z`Hq|QxQL^6_#kA~hGv$H!%Xfx(6-3a)Fa8cp?55fW6^wgBu zPd=knMd~iM(wIB^QH^!}H8nMUR;$%ux7(+vRH|Usr+N$3>(pv>MNCXg*H|z|O`xKp z;&E?p?=&)E^z!o3czSxm!^1?49>FGI6L^9*!<12-EJ>0|p!1qE=T|?;zuuG<$ctqiVNUEFcm)V`F2pTq(FDz%$qBbVavnTo_~$ z?d|PpadC0)iZ&q!yhwbJG<4qNukf!Y{RipCcqJn1{eifSMoyH|Hc`_ zL#z}9q$}rZQSe#@mTfA*flu~fa##;?UVRxIHsxxPKN#7TAmJq{9A4?zDbC#0lVvFR zc3?m4G~0uPd!(5Q9ZB~0qT<)fC^>oz2j5(YHx9goU9+YkH){hHXTFU?W#{nRBU5D+ z3T7HQmh6n>awAvBL)X>U*V`K!8sw7xtdYIUZCRdPXg^)HU+NE=Jk$Q$ncwZnQDJuN zge3d9x=MQl>CRb^F5@va+hAM5xV#0lG@L|(B@~GX6H(vProbGuAzfk&AQMoaj`1Vqv3Uq0oz6f1!T7uVsn(8evke-p zA6-)Y!=lLq_)6tlZA=e2q2=FtEL^-AoZ37%Uk}sIr>Pc&dxBJGti6JeAlcs}Xuc!Z zmaroeprxB{EsIsqsX;ADbMf<<(&T%J_`a+-m1=`2>qM>}>)b>Hxr*!S^=h*CFU(bH?u8W@S3 z>QnG6BwHsL$lM0$k@fE)}f~TT72%P5ca@E4e6-vHpDu zYu0ZoZ7Nk^y~$zhVU_M=&{NO)>#S?e6X_raN3V;8aluU`uJK zOoGoJKbo)^`2M?Ns4V{yEhlzk zh33$Yz77<>o{PxjRY-k!veawqx`ve5^YF*D8@O+#5eE~ zpWjU-rL>jSwzjtQu2L~!!|e))0h~tBD&z$Q1}2PlHF(w?9UaGc&u=L$7bI8?W=m;A z!bovJLq8|ri-jK!;ruT^$IQa#oR9Nu`!SI$IJ|E6ubBh%9a_z~82iz-2=if{e(IbB KDMyl5ZT=5$Q$=S0 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_default_48.png b/web/ia7/graphics/fp_speaker_default_48.png new file mode 100644 index 0000000000000000000000000000000000000000..5e58399af26ea9ac1663d8cf0d46a03f4d0dec31 GIT binary patch literal 9365 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 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_info_128.png b/web/ia7/graphics/fp_speaker_info_128.png new file mode 100644 index 0000000000000000000000000000000000000000..c73e804f049b4b88822c0e45adb402cbb9fdbd9e GIT binary patch literal 65859 zcmeHQ37AyXl|HYwUW(p;ZgyyvUH}1=u!u{PkcEg#HbTINW@aQLnm9&DLW~jeZ{RqN($J zx873so_p^(=bn4+xyyUv)|)?AkUut`hzgc|Xvs%$-M}H&f#1Jgcjs|sw&D6@>3n|>&W7(KiIJ7ByCXqh&NwOdRsgD>-8n+Bsw-5*ato>IHK3S=;i#HL0k16L zN($}~NJUUo1g<)V5a6B4A@IK{?)lk|*SPu-AYB^h0-iS%)&Gj>Q(io@dO?fk|BQbO zr$ELRmRq{NxAVi`(d{VgG6YuN0u&1vtsqz};0fQSrfRC+N902L_!@+-*J z7E8{sUtcVmAvIJAGR3URw3@d{Z643PXn<>g6H}W3V5TcpD_S^0iWnZsOL`TW!AGhO zVW2RyK)G!4+U?&xy!^^@(zF@!GNT1x+VeZc+ZD_GP{DVBUI7)4)~(nOEVlG8JsRU6 z3;{v#2D=Lpu?vRhW*_;VJbc^2rf5kA<$c}jx4efauofUnh8Zagm;H3dD6e1n2Fm;y z1flY|)hjj|gaE2O!+|w}>h%iY2o{yQiXYo7>cjh1E9}T$090<>J>R0LPr=f< zk_n-(Vdrq5o6ndc8s-m=C{(u_8llgCto19F&(*J*x63e6lM<$#x@_h9+#QdB<@Yh) z*|4)X>_Pxcs$<5WVFB>F(HpQKDaM!TMysP>eL6=EX%&F#7q*uxKIJ8-{P|1~#cm@D z!hoSV|1p$+-3uPr2M9sfsVLr09$ay_1t=D+9_v~aIB_l& zP~l|q_rRgZ-2+i8994JJty=iXVAp@}O=?wDs-NHWn4&5_7tFU=$(omQNim<9%))NX zgV6Kn291E}!E}K@3~DPF)Xe5?deiQ5`u-dBU(H0Y=3rRID*qA(?hbstp)Nsv#_6)% zRlDXrJakMs44MGU-m=HptMt^s#``eWDj@xXuYgicQG*$9Q_|5Of(t zr#so82*AvmZNq(T>#Hc$Le_WI{7X;e>*5H6z{^4)1_`v?>hLYwzkJboU1<$QO1S`V z%XeFP{veni>h~@oV=qy@uyH6fftZP;?a%~EQmF}2s)M=D2lHPFG2fPdX<7P-GN0qK zamaKEPIO%bo2iEBXatv@G^rB+ShGQc@5X@IN2R#sv(Cwe*fd~G>0}`gEWFX>^lnbo z=m3uP)IfE`jqvW#-HhGpM4xXB)Fw5#4Lz*v0=HlW)bHUb1Lg9O`%c-LU;fc+X4f<& zTwx~HG^&FR<7x&;%*7W3=@c z^3Oh(2VH}hS=?!L47<*($s|+tbYO;$76_MTepU;ZDV0{xGdORuAV_CZ(3_?K0(1L* zXdu{r1+W2zumMEFN8O6|gEGld^9k@$V^7jy{Q083Ss1@*Vh=i_0L z0&@fat9q*4fksl5H6!tLWrg&;x#eUHE?VgT^Pf3(jnYkQ)DWM zDy25*3yUggMv-Q3N?4Y4?cMa{zaOV#uHLY>=HB|}~C$yK9e9YZ8VV zGxy%gF;ock9}E6)rj5S#j}z3B0^2}^LFj1}2B8}(kf+qIUUVu}S)%=ylm_6<96)Th z=0F-Vl=)9wKBN7tHdx}(n9n5T|II4vIV|~pV9#;7=dEMZ0>6L^t|)QRrg>Ay$z6ttG~z}V z)B<^8Po*sLIPVMz0K0r)g7OAVtn`5ufNQ1%JGcBf@|GW>MJmer*K_T(YI{8$!YDun zvx@TRk;^8C5uBO%GyMgqbeG_ZEb|!e3<&^l-3CtV+h=PO)iJY1N#Np^{dgSa_t8RU zK8*9z<)!p*z`J(ZXd21m@F;2D?e)-|yARXrXWRO+3BTd)XeDq(OK;>o)1Nod0!Dg0 z9ZSRW({pb~0D}AW;qhZi@M5;~2R1<-pFL69=gSG9ugnk498X`W3iR=7rjDXluboc+ ziV;AGgGT_7IJ&VX@PR#z^k--U8Qh1ovtfwJ(i(kd$zeV~u2gZJg1FcFldu5ri5)<` zjI*@l#2yCeEkv^M*x-A7Hmm*UhM18&oY=o`^Vs17lICEi^6K(3`q|ahbX{rTfd4`d zUTpkW2O-#bo{u9M2<$@s_u1t%47kk#YtZ`&5x6PHAKj=E763l31=#Hh8-H8}mV`8h zezl(<^L3P&8QDAVK!`rV7GrP2_pg{jUzt8$YE<@Oy5@m@G}7t*W#4^iN5Lue&r?R3 zMmOez^2np^FAxRR0tCFV_8_nN@{Modw|uNn{d*2Wl8w|J}pp|pdjeXi3Oo1 zju&vngpu@}IpyTwQHBP1#&F%L-dfH<@% zFci_zqEe+Pgcjf~z$o)|mVINxDf)I@Q_S)F^`%9$@$$)(!_FTK-hJ&|^ur_XM)1TA zf%#&^*ogSr^m?C_{swtXA|r!@1b~lgLrwB5Y<-#6okb8Q-V4$4XC=Y>co~HK&u#am zlg)I~wuAKYsTN@ZhW*`ld(DVKT30o$?>YSDxf5sUWLM-w(feV)%qen)M>Tve&dv(( zB;Vuh5WBGTAWKL9#94j7Dr^DB0BoB<58QH|NS65q%6$R*`o4MSB;CHdUMvcc5%*)` zM$ob`B@r~?F5pRM5*ct8Vip}O!EONO94?mSC3}~U06_c&Vry^+B14$UccSHAHi_P6 zRPCEkI9BO*);7>PyyaU04^~g0agx6MFU{?=x6Kuig6mRf(&!-QA%r2I=oSF|i}-Lv zE77ibTmuEulF0%ZA-xxUXXUUwv5!O#zFCD%S~P-BgH11-fI7e9%?5hwaSP|d021waf9Q5Nhw%$qr- zcKm*#6XE&D?)5ii9L~%L_z1sbp{gJ+z08kN0dBpUE}uqAN0rd5!hBjiv5a21YC3%s z4ASseU^wdPp{I^TTKN3un#qyV&py}!zd3V0BKPT_$4H!)WC{dlX#G}I@GBlp-2#B| zWfdb6Q@||nNd#_PX$ML9XG1+}`)_vsd>#Edt94nOx12mn`(YPo;i};UG=^;fO+J5W zk-LlMu&Y=T*T}OaBuA=G@Jm}R?XzwHz?7CiD29MqEJNhEJH{#=)&?uakBq1(x9ZJ} zky?OxdCQoAE@e)dd2jp0?HG)d1T;3o{BJ0GfE(bb6lIA;bV&y8Pq11pqk3_)8R0WHB^Bk?(7BaD`7ZJ2V=F+;EG&8;=qifDeDMXhy*CQ#>)u{fN%ku zAgQ5ZrtKQ14u`wCY2ATiXxjsUlPw#KzFm{P0rCCNyIK%_0!`f0QJhGHyu(@(H__+* zA-b9x0T}8UAe@1_TRZ4Y1TD;#7;M~SoRTnB=H+wsA1XZnb&(k!!F~p%MgW9fOHxV) z)H)px36%dpXQa9=MX+~M&< z_5iZdB@A@^hSwiXq)!M8IXU-B2zn)BMee|5AX^B)8ws`GkyxA@M;2F`HtutOcDhy{ zB;x4fBQEXyktQK_aZ3_CLVp>f$4T`+ddn~thaGF79UlvjrRVi!THsY8+PF^sSb_xD z2d#}Oi5B}+zg;4qzS5*lL;$#V9L{GTCw_Jl-ad52$nXk23_38FvB#`nvtZI>oQ41yb2)f`#6F;(olKJJ4_#| zDyJgJp~r8)6QqSQY#)+YfX$MfBqb7@j88TTrOhYqbH4=W5`aWCfcPjvzvUUXIPwUZ z79o$6; zoP=P>j7V*~xX*nWS~XVy;HYjDQvxO!fpIG_7!f2bZ3J5W4`xoIyxf#xZ%3r2(8kg89MS-|w(5y57jsEU@%3deU&YZC zN?szSg9KRys1crd(ZXe5Of=>n@9v@d4jv^B){bjs=M$qNrwtkRFVFHvCkN)k3jwAN zi}V+>0BGZC%ctjA^#t-mH+@7;eEponr?(lQ-#f5EdNR?*aY@G3+BoTWW`A&>1`R8~ zq0TP4=fGhYX^~rlXMmY2#+HS}4&0vUb<+-sTk+bG$i3{GLTG>pEQ!Ae9yG|pB={8% zgKhx`A@s91fSW)PVJ{4$X8z1pIzdwY(_q?v9BU$;^k?8<*<(je(CvHci2u{p*?|)r zHdK@c4|Ez>nU~%@2VnvpO+N1%898gmE=8VP(8e|LEcnAkQLMTIKzA2F7GFK7HhdP# z3=QBDE$Y6><7pZ!Vu`bCAT@(e`Ea1{-tbhy3-6qz-!)&LF*y!80;cpt**C_exO?I_ znvy?ol_;0(9SF;d@0^P$=A^t_T38%8viNQD`97hGNR;3UHQ25!KBZnyQr!Y@uwl{3 z>S?>WRUhSHPh4Qrpq?C}#A)d6rnLteqWmj=T?y;?=@}ImZ|g3$91*{bAxG!E9^xQC z5|7p9z2)O+<(Np-&uRYi1Qr+wA_)wAKP$$R_T7iyyaHPxR(ZnX#<~Y@@D)qZK)d%J zo!b;EZsWa*?4ny`E=ZPtbxB?uwex?2>u{TePe^!B$ZY+&l zG5Rh9<`wuAwDFBT`-1|2J$4|8R- zcwq7bnv9Wv7WN_j?^|Fu=qQJtIKFOwD2u-(^3j2W1b}l;RKNHruBRh?HbJPSd`(IS z!VyXCd2WD*hS6Y0TRW}Xdr+zcI1Ou%o1)kb8;>`cW(ROyp3VS2;Jf(+KkJ2r1wi#F zF9Ny8--RxLmpz#z!O7sM=iw#PaY7>n9p!F?Q|MH0ees9lCpdNKX|fnKWFW-s_n1c&3iFeCth(Ex2?+@2O*F_2L*fJv4v`dl0# zFtf~a3kqoKWi#mGWpcN#8_a*8;Rw~XcbHBqroRA%HYZ~J2NEUf6%3`q)oZtZ7ng3Y z3t9l0!1S6W$syJRpR1Q^f^(k8Sw17>=TX8#lgsJp>PjlljU4JRx!jmo`clJTdZRtK zq%6sLg_qNhYB2o)T|s{EeL`SJ18@#}hB;OWZ|3_xDddL?0=5ZQ2*k?UV7a)rd;tbr zGEjnvse8tcr=QQBMaxFYUBrx^XBF=S^LMsKE+|bV|4>?YOR(=JWwVhpdz`yCxZ3{kYGz-IBKqy0-MSU3eZC>~_ycyyh43#Yv z*NXhewSA#{n&33Lr=fwq*VL3!<};m{`Cxym$M1b8l)sT{(gLt=<-#*KP-H!4-g6dx z5(}k?hEAs8+SUeRP6KCQR6cg%I4wW0pLTYjzfWcTL0>^J+w;;Rz|OO#C(l>$DjA$2wK8Zev_XwpZ9 zlkNgSrLA8z4|CA!M(zUGKnbo>3;mtFCP0B7jXeJxQS`L{Ok1Y0=x#xCGrl&>#WPJ9 z7@5^kz;?k-KUNg?oMyuy3uaM(>B)2j>~OklUo{J_Y287nK-Dj7FIRn*w}H4gceFx| z5vFGm%IcE2y5aimc~>z1f}(h@IJoNS<7QRfR1Ls|I`pZl8nJ@p4!-X>6BxSieHM&S zfQNoeM+Q~(-(e>68K>z2z)0&>EyM~U<=fm}b)EE+SjEbUAR7{*0n?4?C;ELHV^H_$ zs}frI!@P(GsngG@s#I^?^%E3eHJ@?joMItsnI!6Ac3O%CJqX)AvW*E>tj=;w;Z<0zsH4KceLy1M>qr*0!S&Eva4e9&@%7N%Bm& z0C1i&Yqkydxvjs#?N!_zOeS6`H^{Y|ye#hDL*u)`Npyx{jl-6l>7vRkZx&sciY7N~O*Q2;~sG za%E7dkKplHuB7XjN<4><1)|<_u=dgC2bixu!*m)9<}>XFzX^)b!R|*fwh`Ved!a)4 z6HyQx!f>epyLY=zV09IicniQ#@6gKm52niClhHSARIl2);})E(^b876%wI#uhjD0& zKJ6|m`$hfEhjo1}FrQah11_+WCl%xTctlE@0C2(BZA?B3?>bt*x+teqp}bMJ8MKZm z9(hvmKQw(eJbSG60~0&66Ykwln(g7!Tk^CD02h)!i0gBAJcdr-K0GSC#xQT3h3xV( zmh{#uBjb6nuB3}+O#?$bmVk{sfo5cBw^p$jTtli%-q zvVQfVQ=ylcbj@f12oY*p&0D25kLO+xlLbL+(cyyIU^6&MpcQaUhcNI_Enf8T?h9Z( zym~!3r}16Q80;}cW7*l^?p!+irc13?_rU-H&%-o2YnD}P|AV?SL~0uWx6 z>SuS3P^{_->{-4U@2^1^;8J3ZV8L1h77R-+3`$&iQVMB=2a>A^ zIO=XOEbql|FwMvRshE50hOq3yNaZy?u8mLWx>#CzIK9H}hlBuxS4iA%P%Sg3)Q%V5 zUB{=l(dMhLeQzob^T27&{25O1B}QDuH`sWT!B#r=;t2dT4z&aETM_>VT6Y~(MIE2d zD~@R0pg>P#AxnWQ1+o;#QXor#ECsR@$WkCnfh+~G6v$E_OMxr}vJ}WtAWH!q6v$ay se*n>buyn}{AJ-9m(4xRAsH&fM@3z90=Ud=4CtCXco0q)2_>M>ZAMsE4Qvd(} literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_info_32.png b/web/ia7/graphics/fp_speaker_info_32.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a076af1884ecc91248cf7399f3cf97952363bf GIT binary patch literal 4217 zcmd^CTWpj?6#nMF?WJA1Y-#B(Efl3KrE+_)T&zZI4Kxa(VuUDQ!VCL?A{7B2j6O+P z48(*It0rKIni%^4=!0oUfPglJU}CUas80H%$UHU zlb1KhqU5KdSWO5~Km?F6Yf70*YJyU#62oZK485^)`4{5~qAVYVEL91D zkO?MC(}bxT7pSV%b?jK%HkHwo1j@d->=$jeqeK?vsX)?=QO(edk&$--+txIu(@9HU zadTU}V3+CyNla&(Q9p|W%$VNL{$}O-lNn4(pscygFFB-RWQNWyhm?`fBbq<()~e%H zpH)vFz}H;Pwp|o#4}lGv z(wB#?R(Qd;g`Lsox>*hfPCr$Ga!JL`O92GJ8rGE*VUN#|g2S-|JMml2d7U7@PdK6o z)$tzQJ4so=X5O^S7kAE$DUT4-BZR_fQ}E)#d8n_*$46IhV8|fYRz4r|*)k<#5rtH9 zU)-Kcf&f2W1%(as=#Q19o*e8i@y5%+(jqi19d|xz&Lbe=rF;i={T{$iePI-NJlN^8 zka7tmxH=FH%6U!EW zPwf2gFqXMptYzS~N>RwRNkr9BG5OUQ_ce-~;KT28%1*FEsMyGW&WxuLMo?k1hN||vRlLP`fhX>FJHZn&0a5>$_pS-9f!JZV^?tr8>eXLyO$D4L!~PBWr0}e;4c|7 zMq863z}v&@p)p!i>}kJ_iy?NpS;E(r>Eva*vA?(gcBpc5WX0cvL1&=c&#*{Gfp z0V6_NlV42<0vvNBWHcHvlNqEr3EA=bUZ5jEg5_6S_g*ZB4?F+HmOtB(=b4Lrg>w*z zg|V;eRtgr!HX}%R8%#*Eik7zX&O+hoDHD*$I52-6x(EAkYH0Kc=#ZRfsVIX-5-=1C zVfXb;{G}RJtm63{zGYsx_Dbb52u6@ln3LxKLuE zD-y!F@DRT4y^DUniIS$L4pL2L*rVFxzey_Non_6J{aFjdV^Y?n!{dq>w)8MX_$KuS zwr21ku`ri>|7*RYP^go#XGTH~QAGbW8?L>+I`b!)MS?%g96lVi`5~8q==Xu>U3x(qG-mcJPxiI=WZMQY2YyDPTFRn| zXlG~q*fGY=9vCqSJw#M3gjQn+jqTej|DO+&WD>lc{ArfB)n@Q%_8|VF_kUBR{Z#OS ShBx@kYhTz{^W*bxfASysg&S}H literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_info_48.png b/web/ia7/graphics/fp_speaker_info_48.png new file mode 100644 index 0000000000000000000000000000000000000000..90e5df630ae80f132c429f503ebd91083d0d71da GIT binary patch literal 9365 zcmeHNX>45875?s9X6DU!?6Jpd94}cKI}jkyf}kuVG=Zd)y``)rfLf@kHkHy=G=dfZ zr2>is68%v?Kp-R~t->NyRYfILhzd#yRJ1fsLK9ISS1K-38#e2mMo ztPK6e1jL{Pv)?q0J|T&n*=*{A{@YhXKPPs3+8hM1MDONZH^7qC3L*87#@k8rq;Voy zpjsqu6%pDu{A002B9>|Hg=KE+yXBlMG^sSKIJ}x`kw-OS?J=D%zf-S4y@<5APAJO7 z)cO6a=@b}RmTr(Z#@{UyPo$;HI|DagI9~Wx)n!(UyPx0H>h<73NtUh^iqrr}BIL6o zQ4thbf`u3}4eQlx&bqh%_H%|R4y^F9YVHeO+S%igJinH`%2k4*liZ~OxjDSDEb=Cb z_Dt@Lyd`fqcs$SZ#FKb?D2g4?ED{w~0Nco_q{xPKjh0QlNeEuIag_Z`<3biKN!B_^mBO@|PI?Ye zv^}VbYuN9@syZ)1vU`q)){JH0I-D8~VpVqw?m43izvv7ehps>Y!#Z00DyooJOl`fJ zzj34FQ`Sp9iSRRG8dF8LXkA#;)QCTw*Nw|N>Jf}3u_Ki)&COH3&*~=Xy(*f0DuNyb zOPcDjOk}ZpB8wDTqMQ#a0irv6#o+$YZO{I@9PJbf@ZN^9*;+-DZOzy#Jg@RIb@J^q zTO0A%sm<`)Ruj-<$2<~lRZA@ztP~#j&!>3i;3x*t2JsL$w`(4LJU@sAcGfaJtUpf%4DiN`Qf3b%lVOyPpZ0lCV;H~eX~F^@t#O4>3f(XOZmpquY&fNb z$Ql@k$MN{bN3bWE!-gd-=%6{5E@;7_L95Ow_zaxaS}8ce31sNP_iVWUi0Df<|h31E1d{VjN_rbBS@Hl z&#U2kOWV-GDlN`&TxBoX85_R6)#dh1e&GGQq{_WQ7Rq$N8G@PVl#~G#u7b|%T zrqdJ)78G(%KUu=b+rDQl2`i*9Mn~h;L?Z5)dNGYlliou=?-OqLbX8Ka}Q1)#bO?U|_bK^mYh&Yq`JcZO`tny9*whS{e>s*Ehf z!a!^sj|~nZrZwWbO`7d4@#trGOZH%CFlcWh>K3D zVfL%0e4b6+F*2G>V#|>s?5DI?YP2Br)Yaj%8ctr7+juU6O@o7Yk5=i=Qb}agAnsq# zOzuQ>*0XqJD28@_4bG$Wg+1!iL=+$9*!pgI1Rkbt^($G!RY!6Oz!kGVrnAp7=r9M4 z_G45i%&>iQ6z`55d&gF7982O?i`uc=Pb;XKr~Sp*`$#&9M+PGJh%71tG~UTKALGIoHum3srptYpNx9oja;8*t%GOgDgivT zxD%~H#~TBKcrnR!-O0IB{KXZ`?f4M^4|08p#wQ8!>SbNhd< zJLl^(p{SeE5LXbF-jWRsWA_vJgmnh(JQ`?weeBarcX|@{eDXhZ(rN4<=W}j_>0Vhy zPoM_1lv=4mjsA%kULP64W(MyVH~B#Fusqx=G{^xaFe?&FW=hkO1U`ky(XtFl2x(J z6A~AD!nEUB}U z))F;@cA+l%%a&ItH=ob{3jl=$6TD5em({{-4kGI8GAw|Vhn|ljl8cLdlw~*Wz2)r9 zG=9p0<>p=aY0kdDr9ZEnv;X9(`bDqgL${=e`BY&Dkqm`Dxq>%LF;8B_$J=+tf6uemth#dLwoBGN@PAZ8QiuQm literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_info_64.png b/web/ia7/graphics/fp_speaker_info_64.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3c8e739921f5f113cbd78c27af8be5009a3720 GIT binary patch literal 16561 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 q|2P8f7WWy) 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

8%H?m8c})Pm-B?Uy4#2bar$ug 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 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_success_48.png b/web/ia7/graphics/fp_speaker_success_48.png new file mode 100644 index 0000000000000000000000000000000000000000..32a249f8af04b94ea07f38c9ee39fd4cb6395c40 GIT binary patch literal 9365 zcmeHNc~F(t75{z9TX{U-u?oBZi3pgOps7RyD6tyFq)tM$#3oIhPSd7I9W|!yw9QOs z(q=MEC(%wOHcd0rq@6U=Y1P;)YR82tjB$xbNh(NOfPkRv%Ui#t=YEeT?|bh74>6to z;f2fnmUGYfopaA!&iVNDLu*#XTQjWy;wx7zucEe?RkM-)R`cE$sj+NcwSFrQm&_`M zH{Q$ua#6YbzO_#pxZ>IMEktv(+_b<PLpE_DCo&pU zQY8U`SCXY6NsR*u&Gw6;GcAY(++k%)yaEzGc*tiR+rRB=4#pvpkZxTp## zAuD~dg5A82f3D`oHC?eX6=EeU-CkN?6pSy6BL9dW@Npc^gCiHYAwR^2M$|(5?v$a- z&aTXEXy4b~8w<9W>{q(2w9>$fFN+4gkmHH18g%YhL`;WBp3c>^czCd(I*m2Qa3ObUu7gF;$JlG5Z?q=)J@*ELT@KdB2bpEvTxSBxg1T;OSd z$M7?)zqCq>NfcG2C=!>MQJL|tj(r`Y6DcvF0&KN0Sxhe)4ScmI&~()b0cT3gpjwRa zNQ+AauXy28rF;st8vWnne)d~1v_$b9>;5IHe-2V1x!~OaC)fcT3=am!zcM+x27N^&H z+Tf*4Ep!x^LJWr_tM;TtN$+>nbm`_o#2jD;GlPLIAk7({n9D8BYW(n?ufZ7?H`bAd z$gD{mn2RT+uf|iQ>%hq)=R#b<84~S5xFX^E5=M)9J zXhLHAEEMIg!p|Rg4z}Kp@ao6AX*2+{#feADo<<6pG@3l4$Yp6KUcwlvfbOQ`u@Z;b zWPF^K&b!F{^+T!k5)mg_L3@K%O?({Y=B~xYpxcCUrsMvFiwAJBKj1U5&MWe;9I z(uD6U*@)8gOyoH4LzS}zyPE!td$N9lJd1$2vsWUe?RE6%Sp`N#KrX{budbbwbS}gq zDVi`&wtNM32)2nycVLn89u&~>SePED1?dGyod7(O&-yy-PE;1J$I|>2*tq<8tjRKA zN6nkK?DK;Ytf-j(EqDgb;Y8bcC`3OiX&&Y|k|N|YOym{Wf7cAlK1~s(3P@Ts|2F(E_WPMqo_twKf33Y)}n4x7R7a*adO2HoUU; z(=CjL+uMs{EuX;0I`bAJ69!r8qm4^@0F%KCna)LYk`7pl&g|i`*?qL4Q-Ea;D9ZD2 zT-atpJttxijcM?iWRLo&_KKJc;S)cXu*?Z8&H%OSi8sH1+Uy_?LAJ_fvS>? zD8J=S3TWa9o(GNRKSWJu52{zLLz;;l3jFwY^FEA(iXCG^VW#A#FjLFw1zA(8Hyow= zSjNofS39)x$QS`53RYS1VZ3_K)3X=9+P4i&bTHEvl@R6(N3dV2^P5;#bT4Mv2)yFL zCr$6-orVvwpm+<)Q|zRWjPADg@sFPVYkh)!S?eWCluM`$Pf4yWj~_&AwCPKiJ=hoejIA z%3?vj+b^GE*mPj&Mt7KFFT3`4d9v?!Dzt4^nMCbW6y7K5z1!VJ=lNlpjD<)sTQrNd zw;sSJ6c4Z>c{|Y5bs8sIYtTSmTwS^e%W@YWk>UZbs~h|4-#|^*g{T?d5swUyB>m#} zmXjyLg-0mB2q^OT)VU&OsY&4SD5TS-6LW{js|>gMbJVtef&7fyAbYz|f8hu&xB_#I zg9MkKzQ5#u56>>yfZGz%Xzl0W8oi9&$6mnu?VoAj!)1`)W7sPnuGxI_N8#Yvy>1eR z5U74t-R_c=Q(yOC=2plhUC^FIcP7188CPAbL9v=b|`7hiOr zK+V~m_-AK}jxIn=`5{`<-F~&#Cn>+BXHggMi4*`!w7NL#HVOQ<s^3@6JHXRXY>=$COfrCCyF-qBJQ>%@J_A!+FLU2&pzPsshN}=$fuy6=JkzLg2f6h^7*U7{`V+9^h7M+8P1p#V1#TW zQkSJ2QB=V}A5~b~Y%$QEDK|MX{Zur}--oWi|9)=ugLedi Pw8|B0mhW1&;ragpi?%(l literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_success_64.png b/web/ia7/graphics/fp_speaker_success_64.png new file mode 100644 index 0000000000000000000000000000000000000000..84bcb8dbd03b3038101e43f591c3a6a3724a00f3 GIT binary patch literal 16561 zcmeHP3wTt;75?wOlaNP4Py&)fji4r_+JGV^AwrZu5Ri&!mCpxSj7lXc=;y0cw1rd! zw8SU279T)WC`tq$5EJ>(k_gHp+PwGRnGUfz~1)TR&mB+w^;J_-E)NHD0D6L- z3{u{am3~P$juNpe9tJ}d!@d1lNN z(Bd7fR!C&#PbaVttX6xIU>C0a?EcR}c4~{ML?a}S`R?n{P>Ir6D zPa1PuiFvK2$e`Ccro5*JBHW%Qz-Z9I(uJa!ju_rc@9xPE#6|GHsz%m*0T0Ln^W^^| z5)q-(YCr~ZVil8<{$Bbg z=zfl(yk}7|{z=R_cLdr_tit@&1$b^pjRy(oBObv+X@flKlnP8c^FT-R9HsYMr^+1% zGYlj_m1P=y{QxEA9pqSO`sMgjhW-W<&l`>lNd2ddU4*sM7h{xOz$evnFmG$Mj0YVq zzp?~{(PW{#aCn#X(m6%oz>Fd&3&3GGCObNNs+F?JsLl^D??E69A8Z-K{NDmF z&)mrI)$jic%IXixn53+B2xebbic6^FC)?)ZpBy>t24nI>+?GIEKxMVWJdiOPkQC8tLXYj?dU#9~0yAkUp`!5I6|QcwjG`w<;kQtB8TIBKurLt zv~GMF_f|z?D}NSnLsI(fc#N3B$rteEw;UOFfNnrGrqIN<)o;ZSXC*!LihAsBz)lhU zrMYX|IW+;qNu5Xr-rI_^Oc=ul8lCY~Ro#Zy_t$#hyZGXnh@p2Izxe`gSIC3`{ivDn zCH6MEYlkFXq)MlQUTJwaKf-v7qb7h@sUwf2l02}4QE=ja|MNSrXqRjUh`j*$QDngz zKgB`U?r6d}XJnG|atcSA+$STQA=a1914Hk~1{`+*WIS{SGkPS9FxU&dYt`;@+4?u3 zpJZ&?K!-eE2*VJ|k*R4zt3Vp$oGG=Z98oBFZ@eQi^AO`gWX36CT%a27Q#tp8mr%TM z$;%uyBJeaiTtlE{N?OQ*Cic2fBr18?iRgz(moLH1Lx(u1;@{R9Y&O#~X&lmBCuK9f zJ-(Yd^+Q@B8wLkhyILhY&a%%_WN|19KurVc=ve84r?)DyMm|T(r+J{?xpO5tsAl}| zQLN`me1t_6Yob;xw5}0JeYihD6iV z85g-pu^qrB7M8olCugN^i^S7jpBGf2C;&PZm(zzAl7v#1?mAl;?q6AkBvZ0yBP-dO z#GCLG5$_QQ@oeQT@fe-!uyEr#S6y_%3_LPn7S8Q*(>2&B%3P7Xzm@nS%U&QRSU3lr583BbL z$1dV*+?e+$u1=E~L&n}!gJ(88Pnl#qFm4izt`W_s*s;d{Jkbt7+I+mCr9v@8rfdTo zS5(%UEp`bx6WPcd4$>8yc_=42D1f33klK;Th|h|TwU}*hpTQxfaDbM>U1Z}* zr+V51_HS5K-LM<0wmsk~M)#kA2gcrvVbPvs{`;$^u>9~bufDEsu1vF8+RGnRQ-5yg zmovVcWU-3X9keAl*Hoxawh8xmEC(}wpfHazzD&%XxCXCGn&rd%p|2NV!7kn=)%rT> zJ81UF`d3w)toXZ2A_^JC;}`fdnNA~oIw+oOZ@|`mQ8`|kMq_;HS!ih3fUle7p$~W8 zqEL#7Vtx<=i+i=@Otvitds3s z+l_QF$`r0W@WQQBY^r@2&l_hWlg>K_YX5<)jSW(>U}noqP7l%ANY30nO1Qo2g25^E z;mZ<&PW)(4oKZ(h!W+*|qRj3)slFBV;4e!At&-fzfnzNekl%lI#eEg;1Un-^3xE;UZK|spd{xSs7P^JNkBFrI7zGK!jMb@3cOicaBB6lQ!YX$@%Y%z2%Aj0=0h^TL1t6 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_warning_128.png b/web/ia7/graphics/fp_speaker_warning_128.png new file mode 100644 index 0000000000000000000000000000000000000000..9347b8e3a546de7a2ea36e2e3549734c012adc63 GIT binary patch literal 65859 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 CM!c#3 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_warning_32.png b/web/ia7/graphics/fp_speaker_warning_32.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e543692db4b4358a34d1667babbbbc37956edc GIT binary patch literal 4217 zcmd^CYiv|S6#nMU?%nOS+b57(+k(*)l2RW(EMO`|3V}*3kC^Z+B*+giq#?1GNCTRf7^M=BLJ0z;WxIXs-aGet=5FuZcMA#sm}GY5 zo;mZKZ_b=KGrMbL!&1Mm#0S7%w`}ohYG=vnRq1!z_U$2RvR+%ZZX1wYBrC)I&SD_H zvTpH{&#cX2v+~amlFnUT*zKe93xcE^~wPKp>obejYlr$Qt zB4HXZAi9Odn#x)RdQvrFl1T~lwHF0cD0>ue_oRw3Jf~+;k4R_{7_XLZ8|ZKbk~$f2 z``Zg!x#4c&$f%M8mHtz}gn-s|Z}W}jbQ)>Vt(6PcK5pKc%hLhJ%LMsCcZ)#j*6W|f z%Vj@>z{Oof)o#X5aN%+%D^K;!%j;nL_Y2H$Fhx~xUDI5mrfh3sOr$K4GuFm6w;Kj= zo20V5YB26`blCKDC|KW#!Yw_>s;YP7HipFHg0&?xVntdn{$sdIi*7cln0j6 zs&~K)UBb|T7vV1b9-euP7~RtXg9xY<3o-UEO8@7{f%!#-o-lO)B#Gj+QtAX0M@;MjTr+=;+z{NiF3Im0~0^gz>Ij z(8KfLttf%`_c)A_TG|E$qW^3Hrokl})5!x)5X#4dp=L+;IFnC0tBil!fbm1qq942n zIgMYyQ%0{kyHF6CeTQ-LgX2(ZTadePJ(w|qk%OPYxB38S83^}$7bP$;$8Z@DY0HLi zJRn~(Le;wBJz(`m2LM@zdlB0EEW&+P?76fMX@i2QydM7LWLyLh+W$P*+(vk3PKW;2 zK}62?CwRd0XkGEICWnATB?28n)g|%tb?gE{pCplxy627}_*DxAcW;35Z~*=#Pth)) z#qihfCg4bHjciC;c9CZyp7upipIx%1ig({jvj&{No^klw{$NDh#A+#eI0 ifZg8dzh*Y>-P=C8YE5C~(i2w3s$0^q_=iOs-uoZ+fN5?3 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_warning_48.png b/web/ia7/graphics/fp_speaker_warning_48.png new file mode 100644 index 0000000000000000000000000000000000000000..71d39a51150aff9d502a9abd67b7c00c456fa02b GIT binary patch literal 9365 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 literal 0 HcmV?d00001 diff --git a/web/ia7/graphics/fp_speaker_warning_64.png b/web/ia7/graphics/fp_speaker_warning_64.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca0a9bb6b78cb346f7eb82e5975a109b9f83e2f GIT binary patch literal 16561 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 Date: Wed, 5 Sep 2018 07:25:45 +1000 Subject: [PATCH 56/78] Multioltage Device Registration fix This fixes ZWave multivoltage device registration, so that specifying only the major device ID in the MHT file registers the various subdevices. --- lib/raZberry.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 8209c97e8..4d2f7251e 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1996,7 +1996,7 @@ sub new { #ZWayVDev_zway_x-0-50-5 - Current Sensor A #push( @{ $$self{states} }, 'on', 'off'); I'm not sure we should set the states here, since it's not a controlable item? - unless ( $devid =~ m/^\d+$/ ) { + if ( $devid =~ m/^\d+$/ ) { $$self{master_object} = $object; $$self{type} = "Multilevel Voltage"; $$self{devid} = $devid; From 8ea9c6fdc92f8a4c8bd17f9bfe94e177d33af046 Mon Sep 17 00:00:00 2001 From: Jon Whitear Date: Sat, 8 Sep 2018 09:25:26 +1000 Subject: [PATCH 57/78] CBus->CGate Socket checks Test the CGate Talker and Monitor sockets for activity (i.e. health) on each loop. If one is inactive, try to reconnect it (but not on every loop, to minimise log clutter.) Reduce the frequency of CBus network sync queries when the network is not in sync, from every loop, to every 100 loops, to allow low power hardware to catch up. This makes the CBus module more robust, accomodates connections to CGate going away, and better accomodates startup where the CBUs network has not yet attained sync. --- lib/Clipsal_CBus/CGate.pm | 75 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/lib/Clipsal_CBus/CGate.pm b/lib/Clipsal_CBus/CGate.pm index c803023f2..35a7bf072 100644 --- a/lib/Clipsal_CBus/CGate.pm +++ b/lib/Clipsal_CBus/CGate.pm @@ -55,12 +55,22 @@ sub new { $$self{cbus_app_list} => {}; $$self{CBus_Sync} = new Generic_Item(); $$self{sync_in_progress} = 0; + $$self{network_sync} = 0; + $$self{network_sync_counter} = 0; + $$self{network_sync_limit} = 100; $$self{DELAY_CHECK_SYNC} = 10; $$self{cbus_group_idx} = undef; $$self{cbus_unit_idx} = undef; $$self{request_cgate_scan} = 0; + $$self{monitor_check_loop_counter} = 0; + $$self{monitor_check_loop_limit} = 100; + $$self{talker_check_loop_counter} = 0; + $$self{talker_check_loop_limit} = 100; + bless $self, $class; + + $self->debug( "Clipsal CBus controller v4.0.1 Initializing...", $notice ); $self->monitor_start(); $self->talker_start(); @@ -373,6 +383,20 @@ sub monitor_status { sub monitor_check { my ($self) = @_; + + #Check the monitor socket status once every "limit" loops (rather than every loop, to + #reduce log clutter) and restart it if inactive. + + if ( !$Clipsal_CBus::Monitor->active() ) { + if ($$self{monitor_check_loop_counter} == $$self{monitor_check_loop_limit}) { + $self->monitor_start(); + $$self{monitor_check_loop_counter} = 0; + } + else { + $$self{monitor_check_loop_counter}++; + } + } + # Monitor Voice Command / Menu processing if ( my $data = $::CBus_Monitor_v->said() ) { @@ -386,13 +410,13 @@ sub monitor_check { #Process monitor socket input if ( my $monitor_msg = $Clipsal_CBus::Monitor->said() ) { - $self->debug( "Monitor message: $monitor_msg", $debug ); + $self->debug( "Monitor message: $monitor_msg", $trace ); my @cg = split / /, $monitor_msg; my $cg_code = $cg[1]; unless ( $cg_code == 730 ) { # only code 730 are of interest - $self->debug( "Monitor ignoring uninteresting message type $cg_code", $debug ); + $self->debug( "Monitor ignoring uninteresting message type $cg_code", $trace ); return; } @@ -554,9 +578,15 @@ sub talker_start { $$self{talker_retry} = 0; if ( $Clipsal_CBus::Talker->start() ) { $self->debug( "Talker started", $notice ); + + #reinitialise CBus global variables + %Clipsal_CBus::Groups = (); + %Clipsal_CBus::Units = (); + $Clipsal_CBus::Command_Counter = 0; + $Clipsal_CBus::Command_Counter_Max = 100; } else { - $self->debug( "Talker failed to start", $notice ); + $self->debug( "Talker failed to start", $warn ); } } } @@ -595,6 +625,19 @@ sub talker_status { sub talker_check { my ($self) = @_; + + #Check the talker socket status once every "limit" loops (rather than every loop, to + #reduce log clutter) and restart it if inactive. + + if ( !$Clipsal_CBus::Talker->active() ) { + if ($$self{talker_check_loop_counter} == $$self{talker_check_loop_limit}) { + $self->talker_start(); + $$self{talker_check_loop_counter} = 0; + } + else { + $$self{talker_check_loop_counter}++; + } + } # Talker Voice Command / Menu processing if ( my $data = $::CBus_Talker_v->said() ) { @@ -610,6 +653,22 @@ sub talker_check { $self->debug( "Talker: command $data is not implemented", $warn ); } } + + #If the CBus network is still in SYNC or NEW modes, query CGate for a status update + #every 100 loops. + + if ( !$$self{network_sync} ) { + $$self{network_sync_counter}++; + $self->debug( "CBus network NOT in sync. Incremented sync counter", $trace ); + + if ( $$self{network_sync_counter} == $$self{network_sync_limit} ) { + $Clipsal_CBus::Talker->set( "get " . $self->{cbus_net_list}[0] . " state" ); + $Clipsal_CBus::Talker_last_sent = "get " . $self->{cbus_net_list}[0] . " state"; + $$self{network_sync_counter} = 0; + $self->debug( "Talker: Get state command sent, sync counter reset", $debug ); + } + } + # Process data returned from CBus server after a command is sent # @@ -637,6 +696,7 @@ sub talker_check { # Newly started comms, therefore find the networks available # then we will wait until CGate has sync'ed with the network + $$self{network_sync} = 0; $$self{request_cgate_scan} = 0; $Clipsal_CBus::Talker->set("session_id"); $Clipsal_CBus::Talker_last_sent = "session_id"; @@ -690,10 +750,11 @@ sub talker_check { my $network_state = $1; $self->debug( "Talker CGate Status - $talker_msg", $debug ); if ( $network_state ne "ok" ) { - $Clipsal_CBus::Talker->set( "get " . $self->{cbus_net_list}[0] . " state" ); - $Clipsal_CBus::Talker_last_sent = "get " . $self->{cbus_net_list}[0] . " state"; + #Do nothing - sync queries moved to top of Talker check loop. } else { + #The CBus network is in sync (i.e. OK) + $$self{network_sync} = 1; if ( $$self{request_cgate_scan} ) { # This state request was part of scanning startup @@ -1014,6 +1075,10 @@ sub talker_check { Refactor cbus.pl into Clipsal_CBus.pm, CGate.pm, Group.pm, and Unit.pm, and make CBus support more MisterHouse "native". + V4.0.1 2018-09-05 + Make connectivity to CGate more robust by detecting failed sockets, attempting to restart them, and when + they've come back, reinitialising the CBus objects. + =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 From 2a65c0ea74e7725c73747a27ac6c2e0947dd4c6a Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 9 Sep 2018 15:06:01 -0600 Subject: [PATCH 58/78] removed duplicate message --- code/common/calc_eto.pl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 6eab03e5e..658b885eb 100644 --- a/code/common/calc_eto.pl +++ b/code/common/calc_eto.pl @@ -1251,11 +1251,12 @@ sub main_calc_eto { #$msg = "[calc_eto] RESULTS Calculated Schedule: $rtime"; #print_log $msg; #$msg_string .= $msg . "\n"; - my ($rtime2) = detailSchedule($rtime); + my $rtime2 = ""; + ($rtime2) = detailSchedule($rtime); foreach my $detail (split /\n/,$rtime2) { print_log $detail; } - $msg_string .= $msg . "\n" . $rtime2; + $msg_string .= $rtime2; if ( defined $config_parms{eto_email} ) { print_log "[calc_eto] Emailing results"; net_mail_send( to => $config_parms{eto_email}, subject => "EvapoTranspiration Results for $Time_Now", text => $msg_string ); From 6873d21dac4d2f6b1395ce30478052725484c34b Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 28 Sep 2018 16:55:06 -0600 Subject: [PATCH 59/78] Update Timer State for web display --- lib/Timer.pm | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/Timer.pm b/lib/Timer.pm index 5aa5f94c2..6c301974d 100644 --- a/lib/Timer.pm +++ b/lib/Timer.pm @@ -54,6 +54,7 @@ sub check_for_timer_actions { my $ref; while ( $ref = shift @sets_from_previous_pass ) { &set_from_last_pass($ref); +# &update_state($ref); } for $ref (&expired_timers_with_actions) { &run_action($ref); @@ -110,6 +111,31 @@ sub delete_timer_with_action { } } +sub update_state { + my ($self) = @_; + + return unless ($::New_Second); + if ($self->active() ) { + my $repeat = 0; + $repeat = $self->{repeat} if (defined $self->{repeat}); + my $seconds = $self->seconds_remaining; + my $hours = int($seconds / 3600); + my $minutes = int (($seconds % 3600) / 60); + my $seconds = int($seconds % 60); + my $state = sprintf("%d:%02d:%02d",$hours, $minutes, $seconds); + #&::print_log("****** state=$state"); + $state .= ",$repeat" if ($repeat); + $self->{state} = $state; + $self->{set_time} = $::Time; + } else { + unless ($self->{state} eq "inactive") { + $self->{state} = "inactive" ; + $self->{set_time} = $::Time; + } + } + +} + =item C Used to create the object. @@ -122,6 +148,17 @@ sub new { # Not sure why this gives an error without || Timer bless $self, $class || 'Timer'; + # $id isn't actually used? Going to use that as a flag to enable/disable state setting + $self->{state_enable} = 1; + $self->{state_enable} = $::config_parms{enable_timer_state_updates} if (defined $::config_parms{enable_timer_state_updates}); + $self->{state_enable} = $id if (defined $id); + if ($self->{state_enable}) { + $self->{state} = "inactive"; + } else { + $self->{state} = undef; #restore the previous default behavior + } + $self->{state_updating} = 0; #&::MainLoop_pre_add_hook doesn't seem to be available yet, so add it to the set + return $self; } @@ -178,6 +215,11 @@ sub state_log { return @{ $$self{state_log} } if $$self{state_log}; } +sub get_idle_time { + return undef unless $_[0]->{set_time}; + return $main::Time - $_[0]->{set_time}; +} + =item C $period is the timer period in seconds @@ -195,6 +237,10 @@ sub set { # print "db1 $main::Time_Date running set s=$self s=$state a=$action t=$self->{text} c=@c\n"; return if &main::check_for_tied_filters( $self, $state ); + if (($self->{state_updating} == 0) and ($self->{state_enable})) { + &::MainLoop_pre_add_hook( \&Timer::update_state, 0, $self); + } + # Set states for NEXT pass, so expired, active, etc, # checks are consistent for one pass. push @sets_from_previous_pass, $self; From 334d69f599a6c074281548da1f7a2f9da2704492 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 28 Sep 2018 16:56:57 -0600 Subject: [PATCH 60/78] ia7 v2.0.810 - fixed transition from undefined state --- web/ia7/include/javascript.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 272331a70..cf741ebab 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.800"; +var ia7_ver = "v2.0.810"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -1452,6 +1452,7 @@ var updateList = function(path) { $('button[entity="'+entity+'"]').find('.object-state').text(json.data[entity].state); $('button[entity="'+entity+'"]').removeClass("btn-default"); $('button[entity="'+entity+'"]').removeClass("btn-success"); + $('button[entity="'+entity+'"]').removeClass("btn-purple"); $('button[entity="'+entity+'"]').removeClass("btn-warning"); $('button[entity="'+entity+'"]').removeClass("btn-danger"); $('button[entity="'+entity+'"]').removeClass("btn-info"); @@ -1504,6 +1505,7 @@ var updateItem = function(item,link,time) { var color = getButtonColor(json.data[item].state); $('button[entity="'+item+'"]').find('.object-state').text(json.data[item].state); $('button[entity="'+item+'"]').removeClass("btn-default"); + $('button[entity="'+item+'"]').removeClass("btn-purple"); $('button[entity="'+item+'"]').removeClass("btn-success"); $('button[entity="'+item+'"]').removeClass("btn-warning"); $('button[entity="'+item+'"]').removeClass("btn-danger"); @@ -1570,6 +1572,7 @@ var updateStaticPage = function(link,time) { $('button[entity="'+entity+'"]').find('.object-state').text(json.data[entity].state); $('button[entity="'+entity+'"]').removeClass("btn-default"); $('button[entity="'+entity+'"]').removeClass("btn-success"); + $('button[entity="'+entity+'"]').removeClass("btn-purple"); $('button[entity="'+entity+'"]').removeClass("btn-warning"); $('button[entity="'+entity+'"]').removeClass("btn-danger"); $('button[entity="'+entity+'"]').removeClass("btn-info"); From f168562fff7d6c17e94eafeefb455fad5fb916fc Mon Sep 17 00:00:00 2001 From: H Plato Date: Wed, 3 Oct 2018 11:44:16 -0600 Subject: [PATCH 61/78] only add hooks if timer is active and remove hook for inactive timer --- lib/Timer.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Timer.pm b/lib/Timer.pm index 6c301974d..9d4f3b5ef 100644 --- a/lib/Timer.pm +++ b/lib/Timer.pm @@ -238,7 +238,8 @@ sub set { return if &main::check_for_tied_filters( $self, $state ); if (($self->{state_updating} == 0) and ($self->{state_enable})) { - &::MainLoop_pre_add_hook( \&Timer::update_state, 0, $self); + &::MainLoop_pre_add_hook( \&Timer::update_state, 0, $self) unless ($self->{state_updating} == 1); + $self->{state_updating} = 1; } # Set states for NEXT pass, so expired, active, etc, @@ -261,6 +262,8 @@ sub set_from_last_pass { $self->{time} = undef; &delete_timer_with_action($self); $resort_timers_with_actions = 1; + &::MainLoop_pre_drop_hook( \&Timer::update_state, 0, $self) unless ($self->{state_updating} == 0); + $self->{state_updating} = 0; } # Turn a timer on @@ -300,6 +303,8 @@ sub unset { undef $self->{time}; undef $self->{action}; &delete_timer_with_action($self); + &::MainLoop_pre_drop_hook( \&Timer::update_state, 0, $self) unless ($self->{state_updating} == 0); + $self->{state_updating} = 0; } sub delete_old_timers { From 7f4e0a152377b7b6eb9d20b2e6d04222909cc437 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 6 Oct 2018 18:48:32 -0600 Subject: [PATCH 62/78] v1.2.8 --- lib/Yeelight.pm | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index 14ffc6d02..67deb9d26 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,4 +1,4 @@ -=head1 B v1.2.6 +=head1 B v1.2.8 =head2 Initial Setup # To set up, first pair with mobile app -- the Yeelight needs to be set up initially with the app @@ -24,15 +24,12 @@ $yeelight_rgb = new Yeelight_RGB($yeelight); =head2 MH.INI CONFIG PARAMS -yeelight_timeout HTTP request timeout (default 5) -yeelight_poll_seconds Number of seconds to poll the raZberry -yeelight_user Authentication username -yeelight_password Authentication password -yeelight_max_cmd_queue Maximum number of commands to queue up (default 6) +yeelight_timeout TCP request timeout (default 5) +yeelight_max_cmd_queue Maximum number of commands to queue up (default 8) yeelight_com_threshold Number of failed polls before controller marked offline (default 4) yeelight_command_timeout Number of seconds after a command is issued before it is abandoned (default 60) yeelight_command_timeout_limit Maximum number of retries for a command before abandoned - +yeelight_ssdp_timeout Maximum number of seconds to wait for SSDP data to return (default 1000) =head2 Notes @@ -108,13 +105,16 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.2.6"; + $self->{module_version} = "v1.2.8"; $self->{ssdp_timeout} = 1000; + $self->{ssdp_timeout} = $main::config_parms{yeelight_ssdp_timeout} if ( defined $main::config_parms{yeelight_ssdp_timeout} ); + $self->{socket_connected} = 0; $self->{host} = $location; $self->{port} = 55443; $self->{brightness_state_delay} = 1; - $self->{command_timeout_limit} = 3; + $self->{command_timeout_limit} = 4; + $self->{command_timeout_limit} = $main::config_parms{yeelight_max_cmd_queue} if ( defined $main::config_parms{yeelight_max_cmd_queue} ); if ($location =~ m/:/) { ($self->{host}, $self->{port}) = $location =~ /(.*):(.*)/; @@ -130,11 +130,20 @@ sub new { $self->{loglevel} = 5; ( $self->{loglevel} ) = ( $options =~ /loglevel\=(\d+)/i ) if ($options =~ m/loglevel\=/i ); + $self->{timeout} = 5; + $self->{timeout} = $main::config_parms{yeelight_timeout} if ( defined $main::config_parms{yeelight_timeout} ); + $self->{poll_data_timestamp} = 0; $self->{max_poll_queue} = 3; + $self->{max_cmd_queue} = 8; + $self->{max_cmd_queue} = $main::config_parms{yeelight_max_cmd_queue} if ( defined $main::config_parms{yeelight_max_cmd_queue} ); + $self->{cmd_process_retry_limit} = 6; + $self->{cmd_process_retry_limit} = $main::config_parms{yeelight_command_timeout_limit} if ( defined $main::config_parms{yeelight_command_timeout_limit} ); + $self->{command_timeout} = 60; + $self->{command_timeout} = $main::config_parms{yeelight_command_timeout} if ( defined $main::config_parms{yeelight_command_timeout} ); @{ $self->{poll_queue} } = (); $self->{poll_data_file} = "$::config_parms{data_dir}/Yeelight_poll_" . $self->{name} . ".data"; @@ -207,14 +216,15 @@ sub check_for_socket_data { } else { if (($self->{init}) and ($self->{socket_connected})) { - main::print_log( "[Yeelight:" . $self->{name} . "] Lost connection to Yeelight. Trying to connect again in $self->{reconnect} seconds" ); - $self->{recon_timer}->set($self->{reconnect_time}, sub {$self->{data_socket}->start;}); + main::print_log( "[Yeelight:" . $self->{name} . "] Lost connection to Yeelight. Trying to connect again in $self->{reconnect_time} seconds" ); + $self->{recon_timer}->set($self->{reconnect_time}, sub {$self->{data_socket}->start();}); $com_status = "offline"; + $self->{socket_connected} = 0; } } - if (( defined $self->{child_object}->{comm} ) and ($self->{socket_connected})) { - if ( $self->{status} ne $com_status ) { + if ( defined $self->{child_object}->{comm} ) { + if (( $self->{status} ne $com_status ) or ($self->{child_object}->{comm}->state() ne $com_status)) { $self->{status} = $com_status; if ($self->{child_object}->{comm}->state() ne $com_status) { main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); @@ -403,7 +413,8 @@ sub _get_TCP_data { my $cmdline = "{\"id\":" . $self->{id} . ",\"method\":" . $method{$mode} . ",\"params\":["; $cmdline .= join(',', @{$param_array{$mode}}); $cmdline .= "]}"; - my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; + my $options = "-timeout " . $self->{timeout} . " -rn -quiet "; + my $cmd = "get_tcp " . $options . " " . $self->{host} . ":" . $self->{port} . " '" . $cmdline . "'"; if ( $self->{poll_process}->done() ) { $self->{poll_process}->set($cmd); $self->{poll_process}->start(); @@ -422,7 +433,8 @@ sub _push_TCP_data { my $cmdline = "{ \"id\":" . $self->{id} . ", \"method\":" . $method{$mode} . ", \"params\":["; $cmdline .= join(',',@params); $cmdline .= "] }"; - my $cmd = "get_tcp -rn -quiet $self->{host}:$self->{port} " . "'" . $cmdline . "'"; + my $options = "-timeout " . $self->{timeout} . " -rn -quiet "; + my $cmd = "get_tcp " . $options . " " . $self->{host} . ":" . $self->{port} . " '" . $cmdline . "'"; if ( $self->{cmd_process}->done() ) { $self->{cmd_process}->set($cmd); From 2d05b203515b82046c6993b97850c57800dd6dcf Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 6 Oct 2018 18:49:49 -0600 Subject: [PATCH 63/78] v1.2.9 --- lib/Yeelight.pm | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index 67deb9d26..d4c0b2925 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,4 +1,4 @@ -=head1 B v1.2.8 +=head1 B v1.2.9 =head2 Initial Setup # To set up, first pair with mobile app -- the Yeelight needs to be set up initially with the app @@ -48,6 +48,9 @@ what features are supported - comm tracker went offline when commands dropped - 09/02/18 11:54:33 AM [Yeelight:1] WARNING. Queue has grown past 8. Command get_tcp -rn -quiet 192.168.0.173:55443 '{ "id":1, "method":"set_bright", "params":[90,"smooth",500] }' discarded. - 09/02/18 11:54:33 AM [Yeelight:1] Communication Tracking object found. Updating from online to offline... +- comm device offline, didn't go online when data came back +- lost data and didn't reconnect +- check CPU usage for yeelight =cut @@ -105,7 +108,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.2.8"; + $self->{module_version} = "v1.2.9"; $self->{ssdp_timeout} = 1000; $self->{ssdp_timeout} = $main::config_parms{yeelight_ssdp_timeout} if ( defined $main::config_parms{yeelight_ssdp_timeout} ); @@ -155,7 +158,7 @@ sub new { unlink "$::config_parms{data_dir}/Yeelight_cmd_" . $self->{name} . ".data"; $self->{cmd_process} = new Process_Item; $self->{cmd_process}->set_output( $self->{cmd_data_file} ); - $self->{init} = 0; + $self->{init} = 0 unless ($self->{init}); $self->{init_data} = 0; $self->{init_v_cmd} = 0; $self->{data_socket} = new Socket_Item(undef, undef, "$self->{host}:$self->{port}", "yeelight" . $self->{id}, 'tcp', 'raw'); @@ -179,7 +182,7 @@ sub check_for_socket_data { # Other objects use a socket in $Socket_Items? # $NewCmd = $Socket_Items{$instance}{'socket'}->said; - my $com_status = ""; + my $com_status = "offline"; if ($self->{data_socket}->active) { my $rec_data = $self->{data_socket}->said; $self->{socket_connected} = 1; @@ -266,7 +269,7 @@ sub get_data { } if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne $com_status ) { + if (( $self->{status} ne $com_status ) or ($self->{child_object}->{comm}->state() ne $com_status)) { $self->{status} = $com_status; if ($self->{child_object}->{comm}->state() ne $com_status) { main::print_log "[Yeelight:" . $self->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); @@ -849,6 +852,13 @@ sub set_ct { } } +sub restart_socket { + my ($self) = @_; + + $self->{data_socket}->stop(); + $self->{data_socket}->start(); +} + sub generate_voice_commands { my ($self) = @_; @@ -896,8 +906,8 @@ sub get_voice_cmds { my %voice_cmds = ( 'Print Command Queue to print log' => $self->get_object_name . '->print_command_queue', 'Purge Command Queue' => $self->get_object_name . '->purge_command_queue', - 'Force Yeelight Status query' => $self->get_object_name . '->query_yeelight' - + 'Force Yeelight Status query' => $self->get_object_name . '->query_yeelight', + 'Restart Data Socket connection' => $self->get_object_name . '->restart_socket' ); return \%voice_cmds; From 20cc1366d7c88b0d7725e2dfb33801c6345d6917 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 7 Oct 2018 18:01:56 -0600 Subject: [PATCH 64/78] v1.3.0 - no id needed on object --- lib/Yeelight.pm | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm index d4c0b2925..468774af3 100644 --- a/lib/Yeelight.pm +++ b/lib/Yeelight.pm @@ -1,4 +1,4 @@ -=head1 B v1.2.9 +=head1 B v1.3.0 =head2 Initial Setup # To set up, first pair with mobile app -- the Yeelight needs to be set up initially with the app @@ -53,6 +53,7 @@ what features are supported - check CPU usage for yeelight =cut +our $yl_instances; package Yeelight; @@ -93,14 +94,19 @@ my %param_array; our %active_yeelights = (); sub new { - my ( $class, $location, $id, $options ) = @_; + my ( $class, $location, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - $self->{id} = "1"; - $self->{id} = $id if ((defined $id) and ($id)); - $self->{name} = "1"; - $self->{name} = $id if ((defined $id) and ($id)); + unless (defined $yl_instances) { + $self->{id} = "1"; + $self->{name} = "1"; + $yl_instances = 1; + } else { + $yl_instances++; + $self->{id} = $yl_instances; + $self->{name} = $yl_instances; + } $self->{data} = undef; $self->{child_object} = undef; @@ -108,7 +114,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.2.9"; + $self->{module_version} = "v1.3.0"; $self->{ssdp_timeout} = 1000; $self->{ssdp_timeout} = $main::config_parms{yeelight_ssdp_timeout} if ( defined $main::config_parms{yeelight_ssdp_timeout} ); @@ -126,7 +132,7 @@ sub new { $options = "" unless ( defined $options ); $options = $::config_parms{ "yeelight_" . $self->{name} . "_options" } if ( $::config_parms{ "yeelight_" . $self->{name} . "_options" } ); - $self->{debug} = 5; + $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) if ( $options =~ m/debug\=/i ); $self->{debug} = 0 if ( $self->{debug} < 0 ); @@ -522,14 +528,15 @@ SSDP last unless scalar @ready; recv($sock,$data, 65536,0); - $count++; - &main::print_log( "[Yeelight] Receiving $count ($i) "); my ($location) = $data =~ /Location:\syeelight:\/\/(.*)/; $location =~ s/[^a-zA-Z0-9\:\.\/]*//g; if ($location) { + $count++; my ($host, $port) = $location =~ /(.*):(.*)/; $yl{$host}->{host} = $host; - $yl{$host}->{port} = $port; + $yl{$host}->{port} = $port; + &main::print_log( "[Yeelight] Found $count (loop $i) (location $location)"); + #Go through the rest of the data foreach my $line (split(/\n/,$data)) { my ($field, $value) = $line =~ /(.*)\:\s+(.*)/; From 963cc222dd6f17140d82d4708d21fc4bdd6e06fc Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 19 Nov 2018 17:52:35 -0700 Subject: [PATCH 65/78] v2.0.820 - fixed color sliders on all objects --- web/ia7/include/javascript.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index cf741ebab..747b861db 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -var ia7_ver = "v2.0.810"; +var ia7_ver = "v2.0.820"; var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; @@ -2540,6 +2540,7 @@ var graph_rrd = function(start,group,time) { var previousPoint = null; $("#rrd-graph").bind("plothover", function(event, pos, item) { +//tofixed caused a problem $("#x").text(pos.x.toFixed(2)); $("#y").text(pos.y.toFixed(2)); if (item) { @@ -3552,6 +3553,10 @@ var create_state_modal = function(entity) { var name = entity; if (json_store.objects[entity].label !== undefined) name = json_store.objects[entity].label; $('#slider').remove(); + $('#sliderR').remove(); + $('#sliderG').remove(); + $('#sliderB').remove(); + // $('#control').modal('show'); //make sure the modal is centered on all devices From 818d9a8dc00fd5a30dd8a7a3eb49d4e1dac04742 Mon Sep 17 00:00:00 2001 From: George Clark Date: Sat, 13 Apr 2019 22:08:54 -0400 Subject: [PATCH 66/78] Fix unescaped braces in issue #744 --- web/bin/code_select.pl | 2 +- web/bin/code_unselect.pl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/bin/code_select.pl b/web/bin/code_select.pl index da582cf06..0913818d4 100644 --- a/web/bin/code_select.pl +++ b/web/bin/code_select.pl @@ -73,7 +73,7 @@ sub select_code_form { $category =~ s/\s+$//; # Drop trailing whitespace # Ignore $config_parm{$xyz} entries - while ( $line =~ /config_parms{([^\$]+)}/g ) { + while ( $line =~ /config_parms\{([^\$]+)\}/g ) { $file_parms{$1}++ unless $standard_parms{$1}; } } diff --git a/web/bin/code_unselect.pl b/web/bin/code_unselect.pl index 6fb06624d..c67a69b34 100644 --- a/web/bin/code_unselect.pl +++ b/web/bin/code_unselect.pl @@ -89,7 +89,7 @@ sub select_code_form { $category =~ s/\s+$//; # Drop trailing whitespace # Ignore $config_parm{$xyz} entries - while ( $line =~ /config_parms{([^\$]+)}/g ) { + while ( $line =~ /config_parms\{([^\$]+)\}/g ) { $file_parms{$1}++ unless $standard_parms{$1}; } } From 1bb938759568572cb3638a119e12f6921036c29f Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Mon, 13 May 2019 12:56:48 +0200 Subject: [PATCH 67/78] fix http redirect with nginx proxy If misterhouse is run behind nginx webserver the superfluous space in line 2321 causes broken redirects (Location header is changed to "Location: $url Cache-Control: no-cache" instead of only "Location: $url" --- lib/http_server.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index 7f01fcc34..f631f5cac 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -2318,7 +2318,7 @@ sub http_redirect { $html_head .= "Server: MisterHouse\r\n"; $html_head .= "Location: $url\r\n"; $html_head .= "Content-Length: 0\r\n"; - $html_head .= "Connection: close\r\n " if &http_close_socket; + $html_head .= "Connection: close\r\n" if &http_close_socket; $html_head .= "Cache-Control: no-cache\r\n"; $html_head .= "\r\n"; From 100025695fa0cff06e3df8bf8958db813744c8fd Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Mon, 13 May 2019 13:10:31 +0200 Subject: [PATCH 68/78] prevent warning with alexa/echo If AlexaBidge.pl is used http_server.pl logs a warning when the echo reqeusts to change the state of a light: "[http_server.pl]: Warning, invalid argument string detected (...)" this change prevents the warning since it is a valid request. --- lib/http_server.pl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index 7f01fcc34..5ce52e9cb 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -375,7 +375,11 @@ sub http_process_request { # For this reason we use a regular expresion here instead of # checking for "application/json" using the "eq" operator. # - } elsif ($Http{'Content-Type'} =~ m%^application/json%i && $HTTP_BODY =~ /^\{/) { + # alex/echo sends + # Content-type: application/x-www-form-urlencoded + # with a json body + # + } elsif ($Http{'Content-Type'} =~ m%^application/(json\|x-www-form-urlencoded)%i && $HTTP_BODY =~ /^\{/) { print "[http_server.pl]: posting json data\n" if $main::Debug{http}; } else { &main::print_log("[http_server.pl]: Warning, invalid argument string detected ($buf) ($Http{'Content-Type'}) ($HTTP_BODY)\n"); From 98ea66feb4bab173dbc848edcd6c641c5c1b7ac3 Mon Sep 17 00:00:00 2001 From: hplato Date: Wed, 22 May 2019 13:23:20 -0600 Subject: [PATCH 69/78] v2.0.1 - dark sky as the data provider --- calc_eto.pl | 1307 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1307 insertions(+) create mode 100644 calc_eto.pl diff --git a/calc_eto.pl b/calc_eto.pl new file mode 100644 index 000000000..307ee4154 --- /dev/null +++ b/calc_eto.pl @@ -0,0 +1,1307 @@ +# Category = Irrigation + +# March 2019 +# v2 +# - moved to DarkSky as data provider. Location has to be lats and longs +# +# v1.3 +# - added check if wudata returns null data +# - Email has clearer information on start times and run length. +# - if run after sunrise, then use the sunset times and max 2 cycles +# - write predicted daily rain to the RRD. + +#@ This module allows MisterHouse to calculate daily EvapoTranspiration based on a +#@ Data feed previously from Weatherunderground (WU) now darksky due to IBM removing the free license. +#@ To use it you need to sign up for a key +#@ A location is also required. Best is a lat/long pair. +#@ By default wuData is written to $Data_Dir/wuData and the eto logs are written to $Data_Dir/eto +#@ +#@ The ET programs can be automatically uploaded to an OpenSprinkler. (need >= v1.1 of the lib) + +########################################################################################################### +## Credits ## +########################################################################################################### +## portions of code provided by Zimmerman method used by OpenSprinkler ## +## portions of code provided/edited by Ray and Samer of OpenSprinkler ## +## eto library provided by Mark Richards ## +## Original python code provided by Shawn Harte 2014 no copyright reserved ## +## python cleanup and first pass my Neil Cherry ## +## Code was used with utmost respect to the original authors, your efforts have prevented the ## +## re-invention of the wheel ## +########################################################################################################### + +#The script accounts for wind, freezing conditions, and current/recent +#rainfall when considering start and run times. It will avoid watering +#during midday, unless early morning winds prevent earlier start times. +#The starts are serialized so no odd overlaps should occur. Mornings +#are preferred to evenings to allow for the best use of water and +#absorption without causing mold and fungus growth by leaving grass wet +#overnight. The script is commented quite heavily, so that anyone can +#edit or use it to their liking. Please be mindful that other authors +#work was used or modified when the code seemed generalized enough that +#I shouldn’t be stepping on toes. Please do not pester the original +#author if something doesn’t work for you, as they will probably have +#enough on their own plate with their own original works. +# +#Everything is done based off your latitude and longitude, however, the +#script can find the info when provided with a city/state or country, a +#US Zip Code, or a PWS ID. + +#Usage: +#Create a config_parm{eto_zone_1mm} with the number of seconds to distribute 1mm of water in the zone. +#Create a config_parm{eto_zone_crop} with 1's and 0's (1=grass, 0=shrubs/garden) +#Find your closest weatherunderground location and store it in config_parms{eto_location} +#Get your wu api key and store it in config_parms{wu_key} + +#TODO +# - the safefloat and safeint subs are from python. don't know if they're needed + +#VERIFY +# - line 430 sub getConditionsData chkcond array isn't checked yet +# - line 63 Use use JSON qw(decode_json) instead of JSON::XS +# - line 360 test timezone subroutine, confirm that it actually works +# - line 610 read in multiple water times for overall aggregate +# - line 711 when multiple times are scheduled, only one entry was written to the logs. + +#WU Data elements mapping (useful if we want to look to another provider) +#$hist = $wuData->{history}->{dailysummary}[0]; +#$wuData->{history}->{observations} +#$wuData->{history}->{observations}->[$period]->{date}->{hour} +#$wuData->{history}->{observations}->[$period]->{conds} + +#$tzone = $data->{current_observation}->{local_tz_long}; +#$mm = $data->[$day]->{qpf_allday}->{mm}; +#$cor = $data->[$day]->{pop}; +##$rHour = safe_int( $data->{'sunrise'}->{'hour'}, 6 ); +##$rMin = safe_int( $data->{'sunrise'}->{'minute'} ); +##$sHour = safe_int( $data->{'sunset'}->{'hour'}, 18 ); +##$sMin = safe_int( $data->{'sunset'}->{'minute'} ); +#$conditions->{ $current->{weather} } +#$current->{wind_kph} ), 10 ); +#$cTemp = safe_float( $current->{temp_c}, 20 ); +#$cmm = safe_float( $current->{precip_today_metric} ); +#$predicted->{avewind}->{kph} +#$pLowTemp = safe_float( $predicted->{low}->{celsius} ); +#$pCoR = safe_float( $predicted->{pop} ); +#$pmm = safe_float( $predicted->{qpf_allday}->{mm} ); + +use eto; +use LWP::UserAgent; +use HTTP::Request::Common; + +#use JSON::XS; +use JSON qw(decode_json); +use List::Util qw(min max sum); + +#use Data::Dumper; +use Time::Local; +use Date::Calc qw(Day_of_Year); +my $debug = 5; +my $msg_string; +my $rrd = ""; + +$p_wu_forecast = new Process_Item + qq[get_url --quiet "https://api.darksky.net/forecast/$config_parms{wu_key}/$config_parms{eto_location}?units=ca" "$config_parms{data_dir}/wuData/wu_data.json"]; +$v_get_eto = new Voice_Cmd("Update ETO Programs"); +$t_wu_forecast_timer = new Timer; + +my $eto_data_dir = $config_parms{data_dir} . "/eto"; +$eto_data_dir = $config_parms{eto_data_dir} if ( defined $config_parms{eto_data_dir} ); + +my $eto_calc_time = "3:00 AM"; +$eto_calc_time = $config_parms{eto_calc_time} if ( defined $config_parms{eto_calc_time} ); +$eto_calc_time = " " . $eto_calc_time if ( $eto_calc_time =~ m/^\d:\d\d\s/ ); #time_now has a space in front if only a single digit hour + +my $eto_retries = 3; +$eto_retries = $config_parms{eto_retries} if ( defined $config_parms{eto_retries} ); +my $eto_retries_today; + +$config_parms{eto_rainfallsatpoint} = 25 unless ( defined $config_parms{eto_rainfallsatpoint} ); +$config_parms{eto_minmax} = "5,15" unless ( defined $config_parms{eto_minmax} ); + +my $eto_ready; + +if ( $Startup or $Reload ) { + $eto_ready = 1; + print_log "[calc_eto] v2.0.1 Startup. Checking Configuration..."; + + mkdir "$eto_data_dir" unless ( -d "$eto_data_dir" ); + mkdir "$eto_data_dir/ET" unless ( -d "$eto_data_dir/ET" ); + mkdir "$eto_data_dir/logs" unless ( -d "$eto_data_dir/logs" ); + mkdir "$config_parms{data_dir}/wuData" unless ( -d "$config_parms{data_dir}/wuData" ); + mkdir "$eto_data_dir/weatherprograms" unless ( -d "$eto_data_dir/weatherprograms" ); + + print_log "[calc_eto] Weather Data Powered by Dark Sky..."; + + if ( defined $config_parms{eto_location} ) { + print_log "[calc_eto] Location : $config_parms{eto_location}"; + } + else { + print_log "[calc_eto] ERROR! eto_location undefined!!"; + $eto_ready = 0; + } + if ( defined $config_parms{eto_zone_1mm} ) { + print_log "[calc_eto] 1mm zone runtimes (in seconds) : $config_parms{eto_zone_1mm}"; + } + else { + print_log "[calc_eto] ERROR! eto_zone_1mm undefined!!"; + $eto_ready = 0; + } + if ( defined $config_parms{eto_zone_crop} ) { + print_log "[calc_eto] 1mm crop definitions : $config_parms{eto_zone_crop}"; + } + else { + print_log "[calc_eto] ERROR! eto_zone_crop undefined!!"; + $eto_ready = 0; + } + unless ( defined $config_parms{wu_key} ) { + print_log "[calc_eto] ERROR! wu key undefined!!"; + $eto_ready = 0; + } + if ( defined $config_parms{eto_rrd} ) { + if ($config_parms{eto_rrd} eq "metric") { + print_log "[calc_eto] Will write daily rain to RRD (mms)"; + $rrd = "m"; + } elsif ($config_parms{eto_rrd} eq "in") { + print_log "[calc_eto] Inches to RRD not supported yet"; + $rrd = ""; + } else { + print_log "[calc_eto] Unknown RRD option $config_parms{eto_rrd}"; + $rrd = ""; + } + } + if ( defined $config_parms{eto_irrigation} ) { + print_log "[calc_eto] $config_parms{eto_irrigation} set as programmable irrigation system"; + } + else { + print_log "[calc_eto] WARNING! no sprinkler system defined!"; + } + if ($eto_ready) { + print_log "[calc_eto] Configuration good. ETo Calcuations Ready"; + print_log "[calc_eto] Will email results to $config_parms{eto_email}" if ( defined $config_parms{eto_email} ); + } + else { + print_log "[calc_eto] ERROR! ETo configuration problem. ETo will not calcuate"; + } +} + +if ( ( said $v_get_eto) or ( $New_Minute and ( $Time_Now eq $eto_calc_time ) ) ) { + if ($eto_ready) { + print_log "[calc_eto] Starting Daily ETO Calculation Process..."; + $eto_retries_today = 0; + start $p_wu_forecast; + } + else { + print_log "[calc_eto] ERROR! ETo configuration problem. ETo will not calcuate"; + } +} + +if ( done_now $p_wu_forecast) { + my $write_secs = time() - ( stat("$config_parms{data_dir}/wuData/wu_data.json") )[9]; + if ( $write_secs > 300 ) { + print_log "[calc_eto] Stale Data, not calculating ETo. WU Data written $write_secs ago..."; + } + else { + my $program_data = &calc_eto_runtimes( $eto_data_dir, "file", $config_parms{eto_location}, "$config_parms{data_dir}/wuData/wu_data.json" ); + if ($program_data) { + if ( defined $config_parms{eto_irrigation} ) { + if ( lc $config_parms{eto_irrigation} eq "opensprinkler" ) { + my $os_program = &get_object_by_name( $config_parms{eto_opensprinkler_program} ); + my ( $run_times, $run_seconds ) = $program_data =~ /\[\[(.*)\],\[(.*)\]\]/; + print_log "[calc_eto] Loading values $run_times,$run_seconds into program $config_parms{eto_opensprinkler_program}"; + $os_program->set_program( $Day, $run_times, $run_seconds ); + } #elsif (other sprinkler system...) + } + } + else { + if ( $eto_retries_today < $eto_retries ) { + $eto_retries_today++; + print_log "[calc_eto] WARNING! bad program data, retry attempt $eto_retries_today"; + set $t_wu_forecast_timer 600; + + #start $p_wu_forecast; + } + else { + print_log "[calc_eto] ERROR! retry max $eto_retries reaches. Aborting calculation attempt"; + } + } + } +} + +if ( expired $t_wu_forecast_timer) { + start $p_wu_forecast; +} + +#-------------------------------------------------------------------------------------------------------------------------------# +# Mapping of conditions to a level of shading. +# Since these are for sprinklers any hint of snow will be considered total cover (10) +# Don't worry about wet conditions like fog these are accounted for below we are only concerned with how much sunlight is blocked at ground level + +our $conditions = { + 'Clear' => 0, + 'Partial Fog' => 2, + 'Patches of Fog' => 2, + 'Haze' => 2, + 'Shallow Fog' => 3, + 'Scattered Clouds' => 4, + 'Unknown' => 5, + 'Fog' => 5, + 'Partly Cloudy' => 5, + 'Mostly Cloudy' => 8, + 'Mist' => 10, + 'Light Drizzle' => 10, + 'Light Freezing Drizzle' => 10, + 'Light Freezing Rain' => 10, + 'Light Freezing Fog' => 5, + 'Light Ice Pellets' => 10, + 'Light Rain' => 10, + 'Light Rain Showers' => 10, + 'Light Snow' => 10, + 'Light Snow Grains' => 10, + 'Light Snow Showers' => 10, + 'Light Thunderstorms and Rain' => 10, + 'Low Drifting Snow' => 10, + 'Rain' => 10, + 'Rain Showers' => 10, + 'Snow' => 10, + 'Snow Showers' => 10, + 'Thunderstorm' => 10, + 'Thunderstorms and Rain' => 10, + 'Blowing Snow' => 10, + 'Chance of Snow' => 10, + 'Freezing Rain' => 10, + 'Unknown Precipitation' => 10, + 'Overcast' => 10, +}; + +# List of precipitation conditions we don't want to water in, the conditions will be checked to see if they contain these phrases. + +our $chkcond = { + 'flurries' => 1, + 'rain' => 1, + 'sleet' => 1, + 'snow' => 1, + 'storm' => 1, + 'hail' => 1, + 'ice' => 1, + 'squall' => 1, + 'precip' => 1, + 'funnel' => 1, + 'drizzle' => 1, + 'mist' => 1, + 'freezing' => 1, +}; + +# +################################################################################ +# -[ Functions ]---------------------------------------------------------------- + +# define safe functions for variable conversion, preventing errors with NaN and Null as string values +# 's'=value to convert 'dv'=value to default to on error make sure this is a legal float or integer value + +#just stub for testing, have to fix up the floats and int +sub safe_float { + my ( $arg, $val ) = @_; + + # $val = "0.0" unless ($val); + # $arg = $val unless ($arg); + return ($arg); +} + +sub safe_int { + my ( $arg, $val ) = @_; + + # $val = "0" unless ($val); + # $arg = $val unless ($arg); + return ($arg); +} + +sub isInt { + my ($arg) = @_; + return ( $arg - int($arg) ) ? 0 : 1; +} + +sub isFloat { + my ($arg) = @_; + return 1; + + #return ($arg - int($arg))? 1 : 0; +} + +sub round { + my ( $number, $places ) = @_; + my $sign = ( $number < 0 ) ? '-' : ''; + my $abs = abs($number); + + if ( $places < 0 ) { + print_log "[calc_eto] ERROR! rounding to $places"; + return $number; + } + else { + my $p10 = 10**$places; + return $sign . int( $abs * $p10 + 0.5 ) / $p10; + } +} + +sub findwuLocation { + my ($loc) = @_; + my ( $whttyp, $ploc, $noData, $tzone, $lat, $lon ); + my $ua = new LWP::UserAgent( keep_alive => 1 ); + + my $request = HTTP::Request->new( GET => "http://autocomplete.wunderground.com/aq?format=json&query=$loc" ); + my $responseObj = $ua->request($request); + my $data; + + # eval { $data = JSON::XS->new->decode( $responseObj->content ); }; + eval { $data = decode_json( $responseObj->content ); }; + my $responseCode = $responseObj->code; + my $isSuccessResponse = $responseCode < 400; + if ( $isSuccessResponse and defined $data->{RESULTS} ) { + my $chk = $data->{RESULTS}->[0]->{ll}; # # ll has lat and lon in one spot no matter how we search + if ($chk) { + my @ll = split( ' ', $chk ); + if ( scalar(@ll) == 2 and isFloat( $ll[0] ) and isFloat( $ll[1] ) ) { + $lat = $ll[0]; + $lon = $ll[1]; + } + } + + $chk = $data->{RESULTS}->[0]->{tz}; + if ($chk) { + $tzone = $chk; + } + else { + my $chk2 = $data->{RESULTS}->[0]->{tz_long}; + if ($chk2) { + $tzone = $chk2; + } + else { + $tzone = "None"; + } + } + + $chk = $data->{RESULTS}->[0]->{name}; # # this is great for showing a pretty name for the location + if ($chk) { + $ploc = $chk; + } + + $chk = $data->{RESULTS}->[0]->{type}; + if ($chk) { + $whttyp = $chk; + } + + } + else { + $noData = 1; + $lat = "None"; + $lon = "None"; + $tzone = "None"; + $ploc = "None"; + $whttyp = "None"; + } + return ( $whttyp, $ploc, $noData, $tzone, $lat, $lon ); +} + +sub getwuData { + my ( $loc, $key ) = @_; + my $tloc = split( ',', $loc ); + + #return if ($key == '' or (scalar ($tloc) < 2)); + my $ua = new LWP::UserAgent( keep_alive => 1 ); + + my $request = HTTP::Request->new( GET => "https://api.darksky.net/forecast/$config_parms{wu_key}/$config_parms{eto_location}?units=ca" ); + + my $responseObj = $ua->request($request); + my $data; + + # eval { $data = JSON::XS->new->decode( $responseObj->content ); }; + eval { $data = decode_json( $responseObj->content ); }; + if ($@) { + print_log "[calc_eto] ERROR problem parsing json from web call"; + } + my $responseCode = $responseObj->code; + my $isSuccessResponse = $responseCode < 400; + print "code=$responseCode\n" if ($debug); + + return ($data); + +} + +sub getwuDataTZOffset { + my ( $data, $tzone ) = @_; + + #HP TODO - I'm not sure if this works as expected + if ( $tzone eq "None" or $tzone eq "" ) { + $tzone = $data->{offset}; #??$tzone isn't used. timezone also present in data + } + my $tdelta; + + if ($tzone) { + my @tnow = localtime(time); + $tdelta = timegm(@tnow) - timelocal(@tnow); + + # tdelta = tnow.astimezone(pytz.timezone(tz)).utcoffset() + } + if ($tdelta) { + return ( { 't' => ( $tdelta / 900 + 48 ), 'gmt' => ( $tdelta / 3600 ) } ); + } + else { + return ( { 't' => "None", 'gmt' => "None" } ); + } +} + +# Calculate an adjustment based on predicted rainfall +# Rain forecast should lessen current watering and reduce rain water runoff, making the best use of rain. +# returns tadjust (???) +sub getForecastData { + my ($data) = @_; + + #HP TODO - I don't know why the python wanted to create a bunch of arrays (mm, cor, wfc). It seems like + #HP TODO - just the end result is needed + if ( @{$data->{daily}->{data}} ) { + + #print Dumper @{$data->{daily}->{data}}; + + my $fadjust = 0; +# for ( my $day = 1; $day < scalar( @{$data->{daily}->{data}} ); $day++ ) { only look at the next three days + for ( my $day = 1; (($day < scalar( @{$data->{daily}->{data}}) and ($day < 4)) ); $day++ ) { + my $mm = $data->{daily}->{data}->[$day]->{precipIntensity} * 24; #darkskies returns mm/d so get a day value. + my $cor = $data->{daily}->{data}->[$day]->{precipProbability} * 100; #to make it similar to WU + my $wfc = 1 / $day**2; #HP I assume this is to modify that further days out are more volatile? + $fadjust += safe_float( $mm, -1 ) * ( safe_float( $cor, -1 ) / 100 ) * safe_float( $wfc, -1 ); + print "DBB: forecast mm=$mm cor=$cor wfc=$wfc day=$day fadjust=$fadjust\n" if ($debug); + } + print "DBB: fadjust=$fadjust\n" if ($debug); + return $fadjust; + } + return -1; +} + +# Grab the sunrise and sunset times in minutes from midnight +sub getAstronomyData { + my ($data) = @_; + + if ( not $data ) { + return ( { "rise" => -1, "set" => -1 } ); + } + #my ($sec, $min, $hour, $day,$month,$year) = (localtime($time))[0,1,2,3,4,5]; + my $rHour = (localtime($data->{daily}->{data}->[0]->{sunriseTime}))[2]; + my $rMin = (localtime($data->{daily}->{data}->[0]->{sunriseTime}))[1]; + my $sHour = (localtime($data->{daily}->{data}->[0]->{sunsetTime}))[2]; + my $sMin = (localtime($data->{daily}->{data}->[0]->{sunsetTime}))[1]; + + +# my $rHour = safe_int( $data->{'sunrise'}->{'hour'}, 6 ); +# my $rMin = safe_int( $data->{'sunrise'}->{'minute'} ); +# my $sHour = safe_int( $data->{'sunset'}->{'hour'}, 18 ); +# my $sMin = safe_int( $data->{'sunset'}->{'minute'} ); + if ( $rHour, $rMin, $sHour, $sMin ) { + return ( { "rise" => $rHour * 60 + $rMin, "set" => $sHour * 60 + $sMin } ); + } + else { + return ( { "rise" => -1, "set" => -1 } ); + } +} + +# Let's check the current weather and make sure the wind is calm enough, it's not raining, and the temp is above freezing +# We will also look at what the rest of the day is supposed to look like, we want to stop watering if it is going to rain, +# or if the temperature will drop below freezing, as it would be bad for the pipes to contain water in these conditions. +# Windspeed for the rest of the day is used to determine best low wind watering time. + +sub getConditionsData { + my ( $current, $predicted, $conditions ) = @_; + + my $nowater = 1; + my $whynot = 'Unknown'; + + unless ( $current and $predicted ) { + return ( 0, 1, 'No conditions data' ); + } + my $cWeather = ""; + $cWeather = safe_float( $conditions->{ $current->{summary} }, 5 ); + + unless ( defined $conditions->{ $current->{summary} } ) { + + # check if any of the chkcond words exist in the $current-{weather} + + my $badcond = 0; + foreach my $chkword ( split( ' ', lc $current->{summary} ) ) { + $badcond = 1 if ( defined $chkcond->{$chkword} ); + } + + # if (defined $conditions->{$current->{weather}} ) { + if ($badcond) { + $cWeather = 10; + } + else { + print_log '[calc_eto] INFO Cound not find current conditions ' . $current->{summary}; + $cWeather = 5; + } + } + + my $cWind = &eto::wind_speed_2m( safe_float( $current->{windSpeed} ), 10 ); + my $cTemp = safe_float( $current->{temperature}, 20 ); + + # current rain will only be used to adjust watering right before the start time + + my $cmm = safe_float( $current->{precipIntensity} * 24 ); # Today's predicted rain (mm) - Darkskies returns mm/h so convert to mm/d + my $pWind = &eto::wind_speed_2m( safe_float( $predicted->{windSpeed} ), 10 ); # Today's predicted wind (kph) + my $pLowTemp = safe_float( $predicted->{temperatureLow} ); # Today's predicted low (C) + my $pCoR = safe_float( $predicted->{precipProbability} ); # Today's predicted POP (%) (Probability of Precipitation) + my $pmm = safe_float( $predicted->{precipIntensity} * 24 ); # Today's predicted QFP (mm) (Quantitative Precipitation Forecast) + # + + # Let's check to see if it's raining, windy, or freezing. Since watering is based on yesterday's data + # we will see how much it rained today and how much it might rain later today. This should + # help reduce excess watering, without stopping water when little rain is forecast. + + $nowater = 0; + $whynot = ''; + + # Its precipitating + #HP TODO - this triggered on 'Clear'? + if ( $cWeather == 10 and lc $current->{weather} ne 'overcast' ) { + $nowater = 1; + $whynot .= 'precipitation (' . $current->{weather} . ') '; + } + + # Too windy + if ( $cWind > $pWind and $pWind > 6 or $cWind > 8 ) { + $nowater = 1; + $whynot .= 'wind (' . round( $cWind, 2 ) . ' kph) '; + } + + # Too cold + if ( $cTemp < 4.5 or $pLowTemp < 1 ) { + $nowater = 1; + $whynot .= 'cold (current ' . round( $cTemp, 2 ) . ' C / predicted ' . round( $pLowTemp, 2 ) . ' C) '; + } + +#CHECK Does Darksky take chance of precip into the pmm? + $cmm += $pmm * $pCoR if ($pCoR); + + #HP TODO - Don't know where this except comes from + #HP except: + #HP print 'we had a problem and just decided to water anyway' + #HP nowater = 0 + # + #print "[$cmm,$nowater,$whynot]\n"; + return ( $cmm, $nowater, $whynot ); +} + +sub sun_block { + + # Difference from Python script. If there are multiple forecasts for a given hour (ie overcast and scattered clouds), then it will + # take the last entry for calculating cover. Could average it, but really the difference isn't that huge I don't think. + my ( $wuData, $sunrise, $sunset, $conditions ) = @_; + my $sh = 0; + my $previousCloudCover = 0; + + for ( my $hour = int( $sunrise / 60 ); $hour < int( $sunset / 60 + 1 ); $hour++ ) { + + # Set a default value so we know we found missing data and can handle the gaps + my $cloudCover = -1; + + # Now let's find the data for each hour there are more periods than hours so only grab the first + #in range(len(wuData['history']['observations'])): + for ( my $period = 0; $period < 23 ; $period++ ) { + #get hour from epoch my ($sec, $min, $hour, $day,$month,$year) = (localtime($time))[0,1,2,3,4,5]; + if ( safe_int( (localtime($wuData->{hourly}->{data}->[$period]->{time}))[2], -1 ) == $hour ) { + if ( $wuData->{hourly}->{data}->[$period]->{summary} ) { + print "[$hour," + . $wuData->{hourly}->{data}->[$period]->{summary} . "," + . $conditions->{ $wuData->{hourly}->{data}->[$period]->{summary} } . "]\n" + if ($debug); + $cloudCover = safe_float( $conditions->{ $wuData->{hourly}->{data}->[$period]->{summary} }, 5 ) / 10; + unless ( defined $cloudCover ) { + $cloudCover = 10; + print_log '[calc_eto] INFO Sun Block Condition not found ' . $wuData->{hourly}->{data}->[$period]->{summary}; + } + } + } + } + + # Found nothing, let's assume it was the same as last hour + $cloudCover = $previousCloudCover if ( $cloudCover == -1 ); + print "[$hour,$cloudCover]\n" if ($debug); + # + + $previousCloudCover = $cloudCover; + + # Got something now? let's check + $sh += 1 - $cloudCover if ( $cloudCover != -1 ); + print "total $sh $cloudCover\n" if ($debug); + + } + return ($sh); +} + +sub getHourlyElements { + + # Difference from WU data. DarkSkies has humidity forecast every hour, so look forward 24 hours to find the min and max. + # take the last entry for calculating cover. Could average it, but really the difference isn't that huge I don't think. + my ( $wuData) = @_; + my ($rh_min, $rh_max); + my $meanwindspeed = 0; + + + for ( my $period = 0; $period < 23 ; $period++ ) { + $rh_min = $wuData->{hourly}->{data}->[$period]->{humidity} unless ((defined $rh_min) or ($wuData->{hourly}->{data}->[$period]->{humidity} < $rh_min)); + $rh_max = $wuData->{hourly}->{data}->[$period]->{humidity} unless ((defined $rh_max) or ($wuData->{hourly}->{data}->[$period]->{humidity} > $rh_max)); + $meanwindspeed += $wuData->{hourly}->{data}->[$period]->{windSpeed}; + } + + my $rh_mean = ( $rh_min + $rh_max ) / 2; + $meanwindspeed = $meanwindspeed / 24; + return ( $rh_min, $rh_max, $rh_mean, $meanwindspeed ); +} + +# We need to know how much it rained yesterday and how much we watered versus how much we required +sub mmFromLogs { + my ( $_1mmProg, $logsPath, $ETPath ) = @_; + + my $prevLogFname = int( ( time - ( time % 86400 ) - 1 ) / 86400 ); + + #look that the $prevLogFname exists for both logs and ET. if not look for up to 7 days back for a + #day that they both exist + my $tmpLogFname = $prevLogFname; + my $fnamefound = 0; + for ( my $fx = $tmpLogFname; $fx > $tmpLogFname - 7; $fx-- ) { + print "***** Looking for $fx\n" if ($debug); + if ( ( -e "$logsPath/$fx" ) and ( -e "$ETPath/$fx" ) ) { + print "log file found $fx\n" if ($debug); + $prevLogFname = $fx; + $fnamefound = 1; + last; + } + } + print_log "[calc.eto] WARNING! Couldn't find log/ET files less than 7 days old" unless ($fnamefound); + + my $nStations = scalar( @{ $_1mmProg->{mmTime} } ); + + my @ydur = (-1) x $nStations; + my @ymm = (-1) x $nStations; + + my @yET = ( 0, 0 ); # Yesterday's Evap (evapotranspiration, moisture losses mm/day) + my @tET = ( 0, 0 ); + + # -[ Logs ]----------------------------------------------------------------- + my @logs = (); + + if ( open( FILE, "$logsPath/$prevLogFname" ) ) { + my $d_logs = ; + my $t_logs; + + # eval { $t_logs = JSON::XS->new->decode($d_logs) }; + eval { $t_logs = decode_json($d_logs); }; + @logs = @$t_logs; + close(FILE); + } + else { + print_log "[calc_eto] WARNING Can't open file $logsPath/$prevLogFname!"; + close(FILE); + } + + ### the original code first looked for yesterday's log file and used that + ### filename to get the json from ETPath and LogsPath. + ### I simply check for yesterday's files and if I get an exception I create + ### default vaules of 0 (in the appropriate array format) + ### We now look 7 days back to find the last file. If nothing exists for 7 days, then use 0's + + # -[ ET ]------------------------------------------------------------------- + if ( open( FILE, "$ETPath/$prevLogFname" ) ) { + my $d_yET = ; + my $t_yET; + + # eval { $t_yET = JSON::XS->new->decode($d_yET) }; + eval { $t_yET = decode_json($d_yET) }; + @yET = @$t_yET; + close(FILE); + } + else { + print_log "[calc_eto] WARNING Can't open file $ETPath/$prevLogFname!"; + close(FILE); + } + + # add all the run times together (a zone can have up to 4 daily runtimes) to get the overall amount of water + for ( my $x = 0; $x < scalar(@logs); $x++ ) { + $ydur[ $logs[$x][1] ] += $logs[$x][2]; + print "[logs[$x][2] = " . $logs[$x][2] . " ydur[$logs[$x][1]] = " . $ydur[ $logs[$x][1] ] . "]\n"; + } + + for ( my $x = 0; $x < $nStations; $x++ ) { + if ( $_1mmProg->{mmTime}[$x] ) { + + # 'mmTime': [15, 16, 20, 10, 30, 30] sec/mm + # 'crop': [1, 1, 1, 1, 0, 0] 1 = grasses, 0 = shrubs + #ymm[x] = round( (safe_float(yET[safe_int(_1mmProg['crop'][x])])) - (ydur[x]/safe_float(_1mmProg['mmTime'][x])), 4) * (-1) + # Rewritten to make it readable (nothing more) + my $yesterdaysET = safe_float( $yET[ safe_int( $_1mmProg->{crop}[$x] ) ] ); # in seconds + my $yesterdaysDuration = $ydur[$x]; # in mm + my $mmProg = safe_float( $_1mmProg->{mmTime}[$x] ); # in seconds/mm + # ymm = yET - (ydur / mmTime) // mm - (sec / sec/mm) Units look correct! + $ymm[$x] = round( ( $yesterdaysET - ( $yesterdaysDuration / $mmProg ) ), 4 ) * (-1); + $tET[ int( $_1mmProg->{crop}[$x] ) ] = $ymm[$x]; + print "[$x yesterdaysET=$yesterdaysET yesterdaysDuration=$yesterdaysDuration mmProg=$mmProg ymm[$x] = " + . $ymm[$x] . " tET[" + . int( $_1mmProg->{crop}[$x] ) . "] = " + . $ymm[$x] . "]\n" + if ($debug); + print "E: $x $ymm[$x] = ( " . $yET[ $_1mmProg->{crop}[$x] ] . " ) - ( $ydur[$x] / " . $_1mmProg->{mmTime}[$x] . " ) * -1\n" if ($debug); + print "E: $x _1mmProg['crop'][$x] = " . $_1mmProg->{crop}[$x] . "\n" if ($debug); + print "E: $x tET[" . int( $_1mmProg->{crop}[$x] ) . "] = " . $tET[ int( $_1mmProg->{crop}[$x] ) ] . "\n" if ($debug); + } + else { + $ymm[$x] = 0; + } + } + + print "E: Done - mmFromLogs\n" if ($debug); + return ( \@ymm, \@tET ); +} + +sub writeResults { + my ( $ETG, $ETS, $sun, $todayRain, $tadjust, $noWater, $logsPath, $ETPath, $WPPath ) = @_; + + my @ET = ( $ETG, $ETS ); + my $msg; + my $pid = 2; #for legacy purposes, can probably remove it, but for now want the log files + #to be similar to keep the file structure the same to validate against python + my $data_1mm; + + # Get 1mm & crop data from config_parms + @{ $data_1mm->{mmTime} } = split( /,/, $config_parms{eto_zone_1mm} ); + @{ $data_1mm->{crop} } = split( /,/, $config_parms{eto_zone_crop} ); + + my @minmax; + if ( defined $config_parms{eto_minmax} ) { + @minmax = split( /,/, $config_parms{eto_minmax} ); + } + else { + @minmax = ( 5, 15 ); + } + + my $fname = int( (time) / 86400 ); + + my $minRunmm = 5; + $minRunmm = min @minmax if ( scalar(@minmax) > 0 ) and ( ( min @minmax ) >= 0 ); + my $maxRunmm = 15; + $maxRunmm = max @minmax if ( scalar(@minmax) > 1 ) and ( ( max @minmax ) >= $minRunmm ); + my $times = 0; + + my ( $ymm, $yET ) = mmFromLogs( $data_1mm, $logsPath, $ETPath ); + + print "ymm = " . join( ',', @$ymm ) . "\n" if ($debug); + print "yET = " . join( ',', @$yET ) . "\n" if ($debug); + + my @tET = [0] x scalar(@ET); + + for ( my $x = 0; $x < scalar(@ET); $x++ ) { + print "[ET[$x] = $ET[$x] yET[$x] = @$yET[$x]]\n" if ($debug); + $ET[$x] -= @$yET[$x]; + } + + my @runTime = (); + for ( my $x = 0; $x < scalar( @{ $data_1mm->{mmTime} } ); $x++ ) { + my $aET = safe_float( $ET[ $data_1mm->{crop}[$x] ] - $todayRain - @$ymm[$x] - $tadjust ); # tadjust is global ? + my $pretimes = $times; + + #HP TODO This will determine if a 2nd, 3rd or 4th time is required. + $times = 1 if ( ( $aET / $minRunmm ) > 1 ); #if the minium threshold is met, then run at least once. + $times = int( max( min( $aET / $maxRunmm, 4 ), $times ) ); # int(.999999) = 0 + print "[calc_eto] DB: times=$times aET=$aET minRunm=$minRunmm maxRunm=$maxRunmm\n" if ($debug); + print "E: aET[$x] = $aET (" . $aET / $maxRunmm . ") // mm/Day\n" if ($debug); + print "E: times = $times (max " + . max( min( $aET / $maxRunmm, 4 ), $times ) . "/min " + . min( $aET / $maxRunmm, 4 ) + . " max(min(" + . $aET / $maxRunmm + . ", 4), $pretimes))\n" + if ($debug); + # + # + # @FIXME: this is way too hard to read + + # runTime.append(min(max(safe_int(data['mmTime'][x] * ((aET if aET >= minRunmm else 0)) * (not noWater)), 0), \ + # safe_int(data['mmTime'][x]) * maxRunmm)) + my $tminrun = safe_int( $data_1mm->{mmTime}[$x] ); + $tminrun = 0 unless $aET >= $minRunmm; + $tminrun = int( $tminrun * $aET ); + $tminrun = 0 if $noWater; + my $tmaxrun = safe_int( $data_1mm->{mmTime}[$x] ) * $maxRunmm; + print "E: HP mmTime = " . $data_1mm->{mmTime}[$x] . " tminrun=$tminrun tmaxrum=$tmaxrun\n" if ($debug); + push( @runTime, min( $tminrun, $tmaxrun ) ); + } + + # ######################################### + # # Real logs will be written already ## + # ######################################### + + print "runTime count [" . scalar(@runTime) . "]\n" if ($debug); + if ( open( FILE, ">$logsPath/$fname" ) ) { + my $logData = "["; + for ( my $x = 0; $x < scalar(@runTime); $x++ ) { + for ( my $y = 0; $y < $times; $y++ ) { + my $delim = ""; + $delim = ", " unless ( ( $x == 0 ) and ( $y == 0 ) ); + $logData .= $delim . "[$pid, $x, " . $runTime[$x] . "]"; + } + } + $logData .= "]"; + print FILE $logData; + close(FILE); + } + else { + print_log "[calc_eto] ERROR Can't open log file $logsPath/$fname!"; + close(FILE); + } + + # ######################################### + # # Write final daily water balance ## + # ######################################### + if ( open( FILE, ">$ETPath/$fname" ) ) { + my $Data = "["; + for ( my $x = 0; $x < scalar(@ET); $x++ ) { + my $delim = ""; + $delim = ", " unless $x == 0; + $Data .= $delim . $ET[$x]; + } + $Data .= "]"; + print FILE $Data; + close(FILE); + } + else { + print_log "[calc_eto] ERROR Can't open ET file $ETPath/$fname!"; + close(FILE); + } + + # ########################################## + + #HP - ok, this is explained by the opensprinker setup, a program can have up to 4 runtimes + #HP - useful to avoid grass saturation. So if really dry and needs lots of moisture, then run + #HP - multiple programs + #HP - also set the number of times to water to 0 if $noWater is set. + $times = 0 if ($noWater); + + my @startTime = (-1) x 4; + my @availTimes = ( $sun->{rise} - sum(@runTime) / 60, $sun->{rise} + 60, $sun->{set} - sum(@runTime) / 60, $sun->{set} + 60 ); + + #if the current time is after $sun->{rise} then add two more options to $sun->{set} + if (time_greater_than($Time_Sunrise)) { + print_log "[calc_eto] It's after sunrise, so run extra programs at night"; + @availTimes = ($sun->{set} - sum(@runTime) / 60, $sun->{set} + 60, $sun->{set} + 120, $sun->{set} - (sum(@runTime) / 60) - 60 ); + } + + print "[times=$times, sun->{rise}=" . $sun->{rise} . " sum=" . sum(@runTime) / 60 . "]\n" if ($debug); + + for ( my $i = 0; $i < $times; $i++ ) { + $startTime[$i] = int( $availTimes[$i] ); + } + my $runTime_str = "[[" . join( ',', @startTime ) . "],[" . join( ',', @runTime ) . "]]"; + $msg = "[calc_eto] Current logged ET [" . join( ',', @ET ) . "]"; + print_log $msg; + $msg_string .= $msg . "\n"; + $msg = "[calc_eto] Current 1mm times [" . join( ',', @{ $data_1mm->{mmTime} } ) . "]"; + print_log $msg; + $msg_string .= $msg . "\n"; + $msg = "[calc_eto] Current Calc time $runTime_str"; + print_log $msg; + $msg_string .= $msg . "\n"; + + if ( open( FILE, ">$WPPath/run" ) ) { + ; + print FILE $runTime_str; + close(FILE); + } + else { + print_log "[calc_eto] ERROR Can't open run file $WPPath/run!"; + close(FILE); + } + return $runTime_str; +} + +# -[ Data ]--------------------------------------------------------------------- + +sub writewuData { + my ( $wuData, $noWater, $wuDataPath ) = @_; + my $fname = int( ( time - ( time % 86400 ) - 1 ) / 86400 ); + if ( open( FILE, ">$wuDataPath/$fname" ) ) { + print FILE "observation_epoch, " . $wuData->{currently}->{time} . "\n"; + print FILE "weather, " . $wuData->{currently}->{summary} . "\n"; + print FILE "temp_c, " . $wuData->{currently}->{temperature} . "\n"; + print FILE "relative_humidity, " . $wuData->{currently}->{humidity} . "\n"; + print FILE "wind_degrees, " . $wuData->{currently}->{windBearing} . "\n"; + print FILE "wind_speed, " . $wuData->{currently}->{windSpeed} . "\n"; + print FILE "precip_change, " . $wuData->{currently}->{precipProbability} . "\n"; + print FILE "precip_today_metric, " . $wuData->{currently}->{precipIntensity} . "\n"; + print FILE "noWater, " . $noWater . "\n"; + close(FILE); + } + else { + print_log "[calc_eto] WARNING Can't open wuData file $wuDataPath/$fname for writing!\n"; + } +} + +#calc_eto_runtimes(".","wu",$config_parms{eto_location},$config_parms{wu_key}); +sub calc_eto_runtimes { + my ( $datadir, $method, $loc, $arg1, $arg2 ) = @_; + my ( $rt, $data ); + my $success = 0; + if ( lc $method eq "file" ) { + if ( open( my $fh, "$arg1" ) ) { + local $/; #otherwise raw_data is empty? + my $raw_data = <$fh>; + + # eval { $data = JSON::XS->new->decode($raw_data) }; + eval { $data = decode_json($raw_data) }; + if ($@) { + print_log "[calc_eto] ERROR Problem parsing data $arg1! $@\n"; + } + else { + $success = 1; + } + close($fh); + } + else { + print_log "[calc_eto] ERROR Problem opening data $arg1\n"; + close($fh); + } + } + elsif ( lc $method eq "wu" ) { + $data = getwuData( $loc, $arg1 ); + $success = 1 if ($data); + } + if ($success) { + $rt = main_calc_eto( $datadir, $loc, $data ); + } + else { + print_log "[calc_eto] ERROR Data not available.\n"; + } + return $rt; +} + +sub detailSchedule { + my ($stime) = @_; + my ($times, $lengths) = $stime =~ /\[\[(.*)\],\[(.*)\]\]/; + my $msg = ""; + my $total_time = 0; + foreach my $time (split /,/, $times) { + next if ($time == -1); + my $station_id = 1; + $time = $time * 60; #add in seconds + foreach my $station (split /,/, $lengths) { + $total_time += $station; + my $run_hour = 0; + if ($station > 3600) { + $run_hour = int($station / 3600); + $station = int($station % 3600); + } + my $run_min = int($station / 60); + my $run_sec = int($station % 60); + $msg .= "[calc_eto] : " . formatTime($time) . " : Station:" .sprintf("%2s",$station_id) . " Run Time:" .sprintf("%02d:%02d:%02d",$run_hour,$run_min,$run_sec) . "\n" unless ($station == 0); + $station_id++; + $time += $run_sec + ($run_min * 60) + ($run_hour * 3600); + } + if ($total_time > 0) { + my $t_hours = 0; + if ($total_time > 3600) { + $t_hours = int($total_time / 3600); + $total_time = int($total_time % 3600); + } + my $t_min = int($total_time / 60); + my $t_sec = int($total_time % 60); + $msg .= "[calc_eto] : Total Run Time: " . sprintf("%02d:%02d:%02d",$t_hours,$t_min,$t_sec) . "\n"; + } + } + return ($msg); + + sub formatTime { + my ($t) = @_; + my $hour = int($t / 3600); + my $min = int(($t % 3600) / 60); + my $sec = int(($t % 3600) % 60); + my $ampm = "AM"; + if ($hour > 12) { + $ampm = "PM"; + $hour = $hour - 12; + } + return(sprintf("%2s:%02d:%02d",$hour,$min,$sec) . " $ampm"); + } +} + +sub main_calc_eto { + my ( $datadir, $loc, $wuData ) = @_; + + # -[ Init ]--------------------------------------------------------------------- + $msg_string = ""; + my $msg; + $datadir .= '/' unless ( ( substr( $datadir, -1 ) eq "/" ) or ( substr( $datadir, -1 ) eq "\\" ) ); + my $logsPath = $datadir . 'logs'; + my $ETPath = $datadir . 'ET'; + my $wuDataPath = "$config_parms{data_dir}/wuData"; + my $WPPath = $datadir . 'weatherprograms'; + + my $tzone; + + my $rainfallsatpoint = 25; + $rainfallsatpoint = $config_parms{eto_rainfallsatpoint} if ( defined $config_parms{eto_rainfallsatpoint} ); + +######################################### +## We need your latitude and longitude ## +## Let's try to get it with no api call## +######################################### + + # Hey we were given what we needed let's work with it + our ( $lat, $t1, $lon ) = $loc =~ /^([-+]?\d{1,2}([.]\d+)?),\s*([-+]?\d{1,3}([.]\d+)?)$/; + $lat = "None" unless ($lat); + $lon = "None" unless ($lon); + + # We got a 5+4 zip code, we only need the 5 + $loc =~ s/\-\d\d\d\d//; + # + + # We got a pws id, we don't need to tell wunderground, + # they know how to deal with the id numbers + $loc =~ s/'pws:'//; + # + + # Okay we finally have our loc ready to look up + my $noData = 0; + my ( $whttyp, $ploc ); + if ( $lat eq "None" and $lon eq "None" ) { + ( $whttyp, $ploc, $noData, $tzone, $lat, $lon ) = findwuLocation($loc); + } + + # Okay if all went well we got what we needed and snuck in a few more items we'll store those somewhere + + if ( $lat and $lon ) { + if ( $lat and $lon and $whttyp and $ploc ) { + print_log "[calc_eto] INFO For the $whttyp named: $ploc the lat, lon is: $lat, $lon, and the timezone is $tzone"; + } + else { + print_log "[calc_eto] INFO Resolved your lat:$lat, lon:$lon"; + } + $loc = $lat . ',' . $lon; + } + else { + if ($noData) { + print_log "[calc_eto] ERROR couldn't reach Weather Underground check connection"; + } + else { + print_log "[calc_eto] ERROR $loc can't resolved try another location"; + } + } + + # -[ Main ]--------------------------------------------------------------------- + + our ($offsets) = getwuDataTZOffset( $wuData, $tzone ); + + unless ($wuData) { + print_log "[calc_eto] ERROR WU data appears to be empty, exiting"; + return "[[-1,-1,-1,-1],[0]]"; + } + + # Calculate an adjustment based on predicted rainfall + my $tadjust = getForecastData( $wuData ); + my $sun = getAstronomyData( $wuData ); + my ( $todayRain, $noWater, $whyNot ) = + getConditionsData( $wuData->{currently}, $wuData->{daily}->{data}[0], $conditions ); + +######################## Quick Ref Names For wuData ######################################## + my $hist = $wuData->{daily}->{data}[0]; + +########################### Required Data ################################################## + $lat = safe_float($lat); + my $tmin = safe_float( $hist->{temperatureMin} ); + my $tmax = safe_float( $hist->{temperatureMax} ); + my $tmean = ( $tmin + $tmax ) / 2; + my $alt = 0; + $alt = $config_parms{eto_calc_time} if (defined $config_parms{eto_calc_time}); + my $tdew = safe_float( $hist->{dewPoint} ); + + my ($cday,$cmon,$cyear) = (localtime($hist->{time}))[3,4,5]; + + if ($cday == undef || $cmon == undef || $cyear == undef) { + #problem with the data + my $msg = "[calc_eto] ERROR: Bad Data received from Provider. A date field is empty"; + print_log $msg; + my $msg2 = "[calc_eto] ERROR: Undefined Parameter: Year=[$cyear] Month=[$cmon] Day=[$cday]"; + print_log $msg2; + if ( defined $config_parms{eto_email} ) { + print_log "[calc_eto] Emailing Error"; + net_mail_send( to => $config_parms{eto_email}, subject => "EvapoTranspiration Failed to retrieve data", text => $msg . "\n" . $msg2 ); + } + return "[[-1,-1,-1,-1],[0]]"; + } + $cmon++; #timelocal months start at 0 + $cyear += 1900; + my $doy = Day_of_Year( $cyear, $cmon, $cday ); + my $sun_hours = sun_block( $wuData, $sun->{rise}, $sun->{set}, $conditions ); + my ($rh_min, $rh_max, $rh_mean, $meanwindspeed) = getHourlyElements($wuData); + #my $rh_min = safe_float( $hist->{minhumidity} ); + #my $rh_max = safe_float( $hist->{maxhumidity} ); + #my $rh_mean = ( $rh_min + $rh_max ) / 2; + #my $meanwindspeed = safe_float( $hist->{meanwindspdm} ); + my $rainfall = min( safe_float( $hist->{precipIntensity} ), safe_float($rainfallsatpoint) ); + +############################################################################################ +## Calculations ## +############################################################################################ + # Calc Rn + print + "pl1 [lat=$lat,tmin=$tmin,tmax=$tmax,tmean=$tmean,alt=$alt,tdew=$tdew,doy=$doy,shour=$sun_hours,rmin=$rh_min,rmax=$rh_max,$meanwindspeed,$rainfall,$rainfallsatpoint]\n" + if ($debug); + my $e_tmin = &eto::delta_sat_vap_pres($tmin); + my $e_tmax = &eto::delta_sat_vap_pres($tmax); + my $sd = &eto::sol_dec($doy); + my $sha = &eto::sunset_hour_angle( $lat, $sd ); + my $dl_hours = &eto::daylight_hours($sha); + my $irl = &eto::inv_rel_dist_earth_sun($doy); + my $etrad = &eto::et_rad( $lat, $sd, $sha, $irl ); + my $cs_rad = &eto::clear_sky_rad( $alt, $etrad ); + my $Ra = ""; + + print "pl2 [e_tmin=$e_tmin e_tmax=$e_tmax sd=$sd sha=$sha dl_hours=$dl_hours irl=$irl etrad=$etrad cs_rad=$cs_rad]\n" if ($debug); + + my $sol_rad = &eto::sol_rad_from_sun_hours( $dl_hours, $sun_hours, $etrad ); + $sol_rad = &eto::sol_rad_from_t( $etrad, $cs_rad, $tmin, $tmax ) unless ($sol_rad); + unless ($sol_rad) { + print_log "[calc_eto] INFO Data for Penman-Monteith ETo not available reverting to Hargreaves ETo\n"; + + # Calc Ra + $Ra = $etrad; + print_log "[calc_eto] WARNING Not enough data to complete calculations" unless ($Ra); + } + + $msg = "[calc_eto] RESULTS Sun hours today: $sun_hours"; # tomorrow+2 days forecast rain + print_log $msg; + $msg_string .= $msg . "\n"; + + my $ea = &eto::ea_from_tdew($tdew); + $ea = &eto::ea_from_tmin($tmin) unless ($ea); + $ea = &eto::ea_from_rhmin_rhmax( $e_tmin, $e_tmax, $rh_min, $rh_max ) unless ($ea); + $ea = &eto::ea_from_rhmax( $e_tmin, $rh_max ) unless ($ea); + $ea = &eto::ea_from_rhmean( $e_tmin, $e_tmax, $rh_mean ) unless ($ea); + print_log "[calc_eto] INFO Failed to set actual vapor pressure" unless ($ea); + + my $ni_sw_rad = &eto::net_in_sol_rad($sol_rad); + my $no_lw_rad = &eto::net_out_lw_rad( $tmin, $tmax, $sol_rad, $cs_rad, $ea ); + my $Rn = &eto::net_rad( $ni_sw_rad, $no_lw_rad ); + + # Calc t + + my $t = ( $tmin + $tmax ) / 2; + + # Calc ws (wind speed) + + my $ws = &eto::wind_speed_2m( $meanwindspeed, 10 ); + + # Calc es + + my $es = &eto::mean_es( $tmin, $tmax ); + + print "pl3 [sol_rad=$sol_rad ra=$Ra ea=$ea ni_sw_rad=$ni_sw_rad no_lw_rad=$no_lw_rad rn=$Rn t=$t ws=$ws es=$es]\n" if ($debug); + + # ea done in Rn calcs + # Calc delta_es + + my $delta_es = &eto::delta_sat_vap_pres($t); + + # Calc psy + + my $atmospres = &eto::atmos_pres($alt); + my $psy = &eto::psy_const($atmospres); + print "pl4 [delta_es=$delta_es atmospres=$atmospres psy=$psy]\n" if ($debug); +############################## Print Results ################################### + + $msg = "[calc_eto] RESULTS " . round( $tadjust, 4 ) . " mm precipitation forecast for next 3 days"; # tomorrow+2 days forecast rain + print_log $msg; + $msg_string .= $msg . "\n"; + $msg = "[calc_eto] RESULTS " . round( $todayRain, 4 ) . " mm precipitation fallen and forecast for today"; # rain fallen today + forecast rain for today + print_log $msg; + $msg_string .= $msg . "\n"; + + #write to the RRD if it's enabled + if ($rrd ne "") { + $msg = '[calc_eto] Writing fallen and forecast rain to RRD: ' . round( $todayRain, 4 ) . " mm"; + $Weather{RainTotal} = round( $todayRain, 4 ); + print_log $msg; + $msg_string .= $msg . "\n"; + + } + + # Binary watering determination based on 3 criteria: 1)Currently raining 2)Wind>8kph~5mph 3)Temp<4.5C ~ 40F + if ($noWater) { + $msg = "[calc_eto] RESULTS We will not water because: $whyNot"; + print_log $msg; + $msg_string .= $msg . "\n"; + } + + my ( $ETdailyG, $ETdailyS ); + if ( not $Ra ) { + $ETdailyG = round( &eto::ETo( $Rn, $t, $ws, $es, $ea, $delta_es, $psy, 0 ) - $rainfall, 4 ); #ETo for most lawn grasses + $ETdailyS = round( &eto::ETo( $Rn, $t, $ws, $es, $ea, $delta_es, $psy, 1 ) - $rainfall, 4 ); #ETo for decorative grasses, most shrubs and flowers + $msg = "[calc_eto] RESULTS P-M ETo"; + print_log $msg; + $msg_string .= $msg . "\n"; + $msg = "[calc_eto] RESULTS $ETdailyG mm lost by grass"; + print_log $msg; + $msg_string .= $msg . "\n"; + $msg = "[calc_eto] RESULTS $ETdailyS mm lost by shrubs"; + print_log $msg; + $msg_string .= $msg . "\n"; + + } + else { + $ETdailyG = round( &eto::hargreaves_ETo( $tmin, $tmax, $tmean, $Ra ) - $rainfall, 4 ); + $ETdailyS = $ETdailyG; + $msg = "[calc_eto] RESULTS H ETo"; + print_log $msg; + $msg_string .= $msg . "\n"; + + $msg = "[calc_eto] RESULTS $ETdailyG mm lost today"; + print_log $msg; + $msg_string .= $msg . "\n"; + + } + + my $sr_hour = int($sun->{rise} / 60); + my $sr_min = int($sun->{rise} % 60); + my $ss_hour = int($sun->{set} / 60); + my $ss_min = int($sun->{set} % 60); + + $msg = "[calc_eto] RESULTS sunrise & sunset from midnight local time: $sr_hour:$sr_min (" . $sun->{rise} . ") $ss_hour:$ss_min (" . $sun->{set} . ")"; + print_log $msg; + $msg_string .= $msg . "\n"; + + my $stationID = $wuData->{latitude} . ',' . $wuData->{longitude}; + $msg = '[calc_eto] RESULTS Weather Location: ' . $stationID; + print_log $msg; + $msg_string .= $msg . "\n"; + + my $updateTime = scalar localtime($hist->{time}); + $msg = '[calc_eto] RESULTS Weather data updated: ' . $updateTime; + print_log $msg; + $msg_string .= $msg . "\n"; + + my ($rtime) = writeResults( $ETdailyG, $ETdailyS, $sun, $todayRain, $tadjust, $noWater, $logsPath, $ETPath, $WPPath ); + + #Write the WU data to a file. This can be used for the MH weather data and save an api call + writewuData( $wuData, $noWater, $wuDataPath ); + + #$msg = "[calc_eto] RESULTS Calculated Schedule: $rtime"; + #print_log $msg; + #$msg_string .= $msg . "\n"; + my $rtime2 = ""; + ($rtime2) = detailSchedule($rtime); + foreach my $detail (split /\n/,$rtime2) { + print_log $detail; + } + $msg_string .= $rtime2; + if ( defined $config_parms{eto_email} ) { + print_log "[calc_eto] Emailing results"; + net_mail_send( to => $config_parms{eto_email}, subject => "EvapoTranspiration Results for $Time_Now", text => $msg_string ); + } + return ($rtime); +} + From 30a101c11094e919cb7f800fa876113aafe9ca18 Mon Sep 17 00:00:00 2001 From: Brian M Date: Thu, 23 May 2019 09:04:02 -0700 Subject: [PATCH 70/78] Move led_level from Insteon::MicroSwitch to Insteon::BaseLight, as it is also works for 2476x/2477x switches and probably other devices --- lib/Insteon/Lighting.pm | 50 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 7d730bb4d..ca179c2b3 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -83,6 +83,32 @@ sub get_voice_cmds { return \%voice_cmds; } +=item C + +Sets the LED to brightness percentage. + +=cut + +sub led_level { + my ( $self, $level ) = @_; + return unless defined $level; + my $name = $self->get_object_name; + + ::print_log( "[Insteon::BaseLight] Setting LED level of $name to" . " $level." ) + if $self->debuglevel( 1, 'insteon' ); + + + #For whatever reason 100% = 127 and 50% = 64 + $level = $level * 1.28; + $level = 127 if $level > 127; + $level = 0 if $level < 0; + + my $extra = '000107' . sprintf( '%02X', $level ); + $extra .= '0' x ( 30 - length $extra ); + my $message = new Insteon::InsteonMessage( 'insteon_ext_send', $self, 'extended_set_get', $extra ); + $self->_send_cmd($message); +} + =back =head2 AUTHOR @@ -1367,30 +1393,6 @@ sub enable_beep_button { } } -=item C - -Sets the LED to brightness percentage. - -=cut - -sub led_level { - my ( $self, $level ) = @_; - return unless defined $level; - my $name = $self->get_object_name; - - ::print_log( "[Insteon::MicroSwitch] Setting LED level of $name to" . " $level." ); - - #For whatever reason 100% = 127 and 50% = 64 - $level = $level * 1.28; - $level = 127 if $level > 127; - $level = 0 if $level < 0; - - my $extra = '000107' . sprintf( '%02X', $level ); - $extra .= '0' x ( 30 - length $extra ); - my $message = new Insteon::InsteonMessage( 'insteon_ext_send', $self, 'extended_set_get', $extra ); - $self->_send_cmd($message); -} - =item C Returns a hash of voice commands where the key is the voice command name and the From e87cd580912bb2869afa4e62cd7a821c9f1e34ed Mon Sep 17 00:00:00 2001 From: Brian M Date: Thu, 23 May 2019 10:01:57 -0700 Subject: [PATCH 71/78] Add beep() method to Insteon::DimmableLight --- lib/Insteon/BaseInsteon.pm | 1 + lib/Insteon/Lighting.pm | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index ea2d5dc8d..b4d144e5d 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -1388,6 +1388,7 @@ our %message_types = ( poke_internal => 0x2d, extended_set_get => 0x2e, read_write_aldb => 0x2f, + beep => 0x30, imeter_reset => 0x80, imeter_query => 0x82, ); diff --git a/lib/Insteon/Lighting.pm b/lib/Insteon/Lighting.pm index 7d730bb4d..1afe17635 100644 --- a/lib/Insteon/Lighting.pm +++ b/lib/Insteon/Lighting.pm @@ -423,6 +423,24 @@ sub get_voice_cmds { =back +=item C + +Beep the device; + +=cut + +sub beep { + my ( $self ) = @_; + my $name = $self->get_object_name; + + ::print_log( "[Insteon::DimmableLight] Beeping $name." ) + if $self->debuglevel( 1, 'insteon' ); + + my $message = new Insteon::InsteonMessage( 'insteon_send', $self, 'beep', 0x00 ); + $self->_send_cmd($message); +} + + =head2 AUTHOR Gregg Limming From 42e2ffcd747dc2762208888653d66ea286f91fdd Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Tue, 28 May 2019 10:54:18 +0200 Subject: [PATCH 72/78] fix regulare expression to check content type This fixes the previous commit, which makes the propblem of false warnings actually worse. I'm sorry for the inconvenience, i really don't know how i couldn not notice this before creating the last pull requeste. --- lib/http_server.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index 5ce52e9cb..fe08fd61e 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -379,7 +379,7 @@ sub http_process_request { # Content-type: application/x-www-form-urlencoded # with a json body # - } elsif ($Http{'Content-Type'} =~ m%^application/(json\|x-www-form-urlencoded)%i && $HTTP_BODY =~ /^\{/) { + } elsif ($Http{'Content-Type'} =~ m%^application/(json|x-www-form-urlencoded)%i && $HTTP_BODY =~ /^\{/) { print "[http_server.pl]: posting json data\n" if $main::Debug{http}; } else { &main::print_log("[http_server.pl]: Warning, invalid argument string detected ($buf) ($Http{'Content-Type'}) ($HTTP_BODY)\n"); From 2460bb1fc4726783d6797d4623ec2e7e9bf905ea Mon Sep 17 00:00:00 2001 From: Brian M Date: Tue, 4 Jun 2019 22:14:43 -0700 Subject: [PATCH 73/78] Create fallback directory, and populate it with MH-supplied versions of CPAN modules from libe/site, so they can be overridden if desired. --- bin/mh | 4 ++++ lib/{site => fallback}/Astro/MoonPhase.pm | 0 lib/{site => fallback}/Authen/SASL.pm | 0 lib/{site => fallback}/Authen/SASL.pod | 0 lib/{site => fallback}/Authen/SASL/CRAM_MD5.pm | 0 lib/{site => fallback}/Authen/SASL/EXTERNAL.pm | 0 lib/{site => fallback}/Authen/SASL/Perl.pm | 0 lib/{site => fallback}/Authen/SASL/Perl.pod | 0 .../Authen/SASL/Perl/ANONYMOUS.pm | 0 lib/{site => fallback}/Authen/SASL/Perl/CRAM_MD5.pm | 0 .../Authen/SASL/Perl/DIGEST_MD5.pm | 0 lib/{site => fallback}/Authen/SASL/Perl/EXTERNAL.pm | 0 lib/{site => fallback}/Authen/SASL/Perl/LOGIN.pm | 0 lib/{site => fallback}/Authen/SASL/Perl/PLAIN.pm | 0 lib/{site => fallback}/CDDB.pm | 0 lib/{site => fallback}/Capture/Tiny.pm | 0 lib/{site => fallback}/Class/Accessor.pm | 0 .../Class/Accessor/Chained/Fast.pm | 0 lib/{site => fallback}/Class/Accessor/Fast.pm | 0 lib/{site => fallback}/Class/ErrorHandler.pm | 0 lib/{site => fallback}/Class/Singleton.pm | 0 lib/{site => fallback}/Class/XPath.pm | 0 lib/{site => fallback}/ControlX10/CM11.pm | 0 lib/{site => fallback}/ControlX10/CM11.pm.new | 0 lib/{site => fallback}/ControlX10/CM11.pm.old | 0 lib/{site => fallback}/ControlX10/CM11.txt | 0 lib/{site => fallback}/ControlX10/CM17.pm | 0 lib/{site => fallback}/ControlX10/CM17.pm.old | 0 lib/{site => fallback}/ControlX10/CM17.txt | 0 lib/{site => fallback}/Date/Format.pm | 0 lib/{site => fallback}/Date/Language.pm | 0 lib/{site => fallback}/Date/Language/Afar.pm | 0 lib/{site => fallback}/Date/Language/Amharic.pm | 0 lib/{site => fallback}/Date/Language/Austrian.pm | 0 lib/{site => fallback}/Date/Language/Brazilian.pm | 0 lib/{site => fallback}/Date/Language/Chinese_GB.pm | 0 lib/{site => fallback}/Date/Language/Czech.pm | 0 lib/{site => fallback}/Date/Language/Danish.pm | 0 lib/{site => fallback}/Date/Language/Dutch.pm | 0 lib/{site => fallback}/Date/Language/English.pm | 0 lib/{site => fallback}/Date/Language/Finnish.pm | 0 lib/{site => fallback}/Date/Language/French.pm | 0 lib/{site => fallback}/Date/Language/Gedeo.pm | 0 lib/{site => fallback}/Date/Language/German.pm | 0 lib/{site => fallback}/Date/Language/Greek.pm | 0 lib/{site => fallback}/Date/Language/Italian.pm | 0 lib/{site => fallback}/Date/Language/Norwegian.pm | 0 lib/{site => fallback}/Date/Language/Oromo.pm | 0 lib/{site => fallback}/Date/Language/Sidama.pm | 0 lib/{site => fallback}/Date/Language/Somali.pm | 0 lib/{site => fallback}/Date/Language/Swedish.pm | 0 lib/{site => fallback}/Date/Language/Tigrinya.pm | 0 .../Date/Language/TigrinyaEritrean.pm | 0 .../Date/Language/TigrinyaEthiopian.pm | 0 lib/{site => fallback}/Date/Parse.pm | 0 lib/{site => fallback}/DateTime.pm | 0 lib/{site => fallback}/DateTime/Duration.pm | 0 lib/{site => fallback}/DateTime/Event/ICal.pm | 0 lib/{site => fallback}/DateTime/Event/Recurrence.pm | 0 lib/{site => fallback}/DateTime/Format/ICal.pm | 0 lib/{site => fallback}/DateTime/Helpers.pm | 0 lib/{site => fallback}/DateTime/Infinite.pm | 0 lib/{site => fallback}/DateTime/LeapSecond.pm | 0 lib/{site => fallback}/DateTime/Locale/Base.pm | 0 lib/{site => fallback}/DateTime/Locale/aa.pm | 0 .../DateTime/Locale/aa_ER_SAAHO.pm | 0 lib/{site => fallback}/DateTime/Locale/af.pm | 0 lib/{site => fallback}/DateTime/Locale/af_ZA.pm | 0 lib/{site => fallback}/DateTime/Locale/ak.pm | 0 lib/{site => fallback}/DateTime/Locale/am.pm | 0 lib/{site => fallback}/DateTime/Locale/am_ET.pm | 0 lib/{site => fallback}/DateTime/Locale/ar.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_EG.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_JO.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_LB.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_QA.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_SA.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_SY.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_TN.pm | 0 lib/{site => fallback}/DateTime/Locale/ar_YE.pm | 0 lib/{site => fallback}/DateTime/Locale/as.pm | 0 lib/{site => fallback}/DateTime/Locale/as_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/az.pm | 0 lib/{site => fallback}/DateTime/Locale/az_Cyrl.pm | 0 lib/{site => fallback}/DateTime/Locale/be.pm | 0 lib/{site => fallback}/DateTime/Locale/bg.pm | 0 lib/{site => fallback}/DateTime/Locale/bg_BG.pm | 0 lib/{site => fallback}/DateTime/Locale/bn.pm | 0 lib/{site => fallback}/DateTime/Locale/bn_BD.pm | 0 lib/{site => fallback}/DateTime/Locale/bn_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/bs.pm | 0 lib/{site => fallback}/DateTime/Locale/byn.pm | 0 lib/{site => fallback}/DateTime/Locale/byn_ER.pm | 0 lib/{site => fallback}/DateTime/Locale/ca.pm | 0 lib/{site => fallback}/DateTime/Locale/cch.pm | 0 lib/{site => fallback}/DateTime/Locale/cs.pm | 0 lib/{site => fallback}/DateTime/Locale/cy.pm | 0 lib/{site => fallback}/DateTime/Locale/cy_GB.pm | 0 lib/{site => fallback}/DateTime/Locale/da.pm | 0 lib/{site => fallback}/DateTime/Locale/de.pm | 0 lib/{site => fallback}/DateTime/Locale/de_AT.pm | 0 lib/{site => fallback}/DateTime/Locale/de_BE.pm | 0 lib/{site => fallback}/DateTime/Locale/dz.pm | 0 lib/{site => fallback}/DateTime/Locale/ee.pm | 0 lib/{site => fallback}/DateTime/Locale/el.pm | 0 lib/{site => fallback}/DateTime/Locale/en.pm | 0 lib/{site => fallback}/DateTime/Locale/en_AU.pm | 0 lib/{site => fallback}/DateTime/Locale/en_BE.pm | 0 lib/{site => fallback}/DateTime/Locale/en_BW.pm | 0 lib/{site => fallback}/DateTime/Locale/en_BZ.pm | 0 lib/{site => fallback}/DateTime/Locale/en_CA.pm | 0 lib/{site => fallback}/DateTime/Locale/en_GB.pm | 0 lib/{site => fallback}/DateTime/Locale/en_HK.pm | 0 lib/{site => fallback}/DateTime/Locale/en_IE.pm | 0 lib/{site => fallback}/DateTime/Locale/en_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/en_MT.pm | 0 lib/{site => fallback}/DateTime/Locale/en_NZ.pm | 0 lib/{site => fallback}/DateTime/Locale/en_PH.pm | 0 lib/{site => fallback}/DateTime/Locale/en_PK.pm | 0 lib/{site => fallback}/DateTime/Locale/en_SG.pm | 0 lib/{site => fallback}/DateTime/Locale/en_ZA.pm | 0 lib/{site => fallback}/DateTime/Locale/en_ZW.pm | 0 lib/{site => fallback}/DateTime/Locale/eo.pm | 0 lib/{site => fallback}/DateTime/Locale/es.pm | 0 lib/{site => fallback}/DateTime/Locale/es_AR.pm | 0 lib/{site => fallback}/DateTime/Locale/es_BO.pm | 0 lib/{site => fallback}/DateTime/Locale/es_CL.pm | 0 lib/{site => fallback}/DateTime/Locale/es_CO.pm | 0 lib/{site => fallback}/DateTime/Locale/es_CR.pm | 0 lib/{site => fallback}/DateTime/Locale/es_DO.pm | 0 lib/{site => fallback}/DateTime/Locale/es_EC.pm | 0 lib/{site => fallback}/DateTime/Locale/es_ES.pm | 0 lib/{site => fallback}/DateTime/Locale/es_GT.pm | 0 lib/{site => fallback}/DateTime/Locale/es_HN.pm | 0 lib/{site => fallback}/DateTime/Locale/es_MX.pm | 0 lib/{site => fallback}/DateTime/Locale/es_NI.pm | 0 lib/{site => fallback}/DateTime/Locale/es_PA.pm | 0 lib/{site => fallback}/DateTime/Locale/es_PR.pm | 0 lib/{site => fallback}/DateTime/Locale/es_PY.pm | 0 lib/{site => fallback}/DateTime/Locale/es_SV.pm | 0 lib/{site => fallback}/DateTime/Locale/es_US.pm | 0 lib/{site => fallback}/DateTime/Locale/es_UY.pm | 0 lib/{site => fallback}/DateTime/Locale/es_VE.pm | 0 lib/{site => fallback}/DateTime/Locale/et.pm | 0 lib/{site => fallback}/DateTime/Locale/et_EE.pm | 0 lib/{site => fallback}/DateTime/Locale/eu.pm | 0 lib/{site => fallback}/DateTime/Locale/eu_ES.pm | 0 lib/{site => fallback}/DateTime/Locale/fi.pm | 0 lib/{site => fallback}/DateTime/Locale/fo.pm | 0 lib/{site => fallback}/DateTime/Locale/fr.pm | 0 lib/{site => fallback}/DateTime/Locale/fr_BE.pm | 0 lib/{site => fallback}/DateTime/Locale/fr_CA.pm | 0 lib/{site => fallback}/DateTime/Locale/fr_CH.pm | 0 lib/{site => fallback}/DateTime/Locale/fur.pm | 0 lib/{site => fallback}/DateTime/Locale/ga.pm | 0 lib/{site => fallback}/DateTime/Locale/ga_IE.pm | 0 lib/{site => fallback}/DateTime/Locale/gaa.pm | 0 lib/{site => fallback}/DateTime/Locale/gez.pm | 0 lib/{site => fallback}/DateTime/Locale/gez_ER.pm | 0 lib/{site => fallback}/DateTime/Locale/gez_ET.pm | 0 lib/{site => fallback}/DateTime/Locale/gl.pm | 0 lib/{site => fallback}/DateTime/Locale/gl_ES.pm | 0 lib/{site => fallback}/DateTime/Locale/gu.pm | 0 lib/{site => fallback}/DateTime/Locale/gu_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/gv.pm | 0 lib/{site => fallback}/DateTime/Locale/gv_GB.pm | 0 lib/{site => fallback}/DateTime/Locale/ha.pm | 0 lib/{site => fallback}/DateTime/Locale/ha_Arab.pm | 0 lib/{site => fallback}/DateTime/Locale/haw.pm | 0 lib/{site => fallback}/DateTime/Locale/haw_US.pm | 0 lib/{site => fallback}/DateTime/Locale/he.pm | 0 lib/{site => fallback}/DateTime/Locale/hi.pm | 0 lib/{site => fallback}/DateTime/Locale/hi_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/hr.pm | 0 lib/{site => fallback}/DateTime/Locale/hu.pm | 0 lib/{site => fallback}/DateTime/Locale/hy.pm | 0 lib/{site => fallback}/DateTime/Locale/hy_AM.pm | 0 .../DateTime/Locale/hy_AM_REVISED.pm | 0 lib/{site => fallback}/DateTime/Locale/ia.pm | 0 lib/{site => fallback}/DateTime/Locale/id.pm | 0 lib/{site => fallback}/DateTime/Locale/id_ID.pm | 0 lib/{site => fallback}/DateTime/Locale/ig.pm | 0 lib/{site => fallback}/DateTime/Locale/is.pm | 0 lib/{site => fallback}/DateTime/Locale/it.pm | 0 lib/{site => fallback}/DateTime/Locale/it_CH.pm | 0 lib/{site => fallback}/DateTime/Locale/it_IT.pm | 0 lib/{site => fallback}/DateTime/Locale/iu.pm | 0 lib/{site => fallback}/DateTime/Locale/ja.pm | 0 lib/{site => fallback}/DateTime/Locale/ka.pm | 0 lib/{site => fallback}/DateTime/Locale/kaj.pm | 0 lib/{site => fallback}/DateTime/Locale/kam.pm | 0 lib/{site => fallback}/DateTime/Locale/kcg.pm | 0 lib/{site => fallback}/DateTime/Locale/kfo.pm | 0 lib/{site => fallback}/DateTime/Locale/kk.pm | 0 lib/{site => fallback}/DateTime/Locale/kl.pm | 0 lib/{site => fallback}/DateTime/Locale/kl_GL.pm | 0 lib/{site => fallback}/DateTime/Locale/km.pm | 0 lib/{site => fallback}/DateTime/Locale/kn.pm | 0 lib/{site => fallback}/DateTime/Locale/kn_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/ko.pm | 0 lib/{site => fallback}/DateTime/Locale/ko_KR.pm | 0 lib/{site => fallback}/DateTime/Locale/kok.pm | 0 lib/{site => fallback}/DateTime/Locale/kok_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/kw.pm | 0 lib/{site => fallback}/DateTime/Locale/kw_GB.pm | 0 lib/{site => fallback}/DateTime/Locale/ln.pm | 0 lib/{site => fallback}/DateTime/Locale/lo.pm | 0 lib/{site => fallback}/DateTime/Locale/lt.pm | 0 lib/{site => fallback}/DateTime/Locale/lv.pm | 0 lib/{site => fallback}/DateTime/Locale/mk.pm | 0 lib/{site => fallback}/DateTime/Locale/ml.pm | 0 lib/{site => fallback}/DateTime/Locale/ml_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/mn.pm | 0 lib/{site => fallback}/DateTime/Locale/mr.pm | 0 lib/{site => fallback}/DateTime/Locale/mr_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/ms.pm | 0 lib/{site => fallback}/DateTime/Locale/ms_BN.pm | 0 lib/{site => fallback}/DateTime/Locale/ms_MY.pm | 0 lib/{site => fallback}/DateTime/Locale/mt.pm | 0 lib/{site => fallback}/DateTime/Locale/nb.pm | 0 lib/{site => fallback}/DateTime/Locale/ne.pm | 0 lib/{site => fallback}/DateTime/Locale/nl.pm | 0 lib/{site => fallback}/DateTime/Locale/nl_BE.pm | 0 lib/{site => fallback}/DateTime/Locale/nn.pm | 0 lib/{site => fallback}/DateTime/Locale/nr.pm | 0 lib/{site => fallback}/DateTime/Locale/nso.pm | 0 lib/{site => fallback}/DateTime/Locale/ny.pm | 0 lib/{site => fallback}/DateTime/Locale/om.pm | 0 lib/{site => fallback}/DateTime/Locale/om_ET.pm | 0 lib/{site => fallback}/DateTime/Locale/om_KE.pm | 0 lib/{site => fallback}/DateTime/Locale/pa.pm | 0 lib/{site => fallback}/DateTime/Locale/pa_Arab.pm | 0 lib/{site => fallback}/DateTime/Locale/pa_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/pl.pm | 0 lib/{site => fallback}/DateTime/Locale/pt.pm | 0 lib/{site => fallback}/DateTime/Locale/pt_BR.pm | 0 lib/{site => fallback}/DateTime/Locale/pt_PT.pm | 0 lib/{site => fallback}/DateTime/Locale/ro.pm | 0 lib/{site => fallback}/DateTime/Locale/root.pm | 0 lib/{site => fallback}/DateTime/Locale/ru.pm | 0 lib/{site => fallback}/DateTime/Locale/ru_UA.pm | 0 lib/{site => fallback}/DateTime/Locale/rw.pm | 0 lib/{site => fallback}/DateTime/Locale/sid.pm | 0 lib/{site => fallback}/DateTime/Locale/sk.pm | 0 lib/{site => fallback}/DateTime/Locale/sl.pm | 0 lib/{site => fallback}/DateTime/Locale/so.pm | 0 lib/{site => fallback}/DateTime/Locale/sq.pm | 0 lib/{site => fallback}/DateTime/Locale/sr.pm | 0 lib/{site => fallback}/DateTime/Locale/sr_Cyrl.pm | 0 .../DateTime/Locale/sr_Cyrl_BA.pm | 0 lib/{site => fallback}/DateTime/Locale/sr_Latn.pm | 0 .../DateTime/Locale/sr_Latn_BA.pm | 0 lib/{site => fallback}/DateTime/Locale/ss.pm | 0 lib/{site => fallback}/DateTime/Locale/st.pm | 0 lib/{site => fallback}/DateTime/Locale/sv.pm | 0 lib/{site => fallback}/DateTime/Locale/sv_SE.pm | 0 lib/{site => fallback}/DateTime/Locale/sw.pm | 0 lib/{site => fallback}/DateTime/Locale/syr.pm | 0 lib/{site => fallback}/DateTime/Locale/syr_SY.pm | 0 lib/{site => fallback}/DateTime/Locale/ta.pm | 0 lib/{site => fallback}/DateTime/Locale/ta_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/te.pm | 0 lib/{site => fallback}/DateTime/Locale/te_IN.pm | 0 lib/{site => fallback}/DateTime/Locale/tg.pm | 0 lib/{site => fallback}/DateTime/Locale/th.pm | 0 lib/{site => fallback}/DateTime/Locale/ti.pm | 0 lib/{site => fallback}/DateTime/Locale/ti_ER.pm | 0 lib/{site => fallback}/DateTime/Locale/ti_ET.pm | 0 lib/{site => fallback}/DateTime/Locale/tig.pm | 0 lib/{site => fallback}/DateTime/Locale/tig_ER.pm | 0 lib/{site => fallback}/DateTime/Locale/tn.pm | 0 lib/{site => fallback}/DateTime/Locale/tr.pm | 0 lib/{site => fallback}/DateTime/Locale/ts.pm | 0 lib/{site => fallback}/DateTime/Locale/uk.pm | 0 lib/{site => fallback}/DateTime/Locale/uz.pm | 0 lib/{site => fallback}/DateTime/Locale/uz_Arab.pm | 0 lib/{site => fallback}/DateTime/Locale/uz_Latn.pm | 0 lib/{site => fallback}/DateTime/Locale/ve.pm | 0 lib/{site => fallback}/DateTime/Locale/vi.pm | 0 lib/{site => fallback}/DateTime/Locale/wal.pm | 0 lib/{site => fallback}/DateTime/Locale/wal_ET.pm | 0 lib/{site => fallback}/DateTime/Locale/xh.pm | 0 lib/{site => fallback}/DateTime/Locale/yo.pm | 0 lib/{site => fallback}/DateTime/Locale/zh.pm | 0 .../DateTime/Locale/zh_Hans_CN.pm | 0 .../DateTime/Locale/zh_Hans_SG.pm | 0 lib/{site => fallback}/DateTime/Locale/zh_Hant.pm | 0 .../DateTime/Locale/zh_Hant_HK.pm | 0 .../DateTime/Locale/zh_Hant_MO.pm | 0 lib/{site => fallback}/DateTime/Locale/zu.pm | 0 lib/{site => fallback}/DateTime/Set.pm | 0 lib/{site => fallback}/DateTime/Span.pm | 0 lib/{site => fallback}/DateTime/SpanSet.pm | 0 lib/{site => fallback}/DateTime/TimeZone.pm | 0 lib/{site => fallback}/DateTimePP.pm | 0 lib/{site => fallback}/DateTimePPExtra.pm | 0 lib/{site => fallback}/Device/Changes | 0 .../Device/Device-SerialPort.html | 0 lib/{site => fallback}/Device/README | 0 lib/{site => fallback}/Device/SerialPort.pm | 0 lib/{site => fallback}/Digest/HMAC.pm | 0 lib/{site => fallback}/Digest/HMAC_MD5.pm | 0 lib/{site => fallback}/Digest/HMAC_SHA1.pm | 0 lib/{site => fallback}/Digest/MD2.pm | 0 lib/{site => fallback}/Digest/MD5.pm | 0 lib/{site => fallback}/Digest/Perl/MD5.pm | 0 lib/{site => fallback}/Digest/SHA1.pm | 0 lib/{site => fallback}/Digest/base.pm | 0 lib/{site => fallback}/Email/Date/Format.pm | 0 lib/{site => fallback}/File/Listing.pm | 0 lib/{site => fallback}/File/Which.pm | 0 lib/{site => fallback}/Geo/METAR.pm | 0 lib/{site => fallback}/Geo/Weather.pm | 0 lib/{site => fallback}/Geo/WeatherNOAA.pm | 0 lib/{site => fallback}/Geo/WeatherNOAA.pm.old | 0 lib/{site => fallback}/HTML/AsSubs.pm | 0 lib/{site => fallback}/HTML/Element.pm | 0 lib/{site => fallback}/HTML/Entities.pm | 0 lib/{site => fallback}/HTML/Filter.pm | 0 lib/{site => fallback}/HTML/Form.pm | 0 lib/{site => fallback}/HTML/FormatMarkdown.pm | 0 lib/{site => fallback}/HTML/FormatPS.pm | 0 lib/{site => fallback}/HTML/FormatRTF.pm | 0 lib/{site => fallback}/HTML/FormatText.pm | 0 lib/{site => fallback}/HTML/Formatter.pm | 0 lib/{site => fallback}/HTML/HeadParser.pm | 0 lib/{site => fallback}/HTML/LinkExtor.pm | 0 lib/{site => fallback}/HTML/Parse.pm | 0 lib/{site => fallback}/HTML/Parser.pm | 0 lib/{site => fallback}/HTML/TableExtract.pm | 0 lib/{site => fallback}/HTML/Tagset.pm | 0 lib/{site => fallback}/HTML/TokeParser.pm | 0 lib/{site => fallback}/HTML/TreeBuilder.pm | 0 lib/{site => fallback}/HTTP/Cookies.pm | 0 lib/{site => fallback}/HTTP/Cookies/Microsoft.pm | 0 lib/{site => fallback}/HTTP/Cookies/Netscape.pm | 0 lib/{site => fallback}/HTTP/Daemon.pm | 0 lib/{site => fallback}/HTTP/Date.pm | 0 lib/{site => fallback}/HTTP/Headers.pm | 0 lib/{site => fallback}/HTTP/Headers/Auth.pm | 0 lib/{site => fallback}/HTTP/Headers/ETag.pm | 0 lib/{site => fallback}/HTTP/Headers/Util.pm | 0 lib/{site => fallback}/HTTP/Message.pm | 0 lib/{site => fallback}/HTTP/Negotiate.pm | 0 lib/{site => fallback}/HTTP/Request.pm | 0 lib/{site => fallback}/HTTP/Request/Common.pm | 0 lib/{site => fallback}/HTTP/Response.pm | 0 lib/{site => fallback}/HTTP/Status.pm | 0 .../Hardware/iButton/Connection.pm | 0 lib/{site => fallback}/Hardware/iButton/Device.pm | 0 .../Hardware/iButton/old/Connection.pm | 0 .../Hardware/iButton/old/Device.pm | 0 lib/{site => fallback}/IO/Interface.pm | 0 lib/{site => fallback}/IO/Interface/Simple.pm | 0 lib/{site => fallback}/JSON.pm | 0 lib/{site => fallback}/JSON/PP.pm | 0 lib/{site => fallback}/JSON/PP/Boolean.pm | 0 lib/{site => fallback}/JSON/PP5005.pm | 0 lib/{site => fallback}/JSON/PP56.pm | 0 lib/{site => fallback}/JSON/PP58.pm | 0 lib/{site => fallback}/LWP.pm | 0 lib/{site => fallback}/LWP/Authen/Basic.pm | 0 lib/{site => fallback}/LWP/Authen/Digest.pm | 0 lib/{site => fallback}/LWP/Authen/Ntlm.pm | 0 lib/{site => fallback}/LWP/ConnCache.pm | 0 lib/{site => fallback}/LWP/Debug.pm | 0 lib/{site => fallback}/LWP/DebugFile.pm | 0 lib/{site => fallback}/LWP/MediaTypes.pm | 0 lib/{site => fallback}/LWP/MemberMixin.pm | 0 lib/{site => fallback}/LWP/Protocol.pm | 0 lib/{site => fallback}/LWP/Protocol/GHTTP.pm | 0 lib/{site => fallback}/LWP/Protocol/cpan.pm | 0 lib/{site => fallback}/LWP/Protocol/data.pm | 0 lib/{site => fallback}/LWP/Protocol/file.pm | 0 lib/{site => fallback}/LWP/Protocol/ftp.pm | 0 lib/{site => fallback}/LWP/Protocol/gopher.pm | 0 lib/{site => fallback}/LWP/Protocol/http.pm | 0 lib/{site => fallback}/LWP/Protocol/http10.pm | 0 lib/{site => fallback}/LWP/Protocol/https.pm | 0 lib/{site => fallback}/LWP/Protocol/https10.pm | 0 lib/{site => fallback}/LWP/Protocol/loopback.pm | 0 lib/{site => fallback}/LWP/Protocol/mailto.pm | 0 lib/{site => fallback}/LWP/Protocol/nntp.pm | 0 lib/{site => fallback}/LWP/Protocol/nogo.pm | 0 lib/{site => fallback}/LWP/RobotUA.pm | 0 lib/{site => fallback}/LWP/Simple.pm | 0 lib/{site => fallback}/LWP/UserAgent.pm | 0 lib/{site => fallback}/LWP/media.types | 0 lib/{site => fallback}/Lingua/EN/Numbers.pm | 0 lib/{site => fallback}/Lingua/ES/Numeros.pm | 0 lib/{site => fallback}/Lingua/FR/Numbers.pm | 0 lib/{site => fallback}/Lingua/IT/Numbers.pm | 0 lib/{site => fallback}/Lingua/Num2Word.pm | 0 lib/{site => fallback}/MIME/Lite.pm | 0 lib/{site => fallback}/Net/AOLIM.pm | 0 lib/{site => fallback}/Net/Cmd.pm | 0 lib/{site => fallback}/Net/Config.pm | 0 lib/{site => fallback}/Net/DNS.pm | 0 lib/{site => fallback}/Net/DNS/Header.pm | 0 lib/{site => fallback}/Net/DNS/Packet.pm | 0 lib/{site => fallback}/Net/DNS/Question.pm | 0 lib/{site => fallback}/Net/DNS/RR.pm | 0 lib/{site => fallback}/Net/DNS/RR/A.pm | 0 lib/{site => fallback}/Net/DNS/RR/AAAA.pm | 0 lib/{site => fallback}/Net/DNS/RR/AFSDB.pm | 0 lib/{site => fallback}/Net/DNS/RR/CNAME.pm | 0 lib/{site => fallback}/Net/DNS/RR/EID.pm | 0 lib/{site => fallback}/Net/DNS/RR/HINFO.pm | 0 lib/{site => fallback}/Net/DNS/RR/ISDN.pm | 0 lib/{site => fallback}/Net/DNS/RR/LOC.pm | 0 lib/{site => fallback}/Net/DNS/RR/MB.pm | 0 lib/{site => fallback}/Net/DNS/RR/MG.pm | 0 lib/{site => fallback}/Net/DNS/RR/MINFO.pm | 0 lib/{site => fallback}/Net/DNS/RR/MR.pm | 0 lib/{site => fallback}/Net/DNS/RR/MX.pm | 0 lib/{site => fallback}/Net/DNS/RR/NAPTR.pm | 0 lib/{site => fallback}/Net/DNS/RR/NIMLOC.pm | 0 lib/{site => fallback}/Net/DNS/RR/NS.pm | 0 lib/{site => fallback}/Net/DNS/RR/NSAP.pm | 0 lib/{site => fallback}/Net/DNS/RR/NULL.pm | 0 lib/{site => fallback}/Net/DNS/RR/PTR.pm | 0 lib/{site => fallback}/Net/DNS/RR/PX.pm | 0 lib/{site => fallback}/Net/DNS/RR/RP.pm | 0 lib/{site => fallback}/Net/DNS/RR/RT.pm | 0 lib/{site => fallback}/Net/DNS/RR/SOA.pm | 0 lib/{site => fallback}/Net/DNS/RR/SRV.pm | 0 lib/{site => fallback}/Net/DNS/RR/TXT.pm | 0 lib/{site => fallback}/Net/DNS/RR/X25.pm | 0 lib/{site => fallback}/Net/DNS/Resolver.pm | 0 lib/{site => fallback}/Net/DNS/Update.pm | 0 lib/{site => fallback}/Net/Domain.pm | 0 lib/{site => fallback}/Net/DummyInetd.pm | 0 lib/{site => fallback}/Net/FTP.pm | 0 lib/{site => fallback}/Net/FTP/A.pm | 0 lib/{site => fallback}/Net/FTP/E.pm | 0 lib/{site => fallback}/Net/FTP/I.pm | 0 lib/{site => fallback}/Net/FTP/L.pm | 0 lib/{site => fallback}/Net/FTP/dataconn.pm | 0 lib/{site => fallback}/Net/HTTP.pm | 0 lib/{site => fallback}/Net/HTTP/Methods.pm | 0 lib/{site => fallback}/Net/HTTP/NB.pm | 0 lib/{site => fallback}/Net/HTTPS.pm | 0 lib/{site => fallback}/Net/NNTP.pm | 0 lib/{site => fallback}/Net/Netrc.pm | 0 lib/{site => fallback}/Net/OSCAR.pm | 0 lib/{site => fallback}/Net/OSCAR/Buddylist.pm | 0 lib/{site => fallback}/Net/OSCAR/Callbacks.pm | 0 .../Net/OSCAR/Callbacks/0/error.pm | 0 .../Callbacks/1/incoming_extended_information.pm | 0 .../Net/OSCAR/Callbacks/1/incoming_warning.pm | 0 .../Net/OSCAR/Callbacks/1/migrate.pm | 0 .../Net/OSCAR/Callbacks/1/pause.pm | 0 .../Net/OSCAR/Callbacks/1/rate_change.pm | 0 .../Net/OSCAR/Callbacks/1/rate_info_response.pm | 0 .../Net/OSCAR/Callbacks/1/self_information.pm | 0 .../Net/OSCAR/Callbacks/1/server_ready.pm | 0 .../OSCAR/Callbacks/1/service_redirect_response.pm | 0 .../Net/OSCAR/Callbacks/1/unpause.pm | 0 .../OSCAR/Callbacks/13/chat_navigator_response.pm | 0 .../Net/OSCAR/Callbacks/14/chat_buddy_arrival.pm | 0 .../Net/OSCAR/Callbacks/14/chat_buddy_departure.pm | 0 .../Net/OSCAR/Callbacks/14/chat_room_status.pm | 0 .../Net/OSCAR/Callbacks/14/incoming_chat_IM.pm | 0 .../Net/OSCAR/Callbacks/16/buddy_icon_downloaded.pm | 0 .../Net/OSCAR/Callbacks/16/buddy_icon_uploaded.pm | 0 .../Net/OSCAR/Callbacks/19/buddylist.pm | 0 .../Net/OSCAR/Callbacks/19/buddylist_3_response.pm | 0 .../Net/OSCAR/Callbacks/19/buddylist_add.pm | 0 .../Net/OSCAR/Callbacks/19/buddylist_delete.pm | 0 .../Net/OSCAR/Callbacks/19/buddylist_error.pm | 0 .../19/buddylist_modification_acknowledgement.pm | 0 .../Net/OSCAR/Callbacks/19/buddylist_modify.pm | 0 .../Callbacks/19/end_buddylist_modifications.pm | 0 .../Callbacks/19/start_buddylist_modifications.pm | 0 .../Net/OSCAR/Callbacks/2/incoming_profile.pm | 0 .../Net/OSCAR/Callbacks/21/ICQ_meta_response.pm | 0 .../Net/OSCAR/Callbacks/23/authentication_key.pm | 0 .../OSCAR/Callbacks/23/authorization_response.pm | 0 .../Net/OSCAR/Callbacks/3/buddy_rights_response.pm | 0 .../Net/OSCAR/Callbacks/3/buddy_signoff.pm | 0 .../Net/OSCAR/Callbacks/3/buddy_status_update.pm | 0 .../Net/OSCAR/Callbacks/4/IM_acknowledgement.pm | 0 .../OSCAR/Callbacks/4/chat_invitation_decline.pm | 0 .../Net/OSCAR/Callbacks/4/incoming_IM.pm | 0 .../Net/OSCAR/Callbacks/4/typing_notification.pm | 0 .../Net/OSCAR/Callbacks/7/admin_request_response.pm | 0 .../OSCAR/Callbacks/7/confirm_account_response.pm | 0 .../Net/OSCAR/Callbacks/9/BOS_rights_response.pm | 0 lib/{site => fallback}/Net/OSCAR/Common.pm | 0 lib/{site => fallback}/Net/OSCAR/Connection.pm | 0 lib/{site => fallback}/Net/OSCAR/Connection/Chat.pm | 0 .../Net/OSCAR/Connection/Direct.pm | 0 .../Net/OSCAR/Connection/Server.pm | 0 lib/{site => fallback}/Net/OSCAR/Constants.pm | 0 lib/{site => fallback}/Net/OSCAR/MethodInfo.pm | 0 lib/{site => fallback}/Net/OSCAR/Proxy.pm | 0 lib/{site => fallback}/Net/OSCAR/Screenname.pm | 0 lib/{site => fallback}/Net/OSCAR/ServerCallbacks.pm | 0 .../Net/OSCAR/ServerCallbacks/0/BOS_signon.pm | 0 .../ServerCallbacks/1/personal_info_request.pm | 0 .../OSCAR/ServerCallbacks/1/rate_acknowledgement.pm | 0 .../OSCAR/ServerCallbacks/1/rate_info_request.pm | 0 .../OSCAR/ServerCallbacks/1/set_extended_status.pm | 0 .../OSCAR/ServerCallbacks/1/set_service_versions.pm | 0 .../OSCAR/ServerCallbacks/1/set_tool_versions.pm | 0 .../OSCAR/ServerCallbacks/19/buddylist_request.pm | 0 .../ServerCallbacks/19/buddylist_rights_request.pm | 0 .../Net/OSCAR/ServerCallbacks/2/get_away.pm | 0 .../ServerCallbacks/2/locate_rights_request.pm | 0 .../ServerCallbacks/23/initial_signon_request.pm | 0 .../Net/OSCAR/ServerCallbacks/23/signon.pm | 0 .../OSCAR/ServerCallbacks/3/buddy_rights_request.pm | 0 .../OSCAR/ServerCallbacks/4/IM_parameter_request.pm | 0 .../OSCAR/ServerCallbacks/4/add_IM_parameters.pm | 0 .../Net/OSCAR/ServerCallbacks/4/outgoing_IM.pm | 0 .../OSCAR/ServerCallbacks/9/BOS_rights_request.pm | 0 lib/{site => fallback}/Net/OSCAR/TLV.pm | 0 lib/{site => fallback}/Net/OSCAR/Utility.pm | 0 lib/{site => fallback}/Net/OSCAR/XML.pm | 0 lib/{site => fallback}/Net/OSCAR/XML/Protocol.dtd | 0 .../Net/OSCAR/XML/Protocol.parsed-xml | 0 lib/{site => fallback}/Net/OSCAR/XML/Protocol.xml | 0 lib/{site => fallback}/Net/OSCAR/XML/Template.pm | 0 lib/{site => fallback}/Net/OSCAR/_BLInternal.pm | 0 lib/{site => fallback}/Net/PH.pm | 0 lib/{site => fallback}/Net/POP3.pm | 0 lib/{site => fallback}/Net/SMTP.pm | 0 lib/{site => fallback}/Net/SMTP_auth.pm | 0 lib/{site => fallback}/Net/SNPP.pm | 0 lib/{site => fallback}/Net/Time.pm | 0 lib/{site => fallback}/Params/Validate.pm | 0 lib/{site => fallback}/Params/ValidatePP.pm | 0 lib/{site => fallback}/Params/ValidateXS.pm | 0 lib/{site => fallback}/RRD/Simple.pm | 0 lib/{site => fallback}/RRDTool/Rawish.pm | 0 lib/{site => fallback}/Regexp/Common.pm | 0 lib/{site => fallback}/Regexp/Common/CC.pm | 0 lib/{site => fallback}/Regexp/Common/SEN.pm | 0 lib/{site => fallback}/Regexp/Common/URI.pm | 0 lib/{site => fallback}/Regexp/Common/URI/RFC1035.pm | 0 lib/{site => fallback}/Regexp/Common/URI/RFC1738.pm | 0 lib/{site => fallback}/Regexp/Common/URI/RFC1808.pm | 0 lib/{site => fallback}/Regexp/Common/URI/RFC2384.pm | 0 lib/{site => fallback}/Regexp/Common/URI/RFC2396.pm | 0 lib/{site => fallback}/Regexp/Common/URI/RFC2806.pm | 0 lib/{site => fallback}/Regexp/Common/URI/fax.pm | 0 lib/{site => fallback}/Regexp/Common/URI/file.pm | 0 lib/{site => fallback}/Regexp/Common/URI/ftp.pm | 0 lib/{site => fallback}/Regexp/Common/URI/gopher.pm | 0 lib/{site => fallback}/Regexp/Common/URI/http.pm | 0 lib/{site => fallback}/Regexp/Common/URI/news.pm | 0 lib/{site => fallback}/Regexp/Common/URI/pop.pm | 0 .../Regexp/Common/URI/prospero.pm | 0 lib/{site => fallback}/Regexp/Common/URI/tel.pm | 0 lib/{site => fallback}/Regexp/Common/URI/telnet.pm | 0 lib/{site => fallback}/Regexp/Common/URI/tv.pm | 0 lib/{site => fallback}/Regexp/Common/URI/wais.pm | 0 lib/{site => fallback}/Regexp/Common/_support.pm | 0 lib/{site => fallback}/Regexp/Common/balanced.pm | 0 lib/{site => fallback}/Regexp/Common/comment.pm | 0 lib/{site => fallback}/Regexp/Common/delimited.pm | 0 lib/{site => fallback}/Regexp/Common/lingua.pm | 0 lib/{site => fallback}/Regexp/Common/list.pm | 0 lib/{site => fallback}/Regexp/Common/net.pm | 0 lib/{site => fallback}/Regexp/Common/number.pm | 0 lib/{site => fallback}/Regexp/Common/profanity.pm | 0 lib/{site => fallback}/Regexp/Common/whitespace.pm | 0 lib/{site => fallback}/Regexp/Common/zip.pm | 0 lib/{site => fallback}/SVG.pm | 0 lib/{site => fallback}/SVG/DOM.pm | 0 lib/{site => fallback}/SVG/Element.pm | 0 lib/{site => fallback}/SVG/Extension.pm | 0 lib/{site => fallback}/SVG/XML.pm | 0 lib/{site => fallback}/Set/Infinite.pm | 0 lib/{site => fallback}/Set/Infinite/Arithmetic.pm | 0 lib/{site => fallback}/Set/Infinite/Basic.pm | 0 lib/{site => fallback}/Set/Infinite/_recurrence.pm | 0 lib/{site => fallback}/Text/vFile/asData.pm | 0 lib/{site => fallback}/Tie/Hash.pm | 0 lib/{site => fallback}/Tie/Hash.pm.original | 0 lib/{site => fallback}/Tie/IxHash.pm | 0 lib/{site => fallback}/Time/CTime.pm | 0 lib/{site => fallback}/Time/DaysInMonth.pm | 0 lib/{site => fallback}/Time/JulianDay.pm | 0 lib/{site => fallback}/Time/ParseDate.pm | 0 lib/{site => fallback}/Time/Timezone.pm | 0 lib/{site => fallback}/Time/Zone.pm | 0 lib/{site => fallback}/Tk/CursorControl.pm | 0 lib/{site => fallback}/Tk/ToolBar.pm | 0 lib/{site => fallback}/Tk/ToolBar/tkIcons | 0 lib/{site => fallback}/Tk/trans_cur.mask | 0 lib/{site => fallback}/Tk/trans_cur.xbm | 0 lib/{site => fallback}/URI.pm | 0 lib/{site => fallback}/URI/Escape.pm | 0 lib/{site => fallback}/URI/Heuristic.pm | 0 lib/{site => fallback}/URI/QueryParam.pm | 0 lib/{site => fallback}/URI/Split.pm | 0 lib/{site => fallback}/URI/URL.pm | 0 lib/{site => fallback}/URI/URL/_generic.pm | 0 lib/{site => fallback}/URI/URL/_login.pm | 0 lib/{site => fallback}/URI/URL/data.pm | 0 lib/{site => fallback}/URI/URL/file.pm | 0 lib/{site => fallback}/URI/URL/finger.pm | 0 lib/{site => fallback}/URI/URL/ftp.pm | 0 lib/{site => fallback}/URI/URL/gopher.pm | 0 lib/{site => fallback}/URI/URL/http.pm | 0 lib/{site => fallback}/URI/URL/https.pm | 0 lib/{site => fallback}/URI/URL/mailto.pm | 0 lib/{site => fallback}/URI/URL/news.pm | 0 lib/{site => fallback}/URI/URL/nntp.pm | 0 lib/{site => fallback}/URI/URL/prospero.pm | 0 lib/{site => fallback}/URI/URL/rlogin.pm | 0 lib/{site => fallback}/URI/URL/telnet.pm | 0 lib/{site => fallback}/URI/URL/tn3270.pm | 0 lib/{site => fallback}/URI/URL/wais.pm | 0 lib/{site => fallback}/URI/URL/webster.pm | 0 lib/{site => fallback}/URI/URL/whois.pm | 0 lib/{site => fallback}/URI/WithBase.pm | 0 lib/{site => fallback}/URI/_foreign.pm | 0 lib/{site => fallback}/URI/_generic.pm | 0 lib/{site => fallback}/URI/_ldap.pm | 0 lib/{site => fallback}/URI/_login.pm | 0 lib/{site => fallback}/URI/_query.pm | 0 lib/{site => fallback}/URI/_segment.pm | 0 lib/{site => fallback}/URI/_server.pm | 0 lib/{site => fallback}/URI/_userpass.pm | 0 lib/{site => fallback}/URI/data.pm | 0 lib/{site => fallback}/URI/file.pm | 0 lib/{site => fallback}/URI/file/Base.pm | 0 lib/{site => fallback}/URI/file/FAT.pm | 0 lib/{site => fallback}/URI/file/Mac.pm | 0 lib/{site => fallback}/URI/file/OS2.pm | 0 lib/{site => fallback}/URI/file/QNX.pm | 0 lib/{site => fallback}/URI/file/Unix.pm | 0 lib/{site => fallback}/URI/file/Win32.pm | 0 lib/{site => fallback}/URI/ftp.pm | 0 lib/{site => fallback}/URI/gopher.pm | 0 lib/{site => fallback}/URI/http.pm | 0 lib/{site => fallback}/URI/https.pm | 0 lib/{site => fallback}/URI/ldap.pm | 0 lib/{site => fallback}/URI/ldapi.pm | 0 lib/{site => fallback}/URI/ldaps.pm | 0 lib/{site => fallback}/URI/mailto.pm | 0 lib/{site => fallback}/URI/mms.pm | 0 lib/{site => fallback}/URI/news.pm | 0 lib/{site => fallback}/URI/nntp.pm | 0 lib/{site => fallback}/URI/pop.pm | 0 lib/{site => fallback}/URI/rlogin.pm | 0 lib/{site => fallback}/URI/rsync.pm | 0 lib/{site => fallback}/URI/rtsp.pm | 0 lib/{site => fallback}/URI/rtspu.pm | 0 lib/{site => fallback}/URI/sip.pm | 0 lib/{site => fallback}/URI/sips.pm | 0 lib/{site => fallback}/URI/snews.pm | 0 lib/{site => fallback}/URI/ssh.pm | 0 lib/{site => fallback}/URI/telnet.pm | 0 lib/{site => fallback}/URI/tn3270.pm | 0 lib/{site => fallback}/URI/urn.pm | 0 lib/{site => fallback}/URI/urn/isbn.pm | 0 lib/{site => fallback}/URI/urn/oid.pm | 0 lib/{site => fallback}/Win32/DUN.pm | 0 lib/{site => fallback}/Win32/DriveInfo.pm | 0 lib/{site => fallback}/Win32/DriveInfo.pm.html | 0 lib/{site => fallback}/Win32/DriveInfo.txt | 0 lib/{site => fallback}/Win32/IIPC.pm | 0 lib/{site => fallback}/Win32/IPERFSUP.PM | 0 lib/{site => fallback}/Win32/IPerfmon.pm | 0 lib/{site => fallback}/Win32/IProc.pm | 0 lib/{site => fallback}/Win32/ISYNC.PM | 0 lib/{site => fallback}/Win32/MemMap.pm | 0 lib/{site => fallback}/Win32/SerialPort.html | 0 lib/{site => fallback}/Win32/SerialPort.pm | 0 lib/{site => fallback}/Win32/SerialPort.pm.original | 0 lib/{site => fallback}/Win32/SerialPort.txt | 0 lib/{site => fallback}/Win32/SoundEx.pm | 0 lib/{site => fallback}/Win32/TieRegistry.pm | 0 lib/{site => fallback}/Win32/dun.txt | 0 lib/{site => fallback}/Win32API/CommPort.html | 0 lib/{site => fallback}/Win32API/CommPort.pm | 0 lib/{site => fallback}/Win32API/Resources.html | 0 lib/{site => fallback}/Win32API/Resources.pm | 0 lib/{site => fallback}/XML/DOM.pm | 0 lib/{site => fallback}/XML/DOM/AttDef.pod | 0 lib/{site => fallback}/XML/DOM/AttlistDecl.pod | 0 lib/{site => fallback}/XML/DOM/Attr.pod | 0 lib/{site => fallback}/XML/DOM/CDATASection.pod | 0 lib/{site => fallback}/XML/DOM/CharacterData.pod | 0 lib/{site => fallback}/XML/DOM/Comment.pod | 0 lib/{site => fallback}/XML/DOM/DOMException.pm | 0 .../XML/DOM/DOMImplementation.pod | 0 lib/{site => fallback}/XML/DOM/Document.pod | 0 lib/{site => fallback}/XML/DOM/DocumentFragment.pod | 0 lib/{site => fallback}/XML/DOM/DocumentType.pod | 0 lib/{site => fallback}/XML/DOM/Element.pod | 0 lib/{site => fallback}/XML/DOM/ElementDecl.pod | 0 lib/{site => fallback}/XML/DOM/Entity.pod | 0 lib/{site => fallback}/XML/DOM/EntityReference.pod | 0 lib/{site => fallback}/XML/DOM/NamedNodeMap.pm | 0 lib/{site => fallback}/XML/DOM/NamedNodeMap.pod | 0 lib/{site => fallback}/XML/DOM/Node.pod | 0 lib/{site => fallback}/XML/DOM/NodeList.pm | 0 lib/{site => fallback}/XML/DOM/NodeList.pod | 0 lib/{site => fallback}/XML/DOM/Notation.pod | 0 lib/{site => fallback}/XML/DOM/Parser.pod | 0 lib/{site => fallback}/XML/DOM/PerlSAX.pm | 0 .../XML/DOM/ProcessingInstruction.pod | 0 lib/{site => fallback}/XML/DOM/Text.pod | 0 lib/{site => fallback}/XML/DOM/XMLDecl.pod | 0 lib/{site => fallback}/XML/ESISParser.pm | 0 lib/{site => fallback}/XML/Elemental.pm | 0 lib/{site => fallback}/XML/Elemental/Characters.pm | 0 lib/{site => fallback}/XML/Elemental/Document.pm | 0 lib/{site => fallback}/XML/Elemental/Element.pm | 0 lib/{site => fallback}/XML/Elemental/Node.pm | 0 lib/{site => fallback}/XML/Elemental/SAXHandler.pm | 0 lib/{site => fallback}/XML/Elemental/Util.pm | 0 lib/{site => fallback}/XML/Handler/BuildDOM.pm | 0 .../XML/Handler/CanonXMLWriter.pm | 0 lib/{site => fallback}/XML/Handler/Sample.pm | 0 lib/{site => fallback}/XML/Handler/Subs.pm | 0 lib/{site => fallback}/XML/Handler/XMLWriter.pm | 0 lib/{site => fallback}/XML/NamespaceSupport.pm | 0 lib/{site => fallback}/XML/Parser/PerlSAX.pm | 0 .../XML/Parser/Style/Elemental.pm | 0 lib/{site => fallback}/XML/PatAct/ActionTempl.pm | 0 lib/{site => fallback}/XML/PatAct/Amsterdam.pm | 0 lib/{site => fallback}/XML/PatAct/MatchName.pm | 0 lib/{site => fallback}/XML/PatAct/PatternTempl.pm | 0 lib/{site => fallback}/XML/PatAct/ToObjects.pm | 0 lib/{site => fallback}/XML/Perl2SAX.pm | 0 lib/{site => fallback}/XML/RAI.pm | 0 lib/{site => fallback}/XML/RAI/Channel.pm | 0 lib/{site => fallback}/XML/RAI/Enclosure.pm | 0 lib/{site => fallback}/XML/RAI/Image.pm | 0 lib/{site => fallback}/XML/RAI/Item.pm | 0 lib/{site => fallback}/XML/RAI/Object.pm | 0 lib/{site => fallback}/XML/RSS.pm | 0 lib/{site => fallback}/XML/RSS/Parser.pm | 0 lib/{site => fallback}/XML/RSS/Parser/Characters.pm | 0 lib/{site => fallback}/XML/RSS/Parser/Element.pm | 0 lib/{site => fallback}/XML/RSS/Parser/Feed.pm | 0 lib/{site => fallback}/XML/RSS/Parser/Util.pm | 0 lib/{site => fallback}/XML/RegExp.pm | 0 lib/{site => fallback}/XML/SAX.pm | 0 lib/{site => fallback}/XML/SAX/Base.pm | 0 lib/{site => fallback}/XML/SAX/DocumentLocator.pm | 0 lib/{site => fallback}/XML/SAX/Exception.pm | 0 lib/{site => fallback}/XML/SAX/Intro.pod | 0 lib/{site => fallback}/XML/SAX/ParserDetails.ini | 0 lib/{site => fallback}/XML/SAX/ParserFactory.pm | 0 lib/{site => fallback}/XML/SAX/PurePerl.pm | 0 lib/{site => fallback}/XML/SAX/PurePerl/DTDDecls.pm | 0 .../XML/SAX/PurePerl/DebugHandler.pm | 0 lib/{site => fallback}/XML/SAX/PurePerl/DocType.pm | 0 .../XML/SAX/PurePerl/EncodingDetect.pm | 0 .../XML/SAX/PurePerl/Exception.pm | 0 .../XML/SAX/PurePerl/NoUnicodeExt.pm | 0 .../XML/SAX/PurePerl/Productions.pm | 0 lib/{site => fallback}/XML/SAX/PurePerl/Reader.pm | 0 .../XML/SAX/PurePerl/Reader/NoUnicodeExt.pm | 0 .../XML/SAX/PurePerl/Reader/Stream.pm | 0 .../XML/SAX/PurePerl/Reader/String.pm | 0 .../XML/SAX/PurePerl/Reader/URI.pm | 0 .../XML/SAX/PurePerl/Reader/UnicodeExt.pm | 0 .../XML/SAX/PurePerl/UnicodeExt.pm | 0 lib/{site => fallback}/XML/SAX/PurePerl/XMLDecl.pm | 0 lib/{site => fallback}/XML/SAX/placeholder.pl | 0 lib/{site => fallback}/XML/SAX2Perl.pm | 0 lib/{site => fallback}/XML/Twig.pm | 0 lib/{site => fallback}/XML/XPath.pm | 0 lib/{site => fallback}/XML/XPath/Boolean.pm | 0 lib/{site => fallback}/XML/XPath/Builder.pm | 0 lib/{site => fallback}/XML/XPath/Expr.pm | 0 lib/{site => fallback}/XML/XPath/Function.pm | 0 lib/{site => fallback}/XML/XPath/Literal.pm | 0 lib/{site => fallback}/XML/XPath/LocationPath.pm | 0 lib/{site => fallback}/XML/XPath/Node.pm | 0 lib/{site => fallback}/XML/XPath/Node/Attribute.pm | 0 lib/{site => fallback}/XML/XPath/Node/Comment.pm | 0 lib/{site => fallback}/XML/XPath/Node/Element.pm | 0 lib/{site => fallback}/XML/XPath/Node/Namespace.pm | 0 lib/{site => fallback}/XML/XPath/Node/PI.pm | 0 lib/{site => fallback}/XML/XPath/Node/Text.pm | 0 lib/{site => fallback}/XML/XPath/NodeSet.pm | 0 lib/{site => fallback}/XML/XPath/Number.pm | 0 lib/{site => fallback}/XML/XPath/Parser.pm | 0 lib/{site => fallback}/XML/XPath/PerlSAX.pm | 0 lib/{site => fallback}/XML/XPath/Root.pm | 0 lib/{site => fallback}/XML/XPath/Step.pm | 0 lib/{site => fallback}/XML/XPath/Variable.pm | 0 lib/{site => fallback}/XML/XPath/XMLParser.pm | 0 .../auto/LWP/UserAgent/_need_proxy.al | 0 .../auto/LWP/UserAgent/autosplit.ix | 0 lib/{site => fallback}/auto/LWP/UserAgent/clone.al | 0 .../auto/LWP/UserAgent/env_proxy.al | 0 .../auto/LWP/UserAgent/is_protocol_supported.al | 0 lib/{site => fallback}/auto/LWP/UserAgent/mirror.al | 0 .../auto/LWP/UserAgent/no_proxy.al | 0 lib/{site => fallback}/auto/LWP/UserAgent/proxy.al | 0 lib/{site => fallback}/auto/Net-DNS/.packlist | 0 .../auto/URI/URL/_generic/_netloc_elem.al | 0 lib/{site => fallback}/auto/URI/URL/_generic/abs.al | 0 .../auto/URI/URL/_generic/autosplit.ix | 0 .../auto/URI/URL/_generic/crack.al | 0 .../auto/URI/URL/_generic/eparams.al | 0 .../auto/URI/URL/_generic/epath.al | 0 lib/{site => fallback}/auto/URI/URL/_generic/eq.al | 0 .../auto/URI/URL/_generic/equery.al | 0 .../auto/URI/URL/_generic/frag.al | 0 .../auto/URI/URL/_generic/host.al | 0 .../auto/URI/URL/_generic/params.al | 0 .../auto/URI/URL/_generic/password.al | 0 .../auto/URI/URL/_generic/path.al | 0 .../auto/URI/URL/_generic/path_components.al | 0 .../auto/URI/URL/_generic/port.al | 0 .../auto/URI/URL/_generic/query.al | 0 lib/{site => fallback}/auto/URI/URL/_generic/rel.al | 0 .../auto/URI/URL/_generic/user.al | 0 lib/{site => fallback}/auto/URI/URL/abs.al | 0 lib/{site => fallback}/auto/URI/URL/as_string.al | 0 lib/{site => fallback}/auto/URI/URL/autosplit.ix | 0 lib/{site => fallback}/auto/URI/URL/bad_method.al | 0 lib/{site => fallback}/auto/URI/URL/base.al | 0 lib/{site => fallback}/auto/URI/URL/crack.al | 0 lib/{site => fallback}/auto/URI/URL/eq.al | 0 .../auto/URI/URL/file/autosplit.ix | 0 .../auto/URI/URL/file/dos_path.al | 0 .../auto/URI/URL/file/mac_path.al | 0 .../auto/URI/URL/file/newlocal.al | 0 .../auto/URI/URL/file/unix_path.al | 0 .../auto/URI/URL/file/vms_path.al | 0 .../auto/URI/URL/http/autosplit.ix | 0 .../auto/URI/URL/http/keywords.al | 0 .../auto/URI/URL/http/query_form.al | 0 lib/{site => fallback}/auto/URI/URL/newlocal.al | 0 lib/{site => fallback}/auto/URI/URL/print_on.al | 0 lib/{site => fallback}/auto/URI/URL/rel.al | 0 lib/{site => fallback}/auto/URI/URL/scheme.al | 0 lib/{site => fallback}/auto/URI/URL/strict.al | 0 lib/{site => fallback}/auto/Win32/MemMap/memmap.dll | Bin lib/{site => fallback}/auto/http/autosplit.ix | 0 lib/{site => fallback}/auto/http/keywords.al | 0 lib/{site => fallback}/auto/http/query_form.al | 0 lib/{site => fallback}/enum.pm | 0 lib/{site => fallback}/iCal/Parser.pm | 0 845 files changed, 4 insertions(+) rename lib/{site => fallback}/Astro/MoonPhase.pm (100%) rename lib/{site => fallback}/Authen/SASL.pm (100%) rename lib/{site => fallback}/Authen/SASL.pod (100%) rename lib/{site => fallback}/Authen/SASL/CRAM_MD5.pm (100%) rename lib/{site => fallback}/Authen/SASL/EXTERNAL.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl.pod (100%) rename lib/{site => fallback}/Authen/SASL/Perl/ANONYMOUS.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl/CRAM_MD5.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl/DIGEST_MD5.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl/EXTERNAL.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl/LOGIN.pm (100%) rename lib/{site => fallback}/Authen/SASL/Perl/PLAIN.pm (100%) rename lib/{site => fallback}/CDDB.pm (100%) rename lib/{site => fallback}/Capture/Tiny.pm (100%) rename lib/{site => fallback}/Class/Accessor.pm (100%) rename lib/{site => fallback}/Class/Accessor/Chained/Fast.pm (100%) rename lib/{site => fallback}/Class/Accessor/Fast.pm (100%) rename lib/{site => fallback}/Class/ErrorHandler.pm (100%) rename lib/{site => fallback}/Class/Singleton.pm (100%) rename lib/{site => fallback}/Class/XPath.pm (100%) rename lib/{site => fallback}/ControlX10/CM11.pm (100%) rename lib/{site => fallback}/ControlX10/CM11.pm.new (100%) rename lib/{site => fallback}/ControlX10/CM11.pm.old (100%) rename lib/{site => fallback}/ControlX10/CM11.txt (100%) rename lib/{site => fallback}/ControlX10/CM17.pm (100%) rename lib/{site => fallback}/ControlX10/CM17.pm.old (100%) rename lib/{site => fallback}/ControlX10/CM17.txt (100%) rename lib/{site => fallback}/Date/Format.pm (100%) rename lib/{site => fallback}/Date/Language.pm (100%) rename lib/{site => fallback}/Date/Language/Afar.pm (100%) rename lib/{site => fallback}/Date/Language/Amharic.pm (100%) rename lib/{site => fallback}/Date/Language/Austrian.pm (100%) rename lib/{site => fallback}/Date/Language/Brazilian.pm (100%) rename lib/{site => fallback}/Date/Language/Chinese_GB.pm (100%) rename lib/{site => fallback}/Date/Language/Czech.pm (100%) rename lib/{site => fallback}/Date/Language/Danish.pm (100%) rename lib/{site => fallback}/Date/Language/Dutch.pm (100%) rename lib/{site => fallback}/Date/Language/English.pm (100%) rename lib/{site => fallback}/Date/Language/Finnish.pm (100%) rename lib/{site => fallback}/Date/Language/French.pm (100%) rename lib/{site => fallback}/Date/Language/Gedeo.pm (100%) rename lib/{site => fallback}/Date/Language/German.pm (100%) rename lib/{site => fallback}/Date/Language/Greek.pm (100%) rename lib/{site => fallback}/Date/Language/Italian.pm (100%) rename lib/{site => fallback}/Date/Language/Norwegian.pm (100%) rename lib/{site => fallback}/Date/Language/Oromo.pm (100%) rename lib/{site => fallback}/Date/Language/Sidama.pm (100%) rename lib/{site => fallback}/Date/Language/Somali.pm (100%) rename lib/{site => fallback}/Date/Language/Swedish.pm (100%) rename lib/{site => fallback}/Date/Language/Tigrinya.pm (100%) rename lib/{site => fallback}/Date/Language/TigrinyaEritrean.pm (100%) rename lib/{site => fallback}/Date/Language/TigrinyaEthiopian.pm (100%) rename lib/{site => fallback}/Date/Parse.pm (100%) rename lib/{site => fallback}/DateTime.pm (100%) rename lib/{site => fallback}/DateTime/Duration.pm (100%) rename lib/{site => fallback}/DateTime/Event/ICal.pm (100%) rename lib/{site => fallback}/DateTime/Event/Recurrence.pm (100%) rename lib/{site => fallback}/DateTime/Format/ICal.pm (100%) rename lib/{site => fallback}/DateTime/Helpers.pm (100%) rename lib/{site => fallback}/DateTime/Infinite.pm (100%) rename lib/{site => fallback}/DateTime/LeapSecond.pm (100%) rename lib/{site => fallback}/DateTime/Locale/Base.pm (100%) rename lib/{site => fallback}/DateTime/Locale/aa.pm (100%) rename lib/{site => fallback}/DateTime/Locale/aa_ER_SAAHO.pm (100%) rename lib/{site => fallback}/DateTime/Locale/af.pm (100%) rename lib/{site => fallback}/DateTime/Locale/af_ZA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ak.pm (100%) rename lib/{site => fallback}/DateTime/Locale/am.pm (100%) rename lib/{site => fallback}/DateTime/Locale/am_ET.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_EG.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_JO.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_LB.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_QA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_SA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_SY.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_TN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ar_YE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/as.pm (100%) rename lib/{site => fallback}/DateTime/Locale/as_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/az.pm (100%) rename lib/{site => fallback}/DateTime/Locale/az_Cyrl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/be.pm (100%) rename lib/{site => fallback}/DateTime/Locale/bg.pm (100%) rename lib/{site => fallback}/DateTime/Locale/bg_BG.pm (100%) rename lib/{site => fallback}/DateTime/Locale/bn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/bn_BD.pm (100%) rename lib/{site => fallback}/DateTime/Locale/bn_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/bs.pm (100%) rename lib/{site => fallback}/DateTime/Locale/byn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/byn_ER.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ca.pm (100%) rename lib/{site => fallback}/DateTime/Locale/cch.pm (100%) rename lib/{site => fallback}/DateTime/Locale/cs.pm (100%) rename lib/{site => fallback}/DateTime/Locale/cy.pm (100%) rename lib/{site => fallback}/DateTime/Locale/cy_GB.pm (100%) rename lib/{site => fallback}/DateTime/Locale/da.pm (100%) rename lib/{site => fallback}/DateTime/Locale/de.pm (100%) rename lib/{site => fallback}/DateTime/Locale/de_AT.pm (100%) rename lib/{site => fallback}/DateTime/Locale/de_BE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/dz.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ee.pm (100%) rename lib/{site => fallback}/DateTime/Locale/el.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_AU.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_BE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_BW.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_BZ.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_CA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_GB.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_HK.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_IE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_MT.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_NZ.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_PH.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_PK.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_SG.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_ZA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/en_ZW.pm (100%) rename lib/{site => fallback}/DateTime/Locale/eo.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_AR.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_BO.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_CL.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_CO.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_CR.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_DO.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_EC.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_ES.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_GT.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_HN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_MX.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_NI.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_PA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_PR.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_PY.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_SV.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_US.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_UY.pm (100%) rename lib/{site => fallback}/DateTime/Locale/es_VE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/et.pm (100%) rename lib/{site => fallback}/DateTime/Locale/et_EE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/eu.pm (100%) rename lib/{site => fallback}/DateTime/Locale/eu_ES.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fi.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fo.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fr_BE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fr_CA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fr_CH.pm (100%) rename lib/{site => fallback}/DateTime/Locale/fur.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ga.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ga_IE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gaa.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gez.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gez_ER.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gez_ET.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gl_ES.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gu.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gu_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gv.pm (100%) rename lib/{site => fallback}/DateTime/Locale/gv_GB.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ha.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ha_Arab.pm (100%) rename lib/{site => fallback}/DateTime/Locale/haw.pm (100%) rename lib/{site => fallback}/DateTime/Locale/haw_US.pm (100%) rename lib/{site => fallback}/DateTime/Locale/he.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hi.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hi_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hu.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hy.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hy_AM.pm (100%) rename lib/{site => fallback}/DateTime/Locale/hy_AM_REVISED.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ia.pm (100%) rename lib/{site => fallback}/DateTime/Locale/id.pm (100%) rename lib/{site => fallback}/DateTime/Locale/id_ID.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ig.pm (100%) rename lib/{site => fallback}/DateTime/Locale/is.pm (100%) rename lib/{site => fallback}/DateTime/Locale/it.pm (100%) rename lib/{site => fallback}/DateTime/Locale/it_CH.pm (100%) rename lib/{site => fallback}/DateTime/Locale/it_IT.pm (100%) rename lib/{site => fallback}/DateTime/Locale/iu.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ja.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ka.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kaj.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kam.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kcg.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kfo.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kk.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kl_GL.pm (100%) rename lib/{site => fallback}/DateTime/Locale/km.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kn_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ko.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ko_KR.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kok.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kok_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kw.pm (100%) rename lib/{site => fallback}/DateTime/Locale/kw_GB.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ln.pm (100%) rename lib/{site => fallback}/DateTime/Locale/lo.pm (100%) rename lib/{site => fallback}/DateTime/Locale/lt.pm (100%) rename lib/{site => fallback}/DateTime/Locale/lv.pm (100%) rename lib/{site => fallback}/DateTime/Locale/mk.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ml.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ml_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/mn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/mr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/mr_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ms.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ms_BN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ms_MY.pm (100%) rename lib/{site => fallback}/DateTime/Locale/mt.pm (100%) rename lib/{site => fallback}/DateTime/Locale/nb.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ne.pm (100%) rename lib/{site => fallback}/DateTime/Locale/nl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/nl_BE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/nn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/nr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/nso.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ny.pm (100%) rename lib/{site => fallback}/DateTime/Locale/om.pm (100%) rename lib/{site => fallback}/DateTime/Locale/om_ET.pm (100%) rename lib/{site => fallback}/DateTime/Locale/om_KE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pa.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pa_Arab.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pa_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pt.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pt_BR.pm (100%) rename lib/{site => fallback}/DateTime/Locale/pt_PT.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ro.pm (100%) rename lib/{site => fallback}/DateTime/Locale/root.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ru.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ru_UA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/rw.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sid.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sk.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/so.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sq.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sr_Cyrl.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sr_Cyrl_BA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sr_Latn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sr_Latn_BA.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ss.pm (100%) rename lib/{site => fallback}/DateTime/Locale/st.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sv.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sv_SE.pm (100%) rename lib/{site => fallback}/DateTime/Locale/sw.pm (100%) rename lib/{site => fallback}/DateTime/Locale/syr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/syr_SY.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ta.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ta_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/te.pm (100%) rename lib/{site => fallback}/DateTime/Locale/te_IN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/tg.pm (100%) rename lib/{site => fallback}/DateTime/Locale/th.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ti.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ti_ER.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ti_ET.pm (100%) rename lib/{site => fallback}/DateTime/Locale/tig.pm (100%) rename lib/{site => fallback}/DateTime/Locale/tig_ER.pm (100%) rename lib/{site => fallback}/DateTime/Locale/tn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/tr.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ts.pm (100%) rename lib/{site => fallback}/DateTime/Locale/uk.pm (100%) rename lib/{site => fallback}/DateTime/Locale/uz.pm (100%) rename lib/{site => fallback}/DateTime/Locale/uz_Arab.pm (100%) rename lib/{site => fallback}/DateTime/Locale/uz_Latn.pm (100%) rename lib/{site => fallback}/DateTime/Locale/ve.pm (100%) rename lib/{site => fallback}/DateTime/Locale/vi.pm (100%) rename lib/{site => fallback}/DateTime/Locale/wal.pm (100%) rename lib/{site => fallback}/DateTime/Locale/wal_ET.pm (100%) rename lib/{site => fallback}/DateTime/Locale/xh.pm (100%) rename lib/{site => fallback}/DateTime/Locale/yo.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zh.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zh_Hans_CN.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zh_Hans_SG.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zh_Hant.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zh_Hant_HK.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zh_Hant_MO.pm (100%) rename lib/{site => fallback}/DateTime/Locale/zu.pm (100%) rename lib/{site => fallback}/DateTime/Set.pm (100%) rename lib/{site => fallback}/DateTime/Span.pm (100%) rename lib/{site => fallback}/DateTime/SpanSet.pm (100%) rename lib/{site => fallback}/DateTime/TimeZone.pm (100%) rename lib/{site => fallback}/DateTimePP.pm (100%) rename lib/{site => fallback}/DateTimePPExtra.pm (100%) rename lib/{site => fallback}/Device/Changes (100%) rename lib/{site => fallback}/Device/Device-SerialPort.html (100%) rename lib/{site => fallback}/Device/README (100%) rename lib/{site => fallback}/Device/SerialPort.pm (100%) rename lib/{site => fallback}/Digest/HMAC.pm (100%) rename lib/{site => fallback}/Digest/HMAC_MD5.pm (100%) rename lib/{site => fallback}/Digest/HMAC_SHA1.pm (100%) rename lib/{site => fallback}/Digest/MD2.pm (100%) rename lib/{site => fallback}/Digest/MD5.pm (100%) rename lib/{site => fallback}/Digest/Perl/MD5.pm (100%) rename lib/{site => fallback}/Digest/SHA1.pm (100%) rename lib/{site => fallback}/Digest/base.pm (100%) rename lib/{site => fallback}/Email/Date/Format.pm (100%) rename lib/{site => fallback}/File/Listing.pm (100%) rename lib/{site => fallback}/File/Which.pm (100%) rename lib/{site => fallback}/Geo/METAR.pm (100%) rename lib/{site => fallback}/Geo/Weather.pm (100%) rename lib/{site => fallback}/Geo/WeatherNOAA.pm (100%) rename lib/{site => fallback}/Geo/WeatherNOAA.pm.old (100%) rename lib/{site => fallback}/HTML/AsSubs.pm (100%) rename lib/{site => fallback}/HTML/Element.pm (100%) rename lib/{site => fallback}/HTML/Entities.pm (100%) rename lib/{site => fallback}/HTML/Filter.pm (100%) rename lib/{site => fallback}/HTML/Form.pm (100%) rename lib/{site => fallback}/HTML/FormatMarkdown.pm (100%) rename lib/{site => fallback}/HTML/FormatPS.pm (100%) rename lib/{site => fallback}/HTML/FormatRTF.pm (100%) rename lib/{site => fallback}/HTML/FormatText.pm (100%) rename lib/{site => fallback}/HTML/Formatter.pm (100%) rename lib/{site => fallback}/HTML/HeadParser.pm (100%) rename lib/{site => fallback}/HTML/LinkExtor.pm (100%) rename lib/{site => fallback}/HTML/Parse.pm (100%) rename lib/{site => fallback}/HTML/Parser.pm (100%) rename lib/{site => fallback}/HTML/TableExtract.pm (100%) rename lib/{site => fallback}/HTML/Tagset.pm (100%) rename lib/{site => fallback}/HTML/TokeParser.pm (100%) rename lib/{site => fallback}/HTML/TreeBuilder.pm (100%) rename lib/{site => fallback}/HTTP/Cookies.pm (100%) rename lib/{site => fallback}/HTTP/Cookies/Microsoft.pm (100%) rename lib/{site => fallback}/HTTP/Cookies/Netscape.pm (100%) rename lib/{site => fallback}/HTTP/Daemon.pm (100%) rename lib/{site => fallback}/HTTP/Date.pm (100%) rename lib/{site => fallback}/HTTP/Headers.pm (100%) rename lib/{site => fallback}/HTTP/Headers/Auth.pm (100%) rename lib/{site => fallback}/HTTP/Headers/ETag.pm (100%) rename lib/{site => fallback}/HTTP/Headers/Util.pm (100%) rename lib/{site => fallback}/HTTP/Message.pm (100%) rename lib/{site => fallback}/HTTP/Negotiate.pm (100%) rename lib/{site => fallback}/HTTP/Request.pm (100%) rename lib/{site => fallback}/HTTP/Request/Common.pm (100%) rename lib/{site => fallback}/HTTP/Response.pm (100%) rename lib/{site => fallback}/HTTP/Status.pm (100%) rename lib/{site => fallback}/Hardware/iButton/Connection.pm (100%) rename lib/{site => fallback}/Hardware/iButton/Device.pm (100%) rename lib/{site => fallback}/Hardware/iButton/old/Connection.pm (100%) rename lib/{site => fallback}/Hardware/iButton/old/Device.pm (100%) rename lib/{site => fallback}/IO/Interface.pm (100%) rename lib/{site => fallback}/IO/Interface/Simple.pm (100%) rename lib/{site => fallback}/JSON.pm (100%) rename lib/{site => fallback}/JSON/PP.pm (100%) rename lib/{site => fallback}/JSON/PP/Boolean.pm (100%) rename lib/{site => fallback}/JSON/PP5005.pm (100%) rename lib/{site => fallback}/JSON/PP56.pm (100%) rename lib/{site => fallback}/JSON/PP58.pm (100%) rename lib/{site => fallback}/LWP.pm (100%) rename lib/{site => fallback}/LWP/Authen/Basic.pm (100%) rename lib/{site => fallback}/LWP/Authen/Digest.pm (100%) rename lib/{site => fallback}/LWP/Authen/Ntlm.pm (100%) rename lib/{site => fallback}/LWP/ConnCache.pm (100%) rename lib/{site => fallback}/LWP/Debug.pm (100%) rename lib/{site => fallback}/LWP/DebugFile.pm (100%) rename lib/{site => fallback}/LWP/MediaTypes.pm (100%) rename lib/{site => fallback}/LWP/MemberMixin.pm (100%) rename lib/{site => fallback}/LWP/Protocol.pm (100%) rename lib/{site => fallback}/LWP/Protocol/GHTTP.pm (100%) rename lib/{site => fallback}/LWP/Protocol/cpan.pm (100%) rename lib/{site => fallback}/LWP/Protocol/data.pm (100%) rename lib/{site => fallback}/LWP/Protocol/file.pm (100%) rename lib/{site => fallback}/LWP/Protocol/ftp.pm (100%) rename lib/{site => fallback}/LWP/Protocol/gopher.pm (100%) rename lib/{site => fallback}/LWP/Protocol/http.pm (100%) rename lib/{site => fallback}/LWP/Protocol/http10.pm (100%) rename lib/{site => fallback}/LWP/Protocol/https.pm (100%) rename lib/{site => fallback}/LWP/Protocol/https10.pm (100%) rename lib/{site => fallback}/LWP/Protocol/loopback.pm (100%) rename lib/{site => fallback}/LWP/Protocol/mailto.pm (100%) rename lib/{site => fallback}/LWP/Protocol/nntp.pm (100%) rename lib/{site => fallback}/LWP/Protocol/nogo.pm (100%) rename lib/{site => fallback}/LWP/RobotUA.pm (100%) rename lib/{site => fallback}/LWP/Simple.pm (100%) rename lib/{site => fallback}/LWP/UserAgent.pm (100%) rename lib/{site => fallback}/LWP/media.types (100%) rename lib/{site => fallback}/Lingua/EN/Numbers.pm (100%) rename lib/{site => fallback}/Lingua/ES/Numeros.pm (100%) rename lib/{site => fallback}/Lingua/FR/Numbers.pm (100%) rename lib/{site => fallback}/Lingua/IT/Numbers.pm (100%) rename lib/{site => fallback}/Lingua/Num2Word.pm (100%) rename lib/{site => fallback}/MIME/Lite.pm (100%) rename lib/{site => fallback}/Net/AOLIM.pm (100%) rename lib/{site => fallback}/Net/Cmd.pm (100%) rename lib/{site => fallback}/Net/Config.pm (100%) rename lib/{site => fallback}/Net/DNS.pm (100%) rename lib/{site => fallback}/Net/DNS/Header.pm (100%) rename lib/{site => fallback}/Net/DNS/Packet.pm (100%) rename lib/{site => fallback}/Net/DNS/Question.pm (100%) rename lib/{site => fallback}/Net/DNS/RR.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/A.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/AAAA.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/AFSDB.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/CNAME.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/EID.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/HINFO.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/ISDN.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/LOC.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/MB.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/MG.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/MINFO.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/MR.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/MX.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/NAPTR.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/NIMLOC.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/NS.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/NSAP.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/NULL.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/PTR.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/PX.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/RP.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/RT.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/SOA.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/SRV.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/TXT.pm (100%) rename lib/{site => fallback}/Net/DNS/RR/X25.pm (100%) rename lib/{site => fallback}/Net/DNS/Resolver.pm (100%) rename lib/{site => fallback}/Net/DNS/Update.pm (100%) rename lib/{site => fallback}/Net/Domain.pm (100%) rename lib/{site => fallback}/Net/DummyInetd.pm (100%) rename lib/{site => fallback}/Net/FTP.pm (100%) rename lib/{site => fallback}/Net/FTP/A.pm (100%) rename lib/{site => fallback}/Net/FTP/E.pm (100%) rename lib/{site => fallback}/Net/FTP/I.pm (100%) rename lib/{site => fallback}/Net/FTP/L.pm (100%) rename lib/{site => fallback}/Net/FTP/dataconn.pm (100%) rename lib/{site => fallback}/Net/HTTP.pm (100%) rename lib/{site => fallback}/Net/HTTP/Methods.pm (100%) rename lib/{site => fallback}/Net/HTTP/NB.pm (100%) rename lib/{site => fallback}/Net/HTTPS.pm (100%) rename lib/{site => fallback}/Net/NNTP.pm (100%) rename lib/{site => fallback}/Net/Netrc.pm (100%) rename lib/{site => fallback}/Net/OSCAR.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Buddylist.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/0/error.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/incoming_extended_information.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/incoming_warning.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/migrate.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/pause.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/rate_change.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/rate_info_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/self_information.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/server_ready.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/service_redirect_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/1/unpause.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/13/chat_navigator_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/14/chat_buddy_arrival.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/14/chat_buddy_departure.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/14/chat_room_status.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/14/incoming_chat_IM.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/16/buddy_icon_downloaded.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/16/buddy_icon_uploaded.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist_3_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist_add.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist_delete.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist_error.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist_modification_acknowledgement.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/buddylist_modify.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/end_buddylist_modifications.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/19/start_buddylist_modifications.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/2/incoming_profile.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/21/ICQ_meta_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/23/authentication_key.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/23/authorization_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/3/buddy_rights_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/3/buddy_signoff.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/3/buddy_status_update.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/4/IM_acknowledgement.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/4/chat_invitation_decline.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/4/incoming_IM.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/4/typing_notification.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/7/admin_request_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/7/confirm_account_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Callbacks/9/BOS_rights_response.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Common.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Connection.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Connection/Chat.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Connection/Direct.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Connection/Server.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Constants.pm (100%) rename lib/{site => fallback}/Net/OSCAR/MethodInfo.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Proxy.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Screenname.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/0/BOS_signon.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/1/personal_info_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/1/rate_acknowledgement.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/1/rate_info_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/1/set_extended_status.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/1/set_service_versions.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/1/set_tool_versions.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/19/buddylist_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/19/buddylist_rights_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/2/get_away.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/2/locate_rights_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/23/initial_signon_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/23/signon.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/3/buddy_rights_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/4/IM_parameter_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/4/add_IM_parameters.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/4/outgoing_IM.pm (100%) rename lib/{site => fallback}/Net/OSCAR/ServerCallbacks/9/BOS_rights_request.pm (100%) rename lib/{site => fallback}/Net/OSCAR/TLV.pm (100%) rename lib/{site => fallback}/Net/OSCAR/Utility.pm (100%) rename lib/{site => fallback}/Net/OSCAR/XML.pm (100%) rename lib/{site => fallback}/Net/OSCAR/XML/Protocol.dtd (100%) rename lib/{site => fallback}/Net/OSCAR/XML/Protocol.parsed-xml (100%) rename lib/{site => fallback}/Net/OSCAR/XML/Protocol.xml (100%) rename lib/{site => fallback}/Net/OSCAR/XML/Template.pm (100%) rename lib/{site => fallback}/Net/OSCAR/_BLInternal.pm (100%) rename lib/{site => fallback}/Net/PH.pm (100%) rename lib/{site => fallback}/Net/POP3.pm (100%) rename lib/{site => fallback}/Net/SMTP.pm (100%) rename lib/{site => fallback}/Net/SMTP_auth.pm (100%) rename lib/{site => fallback}/Net/SNPP.pm (100%) rename lib/{site => fallback}/Net/Time.pm (100%) rename lib/{site => fallback}/Params/Validate.pm (100%) rename lib/{site => fallback}/Params/ValidatePP.pm (100%) rename lib/{site => fallback}/Params/ValidateXS.pm (100%) rename lib/{site => fallback}/RRD/Simple.pm (100%) rename lib/{site => fallback}/RRDTool/Rawish.pm (100%) rename lib/{site => fallback}/Regexp/Common.pm (100%) rename lib/{site => fallback}/Regexp/Common/CC.pm (100%) rename lib/{site => fallback}/Regexp/Common/SEN.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/RFC1035.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/RFC1738.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/RFC1808.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/RFC2384.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/RFC2396.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/RFC2806.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/fax.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/file.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/ftp.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/gopher.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/http.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/news.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/pop.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/prospero.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/tel.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/telnet.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/tv.pm (100%) rename lib/{site => fallback}/Regexp/Common/URI/wais.pm (100%) rename lib/{site => fallback}/Regexp/Common/_support.pm (100%) rename lib/{site => fallback}/Regexp/Common/balanced.pm (100%) rename lib/{site => fallback}/Regexp/Common/comment.pm (100%) rename lib/{site => fallback}/Regexp/Common/delimited.pm (100%) rename lib/{site => fallback}/Regexp/Common/lingua.pm (100%) rename lib/{site => fallback}/Regexp/Common/list.pm (100%) rename lib/{site => fallback}/Regexp/Common/net.pm (100%) rename lib/{site => fallback}/Regexp/Common/number.pm (100%) rename lib/{site => fallback}/Regexp/Common/profanity.pm (100%) rename lib/{site => fallback}/Regexp/Common/whitespace.pm (100%) rename lib/{site => fallback}/Regexp/Common/zip.pm (100%) rename lib/{site => fallback}/SVG.pm (100%) rename lib/{site => fallback}/SVG/DOM.pm (100%) rename lib/{site => fallback}/SVG/Element.pm (100%) rename lib/{site => fallback}/SVG/Extension.pm (100%) rename lib/{site => fallback}/SVG/XML.pm (100%) rename lib/{site => fallback}/Set/Infinite.pm (100%) rename lib/{site => fallback}/Set/Infinite/Arithmetic.pm (100%) rename lib/{site => fallback}/Set/Infinite/Basic.pm (100%) rename lib/{site => fallback}/Set/Infinite/_recurrence.pm (100%) rename lib/{site => fallback}/Text/vFile/asData.pm (100%) rename lib/{site => fallback}/Tie/Hash.pm (100%) rename lib/{site => fallback}/Tie/Hash.pm.original (100%) rename lib/{site => fallback}/Tie/IxHash.pm (100%) rename lib/{site => fallback}/Time/CTime.pm (100%) rename lib/{site => fallback}/Time/DaysInMonth.pm (100%) rename lib/{site => fallback}/Time/JulianDay.pm (100%) rename lib/{site => fallback}/Time/ParseDate.pm (100%) rename lib/{site => fallback}/Time/Timezone.pm (100%) rename lib/{site => fallback}/Time/Zone.pm (100%) rename lib/{site => fallback}/Tk/CursorControl.pm (100%) rename lib/{site => fallback}/Tk/ToolBar.pm (100%) rename lib/{site => fallback}/Tk/ToolBar/tkIcons (100%) rename lib/{site => fallback}/Tk/trans_cur.mask (100%) rename lib/{site => fallback}/Tk/trans_cur.xbm (100%) rename lib/{site => fallback}/URI.pm (100%) rename lib/{site => fallback}/URI/Escape.pm (100%) rename lib/{site => fallback}/URI/Heuristic.pm (100%) rename lib/{site => fallback}/URI/QueryParam.pm (100%) rename lib/{site => fallback}/URI/Split.pm (100%) rename lib/{site => fallback}/URI/URL.pm (100%) rename lib/{site => fallback}/URI/URL/_generic.pm (100%) rename lib/{site => fallback}/URI/URL/_login.pm (100%) rename lib/{site => fallback}/URI/URL/data.pm (100%) rename lib/{site => fallback}/URI/URL/file.pm (100%) rename lib/{site => fallback}/URI/URL/finger.pm (100%) rename lib/{site => fallback}/URI/URL/ftp.pm (100%) rename lib/{site => fallback}/URI/URL/gopher.pm (100%) rename lib/{site => fallback}/URI/URL/http.pm (100%) rename lib/{site => fallback}/URI/URL/https.pm (100%) rename lib/{site => fallback}/URI/URL/mailto.pm (100%) rename lib/{site => fallback}/URI/URL/news.pm (100%) rename lib/{site => fallback}/URI/URL/nntp.pm (100%) rename lib/{site => fallback}/URI/URL/prospero.pm (100%) rename lib/{site => fallback}/URI/URL/rlogin.pm (100%) rename lib/{site => fallback}/URI/URL/telnet.pm (100%) rename lib/{site => fallback}/URI/URL/tn3270.pm (100%) rename lib/{site => fallback}/URI/URL/wais.pm (100%) rename lib/{site => fallback}/URI/URL/webster.pm (100%) rename lib/{site => fallback}/URI/URL/whois.pm (100%) rename lib/{site => fallback}/URI/WithBase.pm (100%) rename lib/{site => fallback}/URI/_foreign.pm (100%) rename lib/{site => fallback}/URI/_generic.pm (100%) rename lib/{site => fallback}/URI/_ldap.pm (100%) rename lib/{site => fallback}/URI/_login.pm (100%) rename lib/{site => fallback}/URI/_query.pm (100%) rename lib/{site => fallback}/URI/_segment.pm (100%) rename lib/{site => fallback}/URI/_server.pm (100%) rename lib/{site => fallback}/URI/_userpass.pm (100%) rename lib/{site => fallback}/URI/data.pm (100%) rename lib/{site => fallback}/URI/file.pm (100%) rename lib/{site => fallback}/URI/file/Base.pm (100%) rename lib/{site => fallback}/URI/file/FAT.pm (100%) rename lib/{site => fallback}/URI/file/Mac.pm (100%) rename lib/{site => fallback}/URI/file/OS2.pm (100%) rename lib/{site => fallback}/URI/file/QNX.pm (100%) rename lib/{site => fallback}/URI/file/Unix.pm (100%) rename lib/{site => fallback}/URI/file/Win32.pm (100%) rename lib/{site => fallback}/URI/ftp.pm (100%) rename lib/{site => fallback}/URI/gopher.pm (100%) rename lib/{site => fallback}/URI/http.pm (100%) rename lib/{site => fallback}/URI/https.pm (100%) rename lib/{site => fallback}/URI/ldap.pm (100%) rename lib/{site => fallback}/URI/ldapi.pm (100%) rename lib/{site => fallback}/URI/ldaps.pm (100%) rename lib/{site => fallback}/URI/mailto.pm (100%) rename lib/{site => fallback}/URI/mms.pm (100%) rename lib/{site => fallback}/URI/news.pm (100%) rename lib/{site => fallback}/URI/nntp.pm (100%) rename lib/{site => fallback}/URI/pop.pm (100%) rename lib/{site => fallback}/URI/rlogin.pm (100%) rename lib/{site => fallback}/URI/rsync.pm (100%) rename lib/{site => fallback}/URI/rtsp.pm (100%) rename lib/{site => fallback}/URI/rtspu.pm (100%) rename lib/{site => fallback}/URI/sip.pm (100%) rename lib/{site => fallback}/URI/sips.pm (100%) rename lib/{site => fallback}/URI/snews.pm (100%) rename lib/{site => fallback}/URI/ssh.pm (100%) rename lib/{site => fallback}/URI/telnet.pm (100%) rename lib/{site => fallback}/URI/tn3270.pm (100%) rename lib/{site => fallback}/URI/urn.pm (100%) rename lib/{site => fallback}/URI/urn/isbn.pm (100%) rename lib/{site => fallback}/URI/urn/oid.pm (100%) rename lib/{site => fallback}/Win32/DUN.pm (100%) rename lib/{site => fallback}/Win32/DriveInfo.pm (100%) rename lib/{site => fallback}/Win32/DriveInfo.pm.html (100%) rename lib/{site => fallback}/Win32/DriveInfo.txt (100%) rename lib/{site => fallback}/Win32/IIPC.pm (100%) rename lib/{site => fallback}/Win32/IPERFSUP.PM (100%) rename lib/{site => fallback}/Win32/IPerfmon.pm (100%) rename lib/{site => fallback}/Win32/IProc.pm (100%) rename lib/{site => fallback}/Win32/ISYNC.PM (100%) rename lib/{site => fallback}/Win32/MemMap.pm (100%) rename lib/{site => fallback}/Win32/SerialPort.html (100%) rename lib/{site => fallback}/Win32/SerialPort.pm (100%) rename lib/{site => fallback}/Win32/SerialPort.pm.original (100%) rename lib/{site => fallback}/Win32/SerialPort.txt (100%) rename lib/{site => fallback}/Win32/SoundEx.pm (100%) rename lib/{site => fallback}/Win32/TieRegistry.pm (100%) rename lib/{site => fallback}/Win32/dun.txt (100%) rename lib/{site => fallback}/Win32API/CommPort.html (100%) rename lib/{site => fallback}/Win32API/CommPort.pm (100%) rename lib/{site => fallback}/Win32API/Resources.html (100%) rename lib/{site => fallback}/Win32API/Resources.pm (100%) rename lib/{site => fallback}/XML/DOM.pm (100%) rename lib/{site => fallback}/XML/DOM/AttDef.pod (100%) rename lib/{site => fallback}/XML/DOM/AttlistDecl.pod (100%) rename lib/{site => fallback}/XML/DOM/Attr.pod (100%) rename lib/{site => fallback}/XML/DOM/CDATASection.pod (100%) rename lib/{site => fallback}/XML/DOM/CharacterData.pod (100%) rename lib/{site => fallback}/XML/DOM/Comment.pod (100%) rename lib/{site => fallback}/XML/DOM/DOMException.pm (100%) rename lib/{site => fallback}/XML/DOM/DOMImplementation.pod (100%) rename lib/{site => fallback}/XML/DOM/Document.pod (100%) rename lib/{site => fallback}/XML/DOM/DocumentFragment.pod (100%) rename lib/{site => fallback}/XML/DOM/DocumentType.pod (100%) rename lib/{site => fallback}/XML/DOM/Element.pod (100%) rename lib/{site => fallback}/XML/DOM/ElementDecl.pod (100%) rename lib/{site => fallback}/XML/DOM/Entity.pod (100%) rename lib/{site => fallback}/XML/DOM/EntityReference.pod (100%) rename lib/{site => fallback}/XML/DOM/NamedNodeMap.pm (100%) rename lib/{site => fallback}/XML/DOM/NamedNodeMap.pod (100%) rename lib/{site => fallback}/XML/DOM/Node.pod (100%) rename lib/{site => fallback}/XML/DOM/NodeList.pm (100%) rename lib/{site => fallback}/XML/DOM/NodeList.pod (100%) rename lib/{site => fallback}/XML/DOM/Notation.pod (100%) rename lib/{site => fallback}/XML/DOM/Parser.pod (100%) rename lib/{site => fallback}/XML/DOM/PerlSAX.pm (100%) rename lib/{site => fallback}/XML/DOM/ProcessingInstruction.pod (100%) rename lib/{site => fallback}/XML/DOM/Text.pod (100%) rename lib/{site => fallback}/XML/DOM/XMLDecl.pod (100%) rename lib/{site => fallback}/XML/ESISParser.pm (100%) rename lib/{site => fallback}/XML/Elemental.pm (100%) rename lib/{site => fallback}/XML/Elemental/Characters.pm (100%) rename lib/{site => fallback}/XML/Elemental/Document.pm (100%) rename lib/{site => fallback}/XML/Elemental/Element.pm (100%) rename lib/{site => fallback}/XML/Elemental/Node.pm (100%) rename lib/{site => fallback}/XML/Elemental/SAXHandler.pm (100%) rename lib/{site => fallback}/XML/Elemental/Util.pm (100%) rename lib/{site => fallback}/XML/Handler/BuildDOM.pm (100%) rename lib/{site => fallback}/XML/Handler/CanonXMLWriter.pm (100%) rename lib/{site => fallback}/XML/Handler/Sample.pm (100%) rename lib/{site => fallback}/XML/Handler/Subs.pm (100%) rename lib/{site => fallback}/XML/Handler/XMLWriter.pm (100%) rename lib/{site => fallback}/XML/NamespaceSupport.pm (100%) rename lib/{site => fallback}/XML/Parser/PerlSAX.pm (100%) rename lib/{site => fallback}/XML/Parser/Style/Elemental.pm (100%) rename lib/{site => fallback}/XML/PatAct/ActionTempl.pm (100%) rename lib/{site => fallback}/XML/PatAct/Amsterdam.pm (100%) rename lib/{site => fallback}/XML/PatAct/MatchName.pm (100%) rename lib/{site => fallback}/XML/PatAct/PatternTempl.pm (100%) rename lib/{site => fallback}/XML/PatAct/ToObjects.pm (100%) rename lib/{site => fallback}/XML/Perl2SAX.pm (100%) rename lib/{site => fallback}/XML/RAI.pm (100%) rename lib/{site => fallback}/XML/RAI/Channel.pm (100%) rename lib/{site => fallback}/XML/RAI/Enclosure.pm (100%) rename lib/{site => fallback}/XML/RAI/Image.pm (100%) rename lib/{site => fallback}/XML/RAI/Item.pm (100%) rename lib/{site => fallback}/XML/RAI/Object.pm (100%) rename lib/{site => fallback}/XML/RSS.pm (100%) rename lib/{site => fallback}/XML/RSS/Parser.pm (100%) rename lib/{site => fallback}/XML/RSS/Parser/Characters.pm (100%) rename lib/{site => fallback}/XML/RSS/Parser/Element.pm (100%) rename lib/{site => fallback}/XML/RSS/Parser/Feed.pm (100%) rename lib/{site => fallback}/XML/RSS/Parser/Util.pm (100%) rename lib/{site => fallback}/XML/RegExp.pm (100%) rename lib/{site => fallback}/XML/SAX.pm (100%) rename lib/{site => fallback}/XML/SAX/Base.pm (100%) rename lib/{site => fallback}/XML/SAX/DocumentLocator.pm (100%) rename lib/{site => fallback}/XML/SAX/Exception.pm (100%) rename lib/{site => fallback}/XML/SAX/Intro.pod (100%) rename lib/{site => fallback}/XML/SAX/ParserDetails.ini (100%) rename lib/{site => fallback}/XML/SAX/ParserFactory.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/DTDDecls.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/DebugHandler.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/DocType.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/EncodingDetect.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Exception.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/NoUnicodeExt.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Productions.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Reader.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Reader/NoUnicodeExt.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Reader/Stream.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Reader/String.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Reader/URI.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/Reader/UnicodeExt.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/UnicodeExt.pm (100%) rename lib/{site => fallback}/XML/SAX/PurePerl/XMLDecl.pm (100%) rename lib/{site => fallback}/XML/SAX/placeholder.pl (100%) rename lib/{site => fallback}/XML/SAX2Perl.pm (100%) rename lib/{site => fallback}/XML/Twig.pm (100%) rename lib/{site => fallback}/XML/XPath.pm (100%) rename lib/{site => fallback}/XML/XPath/Boolean.pm (100%) rename lib/{site => fallback}/XML/XPath/Builder.pm (100%) rename lib/{site => fallback}/XML/XPath/Expr.pm (100%) rename lib/{site => fallback}/XML/XPath/Function.pm (100%) rename lib/{site => fallback}/XML/XPath/Literal.pm (100%) rename lib/{site => fallback}/XML/XPath/LocationPath.pm (100%) rename lib/{site => fallback}/XML/XPath/Node.pm (100%) rename lib/{site => fallback}/XML/XPath/Node/Attribute.pm (100%) rename lib/{site => fallback}/XML/XPath/Node/Comment.pm (100%) rename lib/{site => fallback}/XML/XPath/Node/Element.pm (100%) rename lib/{site => fallback}/XML/XPath/Node/Namespace.pm (100%) rename lib/{site => fallback}/XML/XPath/Node/PI.pm (100%) rename lib/{site => fallback}/XML/XPath/Node/Text.pm (100%) rename lib/{site => fallback}/XML/XPath/NodeSet.pm (100%) rename lib/{site => fallback}/XML/XPath/Number.pm (100%) rename lib/{site => fallback}/XML/XPath/Parser.pm (100%) rename lib/{site => fallback}/XML/XPath/PerlSAX.pm (100%) rename lib/{site => fallback}/XML/XPath/Root.pm (100%) rename lib/{site => fallback}/XML/XPath/Step.pm (100%) rename lib/{site => fallback}/XML/XPath/Variable.pm (100%) rename lib/{site => fallback}/XML/XPath/XMLParser.pm (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/_need_proxy.al (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/autosplit.ix (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/clone.al (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/env_proxy.al (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/is_protocol_supported.al (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/mirror.al (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/no_proxy.al (100%) rename lib/{site => fallback}/auto/LWP/UserAgent/proxy.al (100%) rename lib/{site => fallback}/auto/Net-DNS/.packlist (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/_netloc_elem.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/abs.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/autosplit.ix (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/crack.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/eparams.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/epath.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/eq.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/equery.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/frag.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/host.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/params.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/password.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/path.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/path_components.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/port.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/query.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/rel.al (100%) rename lib/{site => fallback}/auto/URI/URL/_generic/user.al (100%) rename lib/{site => fallback}/auto/URI/URL/abs.al (100%) rename lib/{site => fallback}/auto/URI/URL/as_string.al (100%) rename lib/{site => fallback}/auto/URI/URL/autosplit.ix (100%) rename lib/{site => fallback}/auto/URI/URL/bad_method.al (100%) rename lib/{site => fallback}/auto/URI/URL/base.al (100%) rename lib/{site => fallback}/auto/URI/URL/crack.al (100%) rename lib/{site => fallback}/auto/URI/URL/eq.al (100%) rename lib/{site => fallback}/auto/URI/URL/file/autosplit.ix (100%) rename lib/{site => fallback}/auto/URI/URL/file/dos_path.al (100%) rename lib/{site => fallback}/auto/URI/URL/file/mac_path.al (100%) rename lib/{site => fallback}/auto/URI/URL/file/newlocal.al (100%) rename lib/{site => fallback}/auto/URI/URL/file/unix_path.al (100%) rename lib/{site => fallback}/auto/URI/URL/file/vms_path.al (100%) rename lib/{site => fallback}/auto/URI/URL/http/autosplit.ix (100%) rename lib/{site => fallback}/auto/URI/URL/http/keywords.al (100%) rename lib/{site => fallback}/auto/URI/URL/http/query_form.al (100%) rename lib/{site => fallback}/auto/URI/URL/newlocal.al (100%) rename lib/{site => fallback}/auto/URI/URL/print_on.al (100%) rename lib/{site => fallback}/auto/URI/URL/rel.al (100%) rename lib/{site => fallback}/auto/URI/URL/scheme.al (100%) rename lib/{site => fallback}/auto/URI/URL/strict.al (100%) rename lib/{site => fallback}/auto/Win32/MemMap/memmap.dll (100%) rename lib/{site => fallback}/auto/http/autosplit.ix (100%) rename lib/{site => fallback}/auto/http/keywords.al (100%) rename lib/{site => fallback}/auto/http/query_form.al (100%) rename lib/{site => fallback}/enum.pm (100%) rename lib/{site => fallback}/iCal/Parser.pm (100%) diff --git a/bin/mh b/bin/mh index b067b319e..acaac727b 100755 --- a/bin/mh +++ b/bin/mh @@ -209,6 +209,10 @@ BEGIN { # Pick local mh modules first, over any site install ones unshift( @INC, "${Pgm_Path}/../lib", "${Pgm_Path}/../lib/site", '.' ); + # Keep fallback versions of CPAN modules, in case they're not present on the system. Place them last + # so the sysadmin can install more current versions in the normal Perl places if they wish. + push @INC,"${Pgm_Path}/../lib/fallback"; + # my $pwd=cwd(); print "pwd=$pwd inc=@INC\n"; # push (@INC, './../lib', './../lib/site', '.'); # push (@INC, './../lib'); diff --git a/lib/site/Astro/MoonPhase.pm b/lib/fallback/Astro/MoonPhase.pm similarity index 100% rename from lib/site/Astro/MoonPhase.pm rename to lib/fallback/Astro/MoonPhase.pm diff --git a/lib/site/Authen/SASL.pm b/lib/fallback/Authen/SASL.pm similarity index 100% rename from lib/site/Authen/SASL.pm rename to lib/fallback/Authen/SASL.pm diff --git a/lib/site/Authen/SASL.pod b/lib/fallback/Authen/SASL.pod similarity index 100% rename from lib/site/Authen/SASL.pod rename to lib/fallback/Authen/SASL.pod diff --git a/lib/site/Authen/SASL/CRAM_MD5.pm b/lib/fallback/Authen/SASL/CRAM_MD5.pm similarity index 100% rename from lib/site/Authen/SASL/CRAM_MD5.pm rename to lib/fallback/Authen/SASL/CRAM_MD5.pm diff --git a/lib/site/Authen/SASL/EXTERNAL.pm b/lib/fallback/Authen/SASL/EXTERNAL.pm similarity index 100% rename from lib/site/Authen/SASL/EXTERNAL.pm rename to lib/fallback/Authen/SASL/EXTERNAL.pm diff --git a/lib/site/Authen/SASL/Perl.pm b/lib/fallback/Authen/SASL/Perl.pm similarity index 100% rename from lib/site/Authen/SASL/Perl.pm rename to lib/fallback/Authen/SASL/Perl.pm diff --git a/lib/site/Authen/SASL/Perl.pod b/lib/fallback/Authen/SASL/Perl.pod similarity index 100% rename from lib/site/Authen/SASL/Perl.pod rename to lib/fallback/Authen/SASL/Perl.pod diff --git a/lib/site/Authen/SASL/Perl/ANONYMOUS.pm b/lib/fallback/Authen/SASL/Perl/ANONYMOUS.pm similarity index 100% rename from lib/site/Authen/SASL/Perl/ANONYMOUS.pm rename to lib/fallback/Authen/SASL/Perl/ANONYMOUS.pm diff --git a/lib/site/Authen/SASL/Perl/CRAM_MD5.pm b/lib/fallback/Authen/SASL/Perl/CRAM_MD5.pm similarity index 100% rename from lib/site/Authen/SASL/Perl/CRAM_MD5.pm rename to lib/fallback/Authen/SASL/Perl/CRAM_MD5.pm diff --git a/lib/site/Authen/SASL/Perl/DIGEST_MD5.pm b/lib/fallback/Authen/SASL/Perl/DIGEST_MD5.pm similarity index 100% rename from lib/site/Authen/SASL/Perl/DIGEST_MD5.pm rename to lib/fallback/Authen/SASL/Perl/DIGEST_MD5.pm diff --git a/lib/site/Authen/SASL/Perl/EXTERNAL.pm b/lib/fallback/Authen/SASL/Perl/EXTERNAL.pm similarity index 100% rename from lib/site/Authen/SASL/Perl/EXTERNAL.pm rename to lib/fallback/Authen/SASL/Perl/EXTERNAL.pm diff --git a/lib/site/Authen/SASL/Perl/LOGIN.pm b/lib/fallback/Authen/SASL/Perl/LOGIN.pm similarity index 100% rename from lib/site/Authen/SASL/Perl/LOGIN.pm rename to lib/fallback/Authen/SASL/Perl/LOGIN.pm diff --git a/lib/site/Authen/SASL/Perl/PLAIN.pm b/lib/fallback/Authen/SASL/Perl/PLAIN.pm similarity index 100% rename from lib/site/Authen/SASL/Perl/PLAIN.pm rename to lib/fallback/Authen/SASL/Perl/PLAIN.pm diff --git a/lib/site/CDDB.pm b/lib/fallback/CDDB.pm similarity index 100% rename from lib/site/CDDB.pm rename to lib/fallback/CDDB.pm diff --git a/lib/site/Capture/Tiny.pm b/lib/fallback/Capture/Tiny.pm similarity index 100% rename from lib/site/Capture/Tiny.pm rename to lib/fallback/Capture/Tiny.pm diff --git a/lib/site/Class/Accessor.pm b/lib/fallback/Class/Accessor.pm similarity index 100% rename from lib/site/Class/Accessor.pm rename to lib/fallback/Class/Accessor.pm diff --git a/lib/site/Class/Accessor/Chained/Fast.pm b/lib/fallback/Class/Accessor/Chained/Fast.pm similarity index 100% rename from lib/site/Class/Accessor/Chained/Fast.pm rename to lib/fallback/Class/Accessor/Chained/Fast.pm diff --git a/lib/site/Class/Accessor/Fast.pm b/lib/fallback/Class/Accessor/Fast.pm similarity index 100% rename from lib/site/Class/Accessor/Fast.pm rename to lib/fallback/Class/Accessor/Fast.pm diff --git a/lib/site/Class/ErrorHandler.pm b/lib/fallback/Class/ErrorHandler.pm similarity index 100% rename from lib/site/Class/ErrorHandler.pm rename to lib/fallback/Class/ErrorHandler.pm diff --git a/lib/site/Class/Singleton.pm b/lib/fallback/Class/Singleton.pm similarity index 100% rename from lib/site/Class/Singleton.pm rename to lib/fallback/Class/Singleton.pm diff --git a/lib/site/Class/XPath.pm b/lib/fallback/Class/XPath.pm similarity index 100% rename from lib/site/Class/XPath.pm rename to lib/fallback/Class/XPath.pm diff --git a/lib/site/ControlX10/CM11.pm b/lib/fallback/ControlX10/CM11.pm similarity index 100% rename from lib/site/ControlX10/CM11.pm rename to lib/fallback/ControlX10/CM11.pm diff --git a/lib/site/ControlX10/CM11.pm.new b/lib/fallback/ControlX10/CM11.pm.new similarity index 100% rename from lib/site/ControlX10/CM11.pm.new rename to lib/fallback/ControlX10/CM11.pm.new diff --git a/lib/site/ControlX10/CM11.pm.old b/lib/fallback/ControlX10/CM11.pm.old similarity index 100% rename from lib/site/ControlX10/CM11.pm.old rename to lib/fallback/ControlX10/CM11.pm.old diff --git a/lib/site/ControlX10/CM11.txt b/lib/fallback/ControlX10/CM11.txt similarity index 100% rename from lib/site/ControlX10/CM11.txt rename to lib/fallback/ControlX10/CM11.txt diff --git a/lib/site/ControlX10/CM17.pm b/lib/fallback/ControlX10/CM17.pm similarity index 100% rename from lib/site/ControlX10/CM17.pm rename to lib/fallback/ControlX10/CM17.pm diff --git a/lib/site/ControlX10/CM17.pm.old b/lib/fallback/ControlX10/CM17.pm.old similarity index 100% rename from lib/site/ControlX10/CM17.pm.old rename to lib/fallback/ControlX10/CM17.pm.old diff --git a/lib/site/ControlX10/CM17.txt b/lib/fallback/ControlX10/CM17.txt similarity index 100% rename from lib/site/ControlX10/CM17.txt rename to lib/fallback/ControlX10/CM17.txt diff --git a/lib/site/Date/Format.pm b/lib/fallback/Date/Format.pm similarity index 100% rename from lib/site/Date/Format.pm rename to lib/fallback/Date/Format.pm diff --git a/lib/site/Date/Language.pm b/lib/fallback/Date/Language.pm similarity index 100% rename from lib/site/Date/Language.pm rename to lib/fallback/Date/Language.pm diff --git a/lib/site/Date/Language/Afar.pm b/lib/fallback/Date/Language/Afar.pm similarity index 100% rename from lib/site/Date/Language/Afar.pm rename to lib/fallback/Date/Language/Afar.pm diff --git a/lib/site/Date/Language/Amharic.pm b/lib/fallback/Date/Language/Amharic.pm similarity index 100% rename from lib/site/Date/Language/Amharic.pm rename to lib/fallback/Date/Language/Amharic.pm diff --git a/lib/site/Date/Language/Austrian.pm b/lib/fallback/Date/Language/Austrian.pm similarity index 100% rename from lib/site/Date/Language/Austrian.pm rename to lib/fallback/Date/Language/Austrian.pm diff --git a/lib/site/Date/Language/Brazilian.pm b/lib/fallback/Date/Language/Brazilian.pm similarity index 100% rename from lib/site/Date/Language/Brazilian.pm rename to lib/fallback/Date/Language/Brazilian.pm diff --git a/lib/site/Date/Language/Chinese_GB.pm b/lib/fallback/Date/Language/Chinese_GB.pm similarity index 100% rename from lib/site/Date/Language/Chinese_GB.pm rename to lib/fallback/Date/Language/Chinese_GB.pm diff --git a/lib/site/Date/Language/Czech.pm b/lib/fallback/Date/Language/Czech.pm similarity index 100% rename from lib/site/Date/Language/Czech.pm rename to lib/fallback/Date/Language/Czech.pm diff --git a/lib/site/Date/Language/Danish.pm b/lib/fallback/Date/Language/Danish.pm similarity index 100% rename from lib/site/Date/Language/Danish.pm rename to lib/fallback/Date/Language/Danish.pm diff --git a/lib/site/Date/Language/Dutch.pm b/lib/fallback/Date/Language/Dutch.pm similarity index 100% rename from lib/site/Date/Language/Dutch.pm rename to lib/fallback/Date/Language/Dutch.pm diff --git a/lib/site/Date/Language/English.pm b/lib/fallback/Date/Language/English.pm similarity index 100% rename from lib/site/Date/Language/English.pm rename to lib/fallback/Date/Language/English.pm diff --git a/lib/site/Date/Language/Finnish.pm b/lib/fallback/Date/Language/Finnish.pm similarity index 100% rename from lib/site/Date/Language/Finnish.pm rename to lib/fallback/Date/Language/Finnish.pm diff --git a/lib/site/Date/Language/French.pm b/lib/fallback/Date/Language/French.pm similarity index 100% rename from lib/site/Date/Language/French.pm rename to lib/fallback/Date/Language/French.pm diff --git a/lib/site/Date/Language/Gedeo.pm b/lib/fallback/Date/Language/Gedeo.pm similarity index 100% rename from lib/site/Date/Language/Gedeo.pm rename to lib/fallback/Date/Language/Gedeo.pm diff --git a/lib/site/Date/Language/German.pm b/lib/fallback/Date/Language/German.pm similarity index 100% rename from lib/site/Date/Language/German.pm rename to lib/fallback/Date/Language/German.pm diff --git a/lib/site/Date/Language/Greek.pm b/lib/fallback/Date/Language/Greek.pm similarity index 100% rename from lib/site/Date/Language/Greek.pm rename to lib/fallback/Date/Language/Greek.pm diff --git a/lib/site/Date/Language/Italian.pm b/lib/fallback/Date/Language/Italian.pm similarity index 100% rename from lib/site/Date/Language/Italian.pm rename to lib/fallback/Date/Language/Italian.pm diff --git a/lib/site/Date/Language/Norwegian.pm b/lib/fallback/Date/Language/Norwegian.pm similarity index 100% rename from lib/site/Date/Language/Norwegian.pm rename to lib/fallback/Date/Language/Norwegian.pm diff --git a/lib/site/Date/Language/Oromo.pm b/lib/fallback/Date/Language/Oromo.pm similarity index 100% rename from lib/site/Date/Language/Oromo.pm rename to lib/fallback/Date/Language/Oromo.pm diff --git a/lib/site/Date/Language/Sidama.pm b/lib/fallback/Date/Language/Sidama.pm similarity index 100% rename from lib/site/Date/Language/Sidama.pm rename to lib/fallback/Date/Language/Sidama.pm diff --git a/lib/site/Date/Language/Somali.pm b/lib/fallback/Date/Language/Somali.pm similarity index 100% rename from lib/site/Date/Language/Somali.pm rename to lib/fallback/Date/Language/Somali.pm diff --git a/lib/site/Date/Language/Swedish.pm b/lib/fallback/Date/Language/Swedish.pm similarity index 100% rename from lib/site/Date/Language/Swedish.pm rename to lib/fallback/Date/Language/Swedish.pm diff --git a/lib/site/Date/Language/Tigrinya.pm b/lib/fallback/Date/Language/Tigrinya.pm similarity index 100% rename from lib/site/Date/Language/Tigrinya.pm rename to lib/fallback/Date/Language/Tigrinya.pm diff --git a/lib/site/Date/Language/TigrinyaEritrean.pm b/lib/fallback/Date/Language/TigrinyaEritrean.pm similarity index 100% rename from lib/site/Date/Language/TigrinyaEritrean.pm rename to lib/fallback/Date/Language/TigrinyaEritrean.pm diff --git a/lib/site/Date/Language/TigrinyaEthiopian.pm b/lib/fallback/Date/Language/TigrinyaEthiopian.pm similarity index 100% rename from lib/site/Date/Language/TigrinyaEthiopian.pm rename to lib/fallback/Date/Language/TigrinyaEthiopian.pm diff --git a/lib/site/Date/Parse.pm b/lib/fallback/Date/Parse.pm similarity index 100% rename from lib/site/Date/Parse.pm rename to lib/fallback/Date/Parse.pm diff --git a/lib/site/DateTime.pm b/lib/fallback/DateTime.pm similarity index 100% rename from lib/site/DateTime.pm rename to lib/fallback/DateTime.pm diff --git a/lib/site/DateTime/Duration.pm b/lib/fallback/DateTime/Duration.pm similarity index 100% rename from lib/site/DateTime/Duration.pm rename to lib/fallback/DateTime/Duration.pm diff --git a/lib/site/DateTime/Event/ICal.pm b/lib/fallback/DateTime/Event/ICal.pm similarity index 100% rename from lib/site/DateTime/Event/ICal.pm rename to lib/fallback/DateTime/Event/ICal.pm diff --git a/lib/site/DateTime/Event/Recurrence.pm b/lib/fallback/DateTime/Event/Recurrence.pm similarity index 100% rename from lib/site/DateTime/Event/Recurrence.pm rename to lib/fallback/DateTime/Event/Recurrence.pm diff --git a/lib/site/DateTime/Format/ICal.pm b/lib/fallback/DateTime/Format/ICal.pm similarity index 100% rename from lib/site/DateTime/Format/ICal.pm rename to lib/fallback/DateTime/Format/ICal.pm diff --git a/lib/site/DateTime/Helpers.pm b/lib/fallback/DateTime/Helpers.pm similarity index 100% rename from lib/site/DateTime/Helpers.pm rename to lib/fallback/DateTime/Helpers.pm diff --git a/lib/site/DateTime/Infinite.pm b/lib/fallback/DateTime/Infinite.pm similarity index 100% rename from lib/site/DateTime/Infinite.pm rename to lib/fallback/DateTime/Infinite.pm diff --git a/lib/site/DateTime/LeapSecond.pm b/lib/fallback/DateTime/LeapSecond.pm similarity index 100% rename from lib/site/DateTime/LeapSecond.pm rename to lib/fallback/DateTime/LeapSecond.pm diff --git a/lib/site/DateTime/Locale/Base.pm b/lib/fallback/DateTime/Locale/Base.pm similarity index 100% rename from lib/site/DateTime/Locale/Base.pm rename to lib/fallback/DateTime/Locale/Base.pm diff --git a/lib/site/DateTime/Locale/aa.pm b/lib/fallback/DateTime/Locale/aa.pm similarity index 100% rename from lib/site/DateTime/Locale/aa.pm rename to lib/fallback/DateTime/Locale/aa.pm diff --git a/lib/site/DateTime/Locale/aa_ER_SAAHO.pm b/lib/fallback/DateTime/Locale/aa_ER_SAAHO.pm similarity index 100% rename from lib/site/DateTime/Locale/aa_ER_SAAHO.pm rename to lib/fallback/DateTime/Locale/aa_ER_SAAHO.pm diff --git a/lib/site/DateTime/Locale/af.pm b/lib/fallback/DateTime/Locale/af.pm similarity index 100% rename from lib/site/DateTime/Locale/af.pm rename to lib/fallback/DateTime/Locale/af.pm diff --git a/lib/site/DateTime/Locale/af_ZA.pm b/lib/fallback/DateTime/Locale/af_ZA.pm similarity index 100% rename from lib/site/DateTime/Locale/af_ZA.pm rename to lib/fallback/DateTime/Locale/af_ZA.pm diff --git a/lib/site/DateTime/Locale/ak.pm b/lib/fallback/DateTime/Locale/ak.pm similarity index 100% rename from lib/site/DateTime/Locale/ak.pm rename to lib/fallback/DateTime/Locale/ak.pm diff --git a/lib/site/DateTime/Locale/am.pm b/lib/fallback/DateTime/Locale/am.pm similarity index 100% rename from lib/site/DateTime/Locale/am.pm rename to lib/fallback/DateTime/Locale/am.pm diff --git a/lib/site/DateTime/Locale/am_ET.pm b/lib/fallback/DateTime/Locale/am_ET.pm similarity index 100% rename from lib/site/DateTime/Locale/am_ET.pm rename to lib/fallback/DateTime/Locale/am_ET.pm diff --git a/lib/site/DateTime/Locale/ar.pm b/lib/fallback/DateTime/Locale/ar.pm similarity index 100% rename from lib/site/DateTime/Locale/ar.pm rename to lib/fallback/DateTime/Locale/ar.pm diff --git a/lib/site/DateTime/Locale/ar_EG.pm b/lib/fallback/DateTime/Locale/ar_EG.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_EG.pm rename to lib/fallback/DateTime/Locale/ar_EG.pm diff --git a/lib/site/DateTime/Locale/ar_JO.pm b/lib/fallback/DateTime/Locale/ar_JO.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_JO.pm rename to lib/fallback/DateTime/Locale/ar_JO.pm diff --git a/lib/site/DateTime/Locale/ar_LB.pm b/lib/fallback/DateTime/Locale/ar_LB.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_LB.pm rename to lib/fallback/DateTime/Locale/ar_LB.pm diff --git a/lib/site/DateTime/Locale/ar_QA.pm b/lib/fallback/DateTime/Locale/ar_QA.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_QA.pm rename to lib/fallback/DateTime/Locale/ar_QA.pm diff --git a/lib/site/DateTime/Locale/ar_SA.pm b/lib/fallback/DateTime/Locale/ar_SA.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_SA.pm rename to lib/fallback/DateTime/Locale/ar_SA.pm diff --git a/lib/site/DateTime/Locale/ar_SY.pm b/lib/fallback/DateTime/Locale/ar_SY.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_SY.pm rename to lib/fallback/DateTime/Locale/ar_SY.pm diff --git a/lib/site/DateTime/Locale/ar_TN.pm b/lib/fallback/DateTime/Locale/ar_TN.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_TN.pm rename to lib/fallback/DateTime/Locale/ar_TN.pm diff --git a/lib/site/DateTime/Locale/ar_YE.pm b/lib/fallback/DateTime/Locale/ar_YE.pm similarity index 100% rename from lib/site/DateTime/Locale/ar_YE.pm rename to lib/fallback/DateTime/Locale/ar_YE.pm diff --git a/lib/site/DateTime/Locale/as.pm b/lib/fallback/DateTime/Locale/as.pm similarity index 100% rename from lib/site/DateTime/Locale/as.pm rename to lib/fallback/DateTime/Locale/as.pm diff --git a/lib/site/DateTime/Locale/as_IN.pm b/lib/fallback/DateTime/Locale/as_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/as_IN.pm rename to lib/fallback/DateTime/Locale/as_IN.pm diff --git a/lib/site/DateTime/Locale/az.pm b/lib/fallback/DateTime/Locale/az.pm similarity index 100% rename from lib/site/DateTime/Locale/az.pm rename to lib/fallback/DateTime/Locale/az.pm diff --git a/lib/site/DateTime/Locale/az_Cyrl.pm b/lib/fallback/DateTime/Locale/az_Cyrl.pm similarity index 100% rename from lib/site/DateTime/Locale/az_Cyrl.pm rename to lib/fallback/DateTime/Locale/az_Cyrl.pm diff --git a/lib/site/DateTime/Locale/be.pm b/lib/fallback/DateTime/Locale/be.pm similarity index 100% rename from lib/site/DateTime/Locale/be.pm rename to lib/fallback/DateTime/Locale/be.pm diff --git a/lib/site/DateTime/Locale/bg.pm b/lib/fallback/DateTime/Locale/bg.pm similarity index 100% rename from lib/site/DateTime/Locale/bg.pm rename to lib/fallback/DateTime/Locale/bg.pm diff --git a/lib/site/DateTime/Locale/bg_BG.pm b/lib/fallback/DateTime/Locale/bg_BG.pm similarity index 100% rename from lib/site/DateTime/Locale/bg_BG.pm rename to lib/fallback/DateTime/Locale/bg_BG.pm diff --git a/lib/site/DateTime/Locale/bn.pm b/lib/fallback/DateTime/Locale/bn.pm similarity index 100% rename from lib/site/DateTime/Locale/bn.pm rename to lib/fallback/DateTime/Locale/bn.pm diff --git a/lib/site/DateTime/Locale/bn_BD.pm b/lib/fallback/DateTime/Locale/bn_BD.pm similarity index 100% rename from lib/site/DateTime/Locale/bn_BD.pm rename to lib/fallback/DateTime/Locale/bn_BD.pm diff --git a/lib/site/DateTime/Locale/bn_IN.pm b/lib/fallback/DateTime/Locale/bn_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/bn_IN.pm rename to lib/fallback/DateTime/Locale/bn_IN.pm diff --git a/lib/site/DateTime/Locale/bs.pm b/lib/fallback/DateTime/Locale/bs.pm similarity index 100% rename from lib/site/DateTime/Locale/bs.pm rename to lib/fallback/DateTime/Locale/bs.pm diff --git a/lib/site/DateTime/Locale/byn.pm b/lib/fallback/DateTime/Locale/byn.pm similarity index 100% rename from lib/site/DateTime/Locale/byn.pm rename to lib/fallback/DateTime/Locale/byn.pm diff --git a/lib/site/DateTime/Locale/byn_ER.pm b/lib/fallback/DateTime/Locale/byn_ER.pm similarity index 100% rename from lib/site/DateTime/Locale/byn_ER.pm rename to lib/fallback/DateTime/Locale/byn_ER.pm diff --git a/lib/site/DateTime/Locale/ca.pm b/lib/fallback/DateTime/Locale/ca.pm similarity index 100% rename from lib/site/DateTime/Locale/ca.pm rename to lib/fallback/DateTime/Locale/ca.pm diff --git a/lib/site/DateTime/Locale/cch.pm b/lib/fallback/DateTime/Locale/cch.pm similarity index 100% rename from lib/site/DateTime/Locale/cch.pm rename to lib/fallback/DateTime/Locale/cch.pm diff --git a/lib/site/DateTime/Locale/cs.pm b/lib/fallback/DateTime/Locale/cs.pm similarity index 100% rename from lib/site/DateTime/Locale/cs.pm rename to lib/fallback/DateTime/Locale/cs.pm diff --git a/lib/site/DateTime/Locale/cy.pm b/lib/fallback/DateTime/Locale/cy.pm similarity index 100% rename from lib/site/DateTime/Locale/cy.pm rename to lib/fallback/DateTime/Locale/cy.pm diff --git a/lib/site/DateTime/Locale/cy_GB.pm b/lib/fallback/DateTime/Locale/cy_GB.pm similarity index 100% rename from lib/site/DateTime/Locale/cy_GB.pm rename to lib/fallback/DateTime/Locale/cy_GB.pm diff --git a/lib/site/DateTime/Locale/da.pm b/lib/fallback/DateTime/Locale/da.pm similarity index 100% rename from lib/site/DateTime/Locale/da.pm rename to lib/fallback/DateTime/Locale/da.pm diff --git a/lib/site/DateTime/Locale/de.pm b/lib/fallback/DateTime/Locale/de.pm similarity index 100% rename from lib/site/DateTime/Locale/de.pm rename to lib/fallback/DateTime/Locale/de.pm diff --git a/lib/site/DateTime/Locale/de_AT.pm b/lib/fallback/DateTime/Locale/de_AT.pm similarity index 100% rename from lib/site/DateTime/Locale/de_AT.pm rename to lib/fallback/DateTime/Locale/de_AT.pm diff --git a/lib/site/DateTime/Locale/de_BE.pm b/lib/fallback/DateTime/Locale/de_BE.pm similarity index 100% rename from lib/site/DateTime/Locale/de_BE.pm rename to lib/fallback/DateTime/Locale/de_BE.pm diff --git a/lib/site/DateTime/Locale/dz.pm b/lib/fallback/DateTime/Locale/dz.pm similarity index 100% rename from lib/site/DateTime/Locale/dz.pm rename to lib/fallback/DateTime/Locale/dz.pm diff --git a/lib/site/DateTime/Locale/ee.pm b/lib/fallback/DateTime/Locale/ee.pm similarity index 100% rename from lib/site/DateTime/Locale/ee.pm rename to lib/fallback/DateTime/Locale/ee.pm diff --git a/lib/site/DateTime/Locale/el.pm b/lib/fallback/DateTime/Locale/el.pm similarity index 100% rename from lib/site/DateTime/Locale/el.pm rename to lib/fallback/DateTime/Locale/el.pm diff --git a/lib/site/DateTime/Locale/en.pm b/lib/fallback/DateTime/Locale/en.pm similarity index 100% rename from lib/site/DateTime/Locale/en.pm rename to lib/fallback/DateTime/Locale/en.pm diff --git a/lib/site/DateTime/Locale/en_AU.pm b/lib/fallback/DateTime/Locale/en_AU.pm similarity index 100% rename from lib/site/DateTime/Locale/en_AU.pm rename to lib/fallback/DateTime/Locale/en_AU.pm diff --git a/lib/site/DateTime/Locale/en_BE.pm b/lib/fallback/DateTime/Locale/en_BE.pm similarity index 100% rename from lib/site/DateTime/Locale/en_BE.pm rename to lib/fallback/DateTime/Locale/en_BE.pm diff --git a/lib/site/DateTime/Locale/en_BW.pm b/lib/fallback/DateTime/Locale/en_BW.pm similarity index 100% rename from lib/site/DateTime/Locale/en_BW.pm rename to lib/fallback/DateTime/Locale/en_BW.pm diff --git a/lib/site/DateTime/Locale/en_BZ.pm b/lib/fallback/DateTime/Locale/en_BZ.pm similarity index 100% rename from lib/site/DateTime/Locale/en_BZ.pm rename to lib/fallback/DateTime/Locale/en_BZ.pm diff --git a/lib/site/DateTime/Locale/en_CA.pm b/lib/fallback/DateTime/Locale/en_CA.pm similarity index 100% rename from lib/site/DateTime/Locale/en_CA.pm rename to lib/fallback/DateTime/Locale/en_CA.pm diff --git a/lib/site/DateTime/Locale/en_GB.pm b/lib/fallback/DateTime/Locale/en_GB.pm similarity index 100% rename from lib/site/DateTime/Locale/en_GB.pm rename to lib/fallback/DateTime/Locale/en_GB.pm diff --git a/lib/site/DateTime/Locale/en_HK.pm b/lib/fallback/DateTime/Locale/en_HK.pm similarity index 100% rename from lib/site/DateTime/Locale/en_HK.pm rename to lib/fallback/DateTime/Locale/en_HK.pm diff --git a/lib/site/DateTime/Locale/en_IE.pm b/lib/fallback/DateTime/Locale/en_IE.pm similarity index 100% rename from lib/site/DateTime/Locale/en_IE.pm rename to lib/fallback/DateTime/Locale/en_IE.pm diff --git a/lib/site/DateTime/Locale/en_IN.pm b/lib/fallback/DateTime/Locale/en_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/en_IN.pm rename to lib/fallback/DateTime/Locale/en_IN.pm diff --git a/lib/site/DateTime/Locale/en_MT.pm b/lib/fallback/DateTime/Locale/en_MT.pm similarity index 100% rename from lib/site/DateTime/Locale/en_MT.pm rename to lib/fallback/DateTime/Locale/en_MT.pm diff --git a/lib/site/DateTime/Locale/en_NZ.pm b/lib/fallback/DateTime/Locale/en_NZ.pm similarity index 100% rename from lib/site/DateTime/Locale/en_NZ.pm rename to lib/fallback/DateTime/Locale/en_NZ.pm diff --git a/lib/site/DateTime/Locale/en_PH.pm b/lib/fallback/DateTime/Locale/en_PH.pm similarity index 100% rename from lib/site/DateTime/Locale/en_PH.pm rename to lib/fallback/DateTime/Locale/en_PH.pm diff --git a/lib/site/DateTime/Locale/en_PK.pm b/lib/fallback/DateTime/Locale/en_PK.pm similarity index 100% rename from lib/site/DateTime/Locale/en_PK.pm rename to lib/fallback/DateTime/Locale/en_PK.pm diff --git a/lib/site/DateTime/Locale/en_SG.pm b/lib/fallback/DateTime/Locale/en_SG.pm similarity index 100% rename from lib/site/DateTime/Locale/en_SG.pm rename to lib/fallback/DateTime/Locale/en_SG.pm diff --git a/lib/site/DateTime/Locale/en_ZA.pm b/lib/fallback/DateTime/Locale/en_ZA.pm similarity index 100% rename from lib/site/DateTime/Locale/en_ZA.pm rename to lib/fallback/DateTime/Locale/en_ZA.pm diff --git a/lib/site/DateTime/Locale/en_ZW.pm b/lib/fallback/DateTime/Locale/en_ZW.pm similarity index 100% rename from lib/site/DateTime/Locale/en_ZW.pm rename to lib/fallback/DateTime/Locale/en_ZW.pm diff --git a/lib/site/DateTime/Locale/eo.pm b/lib/fallback/DateTime/Locale/eo.pm similarity index 100% rename from lib/site/DateTime/Locale/eo.pm rename to lib/fallback/DateTime/Locale/eo.pm diff --git a/lib/site/DateTime/Locale/es.pm b/lib/fallback/DateTime/Locale/es.pm similarity index 100% rename from lib/site/DateTime/Locale/es.pm rename to lib/fallback/DateTime/Locale/es.pm diff --git a/lib/site/DateTime/Locale/es_AR.pm b/lib/fallback/DateTime/Locale/es_AR.pm similarity index 100% rename from lib/site/DateTime/Locale/es_AR.pm rename to lib/fallback/DateTime/Locale/es_AR.pm diff --git a/lib/site/DateTime/Locale/es_BO.pm b/lib/fallback/DateTime/Locale/es_BO.pm similarity index 100% rename from lib/site/DateTime/Locale/es_BO.pm rename to lib/fallback/DateTime/Locale/es_BO.pm diff --git a/lib/site/DateTime/Locale/es_CL.pm b/lib/fallback/DateTime/Locale/es_CL.pm similarity index 100% rename from lib/site/DateTime/Locale/es_CL.pm rename to lib/fallback/DateTime/Locale/es_CL.pm diff --git a/lib/site/DateTime/Locale/es_CO.pm b/lib/fallback/DateTime/Locale/es_CO.pm similarity index 100% rename from lib/site/DateTime/Locale/es_CO.pm rename to lib/fallback/DateTime/Locale/es_CO.pm diff --git a/lib/site/DateTime/Locale/es_CR.pm b/lib/fallback/DateTime/Locale/es_CR.pm similarity index 100% rename from lib/site/DateTime/Locale/es_CR.pm rename to lib/fallback/DateTime/Locale/es_CR.pm diff --git a/lib/site/DateTime/Locale/es_DO.pm b/lib/fallback/DateTime/Locale/es_DO.pm similarity index 100% rename from lib/site/DateTime/Locale/es_DO.pm rename to lib/fallback/DateTime/Locale/es_DO.pm diff --git a/lib/site/DateTime/Locale/es_EC.pm b/lib/fallback/DateTime/Locale/es_EC.pm similarity index 100% rename from lib/site/DateTime/Locale/es_EC.pm rename to lib/fallback/DateTime/Locale/es_EC.pm diff --git a/lib/site/DateTime/Locale/es_ES.pm b/lib/fallback/DateTime/Locale/es_ES.pm similarity index 100% rename from lib/site/DateTime/Locale/es_ES.pm rename to lib/fallback/DateTime/Locale/es_ES.pm diff --git a/lib/site/DateTime/Locale/es_GT.pm b/lib/fallback/DateTime/Locale/es_GT.pm similarity index 100% rename from lib/site/DateTime/Locale/es_GT.pm rename to lib/fallback/DateTime/Locale/es_GT.pm diff --git a/lib/site/DateTime/Locale/es_HN.pm b/lib/fallback/DateTime/Locale/es_HN.pm similarity index 100% rename from lib/site/DateTime/Locale/es_HN.pm rename to lib/fallback/DateTime/Locale/es_HN.pm diff --git a/lib/site/DateTime/Locale/es_MX.pm b/lib/fallback/DateTime/Locale/es_MX.pm similarity index 100% rename from lib/site/DateTime/Locale/es_MX.pm rename to lib/fallback/DateTime/Locale/es_MX.pm diff --git a/lib/site/DateTime/Locale/es_NI.pm b/lib/fallback/DateTime/Locale/es_NI.pm similarity index 100% rename from lib/site/DateTime/Locale/es_NI.pm rename to lib/fallback/DateTime/Locale/es_NI.pm diff --git a/lib/site/DateTime/Locale/es_PA.pm b/lib/fallback/DateTime/Locale/es_PA.pm similarity index 100% rename from lib/site/DateTime/Locale/es_PA.pm rename to lib/fallback/DateTime/Locale/es_PA.pm diff --git a/lib/site/DateTime/Locale/es_PR.pm b/lib/fallback/DateTime/Locale/es_PR.pm similarity index 100% rename from lib/site/DateTime/Locale/es_PR.pm rename to lib/fallback/DateTime/Locale/es_PR.pm diff --git a/lib/site/DateTime/Locale/es_PY.pm b/lib/fallback/DateTime/Locale/es_PY.pm similarity index 100% rename from lib/site/DateTime/Locale/es_PY.pm rename to lib/fallback/DateTime/Locale/es_PY.pm diff --git a/lib/site/DateTime/Locale/es_SV.pm b/lib/fallback/DateTime/Locale/es_SV.pm similarity index 100% rename from lib/site/DateTime/Locale/es_SV.pm rename to lib/fallback/DateTime/Locale/es_SV.pm diff --git a/lib/site/DateTime/Locale/es_US.pm b/lib/fallback/DateTime/Locale/es_US.pm similarity index 100% rename from lib/site/DateTime/Locale/es_US.pm rename to lib/fallback/DateTime/Locale/es_US.pm diff --git a/lib/site/DateTime/Locale/es_UY.pm b/lib/fallback/DateTime/Locale/es_UY.pm similarity index 100% rename from lib/site/DateTime/Locale/es_UY.pm rename to lib/fallback/DateTime/Locale/es_UY.pm diff --git a/lib/site/DateTime/Locale/es_VE.pm b/lib/fallback/DateTime/Locale/es_VE.pm similarity index 100% rename from lib/site/DateTime/Locale/es_VE.pm rename to lib/fallback/DateTime/Locale/es_VE.pm diff --git a/lib/site/DateTime/Locale/et.pm b/lib/fallback/DateTime/Locale/et.pm similarity index 100% rename from lib/site/DateTime/Locale/et.pm rename to lib/fallback/DateTime/Locale/et.pm diff --git a/lib/site/DateTime/Locale/et_EE.pm b/lib/fallback/DateTime/Locale/et_EE.pm similarity index 100% rename from lib/site/DateTime/Locale/et_EE.pm rename to lib/fallback/DateTime/Locale/et_EE.pm diff --git a/lib/site/DateTime/Locale/eu.pm b/lib/fallback/DateTime/Locale/eu.pm similarity index 100% rename from lib/site/DateTime/Locale/eu.pm rename to lib/fallback/DateTime/Locale/eu.pm diff --git a/lib/site/DateTime/Locale/eu_ES.pm b/lib/fallback/DateTime/Locale/eu_ES.pm similarity index 100% rename from lib/site/DateTime/Locale/eu_ES.pm rename to lib/fallback/DateTime/Locale/eu_ES.pm diff --git a/lib/site/DateTime/Locale/fi.pm b/lib/fallback/DateTime/Locale/fi.pm similarity index 100% rename from lib/site/DateTime/Locale/fi.pm rename to lib/fallback/DateTime/Locale/fi.pm diff --git a/lib/site/DateTime/Locale/fo.pm b/lib/fallback/DateTime/Locale/fo.pm similarity index 100% rename from lib/site/DateTime/Locale/fo.pm rename to lib/fallback/DateTime/Locale/fo.pm diff --git a/lib/site/DateTime/Locale/fr.pm b/lib/fallback/DateTime/Locale/fr.pm similarity index 100% rename from lib/site/DateTime/Locale/fr.pm rename to lib/fallback/DateTime/Locale/fr.pm diff --git a/lib/site/DateTime/Locale/fr_BE.pm b/lib/fallback/DateTime/Locale/fr_BE.pm similarity index 100% rename from lib/site/DateTime/Locale/fr_BE.pm rename to lib/fallback/DateTime/Locale/fr_BE.pm diff --git a/lib/site/DateTime/Locale/fr_CA.pm b/lib/fallback/DateTime/Locale/fr_CA.pm similarity index 100% rename from lib/site/DateTime/Locale/fr_CA.pm rename to lib/fallback/DateTime/Locale/fr_CA.pm diff --git a/lib/site/DateTime/Locale/fr_CH.pm b/lib/fallback/DateTime/Locale/fr_CH.pm similarity index 100% rename from lib/site/DateTime/Locale/fr_CH.pm rename to lib/fallback/DateTime/Locale/fr_CH.pm diff --git a/lib/site/DateTime/Locale/fur.pm b/lib/fallback/DateTime/Locale/fur.pm similarity index 100% rename from lib/site/DateTime/Locale/fur.pm rename to lib/fallback/DateTime/Locale/fur.pm diff --git a/lib/site/DateTime/Locale/ga.pm b/lib/fallback/DateTime/Locale/ga.pm similarity index 100% rename from lib/site/DateTime/Locale/ga.pm rename to lib/fallback/DateTime/Locale/ga.pm diff --git a/lib/site/DateTime/Locale/ga_IE.pm b/lib/fallback/DateTime/Locale/ga_IE.pm similarity index 100% rename from lib/site/DateTime/Locale/ga_IE.pm rename to lib/fallback/DateTime/Locale/ga_IE.pm diff --git a/lib/site/DateTime/Locale/gaa.pm b/lib/fallback/DateTime/Locale/gaa.pm similarity index 100% rename from lib/site/DateTime/Locale/gaa.pm rename to lib/fallback/DateTime/Locale/gaa.pm diff --git a/lib/site/DateTime/Locale/gez.pm b/lib/fallback/DateTime/Locale/gez.pm similarity index 100% rename from lib/site/DateTime/Locale/gez.pm rename to lib/fallback/DateTime/Locale/gez.pm diff --git a/lib/site/DateTime/Locale/gez_ER.pm b/lib/fallback/DateTime/Locale/gez_ER.pm similarity index 100% rename from lib/site/DateTime/Locale/gez_ER.pm rename to lib/fallback/DateTime/Locale/gez_ER.pm diff --git a/lib/site/DateTime/Locale/gez_ET.pm b/lib/fallback/DateTime/Locale/gez_ET.pm similarity index 100% rename from lib/site/DateTime/Locale/gez_ET.pm rename to lib/fallback/DateTime/Locale/gez_ET.pm diff --git a/lib/site/DateTime/Locale/gl.pm b/lib/fallback/DateTime/Locale/gl.pm similarity index 100% rename from lib/site/DateTime/Locale/gl.pm rename to lib/fallback/DateTime/Locale/gl.pm diff --git a/lib/site/DateTime/Locale/gl_ES.pm b/lib/fallback/DateTime/Locale/gl_ES.pm similarity index 100% rename from lib/site/DateTime/Locale/gl_ES.pm rename to lib/fallback/DateTime/Locale/gl_ES.pm diff --git a/lib/site/DateTime/Locale/gu.pm b/lib/fallback/DateTime/Locale/gu.pm similarity index 100% rename from lib/site/DateTime/Locale/gu.pm rename to lib/fallback/DateTime/Locale/gu.pm diff --git a/lib/site/DateTime/Locale/gu_IN.pm b/lib/fallback/DateTime/Locale/gu_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/gu_IN.pm rename to lib/fallback/DateTime/Locale/gu_IN.pm diff --git a/lib/site/DateTime/Locale/gv.pm b/lib/fallback/DateTime/Locale/gv.pm similarity index 100% rename from lib/site/DateTime/Locale/gv.pm rename to lib/fallback/DateTime/Locale/gv.pm diff --git a/lib/site/DateTime/Locale/gv_GB.pm b/lib/fallback/DateTime/Locale/gv_GB.pm similarity index 100% rename from lib/site/DateTime/Locale/gv_GB.pm rename to lib/fallback/DateTime/Locale/gv_GB.pm diff --git a/lib/site/DateTime/Locale/ha.pm b/lib/fallback/DateTime/Locale/ha.pm similarity index 100% rename from lib/site/DateTime/Locale/ha.pm rename to lib/fallback/DateTime/Locale/ha.pm diff --git a/lib/site/DateTime/Locale/ha_Arab.pm b/lib/fallback/DateTime/Locale/ha_Arab.pm similarity index 100% rename from lib/site/DateTime/Locale/ha_Arab.pm rename to lib/fallback/DateTime/Locale/ha_Arab.pm diff --git a/lib/site/DateTime/Locale/haw.pm b/lib/fallback/DateTime/Locale/haw.pm similarity index 100% rename from lib/site/DateTime/Locale/haw.pm rename to lib/fallback/DateTime/Locale/haw.pm diff --git a/lib/site/DateTime/Locale/haw_US.pm b/lib/fallback/DateTime/Locale/haw_US.pm similarity index 100% rename from lib/site/DateTime/Locale/haw_US.pm rename to lib/fallback/DateTime/Locale/haw_US.pm diff --git a/lib/site/DateTime/Locale/he.pm b/lib/fallback/DateTime/Locale/he.pm similarity index 100% rename from lib/site/DateTime/Locale/he.pm rename to lib/fallback/DateTime/Locale/he.pm diff --git a/lib/site/DateTime/Locale/hi.pm b/lib/fallback/DateTime/Locale/hi.pm similarity index 100% rename from lib/site/DateTime/Locale/hi.pm rename to lib/fallback/DateTime/Locale/hi.pm diff --git a/lib/site/DateTime/Locale/hi_IN.pm b/lib/fallback/DateTime/Locale/hi_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/hi_IN.pm rename to lib/fallback/DateTime/Locale/hi_IN.pm diff --git a/lib/site/DateTime/Locale/hr.pm b/lib/fallback/DateTime/Locale/hr.pm similarity index 100% rename from lib/site/DateTime/Locale/hr.pm rename to lib/fallback/DateTime/Locale/hr.pm diff --git a/lib/site/DateTime/Locale/hu.pm b/lib/fallback/DateTime/Locale/hu.pm similarity index 100% rename from lib/site/DateTime/Locale/hu.pm rename to lib/fallback/DateTime/Locale/hu.pm diff --git a/lib/site/DateTime/Locale/hy.pm b/lib/fallback/DateTime/Locale/hy.pm similarity index 100% rename from lib/site/DateTime/Locale/hy.pm rename to lib/fallback/DateTime/Locale/hy.pm diff --git a/lib/site/DateTime/Locale/hy_AM.pm b/lib/fallback/DateTime/Locale/hy_AM.pm similarity index 100% rename from lib/site/DateTime/Locale/hy_AM.pm rename to lib/fallback/DateTime/Locale/hy_AM.pm diff --git a/lib/site/DateTime/Locale/hy_AM_REVISED.pm b/lib/fallback/DateTime/Locale/hy_AM_REVISED.pm similarity index 100% rename from lib/site/DateTime/Locale/hy_AM_REVISED.pm rename to lib/fallback/DateTime/Locale/hy_AM_REVISED.pm diff --git a/lib/site/DateTime/Locale/ia.pm b/lib/fallback/DateTime/Locale/ia.pm similarity index 100% rename from lib/site/DateTime/Locale/ia.pm rename to lib/fallback/DateTime/Locale/ia.pm diff --git a/lib/site/DateTime/Locale/id.pm b/lib/fallback/DateTime/Locale/id.pm similarity index 100% rename from lib/site/DateTime/Locale/id.pm rename to lib/fallback/DateTime/Locale/id.pm diff --git a/lib/site/DateTime/Locale/id_ID.pm b/lib/fallback/DateTime/Locale/id_ID.pm similarity index 100% rename from lib/site/DateTime/Locale/id_ID.pm rename to lib/fallback/DateTime/Locale/id_ID.pm diff --git a/lib/site/DateTime/Locale/ig.pm b/lib/fallback/DateTime/Locale/ig.pm similarity index 100% rename from lib/site/DateTime/Locale/ig.pm rename to lib/fallback/DateTime/Locale/ig.pm diff --git a/lib/site/DateTime/Locale/is.pm b/lib/fallback/DateTime/Locale/is.pm similarity index 100% rename from lib/site/DateTime/Locale/is.pm rename to lib/fallback/DateTime/Locale/is.pm diff --git a/lib/site/DateTime/Locale/it.pm b/lib/fallback/DateTime/Locale/it.pm similarity index 100% rename from lib/site/DateTime/Locale/it.pm rename to lib/fallback/DateTime/Locale/it.pm diff --git a/lib/site/DateTime/Locale/it_CH.pm b/lib/fallback/DateTime/Locale/it_CH.pm similarity index 100% rename from lib/site/DateTime/Locale/it_CH.pm rename to lib/fallback/DateTime/Locale/it_CH.pm diff --git a/lib/site/DateTime/Locale/it_IT.pm b/lib/fallback/DateTime/Locale/it_IT.pm similarity index 100% rename from lib/site/DateTime/Locale/it_IT.pm rename to lib/fallback/DateTime/Locale/it_IT.pm diff --git a/lib/site/DateTime/Locale/iu.pm b/lib/fallback/DateTime/Locale/iu.pm similarity index 100% rename from lib/site/DateTime/Locale/iu.pm rename to lib/fallback/DateTime/Locale/iu.pm diff --git a/lib/site/DateTime/Locale/ja.pm b/lib/fallback/DateTime/Locale/ja.pm similarity index 100% rename from lib/site/DateTime/Locale/ja.pm rename to lib/fallback/DateTime/Locale/ja.pm diff --git a/lib/site/DateTime/Locale/ka.pm b/lib/fallback/DateTime/Locale/ka.pm similarity index 100% rename from lib/site/DateTime/Locale/ka.pm rename to lib/fallback/DateTime/Locale/ka.pm diff --git a/lib/site/DateTime/Locale/kaj.pm b/lib/fallback/DateTime/Locale/kaj.pm similarity index 100% rename from lib/site/DateTime/Locale/kaj.pm rename to lib/fallback/DateTime/Locale/kaj.pm diff --git a/lib/site/DateTime/Locale/kam.pm b/lib/fallback/DateTime/Locale/kam.pm similarity index 100% rename from lib/site/DateTime/Locale/kam.pm rename to lib/fallback/DateTime/Locale/kam.pm diff --git a/lib/site/DateTime/Locale/kcg.pm b/lib/fallback/DateTime/Locale/kcg.pm similarity index 100% rename from lib/site/DateTime/Locale/kcg.pm rename to lib/fallback/DateTime/Locale/kcg.pm diff --git a/lib/site/DateTime/Locale/kfo.pm b/lib/fallback/DateTime/Locale/kfo.pm similarity index 100% rename from lib/site/DateTime/Locale/kfo.pm rename to lib/fallback/DateTime/Locale/kfo.pm diff --git a/lib/site/DateTime/Locale/kk.pm b/lib/fallback/DateTime/Locale/kk.pm similarity index 100% rename from lib/site/DateTime/Locale/kk.pm rename to lib/fallback/DateTime/Locale/kk.pm diff --git a/lib/site/DateTime/Locale/kl.pm b/lib/fallback/DateTime/Locale/kl.pm similarity index 100% rename from lib/site/DateTime/Locale/kl.pm rename to lib/fallback/DateTime/Locale/kl.pm diff --git a/lib/site/DateTime/Locale/kl_GL.pm b/lib/fallback/DateTime/Locale/kl_GL.pm similarity index 100% rename from lib/site/DateTime/Locale/kl_GL.pm rename to lib/fallback/DateTime/Locale/kl_GL.pm diff --git a/lib/site/DateTime/Locale/km.pm b/lib/fallback/DateTime/Locale/km.pm similarity index 100% rename from lib/site/DateTime/Locale/km.pm rename to lib/fallback/DateTime/Locale/km.pm diff --git a/lib/site/DateTime/Locale/kn.pm b/lib/fallback/DateTime/Locale/kn.pm similarity index 100% rename from lib/site/DateTime/Locale/kn.pm rename to lib/fallback/DateTime/Locale/kn.pm diff --git a/lib/site/DateTime/Locale/kn_IN.pm b/lib/fallback/DateTime/Locale/kn_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/kn_IN.pm rename to lib/fallback/DateTime/Locale/kn_IN.pm diff --git a/lib/site/DateTime/Locale/ko.pm b/lib/fallback/DateTime/Locale/ko.pm similarity index 100% rename from lib/site/DateTime/Locale/ko.pm rename to lib/fallback/DateTime/Locale/ko.pm diff --git a/lib/site/DateTime/Locale/ko_KR.pm b/lib/fallback/DateTime/Locale/ko_KR.pm similarity index 100% rename from lib/site/DateTime/Locale/ko_KR.pm rename to lib/fallback/DateTime/Locale/ko_KR.pm diff --git a/lib/site/DateTime/Locale/kok.pm b/lib/fallback/DateTime/Locale/kok.pm similarity index 100% rename from lib/site/DateTime/Locale/kok.pm rename to lib/fallback/DateTime/Locale/kok.pm diff --git a/lib/site/DateTime/Locale/kok_IN.pm b/lib/fallback/DateTime/Locale/kok_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/kok_IN.pm rename to lib/fallback/DateTime/Locale/kok_IN.pm diff --git a/lib/site/DateTime/Locale/kw.pm b/lib/fallback/DateTime/Locale/kw.pm similarity index 100% rename from lib/site/DateTime/Locale/kw.pm rename to lib/fallback/DateTime/Locale/kw.pm diff --git a/lib/site/DateTime/Locale/kw_GB.pm b/lib/fallback/DateTime/Locale/kw_GB.pm similarity index 100% rename from lib/site/DateTime/Locale/kw_GB.pm rename to lib/fallback/DateTime/Locale/kw_GB.pm diff --git a/lib/site/DateTime/Locale/ln.pm b/lib/fallback/DateTime/Locale/ln.pm similarity index 100% rename from lib/site/DateTime/Locale/ln.pm rename to lib/fallback/DateTime/Locale/ln.pm diff --git a/lib/site/DateTime/Locale/lo.pm b/lib/fallback/DateTime/Locale/lo.pm similarity index 100% rename from lib/site/DateTime/Locale/lo.pm rename to lib/fallback/DateTime/Locale/lo.pm diff --git a/lib/site/DateTime/Locale/lt.pm b/lib/fallback/DateTime/Locale/lt.pm similarity index 100% rename from lib/site/DateTime/Locale/lt.pm rename to lib/fallback/DateTime/Locale/lt.pm diff --git a/lib/site/DateTime/Locale/lv.pm b/lib/fallback/DateTime/Locale/lv.pm similarity index 100% rename from lib/site/DateTime/Locale/lv.pm rename to lib/fallback/DateTime/Locale/lv.pm diff --git a/lib/site/DateTime/Locale/mk.pm b/lib/fallback/DateTime/Locale/mk.pm similarity index 100% rename from lib/site/DateTime/Locale/mk.pm rename to lib/fallback/DateTime/Locale/mk.pm diff --git a/lib/site/DateTime/Locale/ml.pm b/lib/fallback/DateTime/Locale/ml.pm similarity index 100% rename from lib/site/DateTime/Locale/ml.pm rename to lib/fallback/DateTime/Locale/ml.pm diff --git a/lib/site/DateTime/Locale/ml_IN.pm b/lib/fallback/DateTime/Locale/ml_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/ml_IN.pm rename to lib/fallback/DateTime/Locale/ml_IN.pm diff --git a/lib/site/DateTime/Locale/mn.pm b/lib/fallback/DateTime/Locale/mn.pm similarity index 100% rename from lib/site/DateTime/Locale/mn.pm rename to lib/fallback/DateTime/Locale/mn.pm diff --git a/lib/site/DateTime/Locale/mr.pm b/lib/fallback/DateTime/Locale/mr.pm similarity index 100% rename from lib/site/DateTime/Locale/mr.pm rename to lib/fallback/DateTime/Locale/mr.pm diff --git a/lib/site/DateTime/Locale/mr_IN.pm b/lib/fallback/DateTime/Locale/mr_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/mr_IN.pm rename to lib/fallback/DateTime/Locale/mr_IN.pm diff --git a/lib/site/DateTime/Locale/ms.pm b/lib/fallback/DateTime/Locale/ms.pm similarity index 100% rename from lib/site/DateTime/Locale/ms.pm rename to lib/fallback/DateTime/Locale/ms.pm diff --git a/lib/site/DateTime/Locale/ms_BN.pm b/lib/fallback/DateTime/Locale/ms_BN.pm similarity index 100% rename from lib/site/DateTime/Locale/ms_BN.pm rename to lib/fallback/DateTime/Locale/ms_BN.pm diff --git a/lib/site/DateTime/Locale/ms_MY.pm b/lib/fallback/DateTime/Locale/ms_MY.pm similarity index 100% rename from lib/site/DateTime/Locale/ms_MY.pm rename to lib/fallback/DateTime/Locale/ms_MY.pm diff --git a/lib/site/DateTime/Locale/mt.pm b/lib/fallback/DateTime/Locale/mt.pm similarity index 100% rename from lib/site/DateTime/Locale/mt.pm rename to lib/fallback/DateTime/Locale/mt.pm diff --git a/lib/site/DateTime/Locale/nb.pm b/lib/fallback/DateTime/Locale/nb.pm similarity index 100% rename from lib/site/DateTime/Locale/nb.pm rename to lib/fallback/DateTime/Locale/nb.pm diff --git a/lib/site/DateTime/Locale/ne.pm b/lib/fallback/DateTime/Locale/ne.pm similarity index 100% rename from lib/site/DateTime/Locale/ne.pm rename to lib/fallback/DateTime/Locale/ne.pm diff --git a/lib/site/DateTime/Locale/nl.pm b/lib/fallback/DateTime/Locale/nl.pm similarity index 100% rename from lib/site/DateTime/Locale/nl.pm rename to lib/fallback/DateTime/Locale/nl.pm diff --git a/lib/site/DateTime/Locale/nl_BE.pm b/lib/fallback/DateTime/Locale/nl_BE.pm similarity index 100% rename from lib/site/DateTime/Locale/nl_BE.pm rename to lib/fallback/DateTime/Locale/nl_BE.pm diff --git a/lib/site/DateTime/Locale/nn.pm b/lib/fallback/DateTime/Locale/nn.pm similarity index 100% rename from lib/site/DateTime/Locale/nn.pm rename to lib/fallback/DateTime/Locale/nn.pm diff --git a/lib/site/DateTime/Locale/nr.pm b/lib/fallback/DateTime/Locale/nr.pm similarity index 100% rename from lib/site/DateTime/Locale/nr.pm rename to lib/fallback/DateTime/Locale/nr.pm diff --git a/lib/site/DateTime/Locale/nso.pm b/lib/fallback/DateTime/Locale/nso.pm similarity index 100% rename from lib/site/DateTime/Locale/nso.pm rename to lib/fallback/DateTime/Locale/nso.pm diff --git a/lib/site/DateTime/Locale/ny.pm b/lib/fallback/DateTime/Locale/ny.pm similarity index 100% rename from lib/site/DateTime/Locale/ny.pm rename to lib/fallback/DateTime/Locale/ny.pm diff --git a/lib/site/DateTime/Locale/om.pm b/lib/fallback/DateTime/Locale/om.pm similarity index 100% rename from lib/site/DateTime/Locale/om.pm rename to lib/fallback/DateTime/Locale/om.pm diff --git a/lib/site/DateTime/Locale/om_ET.pm b/lib/fallback/DateTime/Locale/om_ET.pm similarity index 100% rename from lib/site/DateTime/Locale/om_ET.pm rename to lib/fallback/DateTime/Locale/om_ET.pm diff --git a/lib/site/DateTime/Locale/om_KE.pm b/lib/fallback/DateTime/Locale/om_KE.pm similarity index 100% rename from lib/site/DateTime/Locale/om_KE.pm rename to lib/fallback/DateTime/Locale/om_KE.pm diff --git a/lib/site/DateTime/Locale/pa.pm b/lib/fallback/DateTime/Locale/pa.pm similarity index 100% rename from lib/site/DateTime/Locale/pa.pm rename to lib/fallback/DateTime/Locale/pa.pm diff --git a/lib/site/DateTime/Locale/pa_Arab.pm b/lib/fallback/DateTime/Locale/pa_Arab.pm similarity index 100% rename from lib/site/DateTime/Locale/pa_Arab.pm rename to lib/fallback/DateTime/Locale/pa_Arab.pm diff --git a/lib/site/DateTime/Locale/pa_IN.pm b/lib/fallback/DateTime/Locale/pa_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/pa_IN.pm rename to lib/fallback/DateTime/Locale/pa_IN.pm diff --git a/lib/site/DateTime/Locale/pl.pm b/lib/fallback/DateTime/Locale/pl.pm similarity index 100% rename from lib/site/DateTime/Locale/pl.pm rename to lib/fallback/DateTime/Locale/pl.pm diff --git a/lib/site/DateTime/Locale/pt.pm b/lib/fallback/DateTime/Locale/pt.pm similarity index 100% rename from lib/site/DateTime/Locale/pt.pm rename to lib/fallback/DateTime/Locale/pt.pm diff --git a/lib/site/DateTime/Locale/pt_BR.pm b/lib/fallback/DateTime/Locale/pt_BR.pm similarity index 100% rename from lib/site/DateTime/Locale/pt_BR.pm rename to lib/fallback/DateTime/Locale/pt_BR.pm diff --git a/lib/site/DateTime/Locale/pt_PT.pm b/lib/fallback/DateTime/Locale/pt_PT.pm similarity index 100% rename from lib/site/DateTime/Locale/pt_PT.pm rename to lib/fallback/DateTime/Locale/pt_PT.pm diff --git a/lib/site/DateTime/Locale/ro.pm b/lib/fallback/DateTime/Locale/ro.pm similarity index 100% rename from lib/site/DateTime/Locale/ro.pm rename to lib/fallback/DateTime/Locale/ro.pm diff --git a/lib/site/DateTime/Locale/root.pm b/lib/fallback/DateTime/Locale/root.pm similarity index 100% rename from lib/site/DateTime/Locale/root.pm rename to lib/fallback/DateTime/Locale/root.pm diff --git a/lib/site/DateTime/Locale/ru.pm b/lib/fallback/DateTime/Locale/ru.pm similarity index 100% rename from lib/site/DateTime/Locale/ru.pm rename to lib/fallback/DateTime/Locale/ru.pm diff --git a/lib/site/DateTime/Locale/ru_UA.pm b/lib/fallback/DateTime/Locale/ru_UA.pm similarity index 100% rename from lib/site/DateTime/Locale/ru_UA.pm rename to lib/fallback/DateTime/Locale/ru_UA.pm diff --git a/lib/site/DateTime/Locale/rw.pm b/lib/fallback/DateTime/Locale/rw.pm similarity index 100% rename from lib/site/DateTime/Locale/rw.pm rename to lib/fallback/DateTime/Locale/rw.pm diff --git a/lib/site/DateTime/Locale/sid.pm b/lib/fallback/DateTime/Locale/sid.pm similarity index 100% rename from lib/site/DateTime/Locale/sid.pm rename to lib/fallback/DateTime/Locale/sid.pm diff --git a/lib/site/DateTime/Locale/sk.pm b/lib/fallback/DateTime/Locale/sk.pm similarity index 100% rename from lib/site/DateTime/Locale/sk.pm rename to lib/fallback/DateTime/Locale/sk.pm diff --git a/lib/site/DateTime/Locale/sl.pm b/lib/fallback/DateTime/Locale/sl.pm similarity index 100% rename from lib/site/DateTime/Locale/sl.pm rename to lib/fallback/DateTime/Locale/sl.pm diff --git a/lib/site/DateTime/Locale/so.pm b/lib/fallback/DateTime/Locale/so.pm similarity index 100% rename from lib/site/DateTime/Locale/so.pm rename to lib/fallback/DateTime/Locale/so.pm diff --git a/lib/site/DateTime/Locale/sq.pm b/lib/fallback/DateTime/Locale/sq.pm similarity index 100% rename from lib/site/DateTime/Locale/sq.pm rename to lib/fallback/DateTime/Locale/sq.pm diff --git a/lib/site/DateTime/Locale/sr.pm b/lib/fallback/DateTime/Locale/sr.pm similarity index 100% rename from lib/site/DateTime/Locale/sr.pm rename to lib/fallback/DateTime/Locale/sr.pm diff --git a/lib/site/DateTime/Locale/sr_Cyrl.pm b/lib/fallback/DateTime/Locale/sr_Cyrl.pm similarity index 100% rename from lib/site/DateTime/Locale/sr_Cyrl.pm rename to lib/fallback/DateTime/Locale/sr_Cyrl.pm diff --git a/lib/site/DateTime/Locale/sr_Cyrl_BA.pm b/lib/fallback/DateTime/Locale/sr_Cyrl_BA.pm similarity index 100% rename from lib/site/DateTime/Locale/sr_Cyrl_BA.pm rename to lib/fallback/DateTime/Locale/sr_Cyrl_BA.pm diff --git a/lib/site/DateTime/Locale/sr_Latn.pm b/lib/fallback/DateTime/Locale/sr_Latn.pm similarity index 100% rename from lib/site/DateTime/Locale/sr_Latn.pm rename to lib/fallback/DateTime/Locale/sr_Latn.pm diff --git a/lib/site/DateTime/Locale/sr_Latn_BA.pm b/lib/fallback/DateTime/Locale/sr_Latn_BA.pm similarity index 100% rename from lib/site/DateTime/Locale/sr_Latn_BA.pm rename to lib/fallback/DateTime/Locale/sr_Latn_BA.pm diff --git a/lib/site/DateTime/Locale/ss.pm b/lib/fallback/DateTime/Locale/ss.pm similarity index 100% rename from lib/site/DateTime/Locale/ss.pm rename to lib/fallback/DateTime/Locale/ss.pm diff --git a/lib/site/DateTime/Locale/st.pm b/lib/fallback/DateTime/Locale/st.pm similarity index 100% rename from lib/site/DateTime/Locale/st.pm rename to lib/fallback/DateTime/Locale/st.pm diff --git a/lib/site/DateTime/Locale/sv.pm b/lib/fallback/DateTime/Locale/sv.pm similarity index 100% rename from lib/site/DateTime/Locale/sv.pm rename to lib/fallback/DateTime/Locale/sv.pm diff --git a/lib/site/DateTime/Locale/sv_SE.pm b/lib/fallback/DateTime/Locale/sv_SE.pm similarity index 100% rename from lib/site/DateTime/Locale/sv_SE.pm rename to lib/fallback/DateTime/Locale/sv_SE.pm diff --git a/lib/site/DateTime/Locale/sw.pm b/lib/fallback/DateTime/Locale/sw.pm similarity index 100% rename from lib/site/DateTime/Locale/sw.pm rename to lib/fallback/DateTime/Locale/sw.pm diff --git a/lib/site/DateTime/Locale/syr.pm b/lib/fallback/DateTime/Locale/syr.pm similarity index 100% rename from lib/site/DateTime/Locale/syr.pm rename to lib/fallback/DateTime/Locale/syr.pm diff --git a/lib/site/DateTime/Locale/syr_SY.pm b/lib/fallback/DateTime/Locale/syr_SY.pm similarity index 100% rename from lib/site/DateTime/Locale/syr_SY.pm rename to lib/fallback/DateTime/Locale/syr_SY.pm diff --git a/lib/site/DateTime/Locale/ta.pm b/lib/fallback/DateTime/Locale/ta.pm similarity index 100% rename from lib/site/DateTime/Locale/ta.pm rename to lib/fallback/DateTime/Locale/ta.pm diff --git a/lib/site/DateTime/Locale/ta_IN.pm b/lib/fallback/DateTime/Locale/ta_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/ta_IN.pm rename to lib/fallback/DateTime/Locale/ta_IN.pm diff --git a/lib/site/DateTime/Locale/te.pm b/lib/fallback/DateTime/Locale/te.pm similarity index 100% rename from lib/site/DateTime/Locale/te.pm rename to lib/fallback/DateTime/Locale/te.pm diff --git a/lib/site/DateTime/Locale/te_IN.pm b/lib/fallback/DateTime/Locale/te_IN.pm similarity index 100% rename from lib/site/DateTime/Locale/te_IN.pm rename to lib/fallback/DateTime/Locale/te_IN.pm diff --git a/lib/site/DateTime/Locale/tg.pm b/lib/fallback/DateTime/Locale/tg.pm similarity index 100% rename from lib/site/DateTime/Locale/tg.pm rename to lib/fallback/DateTime/Locale/tg.pm diff --git a/lib/site/DateTime/Locale/th.pm b/lib/fallback/DateTime/Locale/th.pm similarity index 100% rename from lib/site/DateTime/Locale/th.pm rename to lib/fallback/DateTime/Locale/th.pm diff --git a/lib/site/DateTime/Locale/ti.pm b/lib/fallback/DateTime/Locale/ti.pm similarity index 100% rename from lib/site/DateTime/Locale/ti.pm rename to lib/fallback/DateTime/Locale/ti.pm diff --git a/lib/site/DateTime/Locale/ti_ER.pm b/lib/fallback/DateTime/Locale/ti_ER.pm similarity index 100% rename from lib/site/DateTime/Locale/ti_ER.pm rename to lib/fallback/DateTime/Locale/ti_ER.pm diff --git a/lib/site/DateTime/Locale/ti_ET.pm b/lib/fallback/DateTime/Locale/ti_ET.pm similarity index 100% rename from lib/site/DateTime/Locale/ti_ET.pm rename to lib/fallback/DateTime/Locale/ti_ET.pm diff --git a/lib/site/DateTime/Locale/tig.pm b/lib/fallback/DateTime/Locale/tig.pm similarity index 100% rename from lib/site/DateTime/Locale/tig.pm rename to lib/fallback/DateTime/Locale/tig.pm diff --git a/lib/site/DateTime/Locale/tig_ER.pm b/lib/fallback/DateTime/Locale/tig_ER.pm similarity index 100% rename from lib/site/DateTime/Locale/tig_ER.pm rename to lib/fallback/DateTime/Locale/tig_ER.pm diff --git a/lib/site/DateTime/Locale/tn.pm b/lib/fallback/DateTime/Locale/tn.pm similarity index 100% rename from lib/site/DateTime/Locale/tn.pm rename to lib/fallback/DateTime/Locale/tn.pm diff --git a/lib/site/DateTime/Locale/tr.pm b/lib/fallback/DateTime/Locale/tr.pm similarity index 100% rename from lib/site/DateTime/Locale/tr.pm rename to lib/fallback/DateTime/Locale/tr.pm diff --git a/lib/site/DateTime/Locale/ts.pm b/lib/fallback/DateTime/Locale/ts.pm similarity index 100% rename from lib/site/DateTime/Locale/ts.pm rename to lib/fallback/DateTime/Locale/ts.pm diff --git a/lib/site/DateTime/Locale/uk.pm b/lib/fallback/DateTime/Locale/uk.pm similarity index 100% rename from lib/site/DateTime/Locale/uk.pm rename to lib/fallback/DateTime/Locale/uk.pm diff --git a/lib/site/DateTime/Locale/uz.pm b/lib/fallback/DateTime/Locale/uz.pm similarity index 100% rename from lib/site/DateTime/Locale/uz.pm rename to lib/fallback/DateTime/Locale/uz.pm diff --git a/lib/site/DateTime/Locale/uz_Arab.pm b/lib/fallback/DateTime/Locale/uz_Arab.pm similarity index 100% rename from lib/site/DateTime/Locale/uz_Arab.pm rename to lib/fallback/DateTime/Locale/uz_Arab.pm diff --git a/lib/site/DateTime/Locale/uz_Latn.pm b/lib/fallback/DateTime/Locale/uz_Latn.pm similarity index 100% rename from lib/site/DateTime/Locale/uz_Latn.pm rename to lib/fallback/DateTime/Locale/uz_Latn.pm diff --git a/lib/site/DateTime/Locale/ve.pm b/lib/fallback/DateTime/Locale/ve.pm similarity index 100% rename from lib/site/DateTime/Locale/ve.pm rename to lib/fallback/DateTime/Locale/ve.pm diff --git a/lib/site/DateTime/Locale/vi.pm b/lib/fallback/DateTime/Locale/vi.pm similarity index 100% rename from lib/site/DateTime/Locale/vi.pm rename to lib/fallback/DateTime/Locale/vi.pm diff --git a/lib/site/DateTime/Locale/wal.pm b/lib/fallback/DateTime/Locale/wal.pm similarity index 100% rename from lib/site/DateTime/Locale/wal.pm rename to lib/fallback/DateTime/Locale/wal.pm diff --git a/lib/site/DateTime/Locale/wal_ET.pm b/lib/fallback/DateTime/Locale/wal_ET.pm similarity index 100% rename from lib/site/DateTime/Locale/wal_ET.pm rename to lib/fallback/DateTime/Locale/wal_ET.pm diff --git a/lib/site/DateTime/Locale/xh.pm b/lib/fallback/DateTime/Locale/xh.pm similarity index 100% rename from lib/site/DateTime/Locale/xh.pm rename to lib/fallback/DateTime/Locale/xh.pm diff --git a/lib/site/DateTime/Locale/yo.pm b/lib/fallback/DateTime/Locale/yo.pm similarity index 100% rename from lib/site/DateTime/Locale/yo.pm rename to lib/fallback/DateTime/Locale/yo.pm diff --git a/lib/site/DateTime/Locale/zh.pm b/lib/fallback/DateTime/Locale/zh.pm similarity index 100% rename from lib/site/DateTime/Locale/zh.pm rename to lib/fallback/DateTime/Locale/zh.pm diff --git a/lib/site/DateTime/Locale/zh_Hans_CN.pm b/lib/fallback/DateTime/Locale/zh_Hans_CN.pm similarity index 100% rename from lib/site/DateTime/Locale/zh_Hans_CN.pm rename to lib/fallback/DateTime/Locale/zh_Hans_CN.pm diff --git a/lib/site/DateTime/Locale/zh_Hans_SG.pm b/lib/fallback/DateTime/Locale/zh_Hans_SG.pm similarity index 100% rename from lib/site/DateTime/Locale/zh_Hans_SG.pm rename to lib/fallback/DateTime/Locale/zh_Hans_SG.pm diff --git a/lib/site/DateTime/Locale/zh_Hant.pm b/lib/fallback/DateTime/Locale/zh_Hant.pm similarity index 100% rename from lib/site/DateTime/Locale/zh_Hant.pm rename to lib/fallback/DateTime/Locale/zh_Hant.pm diff --git a/lib/site/DateTime/Locale/zh_Hant_HK.pm b/lib/fallback/DateTime/Locale/zh_Hant_HK.pm similarity index 100% rename from lib/site/DateTime/Locale/zh_Hant_HK.pm rename to lib/fallback/DateTime/Locale/zh_Hant_HK.pm diff --git a/lib/site/DateTime/Locale/zh_Hant_MO.pm b/lib/fallback/DateTime/Locale/zh_Hant_MO.pm similarity index 100% rename from lib/site/DateTime/Locale/zh_Hant_MO.pm rename to lib/fallback/DateTime/Locale/zh_Hant_MO.pm diff --git a/lib/site/DateTime/Locale/zu.pm b/lib/fallback/DateTime/Locale/zu.pm similarity index 100% rename from lib/site/DateTime/Locale/zu.pm rename to lib/fallback/DateTime/Locale/zu.pm diff --git a/lib/site/DateTime/Set.pm b/lib/fallback/DateTime/Set.pm similarity index 100% rename from lib/site/DateTime/Set.pm rename to lib/fallback/DateTime/Set.pm diff --git a/lib/site/DateTime/Span.pm b/lib/fallback/DateTime/Span.pm similarity index 100% rename from lib/site/DateTime/Span.pm rename to lib/fallback/DateTime/Span.pm diff --git a/lib/site/DateTime/SpanSet.pm b/lib/fallback/DateTime/SpanSet.pm similarity index 100% rename from lib/site/DateTime/SpanSet.pm rename to lib/fallback/DateTime/SpanSet.pm diff --git a/lib/site/DateTime/TimeZone.pm b/lib/fallback/DateTime/TimeZone.pm similarity index 100% rename from lib/site/DateTime/TimeZone.pm rename to lib/fallback/DateTime/TimeZone.pm diff --git a/lib/site/DateTimePP.pm b/lib/fallback/DateTimePP.pm similarity index 100% rename from lib/site/DateTimePP.pm rename to lib/fallback/DateTimePP.pm diff --git a/lib/site/DateTimePPExtra.pm b/lib/fallback/DateTimePPExtra.pm similarity index 100% rename from lib/site/DateTimePPExtra.pm rename to lib/fallback/DateTimePPExtra.pm diff --git a/lib/site/Device/Changes b/lib/fallback/Device/Changes similarity index 100% rename from lib/site/Device/Changes rename to lib/fallback/Device/Changes diff --git a/lib/site/Device/Device-SerialPort.html b/lib/fallback/Device/Device-SerialPort.html similarity index 100% rename from lib/site/Device/Device-SerialPort.html rename to lib/fallback/Device/Device-SerialPort.html diff --git a/lib/site/Device/README b/lib/fallback/Device/README similarity index 100% rename from lib/site/Device/README rename to lib/fallback/Device/README diff --git a/lib/site/Device/SerialPort.pm b/lib/fallback/Device/SerialPort.pm similarity index 100% rename from lib/site/Device/SerialPort.pm rename to lib/fallback/Device/SerialPort.pm diff --git a/lib/site/Digest/HMAC.pm b/lib/fallback/Digest/HMAC.pm similarity index 100% rename from lib/site/Digest/HMAC.pm rename to lib/fallback/Digest/HMAC.pm diff --git a/lib/site/Digest/HMAC_MD5.pm b/lib/fallback/Digest/HMAC_MD5.pm similarity index 100% rename from lib/site/Digest/HMAC_MD5.pm rename to lib/fallback/Digest/HMAC_MD5.pm diff --git a/lib/site/Digest/HMAC_SHA1.pm b/lib/fallback/Digest/HMAC_SHA1.pm similarity index 100% rename from lib/site/Digest/HMAC_SHA1.pm rename to lib/fallback/Digest/HMAC_SHA1.pm diff --git a/lib/site/Digest/MD2.pm b/lib/fallback/Digest/MD2.pm similarity index 100% rename from lib/site/Digest/MD2.pm rename to lib/fallback/Digest/MD2.pm diff --git a/lib/site/Digest/MD5.pm b/lib/fallback/Digest/MD5.pm similarity index 100% rename from lib/site/Digest/MD5.pm rename to lib/fallback/Digest/MD5.pm diff --git a/lib/site/Digest/Perl/MD5.pm b/lib/fallback/Digest/Perl/MD5.pm similarity index 100% rename from lib/site/Digest/Perl/MD5.pm rename to lib/fallback/Digest/Perl/MD5.pm diff --git a/lib/site/Digest/SHA1.pm b/lib/fallback/Digest/SHA1.pm similarity index 100% rename from lib/site/Digest/SHA1.pm rename to lib/fallback/Digest/SHA1.pm diff --git a/lib/site/Digest/base.pm b/lib/fallback/Digest/base.pm similarity index 100% rename from lib/site/Digest/base.pm rename to lib/fallback/Digest/base.pm diff --git a/lib/site/Email/Date/Format.pm b/lib/fallback/Email/Date/Format.pm similarity index 100% rename from lib/site/Email/Date/Format.pm rename to lib/fallback/Email/Date/Format.pm diff --git a/lib/site/File/Listing.pm b/lib/fallback/File/Listing.pm similarity index 100% rename from lib/site/File/Listing.pm rename to lib/fallback/File/Listing.pm diff --git a/lib/site/File/Which.pm b/lib/fallback/File/Which.pm similarity index 100% rename from lib/site/File/Which.pm rename to lib/fallback/File/Which.pm diff --git a/lib/site/Geo/METAR.pm b/lib/fallback/Geo/METAR.pm similarity index 100% rename from lib/site/Geo/METAR.pm rename to lib/fallback/Geo/METAR.pm diff --git a/lib/site/Geo/Weather.pm b/lib/fallback/Geo/Weather.pm similarity index 100% rename from lib/site/Geo/Weather.pm rename to lib/fallback/Geo/Weather.pm diff --git a/lib/site/Geo/WeatherNOAA.pm b/lib/fallback/Geo/WeatherNOAA.pm similarity index 100% rename from lib/site/Geo/WeatherNOAA.pm rename to lib/fallback/Geo/WeatherNOAA.pm diff --git a/lib/site/Geo/WeatherNOAA.pm.old b/lib/fallback/Geo/WeatherNOAA.pm.old similarity index 100% rename from lib/site/Geo/WeatherNOAA.pm.old rename to lib/fallback/Geo/WeatherNOAA.pm.old diff --git a/lib/site/HTML/AsSubs.pm b/lib/fallback/HTML/AsSubs.pm similarity index 100% rename from lib/site/HTML/AsSubs.pm rename to lib/fallback/HTML/AsSubs.pm diff --git a/lib/site/HTML/Element.pm b/lib/fallback/HTML/Element.pm similarity index 100% rename from lib/site/HTML/Element.pm rename to lib/fallback/HTML/Element.pm diff --git a/lib/site/HTML/Entities.pm b/lib/fallback/HTML/Entities.pm similarity index 100% rename from lib/site/HTML/Entities.pm rename to lib/fallback/HTML/Entities.pm diff --git a/lib/site/HTML/Filter.pm b/lib/fallback/HTML/Filter.pm similarity index 100% rename from lib/site/HTML/Filter.pm rename to lib/fallback/HTML/Filter.pm diff --git a/lib/site/HTML/Form.pm b/lib/fallback/HTML/Form.pm similarity index 100% rename from lib/site/HTML/Form.pm rename to lib/fallback/HTML/Form.pm diff --git a/lib/site/HTML/FormatMarkdown.pm b/lib/fallback/HTML/FormatMarkdown.pm similarity index 100% rename from lib/site/HTML/FormatMarkdown.pm rename to lib/fallback/HTML/FormatMarkdown.pm diff --git a/lib/site/HTML/FormatPS.pm b/lib/fallback/HTML/FormatPS.pm similarity index 100% rename from lib/site/HTML/FormatPS.pm rename to lib/fallback/HTML/FormatPS.pm diff --git a/lib/site/HTML/FormatRTF.pm b/lib/fallback/HTML/FormatRTF.pm similarity index 100% rename from lib/site/HTML/FormatRTF.pm rename to lib/fallback/HTML/FormatRTF.pm diff --git a/lib/site/HTML/FormatText.pm b/lib/fallback/HTML/FormatText.pm similarity index 100% rename from lib/site/HTML/FormatText.pm rename to lib/fallback/HTML/FormatText.pm diff --git a/lib/site/HTML/Formatter.pm b/lib/fallback/HTML/Formatter.pm similarity index 100% rename from lib/site/HTML/Formatter.pm rename to lib/fallback/HTML/Formatter.pm diff --git a/lib/site/HTML/HeadParser.pm b/lib/fallback/HTML/HeadParser.pm similarity index 100% rename from lib/site/HTML/HeadParser.pm rename to lib/fallback/HTML/HeadParser.pm diff --git a/lib/site/HTML/LinkExtor.pm b/lib/fallback/HTML/LinkExtor.pm similarity index 100% rename from lib/site/HTML/LinkExtor.pm rename to lib/fallback/HTML/LinkExtor.pm diff --git a/lib/site/HTML/Parse.pm b/lib/fallback/HTML/Parse.pm similarity index 100% rename from lib/site/HTML/Parse.pm rename to lib/fallback/HTML/Parse.pm diff --git a/lib/site/HTML/Parser.pm b/lib/fallback/HTML/Parser.pm similarity index 100% rename from lib/site/HTML/Parser.pm rename to lib/fallback/HTML/Parser.pm diff --git a/lib/site/HTML/TableExtract.pm b/lib/fallback/HTML/TableExtract.pm similarity index 100% rename from lib/site/HTML/TableExtract.pm rename to lib/fallback/HTML/TableExtract.pm diff --git a/lib/site/HTML/Tagset.pm b/lib/fallback/HTML/Tagset.pm similarity index 100% rename from lib/site/HTML/Tagset.pm rename to lib/fallback/HTML/Tagset.pm diff --git a/lib/site/HTML/TokeParser.pm b/lib/fallback/HTML/TokeParser.pm similarity index 100% rename from lib/site/HTML/TokeParser.pm rename to lib/fallback/HTML/TokeParser.pm diff --git a/lib/site/HTML/TreeBuilder.pm b/lib/fallback/HTML/TreeBuilder.pm similarity index 100% rename from lib/site/HTML/TreeBuilder.pm rename to lib/fallback/HTML/TreeBuilder.pm diff --git a/lib/site/HTTP/Cookies.pm b/lib/fallback/HTTP/Cookies.pm similarity index 100% rename from lib/site/HTTP/Cookies.pm rename to lib/fallback/HTTP/Cookies.pm diff --git a/lib/site/HTTP/Cookies/Microsoft.pm b/lib/fallback/HTTP/Cookies/Microsoft.pm similarity index 100% rename from lib/site/HTTP/Cookies/Microsoft.pm rename to lib/fallback/HTTP/Cookies/Microsoft.pm diff --git a/lib/site/HTTP/Cookies/Netscape.pm b/lib/fallback/HTTP/Cookies/Netscape.pm similarity index 100% rename from lib/site/HTTP/Cookies/Netscape.pm rename to lib/fallback/HTTP/Cookies/Netscape.pm diff --git a/lib/site/HTTP/Daemon.pm b/lib/fallback/HTTP/Daemon.pm similarity index 100% rename from lib/site/HTTP/Daemon.pm rename to lib/fallback/HTTP/Daemon.pm diff --git a/lib/site/HTTP/Date.pm b/lib/fallback/HTTP/Date.pm similarity index 100% rename from lib/site/HTTP/Date.pm rename to lib/fallback/HTTP/Date.pm diff --git a/lib/site/HTTP/Headers.pm b/lib/fallback/HTTP/Headers.pm similarity index 100% rename from lib/site/HTTP/Headers.pm rename to lib/fallback/HTTP/Headers.pm diff --git a/lib/site/HTTP/Headers/Auth.pm b/lib/fallback/HTTP/Headers/Auth.pm similarity index 100% rename from lib/site/HTTP/Headers/Auth.pm rename to lib/fallback/HTTP/Headers/Auth.pm diff --git a/lib/site/HTTP/Headers/ETag.pm b/lib/fallback/HTTP/Headers/ETag.pm similarity index 100% rename from lib/site/HTTP/Headers/ETag.pm rename to lib/fallback/HTTP/Headers/ETag.pm diff --git a/lib/site/HTTP/Headers/Util.pm b/lib/fallback/HTTP/Headers/Util.pm similarity index 100% rename from lib/site/HTTP/Headers/Util.pm rename to lib/fallback/HTTP/Headers/Util.pm diff --git a/lib/site/HTTP/Message.pm b/lib/fallback/HTTP/Message.pm similarity index 100% rename from lib/site/HTTP/Message.pm rename to lib/fallback/HTTP/Message.pm diff --git a/lib/site/HTTP/Negotiate.pm b/lib/fallback/HTTP/Negotiate.pm similarity index 100% rename from lib/site/HTTP/Negotiate.pm rename to lib/fallback/HTTP/Negotiate.pm diff --git a/lib/site/HTTP/Request.pm b/lib/fallback/HTTP/Request.pm similarity index 100% rename from lib/site/HTTP/Request.pm rename to lib/fallback/HTTP/Request.pm diff --git a/lib/site/HTTP/Request/Common.pm b/lib/fallback/HTTP/Request/Common.pm similarity index 100% rename from lib/site/HTTP/Request/Common.pm rename to lib/fallback/HTTP/Request/Common.pm diff --git a/lib/site/HTTP/Response.pm b/lib/fallback/HTTP/Response.pm similarity index 100% rename from lib/site/HTTP/Response.pm rename to lib/fallback/HTTP/Response.pm diff --git a/lib/site/HTTP/Status.pm b/lib/fallback/HTTP/Status.pm similarity index 100% rename from lib/site/HTTP/Status.pm rename to lib/fallback/HTTP/Status.pm diff --git a/lib/site/Hardware/iButton/Connection.pm b/lib/fallback/Hardware/iButton/Connection.pm similarity index 100% rename from lib/site/Hardware/iButton/Connection.pm rename to lib/fallback/Hardware/iButton/Connection.pm diff --git a/lib/site/Hardware/iButton/Device.pm b/lib/fallback/Hardware/iButton/Device.pm similarity index 100% rename from lib/site/Hardware/iButton/Device.pm rename to lib/fallback/Hardware/iButton/Device.pm diff --git a/lib/site/Hardware/iButton/old/Connection.pm b/lib/fallback/Hardware/iButton/old/Connection.pm similarity index 100% rename from lib/site/Hardware/iButton/old/Connection.pm rename to lib/fallback/Hardware/iButton/old/Connection.pm diff --git a/lib/site/Hardware/iButton/old/Device.pm b/lib/fallback/Hardware/iButton/old/Device.pm similarity index 100% rename from lib/site/Hardware/iButton/old/Device.pm rename to lib/fallback/Hardware/iButton/old/Device.pm diff --git a/lib/site/IO/Interface.pm b/lib/fallback/IO/Interface.pm similarity index 100% rename from lib/site/IO/Interface.pm rename to lib/fallback/IO/Interface.pm diff --git a/lib/site/IO/Interface/Simple.pm b/lib/fallback/IO/Interface/Simple.pm similarity index 100% rename from lib/site/IO/Interface/Simple.pm rename to lib/fallback/IO/Interface/Simple.pm diff --git a/lib/site/JSON.pm b/lib/fallback/JSON.pm similarity index 100% rename from lib/site/JSON.pm rename to lib/fallback/JSON.pm diff --git a/lib/site/JSON/PP.pm b/lib/fallback/JSON/PP.pm similarity index 100% rename from lib/site/JSON/PP.pm rename to lib/fallback/JSON/PP.pm diff --git a/lib/site/JSON/PP/Boolean.pm b/lib/fallback/JSON/PP/Boolean.pm similarity index 100% rename from lib/site/JSON/PP/Boolean.pm rename to lib/fallback/JSON/PP/Boolean.pm diff --git a/lib/site/JSON/PP5005.pm b/lib/fallback/JSON/PP5005.pm similarity index 100% rename from lib/site/JSON/PP5005.pm rename to lib/fallback/JSON/PP5005.pm diff --git a/lib/site/JSON/PP56.pm b/lib/fallback/JSON/PP56.pm similarity index 100% rename from lib/site/JSON/PP56.pm rename to lib/fallback/JSON/PP56.pm diff --git a/lib/site/JSON/PP58.pm b/lib/fallback/JSON/PP58.pm similarity index 100% rename from lib/site/JSON/PP58.pm rename to lib/fallback/JSON/PP58.pm diff --git a/lib/site/LWP.pm b/lib/fallback/LWP.pm similarity index 100% rename from lib/site/LWP.pm rename to lib/fallback/LWP.pm diff --git a/lib/site/LWP/Authen/Basic.pm b/lib/fallback/LWP/Authen/Basic.pm similarity index 100% rename from lib/site/LWP/Authen/Basic.pm rename to lib/fallback/LWP/Authen/Basic.pm diff --git a/lib/site/LWP/Authen/Digest.pm b/lib/fallback/LWP/Authen/Digest.pm similarity index 100% rename from lib/site/LWP/Authen/Digest.pm rename to lib/fallback/LWP/Authen/Digest.pm diff --git a/lib/site/LWP/Authen/Ntlm.pm b/lib/fallback/LWP/Authen/Ntlm.pm similarity index 100% rename from lib/site/LWP/Authen/Ntlm.pm rename to lib/fallback/LWP/Authen/Ntlm.pm diff --git a/lib/site/LWP/ConnCache.pm b/lib/fallback/LWP/ConnCache.pm similarity index 100% rename from lib/site/LWP/ConnCache.pm rename to lib/fallback/LWP/ConnCache.pm diff --git a/lib/site/LWP/Debug.pm b/lib/fallback/LWP/Debug.pm similarity index 100% rename from lib/site/LWP/Debug.pm rename to lib/fallback/LWP/Debug.pm diff --git a/lib/site/LWP/DebugFile.pm b/lib/fallback/LWP/DebugFile.pm similarity index 100% rename from lib/site/LWP/DebugFile.pm rename to lib/fallback/LWP/DebugFile.pm diff --git a/lib/site/LWP/MediaTypes.pm b/lib/fallback/LWP/MediaTypes.pm similarity index 100% rename from lib/site/LWP/MediaTypes.pm rename to lib/fallback/LWP/MediaTypes.pm diff --git a/lib/site/LWP/MemberMixin.pm b/lib/fallback/LWP/MemberMixin.pm similarity index 100% rename from lib/site/LWP/MemberMixin.pm rename to lib/fallback/LWP/MemberMixin.pm diff --git a/lib/site/LWP/Protocol.pm b/lib/fallback/LWP/Protocol.pm similarity index 100% rename from lib/site/LWP/Protocol.pm rename to lib/fallback/LWP/Protocol.pm diff --git a/lib/site/LWP/Protocol/GHTTP.pm b/lib/fallback/LWP/Protocol/GHTTP.pm similarity index 100% rename from lib/site/LWP/Protocol/GHTTP.pm rename to lib/fallback/LWP/Protocol/GHTTP.pm diff --git a/lib/site/LWP/Protocol/cpan.pm b/lib/fallback/LWP/Protocol/cpan.pm similarity index 100% rename from lib/site/LWP/Protocol/cpan.pm rename to lib/fallback/LWP/Protocol/cpan.pm diff --git a/lib/site/LWP/Protocol/data.pm b/lib/fallback/LWP/Protocol/data.pm similarity index 100% rename from lib/site/LWP/Protocol/data.pm rename to lib/fallback/LWP/Protocol/data.pm diff --git a/lib/site/LWP/Protocol/file.pm b/lib/fallback/LWP/Protocol/file.pm similarity index 100% rename from lib/site/LWP/Protocol/file.pm rename to lib/fallback/LWP/Protocol/file.pm diff --git a/lib/site/LWP/Protocol/ftp.pm b/lib/fallback/LWP/Protocol/ftp.pm similarity index 100% rename from lib/site/LWP/Protocol/ftp.pm rename to lib/fallback/LWP/Protocol/ftp.pm diff --git a/lib/site/LWP/Protocol/gopher.pm b/lib/fallback/LWP/Protocol/gopher.pm similarity index 100% rename from lib/site/LWP/Protocol/gopher.pm rename to lib/fallback/LWP/Protocol/gopher.pm diff --git a/lib/site/LWP/Protocol/http.pm b/lib/fallback/LWP/Protocol/http.pm similarity index 100% rename from lib/site/LWP/Protocol/http.pm rename to lib/fallback/LWP/Protocol/http.pm diff --git a/lib/site/LWP/Protocol/http10.pm b/lib/fallback/LWP/Protocol/http10.pm similarity index 100% rename from lib/site/LWP/Protocol/http10.pm rename to lib/fallback/LWP/Protocol/http10.pm diff --git a/lib/site/LWP/Protocol/https.pm b/lib/fallback/LWP/Protocol/https.pm similarity index 100% rename from lib/site/LWP/Protocol/https.pm rename to lib/fallback/LWP/Protocol/https.pm diff --git a/lib/site/LWP/Protocol/https10.pm b/lib/fallback/LWP/Protocol/https10.pm similarity index 100% rename from lib/site/LWP/Protocol/https10.pm rename to lib/fallback/LWP/Protocol/https10.pm diff --git a/lib/site/LWP/Protocol/loopback.pm b/lib/fallback/LWP/Protocol/loopback.pm similarity index 100% rename from lib/site/LWP/Protocol/loopback.pm rename to lib/fallback/LWP/Protocol/loopback.pm diff --git a/lib/site/LWP/Protocol/mailto.pm b/lib/fallback/LWP/Protocol/mailto.pm similarity index 100% rename from lib/site/LWP/Protocol/mailto.pm rename to lib/fallback/LWP/Protocol/mailto.pm diff --git a/lib/site/LWP/Protocol/nntp.pm b/lib/fallback/LWP/Protocol/nntp.pm similarity index 100% rename from lib/site/LWP/Protocol/nntp.pm rename to lib/fallback/LWP/Protocol/nntp.pm diff --git a/lib/site/LWP/Protocol/nogo.pm b/lib/fallback/LWP/Protocol/nogo.pm similarity index 100% rename from lib/site/LWP/Protocol/nogo.pm rename to lib/fallback/LWP/Protocol/nogo.pm diff --git a/lib/site/LWP/RobotUA.pm b/lib/fallback/LWP/RobotUA.pm similarity index 100% rename from lib/site/LWP/RobotUA.pm rename to lib/fallback/LWP/RobotUA.pm diff --git a/lib/site/LWP/Simple.pm b/lib/fallback/LWP/Simple.pm similarity index 100% rename from lib/site/LWP/Simple.pm rename to lib/fallback/LWP/Simple.pm diff --git a/lib/site/LWP/UserAgent.pm b/lib/fallback/LWP/UserAgent.pm similarity index 100% rename from lib/site/LWP/UserAgent.pm rename to lib/fallback/LWP/UserAgent.pm diff --git a/lib/site/LWP/media.types b/lib/fallback/LWP/media.types similarity index 100% rename from lib/site/LWP/media.types rename to lib/fallback/LWP/media.types diff --git a/lib/site/Lingua/EN/Numbers.pm b/lib/fallback/Lingua/EN/Numbers.pm similarity index 100% rename from lib/site/Lingua/EN/Numbers.pm rename to lib/fallback/Lingua/EN/Numbers.pm diff --git a/lib/site/Lingua/ES/Numeros.pm b/lib/fallback/Lingua/ES/Numeros.pm similarity index 100% rename from lib/site/Lingua/ES/Numeros.pm rename to lib/fallback/Lingua/ES/Numeros.pm diff --git a/lib/site/Lingua/FR/Numbers.pm b/lib/fallback/Lingua/FR/Numbers.pm similarity index 100% rename from lib/site/Lingua/FR/Numbers.pm rename to lib/fallback/Lingua/FR/Numbers.pm diff --git a/lib/site/Lingua/IT/Numbers.pm b/lib/fallback/Lingua/IT/Numbers.pm similarity index 100% rename from lib/site/Lingua/IT/Numbers.pm rename to lib/fallback/Lingua/IT/Numbers.pm diff --git a/lib/site/Lingua/Num2Word.pm b/lib/fallback/Lingua/Num2Word.pm similarity index 100% rename from lib/site/Lingua/Num2Word.pm rename to lib/fallback/Lingua/Num2Word.pm diff --git a/lib/site/MIME/Lite.pm b/lib/fallback/MIME/Lite.pm similarity index 100% rename from lib/site/MIME/Lite.pm rename to lib/fallback/MIME/Lite.pm diff --git a/lib/site/Net/AOLIM.pm b/lib/fallback/Net/AOLIM.pm similarity index 100% rename from lib/site/Net/AOLIM.pm rename to lib/fallback/Net/AOLIM.pm diff --git a/lib/site/Net/Cmd.pm b/lib/fallback/Net/Cmd.pm similarity index 100% rename from lib/site/Net/Cmd.pm rename to lib/fallback/Net/Cmd.pm diff --git a/lib/site/Net/Config.pm b/lib/fallback/Net/Config.pm similarity index 100% rename from lib/site/Net/Config.pm rename to lib/fallback/Net/Config.pm diff --git a/lib/site/Net/DNS.pm b/lib/fallback/Net/DNS.pm similarity index 100% rename from lib/site/Net/DNS.pm rename to lib/fallback/Net/DNS.pm diff --git a/lib/site/Net/DNS/Header.pm b/lib/fallback/Net/DNS/Header.pm similarity index 100% rename from lib/site/Net/DNS/Header.pm rename to lib/fallback/Net/DNS/Header.pm diff --git a/lib/site/Net/DNS/Packet.pm b/lib/fallback/Net/DNS/Packet.pm similarity index 100% rename from lib/site/Net/DNS/Packet.pm rename to lib/fallback/Net/DNS/Packet.pm diff --git a/lib/site/Net/DNS/Question.pm b/lib/fallback/Net/DNS/Question.pm similarity index 100% rename from lib/site/Net/DNS/Question.pm rename to lib/fallback/Net/DNS/Question.pm diff --git a/lib/site/Net/DNS/RR.pm b/lib/fallback/Net/DNS/RR.pm similarity index 100% rename from lib/site/Net/DNS/RR.pm rename to lib/fallback/Net/DNS/RR.pm diff --git a/lib/site/Net/DNS/RR/A.pm b/lib/fallback/Net/DNS/RR/A.pm similarity index 100% rename from lib/site/Net/DNS/RR/A.pm rename to lib/fallback/Net/DNS/RR/A.pm diff --git a/lib/site/Net/DNS/RR/AAAA.pm b/lib/fallback/Net/DNS/RR/AAAA.pm similarity index 100% rename from lib/site/Net/DNS/RR/AAAA.pm rename to lib/fallback/Net/DNS/RR/AAAA.pm diff --git a/lib/site/Net/DNS/RR/AFSDB.pm b/lib/fallback/Net/DNS/RR/AFSDB.pm similarity index 100% rename from lib/site/Net/DNS/RR/AFSDB.pm rename to lib/fallback/Net/DNS/RR/AFSDB.pm diff --git a/lib/site/Net/DNS/RR/CNAME.pm b/lib/fallback/Net/DNS/RR/CNAME.pm similarity index 100% rename from lib/site/Net/DNS/RR/CNAME.pm rename to lib/fallback/Net/DNS/RR/CNAME.pm diff --git a/lib/site/Net/DNS/RR/EID.pm b/lib/fallback/Net/DNS/RR/EID.pm similarity index 100% rename from lib/site/Net/DNS/RR/EID.pm rename to lib/fallback/Net/DNS/RR/EID.pm diff --git a/lib/site/Net/DNS/RR/HINFO.pm b/lib/fallback/Net/DNS/RR/HINFO.pm similarity index 100% rename from lib/site/Net/DNS/RR/HINFO.pm rename to lib/fallback/Net/DNS/RR/HINFO.pm diff --git a/lib/site/Net/DNS/RR/ISDN.pm b/lib/fallback/Net/DNS/RR/ISDN.pm similarity index 100% rename from lib/site/Net/DNS/RR/ISDN.pm rename to lib/fallback/Net/DNS/RR/ISDN.pm diff --git a/lib/site/Net/DNS/RR/LOC.pm b/lib/fallback/Net/DNS/RR/LOC.pm similarity index 100% rename from lib/site/Net/DNS/RR/LOC.pm rename to lib/fallback/Net/DNS/RR/LOC.pm diff --git a/lib/site/Net/DNS/RR/MB.pm b/lib/fallback/Net/DNS/RR/MB.pm similarity index 100% rename from lib/site/Net/DNS/RR/MB.pm rename to lib/fallback/Net/DNS/RR/MB.pm diff --git a/lib/site/Net/DNS/RR/MG.pm b/lib/fallback/Net/DNS/RR/MG.pm similarity index 100% rename from lib/site/Net/DNS/RR/MG.pm rename to lib/fallback/Net/DNS/RR/MG.pm diff --git a/lib/site/Net/DNS/RR/MINFO.pm b/lib/fallback/Net/DNS/RR/MINFO.pm similarity index 100% rename from lib/site/Net/DNS/RR/MINFO.pm rename to lib/fallback/Net/DNS/RR/MINFO.pm diff --git a/lib/site/Net/DNS/RR/MR.pm b/lib/fallback/Net/DNS/RR/MR.pm similarity index 100% rename from lib/site/Net/DNS/RR/MR.pm rename to lib/fallback/Net/DNS/RR/MR.pm diff --git a/lib/site/Net/DNS/RR/MX.pm b/lib/fallback/Net/DNS/RR/MX.pm similarity index 100% rename from lib/site/Net/DNS/RR/MX.pm rename to lib/fallback/Net/DNS/RR/MX.pm diff --git a/lib/site/Net/DNS/RR/NAPTR.pm b/lib/fallback/Net/DNS/RR/NAPTR.pm similarity index 100% rename from lib/site/Net/DNS/RR/NAPTR.pm rename to lib/fallback/Net/DNS/RR/NAPTR.pm diff --git a/lib/site/Net/DNS/RR/NIMLOC.pm b/lib/fallback/Net/DNS/RR/NIMLOC.pm similarity index 100% rename from lib/site/Net/DNS/RR/NIMLOC.pm rename to lib/fallback/Net/DNS/RR/NIMLOC.pm diff --git a/lib/site/Net/DNS/RR/NS.pm b/lib/fallback/Net/DNS/RR/NS.pm similarity index 100% rename from lib/site/Net/DNS/RR/NS.pm rename to lib/fallback/Net/DNS/RR/NS.pm diff --git a/lib/site/Net/DNS/RR/NSAP.pm b/lib/fallback/Net/DNS/RR/NSAP.pm similarity index 100% rename from lib/site/Net/DNS/RR/NSAP.pm rename to lib/fallback/Net/DNS/RR/NSAP.pm diff --git a/lib/site/Net/DNS/RR/NULL.pm b/lib/fallback/Net/DNS/RR/NULL.pm similarity index 100% rename from lib/site/Net/DNS/RR/NULL.pm rename to lib/fallback/Net/DNS/RR/NULL.pm diff --git a/lib/site/Net/DNS/RR/PTR.pm b/lib/fallback/Net/DNS/RR/PTR.pm similarity index 100% rename from lib/site/Net/DNS/RR/PTR.pm rename to lib/fallback/Net/DNS/RR/PTR.pm diff --git a/lib/site/Net/DNS/RR/PX.pm b/lib/fallback/Net/DNS/RR/PX.pm similarity index 100% rename from lib/site/Net/DNS/RR/PX.pm rename to lib/fallback/Net/DNS/RR/PX.pm diff --git a/lib/site/Net/DNS/RR/RP.pm b/lib/fallback/Net/DNS/RR/RP.pm similarity index 100% rename from lib/site/Net/DNS/RR/RP.pm rename to lib/fallback/Net/DNS/RR/RP.pm diff --git a/lib/site/Net/DNS/RR/RT.pm b/lib/fallback/Net/DNS/RR/RT.pm similarity index 100% rename from lib/site/Net/DNS/RR/RT.pm rename to lib/fallback/Net/DNS/RR/RT.pm diff --git a/lib/site/Net/DNS/RR/SOA.pm b/lib/fallback/Net/DNS/RR/SOA.pm similarity index 100% rename from lib/site/Net/DNS/RR/SOA.pm rename to lib/fallback/Net/DNS/RR/SOA.pm diff --git a/lib/site/Net/DNS/RR/SRV.pm b/lib/fallback/Net/DNS/RR/SRV.pm similarity index 100% rename from lib/site/Net/DNS/RR/SRV.pm rename to lib/fallback/Net/DNS/RR/SRV.pm diff --git a/lib/site/Net/DNS/RR/TXT.pm b/lib/fallback/Net/DNS/RR/TXT.pm similarity index 100% rename from lib/site/Net/DNS/RR/TXT.pm rename to lib/fallback/Net/DNS/RR/TXT.pm diff --git a/lib/site/Net/DNS/RR/X25.pm b/lib/fallback/Net/DNS/RR/X25.pm similarity index 100% rename from lib/site/Net/DNS/RR/X25.pm rename to lib/fallback/Net/DNS/RR/X25.pm diff --git a/lib/site/Net/DNS/Resolver.pm b/lib/fallback/Net/DNS/Resolver.pm similarity index 100% rename from lib/site/Net/DNS/Resolver.pm rename to lib/fallback/Net/DNS/Resolver.pm diff --git a/lib/site/Net/DNS/Update.pm b/lib/fallback/Net/DNS/Update.pm similarity index 100% rename from lib/site/Net/DNS/Update.pm rename to lib/fallback/Net/DNS/Update.pm diff --git a/lib/site/Net/Domain.pm b/lib/fallback/Net/Domain.pm similarity index 100% rename from lib/site/Net/Domain.pm rename to lib/fallback/Net/Domain.pm diff --git a/lib/site/Net/DummyInetd.pm b/lib/fallback/Net/DummyInetd.pm similarity index 100% rename from lib/site/Net/DummyInetd.pm rename to lib/fallback/Net/DummyInetd.pm diff --git a/lib/site/Net/FTP.pm b/lib/fallback/Net/FTP.pm similarity index 100% rename from lib/site/Net/FTP.pm rename to lib/fallback/Net/FTP.pm diff --git a/lib/site/Net/FTP/A.pm b/lib/fallback/Net/FTP/A.pm similarity index 100% rename from lib/site/Net/FTP/A.pm rename to lib/fallback/Net/FTP/A.pm diff --git a/lib/site/Net/FTP/E.pm b/lib/fallback/Net/FTP/E.pm similarity index 100% rename from lib/site/Net/FTP/E.pm rename to lib/fallback/Net/FTP/E.pm diff --git a/lib/site/Net/FTP/I.pm b/lib/fallback/Net/FTP/I.pm similarity index 100% rename from lib/site/Net/FTP/I.pm rename to lib/fallback/Net/FTP/I.pm diff --git a/lib/site/Net/FTP/L.pm b/lib/fallback/Net/FTP/L.pm similarity index 100% rename from lib/site/Net/FTP/L.pm rename to lib/fallback/Net/FTP/L.pm diff --git a/lib/site/Net/FTP/dataconn.pm b/lib/fallback/Net/FTP/dataconn.pm similarity index 100% rename from lib/site/Net/FTP/dataconn.pm rename to lib/fallback/Net/FTP/dataconn.pm diff --git a/lib/site/Net/HTTP.pm b/lib/fallback/Net/HTTP.pm similarity index 100% rename from lib/site/Net/HTTP.pm rename to lib/fallback/Net/HTTP.pm diff --git a/lib/site/Net/HTTP/Methods.pm b/lib/fallback/Net/HTTP/Methods.pm similarity index 100% rename from lib/site/Net/HTTP/Methods.pm rename to lib/fallback/Net/HTTP/Methods.pm diff --git a/lib/site/Net/HTTP/NB.pm b/lib/fallback/Net/HTTP/NB.pm similarity index 100% rename from lib/site/Net/HTTP/NB.pm rename to lib/fallback/Net/HTTP/NB.pm diff --git a/lib/site/Net/HTTPS.pm b/lib/fallback/Net/HTTPS.pm similarity index 100% rename from lib/site/Net/HTTPS.pm rename to lib/fallback/Net/HTTPS.pm diff --git a/lib/site/Net/NNTP.pm b/lib/fallback/Net/NNTP.pm similarity index 100% rename from lib/site/Net/NNTP.pm rename to lib/fallback/Net/NNTP.pm diff --git a/lib/site/Net/Netrc.pm b/lib/fallback/Net/Netrc.pm similarity index 100% rename from lib/site/Net/Netrc.pm rename to lib/fallback/Net/Netrc.pm diff --git a/lib/site/Net/OSCAR.pm b/lib/fallback/Net/OSCAR.pm similarity index 100% rename from lib/site/Net/OSCAR.pm rename to lib/fallback/Net/OSCAR.pm diff --git a/lib/site/Net/OSCAR/Buddylist.pm b/lib/fallback/Net/OSCAR/Buddylist.pm similarity index 100% rename from lib/site/Net/OSCAR/Buddylist.pm rename to lib/fallback/Net/OSCAR/Buddylist.pm diff --git a/lib/site/Net/OSCAR/Callbacks.pm b/lib/fallback/Net/OSCAR/Callbacks.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks.pm rename to lib/fallback/Net/OSCAR/Callbacks.pm diff --git a/lib/site/Net/OSCAR/Callbacks/0/error.pm b/lib/fallback/Net/OSCAR/Callbacks/0/error.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/0/error.pm rename to lib/fallback/Net/OSCAR/Callbacks/0/error.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/incoming_extended_information.pm b/lib/fallback/Net/OSCAR/Callbacks/1/incoming_extended_information.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/incoming_extended_information.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/incoming_extended_information.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/incoming_warning.pm b/lib/fallback/Net/OSCAR/Callbacks/1/incoming_warning.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/incoming_warning.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/incoming_warning.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/migrate.pm b/lib/fallback/Net/OSCAR/Callbacks/1/migrate.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/migrate.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/migrate.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/pause.pm b/lib/fallback/Net/OSCAR/Callbacks/1/pause.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/pause.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/pause.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/rate_change.pm b/lib/fallback/Net/OSCAR/Callbacks/1/rate_change.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/rate_change.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/rate_change.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/rate_info_response.pm b/lib/fallback/Net/OSCAR/Callbacks/1/rate_info_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/rate_info_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/rate_info_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/self_information.pm b/lib/fallback/Net/OSCAR/Callbacks/1/self_information.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/self_information.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/self_information.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/server_ready.pm b/lib/fallback/Net/OSCAR/Callbacks/1/server_ready.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/server_ready.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/server_ready.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/service_redirect_response.pm b/lib/fallback/Net/OSCAR/Callbacks/1/service_redirect_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/service_redirect_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/service_redirect_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/1/unpause.pm b/lib/fallback/Net/OSCAR/Callbacks/1/unpause.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/1/unpause.pm rename to lib/fallback/Net/OSCAR/Callbacks/1/unpause.pm diff --git a/lib/site/Net/OSCAR/Callbacks/13/chat_navigator_response.pm b/lib/fallback/Net/OSCAR/Callbacks/13/chat_navigator_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/13/chat_navigator_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/13/chat_navigator_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/14/chat_buddy_arrival.pm b/lib/fallback/Net/OSCAR/Callbacks/14/chat_buddy_arrival.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/14/chat_buddy_arrival.pm rename to lib/fallback/Net/OSCAR/Callbacks/14/chat_buddy_arrival.pm diff --git a/lib/site/Net/OSCAR/Callbacks/14/chat_buddy_departure.pm b/lib/fallback/Net/OSCAR/Callbacks/14/chat_buddy_departure.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/14/chat_buddy_departure.pm rename to lib/fallback/Net/OSCAR/Callbacks/14/chat_buddy_departure.pm diff --git a/lib/site/Net/OSCAR/Callbacks/14/chat_room_status.pm b/lib/fallback/Net/OSCAR/Callbacks/14/chat_room_status.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/14/chat_room_status.pm rename to lib/fallback/Net/OSCAR/Callbacks/14/chat_room_status.pm diff --git a/lib/site/Net/OSCAR/Callbacks/14/incoming_chat_IM.pm b/lib/fallback/Net/OSCAR/Callbacks/14/incoming_chat_IM.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/14/incoming_chat_IM.pm rename to lib/fallback/Net/OSCAR/Callbacks/14/incoming_chat_IM.pm diff --git a/lib/site/Net/OSCAR/Callbacks/16/buddy_icon_downloaded.pm b/lib/fallback/Net/OSCAR/Callbacks/16/buddy_icon_downloaded.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/16/buddy_icon_downloaded.pm rename to lib/fallback/Net/OSCAR/Callbacks/16/buddy_icon_downloaded.pm diff --git a/lib/site/Net/OSCAR/Callbacks/16/buddy_icon_uploaded.pm b/lib/fallback/Net/OSCAR/Callbacks/16/buddy_icon_uploaded.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/16/buddy_icon_uploaded.pm rename to lib/fallback/Net/OSCAR/Callbacks/16/buddy_icon_uploaded.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist_3_response.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist_3_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist_3_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist_3_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist_add.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist_add.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist_add.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist_add.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist_delete.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist_delete.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist_delete.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist_delete.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist_error.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist_error.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist_error.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist_error.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist_modification_acknowledgement.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist_modification_acknowledgement.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist_modification_acknowledgement.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist_modification_acknowledgement.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/buddylist_modify.pm b/lib/fallback/Net/OSCAR/Callbacks/19/buddylist_modify.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/buddylist_modify.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/buddylist_modify.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/end_buddylist_modifications.pm b/lib/fallback/Net/OSCAR/Callbacks/19/end_buddylist_modifications.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/end_buddylist_modifications.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/end_buddylist_modifications.pm diff --git a/lib/site/Net/OSCAR/Callbacks/19/start_buddylist_modifications.pm b/lib/fallback/Net/OSCAR/Callbacks/19/start_buddylist_modifications.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/19/start_buddylist_modifications.pm rename to lib/fallback/Net/OSCAR/Callbacks/19/start_buddylist_modifications.pm diff --git a/lib/site/Net/OSCAR/Callbacks/2/incoming_profile.pm b/lib/fallback/Net/OSCAR/Callbacks/2/incoming_profile.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/2/incoming_profile.pm rename to lib/fallback/Net/OSCAR/Callbacks/2/incoming_profile.pm diff --git a/lib/site/Net/OSCAR/Callbacks/21/ICQ_meta_response.pm b/lib/fallback/Net/OSCAR/Callbacks/21/ICQ_meta_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/21/ICQ_meta_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/21/ICQ_meta_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/23/authentication_key.pm b/lib/fallback/Net/OSCAR/Callbacks/23/authentication_key.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/23/authentication_key.pm rename to lib/fallback/Net/OSCAR/Callbacks/23/authentication_key.pm diff --git a/lib/site/Net/OSCAR/Callbacks/23/authorization_response.pm b/lib/fallback/Net/OSCAR/Callbacks/23/authorization_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/23/authorization_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/23/authorization_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/3/buddy_rights_response.pm b/lib/fallback/Net/OSCAR/Callbacks/3/buddy_rights_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/3/buddy_rights_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/3/buddy_rights_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/3/buddy_signoff.pm b/lib/fallback/Net/OSCAR/Callbacks/3/buddy_signoff.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/3/buddy_signoff.pm rename to lib/fallback/Net/OSCAR/Callbacks/3/buddy_signoff.pm diff --git a/lib/site/Net/OSCAR/Callbacks/3/buddy_status_update.pm b/lib/fallback/Net/OSCAR/Callbacks/3/buddy_status_update.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/3/buddy_status_update.pm rename to lib/fallback/Net/OSCAR/Callbacks/3/buddy_status_update.pm diff --git a/lib/site/Net/OSCAR/Callbacks/4/IM_acknowledgement.pm b/lib/fallback/Net/OSCAR/Callbacks/4/IM_acknowledgement.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/4/IM_acknowledgement.pm rename to lib/fallback/Net/OSCAR/Callbacks/4/IM_acknowledgement.pm diff --git a/lib/site/Net/OSCAR/Callbacks/4/chat_invitation_decline.pm b/lib/fallback/Net/OSCAR/Callbacks/4/chat_invitation_decline.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/4/chat_invitation_decline.pm rename to lib/fallback/Net/OSCAR/Callbacks/4/chat_invitation_decline.pm diff --git a/lib/site/Net/OSCAR/Callbacks/4/incoming_IM.pm b/lib/fallback/Net/OSCAR/Callbacks/4/incoming_IM.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/4/incoming_IM.pm rename to lib/fallback/Net/OSCAR/Callbacks/4/incoming_IM.pm diff --git a/lib/site/Net/OSCAR/Callbacks/4/typing_notification.pm b/lib/fallback/Net/OSCAR/Callbacks/4/typing_notification.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/4/typing_notification.pm rename to lib/fallback/Net/OSCAR/Callbacks/4/typing_notification.pm diff --git a/lib/site/Net/OSCAR/Callbacks/7/admin_request_response.pm b/lib/fallback/Net/OSCAR/Callbacks/7/admin_request_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/7/admin_request_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/7/admin_request_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/7/confirm_account_response.pm b/lib/fallback/Net/OSCAR/Callbacks/7/confirm_account_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/7/confirm_account_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/7/confirm_account_response.pm diff --git a/lib/site/Net/OSCAR/Callbacks/9/BOS_rights_response.pm b/lib/fallback/Net/OSCAR/Callbacks/9/BOS_rights_response.pm similarity index 100% rename from lib/site/Net/OSCAR/Callbacks/9/BOS_rights_response.pm rename to lib/fallback/Net/OSCAR/Callbacks/9/BOS_rights_response.pm diff --git a/lib/site/Net/OSCAR/Common.pm b/lib/fallback/Net/OSCAR/Common.pm similarity index 100% rename from lib/site/Net/OSCAR/Common.pm rename to lib/fallback/Net/OSCAR/Common.pm diff --git a/lib/site/Net/OSCAR/Connection.pm b/lib/fallback/Net/OSCAR/Connection.pm similarity index 100% rename from lib/site/Net/OSCAR/Connection.pm rename to lib/fallback/Net/OSCAR/Connection.pm diff --git a/lib/site/Net/OSCAR/Connection/Chat.pm b/lib/fallback/Net/OSCAR/Connection/Chat.pm similarity index 100% rename from lib/site/Net/OSCAR/Connection/Chat.pm rename to lib/fallback/Net/OSCAR/Connection/Chat.pm diff --git a/lib/site/Net/OSCAR/Connection/Direct.pm b/lib/fallback/Net/OSCAR/Connection/Direct.pm similarity index 100% rename from lib/site/Net/OSCAR/Connection/Direct.pm rename to lib/fallback/Net/OSCAR/Connection/Direct.pm diff --git a/lib/site/Net/OSCAR/Connection/Server.pm b/lib/fallback/Net/OSCAR/Connection/Server.pm similarity index 100% rename from lib/site/Net/OSCAR/Connection/Server.pm rename to lib/fallback/Net/OSCAR/Connection/Server.pm diff --git a/lib/site/Net/OSCAR/Constants.pm b/lib/fallback/Net/OSCAR/Constants.pm similarity index 100% rename from lib/site/Net/OSCAR/Constants.pm rename to lib/fallback/Net/OSCAR/Constants.pm diff --git a/lib/site/Net/OSCAR/MethodInfo.pm b/lib/fallback/Net/OSCAR/MethodInfo.pm similarity index 100% rename from lib/site/Net/OSCAR/MethodInfo.pm rename to lib/fallback/Net/OSCAR/MethodInfo.pm diff --git a/lib/site/Net/OSCAR/Proxy.pm b/lib/fallback/Net/OSCAR/Proxy.pm similarity index 100% rename from lib/site/Net/OSCAR/Proxy.pm rename to lib/fallback/Net/OSCAR/Proxy.pm diff --git a/lib/site/Net/OSCAR/Screenname.pm b/lib/fallback/Net/OSCAR/Screenname.pm similarity index 100% rename from lib/site/Net/OSCAR/Screenname.pm rename to lib/fallback/Net/OSCAR/Screenname.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks.pm b/lib/fallback/Net/OSCAR/ServerCallbacks.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/0/BOS_signon.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/0/BOS_signon.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/0/BOS_signon.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/0/BOS_signon.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/1/personal_info_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/1/personal_info_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/1/personal_info_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/1/personal_info_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/1/rate_acknowledgement.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/1/rate_acknowledgement.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/1/rate_acknowledgement.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/1/rate_acknowledgement.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/1/rate_info_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/1/rate_info_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/1/rate_info_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/1/rate_info_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/1/set_extended_status.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/1/set_extended_status.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/1/set_extended_status.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/1/set_extended_status.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/1/set_service_versions.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/1/set_service_versions.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/1/set_service_versions.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/1/set_service_versions.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/1/set_tool_versions.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/1/set_tool_versions.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/1/set_tool_versions.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/1/set_tool_versions.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/19/buddylist_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/19/buddylist_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/19/buddylist_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/19/buddylist_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/19/buddylist_rights_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/19/buddylist_rights_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/19/buddylist_rights_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/19/buddylist_rights_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/2/get_away.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/2/get_away.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/2/get_away.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/2/get_away.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/2/locate_rights_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/2/locate_rights_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/2/locate_rights_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/2/locate_rights_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/23/initial_signon_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/23/initial_signon_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/23/initial_signon_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/23/initial_signon_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/23/signon.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/23/signon.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/23/signon.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/23/signon.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/3/buddy_rights_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/3/buddy_rights_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/3/buddy_rights_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/3/buddy_rights_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/4/IM_parameter_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/4/IM_parameter_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/4/IM_parameter_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/4/IM_parameter_request.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/4/add_IM_parameters.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/4/add_IM_parameters.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/4/add_IM_parameters.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/4/add_IM_parameters.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/4/outgoing_IM.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/4/outgoing_IM.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/4/outgoing_IM.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/4/outgoing_IM.pm diff --git a/lib/site/Net/OSCAR/ServerCallbacks/9/BOS_rights_request.pm b/lib/fallback/Net/OSCAR/ServerCallbacks/9/BOS_rights_request.pm similarity index 100% rename from lib/site/Net/OSCAR/ServerCallbacks/9/BOS_rights_request.pm rename to lib/fallback/Net/OSCAR/ServerCallbacks/9/BOS_rights_request.pm diff --git a/lib/site/Net/OSCAR/TLV.pm b/lib/fallback/Net/OSCAR/TLV.pm similarity index 100% rename from lib/site/Net/OSCAR/TLV.pm rename to lib/fallback/Net/OSCAR/TLV.pm diff --git a/lib/site/Net/OSCAR/Utility.pm b/lib/fallback/Net/OSCAR/Utility.pm similarity index 100% rename from lib/site/Net/OSCAR/Utility.pm rename to lib/fallback/Net/OSCAR/Utility.pm diff --git a/lib/site/Net/OSCAR/XML.pm b/lib/fallback/Net/OSCAR/XML.pm similarity index 100% rename from lib/site/Net/OSCAR/XML.pm rename to lib/fallback/Net/OSCAR/XML.pm diff --git a/lib/site/Net/OSCAR/XML/Protocol.dtd b/lib/fallback/Net/OSCAR/XML/Protocol.dtd similarity index 100% rename from lib/site/Net/OSCAR/XML/Protocol.dtd rename to lib/fallback/Net/OSCAR/XML/Protocol.dtd diff --git a/lib/site/Net/OSCAR/XML/Protocol.parsed-xml b/lib/fallback/Net/OSCAR/XML/Protocol.parsed-xml similarity index 100% rename from lib/site/Net/OSCAR/XML/Protocol.parsed-xml rename to lib/fallback/Net/OSCAR/XML/Protocol.parsed-xml diff --git a/lib/site/Net/OSCAR/XML/Protocol.xml b/lib/fallback/Net/OSCAR/XML/Protocol.xml similarity index 100% rename from lib/site/Net/OSCAR/XML/Protocol.xml rename to lib/fallback/Net/OSCAR/XML/Protocol.xml diff --git a/lib/site/Net/OSCAR/XML/Template.pm b/lib/fallback/Net/OSCAR/XML/Template.pm similarity index 100% rename from lib/site/Net/OSCAR/XML/Template.pm rename to lib/fallback/Net/OSCAR/XML/Template.pm diff --git a/lib/site/Net/OSCAR/_BLInternal.pm b/lib/fallback/Net/OSCAR/_BLInternal.pm similarity index 100% rename from lib/site/Net/OSCAR/_BLInternal.pm rename to lib/fallback/Net/OSCAR/_BLInternal.pm diff --git a/lib/site/Net/PH.pm b/lib/fallback/Net/PH.pm similarity index 100% rename from lib/site/Net/PH.pm rename to lib/fallback/Net/PH.pm diff --git a/lib/site/Net/POP3.pm b/lib/fallback/Net/POP3.pm similarity index 100% rename from lib/site/Net/POP3.pm rename to lib/fallback/Net/POP3.pm diff --git a/lib/site/Net/SMTP.pm b/lib/fallback/Net/SMTP.pm similarity index 100% rename from lib/site/Net/SMTP.pm rename to lib/fallback/Net/SMTP.pm diff --git a/lib/site/Net/SMTP_auth.pm b/lib/fallback/Net/SMTP_auth.pm similarity index 100% rename from lib/site/Net/SMTP_auth.pm rename to lib/fallback/Net/SMTP_auth.pm diff --git a/lib/site/Net/SNPP.pm b/lib/fallback/Net/SNPP.pm similarity index 100% rename from lib/site/Net/SNPP.pm rename to lib/fallback/Net/SNPP.pm diff --git a/lib/site/Net/Time.pm b/lib/fallback/Net/Time.pm similarity index 100% rename from lib/site/Net/Time.pm rename to lib/fallback/Net/Time.pm diff --git a/lib/site/Params/Validate.pm b/lib/fallback/Params/Validate.pm similarity index 100% rename from lib/site/Params/Validate.pm rename to lib/fallback/Params/Validate.pm diff --git a/lib/site/Params/ValidatePP.pm b/lib/fallback/Params/ValidatePP.pm similarity index 100% rename from lib/site/Params/ValidatePP.pm rename to lib/fallback/Params/ValidatePP.pm diff --git a/lib/site/Params/ValidateXS.pm b/lib/fallback/Params/ValidateXS.pm similarity index 100% rename from lib/site/Params/ValidateXS.pm rename to lib/fallback/Params/ValidateXS.pm diff --git a/lib/site/RRD/Simple.pm b/lib/fallback/RRD/Simple.pm similarity index 100% rename from lib/site/RRD/Simple.pm rename to lib/fallback/RRD/Simple.pm diff --git a/lib/site/RRDTool/Rawish.pm b/lib/fallback/RRDTool/Rawish.pm similarity index 100% rename from lib/site/RRDTool/Rawish.pm rename to lib/fallback/RRDTool/Rawish.pm diff --git a/lib/site/Regexp/Common.pm b/lib/fallback/Regexp/Common.pm similarity index 100% rename from lib/site/Regexp/Common.pm rename to lib/fallback/Regexp/Common.pm diff --git a/lib/site/Regexp/Common/CC.pm b/lib/fallback/Regexp/Common/CC.pm similarity index 100% rename from lib/site/Regexp/Common/CC.pm rename to lib/fallback/Regexp/Common/CC.pm diff --git a/lib/site/Regexp/Common/SEN.pm b/lib/fallback/Regexp/Common/SEN.pm similarity index 100% rename from lib/site/Regexp/Common/SEN.pm rename to lib/fallback/Regexp/Common/SEN.pm diff --git a/lib/site/Regexp/Common/URI.pm b/lib/fallback/Regexp/Common/URI.pm similarity index 100% rename from lib/site/Regexp/Common/URI.pm rename to lib/fallback/Regexp/Common/URI.pm diff --git a/lib/site/Regexp/Common/URI/RFC1035.pm b/lib/fallback/Regexp/Common/URI/RFC1035.pm similarity index 100% rename from lib/site/Regexp/Common/URI/RFC1035.pm rename to lib/fallback/Regexp/Common/URI/RFC1035.pm diff --git a/lib/site/Regexp/Common/URI/RFC1738.pm b/lib/fallback/Regexp/Common/URI/RFC1738.pm similarity index 100% rename from lib/site/Regexp/Common/URI/RFC1738.pm rename to lib/fallback/Regexp/Common/URI/RFC1738.pm diff --git a/lib/site/Regexp/Common/URI/RFC1808.pm b/lib/fallback/Regexp/Common/URI/RFC1808.pm similarity index 100% rename from lib/site/Regexp/Common/URI/RFC1808.pm rename to lib/fallback/Regexp/Common/URI/RFC1808.pm diff --git a/lib/site/Regexp/Common/URI/RFC2384.pm b/lib/fallback/Regexp/Common/URI/RFC2384.pm similarity index 100% rename from lib/site/Regexp/Common/URI/RFC2384.pm rename to lib/fallback/Regexp/Common/URI/RFC2384.pm diff --git a/lib/site/Regexp/Common/URI/RFC2396.pm b/lib/fallback/Regexp/Common/URI/RFC2396.pm similarity index 100% rename from lib/site/Regexp/Common/URI/RFC2396.pm rename to lib/fallback/Regexp/Common/URI/RFC2396.pm diff --git a/lib/site/Regexp/Common/URI/RFC2806.pm b/lib/fallback/Regexp/Common/URI/RFC2806.pm similarity index 100% rename from lib/site/Regexp/Common/URI/RFC2806.pm rename to lib/fallback/Regexp/Common/URI/RFC2806.pm diff --git a/lib/site/Regexp/Common/URI/fax.pm b/lib/fallback/Regexp/Common/URI/fax.pm similarity index 100% rename from lib/site/Regexp/Common/URI/fax.pm rename to lib/fallback/Regexp/Common/URI/fax.pm diff --git a/lib/site/Regexp/Common/URI/file.pm b/lib/fallback/Regexp/Common/URI/file.pm similarity index 100% rename from lib/site/Regexp/Common/URI/file.pm rename to lib/fallback/Regexp/Common/URI/file.pm diff --git a/lib/site/Regexp/Common/URI/ftp.pm b/lib/fallback/Regexp/Common/URI/ftp.pm similarity index 100% rename from lib/site/Regexp/Common/URI/ftp.pm rename to lib/fallback/Regexp/Common/URI/ftp.pm diff --git a/lib/site/Regexp/Common/URI/gopher.pm b/lib/fallback/Regexp/Common/URI/gopher.pm similarity index 100% rename from lib/site/Regexp/Common/URI/gopher.pm rename to lib/fallback/Regexp/Common/URI/gopher.pm diff --git a/lib/site/Regexp/Common/URI/http.pm b/lib/fallback/Regexp/Common/URI/http.pm similarity index 100% rename from lib/site/Regexp/Common/URI/http.pm rename to lib/fallback/Regexp/Common/URI/http.pm diff --git a/lib/site/Regexp/Common/URI/news.pm b/lib/fallback/Regexp/Common/URI/news.pm similarity index 100% rename from lib/site/Regexp/Common/URI/news.pm rename to lib/fallback/Regexp/Common/URI/news.pm diff --git a/lib/site/Regexp/Common/URI/pop.pm b/lib/fallback/Regexp/Common/URI/pop.pm similarity index 100% rename from lib/site/Regexp/Common/URI/pop.pm rename to lib/fallback/Regexp/Common/URI/pop.pm diff --git a/lib/site/Regexp/Common/URI/prospero.pm b/lib/fallback/Regexp/Common/URI/prospero.pm similarity index 100% rename from lib/site/Regexp/Common/URI/prospero.pm rename to lib/fallback/Regexp/Common/URI/prospero.pm diff --git a/lib/site/Regexp/Common/URI/tel.pm b/lib/fallback/Regexp/Common/URI/tel.pm similarity index 100% rename from lib/site/Regexp/Common/URI/tel.pm rename to lib/fallback/Regexp/Common/URI/tel.pm diff --git a/lib/site/Regexp/Common/URI/telnet.pm b/lib/fallback/Regexp/Common/URI/telnet.pm similarity index 100% rename from lib/site/Regexp/Common/URI/telnet.pm rename to lib/fallback/Regexp/Common/URI/telnet.pm diff --git a/lib/site/Regexp/Common/URI/tv.pm b/lib/fallback/Regexp/Common/URI/tv.pm similarity index 100% rename from lib/site/Regexp/Common/URI/tv.pm rename to lib/fallback/Regexp/Common/URI/tv.pm diff --git a/lib/site/Regexp/Common/URI/wais.pm b/lib/fallback/Regexp/Common/URI/wais.pm similarity index 100% rename from lib/site/Regexp/Common/URI/wais.pm rename to lib/fallback/Regexp/Common/URI/wais.pm diff --git a/lib/site/Regexp/Common/_support.pm b/lib/fallback/Regexp/Common/_support.pm similarity index 100% rename from lib/site/Regexp/Common/_support.pm rename to lib/fallback/Regexp/Common/_support.pm diff --git a/lib/site/Regexp/Common/balanced.pm b/lib/fallback/Regexp/Common/balanced.pm similarity index 100% rename from lib/site/Regexp/Common/balanced.pm rename to lib/fallback/Regexp/Common/balanced.pm diff --git a/lib/site/Regexp/Common/comment.pm b/lib/fallback/Regexp/Common/comment.pm similarity index 100% rename from lib/site/Regexp/Common/comment.pm rename to lib/fallback/Regexp/Common/comment.pm diff --git a/lib/site/Regexp/Common/delimited.pm b/lib/fallback/Regexp/Common/delimited.pm similarity index 100% rename from lib/site/Regexp/Common/delimited.pm rename to lib/fallback/Regexp/Common/delimited.pm diff --git a/lib/site/Regexp/Common/lingua.pm b/lib/fallback/Regexp/Common/lingua.pm similarity index 100% rename from lib/site/Regexp/Common/lingua.pm rename to lib/fallback/Regexp/Common/lingua.pm diff --git a/lib/site/Regexp/Common/list.pm b/lib/fallback/Regexp/Common/list.pm similarity index 100% rename from lib/site/Regexp/Common/list.pm rename to lib/fallback/Regexp/Common/list.pm diff --git a/lib/site/Regexp/Common/net.pm b/lib/fallback/Regexp/Common/net.pm similarity index 100% rename from lib/site/Regexp/Common/net.pm rename to lib/fallback/Regexp/Common/net.pm diff --git a/lib/site/Regexp/Common/number.pm b/lib/fallback/Regexp/Common/number.pm similarity index 100% rename from lib/site/Regexp/Common/number.pm rename to lib/fallback/Regexp/Common/number.pm diff --git a/lib/site/Regexp/Common/profanity.pm b/lib/fallback/Regexp/Common/profanity.pm similarity index 100% rename from lib/site/Regexp/Common/profanity.pm rename to lib/fallback/Regexp/Common/profanity.pm diff --git a/lib/site/Regexp/Common/whitespace.pm b/lib/fallback/Regexp/Common/whitespace.pm similarity index 100% rename from lib/site/Regexp/Common/whitespace.pm rename to lib/fallback/Regexp/Common/whitespace.pm diff --git a/lib/site/Regexp/Common/zip.pm b/lib/fallback/Regexp/Common/zip.pm similarity index 100% rename from lib/site/Regexp/Common/zip.pm rename to lib/fallback/Regexp/Common/zip.pm diff --git a/lib/site/SVG.pm b/lib/fallback/SVG.pm similarity index 100% rename from lib/site/SVG.pm rename to lib/fallback/SVG.pm diff --git a/lib/site/SVG/DOM.pm b/lib/fallback/SVG/DOM.pm similarity index 100% rename from lib/site/SVG/DOM.pm rename to lib/fallback/SVG/DOM.pm diff --git a/lib/site/SVG/Element.pm b/lib/fallback/SVG/Element.pm similarity index 100% rename from lib/site/SVG/Element.pm rename to lib/fallback/SVG/Element.pm diff --git a/lib/site/SVG/Extension.pm b/lib/fallback/SVG/Extension.pm similarity index 100% rename from lib/site/SVG/Extension.pm rename to lib/fallback/SVG/Extension.pm diff --git a/lib/site/SVG/XML.pm b/lib/fallback/SVG/XML.pm similarity index 100% rename from lib/site/SVG/XML.pm rename to lib/fallback/SVG/XML.pm diff --git a/lib/site/Set/Infinite.pm b/lib/fallback/Set/Infinite.pm similarity index 100% rename from lib/site/Set/Infinite.pm rename to lib/fallback/Set/Infinite.pm diff --git a/lib/site/Set/Infinite/Arithmetic.pm b/lib/fallback/Set/Infinite/Arithmetic.pm similarity index 100% rename from lib/site/Set/Infinite/Arithmetic.pm rename to lib/fallback/Set/Infinite/Arithmetic.pm diff --git a/lib/site/Set/Infinite/Basic.pm b/lib/fallback/Set/Infinite/Basic.pm similarity index 100% rename from lib/site/Set/Infinite/Basic.pm rename to lib/fallback/Set/Infinite/Basic.pm diff --git a/lib/site/Set/Infinite/_recurrence.pm b/lib/fallback/Set/Infinite/_recurrence.pm similarity index 100% rename from lib/site/Set/Infinite/_recurrence.pm rename to lib/fallback/Set/Infinite/_recurrence.pm diff --git a/lib/site/Text/vFile/asData.pm b/lib/fallback/Text/vFile/asData.pm similarity index 100% rename from lib/site/Text/vFile/asData.pm rename to lib/fallback/Text/vFile/asData.pm diff --git a/lib/site/Tie/Hash.pm b/lib/fallback/Tie/Hash.pm similarity index 100% rename from lib/site/Tie/Hash.pm rename to lib/fallback/Tie/Hash.pm diff --git a/lib/site/Tie/Hash.pm.original b/lib/fallback/Tie/Hash.pm.original similarity index 100% rename from lib/site/Tie/Hash.pm.original rename to lib/fallback/Tie/Hash.pm.original diff --git a/lib/site/Tie/IxHash.pm b/lib/fallback/Tie/IxHash.pm similarity index 100% rename from lib/site/Tie/IxHash.pm rename to lib/fallback/Tie/IxHash.pm diff --git a/lib/site/Time/CTime.pm b/lib/fallback/Time/CTime.pm similarity index 100% rename from lib/site/Time/CTime.pm rename to lib/fallback/Time/CTime.pm diff --git a/lib/site/Time/DaysInMonth.pm b/lib/fallback/Time/DaysInMonth.pm similarity index 100% rename from lib/site/Time/DaysInMonth.pm rename to lib/fallback/Time/DaysInMonth.pm diff --git a/lib/site/Time/JulianDay.pm b/lib/fallback/Time/JulianDay.pm similarity index 100% rename from lib/site/Time/JulianDay.pm rename to lib/fallback/Time/JulianDay.pm diff --git a/lib/site/Time/ParseDate.pm b/lib/fallback/Time/ParseDate.pm similarity index 100% rename from lib/site/Time/ParseDate.pm rename to lib/fallback/Time/ParseDate.pm diff --git a/lib/site/Time/Timezone.pm b/lib/fallback/Time/Timezone.pm similarity index 100% rename from lib/site/Time/Timezone.pm rename to lib/fallback/Time/Timezone.pm diff --git a/lib/site/Time/Zone.pm b/lib/fallback/Time/Zone.pm similarity index 100% rename from lib/site/Time/Zone.pm rename to lib/fallback/Time/Zone.pm diff --git a/lib/site/Tk/CursorControl.pm b/lib/fallback/Tk/CursorControl.pm similarity index 100% rename from lib/site/Tk/CursorControl.pm rename to lib/fallback/Tk/CursorControl.pm diff --git a/lib/site/Tk/ToolBar.pm b/lib/fallback/Tk/ToolBar.pm similarity index 100% rename from lib/site/Tk/ToolBar.pm rename to lib/fallback/Tk/ToolBar.pm diff --git a/lib/site/Tk/ToolBar/tkIcons b/lib/fallback/Tk/ToolBar/tkIcons similarity index 100% rename from lib/site/Tk/ToolBar/tkIcons rename to lib/fallback/Tk/ToolBar/tkIcons diff --git a/lib/site/Tk/trans_cur.mask b/lib/fallback/Tk/trans_cur.mask similarity index 100% rename from lib/site/Tk/trans_cur.mask rename to lib/fallback/Tk/trans_cur.mask diff --git a/lib/site/Tk/trans_cur.xbm b/lib/fallback/Tk/trans_cur.xbm similarity index 100% rename from lib/site/Tk/trans_cur.xbm rename to lib/fallback/Tk/trans_cur.xbm diff --git a/lib/site/URI.pm b/lib/fallback/URI.pm similarity index 100% rename from lib/site/URI.pm rename to lib/fallback/URI.pm diff --git a/lib/site/URI/Escape.pm b/lib/fallback/URI/Escape.pm similarity index 100% rename from lib/site/URI/Escape.pm rename to lib/fallback/URI/Escape.pm diff --git a/lib/site/URI/Heuristic.pm b/lib/fallback/URI/Heuristic.pm similarity index 100% rename from lib/site/URI/Heuristic.pm rename to lib/fallback/URI/Heuristic.pm diff --git a/lib/site/URI/QueryParam.pm b/lib/fallback/URI/QueryParam.pm similarity index 100% rename from lib/site/URI/QueryParam.pm rename to lib/fallback/URI/QueryParam.pm diff --git a/lib/site/URI/Split.pm b/lib/fallback/URI/Split.pm similarity index 100% rename from lib/site/URI/Split.pm rename to lib/fallback/URI/Split.pm diff --git a/lib/site/URI/URL.pm b/lib/fallback/URI/URL.pm similarity index 100% rename from lib/site/URI/URL.pm rename to lib/fallback/URI/URL.pm diff --git a/lib/site/URI/URL/_generic.pm b/lib/fallback/URI/URL/_generic.pm similarity index 100% rename from lib/site/URI/URL/_generic.pm rename to lib/fallback/URI/URL/_generic.pm diff --git a/lib/site/URI/URL/_login.pm b/lib/fallback/URI/URL/_login.pm similarity index 100% rename from lib/site/URI/URL/_login.pm rename to lib/fallback/URI/URL/_login.pm diff --git a/lib/site/URI/URL/data.pm b/lib/fallback/URI/URL/data.pm similarity index 100% rename from lib/site/URI/URL/data.pm rename to lib/fallback/URI/URL/data.pm diff --git a/lib/site/URI/URL/file.pm b/lib/fallback/URI/URL/file.pm similarity index 100% rename from lib/site/URI/URL/file.pm rename to lib/fallback/URI/URL/file.pm diff --git a/lib/site/URI/URL/finger.pm b/lib/fallback/URI/URL/finger.pm similarity index 100% rename from lib/site/URI/URL/finger.pm rename to lib/fallback/URI/URL/finger.pm diff --git a/lib/site/URI/URL/ftp.pm b/lib/fallback/URI/URL/ftp.pm similarity index 100% rename from lib/site/URI/URL/ftp.pm rename to lib/fallback/URI/URL/ftp.pm diff --git a/lib/site/URI/URL/gopher.pm b/lib/fallback/URI/URL/gopher.pm similarity index 100% rename from lib/site/URI/URL/gopher.pm rename to lib/fallback/URI/URL/gopher.pm diff --git a/lib/site/URI/URL/http.pm b/lib/fallback/URI/URL/http.pm similarity index 100% rename from lib/site/URI/URL/http.pm rename to lib/fallback/URI/URL/http.pm diff --git a/lib/site/URI/URL/https.pm b/lib/fallback/URI/URL/https.pm similarity index 100% rename from lib/site/URI/URL/https.pm rename to lib/fallback/URI/URL/https.pm diff --git a/lib/site/URI/URL/mailto.pm b/lib/fallback/URI/URL/mailto.pm similarity index 100% rename from lib/site/URI/URL/mailto.pm rename to lib/fallback/URI/URL/mailto.pm diff --git a/lib/site/URI/URL/news.pm b/lib/fallback/URI/URL/news.pm similarity index 100% rename from lib/site/URI/URL/news.pm rename to lib/fallback/URI/URL/news.pm diff --git a/lib/site/URI/URL/nntp.pm b/lib/fallback/URI/URL/nntp.pm similarity index 100% rename from lib/site/URI/URL/nntp.pm rename to lib/fallback/URI/URL/nntp.pm diff --git a/lib/site/URI/URL/prospero.pm b/lib/fallback/URI/URL/prospero.pm similarity index 100% rename from lib/site/URI/URL/prospero.pm rename to lib/fallback/URI/URL/prospero.pm diff --git a/lib/site/URI/URL/rlogin.pm b/lib/fallback/URI/URL/rlogin.pm similarity index 100% rename from lib/site/URI/URL/rlogin.pm rename to lib/fallback/URI/URL/rlogin.pm diff --git a/lib/site/URI/URL/telnet.pm b/lib/fallback/URI/URL/telnet.pm similarity index 100% rename from lib/site/URI/URL/telnet.pm rename to lib/fallback/URI/URL/telnet.pm diff --git a/lib/site/URI/URL/tn3270.pm b/lib/fallback/URI/URL/tn3270.pm similarity index 100% rename from lib/site/URI/URL/tn3270.pm rename to lib/fallback/URI/URL/tn3270.pm diff --git a/lib/site/URI/URL/wais.pm b/lib/fallback/URI/URL/wais.pm similarity index 100% rename from lib/site/URI/URL/wais.pm rename to lib/fallback/URI/URL/wais.pm diff --git a/lib/site/URI/URL/webster.pm b/lib/fallback/URI/URL/webster.pm similarity index 100% rename from lib/site/URI/URL/webster.pm rename to lib/fallback/URI/URL/webster.pm diff --git a/lib/site/URI/URL/whois.pm b/lib/fallback/URI/URL/whois.pm similarity index 100% rename from lib/site/URI/URL/whois.pm rename to lib/fallback/URI/URL/whois.pm diff --git a/lib/site/URI/WithBase.pm b/lib/fallback/URI/WithBase.pm similarity index 100% rename from lib/site/URI/WithBase.pm rename to lib/fallback/URI/WithBase.pm diff --git a/lib/site/URI/_foreign.pm b/lib/fallback/URI/_foreign.pm similarity index 100% rename from lib/site/URI/_foreign.pm rename to lib/fallback/URI/_foreign.pm diff --git a/lib/site/URI/_generic.pm b/lib/fallback/URI/_generic.pm similarity index 100% rename from lib/site/URI/_generic.pm rename to lib/fallback/URI/_generic.pm diff --git a/lib/site/URI/_ldap.pm b/lib/fallback/URI/_ldap.pm similarity index 100% rename from lib/site/URI/_ldap.pm rename to lib/fallback/URI/_ldap.pm diff --git a/lib/site/URI/_login.pm b/lib/fallback/URI/_login.pm similarity index 100% rename from lib/site/URI/_login.pm rename to lib/fallback/URI/_login.pm diff --git a/lib/site/URI/_query.pm b/lib/fallback/URI/_query.pm similarity index 100% rename from lib/site/URI/_query.pm rename to lib/fallback/URI/_query.pm diff --git a/lib/site/URI/_segment.pm b/lib/fallback/URI/_segment.pm similarity index 100% rename from lib/site/URI/_segment.pm rename to lib/fallback/URI/_segment.pm diff --git a/lib/site/URI/_server.pm b/lib/fallback/URI/_server.pm similarity index 100% rename from lib/site/URI/_server.pm rename to lib/fallback/URI/_server.pm diff --git a/lib/site/URI/_userpass.pm b/lib/fallback/URI/_userpass.pm similarity index 100% rename from lib/site/URI/_userpass.pm rename to lib/fallback/URI/_userpass.pm diff --git a/lib/site/URI/data.pm b/lib/fallback/URI/data.pm similarity index 100% rename from lib/site/URI/data.pm rename to lib/fallback/URI/data.pm diff --git a/lib/site/URI/file.pm b/lib/fallback/URI/file.pm similarity index 100% rename from lib/site/URI/file.pm rename to lib/fallback/URI/file.pm diff --git a/lib/site/URI/file/Base.pm b/lib/fallback/URI/file/Base.pm similarity index 100% rename from lib/site/URI/file/Base.pm rename to lib/fallback/URI/file/Base.pm diff --git a/lib/site/URI/file/FAT.pm b/lib/fallback/URI/file/FAT.pm similarity index 100% rename from lib/site/URI/file/FAT.pm rename to lib/fallback/URI/file/FAT.pm diff --git a/lib/site/URI/file/Mac.pm b/lib/fallback/URI/file/Mac.pm similarity index 100% rename from lib/site/URI/file/Mac.pm rename to lib/fallback/URI/file/Mac.pm diff --git a/lib/site/URI/file/OS2.pm b/lib/fallback/URI/file/OS2.pm similarity index 100% rename from lib/site/URI/file/OS2.pm rename to lib/fallback/URI/file/OS2.pm diff --git a/lib/site/URI/file/QNX.pm b/lib/fallback/URI/file/QNX.pm similarity index 100% rename from lib/site/URI/file/QNX.pm rename to lib/fallback/URI/file/QNX.pm diff --git a/lib/site/URI/file/Unix.pm b/lib/fallback/URI/file/Unix.pm similarity index 100% rename from lib/site/URI/file/Unix.pm rename to lib/fallback/URI/file/Unix.pm diff --git a/lib/site/URI/file/Win32.pm b/lib/fallback/URI/file/Win32.pm similarity index 100% rename from lib/site/URI/file/Win32.pm rename to lib/fallback/URI/file/Win32.pm diff --git a/lib/site/URI/ftp.pm b/lib/fallback/URI/ftp.pm similarity index 100% rename from lib/site/URI/ftp.pm rename to lib/fallback/URI/ftp.pm diff --git a/lib/site/URI/gopher.pm b/lib/fallback/URI/gopher.pm similarity index 100% rename from lib/site/URI/gopher.pm rename to lib/fallback/URI/gopher.pm diff --git a/lib/site/URI/http.pm b/lib/fallback/URI/http.pm similarity index 100% rename from lib/site/URI/http.pm rename to lib/fallback/URI/http.pm diff --git a/lib/site/URI/https.pm b/lib/fallback/URI/https.pm similarity index 100% rename from lib/site/URI/https.pm rename to lib/fallback/URI/https.pm diff --git a/lib/site/URI/ldap.pm b/lib/fallback/URI/ldap.pm similarity index 100% rename from lib/site/URI/ldap.pm rename to lib/fallback/URI/ldap.pm diff --git a/lib/site/URI/ldapi.pm b/lib/fallback/URI/ldapi.pm similarity index 100% rename from lib/site/URI/ldapi.pm rename to lib/fallback/URI/ldapi.pm diff --git a/lib/site/URI/ldaps.pm b/lib/fallback/URI/ldaps.pm similarity index 100% rename from lib/site/URI/ldaps.pm rename to lib/fallback/URI/ldaps.pm diff --git a/lib/site/URI/mailto.pm b/lib/fallback/URI/mailto.pm similarity index 100% rename from lib/site/URI/mailto.pm rename to lib/fallback/URI/mailto.pm diff --git a/lib/site/URI/mms.pm b/lib/fallback/URI/mms.pm similarity index 100% rename from lib/site/URI/mms.pm rename to lib/fallback/URI/mms.pm diff --git a/lib/site/URI/news.pm b/lib/fallback/URI/news.pm similarity index 100% rename from lib/site/URI/news.pm rename to lib/fallback/URI/news.pm diff --git a/lib/site/URI/nntp.pm b/lib/fallback/URI/nntp.pm similarity index 100% rename from lib/site/URI/nntp.pm rename to lib/fallback/URI/nntp.pm diff --git a/lib/site/URI/pop.pm b/lib/fallback/URI/pop.pm similarity index 100% rename from lib/site/URI/pop.pm rename to lib/fallback/URI/pop.pm diff --git a/lib/site/URI/rlogin.pm b/lib/fallback/URI/rlogin.pm similarity index 100% rename from lib/site/URI/rlogin.pm rename to lib/fallback/URI/rlogin.pm diff --git a/lib/site/URI/rsync.pm b/lib/fallback/URI/rsync.pm similarity index 100% rename from lib/site/URI/rsync.pm rename to lib/fallback/URI/rsync.pm diff --git a/lib/site/URI/rtsp.pm b/lib/fallback/URI/rtsp.pm similarity index 100% rename from lib/site/URI/rtsp.pm rename to lib/fallback/URI/rtsp.pm diff --git a/lib/site/URI/rtspu.pm b/lib/fallback/URI/rtspu.pm similarity index 100% rename from lib/site/URI/rtspu.pm rename to lib/fallback/URI/rtspu.pm diff --git a/lib/site/URI/sip.pm b/lib/fallback/URI/sip.pm similarity index 100% rename from lib/site/URI/sip.pm rename to lib/fallback/URI/sip.pm diff --git a/lib/site/URI/sips.pm b/lib/fallback/URI/sips.pm similarity index 100% rename from lib/site/URI/sips.pm rename to lib/fallback/URI/sips.pm diff --git a/lib/site/URI/snews.pm b/lib/fallback/URI/snews.pm similarity index 100% rename from lib/site/URI/snews.pm rename to lib/fallback/URI/snews.pm diff --git a/lib/site/URI/ssh.pm b/lib/fallback/URI/ssh.pm similarity index 100% rename from lib/site/URI/ssh.pm rename to lib/fallback/URI/ssh.pm diff --git a/lib/site/URI/telnet.pm b/lib/fallback/URI/telnet.pm similarity index 100% rename from lib/site/URI/telnet.pm rename to lib/fallback/URI/telnet.pm diff --git a/lib/site/URI/tn3270.pm b/lib/fallback/URI/tn3270.pm similarity index 100% rename from lib/site/URI/tn3270.pm rename to lib/fallback/URI/tn3270.pm diff --git a/lib/site/URI/urn.pm b/lib/fallback/URI/urn.pm similarity index 100% rename from lib/site/URI/urn.pm rename to lib/fallback/URI/urn.pm diff --git a/lib/site/URI/urn/isbn.pm b/lib/fallback/URI/urn/isbn.pm similarity index 100% rename from lib/site/URI/urn/isbn.pm rename to lib/fallback/URI/urn/isbn.pm diff --git a/lib/site/URI/urn/oid.pm b/lib/fallback/URI/urn/oid.pm similarity index 100% rename from lib/site/URI/urn/oid.pm rename to lib/fallback/URI/urn/oid.pm diff --git a/lib/site/Win32/DUN.pm b/lib/fallback/Win32/DUN.pm similarity index 100% rename from lib/site/Win32/DUN.pm rename to lib/fallback/Win32/DUN.pm diff --git a/lib/site/Win32/DriveInfo.pm b/lib/fallback/Win32/DriveInfo.pm similarity index 100% rename from lib/site/Win32/DriveInfo.pm rename to lib/fallback/Win32/DriveInfo.pm diff --git a/lib/site/Win32/DriveInfo.pm.html b/lib/fallback/Win32/DriveInfo.pm.html similarity index 100% rename from lib/site/Win32/DriveInfo.pm.html rename to lib/fallback/Win32/DriveInfo.pm.html diff --git a/lib/site/Win32/DriveInfo.txt b/lib/fallback/Win32/DriveInfo.txt similarity index 100% rename from lib/site/Win32/DriveInfo.txt rename to lib/fallback/Win32/DriveInfo.txt diff --git a/lib/site/Win32/IIPC.pm b/lib/fallback/Win32/IIPC.pm similarity index 100% rename from lib/site/Win32/IIPC.pm rename to lib/fallback/Win32/IIPC.pm diff --git a/lib/site/Win32/IPERFSUP.PM b/lib/fallback/Win32/IPERFSUP.PM similarity index 100% rename from lib/site/Win32/IPERFSUP.PM rename to lib/fallback/Win32/IPERFSUP.PM diff --git a/lib/site/Win32/IPerfmon.pm b/lib/fallback/Win32/IPerfmon.pm similarity index 100% rename from lib/site/Win32/IPerfmon.pm rename to lib/fallback/Win32/IPerfmon.pm diff --git a/lib/site/Win32/IProc.pm b/lib/fallback/Win32/IProc.pm similarity index 100% rename from lib/site/Win32/IProc.pm rename to lib/fallback/Win32/IProc.pm diff --git a/lib/site/Win32/ISYNC.PM b/lib/fallback/Win32/ISYNC.PM similarity index 100% rename from lib/site/Win32/ISYNC.PM rename to lib/fallback/Win32/ISYNC.PM diff --git a/lib/site/Win32/MemMap.pm b/lib/fallback/Win32/MemMap.pm similarity index 100% rename from lib/site/Win32/MemMap.pm rename to lib/fallback/Win32/MemMap.pm diff --git a/lib/site/Win32/SerialPort.html b/lib/fallback/Win32/SerialPort.html similarity index 100% rename from lib/site/Win32/SerialPort.html rename to lib/fallback/Win32/SerialPort.html diff --git a/lib/site/Win32/SerialPort.pm b/lib/fallback/Win32/SerialPort.pm similarity index 100% rename from lib/site/Win32/SerialPort.pm rename to lib/fallback/Win32/SerialPort.pm diff --git a/lib/site/Win32/SerialPort.pm.original b/lib/fallback/Win32/SerialPort.pm.original similarity index 100% rename from lib/site/Win32/SerialPort.pm.original rename to lib/fallback/Win32/SerialPort.pm.original diff --git a/lib/site/Win32/SerialPort.txt b/lib/fallback/Win32/SerialPort.txt similarity index 100% rename from lib/site/Win32/SerialPort.txt rename to lib/fallback/Win32/SerialPort.txt diff --git a/lib/site/Win32/SoundEx.pm b/lib/fallback/Win32/SoundEx.pm similarity index 100% rename from lib/site/Win32/SoundEx.pm rename to lib/fallback/Win32/SoundEx.pm diff --git a/lib/site/Win32/TieRegistry.pm b/lib/fallback/Win32/TieRegistry.pm similarity index 100% rename from lib/site/Win32/TieRegistry.pm rename to lib/fallback/Win32/TieRegistry.pm diff --git a/lib/site/Win32/dun.txt b/lib/fallback/Win32/dun.txt similarity index 100% rename from lib/site/Win32/dun.txt rename to lib/fallback/Win32/dun.txt diff --git a/lib/site/Win32API/CommPort.html b/lib/fallback/Win32API/CommPort.html similarity index 100% rename from lib/site/Win32API/CommPort.html rename to lib/fallback/Win32API/CommPort.html diff --git a/lib/site/Win32API/CommPort.pm b/lib/fallback/Win32API/CommPort.pm similarity index 100% rename from lib/site/Win32API/CommPort.pm rename to lib/fallback/Win32API/CommPort.pm diff --git a/lib/site/Win32API/Resources.html b/lib/fallback/Win32API/Resources.html similarity index 100% rename from lib/site/Win32API/Resources.html rename to lib/fallback/Win32API/Resources.html diff --git a/lib/site/Win32API/Resources.pm b/lib/fallback/Win32API/Resources.pm similarity index 100% rename from lib/site/Win32API/Resources.pm rename to lib/fallback/Win32API/Resources.pm diff --git a/lib/site/XML/DOM.pm b/lib/fallback/XML/DOM.pm similarity index 100% rename from lib/site/XML/DOM.pm rename to lib/fallback/XML/DOM.pm diff --git a/lib/site/XML/DOM/AttDef.pod b/lib/fallback/XML/DOM/AttDef.pod similarity index 100% rename from lib/site/XML/DOM/AttDef.pod rename to lib/fallback/XML/DOM/AttDef.pod diff --git a/lib/site/XML/DOM/AttlistDecl.pod b/lib/fallback/XML/DOM/AttlistDecl.pod similarity index 100% rename from lib/site/XML/DOM/AttlistDecl.pod rename to lib/fallback/XML/DOM/AttlistDecl.pod diff --git a/lib/site/XML/DOM/Attr.pod b/lib/fallback/XML/DOM/Attr.pod similarity index 100% rename from lib/site/XML/DOM/Attr.pod rename to lib/fallback/XML/DOM/Attr.pod diff --git a/lib/site/XML/DOM/CDATASection.pod b/lib/fallback/XML/DOM/CDATASection.pod similarity index 100% rename from lib/site/XML/DOM/CDATASection.pod rename to lib/fallback/XML/DOM/CDATASection.pod diff --git a/lib/site/XML/DOM/CharacterData.pod b/lib/fallback/XML/DOM/CharacterData.pod similarity index 100% rename from lib/site/XML/DOM/CharacterData.pod rename to lib/fallback/XML/DOM/CharacterData.pod diff --git a/lib/site/XML/DOM/Comment.pod b/lib/fallback/XML/DOM/Comment.pod similarity index 100% rename from lib/site/XML/DOM/Comment.pod rename to lib/fallback/XML/DOM/Comment.pod diff --git a/lib/site/XML/DOM/DOMException.pm b/lib/fallback/XML/DOM/DOMException.pm similarity index 100% rename from lib/site/XML/DOM/DOMException.pm rename to lib/fallback/XML/DOM/DOMException.pm diff --git a/lib/site/XML/DOM/DOMImplementation.pod b/lib/fallback/XML/DOM/DOMImplementation.pod similarity index 100% rename from lib/site/XML/DOM/DOMImplementation.pod rename to lib/fallback/XML/DOM/DOMImplementation.pod diff --git a/lib/site/XML/DOM/Document.pod b/lib/fallback/XML/DOM/Document.pod similarity index 100% rename from lib/site/XML/DOM/Document.pod rename to lib/fallback/XML/DOM/Document.pod diff --git a/lib/site/XML/DOM/DocumentFragment.pod b/lib/fallback/XML/DOM/DocumentFragment.pod similarity index 100% rename from lib/site/XML/DOM/DocumentFragment.pod rename to lib/fallback/XML/DOM/DocumentFragment.pod diff --git a/lib/site/XML/DOM/DocumentType.pod b/lib/fallback/XML/DOM/DocumentType.pod similarity index 100% rename from lib/site/XML/DOM/DocumentType.pod rename to lib/fallback/XML/DOM/DocumentType.pod diff --git a/lib/site/XML/DOM/Element.pod b/lib/fallback/XML/DOM/Element.pod similarity index 100% rename from lib/site/XML/DOM/Element.pod rename to lib/fallback/XML/DOM/Element.pod diff --git a/lib/site/XML/DOM/ElementDecl.pod b/lib/fallback/XML/DOM/ElementDecl.pod similarity index 100% rename from lib/site/XML/DOM/ElementDecl.pod rename to lib/fallback/XML/DOM/ElementDecl.pod diff --git a/lib/site/XML/DOM/Entity.pod b/lib/fallback/XML/DOM/Entity.pod similarity index 100% rename from lib/site/XML/DOM/Entity.pod rename to lib/fallback/XML/DOM/Entity.pod diff --git a/lib/site/XML/DOM/EntityReference.pod b/lib/fallback/XML/DOM/EntityReference.pod similarity index 100% rename from lib/site/XML/DOM/EntityReference.pod rename to lib/fallback/XML/DOM/EntityReference.pod diff --git a/lib/site/XML/DOM/NamedNodeMap.pm b/lib/fallback/XML/DOM/NamedNodeMap.pm similarity index 100% rename from lib/site/XML/DOM/NamedNodeMap.pm rename to lib/fallback/XML/DOM/NamedNodeMap.pm diff --git a/lib/site/XML/DOM/NamedNodeMap.pod b/lib/fallback/XML/DOM/NamedNodeMap.pod similarity index 100% rename from lib/site/XML/DOM/NamedNodeMap.pod rename to lib/fallback/XML/DOM/NamedNodeMap.pod diff --git a/lib/site/XML/DOM/Node.pod b/lib/fallback/XML/DOM/Node.pod similarity index 100% rename from lib/site/XML/DOM/Node.pod rename to lib/fallback/XML/DOM/Node.pod diff --git a/lib/site/XML/DOM/NodeList.pm b/lib/fallback/XML/DOM/NodeList.pm similarity index 100% rename from lib/site/XML/DOM/NodeList.pm rename to lib/fallback/XML/DOM/NodeList.pm diff --git a/lib/site/XML/DOM/NodeList.pod b/lib/fallback/XML/DOM/NodeList.pod similarity index 100% rename from lib/site/XML/DOM/NodeList.pod rename to lib/fallback/XML/DOM/NodeList.pod diff --git a/lib/site/XML/DOM/Notation.pod b/lib/fallback/XML/DOM/Notation.pod similarity index 100% rename from lib/site/XML/DOM/Notation.pod rename to lib/fallback/XML/DOM/Notation.pod diff --git a/lib/site/XML/DOM/Parser.pod b/lib/fallback/XML/DOM/Parser.pod similarity index 100% rename from lib/site/XML/DOM/Parser.pod rename to lib/fallback/XML/DOM/Parser.pod diff --git a/lib/site/XML/DOM/PerlSAX.pm b/lib/fallback/XML/DOM/PerlSAX.pm similarity index 100% rename from lib/site/XML/DOM/PerlSAX.pm rename to lib/fallback/XML/DOM/PerlSAX.pm diff --git a/lib/site/XML/DOM/ProcessingInstruction.pod b/lib/fallback/XML/DOM/ProcessingInstruction.pod similarity index 100% rename from lib/site/XML/DOM/ProcessingInstruction.pod rename to lib/fallback/XML/DOM/ProcessingInstruction.pod diff --git a/lib/site/XML/DOM/Text.pod b/lib/fallback/XML/DOM/Text.pod similarity index 100% rename from lib/site/XML/DOM/Text.pod rename to lib/fallback/XML/DOM/Text.pod diff --git a/lib/site/XML/DOM/XMLDecl.pod b/lib/fallback/XML/DOM/XMLDecl.pod similarity index 100% rename from lib/site/XML/DOM/XMLDecl.pod rename to lib/fallback/XML/DOM/XMLDecl.pod diff --git a/lib/site/XML/ESISParser.pm b/lib/fallback/XML/ESISParser.pm similarity index 100% rename from lib/site/XML/ESISParser.pm rename to lib/fallback/XML/ESISParser.pm diff --git a/lib/site/XML/Elemental.pm b/lib/fallback/XML/Elemental.pm similarity index 100% rename from lib/site/XML/Elemental.pm rename to lib/fallback/XML/Elemental.pm diff --git a/lib/site/XML/Elemental/Characters.pm b/lib/fallback/XML/Elemental/Characters.pm similarity index 100% rename from lib/site/XML/Elemental/Characters.pm rename to lib/fallback/XML/Elemental/Characters.pm diff --git a/lib/site/XML/Elemental/Document.pm b/lib/fallback/XML/Elemental/Document.pm similarity index 100% rename from lib/site/XML/Elemental/Document.pm rename to lib/fallback/XML/Elemental/Document.pm diff --git a/lib/site/XML/Elemental/Element.pm b/lib/fallback/XML/Elemental/Element.pm similarity index 100% rename from lib/site/XML/Elemental/Element.pm rename to lib/fallback/XML/Elemental/Element.pm diff --git a/lib/site/XML/Elemental/Node.pm b/lib/fallback/XML/Elemental/Node.pm similarity index 100% rename from lib/site/XML/Elemental/Node.pm rename to lib/fallback/XML/Elemental/Node.pm diff --git a/lib/site/XML/Elemental/SAXHandler.pm b/lib/fallback/XML/Elemental/SAXHandler.pm similarity index 100% rename from lib/site/XML/Elemental/SAXHandler.pm rename to lib/fallback/XML/Elemental/SAXHandler.pm diff --git a/lib/site/XML/Elemental/Util.pm b/lib/fallback/XML/Elemental/Util.pm similarity index 100% rename from lib/site/XML/Elemental/Util.pm rename to lib/fallback/XML/Elemental/Util.pm diff --git a/lib/site/XML/Handler/BuildDOM.pm b/lib/fallback/XML/Handler/BuildDOM.pm similarity index 100% rename from lib/site/XML/Handler/BuildDOM.pm rename to lib/fallback/XML/Handler/BuildDOM.pm diff --git a/lib/site/XML/Handler/CanonXMLWriter.pm b/lib/fallback/XML/Handler/CanonXMLWriter.pm similarity index 100% rename from lib/site/XML/Handler/CanonXMLWriter.pm rename to lib/fallback/XML/Handler/CanonXMLWriter.pm diff --git a/lib/site/XML/Handler/Sample.pm b/lib/fallback/XML/Handler/Sample.pm similarity index 100% rename from lib/site/XML/Handler/Sample.pm rename to lib/fallback/XML/Handler/Sample.pm diff --git a/lib/site/XML/Handler/Subs.pm b/lib/fallback/XML/Handler/Subs.pm similarity index 100% rename from lib/site/XML/Handler/Subs.pm rename to lib/fallback/XML/Handler/Subs.pm diff --git a/lib/site/XML/Handler/XMLWriter.pm b/lib/fallback/XML/Handler/XMLWriter.pm similarity index 100% rename from lib/site/XML/Handler/XMLWriter.pm rename to lib/fallback/XML/Handler/XMLWriter.pm diff --git a/lib/site/XML/NamespaceSupport.pm b/lib/fallback/XML/NamespaceSupport.pm similarity index 100% rename from lib/site/XML/NamespaceSupport.pm rename to lib/fallback/XML/NamespaceSupport.pm diff --git a/lib/site/XML/Parser/PerlSAX.pm b/lib/fallback/XML/Parser/PerlSAX.pm similarity index 100% rename from lib/site/XML/Parser/PerlSAX.pm rename to lib/fallback/XML/Parser/PerlSAX.pm diff --git a/lib/site/XML/Parser/Style/Elemental.pm b/lib/fallback/XML/Parser/Style/Elemental.pm similarity index 100% rename from lib/site/XML/Parser/Style/Elemental.pm rename to lib/fallback/XML/Parser/Style/Elemental.pm diff --git a/lib/site/XML/PatAct/ActionTempl.pm b/lib/fallback/XML/PatAct/ActionTempl.pm similarity index 100% rename from lib/site/XML/PatAct/ActionTempl.pm rename to lib/fallback/XML/PatAct/ActionTempl.pm diff --git a/lib/site/XML/PatAct/Amsterdam.pm b/lib/fallback/XML/PatAct/Amsterdam.pm similarity index 100% rename from lib/site/XML/PatAct/Amsterdam.pm rename to lib/fallback/XML/PatAct/Amsterdam.pm diff --git a/lib/site/XML/PatAct/MatchName.pm b/lib/fallback/XML/PatAct/MatchName.pm similarity index 100% rename from lib/site/XML/PatAct/MatchName.pm rename to lib/fallback/XML/PatAct/MatchName.pm diff --git a/lib/site/XML/PatAct/PatternTempl.pm b/lib/fallback/XML/PatAct/PatternTempl.pm similarity index 100% rename from lib/site/XML/PatAct/PatternTempl.pm rename to lib/fallback/XML/PatAct/PatternTempl.pm diff --git a/lib/site/XML/PatAct/ToObjects.pm b/lib/fallback/XML/PatAct/ToObjects.pm similarity index 100% rename from lib/site/XML/PatAct/ToObjects.pm rename to lib/fallback/XML/PatAct/ToObjects.pm diff --git a/lib/site/XML/Perl2SAX.pm b/lib/fallback/XML/Perl2SAX.pm similarity index 100% rename from lib/site/XML/Perl2SAX.pm rename to lib/fallback/XML/Perl2SAX.pm diff --git a/lib/site/XML/RAI.pm b/lib/fallback/XML/RAI.pm similarity index 100% rename from lib/site/XML/RAI.pm rename to lib/fallback/XML/RAI.pm diff --git a/lib/site/XML/RAI/Channel.pm b/lib/fallback/XML/RAI/Channel.pm similarity index 100% rename from lib/site/XML/RAI/Channel.pm rename to lib/fallback/XML/RAI/Channel.pm diff --git a/lib/site/XML/RAI/Enclosure.pm b/lib/fallback/XML/RAI/Enclosure.pm similarity index 100% rename from lib/site/XML/RAI/Enclosure.pm rename to lib/fallback/XML/RAI/Enclosure.pm diff --git a/lib/site/XML/RAI/Image.pm b/lib/fallback/XML/RAI/Image.pm similarity index 100% rename from lib/site/XML/RAI/Image.pm rename to lib/fallback/XML/RAI/Image.pm diff --git a/lib/site/XML/RAI/Item.pm b/lib/fallback/XML/RAI/Item.pm similarity index 100% rename from lib/site/XML/RAI/Item.pm rename to lib/fallback/XML/RAI/Item.pm diff --git a/lib/site/XML/RAI/Object.pm b/lib/fallback/XML/RAI/Object.pm similarity index 100% rename from lib/site/XML/RAI/Object.pm rename to lib/fallback/XML/RAI/Object.pm diff --git a/lib/site/XML/RSS.pm b/lib/fallback/XML/RSS.pm similarity index 100% rename from lib/site/XML/RSS.pm rename to lib/fallback/XML/RSS.pm diff --git a/lib/site/XML/RSS/Parser.pm b/lib/fallback/XML/RSS/Parser.pm similarity index 100% rename from lib/site/XML/RSS/Parser.pm rename to lib/fallback/XML/RSS/Parser.pm diff --git a/lib/site/XML/RSS/Parser/Characters.pm b/lib/fallback/XML/RSS/Parser/Characters.pm similarity index 100% rename from lib/site/XML/RSS/Parser/Characters.pm rename to lib/fallback/XML/RSS/Parser/Characters.pm diff --git a/lib/site/XML/RSS/Parser/Element.pm b/lib/fallback/XML/RSS/Parser/Element.pm similarity index 100% rename from lib/site/XML/RSS/Parser/Element.pm rename to lib/fallback/XML/RSS/Parser/Element.pm diff --git a/lib/site/XML/RSS/Parser/Feed.pm b/lib/fallback/XML/RSS/Parser/Feed.pm similarity index 100% rename from lib/site/XML/RSS/Parser/Feed.pm rename to lib/fallback/XML/RSS/Parser/Feed.pm diff --git a/lib/site/XML/RSS/Parser/Util.pm b/lib/fallback/XML/RSS/Parser/Util.pm similarity index 100% rename from lib/site/XML/RSS/Parser/Util.pm rename to lib/fallback/XML/RSS/Parser/Util.pm diff --git a/lib/site/XML/RegExp.pm b/lib/fallback/XML/RegExp.pm similarity index 100% rename from lib/site/XML/RegExp.pm rename to lib/fallback/XML/RegExp.pm diff --git a/lib/site/XML/SAX.pm b/lib/fallback/XML/SAX.pm similarity index 100% rename from lib/site/XML/SAX.pm rename to lib/fallback/XML/SAX.pm diff --git a/lib/site/XML/SAX/Base.pm b/lib/fallback/XML/SAX/Base.pm similarity index 100% rename from lib/site/XML/SAX/Base.pm rename to lib/fallback/XML/SAX/Base.pm diff --git a/lib/site/XML/SAX/DocumentLocator.pm b/lib/fallback/XML/SAX/DocumentLocator.pm similarity index 100% rename from lib/site/XML/SAX/DocumentLocator.pm rename to lib/fallback/XML/SAX/DocumentLocator.pm diff --git a/lib/site/XML/SAX/Exception.pm b/lib/fallback/XML/SAX/Exception.pm similarity index 100% rename from lib/site/XML/SAX/Exception.pm rename to lib/fallback/XML/SAX/Exception.pm diff --git a/lib/site/XML/SAX/Intro.pod b/lib/fallback/XML/SAX/Intro.pod similarity index 100% rename from lib/site/XML/SAX/Intro.pod rename to lib/fallback/XML/SAX/Intro.pod diff --git a/lib/site/XML/SAX/ParserDetails.ini b/lib/fallback/XML/SAX/ParserDetails.ini similarity index 100% rename from lib/site/XML/SAX/ParserDetails.ini rename to lib/fallback/XML/SAX/ParserDetails.ini diff --git a/lib/site/XML/SAX/ParserFactory.pm b/lib/fallback/XML/SAX/ParserFactory.pm similarity index 100% rename from lib/site/XML/SAX/ParserFactory.pm rename to lib/fallback/XML/SAX/ParserFactory.pm diff --git a/lib/site/XML/SAX/PurePerl.pm b/lib/fallback/XML/SAX/PurePerl.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl.pm rename to lib/fallback/XML/SAX/PurePerl.pm diff --git a/lib/site/XML/SAX/PurePerl/DTDDecls.pm b/lib/fallback/XML/SAX/PurePerl/DTDDecls.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/DTDDecls.pm rename to lib/fallback/XML/SAX/PurePerl/DTDDecls.pm diff --git a/lib/site/XML/SAX/PurePerl/DebugHandler.pm b/lib/fallback/XML/SAX/PurePerl/DebugHandler.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/DebugHandler.pm rename to lib/fallback/XML/SAX/PurePerl/DebugHandler.pm diff --git a/lib/site/XML/SAX/PurePerl/DocType.pm b/lib/fallback/XML/SAX/PurePerl/DocType.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/DocType.pm rename to lib/fallback/XML/SAX/PurePerl/DocType.pm diff --git a/lib/site/XML/SAX/PurePerl/EncodingDetect.pm b/lib/fallback/XML/SAX/PurePerl/EncodingDetect.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/EncodingDetect.pm rename to lib/fallback/XML/SAX/PurePerl/EncodingDetect.pm diff --git a/lib/site/XML/SAX/PurePerl/Exception.pm b/lib/fallback/XML/SAX/PurePerl/Exception.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Exception.pm rename to lib/fallback/XML/SAX/PurePerl/Exception.pm diff --git a/lib/site/XML/SAX/PurePerl/NoUnicodeExt.pm b/lib/fallback/XML/SAX/PurePerl/NoUnicodeExt.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/NoUnicodeExt.pm rename to lib/fallback/XML/SAX/PurePerl/NoUnicodeExt.pm diff --git a/lib/site/XML/SAX/PurePerl/Productions.pm b/lib/fallback/XML/SAX/PurePerl/Productions.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Productions.pm rename to lib/fallback/XML/SAX/PurePerl/Productions.pm diff --git a/lib/site/XML/SAX/PurePerl/Reader.pm b/lib/fallback/XML/SAX/PurePerl/Reader.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Reader.pm rename to lib/fallback/XML/SAX/PurePerl/Reader.pm diff --git a/lib/site/XML/SAX/PurePerl/Reader/NoUnicodeExt.pm b/lib/fallback/XML/SAX/PurePerl/Reader/NoUnicodeExt.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Reader/NoUnicodeExt.pm rename to lib/fallback/XML/SAX/PurePerl/Reader/NoUnicodeExt.pm diff --git a/lib/site/XML/SAX/PurePerl/Reader/Stream.pm b/lib/fallback/XML/SAX/PurePerl/Reader/Stream.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Reader/Stream.pm rename to lib/fallback/XML/SAX/PurePerl/Reader/Stream.pm diff --git a/lib/site/XML/SAX/PurePerl/Reader/String.pm b/lib/fallback/XML/SAX/PurePerl/Reader/String.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Reader/String.pm rename to lib/fallback/XML/SAX/PurePerl/Reader/String.pm diff --git a/lib/site/XML/SAX/PurePerl/Reader/URI.pm b/lib/fallback/XML/SAX/PurePerl/Reader/URI.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Reader/URI.pm rename to lib/fallback/XML/SAX/PurePerl/Reader/URI.pm diff --git a/lib/site/XML/SAX/PurePerl/Reader/UnicodeExt.pm b/lib/fallback/XML/SAX/PurePerl/Reader/UnicodeExt.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/Reader/UnicodeExt.pm rename to lib/fallback/XML/SAX/PurePerl/Reader/UnicodeExt.pm diff --git a/lib/site/XML/SAX/PurePerl/UnicodeExt.pm b/lib/fallback/XML/SAX/PurePerl/UnicodeExt.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/UnicodeExt.pm rename to lib/fallback/XML/SAX/PurePerl/UnicodeExt.pm diff --git a/lib/site/XML/SAX/PurePerl/XMLDecl.pm b/lib/fallback/XML/SAX/PurePerl/XMLDecl.pm similarity index 100% rename from lib/site/XML/SAX/PurePerl/XMLDecl.pm rename to lib/fallback/XML/SAX/PurePerl/XMLDecl.pm diff --git a/lib/site/XML/SAX/placeholder.pl b/lib/fallback/XML/SAX/placeholder.pl similarity index 100% rename from lib/site/XML/SAX/placeholder.pl rename to lib/fallback/XML/SAX/placeholder.pl diff --git a/lib/site/XML/SAX2Perl.pm b/lib/fallback/XML/SAX2Perl.pm similarity index 100% rename from lib/site/XML/SAX2Perl.pm rename to lib/fallback/XML/SAX2Perl.pm diff --git a/lib/site/XML/Twig.pm b/lib/fallback/XML/Twig.pm similarity index 100% rename from lib/site/XML/Twig.pm rename to lib/fallback/XML/Twig.pm diff --git a/lib/site/XML/XPath.pm b/lib/fallback/XML/XPath.pm similarity index 100% rename from lib/site/XML/XPath.pm rename to lib/fallback/XML/XPath.pm diff --git a/lib/site/XML/XPath/Boolean.pm b/lib/fallback/XML/XPath/Boolean.pm similarity index 100% rename from lib/site/XML/XPath/Boolean.pm rename to lib/fallback/XML/XPath/Boolean.pm diff --git a/lib/site/XML/XPath/Builder.pm b/lib/fallback/XML/XPath/Builder.pm similarity index 100% rename from lib/site/XML/XPath/Builder.pm rename to lib/fallback/XML/XPath/Builder.pm diff --git a/lib/site/XML/XPath/Expr.pm b/lib/fallback/XML/XPath/Expr.pm similarity index 100% rename from lib/site/XML/XPath/Expr.pm rename to lib/fallback/XML/XPath/Expr.pm diff --git a/lib/site/XML/XPath/Function.pm b/lib/fallback/XML/XPath/Function.pm similarity index 100% rename from lib/site/XML/XPath/Function.pm rename to lib/fallback/XML/XPath/Function.pm diff --git a/lib/site/XML/XPath/Literal.pm b/lib/fallback/XML/XPath/Literal.pm similarity index 100% rename from lib/site/XML/XPath/Literal.pm rename to lib/fallback/XML/XPath/Literal.pm diff --git a/lib/site/XML/XPath/LocationPath.pm b/lib/fallback/XML/XPath/LocationPath.pm similarity index 100% rename from lib/site/XML/XPath/LocationPath.pm rename to lib/fallback/XML/XPath/LocationPath.pm diff --git a/lib/site/XML/XPath/Node.pm b/lib/fallback/XML/XPath/Node.pm similarity index 100% rename from lib/site/XML/XPath/Node.pm rename to lib/fallback/XML/XPath/Node.pm diff --git a/lib/site/XML/XPath/Node/Attribute.pm b/lib/fallback/XML/XPath/Node/Attribute.pm similarity index 100% rename from lib/site/XML/XPath/Node/Attribute.pm rename to lib/fallback/XML/XPath/Node/Attribute.pm diff --git a/lib/site/XML/XPath/Node/Comment.pm b/lib/fallback/XML/XPath/Node/Comment.pm similarity index 100% rename from lib/site/XML/XPath/Node/Comment.pm rename to lib/fallback/XML/XPath/Node/Comment.pm diff --git a/lib/site/XML/XPath/Node/Element.pm b/lib/fallback/XML/XPath/Node/Element.pm similarity index 100% rename from lib/site/XML/XPath/Node/Element.pm rename to lib/fallback/XML/XPath/Node/Element.pm diff --git a/lib/site/XML/XPath/Node/Namespace.pm b/lib/fallback/XML/XPath/Node/Namespace.pm similarity index 100% rename from lib/site/XML/XPath/Node/Namespace.pm rename to lib/fallback/XML/XPath/Node/Namespace.pm diff --git a/lib/site/XML/XPath/Node/PI.pm b/lib/fallback/XML/XPath/Node/PI.pm similarity index 100% rename from lib/site/XML/XPath/Node/PI.pm rename to lib/fallback/XML/XPath/Node/PI.pm diff --git a/lib/site/XML/XPath/Node/Text.pm b/lib/fallback/XML/XPath/Node/Text.pm similarity index 100% rename from lib/site/XML/XPath/Node/Text.pm rename to lib/fallback/XML/XPath/Node/Text.pm diff --git a/lib/site/XML/XPath/NodeSet.pm b/lib/fallback/XML/XPath/NodeSet.pm similarity index 100% rename from lib/site/XML/XPath/NodeSet.pm rename to lib/fallback/XML/XPath/NodeSet.pm diff --git a/lib/site/XML/XPath/Number.pm b/lib/fallback/XML/XPath/Number.pm similarity index 100% rename from lib/site/XML/XPath/Number.pm rename to lib/fallback/XML/XPath/Number.pm diff --git a/lib/site/XML/XPath/Parser.pm b/lib/fallback/XML/XPath/Parser.pm similarity index 100% rename from lib/site/XML/XPath/Parser.pm rename to lib/fallback/XML/XPath/Parser.pm diff --git a/lib/site/XML/XPath/PerlSAX.pm b/lib/fallback/XML/XPath/PerlSAX.pm similarity index 100% rename from lib/site/XML/XPath/PerlSAX.pm rename to lib/fallback/XML/XPath/PerlSAX.pm diff --git a/lib/site/XML/XPath/Root.pm b/lib/fallback/XML/XPath/Root.pm similarity index 100% rename from lib/site/XML/XPath/Root.pm rename to lib/fallback/XML/XPath/Root.pm diff --git a/lib/site/XML/XPath/Step.pm b/lib/fallback/XML/XPath/Step.pm similarity index 100% rename from lib/site/XML/XPath/Step.pm rename to lib/fallback/XML/XPath/Step.pm diff --git a/lib/site/XML/XPath/Variable.pm b/lib/fallback/XML/XPath/Variable.pm similarity index 100% rename from lib/site/XML/XPath/Variable.pm rename to lib/fallback/XML/XPath/Variable.pm diff --git a/lib/site/XML/XPath/XMLParser.pm b/lib/fallback/XML/XPath/XMLParser.pm similarity index 100% rename from lib/site/XML/XPath/XMLParser.pm rename to lib/fallback/XML/XPath/XMLParser.pm diff --git a/lib/site/auto/LWP/UserAgent/_need_proxy.al b/lib/fallback/auto/LWP/UserAgent/_need_proxy.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/_need_proxy.al rename to lib/fallback/auto/LWP/UserAgent/_need_proxy.al diff --git a/lib/site/auto/LWP/UserAgent/autosplit.ix b/lib/fallback/auto/LWP/UserAgent/autosplit.ix similarity index 100% rename from lib/site/auto/LWP/UserAgent/autosplit.ix rename to lib/fallback/auto/LWP/UserAgent/autosplit.ix diff --git a/lib/site/auto/LWP/UserAgent/clone.al b/lib/fallback/auto/LWP/UserAgent/clone.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/clone.al rename to lib/fallback/auto/LWP/UserAgent/clone.al diff --git a/lib/site/auto/LWP/UserAgent/env_proxy.al b/lib/fallback/auto/LWP/UserAgent/env_proxy.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/env_proxy.al rename to lib/fallback/auto/LWP/UserAgent/env_proxy.al diff --git a/lib/site/auto/LWP/UserAgent/is_protocol_supported.al b/lib/fallback/auto/LWP/UserAgent/is_protocol_supported.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/is_protocol_supported.al rename to lib/fallback/auto/LWP/UserAgent/is_protocol_supported.al diff --git a/lib/site/auto/LWP/UserAgent/mirror.al b/lib/fallback/auto/LWP/UserAgent/mirror.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/mirror.al rename to lib/fallback/auto/LWP/UserAgent/mirror.al diff --git a/lib/site/auto/LWP/UserAgent/no_proxy.al b/lib/fallback/auto/LWP/UserAgent/no_proxy.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/no_proxy.al rename to lib/fallback/auto/LWP/UserAgent/no_proxy.al diff --git a/lib/site/auto/LWP/UserAgent/proxy.al b/lib/fallback/auto/LWP/UserAgent/proxy.al similarity index 100% rename from lib/site/auto/LWP/UserAgent/proxy.al rename to lib/fallback/auto/LWP/UserAgent/proxy.al diff --git a/lib/site/auto/Net-DNS/.packlist b/lib/fallback/auto/Net-DNS/.packlist similarity index 100% rename from lib/site/auto/Net-DNS/.packlist rename to lib/fallback/auto/Net-DNS/.packlist diff --git a/lib/site/auto/URI/URL/_generic/_netloc_elem.al b/lib/fallback/auto/URI/URL/_generic/_netloc_elem.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/_netloc_elem.al rename to lib/fallback/auto/URI/URL/_generic/_netloc_elem.al diff --git a/lib/site/auto/URI/URL/_generic/abs.al b/lib/fallback/auto/URI/URL/_generic/abs.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/abs.al rename to lib/fallback/auto/URI/URL/_generic/abs.al diff --git a/lib/site/auto/URI/URL/_generic/autosplit.ix b/lib/fallback/auto/URI/URL/_generic/autosplit.ix similarity index 100% rename from lib/site/auto/URI/URL/_generic/autosplit.ix rename to lib/fallback/auto/URI/URL/_generic/autosplit.ix diff --git a/lib/site/auto/URI/URL/_generic/crack.al b/lib/fallback/auto/URI/URL/_generic/crack.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/crack.al rename to lib/fallback/auto/URI/URL/_generic/crack.al diff --git a/lib/site/auto/URI/URL/_generic/eparams.al b/lib/fallback/auto/URI/URL/_generic/eparams.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/eparams.al rename to lib/fallback/auto/URI/URL/_generic/eparams.al diff --git a/lib/site/auto/URI/URL/_generic/epath.al b/lib/fallback/auto/URI/URL/_generic/epath.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/epath.al rename to lib/fallback/auto/URI/URL/_generic/epath.al diff --git a/lib/site/auto/URI/URL/_generic/eq.al b/lib/fallback/auto/URI/URL/_generic/eq.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/eq.al rename to lib/fallback/auto/URI/URL/_generic/eq.al diff --git a/lib/site/auto/URI/URL/_generic/equery.al b/lib/fallback/auto/URI/URL/_generic/equery.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/equery.al rename to lib/fallback/auto/URI/URL/_generic/equery.al diff --git a/lib/site/auto/URI/URL/_generic/frag.al b/lib/fallback/auto/URI/URL/_generic/frag.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/frag.al rename to lib/fallback/auto/URI/URL/_generic/frag.al diff --git a/lib/site/auto/URI/URL/_generic/host.al b/lib/fallback/auto/URI/URL/_generic/host.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/host.al rename to lib/fallback/auto/URI/URL/_generic/host.al diff --git a/lib/site/auto/URI/URL/_generic/params.al b/lib/fallback/auto/URI/URL/_generic/params.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/params.al rename to lib/fallback/auto/URI/URL/_generic/params.al diff --git a/lib/site/auto/URI/URL/_generic/password.al b/lib/fallback/auto/URI/URL/_generic/password.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/password.al rename to lib/fallback/auto/URI/URL/_generic/password.al diff --git a/lib/site/auto/URI/URL/_generic/path.al b/lib/fallback/auto/URI/URL/_generic/path.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/path.al rename to lib/fallback/auto/URI/URL/_generic/path.al diff --git a/lib/site/auto/URI/URL/_generic/path_components.al b/lib/fallback/auto/URI/URL/_generic/path_components.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/path_components.al rename to lib/fallback/auto/URI/URL/_generic/path_components.al diff --git a/lib/site/auto/URI/URL/_generic/port.al b/lib/fallback/auto/URI/URL/_generic/port.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/port.al rename to lib/fallback/auto/URI/URL/_generic/port.al diff --git a/lib/site/auto/URI/URL/_generic/query.al b/lib/fallback/auto/URI/URL/_generic/query.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/query.al rename to lib/fallback/auto/URI/URL/_generic/query.al diff --git a/lib/site/auto/URI/URL/_generic/rel.al b/lib/fallback/auto/URI/URL/_generic/rel.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/rel.al rename to lib/fallback/auto/URI/URL/_generic/rel.al diff --git a/lib/site/auto/URI/URL/_generic/user.al b/lib/fallback/auto/URI/URL/_generic/user.al similarity index 100% rename from lib/site/auto/URI/URL/_generic/user.al rename to lib/fallback/auto/URI/URL/_generic/user.al diff --git a/lib/site/auto/URI/URL/abs.al b/lib/fallback/auto/URI/URL/abs.al similarity index 100% rename from lib/site/auto/URI/URL/abs.al rename to lib/fallback/auto/URI/URL/abs.al diff --git a/lib/site/auto/URI/URL/as_string.al b/lib/fallback/auto/URI/URL/as_string.al similarity index 100% rename from lib/site/auto/URI/URL/as_string.al rename to lib/fallback/auto/URI/URL/as_string.al diff --git a/lib/site/auto/URI/URL/autosplit.ix b/lib/fallback/auto/URI/URL/autosplit.ix similarity index 100% rename from lib/site/auto/URI/URL/autosplit.ix rename to lib/fallback/auto/URI/URL/autosplit.ix diff --git a/lib/site/auto/URI/URL/bad_method.al b/lib/fallback/auto/URI/URL/bad_method.al similarity index 100% rename from lib/site/auto/URI/URL/bad_method.al rename to lib/fallback/auto/URI/URL/bad_method.al diff --git a/lib/site/auto/URI/URL/base.al b/lib/fallback/auto/URI/URL/base.al similarity index 100% rename from lib/site/auto/URI/URL/base.al rename to lib/fallback/auto/URI/URL/base.al diff --git a/lib/site/auto/URI/URL/crack.al b/lib/fallback/auto/URI/URL/crack.al similarity index 100% rename from lib/site/auto/URI/URL/crack.al rename to lib/fallback/auto/URI/URL/crack.al diff --git a/lib/site/auto/URI/URL/eq.al b/lib/fallback/auto/URI/URL/eq.al similarity index 100% rename from lib/site/auto/URI/URL/eq.al rename to lib/fallback/auto/URI/URL/eq.al diff --git a/lib/site/auto/URI/URL/file/autosplit.ix b/lib/fallback/auto/URI/URL/file/autosplit.ix similarity index 100% rename from lib/site/auto/URI/URL/file/autosplit.ix rename to lib/fallback/auto/URI/URL/file/autosplit.ix diff --git a/lib/site/auto/URI/URL/file/dos_path.al b/lib/fallback/auto/URI/URL/file/dos_path.al similarity index 100% rename from lib/site/auto/URI/URL/file/dos_path.al rename to lib/fallback/auto/URI/URL/file/dos_path.al diff --git a/lib/site/auto/URI/URL/file/mac_path.al b/lib/fallback/auto/URI/URL/file/mac_path.al similarity index 100% rename from lib/site/auto/URI/URL/file/mac_path.al rename to lib/fallback/auto/URI/URL/file/mac_path.al diff --git a/lib/site/auto/URI/URL/file/newlocal.al b/lib/fallback/auto/URI/URL/file/newlocal.al similarity index 100% rename from lib/site/auto/URI/URL/file/newlocal.al rename to lib/fallback/auto/URI/URL/file/newlocal.al diff --git a/lib/site/auto/URI/URL/file/unix_path.al b/lib/fallback/auto/URI/URL/file/unix_path.al similarity index 100% rename from lib/site/auto/URI/URL/file/unix_path.al rename to lib/fallback/auto/URI/URL/file/unix_path.al diff --git a/lib/site/auto/URI/URL/file/vms_path.al b/lib/fallback/auto/URI/URL/file/vms_path.al similarity index 100% rename from lib/site/auto/URI/URL/file/vms_path.al rename to lib/fallback/auto/URI/URL/file/vms_path.al diff --git a/lib/site/auto/URI/URL/http/autosplit.ix b/lib/fallback/auto/URI/URL/http/autosplit.ix similarity index 100% rename from lib/site/auto/URI/URL/http/autosplit.ix rename to lib/fallback/auto/URI/URL/http/autosplit.ix diff --git a/lib/site/auto/URI/URL/http/keywords.al b/lib/fallback/auto/URI/URL/http/keywords.al similarity index 100% rename from lib/site/auto/URI/URL/http/keywords.al rename to lib/fallback/auto/URI/URL/http/keywords.al diff --git a/lib/site/auto/URI/URL/http/query_form.al b/lib/fallback/auto/URI/URL/http/query_form.al similarity index 100% rename from lib/site/auto/URI/URL/http/query_form.al rename to lib/fallback/auto/URI/URL/http/query_form.al diff --git a/lib/site/auto/URI/URL/newlocal.al b/lib/fallback/auto/URI/URL/newlocal.al similarity index 100% rename from lib/site/auto/URI/URL/newlocal.al rename to lib/fallback/auto/URI/URL/newlocal.al diff --git a/lib/site/auto/URI/URL/print_on.al b/lib/fallback/auto/URI/URL/print_on.al similarity index 100% rename from lib/site/auto/URI/URL/print_on.al rename to lib/fallback/auto/URI/URL/print_on.al diff --git a/lib/site/auto/URI/URL/rel.al b/lib/fallback/auto/URI/URL/rel.al similarity index 100% rename from lib/site/auto/URI/URL/rel.al rename to lib/fallback/auto/URI/URL/rel.al diff --git a/lib/site/auto/URI/URL/scheme.al b/lib/fallback/auto/URI/URL/scheme.al similarity index 100% rename from lib/site/auto/URI/URL/scheme.al rename to lib/fallback/auto/URI/URL/scheme.al diff --git a/lib/site/auto/URI/URL/strict.al b/lib/fallback/auto/URI/URL/strict.al similarity index 100% rename from lib/site/auto/URI/URL/strict.al rename to lib/fallback/auto/URI/URL/strict.al diff --git a/lib/site/auto/Win32/MemMap/memmap.dll b/lib/fallback/auto/Win32/MemMap/memmap.dll similarity index 100% rename from lib/site/auto/Win32/MemMap/memmap.dll rename to lib/fallback/auto/Win32/MemMap/memmap.dll diff --git a/lib/site/auto/http/autosplit.ix b/lib/fallback/auto/http/autosplit.ix similarity index 100% rename from lib/site/auto/http/autosplit.ix rename to lib/fallback/auto/http/autosplit.ix diff --git a/lib/site/auto/http/keywords.al b/lib/fallback/auto/http/keywords.al similarity index 100% rename from lib/site/auto/http/keywords.al rename to lib/fallback/auto/http/keywords.al diff --git a/lib/site/auto/http/query_form.al b/lib/fallback/auto/http/query_form.al similarity index 100% rename from lib/site/auto/http/query_form.al rename to lib/fallback/auto/http/query_form.al diff --git a/lib/site/enum.pm b/lib/fallback/enum.pm similarity index 100% rename from lib/site/enum.pm rename to lib/fallback/enum.pm diff --git a/lib/site/iCal/Parser.pm b/lib/fallback/iCal/Parser.pm similarity index 100% rename from lib/site/iCal/Parser.pm rename to lib/fallback/iCal/Parser.pm From 3a9dbeb5273d503e51ee8773694a77c8112e8986 Mon Sep 17 00:00:00 2001 From: Brian M Date: Wed, 5 Jun 2019 09:02:29 -0700 Subject: [PATCH 74/78] Add lib/fallback to stand-alone programs --- bin/alpha_page | 2 +- bin/get_earthquakes | 2 +- bin/get_email | 2 +- bin/get_tcp | 2 +- bin/get_tv_grid | 2 +- bin/get_tv_grid_ge | 2 +- bin/get_tv_grid_xmltv | 2 +- bin/get_tv_info | 2 +- bin/get_tv_info_ge | 2 +- bin/get_url | 2 +- bin/get_weather | 4 ++-- bin/get_weather_ca | 4 ++-- bin/ical2vsdb | 2 +- bin/ical_load | 2 +- bin/net_ftp | 2 +- bin/outlook_read | 2 +- bin/pocketsphinx | 2 +- bin/print_socket_fork_memmap.pl | 1 + bin/read_email | 2 +- bin/report_weblog | 2 +- bin/send_email | 2 +- bin/set_clock | 2 +- bin/set_password | 2 +- bin/snpp_page | 2 +- bin/sun_time | 2 +- bin/test_x10 | 2 +- bin/update_docs | 2 +- bin/vv_tts.pl | 2 +- 28 files changed, 30 insertions(+), 29 deletions(-) diff --git a/bin/alpha_page b/bin/alpha_page index 62bd83124..2f29b970a 100755 --- a/bin/alpha_page +++ b/bin/alpha_page @@ -31,7 +31,7 @@ BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; $Pgm_Root = "$Pgm_Path/.."; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } use Getopt::Long; diff --git a/bin/get_earthquakes b/bin/get_earthquakes index dc382c264..8902bc1de 100755 --- a/bin/get_earthquakes +++ b/bin/get_earthquakes @@ -55,7 +55,7 @@ eof } BEGIN { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; } # Use BEGIN eval to keep perl2exe happy require 'handy_utilities.pl'; # For read_mh_opts funcion diff --git a/bin/get_email b/bin/get_email index de87dc217..7ad041fa7 100755 --- a/bin/get_email +++ b/bin/get_email @@ -31,7 +31,7 @@ BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; $Pgm_Root = "$Pgm_Path/.."; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } use Getopt::Long; diff --git a/bin/get_tcp b/bin/get_tcp index 22a2d074d..ebb9b32c3 100755 --- a/bin/get_tcp +++ b/bin/get_tcp @@ -11,7 +11,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } my ( %config_parms, %parms ); diff --git a/bin/get_tv_grid b/bin/get_tv_grid index 1bfcdbd0d..f8c4b52ed 100755 --- a/bin/get_tv_grid +++ b/bin/get_tv_grid @@ -45,7 +45,7 @@ BEGIN { ($Version) = q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } require "RedirAgent.pm"; diff --git a/bin/get_tv_grid_ge b/bin/get_tv_grid_ge index 9d26dfa01..68081f307 100755 --- a/bin/get_tv_grid_ge +++ b/bin/get_tv_grid_ge @@ -40,7 +40,7 @@ BEGIN { ($Version) = q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } my %parms; use Getopt::Long; diff --git a/bin/get_tv_grid_xmltv b/bin/get_tv_grid_xmltv index fb212832d..c7fcb73a4 100755 --- a/bin/get_tv_grid_xmltv +++ b/bin/get_tv_grid_xmltv @@ -57,7 +57,7 @@ BEGIN { ($Version) = q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } use XMLTV::Version '$Id$ '; diff --git a/bin/get_tv_info b/bin/get_tv_info index b47a1122b..9fc16d923 100755 --- a/bin/get_tv_info +++ b/bin/get_tv_info @@ -26,7 +26,7 @@ BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } my %parms; diff --git a/bin/get_tv_info_ge b/bin/get_tv_info_ge index 17d05da58..1a82a7bd0 100755 --- a/bin/get_tv_info_ge +++ b/bin/get_tv_info_ge @@ -23,7 +23,7 @@ BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } my %parms; diff --git a/bin/get_url b/bin/get_url index 93643f7c7..a63617fa5 100755 --- a/bin/get_url +++ b/bin/get_url @@ -9,7 +9,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback' "; # So perl2exe works } my ( %config_parms, %parms ); diff --git a/bin/get_weather b/bin/get_weather index 2ad33a2b9..c9d7643a0 100755 --- a/bin/get_weather +++ b/bin/get_weather @@ -81,9 +81,9 @@ $opt_v++ if $parms{v}; # Geo::Weather looks at this 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 +#use my_lib "$Pgm_Path/../lib/site", "$Pgm_Path/../lib/fallback"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination BEGIN { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; } # Use BEGIN eval to keep perl2exe happy require 'handy_utilities.pl'; # For read_mh_opts funcion diff --git a/bin/get_weather_ca b/bin/get_weather_ca index df4e53777..ce512537a 100755 --- a/bin/get_weather_ca +++ b/bin/get_weather_ca @@ -75,9 +75,9 @@ 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 +#use my_lib "$Pgm_Path/../lib/site", "$Pgm_Path/../lib/fallback"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination BEGIN { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; } # Use BEGIN eval to keep perl2exe happy require 'handy_utilities.pl'; # For read_mh_opts funcion diff --git a/bin/ical2vsdb b/bin/ical2vsdb index c94674a4f..c8fdcf546 100755 --- a/bin/ical2vsdb +++ b/bin/ical2vsdb @@ -30,7 +30,7 @@ use strict; ## Sometimes icals are unchanged, but the order is different when downloading. Add option to sort the ical to ensure ## Data hasn't changed even if the order did. Added option sort_before_md5 = 1 to enable globally or to the specific ical -use lib '../lib', '../lib/site'; +use lib '../lib', '../lib/site', '../lib/fallback'; use iCal::Parser; use DateTime; use Digest::MD5 qw(md5 md5_hex); diff --git a/bin/ical_load b/bin/ical_load index 4d9ee1c6d..eada4d38b 100755 --- a/bin/ical_load +++ b/bin/ical_load @@ -27,7 +27,7 @@ BEGIN { ($Version) = q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } use Getopt::Long; diff --git a/bin/net_ftp b/bin/net_ftp index 102d2755f..e62ab361b 100755 --- a/bin/net_ftp +++ b/bin/net_ftp @@ -7,7 +7,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } my %parms; diff --git a/bin/outlook_read b/bin/outlook_read index 4e338acef..c39e4b379 100755 --- a/bin/outlook_read +++ b/bin/outlook_read @@ -40,7 +40,7 @@ BEGIN { ($Version) = q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } use Getopt::Long; diff --git a/bin/pocketsphinx b/bin/pocketsphinx index a7d8794fb..24e25d670 100755 --- a/bin/pocketsphinx +++ b/bin/pocketsphinx @@ -39,7 +39,7 @@ BEGIN { ($Version) = q$Revision: 1084 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } use Getopt::Long; diff --git a/bin/print_socket_fork_memmap.pl b/bin/print_socket_fork_memmap.pl index 0f0b70608..5e0f3ea48 100755 --- a/bin/print_socket_fork_memmap.pl +++ b/bin/print_socket_fork_memmap.pl @@ -53,6 +53,7 @@ BEGIN } push @INC, './../lib/site'; push @INC, './../lib'; + push @INC, './../lib/fallback'; } require "$Pgm_Path/../lib/handy_utilities.pl"; diff --git a/bin/read_email b/bin/read_email index 677e5a769..4d0e048e9 100755 --- a/bin/read_email +++ b/bin/read_email @@ -7,7 +7,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } my %parms; diff --git a/bin/report_weblog b/bin/report_weblog index c25c4196c..f972c4240 100755 --- a/bin/report_weblog +++ b/bin/report_weblog @@ -32,7 +32,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; } my %parms; diff --git a/bin/send_email b/bin/send_email index 897257bf2..d6edcc3c8 100755 --- a/bin/send_email +++ b/bin/send_email @@ -7,7 +7,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } my %parms; diff --git a/bin/set_clock b/bin/set_clock index a5e2050db..ee1de2fd2 100755 --- a/bin/set_clock +++ b/bin/set_clock @@ -89,7 +89,7 @@ print "Requesting the time from $parms{server} using $parms{method} method\n"; #use my_lib "$Pgm_Path/../lib"; # See note in lib/mh_perl2exe.pl for lib -> my_lib explaination #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'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; } # Use BEGIN eval to keep perl2exe happy my $time_record; diff --git a/bin/set_password b/bin/set_password index c0af20e26..1c35c78a6 100755 --- a/bin/set_password +++ b/bin/set_password @@ -90,7 +90,7 @@ else { my ( %config_parms, %passwords ); sub setup { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works require 'handy_utilities.pl'; # For read_mh_opts funcion &main::read_mh_opts( \%config_parms, $Pgm_Path ); diff --git a/bin/snpp_page b/bin/snpp_page index cdb494257..950a222a8 100755 --- a/bin/snpp_page +++ b/bin/snpp_page @@ -43,7 +43,7 @@ BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; $Pgm_Root = "$Pgm_Path/.."; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; } #-------------------------------------------------------------------------------- diff --git a/bin/sun_time b/bin/sun_time index cc4c71fbe..345b66d76 100755 --- a/bin/sun_time +++ b/bin/sun_time @@ -32,7 +32,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } use Getopt::Long; diff --git a/bin/test_x10 b/bin/test_x10 index 82dac39b5..3dd579b14 100755 --- a/bin/test_x10 +++ b/bin/test_x10 @@ -11,7 +11,7 @@ # perl test_x10 CM17 COM1 B 2 use strict; -use lib '../lib', '../lib/site'; +use lib '../lib', '../lib/site', '../lib/fallback'; my ( $device, $port, $house, $unit, $interface, %config_parms ); diff --git a/bin/update_docs b/bin/update_docs index 80fe0a3de..39e827949 100755 --- a/bin/update_docs +++ b/bin/update_docs @@ -53,7 +53,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # So perl2exe works + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # So perl2exe works } use strict; diff --git a/bin/vv_tts.pl b/bin/vv_tts.pl index 5983807c5..896c02e68 100755 --- a/bin/vv_tts.pl +++ b/bin/vv_tts.pl @@ -24,7 +24,7 @@ BEGIN ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.+)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; $Pgm_Root = "$Pgm_Path/.."; - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site', '$Pgm_Path/../lib/fallback'"; # Use BEGIN eval to keep perl2exe happy } use Getopt::Long; From 60c6ee832df3e683120cb8ec43a8bb2ae833ea79 Mon Sep 17 00:00:00 2001 From: Brian M Date: Mon, 10 Jun 2019 20:49:30 -0700 Subject: [PATCH 75/78] Move lib/fallback/Geo back to lib/site/Geo, as these are modified versions of CPAN, not just old versions. --- lib/{fallback => site}/Geo/METAR.pm | 0 lib/{fallback => site}/Geo/Weather.pm | 0 lib/{fallback => site}/Geo/WeatherNOAA.pm | 0 lib/{fallback => site}/Geo/WeatherNOAA.pm.old | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename lib/{fallback => site}/Geo/METAR.pm (100%) rename lib/{fallback => site}/Geo/Weather.pm (100%) rename lib/{fallback => site}/Geo/WeatherNOAA.pm (100%) rename lib/{fallback => site}/Geo/WeatherNOAA.pm.old (100%) diff --git a/lib/fallback/Geo/METAR.pm b/lib/site/Geo/METAR.pm similarity index 100% rename from lib/fallback/Geo/METAR.pm rename to lib/site/Geo/METAR.pm diff --git a/lib/fallback/Geo/Weather.pm b/lib/site/Geo/Weather.pm similarity index 100% rename from lib/fallback/Geo/Weather.pm rename to lib/site/Geo/Weather.pm diff --git a/lib/fallback/Geo/WeatherNOAA.pm b/lib/site/Geo/WeatherNOAA.pm similarity index 100% rename from lib/fallback/Geo/WeatherNOAA.pm rename to lib/site/Geo/WeatherNOAA.pm diff --git a/lib/fallback/Geo/WeatherNOAA.pm.old b/lib/site/Geo/WeatherNOAA.pm.old similarity index 100% rename from lib/fallback/Geo/WeatherNOAA.pm.old rename to lib/site/Geo/WeatherNOAA.pm.old From 7e7b7e828f999666f545e693f019ea2075f4b5ba Mon Sep 17 00:00:00 2001 From: waynieack Date: Thu, 14 Nov 2019 21:28:47 -0600 Subject: [PATCH 76/78] Long discovery string is only returned with gzip compression now. The newer Echo version uses compression, the older does not. Without compression, only 20 devices can be returned so the short string is used --- lib/AlexaBridge.pm | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 785613682..1b0b15ae8 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -861,10 +861,15 @@ sub process_http { next unless $name; my $state = &get_set_state( $self, $AlexaObjects, $uuid, 'get' ); $statep1 = qq[{"]; - $statep2=qq[":{"state":{$state,"alert": "select","mode": "homeautomation","reachable": true},"swupdate": {"state": "readytoinstall","lastinstall": null},"type": "Dimmable light","name": "]; - $statep3=qq[","modelid": "LWB014","manufacturername": "Philips","productname": "Hue white lamp","capabilities": {"certified": true,"control": {"mindimlevel": 5000,"maxlumen": 840},"streaming": {"renderer": false,"proxy": false}},"config": {"archetype": "classicbulb","function": "functional","direction": "omnidirectional"},"uniqueid": "00:17:88:01:04:00:3d:96-0b","swversion": "1.23.0_r20156","swconfigid": "321D79EA","productid": "Philips-LWB014-1-A19DLv4"}]; - #$statep2 = qq[":{"state":{$state,"reachable":true},"type":"Extended color light","name":"]; - #$statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; + if ( $Http{'Accept-Encoding'} =~ m/gzip/ ) { + &main::print_log("[Alexa] Debug: Returning long format. Accept-Encoding=" . $Http{'Accept-Encoding'}) if $main::Debug{'alexa'}; + $statep2=qq[":{"state":{$state,"alert": "select","mode": "homeautomation","reachable": true},"swupdate": {"state": "readytoinstall","lastinstall": null},"type": "Dimmable light","name": "]; + $statep3=qq[","modelid": "LWB014","manufacturername": "Philips","productname": "Hue white lamp","capabilities": {"certified": true,"control": {"mindimlevel": 5000,"maxlumen": 840},"streaming": {"renderer": false,"proxy": false}},"config": {"archetype": "classicbulb","function": "functional","direction": "omnidirectional"},"uniqueid": "00:17:88:01:04:00:3d:96-0b","swversion": "1.23.0_r20156","swconfigid": "321D79EA","productid": "Philips-LWB014-1-A19DLv4"}]; + } else { + &main::print_log("[Alexa] Debug: Returning short format. Accept-Encoding=" . $Http{'Accept-Encoding'}) if $main::Debug{'alexa'}; + $statep2 = qq[":{"state":{$state,"reachable":true},"type":"Extended color light","name":"]; + $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; + } $end = qq[}]; $delm = qq[,"]; $name =~ s/_/ /g; From fae76f60f314b44776fdff541f7c13d6da93c607 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 2 Dec 2019 10:18:12 -0600 Subject: [PATCH 77/78] Fixed matching alpha in newer AD2 firmware, older firmware was numeric only. --- lib/AD2.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/AD2.pm b/lib/AD2.pm index 8a439c4fe..0ffc61483 100755 --- a/lib/AD2.pm +++ b/lib/AD2.pm @@ -788,7 +788,7 @@ sub GetStatusType { $message{cmd} = $AdemcoStr; # Panel Message Format - if ($AdemcoStr =~ /(!KPM:)?\[([\d-]*)\],(\d{3}),\[(.*)\],\"(.*)\"/) { + if ($AdemcoStr =~ /(!KPM:)?\[([\dABCDEF\-]*)\],(\d{3}),\[(.*)\],\"(.*)\"/) { $message{keypad} = 1; # Parse The Cmd into Message Parts From a635e121cb8684d607cf81f61e50b858cb4e9b2e Mon Sep 17 00:00:00 2001 From: Marc MERLIN Date: Sun, 3 May 2020 16:50:20 -0700 Subject: [PATCH 78/78] Fixed debugging for CMxx, now process data the first time it's sent. mochad now does deduping for us, which caused the "skip the first send" code to work and never see a resend. This was disabled and the first send does what we need. --- lib/X10_CMxx.pm | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/X10_CMxx.pm b/lib/X10_CMxx.pm index 84d53dfd9..5a4300891 100644 --- a/lib/X10_CMxx.pm +++ b/lib/X10_CMxx.pm @@ -41,6 +41,16 @@ back out to the powerline, use mh/code/common/x10_rf_relay.pl. Also see X10_W800.pm for a similar interface. +This is how decoding works: +CMxx: X10RF data from mochad: 15 1A 80 7F 09 00 +X10_CMXX: reordered data: 01 fe a8 58 +x10_cmxx: this is x10 security data +X10_CMXX: security: device_id = 0xa8, cmd = 0x01 +X10_CMXX: security: class_id = sensor, item_id = a8, state = NormalMax +a8: x10sec_test set NormalMax +CMxx: sending bytes to process, got state NormalMax +CMxx: skipping decoded data received from mochad: 05/03 16:23:29 Rx RFSEC Addr: 15:09:00 Func: Contact_normal_max_DS10A + =cut use strict; @@ -127,7 +137,7 @@ sub check_for_data { foreach my $line ( split( /\n/, $buffer ) ) { if ( not $line =~ /.* Raw data received: / ) { - &::print_log("CMxx: decoded data received from mochad: $line") + &::print_log("CMxx: skipping decoded data received from mochad: $line") if $main::Debug{cmxx}; return; } @@ -149,7 +159,8 @@ sub check_for_data { # Data gets sent multiple times # - Check time # - Process data only on the 2nd occurance, to avoid noise (seems essential) - my $duplicate_threshold = 1; # 2nd occurance; set to 0 to omit duplicate check + #my $duplicate_threshold = 1; # 2nd occurance; set to 0 to omit duplicate check + my $duplicate_threshold = 0; # mochad does deduplication now my $duplicate_count = duplicate_count($data); if ( $duplicate_count == $duplicate_threshold ) { my @bytes; @@ -164,7 +175,10 @@ sub check_for_data { $byteidx++; } + # This calls X10_RF::decode_rf_bytes which in turn + # calls rf_process_security and rf_set_RF_Item my $state = X10_RF::decode_rf_bytes( 'X10_CMxx', @bytes ); + &::print_log("CMxx: sent bytes to process, got state ".$state) if $main::Debug{cmxx}; # If the decode_rf_bytes routine didn't like the data that it got, # we just drop the data (it's been preprocessed by mochad, so we can't hope to fix