diff --git a/.travis.yml b/.travis.yml index 6c16a5b39..4332fbf65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,10 @@ os: dist: trusty perl: + - "5.30" - "5.24" - "5.18" - "5.16" - - "5.08" install: true diff --git a/README.md b/README.md index ab4698402..e1c000854 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ MisterHouse =========== -[![Build Status](https://travis-ci.org/hollie/misterhouse.svg?branch=master)](https://travis-ci.org/hollie/misterhouse) +[![Build Status](https://travis-ci.com/hollie/misterhouse.svg?branch=master)](https://travis-ci.com/hollie/misterhouse) Perl open source home automation program. It's fun, it's free, and it's entirely geeky. diff --git a/bin/alpha_page b/bin/alpha_page index 62bd83124..8d8764735 100755 --- a/bin/alpha_page +++ b/bin/alpha_page @@ -31,7 +31,9 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; + } use Getopt::Long; diff --git a/bin/get_earthquakes b/bin/get_earthquakes index dc382c264..4dbc75cbd 100755 --- a/bin/get_earthquakes +++ b/bin/get_earthquakes @@ -55,7 +55,8 @@ eof } BEGIN { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$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..3a4c4d112 100755 --- a/bin/get_email +++ b/bin/get_email @@ -31,7 +31,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } use Getopt::Long; diff --git a/bin/get_tcp b/bin/get_tcp new file mode 100755 index 000000000..78ffbd684 --- /dev/null +++ b/bin/get_tcp @@ -0,0 +1,125 @@ +#!/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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; +} + +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/bin/get_tv_grid b/bin/get_tv_grid index 1bfcdbd0d..b4359499c 100755 --- a/bin/get_tv_grid +++ b/bin/get_tv_grid @@ -45,7 +45,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } require "RedirAgent.pm"; diff --git a/bin/get_tv_grid_ge b/bin/get_tv_grid_ge index 9d26dfa01..a0cf8e05d 100755 --- a/bin/get_tv_grid_ge +++ b/bin/get_tv_grid_ge @@ -40,7 +40,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; use Getopt::Long; diff --git a/bin/get_tv_grid_xmltv b/bin/get_tv_grid_xmltv index fb212832d..494bfd75b 100755 --- a/bin/get_tv_grid_xmltv +++ b/bin/get_tv_grid_xmltv @@ -57,7 +57,8 @@ 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';"; # So perl2exe works + eval "push \@INC, '$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..04928dd76 100755 --- a/bin/get_tv_info +++ b/bin/get_tv_info @@ -26,7 +26,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; diff --git a/bin/get_tv_info_ge b/bin/get_tv_info_ge index 17d05da58..5ccc24cc6 100755 --- a/bin/get_tv_info_ge +++ b/bin/get_tv_info_ge @@ -23,7 +23,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; diff --git a/bin/get_url b/bin/get_url index 3a62ca279..513d8424b 100755 --- a/bin/get_url +++ b/bin/get_url @@ -9,7 +9,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my ( %config_parms, %parms ); @@ -19,7 +20,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 +33,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 +60,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 +115,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/bin/get_weather b/bin/get_weather index 2ad33a2b9..ba566914b 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 BEGIN { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$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..4b25093b6 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 BEGIN { - eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site'"; + eval "use lib '$Pgm_Path/../lib', '$Pgm_Path/../lib/site';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$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 109e41033..5d5a4d3a1 100755 --- a/bin/ical2vsdb +++ b/bin/ical2vsdb @@ -22,8 +22,16 @@ use strict; ## NOTE: v4 requires the update 2014 vsdb calendar schema (adding a control attribute). ## Common module organizer 2014 and vsdb calendar.pl 1.6.0-3 required. ## +## v5 02-2018: Some ALARM DATA has wonky dates (ie BEGIN:VALARM TRIGGER;VALUE=DATE-TIME:19760401T005545Z) +## ignore these entries and use 15 minutes for notification +## also enable a override_alarms to set a standard for all notifications ignoring what's in the ical data +## set to 'all' to remove all user specified alarm settings +## v5.1 02-2018 +## 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'; +push @INC, "../lib/fallback"; use iCal::Parser; use DateTime; use Digest::MD5 qw(md5 md5_hex); @@ -40,20 +48,22 @@ use vsLock; # verify locking works as expected my $progname = "ical2vsdb"; -my $progver = "v4 01-2014"; +my $progver = "v5.1 02-2018"; my $DB = 0; -my $days_before = -180; # defaults to avoid large vsdb databases, can be overriden +my $days_before = -180; # defaults to avoid large vsdb databases, can be overridden my $days_after = 180; # -my $config_file = ""; -my $config_version = 2; -my $vsdb_cal_file = "calendar.tab"; -my $vsdb_todo_file = "tasks.tab"; -my $sleep_time = 900; #poll cycle default is 15 minutes -my $md5file = ""; -my $local_cache = ""; -my $data = ""; +my $config_file = ""; +my $config_version = 2; +my $vsdb_cal_file = "calendar.tab"; +my $vsdb_todo_file = "tasks.tab"; +my $sleep_time = 900; #poll cycle default is 15 minutes +my $md5file = ""; +my $local_cache = ""; +my $data = ""; +my $override_alarms = "0"; #ignore ical alarms and always do $override_alarms in minutes speech events +my $sort_before_md5 = 0; $config_file = $ARGV[0] if $ARGV[0]; @@ -89,13 +99,19 @@ if ( ( $count == 0 ) or ( $output_dir eq "" ) ) { &read_md5file if ( $md5file ne "" ); print "Writing http requests to local cache: $local_cache\n" if $local_cache; +if ($override_alarms) { + if (lc $override_alarms eq "all") { + print "** Removing custom alarms for all events\n"; + } else { + print "Using a standard " .$override_alarms . "m alarm for all events\n"; + } +} print "Processing $count iCals"; print ", $days_after days in the future" if $days_after; my $abs_days_before = abs $days_before; print ", $abs_days_before in the past" if $abs_days_before; print "\nTarget vsdb directory is :$output_dir\n" if ($DB); print "MD5 temp file is :$md5file\n" if ( $md5file ne "" and $DB ); - #------------- Main Processing Loop ----------------- #BEGIN while (1) { @@ -189,7 +205,12 @@ while (1) { #print "\n\n$data\n"; - my $digest = md5_hex($data); + my $digest; + if ($sort_before_md5 || defined $ical_data[$loop]->{options}->{sort_before_md5}) { + $digest = md5_hex(join "\n", sort (split(/\n/,$data))); + } else { + $digest = md5_hex($data); + } # print "Debug: MD5=$digest\n"; # print "Debug: Hash=$ical_data[$loop]->{hash}\n" if (defined $ical_data[$loop]->{hash}); @@ -208,7 +229,6 @@ while (1) { else { $parser = $parser->calendar unless $ical_data[$loop]->{method} ne "dir"; - print "."; #print Dumper $parser; $ical_data[$loop]->{hash} = $digest; @@ -217,10 +237,9 @@ while (1) { push( @changed_icals, @$data_info[0] ); push( @master_cal, @$data_cals ); push( @master_todo, @$data_todos ); - print "."; &cache_local( $data, $ical_data[$loop]->{options}->{name} ) if $local_cache; - print "done\n"; + print "Processing Complete\n"; print "Calendar Info:" if $DB; print @$data_info[0] . "," . @$data_info[1] . "\n" if $DB; } @@ -239,7 +258,7 @@ while (1) { #print Dumper @master_cal; update_db( "$output_dir/$vsdb_cal_file", \@changed_icals, \@master_cal ); update_db( "$output_dir/$vsdb_todo_file", \@changed_icals, \@master_todo ); - print "done\n"; + print "DataBases Updated\n"; } #--------------- Empty data structures and sleep ------------ @@ -364,12 +383,13 @@ sub parse_cal { push @out_info, $calname; push @out_info, $caldesc; my $count = 0; - + my $ical_messages = ""; + #print Dumper $cal; while ( my $year = each %{ $cal->{events} } ) { while ( my $month = each %{ $cal->{events}->{$year} } ) { while ( my $day = each %{ $cal->{events}->{$year}->{$month} } ) { - print "."; #give some progress +# print "."; #give some progress while ( my $uid = each %{ $cal->{events}->{$year}->{$month}->{$day} } ) { my $delta; @@ -405,10 +425,14 @@ sub parse_cal { } my $reminder = ''; + my $bad_alarm = 0; if ( $event_ref->{VALARM} ) { foreach my $alarm ( @{ $event_ref->{VALARM} } ) { if ( $alarm->{when} && $event_ref->{DTSTART} ) { + $bad_alarm = $alarm->{when} if ($alarm->{when}->{local_c}->{year} < $event_ref->{DTSTART}->{local_c}->{year}); my $duration = $event_ref->{DTSTART} - $alarm->{when}; + print "db: D[" . $event_ref->{DESCRIPTION}. " S[" . $event_ref->{SUMMARY}. "]" if ($DB > 2); + print "db: [dt=" . $event_ref->{DTSTART} . " alarm=" , $alarm->{when} . " rem=$reminder dur=$duration]\n" if ($DB > 2); $reminder .= ($reminder) ? ',' : ''; if ( $duration->delta_days ) { $reminder .= $duration->delta_days . 'd'; @@ -418,6 +442,20 @@ sub parse_cal { } } } + if (lc $override_alarms eq 'all') { + $reminder = ''; + print ":"; + } elsif ($override_alarms) { + $reminder = $override_alarms . 'm'; + print "."; + } elsif ($bad_alarm) { + $reminder = '15m'; + $ical_messages .= "ical2vsdb: WARNING event " . $event_ref->{SUMMARY} . " on " . $event_ref->{DTSTART} . " has bad alarm date " . $bad_alarm . ". Using 15 minute default\n"; + print "!"; + } else { + print "."; + } + print "db2: rem2=$reminder\n" if ($DB > 2); } #MH Specific attributes @@ -458,6 +496,8 @@ sub parse_cal { } } } + print ".done\n"; + print $ical_messages if ($ical_messages); print "db: $count calendar records processed\n" if $DB; my @todos = $cal->{todos}; @@ -564,6 +604,7 @@ sub init { my $count = 0; my $version = 0; + my $dcsicals = ""; open( CFGFILE, $config_file ) || die "Error: Cannot open config file $config_file!"; @@ -577,32 +618,31 @@ sub init { if ( lc $type eq "output_dir" ) { $output_dir = $url; - } elsif ( lc $type eq "days_before" ) { $days_before = 0 - $url; - } elsif ( lc $type eq "days_after" ) { $days_after = $url; - } elsif ( lc $type eq "sleep_delay" ) { $sleep_time = $url; - } elsif ( lc $type eq "md5file" ) { $md5file = $url; - } elsif ( lc $type eq "cfg_version" ) { $version = $url; - } elsif ( lc $type eq "local_cache_dir" ) { $local_cache = $url; - + } + elsif ( lc $type eq "override_alarms" ) { + $override_alarms = $url; } + elsif ( lc $type eq "sort_before_md5" ) { + $sort_before_md5 = $url; + } else { $ical_data[$count]->{type} = $type; foreach my $opt ( split /,/, lc $options ) { @@ -611,7 +651,7 @@ sub init { $ical_data[$count]->{options}->{$key} = $value; } $ical_data[$count]->{hash} = "none"; - print "Applying second level ical parsing (set nodscfix if this breaks anything) ...\n" + $dcsicals .= $ical_data[$count]->{options}->{name} . " " unless ( exists $ical_data[$count]->{options}->{nodcsfix} ); my ( $method, $loc ) = split( /:\/\//, $url ); @@ -636,6 +676,8 @@ sub init { } } close(CFGFILE); + + print "Applied second level ical parsing to ($dcsicals).\n" if ($dcsicals); die "Incompatible configuration file version!" if ( $version != $config_version ); diff --git a/bin/ical_load b/bin/ical_load index 4d9ee1c6d..088695cd3 100755 --- a/bin/ical_load +++ b/bin/ical_load @@ -27,7 +27,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } use Getopt::Long; diff --git a/bin/mh b/bin/mh index 6f46a9c1c..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'); @@ -797,6 +801,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) { @@ -2685,8 +2693,14 @@ sub check_for_socket_data { $data = <$sock>; } else { - # 1500 is ethernet packet size - my $from_saddr = recv( $sock, $data, 1500, 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} diff --git a/bin/net_ftp b/bin/net_ftp index 102d2755f..e5657150a 100755 --- a/bin/net_ftp +++ b/bin/net_ftp @@ -7,7 +7,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; diff --git a/bin/outlook_read b/bin/outlook_read index 4e338acef..47062c65e 100755 --- a/bin/outlook_read +++ b/bin/outlook_read @@ -40,7 +40,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } use Getopt::Long; diff --git a/bin/pocketsphinx b/bin/pocketsphinx index a7d8794fb..ef5a19c82 100755 --- a/bin/pocketsphinx +++ b/bin/pocketsphinx @@ -39,7 +39,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } 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..a3147c1d7 100755 --- a/bin/read_email +++ b/bin/read_email @@ -7,7 +7,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; diff --git a/bin/report_weblog b/bin/report_weblog index c25c4196c..57aabf910 100755 --- a/bin/report_weblog +++ b/bin/report_weblog @@ -32,7 +32,8 @@ 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', '$Pgm_Path/../lib/site';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; diff --git a/bin/send_email b/bin/send_email index 897257bf2..3cec3ab55 100755 --- a/bin/send_email +++ b/bin/send_email @@ -7,7 +7,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } my %parms; diff --git a/bin/set_clock b/bin/set_clock index a5e2050db..1e90af613 100755 --- a/bin/set_clock +++ b/bin/set_clock @@ -89,7 +89,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$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..dccbf0cde 100755 --- a/bin/set_password +++ b/bin/set_password @@ -90,7 +90,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; 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..2c1288fc7 100755 --- a/bin/snpp_page +++ b/bin/snpp_page @@ -43,7 +43,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } #-------------------------------------------------------------------------------- diff --git a/bin/sun_time b/bin/sun_time index cc4c71fbe..b81a0b570 100755 --- a/bin/sun_time +++ b/bin/sun_time @@ -32,7 +32,8 @@ 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', '$Pgm_Path/../lib/site';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } use Getopt::Long; diff --git a/bin/test_x10 b/bin/test_x10 index 82dac39b5..6c5ca62b9 100755 --- a/bin/test_x10 +++ b/bin/test_x10 @@ -12,6 +12,7 @@ use strict; use lib '../lib', '../lib/site'; +push @INC, '$Pgm_Path/../lib/fallback'; my ( $device, $port, $house, $unit, $interface, %config_parms ); diff --git a/bin/update_docs b/bin/update_docs index 80fe0a3de..4dfeecd61 100755 --- a/bin/update_docs +++ b/bin/update_docs @@ -53,7 +53,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } use strict; diff --git a/bin/vv_tts.pl b/bin/vv_tts.pl index 5983807c5..1aeade509 100755 --- a/bin/vv_tts.pl +++ b/bin/vv_tts.pl @@ -24,7 +24,8 @@ 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';"; # Use BEGIN eval to keep perl2exe happy + eval "push \@INC, '$Pgm_Path/../lib/fallback';"; } use Getopt::Long; 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); +} + diff --git a/code/common/calc_eto.pl b/code/common/calc_eto.pl index 8c60a90c1..658b885eb 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 = ""; $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.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" ); @@ -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] 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} ); @@ -734,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 " @@ -811,7 +847,13 @@ 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 ); - print "[times=$times, sun->{rise}=" . $sun->{rise} . " sum=" . sum(@runTime) / 60 . "]\n"; # if ($debug); + #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] ); @@ -904,6 +946,55 @@ sub calc_eto_runtimes { 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 ) = @_; @@ -992,6 +1083,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} ); @@ -1077,6 +1180,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"; @@ -1112,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"; @@ -1130,9 +1247,16 @@ 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"; + + #$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 ); diff --git a/code/common/homebridge.pl b/code/common/homebridge.pl index f3c36538b..f28cf9b56 100644 --- a/code/common/homebridge.pl +++ b/code/common/homebridge.pl @@ -6,6 +6,8 @@ #@ Thermostat control only tested with a few models. my $hb_debug = 1; +my $http_address = $config_parms{http_address}; +$http_address = $Info{IPAddress_local} unless ($http_address); my $port = $config_parms{homebridge_port}; $port = 51826 unless ($port); my $name = $config_parms{homebridge_name}; @@ -58,7 +60,7 @@ $config_json .= add_group("thermostat"); $config_json .= "\t\t}\n\t]\n}\n"; - print_log "[Homebridge]: Writing configuration for server " . $Info{IPAddress_local} . " to $filepath..."; + print_log "[Homebridge]: Writing configuration for server " . $http_address . " to $filepath..."; #print_log $config_json; file_write( $filepath, $config_json ); @@ -95,14 +97,14 @@ sub add_group { if ( $type eq "thermostat" ) { $text .= "\t\t\"mode_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SUB?hb_thermo_set_state%28" . $obj_name . ",%VALUE%%29\",\n"; $text .= "\t\t\"status_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SUB?hb_thermo_get_state%28" . $obj_name . "," @@ -110,14 +112,14 @@ sub add_group { . "%29\",\n"; $text .= "\t\t\"setpoint_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SUB?hb_thermo_set_setpoint%28" . $obj_name . ",%VALUE%%29\",\n"; $text .= "\t\t\"gettemp_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SUB?hb_thermo_get_setpoint%28" . $obj_name @@ -133,7 +135,7 @@ sub add_group { "\t\t\"" . $on . "_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} @@ -143,7 +145,7 @@ sub add_group { "\t\t\"" . $off . "_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} @@ -151,7 +153,7 @@ sub add_group { . $off . "\",\n"; $text .= "\t\t\"brightness_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} @@ -159,7 +161,7 @@ sub add_group { if ( $type eq "light" ); $text .= "\t\t\"speed_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} @@ -167,7 +169,7 @@ sub add_group { if ( $type eq "fan" ); $text .= "\t\t\"status_url\": \"http://" - . $Info{IPAddress_local} . ":" + . $http_address . ":" . $config_parms{http_port} . "/SUB?hb_status%28" . $obj_name . "," 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"); } 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 diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index fff34c9d9..1b0b15ae8 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -4,6 +4,20 @@ =head2 DESCRIPTION Module emulates the HUE to allow for direct connectivity from the Amazon Echo, Google Home, and any other devices that support the HUE bridge. +Version 2.0 + +=head2 Release Notes + +Version 2.0 (10-26-19): +Added the ability to respond with a state and level at the same time (from a sub) if needed. See Example below. + +Fixed the response to a get when you use the custom mapped states with a sub. Before the fix, if the sub responded with the custom state it would not be mapped back to the on or off before it was sent to alexa. + +The state sent from alexa (in a get request after alexa sends a set request) is used as the get response if it is not returned from a configured sub. This is so the get state request from alexa always matches what she set it to so she won't throw the malfunction error. + +I use the state sent from alexa for the get request to an MH voice command to avoid the malfunction error. We don't do any state checks on an MH voice command. + +We now respond with the full light definition during a discovery because the new Echo version doesn't like the short definition. This is ok now because the Echo now uses gzip compression so it does not limit the amount of supported devices =head2 CONFIGURATION @@ -25,6 +39,7 @@ For Google Home and a reverse proxy (Apache/IIS/etc): alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response For Google Home using the builtin proxy port: +This method should not be used. alexa_enable = 1 alexaHttpPortCount = 1 # Open 1 proxy port on port 80 (We default to port 80 so no need to define it) @@ -32,6 +47,9 @@ For Google Home using the builtin proxy port: alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response +Note: +On some newer Echo versions, you must use port 80. + For Echo (Chunked method): alexa_enable = 1 @@ -39,7 +57,7 @@ For Echo (Chunked method): For Echo (Multi-port method): -This method should not be needed unless for some reason your Echo does not work with the Chunked method. +This method should not be used. alexa_enable = 1 alexaHttpPortCount = 1 # Open 1 proxy port for a total of 2 ports including the default MH web port. We only support 1 for now unless I see a need for more. @@ -211,6 +229,31 @@ When the above is said to the Echo, it first gets the current state, then subtra ALEXABRIDGE_ADD, AlexaItems, thermostat, thermostat, &temperature +# This is a simple generic example of a sub: + + sub light1 { + my ($type, $state) = @_; + #Type state - should return: + # - on or off + # - or a custom state mapped to on or off in the AlexaBridge_Item + # - or a number between 0 and 100 (level) + # - or the state and level number comma separated. on/off/custom state,level. IE: on,75 + # + #Type set - this is when alexa is asked to turn this device on/off so the state should be set to $state or your action should be run. + + if ($type eq 'state') { + if ( $light1->can('state_level') ) { + return (state $light1).','.$light1->level; # Example returning state and level. NOTE: Ensure state is NOT numeric + return $light1->level; # Example returning level only. + } + else { + return (state $light1); # Example returning state only. If a numeric value is returned, it is considered the level and the state is considered on. + } + } + elsif ($type eq 'set') { + $light1->set($state); + } + } 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: @@ -785,11 +828,14 @@ sub process_http { if ( ( $uris[3] eq 'lights' ) && ( $AlexaObjects->{'uuid'}->{ $uris[4] } ) ) { $uuid = $uris[4]; $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; - my $state = &get_set_state( $self, $AlexaObjects, $uuid, 'get' ); - $statep1 = - qq[{"state":{$state,"hue":15823,"sat":88,"effect":"none","ct":313,"alert":"none","colormode":"ct","reachable":true,"xy":\[0.4255,0.3998\]},"type":"Extended color light","name":"]; - $statep2 = - qq[","modelid":"LCT001","manufacturername":"Philips","uniqueid":"$uuid","swversion":"65003148","pointsymbol":{"1":"none","2":"none","3":"none","4":"none","5":"none","6":"none","7":"none","8":"none"}}]; + $body =~ s/: /:/g; + my $state = &get_set_state( $self, $AlexaObjects, $uuid, 'get', $body ); + $statep1 = qq[{"state": {$state,"effect": "none","alert": "none","sat": 200,"ct": 500,"xy": \[0.5, 0.5\],"reachable": true,"colormode": "hs"},"type": "Dimmable light","name":"]; + $statep2 = qq[","modelid": "LWB014","swversion": "1.23.0_r20156"}]; + #$statep1 = + #qq[{"state":{$state,"hue":15823,"sat":88,"effect":"none","ct":313,"alert":"none","colormode":"ct","reachable":true,"xy":\[0.4255,0.3998\]},"type":"Extended color light","name":"]; + #$statep2 = + #qq[","modelid":"LCT001","manufacturername":"Philips","uniqueid":"$uuid","swversion":"65003148","pointsymbol":{"1":"none","2":"none","3":"none","4":"none","5":"none","6":"none","7":"none","8":"none"}}]; $content = $statep1 . $name . $statep2; $count = 1; } @@ -815,10 +861,18 @@ sub process_http { next unless $name; my $state = &get_set_state( $self, $AlexaObjects, $uuid, 'get' ); $statep1 = qq[{"]; - $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; if ( $count >= 1 ) { $content = $content . $delm . $uuid . $statep2 . $name . $statep3 } else { $content = $statep1 . $uuid . $statep2 . $name . $statep3 } $count++; @@ -856,12 +910,17 @@ sub process_http { $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; # $end = qq[}}]; $delm = qq[,"]; + $name =~ s/_/ /g; if ( $count >= 1 ) { $content = $content . $delm . $uuid . $statep2 . $name . $statep3 } else { $content = $statep1 . $uuid . $statep2 . $name . $statep3 } $count++; } } - if ( $count >= 1 ) { + if ( $count <= 0 ) { + $end = ''; + $content = '{}'; + } + #if ( $count >= 1 ) { $content = $content . $end; $debugcontent = $content if $main::Debug{'alexa'} >= 2; $content = &_Gzip( $content, $Http{'Accept-Encoding'} ); @@ -879,10 +938,10 @@ sub process_http { $output .= "\r\n"; $debugcontent = $output . $debugcontent if $main::Debug{'alexa'} >= 2; $output .= $content; - } - else { - $output = "HTTP/1.1 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\nContent-Length: 2\r\nDate: " . time2str(time) . "\r\n\r\n.."; - } + #} + #else { + # $output = "HTTP/1.1 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\nContent-Length: 2\r\nDate: " . time2str(time) . "\r\n\r\n.."; + #} &main::print_log("[Alexa] Debug: MH Response $debugcontent \n") if $main::Debug{'alexa'} >= 2; return $output; } @@ -950,9 +1009,22 @@ sub get_set_state { my $sub = $AlexaObjects->{'uuid'}->{$uuid}->{'sub'}; my $statesub = $AlexaObjects->{'uuid'}->{$uuid}->{'statesub'}; $state = $AlexaObjects->{'uuid'}->{$uuid}->{ lc($state) } if $AlexaObjects->{'uuid'}->{$uuid}->{ lc($state) }; - if ( $state =~ /\d+/ ) { $state = &roundoff( $state / 2.54 ) } - &main::print_log("[Alexa] Debug: get_set_state ($uuid $action $state) : name: $name realname: $realname sub: $sub state: $state\n") - if $main::Debug{'alexa'}; + if ( ( $state =~ /\d+/ ) and ( $action eq 'set' ) ) { $state = &roundoff( $state / 2.54 ) } + &main::print_log("[Alexa] Debug: get_set_state ($uuid $action $state) : name: $name realname: $realname sub: $sub state: $state\n") if $main::Debug{'alexa'}; + + # Alexa expects that the queried state is what she set it to or she complains about a malfunction. + # She sends the state she thinks the deivce is in, in the body of a get state request for a period after a set request. + # For instances like subs and voice commands where we may not get a state, we just return the one alexa sent. + my $pstate = 'on'; #Default prev state + my $plevel = '254'; #Default prev level + my $statesrc = 'DEFAULT'; + my $levelsrc = 'DEFAULT'; + if ( $action eq 'get' ) { #Get prev state/level from the body of the get + if ( $state =~ /\"(on)\":(true)/ ) { $pstate = 'on'; $statesrc = 'ALEXA'; } + elsif ( $state =~ /\"(on)\":(false)/ ) { $pstate = 'off'; $statesrc = 'ALEXA'; } + if ( $state =~ /\"(bri)\":(\d+)/ ) { $plevel = $2; $levelsrc = 'ALEXA'; } + } + if ( $realname =~ /^\$/ ) { my $object = ::get_object_by_name($realname); @@ -960,9 +1032,9 @@ sub get_set_state { if ( $action eq 'get' ) { my $cstate = $object->$statesub; $cstate =~ s/\%//; - my $level = '254'; + my $level = $plevel; #Set the default level to the prev level from the body of the get my $type = $object->get_type(); - my $debug = "[Alexa] Debug: get_state (actual object state: $cstate) - (object type: $type) - "; + my $debug = "[Alexa] Debug: get_set_state (actual object state: $cstate) - (object type: $type) - "; my $return; if ( $object->can('state_level') ) { my $l = $object->level; @@ -999,8 +1071,12 @@ sub get_set_state { &main::run_voice_cmd("$realname"); return; } - elsif ( $action eq 'get' ) { - return qq["on":true,"bri":254]; + elsif ( $action eq 'get' ) { #Return prev state/level sent in the body of the get + $pstate = '"on":true' if $pstate eq 'on'; + $pstate = '"on":false' if $pstate eq 'off'; + my $return = qq[$pstate,"bri":$plevel]; + &main::print_log("[Alexa] Debug: get_set_state request: ( get ) voice command: ( $realname ) - returning $return") if $main::Debug{'alexa'}; + return $return; } } @@ -1011,15 +1087,57 @@ sub get_set_state { return; } elsif ( $action eq 'get' ) { - my $debug = "[Alexa] 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]; - &main::print_log("$debug returning - $return\n") if $main::Debug{'alexa'}; - return $return; + my $debug = "[Alexa] Debug: get_set_state running sub: $sub( state, $state ) - "; + my $state = &{$sub}('state'); #Try to get the state from the sub + my $level; + + #Allow returning state and level comma seperated + #IE: on,50 + if ($state =~ /,/) { + my @splstr = split /,/, $state; + $level = $splstr[1]; + $state = lc($splstr[0]); + $level =~ s/ //g; + $state =~ s/ //g; + &main::print_log("Split return state: $state level: $level"); + } + + # Check on/off has been mapped to a different state by user in AlexaBridge_Item + if ( (defined $state) and (lc( $AlexaObjects->{'uuid'}->{$uuid}->{'on'}) ) eq lc($state) ) { $state = 'on' } + elsif ( (defined $state) and (lc( $AlexaObjects->{'uuid'}->{$uuid}->{'off'}) ) eq lc($state) ) { $state = 'off' } + + + if ( (defined $state) and ($state =~ /\d+/) ) { + $level = ( &roundoff( ( $state * 2.54 ) ) ); + $debug .= "using state: $level returned from SUB - state defaulting to on - "; + my $return = qq["on":true,"bri":$level]; + &main::print_log("$debug returning - $return\n") if $main::Debug{'alexa'}; + return $return; } - return qq["on":true,"bri":254]; + elsif ( (defined $level) and ($level =~ /\d+/) ) { #Level split from state + $level = ( &roundoff( ( $level * 2.54 ) ) ); #Convert 0 - 100 scale to 0 - 254 + $debug .= "using level: $level returned from SUB - "; + } + else { + $level = $plevel; #Use prev level sent in the body of the get, if not returned from the sub + $debug .= "using level: $level returned from $levelsrc - "; + } + + + if ( (defined $state) and ($state =~ /on|off/) ) { + $debug .= "using state: $state returned from SUB - "; + } + else { + $state = $pstate; #Use prev state sent in the body of the get, if not returned from the sub + $debug .= "using state: $state returned from $statesrc - "; + } + + $state = '"on":true' if $state eq 'on'; + $state = '"on":false' if $state eq 'off'; + + my $return = qq[$state,"bri":$level]; + &main::print_log("$debug returning - $return\n") if $main::Debug{'alexa'}; + return $return; } } diff --git a/lib/AoGSmartHome_Items.pm b/lib/AoGSmartHome_Items.pm new file mode 100644 index 000000000..08221d4c3 --- /dev/null +++ b/lib/AoGSmartHome_Items.pm @@ -0,0 +1,981 @@ + +=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; + +sub set_state { + my ( $self, $uuid, $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: 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; + + $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 ( ref $sub eq 'CODE' ) { + 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 { + # + # 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 ( ($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 get_state { + my ( $self, $uuid, $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_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; + + 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; + } +} + +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 will 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; + + # 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 AoG item."); + return; + } + } + } + + 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'; + # If no device type is provided we default to 'light' + $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}->{'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 .= <{'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'} } ) { + my $uuid = $device->{'id'}; # Makes things easier below... + + if ( !exists $self->{'uuids'}->{$uuid} ) { + $response .= <{'uuids'}->{$uuid}->{'type'} eq 'scene' ) { + $response .= <{'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 .= < 0 ? 'true' : 'false'; + + $response .= <{'uuids'}->{$uuid}->{'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'} } ) { + set_state( $self, $device->{'id'}, $turn_on ? 'on' : 'off' ); + $response .= qq["$device->{'id'}",]; + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= "],\n"; + + $response .= <{'execution'}->[0]->{'params'}->{'brightness'}; + + foreach my $device ( @{ $command->{'devices'} } ) { + set_state( $self, $device->{'id'}, $brightness); + $response .= qq["$device->{'id'}",]; + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= "],\n"; + + $response .= <{'devices'} } ) { + set_state( $self, $device->{'id'}); + $response .= qq["$device->{'id'}",]; + } + + # Remove extra ',' at the end + $response =~ s/,$//; + + $response .= "],\n"; + + $response .= <{'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. + +=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 ); + } + elsif ( $execution_command =~ /^action\.devices\.commands\.Thermostat.+$/ ) { + $response .= execute_ThermostatX( $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/BondHome.pm b/lib/BondHome.pm new file mode 100644 index 000000000..4ace3ea83 --- /dev/null +++ b/lib/BondHome.pm @@ -0,0 +1,854 @@ + +=head1 B + + +=head2 DESCRIPTION + +Module for interfacing with the BondHome Hub to control IR/RF devices such as fans. + +=head2 CONFIGURATION + +At minimum, you must define the BondHome_ip in the mh.private.ini and the Interface +object. Once they are configured you can restart MH, once MH is back up restart the Bond Hub, +next set the Bond Hub interface object to "GetToken" within a few min after the Bond Hub reboot. +Once the token is successfuly retreved, set the Bond Hub interface object to "LogDevs" +and copy the device code from the MH logs and paste into a MH .mht file in your code directory. +You will need all the devices preconfigured in the BondHome Hub because MH pulls them including the names from it. +The BondHome_Device objects allow for the display and control of these objects as separate items +in the MH interface and allows users to interact directly with these objects +using the basic Generic_Item functions such as tie_event. + + +The BondHome_Device object is for tracking the state of and controlling +devices configured in the BondHome hub through the BondHome app and stored in +the local BondHome hub database. These devices are pulled from the BondHome Hub +with the local api. + +Misterhouse loads all devices and device commands from BondHome when it is started. +You must reload the BondHome device and trigger and retrieve an auth token by setting the +parent object to "GetToken" with in a few min after the reboot. + +The BondHome_Manual object is for manually recording remote signals and sending them from Misterhouse. +This bypasses the BondHome Hub database and just tells the BondHome Hub what signal to transmit directly. +To record a signal, set the Bond Hub interface object to ScanRF or ScanIR depending on what kind of remote +you are recording. Once scan is enabled, put the remote close to the hub and push the button you want to +record a few times and watch for the hub lights to change colors (This is the same process as the initial hub setup +through the app). Next set the Bond Hub interface object to ScanCheck and the .mht code for the recorded +command will be logged, update the device name IE: MasterFan and the command name IE: PowerOff and +paste the code in your .mht file. + +=head2 Interface Configuration + +mh.private.ini configuration: + +In order to allow for multiple BondHome Hubs, instance names are used. +the following are prefixed with the instance name (BondHome). + + + +The IP of the BondHome Hub: + BondHome_ip=192.168.1.50 + + +Max command retrys when a command fails to send to the BondHome Hub: + BondHome_maxretry=4 + + +=head2 Defining the Interface Object + +In addition to the above configuration, you must also define the interface +object. The object can be defined in the user code. + +In user code: + + $BondHomeHub = new BondHome('BondHome'); + +Wherein the format for the definition is: + + $BondHomeHub = new BondHome(INSTANCE); + +States: +GetToken,Reboot,LogDevs,ReloadCache,LogVersion,ScanRF,ScanIR,ScanStop,ScanCheck + + + +=head2 NOTES + +An example mh.private.ini: + + BondHome_maxretry=4 + BondHome_ip=192.168.1.50 + + +An example user code: + + #noloop=start + + use BondHome; + + $BondHomeHub = new BondHome('BondHome'); + + $MasterFan = new BondHome_Device('BondHome','MasterFan'); + + $TV = new BondHome_Manual('BondHome'); + $TV->addcmd('power', '38', 'OOK', 'hex', '40000', '1', '00000'); + + #noloop=stop + + +An example .mht code: + BONDHOME, BondHome, BondHome + BONDHOME_DEVICE, masterfan, BondHome, masterfan + BONDHOME_DEVICE, guestfan, BondHome, guestfan + + + BONDHOME_MANUAL, TV, BondHome + BONDHOME_MANUAL_CMD, TV, power, 38, OOK, hex, 40000, 1, 0000000 + + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package BondHome; +@BondHome::ISA = ('Generic_Item'); + +use Data::Dumper; +use JSON; + +sub new { + my ( $class, $instance ) = @_; + $instance = "BondHome" if ( !defined($instance) ); + ::print_log("Starting $instance instance of BondHome interface module"); + + my $self = new Generic_Item(); + + # Initialize Variables + $$self{instance} = $instance; + $$self{maxretry} = $::config_parms{ $instance . '_maxretry' } || 4; + $$self{ip} = $::config_parms{ $instance . '_ip' }; + my $year_mon = &::time_date_stamp( 10, time ); + $$self{log_file} = $::config_parms{'data_dir'} . "/logs/BondHome.$year_mon.log"; + $$self{token_file} = $::config_parms{'data_dir'} . "/.bh-$instance"; + + bless $self, $class; + + #Store Object with Instance Name + $self->_set_object_instance($instance); + #$self->restore_data( 'token' ); + $$self{token} = $self->get_data($$self{token_file}); #The normal restore_data happens after new is called, so we have to save to a file. + @{$$self{states}} = ('GetToken','Reboot','LogDevs','ReloadCache', 'LogVersion', 'ScanRF', 'ScanIR', 'ScanStop', 'ScanCheck'); + return $self; +} + +sub get_object_by_instance { + my ($instance) = @_; + return $Interfaces{$instance}; +} + +sub _set_object_instance { + my ( $self, $instance ) = @_; + $Interfaces{$instance} = $self; +} + +sub init { + +} + +=item C + +Used to associate child objects with the interface. + +=cut + +sub register { + my ( $self, $object, $class ) = @_; + if ( $object->isa('BondHome_Device') ) { + ::print_log("Registering BondHome Device Child Object"); + push @{ $self->{device_object} }, $object; + } elsif ( $object->isa('BondHome_Manual') ) { + ::print_log("Registering BondHome Manual Child Object" ); + push @{ $self->{manual_object} }, $object; + } +} + + + +sub set { + my ( $self, $p_state, $p_setby, $p_response ) = @_; + if ( uc $p_state eq 'GETTOKEN' ) { + ::print_log( "[BondHome] Received request " . $p_state . " for " . $self->get_object_name ); + $self->SUPER::set( $p_state, $p_setby ); + $self->gettoken( $object, $class ); + } elsif ( uc $p_state eq 'REBOOT' ) { + ::print_log( "[BondHome] Received request " . $p_state . " for " . $self->get_object_name ); + $self->SUPER::set( $p_state, $p_setby ); + return unless $self->tokencheck; + $self->bondcmd( $class, '/v2/sys/reboot', 'PUT', '{}' ); + } elsif ( uc $p_state eq 'LOGDEVS' ) { + ::print_log( "[BondHome] Received request " . $p_state . " for " . $self->get_object_name ); + $self->SUPER::set( $p_state, $p_setby ); + $self->logdevs( $object, $class ); + } elsif ( uc $p_state eq 'LOGVERSION' ) { + ::print_log( "[BondHome] Received request " . $p_state . " for " . $self->get_object_name ); + $self->SUPER::set( $p_state, $p_setby ); + return unless $self->tokencheck; + ::print_log "[BondHome] version ".$self->bondcmd( $class, '/v2/sys/version', 'GET')->{_content}; + } elsif ( uc $p_state eq 'RELOADCACHE' ) { + ::print_log( "[BondHome] Received request " . $p_state . " for " . $self->get_object_name ); + $self->SUPER::set( $p_state, $p_setby ); + return unless $self->tokencheck; + ::print_log "[BondHome] Reloading local device cache from Bond"; + delete $self->{devicehash}->{devicename} if $self->{devicehash}->{devicename}; + $self->getbonddevs( $object, $class ); + foreach my $child ( @{ $self->{device_object} } ) { + $child->updatestates($class); + } + } elsif ( uc $p_state eq 'SCANRF' ) { + $self->scan('RF',$class ); + } elsif ( uc $p_state eq 'SCANIR' ) { + $self->scan('IR',$class ); + } elsif ( uc $p_state eq 'SCANSTOP' ) { + $self->scan('STOP',$class ); + } elsif ( uc $p_state eq 'SCANCHECK' ) { + $self->scan('CHECK',$class ); + } else { + ::print_log( "[BondHome] Unknown request " . $p_state . " for " . $self->get_object_name ); + } +} + +sub tokencheck { + my ( $self ) = @_; + unless ( $$self{token} ) { + ::print_log "[BondHome] You must reboot the Bond Home and set the parent BondHome object to gettoken to get/set the token"; + return; + } + return 1; +} + +sub devexists { + my ( $self, $object, $class ) = @_; + my $token = $$self{token}; + my $devicename = $$object{devicename}; + + return unless $self->tokencheck; + + unless ( $self->{devicehash}->{devicename} ) { + ::print_log ("[BondHome] There are no devices in cache, running reload cache ". $self->get_object_name ); + $self->getbonddevs( $object, $class ); + + unless ( $self->{devicehash}->{devicename} ) { + ::print_log ("[BondHome] (Sub devexists) There are no devices in cache after reloading the cache, something went wrong"); + return; + } + } + + return 1 if ( exists $self->{devicehash}->{devicename}->{$devicename} ); + + ::print_log ( "[BondHome] there is no device with the name \"$devicename\" configured on the BondHome Hub " . $self->get_object_name ); + return; +} + + +sub getdevstates { + my ( $self, $object, $class ) = @_; + my $token = $$self{token}; + my $devicename = $$object{devicename}; + + return unless $self->tokencheck; + + unless ( $self->{devicehash}->{devicename} ) { + ::print_log "[BondHome] There are no devices in cache, running reload cache"; + $self->getbonddevs( $object, $class ); + + unless ( $self->{devicehash}->{devicename} ) { + ::print_log "[BondHome] (Sub getdevstates) There are no devices in cache after reloading the cache, something went wrong"; + return; + } + } + + + my @states; + my @speeds; + foreach my $command (keys %{$self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}} ) { + push @states, ucfirst($command); + if ( normalize($command) =~ /speed(\d)/ ) { + push @speeds, $1; + } + } + @speeds = sort @speeds; + push @states, @speeds; + return @states; +} + + +sub normalize { + my ( $string ) = @_; + $string = lc $string; + $string =~ s/ //g; + return $string; +} + +sub get_data { + my ( $self, $file ) = @_; + if( -e $file ) { + open my $FH, '<', $file or ::print_log "[BondHome] failed to open token file $!"; + while (my $line = <$FH>) { + $line =~ s/^\s+//; + $line =~ s/\s+$//; + if ( length($line) > 1 ) { + close $FH; + return $line; + } + } + close $FH; + } +} + + +sub save_data { + my ( $self, $file, $data ) = @_; + ::print_log "[BondHome] saving token to $file"; + open my $FH, '>', $file or ::print_log "[BondHome] failed to save token to file $!"; + print $FH $data; + close($FH); +} + + +sub scan { + my ( $self, $type, $class ) = @_; + my $token = $$self{token}; + my $instance = $$self{instance}; + my $http; + my $content; + my $url = '/v2/signal/scan'; + + if ($type eq 'IR') { + $http = 'PUT'; + $content = '{ "freq": 38, "modulation": "OOK" }'; + } elsif ($type eq 'RF') { + $http = 'PUT'; + $content = '{ "modulation": "OOK" }'; #RF all frequencies + } elsif ($type eq 'STOP') { + $http = 'DELETE'; + } elsif ($type eq 'CHECK') { + $http = 'GET'; + } + + return unless $self->tokencheck; + + my $response = $self->bondcmd( $class, $url, $http, $content ); + + #BONDHOME_MANUAL, masterfan, BondHome + #BONDHOME_MANUAL_CMD, masterfan, power, 434000, OOK, cq, 1000, 12, 110100110110H + + + if ($type eq 'CHECK') { + return unless $response; + my $message = $response->decoded_content; + eval { $message = decode_json($message) }; + if ( $message->{success} ) { + my $response2 = $self->bondcmd( $class, $url.'/signal', 'GET' ); + return unless $response2; + my $message2 = $response2->decoded_content; + eval { $message2 = decode_json($message2) }; + + ::print_log "[BondHome] Listing mht code for manual device command"; + my $msg = "\nBONDHOME_MANUAL, dev_name_update_me, $instance"; + $msg .= "\nBONDHOME_MANUAL_CMD, dev_name_update_me, cmd_name_update_me, ".$message2->{freq}.", ".$message2->{modulation}.", ".$message2->{encoding}.", ".$message2->{bps}.", ".$message2->{reps}.", ".$message2->{data}; + ::print_log $msg; + } elsif ( $message->{running} ) { + ::print_log "[BondHome] Scan is still running and no signals have been seen"; + } else { + ::print_log "[BondHome] Scan has timed out and no signals have been seen"; + } + } +} + + + +sub logdevs { + my ( $self, $object, $class ) = @_; + my $token = $$self{token}; + my $instance = $$self{instance}; + my $name = $self->get_object_name; + $name =~ s/\$//; + + return unless $self->tokencheck; + + unless ( $self->{devicehash}->{devicename} ) { + ::print_log "[BondHome] There are no devices in cache, running reload cache"; + $self->getbonddevs( $object, $class ); + } + ::print_log "[BondHome] Listing mht code for Bond devices"; + my $message = "\nBONDHOME, $name, $instance"; + foreach my $devicename (keys %{$self->{devicehash}->{devicename}}) { + $message .= "\nBONDHOME_DEVICE, $devicename, $instance, $devicename"; + } + ::print_log $message; +} + + +sub gettoken { + my ( $self, $object, $class ) = @_; + ::print_log "[BondHome] Getting Token"; + my $response = $self->bondcmd( $class, '/v2/token', 'GET' ); + my $message = $response->decoded_content; + eval { $message = decode_json($message) }; + if ( $message->{locked} ) { + ::print_log "[BondHome] You must reboot the Bond Home before running gettoken"; + return; + } + $$self{token} = $message->{token}; + my $token = $message->{token}; + $self->save_data( $$self{token_file}, $token ); + ::print_log "[BondHome] Got Token: $$self{token}" if $$self{token}; + delete $self->{devicehash} if $self->{devicehash}; + $self->{devicehash} = $self->getbonddevs( $object, $class); +} + + +sub reloadcache { + my ( $self, $object, $class ) = @_; + + ::print_log "[BondHome] Reloading local device cache from Bond"; + delete $self->{devicehash}->{devicename} if $self->{devicehash}->{devicename}; + $self->getbonddevs( $object, $class ); +} + + +sub getbonddevs { + my ( $self, $class ) = @_; + my $maxretry = $$self{maxretry}; + my $token = $$self{token}; + my $response; + return unless $self->tokencheck; + for (0..$maxretry) { + $response = $self->bondcmd( $class, '/v2/devices', 'GET' ); + } + return unless $response; + my $message = $response->decoded_content; + eval { $message = decode_json($message) }; + + ::print_log("[BondHome] reloading MH device cache from bond"); + + foreach my $deviceid (keys %{$message}) { + next if $deviceid =~ /_/; + for (0..$maxretry) { + $response = $self->bondcmd( $class, "/v2/devices/$deviceid", 'GET' ); + last if $response; + } + next unless $response; + my $message = $response->decoded_content; + eval { $message = decode_json($message) }; + my $devicename = normalize($message->{name}); + $self->{devicehash}->{devicename}->{$devicename}->{id}=$deviceid; + + foreach my $action ( @{$message->{actions}} ) { + next if ( $action =~ /^Set/ ); #Skip the Set actions because they require arguments. + next if ( $action =~ /^IncreaseSpeed/ ); #Skip the IncreaseSpeed actions because they require arguments. + next if ( $action =~ /^DecreaseSpeed/ ); #Skip the DecreaseSpeed actions because they require arguments. + $self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}->{normalize($action)}->{action}=$action; + } + #::print_log Dumper $message; + my $response2; + for (0..$maxretry) { + $response2 = $self->bondcmd( $class, "/v2/devices/$deviceid/commands", 'GET' ); + last if $response2; + } + next unless $response2; + my $message2 = $response2->decoded_content; + eval { $message2 = decode_json($message2) }; + foreach my $cmdid (keys %{$message2}) { + next if $cmdid =~ /_/; + for (0..$maxretry) { + $response = $self->bondcmd( $class, "/v2/devices/$deviceid/commands/$cmdid", 'GET' ); + last if $response; + } + next unless $response; + my $message = $response->decoded_content; + eval { $message = decode_json($message) }; + my $cmdname = normalize($message->{name}); + $self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}->{$cmdname}->{id}=$cmdid; + #::print_log Dumper $$self{devicehash}; + } + + ::print_log "[BondHome] \n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n" if $debug; + } +} + + + +sub sendcmd { + my ( $self, $object, $cmdname, $class, $argument ) = @_; + + return unless $self->tokencheck; + + my $maxretry = $$self{maxretry}; + my $devicename = $$object{devicename}; + $cmdname = normalize($cmdname); + + if ( exists $self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}->{$cmdname}->{id} ) { + my $deviceid = $self->{devicehash}->{devicename}->{$devicename}->{id}; + my $cmdid = $self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}->{$cmdname}->{id}; + for (0..$maxretry) { + my $response = $self->bondcmd( $class, "/v2/devices/$deviceid/commands/$cmdid/tx", 'PUT', '{}' ); + last if $response; + } + } elsif ( exists $self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}->{$cmdname}->{action} ) { + my $deviceid = $self->{devicehash}->{devicename}->{$devicename}->{id}; + my $action = $self->{devicehash}->{devicename}->{$devicename}->{commands}->{name}->{$cmdname}->{action}; + if ( $argument ) { + $argument = '{"argument": '.$argument.'}'; + } else { + $argument = '{}'; + } + for (0..$maxretry) { + my $response = $self->bondcmd( $class, "/v2/devices/$deviceid/actions/$action", 'PUT', $argument ); + last if $response; + } + } else { + ::print_log "[BondHome] Invalid command: $cmdname for " . $self->get_object_name; + } + +} + + +sub bondcmd { + my ( $self, $class, $url, $function, $content ) = @_; + use LWP::UserAgent; + use HTTP::Request; + my $ip = $$self{ip}; + my $token = $$self{token}; + my $userAgent = LWP::UserAgent->new(); + $userAgent->timeout(1); + my $request; + + if ( $function eq 'PUT' ) { + $request = HTTP::Request->new(PUT => 'http://'.$ip.$url); + $request->content($content); + } elsif ( $function eq 'POST' ) { + $request = HTTP::Request->new(POST => 'http://'.$ip.$url); + $request->content($content); + } elsif ( $function eq 'DELETE' ) { + $request = HTTP::Request->new(DELETE => 'http://'.$ip.$url); + } elsif ( $function eq 'GET' ) { + $request = HTTP::Request->new(GET => 'http://'.$ip.$url); + } + + unless ( $url =~ /\/token$/ ) { + $request->header('Host' => "$ip"); + $request->header('BOND-Token' => "$token"); + } + + #::print_log("[BondHome] request: ".Dumper $request); + my $response = $userAgent->request($request); + if ($response->is_error) { + ::print_log("[BondHome] http request: http://$ip$url failed - ". $response->status_line ." ". $response ->decoded_content); + if ($response->status_line =~ /read timeout/) { + ::print_log("[BondHome] retrying request: http://$ip$url"); + } + return 0; + } + return $response; +} + +=back + +=head1 B + +=head2 SYNOPSIS + +User code: + + $LivingRoomFan = new BondHome_Device('BondHome','livingroomfan'); + + Wherein the format for the definition is: + $BondHome_Device = new BondHome_Device(INSTANCE,BondHomeDeviceName); + +States: +Dynamic from BondHome Hub + +See C for a more detailed description of the arguments. + + +=head2 DESCRIPTION + + Configures a device from the BondHome to be controlled by MH. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package BondHome_Device; +@BondHome_Device::ISA = ('Generic_Item'); + +=item C + +Instantiates a new object. + +$instance = The instance of the parent BondHome hub object that this device is found on + +$devicename = The name of the device used on the bondhome hub + +=cut + + +sub new { + my ( $class, $instance, $devicename ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{parent} = BondHome::get_object_by_instance($instance); + $$self{parent}->register( $self, $class ); + $$self{devicename} = normalize($devicename); + + $$self{parent}->devexists( $self, $class ); + + @{ $$self{states} } = $$self{parent}->getdevstates( $self, $class ); + + return $self; +} + + +sub set { + my ( $self, $p_state, $p_setby, $p_response ) = @_; + + $p_state = normalize($p_state); + if ( $p_state =~ /^\d+$/ ) { + $p_state = "speed$p_state"; + } + if ( $self->validstate( $p_state ) ) { + ::print_log( "[BondHome::Device] Received request " . $p_state . " for " . $self->get_object_name ); + $self->SUPER::set( $p_state, $p_setby ); + $$self{parent}->sendcmd( $self, normalize($p_state), $class ); + } + else { + ::print_log( "[BondHome::Device] Received INVALID request " . $p_state . " for " . $self->get_object_name ); + } +} + +sub validstate { + my ( $self, $p_state ) = @_; + + foreach my $state ( @{ $$self{states} } ) { + if ( normalize($state) eq $p_state ) { + return $p_state; + } + + } + return 0; +} + +sub getcmd { + my ( $self, $cmd ) = @_; + + foreach my $state ( @{ $$self{states} } ) { + if ( normalize($state) =~ /$cmd/ ) { + return normalize($state); + } + } + return 0; +} + + +sub updatestates { + my ( $self, $class ) = @_; + @{ $$self{states} } = $$self{parent}->getdevstates( $self, $class ); +} + + +sub normalize { + my ( $string ) = @_; + $string = lc $string; + $string =~ s/ //g; + return $string; +} + + +=back + +=head1 B + +=head2 SYNOPSIS + +User code: + + $LivingRoomFan = new BondHome_Manual('BondHome'); + + Wherein the format for the definition is: + $BondHomeManualObject = new BondHome_Manual(INSTANCE); + + Add discovered commands with: + $LivingRoomFan->addcmd('power', '434000', 'OOK', 'cq', '1000', '12', '110100110110H'); + + Wherein the format for the definition is: + $BondHomeManualObject->addcmd(CommandName, Frequency, Modulation, Encoding, Bps, Reps, Data); + +mht file code: + + BONDHOME_MANUAL, LivingRoomFan, BondHome + + Wherein the format for the definition is: + BONDHOME_MANUAL, ObjectName, INSTANCE + + Add discovered commands with: + BONDHOME_MANUAL_CMD, LivingRoomFan, power, 434000, OOK, cq, 1000, 12, 110100110110H + + Wherein the format for the definition is: + BONDHOME_MANUAL_CMD, BondHomeManualObject, CommandName, Frequency, Modulation, Encoding, Bps, Reps, Data + +States: +Created from the BondHome addcmd sub routine + + +See C for a more detailed description of the arguments. + + +=head2 DESCRIPTION + + Configures a device from the BondHome to be controlled by MH. + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + +package BondHome_Manual; +@BondHome_Manual::ISA = ('Generic_Item'); + +=item C + +Instantiates a new object. + +$instance = The instance of the parent BondHome hub object that this device is found on + + +=cut + +use Data::Dumper; +use JSON; + +sub new { + my ( $class, $instance ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{parent} = BondHome::get_object_by_instance($instance); + $$self{parent}->register( $self, $class ); + @{$$self{states}} = (' '); + + return $self; +} + + +sub set { + my ( $self, $p_state, $p_setby, $p_response ) = @_; + + $p_state = normalize($p_state); + if ( exists $self->{devicehash}->{commands}->{name}->{$p_state} ) { + ::print_log( "[BondHome::Manual] Received request " . $p_state . " for " . $self->get_object_name ); + + my $content; + $content->{freq} = $self->{devicehash}->{commands}->{name}->{$p_state}->{freq}; + $content->{modulation} = $self->{devicehash}->{commands}->{name}->{$p_state}->{modulation}; + $content->{data} = $self->{devicehash}->{commands}->{name}->{$p_state}->{data}; + $content->{encoding} = $self->{devicehash}->{commands}->{name}->{$p_state}->{encoding}; + $content->{bps} = $self->{devicehash}->{commands}->{name}->{$p_state}->{bps}; + $content->{reps} = $self->{devicehash}->{commands}->{name}->{$p_state}->{reps}; + $content->{use_scan}='false'; + + $content = encode_json($content); + #::print_log( "[BondHome::Manual] content: $content" ); + $$self{parent}->bondcmd( $class, '/v2/signal/tx', 'PUT', $content ); + + $self->SUPER::set( $p_state, $p_setby ); + + } + else { + ::print_log( "[BondHome::Manual] Received INVALID request " . $p_state . " for " . $self->get_object_name ); + } +} + + +sub addcmd { + my ($self, $cmdname, $frequency, $modulation, $encoding, $bps, $reps, $data) = @_; + + #::print_log "[BondHome::Manual] ". Dumper Dumper $self; + ::print_log( "[BondHome::Manual] adding new command $cmdname" ); + $cmdname = normalize($cmdname); + $self->{devicehash}->{commands}->{name}->{$cmdname}->{modulation} = $modulation; + $self->{devicehash}->{commands}->{name}->{$cmdname}->{data} = $data; + $self->{devicehash}->{commands}->{name}->{$cmdname}->{freq} = $frequency; + $self->{devicehash}->{commands}->{name}->{$cmdname}->{encoding} = $encoding; + $self->{devicehash}->{commands}->{name}->{$cmdname}->{bps} = $bps; + $self->{devicehash}->{commands}->{name}->{$cmdname}->{reps} = $reps; + + $self->{ 'cmdtimer' } = ::Timer::new(); + $self->{ 'cmdtimer' }->set( + 5, + sub { $self->updatestates; } + ); + +} + + +sub updatestates { + my ( $self ) = @_; + my @speeds; + foreach my $command (keys %{$self->{devicehash}->{commands}->{name}} ) { + push @{ $$self{states} }, ucfirst($command); + if ( normalize($command) =~ /speed(\d)/ ) { + push @speeds, $1; + } + } + @speeds = sort @speeds; + push @{ $$self{states} }, @speeds; + +} + + +sub normalize { + my ( $string ) = @_; + $string = lc $string; + $string =~ s/ //g; + return $string; +} + +=back + +=head2 INI PARAMETERS + +=head2 NOTES + +=head2 AUTHOR + +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/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 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..a17ae9947 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 @@ -423,6 +449,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 @@ -1367,30 +1411,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 diff --git a/lib/MySensors.pm b/lib/MySensors.pm old mode 100755 new mode 100644 index d4b8f1012..3fdc6eda3 --- a/lib/MySensors.pm +++ b/lib/MySensors.pm @@ -2,6 +2,8 @@ # Interface Package # ##################### +=pod + =head1 B =head2 SYNOPSIS @@ -12,20 +14,36 @@ The current version supports Ethernet and serial gateways. The interface must be defined with 3 parameters: a type (serial or ethernet), an address (/dev/tty or an IP address:TCP port number) and a name used to -easily identify the interface. +easily identify the interface. In an MHT file the order is name, type, +address and (optionally) groups. The node must be defined with 3 parameters: a node ID, name and gateway -object. +object. In an MHT file the order is node ID, object, then name, gateway, +and (optionally) groups. The sensors must also be defined with 3 parameters: a sensor ID, name and -node object. +node object. In an MHT file the order is node ID, object, then name, node, +and (optionally) groups. Debugging information can be enabled by setting: debug=MySensors in a Misterhouse INI file. -In user code: +Define objects in an MHT file: + +# MYS gateways +MYS_INTERFACE, Basement_GW, Basement Gateway, serial, /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AJ03J18F-if00-port0, Basement + +# MYS nodes and sensors +MYS_NODE, 0, Basement_GW_ND, Basement Gateway Node, $Basement_GW, Basement +MYS_BINARY, 1, Humidifier_Flush_Pump, Humidifier Flush Pump, $Basement_GW_ND, Basement + +MYS_NODE, 2, Basement_MS_ND, Basement Motion Sensor Node, $Basement_GW, Basement +MYS_MOTION, 0, Basement_Laundry_MS, Basement Laundry Motion Sensor, $Basement_MS_ND, Basement +MYS_MOTION, 1, Downstairs_Hallway_MS, Downstairs Hallway Motion Sensor, $Basement_MS_ND, Basement + +or alternatively in user code: $basement_gateway = new MySensors::Interface(serial, "/dev/ttyACM0", "Basement Gateway"); $media_room_gateway = new MySensors::Interface(ethernet, "192.168.0.22:5003", "Media Room Gateway"); @@ -33,7 +51,10 @@ In user code: $bedroom_node = new MySensors::Node(1, "Bedroom Node", $media_room_gateway); $bedroom_motion = new MySensors::Motion(1, "Bedroom Motion", $bedroom_node); - if (state_now($bedroom_motion) eq ON) { print_log "Motion detected in the bedroom" }; + +Then to use the objects treat them as you would any other object based on a Generic_Item: + + if (state_now($bedroom_motion) eq motion) { print_log "Motion detected in the bedroom" }; =head2 DESCRIPTION @@ -48,28 +69,40 @@ Maximum payload is 25 bytes Note that in MySensor terms the interface is known as a gateway, the sensor radio is known as a node and the sensors themselves are known as children. -Currently supports MySensors release 2.0 +Currently supports MySensors release 2.x -Last modified: 2016-09-14 to fix some motion sensor bugs +Last modified: 2018-02-08 to add custom sensors and update POD Known Limitations: -1. The current implementation does not distinguish SET/REQ and treats them all -as SET -2. The current implementaton handles only a small number of the most common -sensor types. More may be added in the future. -3. The current implementation does not distinguish SET/REQ subtypes which -means any sensor that sends multiple subtypes will behave unpredictably. -4. The current implementation assumes all subtypes are read/write. This may -cause problems if an input is written to. For example, writing to most input -pins will enable/disable the internal pullup resistor. While this may be -desirable in some cases it could result in unexpected behavior. -5. Minimal error trapping is done so errors in configuration or incompatible + +=over + +=item 1. Does not distinguish incoming SET/REQ and treats them all as SET + +=item 2. Handles only a small number of the most common sensor types. More may be +added in the future. + +=item 3. Does not distinguish SET/REQ subtypes for a single sensor which means any +sensor that sends multiple subtypes will behave unpredictably + +=item 4. Assumes all subtypes are read/write which may cause problems if an input +is written to. For example, writing to most input pins will enable/disable +the internal pullup resistor. While this may be desirable in some cases it +could result in unexpected behavior. +=cut + +=item 5. Minimal error trapping is done so errors in configuration or incompatible sensor implementations could cause unpredictable behavior or even crash Misterhouse. -6. The current implementation does not use ACKs -7. The current implementation does not handle units (or requests for units) -8. The current implementation does not attempt to reconnect any port or socket -disconnection + +=item 6. Does not handle units (or requests for units) + +=item 7. Does not attempt to reconnect any port or socket disconnection + +=item 8. Does not handle reloads so a restart might be required if files have +changed + +=back =head2 INHERITS @@ -86,6 +119,7 @@ package MySensors::Interface; use parent 'Generic_Item'; use strict; +use DateTime; # API details as of release 2.0 # For more information see: https://www.mysensors.org/download/serial_api_20 @@ -465,6 +499,20 @@ sub parse_message { "[MySensors] INFO: $$self{name} received battery level $data% from $$self{nodes}{$node_id}{name} (node ID: $node_id) child ID $child_id" ) if $::Debug{mysensors}; + # Handle time requests. Note that the time returned to MyS devices must be in local time but $Time is UTC. + } + elsif ( $subtype == 1 ) { + + # Time needs to be local to controller timezone so use the DateTime library to convert this + my $dt = DateTime->now(); + my $tz = DateTime::TimeZone->new( name => "local" ); + $dt->add( seconds => $tz->offset_for_datetime($dt) ); + + $self->send_message( $node_id, 255, 3, 0, 1, $dt->epoch ); + &::print_log( + "[MySensors] INFO: $$self{name} received time request from $$self{nodes}{$node_id}{name} (node ID: $node_id) child ID $child_id. Responded with time $main::Time." + ) if $::Debug{mysensors}; + # Handle heartbeat responses. This is used to update the state log, and thus the idle_time, of an object. } elsif ( $subtype == 22 ) { @@ -536,12 +584,26 @@ sub send_message { return 0; } +=back + +=head2 CHILD PACKAGES + +The following are child packages to the interface + +All varieties of sensors are children of the MySensors::Sensor + +=cut + ################ # Node Package # ################ -# Note that the nodes are also Generic_Items not MySensors::Interfaces. This -# is similar to the Insteon design but not X10. +=head3 NODE PACKAGE + +Note that the nodes are also Generic_Items not MySensors::Interfaces. This +is similar to the Insteon design but not X10. + +=cut package MySensors::Node; @@ -549,6 +611,8 @@ use strict; use parent 'Generic_Item'; +=over + =item C Instantiates a new node. @@ -581,6 +645,8 @@ Adds a new child sensor to a node. Returns zero for success or the failed child_id otherwise. +=back + =cut sub add_sensor { @@ -604,7 +670,11 @@ sub add_sensor { # Sensor Package # ################## -# All varieties of sensors are children of the MySensors::Sensor +=head3 SENSOR PACKAGE + +All sensors are children of the sensor package + +=cut package MySensors::Sensor; @@ -612,6 +682,8 @@ use strict; use parent 'Generic_Item'; +=over + =item C Instantiates a new sensor. @@ -677,12 +749,10 @@ sub convert_data_to_state { if ( exists $$self{data_to_state}{$data} ) { $state = $$self{data_to_state}{$data}; - # Some sensors return numerical values and for these the state and data are the same } - elsif (( $$self{type} == 0 ) - || ( $$self{type} == 1 ) - || ( $$self{type} == 3 ) ) - { + + # Assume all other sensors return numerical values and for these the state and data are the same + else { $state = $data; } @@ -706,12 +776,10 @@ sub convert_state_to_data { if ( exists $$self{state_to_data}{$state} ) { $data = $$self{state_to_data}{$state}; - # Some sensors return numerical values and for these the state and data are the same } - elsif (( $$self{type} == 0 ) - || ( $$self{type} == 1 ) - || ( $$self{type} == 3 ) ) - { + + # Assume all other sensors return numerical values and for these the state and data are the same + else { $data = $state; } @@ -763,6 +831,8 @@ interface. Returns state +=back + =cut sub set_receive { @@ -789,16 +859,24 @@ sub set_receive { # Door Package # ################ +=head3 DOOR PACKAGE + +=cut + package MySensors::Door; use strict; use parent-norequire, 'MySensors::Sensor'; +=over + =item C Instantiates a new door/window sensor. +=back + =cut sub new { @@ -827,16 +905,24 @@ sub new { # Motion Sensor Package # ######################### +=head3 MOTION SENSOR PACKAGE + +=cut + package MySensors::Motion; use strict; use parent-norequire, 'MySensors::Sensor'; +=over + =item C Instantiates a new motion sensor. +=back + =cut sub new { @@ -865,16 +951,24 @@ sub new { # Light Package # ################# +=head3 LIGHT PACKAGE + +=cut + package MySensors::Light; use strict; use parent-norequire, 'MySensors::Sensor'; +=over + =item C Instantiates a new light. +=back + =cut sub new { @@ -903,16 +997,24 @@ sub new { # Binary Package # ################## +=head3 BINARY PACKAGE + +=cut + package MySensors::Binary; use strict; use parent-norequire, 'MySensors::Sensor'; +=over + =item C Instantiates a new binary sensor. This is an alias for a light. +=back + =cut sub new { @@ -941,16 +1043,24 @@ sub new { # Temperature Package # ####################### +=head3 TEMPERATURE PACKAGE + +=cut + package MySensors::Temperature; use strict; use parent-norequire, 'MySensors::Sensor'; +=over + =item C Instantiates a new temperature sensor. +=back + =cut sub new { @@ -974,16 +1084,24 @@ sub new { # Humidity Package # #################### +=head3 HUMIDITY PACKAGE + +=cut + package MySensors::Humidity; use strict; use parent-norequire, 'MySensors::Sensor'; +=over + =item C Instantiates a new humidity sensor. +=back + =cut sub new { @@ -1003,6 +1121,88 @@ sub new { return $self; } +################## +# Custom Package # +################## + +=head3 CUSTOM PACKAGE + +=cut + +package MySensors::Custom; + +use strict; + +use parent-norequire, 'MySensors::Sensor'; + +=over + +=item C + +Instantiates a new custom sensor. + +=back + +=cut + +sub new { + my $class = shift; + + # Instantiate as a MySensors::Sensor first + my $self = $class->SUPER::new(@_); + + # Custom are presentation type 23 + $$self{type} = 23; + + # Custom type sensors use the V_CUSTOM subtype + $$self{subtypes} = [48]; + + # Note: there are no predefined states or state mappings for custom sensors + + return $self; +} + +###################### +# Multimeter Package # +###################### + +=head3 MULTIMETER PACKAGE + +=cut + +package MySensors::Multimeter; + +use strict; + +use parent-norequire, 'MySensors::Sensor'; + +=over + +=item C + +Instantiates a new multimeter sensor. + +=back + +=cut + +sub new { + my $class = shift; + + # Instantiate as a MySensors::Sensor first + my $self = $class->SUPER::new(@_); + + # Multimeter are presentation type 30 + $$self{type} = 30; + + # Multimeter type sensors use the V_IMPEDANCE, V_VOLTAGE and V_CURRENT subtypes + $$self{subtypes} = [ 14, 38, 39 ]; + + # Note: there are no predefined states or state mappings for multimeter sensors + + return $self; +} + =head2 AUTHOR Jeff Siddall (news@siddall.name) @@ -1016,3 +1216,4 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. =cut + diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index 575f0ed48..f8851ae6b 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -1,6 +1,6 @@ package Nanoleaf_Aurora; -# v1.0.15 +# 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 @@ -31,6 +31,7 @@ use IO::Socket::INET; # the location URL and tokens are stored in the mh.ini file # Firmware supported +# 2.2.0 - needs v1.1 # 1.5.0 to 2.1.3 - yes # 1.4.39 - pass the option api=beta # 1.4.38 or earlier - no @@ -80,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"; @@ -96,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 '{}'"; @@ -121,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.03"; $self->{ssdp_timeout} = 4000; $self->{last_static} = ""; @@ -156,14 +164,15 @@ 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} ); - &::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%"; } @@ -310,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}; @@ -700,6 +715,13 @@ 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} . "] 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} ); @@ -740,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} ) { @@ -993,11 +1015,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 { @@ -1037,6 +1059,14 @@ sub check_static { return ('1'); } +sub is_rhythm_effect { + my ( $self) = @_; + my $return = 0; + $return = 1 if ($self->{data}->{info}->{rhythm}->{rhythmActive}); + + return $return; +} + sub print_static { my ($self) = @_; @@ -1076,40 +1106,43 @@ sub identify { sub generate_voice_commands { my ($self) = @_; - my $object_string; - my $object_name = $self->get_object_name; - &main::print_log("Generating Voice commands for Nanoleaf Aurora Controller $object_name"); + 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 ) { + 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"; + #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/_/ /; + #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"; + #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"; + #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++; - } + #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 $@; + #Evaluate the resulting object generating string + package main; + eval $object_string; + print "Error in nanoleaf_aurora_item_commands: $@\n" if $@; - package Nanoleaf_Aurora; + package Nanoleaf_Aurora; + } } sub get_voice_cmds { @@ -1295,4 +1328,6 @@ sub set { # 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 \ No newline at end of file +# v1.0.14 - commands now queue properly +# v1.0.15 - fixed polling +# v1.1.03 - fixed a few typos diff --git a/lib/PLCBUS.pm b/lib/PLCBUS.pm index 1d710babd..b938d34f0 100644 --- a/lib/PLCBUS.pm +++ b/lib/PLCBUS.pm @@ -240,7 +240,7 @@ my %cmd_to_hex = ( cmd => 0x1D, flags => 0x00, data => 0, - expected_response => ['report_only_on_pulse'], + expected_response => ['report_all_id_pulse', 'report_only_on_pulse'], home_cmd => 1, description => "(THE SAME USER AND THE SAME HOME) Check the Only ON ID PULSE in the same USER & HOME." }, @@ -606,7 +606,7 @@ sub _handle_REPORT_ALL_ID_PULSE($$$) { } } _logdd( "$home report_all_id_pulse 'd1d2': '" . bin_rep($d1) . bin_rep($d2) . "'" ); - _log("$home present: $exist"); + _log("$home present: '$exist'"); } sub _check_external_plcbus_command_file() { @@ -658,9 +658,9 @@ sub _is_current_command_complete() { } my $current = $self->{current_cmd}; my $ok = 1; - my $what; + my $what = ""; if ( !$current->{echo_seen} ) { - $what = $what . "'echo' "; + $what .= "'echo' "; $ok = 0; } if ( $current->{waits_for_ack} && !$current->{ack_seen} ) { @@ -691,10 +691,9 @@ sub _is_current_command_complete() { } if ( ( $current->{cmd} eq "on" or $current->{cmd} eq "off" ) - and $current->{three_phase} == 0 and $current->{ack_seen} ) { - _logdd("Considering 1-Phase command completed, because ACK was received"); + _logdd("Considering command completed, because ACK was received"); $ok = 1; $current->{completed} = 1; my $module = @@ -1156,7 +1155,7 @@ sub decode_rx_tx_switch($$$$) { sub _split_homeunit { my ($address) = @_; die("$address is not a valid PLCBUS home unit address") - unless ( $address =~ /^([A-O])([0-9]{1,2})$/ ); + unless ( $address =~ /^([A-P])([0-9]{1,2})$/ ); return ( $1, $2 ); } @@ -1176,9 +1175,11 @@ sub get_cmd_list($) { my $homes = (); sub generate_code(@) { - my ( $self, $type, $address, $name, $grouplist, $scene_list ) = @_; + my ( $self, $type, $address, $name, $grouplist, $scene_list, $totaltime ) = @_; my ( $home, $unit ) = _split_homeunit($address); + _log("Got @_"); + my $home_name = "PLCBUS_$home"; $grouplist = "" unless $grouplist; $scene_list = "" unless $scene_list; @@ -1259,11 +1260,15 @@ sub generate_code(@) { $more .= " respond \"setting up preset dim and scenes\";\n"; $more .= " PLCBUS->instance()->setup_house();\n"; $more .= " }\n"; + $more .= " \$PLCBUS_scan_only_on_house = new Voice_Cmd(\"PLCBUS scan house only on\");\n"; + $more .= ::store_object_data( "\$PLCBUS_scan_only_on_house", 'Voice_Cmd', 'PLCBUS_House', 'PLCBUS_House' ); + $more .= " if (my \$status = said \$PLCBUS_scan_only_on_house){\n"; + $more .= " respond \"scanning home codes A .. P for devices in 'on' state\";\n"; + $more .= " PLCBUS->instance()->scan_whole_house_for_on();\n"; + $more .= " }\n"; + $more .= "\n"; } - # if ($more){ - # _logdd($more); - # } return ( $object, $grouplist, $more ); } @@ -1287,7 +1292,14 @@ sub setup_house { sub scan_whole_house { my ($self) = @_; foreach my $home ( "A" .. "P" ) { - $self->queue_command( { home => $home, unit => 0, cmd => 'report_all_id_pulse' } ); + $self->queue_command( { home => $home, unit => 0, cmd => 'get_all_id_pulse' } ); + } +} + +sub scan_whole_house_for_on { + my ($self) = @_; + foreach my $home ( "A" .. "P" ) { + $self->queue_command( { home => $home, unit => 0, cmd => 'get_only_on_id_pulse' } ); } } @@ -1343,6 +1355,7 @@ started by this module plcbussrv_port=4567 debug=plcbus:2|plcbus_module:2 plcbus_logfile=1 + plcbus_setup_voice_cmds=1 =over @@ -1382,6 +1395,14 @@ setting. if set to '0' or omitted, no logfile is created. +=item B + +if set to anything but '0' the module creates a bunch of setup voice comands +to program the devices. Since this may create quite a lot of commands it may +slow down misterhouse. + +if set to '0' or omitted only on/off voice commands are created. + =back =head2 SAMPLE .MHT FILE @@ -1605,7 +1626,11 @@ sub new { $self->_log("Can not parse scenesetting '$s'. Requiered format: Sceneaddress:Dimlevel\@Faderate"); } } - my @default_states = qw|on off bright dim status_req get_noise_strength get_signal_strength all_scenes_addrs_erase|; + my @default_states = qw|on off status_req|; + if ($::config_parms{plcbus_setup_voice_cmds}) + { + push(@default_states, qw |bright dim get_noise_strength get_signal_strength all_scenes_addrs_erase|); + } $self->set_states(@default_states); $self->_logd("ctor $self->{name} home: $self->{home} unit: $self->{unit} scenes: $self->{scenes}"); PLCBUS->instance()->add_device($self); @@ -1617,7 +1642,7 @@ sub new { sub generate_voice_commands { my ($self) = @_; $self->_log("Generating Voice commands"); - my $object_string; + my $object_string = ""; my $name = $self->{name}; my $varlist; @@ -1633,9 +1658,13 @@ sub generate_voice_commands { $object_string .= "$vc_var_name -> tie_event('" . $voice_cmds->{$_}[1] . "');\n"; $object_string .= ::store_object_data( "$vc_var_name", 'Voice_Cmd', 'PLCBUS_Devices', 'PLCBUS_Devices' ); } + + return if $object_string eq ""; + + $object_string = "use vars qw($varlist);\n" . $object_string; - # $self->_log("\n\n$object_string"); + #$self->_log("\n\n$object_string"); #Evaluate the resulting object generating string package main; @@ -1649,16 +1678,26 @@ sub get_voice_cmds { my ($self) = @_; my $object_name = $self->{name}; my $level = '[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]'; - my %voice_cmds = ( - 'change_state' => - [ '[on,off,status req,get signal strength,get noise strength,1 phase,3 phase,use mh ini phase mode]', "\$$object_name->set(\$state)" ], - 'bright_cmd' => [ 'bright ' . $level . '%', "\$$object_name->command(\"bright\", \$state, 1)" ], - 'dim_cmd' => [ 'dim ' . $level . '%', "\$$object_name->command(\"dim\", \$state, 1)" ], - ); - - for ( my $i = 5; $i <= 100; $i += 5 ) { - $voice_cmds{ 'bright_' . sprintf( "%03d", $i ) } = - [ 'presetdim to ' . $i . '% within [0,1,2,3,4,5,6,7,8,9,10]s', "\$$object_name->preset_dim_from_voice_cmd($i, \$state)" ]; + my %voice_cmds = (); + if ($::config_parms{plcbus_setup_voice_cmds}) + { + %voice_cmds = ( + 'change_state' => + [ '[on,off,status req,get signal strength,get noise strength,1 phase,3 phase,use mh ini phase mode]', "\$$object_name->set(\$state)" ], + 'bright_cmd' => [ 'bright ' . $level . '%', "\$$object_name->command(\"bright\", \$state, 1)" ], + 'dim_cmd' => [ 'dim ' . $level . '%', "\$$object_name->command(\"dim\", \$state, 1)" ], + ); + + for ( my $i = 5; $i <= 100; $i += 5 ) { + $voice_cmds{ 'bright_' . sprintf( "%03d", $i ) } = + [ 'presetdim to ' . $i . '% within [0,1,2,3,4,5,6,7,8,9,10]s', "\$$object_name->preset_dim_from_voice_cmd($i, \$state)" ]; + } + } + else + { + %voice_cmds = ( + 'change_state' => [ '[on,off]', "\$$object_name->set(\$state)" ], + ); } return \%voice_cmds; @@ -1836,6 +1875,30 @@ sub setup { package PLCBUS_LightItem; @PLCBUS_LightItem::ISA = ('PLCBUS_Item'); +sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + my @states = qw|on off status_req|; + if ($::config_parms{plcbus_setup_voice_cmds}) + { + push(@states, qw |bright dim get_noise_strength get_signal_strength all_scenes_addrs_erase set_default_brightness|); + } + $self->set_states(@states); + return $self; +} + +sub set { + my ( $self, $new_state, $setby, $respond ) = @_; + my $home = $self->{home}; + my $unit = $self->{unit}; + $new_state =~ s/ /_/g; + if ( $new_state eq 'set_default_brightness' ) { + $self->setup(); + return; + } + PLCBUS_Item::set(@_); +} + sub setup { my ($self) = @_; my $home = $self->{home}; @@ -1863,13 +1926,6 @@ package PLCBUS_2263; package PLCBUS_2268; @PLCBUS_2268::ISA = ('PLCBUS_Item'); -sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - $self->set_states(qw |on off|); - return $self; -} - package PLCBUS_Shutter; @PLCBUS_Shutter::ISA = ('PLCBUS_Item'); @@ -1908,6 +1964,24 @@ sub _set { PLCBUS_Item::_set( $self, $new_state, $setby, $respond ); } +sub get_voice_cmds +{ + my ($self) = @_; + my %voice_cmds = (); + if ($::config_parms{plcbus_setup_voice_cmds}) + { + return PLCBUS_Item::get_voice_cmds($self); + } + else + { + my %voice_cmds = + ( + 'change_state' => [ '[up,down]', "\$".$self->{name}."->set(\$state)" ], + ); + return \%voice_cmds; + } +} + package PLCBUS_Scene; @PLCBUS_Scene::ISA = ('PLCBUS_Item'); use Data::Dumper qw(Dumper); @@ -1963,7 +2037,10 @@ sub get_voice_cmds { my $unit = $self->{unit}; my %voice_cmds = (); - $voice_cmds{ 'program_sceneaddr_' . $object_name } = [ 'program scene', "\$$object_name->program_sceneaddr()" ]; + if ($::config_parms{plcbus_setup_voice_cmds}) + { + $voice_cmds{ 'program_sceneaddr_' . $object_name } = [ 'program scene', "\$$object_name->program_sceneaddr()" ]; + } return \%voice_cmds; } diff --git a/lib/Tasmota_HTTP_Item.pm b/lib/Tasmota_HTTP_Item.pm new file mode 100644 index 000000000..6bdee37e9 --- /dev/null +++ b/lib/Tasmota_HTTP_Item.pm @@ -0,0 +1,170 @@ + +=begin comment + +Tasmota_HTTP_Item.pm + +Basic Tasmota support using the HTTP interface rather than MQTT +Copyright (C) 2020 Jeff Siddall (jeff@siddall.name) +Last modified: 2020-12-15 + +This module currently supports Tasmota switch type devices but other devices +can be added with extra packages added + +Requirements: + + The Tasmota device needs to be setup with a rule to send HTTP requests to MH + if two-way communication is desired. For example, a Sonoff Mini switch input + can be sent to MH with the rule: + Rule1 ON Power1#State DO WebSend [192.168.0.1:80] /SET;none?select_item=Kitchen_Light&select_state=%value% ENDON + +Setup: + +In your code define Tasmota_HTTP::Things in an MHT: + + TASMOTA_HTTP_SWITCH, 192.168.x.y, Kitchen_Light, Kitchen + +Or in a code file: + + $Kitchen_Light = new Tasmota_HTTP::Switch("192.168.x.y"); + Where: + 192.168.x.y is the IPv4 address or hostname of the Tasmota device + + $Kitchen_Light->set(ON); + +=cut + +#======================================================================================= +# +# Generic Tasmota_HTTP::Item +# +#======================================================================================= + +# The Tasmota_HTTP::Item is a base item for other real devices (see below) + +package Tasmota_HTTP::Item; +use strict; +use parent 'Generic_Item'; + +# Item class constructor +sub new { + my ( $class, $address ) = @_; + + # Call the parent class constructor to make sure all the important things are done + my $self = new Generic_Item(); + bless $self, $class; + + # Additional Tasmota variables + $self->{address} = $address; + $self->{output_name} = 'POWER1'; + $self->{ack} = 0; + $self->{last_http_status}; + + return $self; +} + +# Use HTTP get calls to set the Tasmota item, being sure to check that the set did not come +# from the device itself +sub set { + my ( $self, $state, $set_by, $respond ) = @_; + + # Debug logging + my $debug = $self->{debug} || $main::Debug{tasmota}; + + # Determine whether the update came from the Tasmota device itself and convert states + # and record the set as an ack + if ( $set_by eq "web [$self->{address}]" ) { + + # Convert Tasmota states to MH states + $state = $self->{tasmota_to_state}{$state}; + + # If the current state is the same as the received state, and ack=0 then consider + # this set an ack and do not update the state of the item + if ( ( $state eq $self->{state} ) && ( $self->{ack} == 0 ) ) { + &main::print_log("[Tasmota_HTTP::Item] DEBUG: Received ack from $self->{object_name} ($self->{address})") if $debug; + $self->{ack} = 1; + } + else { + &main::print_log("[Tasmota_HTTP::Item] DEBUG: Received set state to $state from $self->{object_name} ($self->{address})") if $debug; + + # Call the parent class set to make sure all the important things are done + $self->SUPER::set( $state, $set_by, $respond ); + } + + # Only send an update to the device if the set did not come from the device to prevent + # set loops + } + else { + use LWP::UserAgent (); + + # Use a small timeout since devices are typically local and should respond quickly + # 5 seconds should allow for 3 syn attempts plus another second to get a response + my $ua = LWP::UserAgent->new( timeout => 5 ); + + # Reset the ack flag + $self->{ack} = 0; + + # Send the HTTP request + my $response = $ua->get("http://$self->{address}/cm?cmnd=$self->{output_name}%20$self->{state_to_tasmota}{$state}"); + + # Record the status of the last request + $self->{last_http_status} = $response->status_line; + + # Log request failures + if ( !$response->is_success ) { + &main::print_log("[Tasmota_HTTP::Item] ERROR: Received HTTP response code $self->{last_http_status} from last command)"); + } + + # Call the parent class set to make sure all the important things are done + $self->SUPER::set( $state, $set_by, $respond ); + &main::print_log("[Tasmota_HTTP::Item] DEBUG: Set $self->{object_name} state to $state") if $debug; + } +} + +#======================================================================================= +# +# Basic Tasmota_HTTP::Switch +# +#======================================================================================= + +# To add table support, add these lines to the read_table_A.pl file: +# elsif ( $type eq "TASMOTA_HTTP_SWITCH" ) { +# require Tasmota_HTTP_Item; +# ( $address, $name, $grouplist ) = @item_info; +# $object = "Tasmota_HTTP::Switch('$address')"; +# } + +package Tasmota_HTTP::Switch; +use strict; +use parent-norequire, 'Tasmota_HTTP::Item'; + +# Switch class constructor +sub new { + my $class = shift; + + # Call the parent class constructor to make sure all the important things are done + my $self = $class->SUPER::new(@_); + + # Additional switch variables + # Add additional hash pairs (rows) to this variable to send other states to devices + $self->{state_to_tasmota} = { + "off" => "0", + "on" => "1", + }; + + # Add additional hash pairs (rows) to this variable to use other states from devices + $self->{tasmota_to_state} = { + "0" => "off", + "1" => "on", + }; + + # Initialize states + push( @{ $self->{states} }, keys( %{ $self->{state_to_tasmota} } ) ); + + # Log the setup of the item + &main::print_log("[Tasmota_HTTP::Switch] Created item with address $self->{address}"); + + return $self; +} + +# Perl modules need to return true +1; diff --git a/lib/Timer.pm b/lib/Timer.pm index 5aa5f94c2..9d4f3b5ef 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,11 @@ 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) unless ($self->{state_updating} == 1); + $self->{state_updating} = 1; + } + # Set states for NEXT pass, so expired, active, etc, # checks are consistent for one pass. push @sets_from_previous_pass, $self; @@ -215,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 @@ -254,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 { diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 269d90bfa..2ec3a35c6 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -176,7 +176,7 @@ sub _init { $self->{active} = 1; $self->{previous}->{tempunits} = $self->{data}->{tempunits}; $self->{previous}->{name} = $self->{data}->{name}; - foreach my $key1 ( keys $self->{data}->{info} ) { + foreach my $key1 ( keys %{$self->{data}->{info}} ) { $self->{previous}->{info}->{$key1} = $self->{data}->{info}->{$key1}; } $self->{previous}->{sensors}->{sensors}[0]->{temp} = $self->{data}->{sensors}->{sensors}[0]->{temp}; @@ -270,7 +270,7 @@ sub process_check { $file_data = "" unless ($file_data); #just to prevent warning messages #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} ); - my ($json_data) = $file_data =~ /({.*})/; + my ($json_data) = $file_data =~ /(\{.*\})/; $json_data = "" unless ($json_data); #just to prevent warning messages print "debug: json_data=$json_data\n" if ( $self->{debug} ); unless ( ($file_data) and ($json_data) ) { @@ -288,7 +288,7 @@ sub process_check { $com_status = "offline"; } else { - if ( keys $data ) { + if ( keys %{$data} ) { $self->{poll_data_timestamp} = &main::get_tickcount(); if ( $self->{poll_process_mode} eq "info" ) { $self->{data}->{tempunits} = $data->{tempunits}; @@ -345,7 +345,7 @@ sub process_check { } #for some reason get_url adds garbage to the output. Clean out the characters before and after the json - my ($json_data) = $file_data =~ /({.*})/; + my ($json_data) = $file_data =~ /(\{.*\})/; my $data; eval { $data = JSON::XS->new->decode($json_data); }; @@ -356,7 +356,7 @@ sub process_check { } else { - if ( keys $data ) { + if ( keys %{$data} ) { if ( $data->{success} eq "true" ) { shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful $self->{cmd_process_retry} = 0; @@ -1586,11 +1586,14 @@ sub get_units { } sub get_temp { - my ($self) = @_; + my ($self, $index) = @_; + + $index=0 unless ( defined $index ); + # my ($isSuccessResponse) = $self->poll; # if ($isSuccessResponse) { - return ( $self->{data}->{sensors}->{sensors}[0]->{temp} ); + return ( $self->{data}->{sensors}->{sensors}[$index]->{temp} ); # } else { # return ("unknown"); 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 diff --git a/lib/Yeelight.pm b/lib/Yeelight.pm new file mode 100644 index 000000000..468774af3 --- /dev/null +++ b/lib/Yeelight.pm @@ -0,0 +1,1018 @@ +=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 +# 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 + +=head2 Firmware supported +# led strip (stripe) : 44 + +=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 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 + +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. + +=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... +- comm device offline, didn't go online when data came back +- lost data and didn't reconnect +- check CPU usage for yeelight + +=cut +our $yl_instances; + +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'); + +# -------------------- 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{bright}} = ('"smooth"',500); +@{$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, $location, $options ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + + 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; + + $self->{updating} = 0; + $self->{data}->{retry} = 0; + $self->{status} = ""; + $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} ); + + $self->{socket_connected} = 0; + $self->{host} = $location; + $self->{port} = 55443; + $self->{brightness_state_delay} = 1; + $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 =~ /(.*):(.*)/; + } + + $options = "" unless ( defined $options ); + $options = $::config_parms{ "yeelight_" . $self->{name} . "_options" } if ( $::config_parms{ "yeelight_" . $self->{name} . "_options" } ); + + $self->{debug} = 0; + ( $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->{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"; + 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 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'); + $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 ); + #push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); + push( @{ $$self{states} }, 'off'); + for my $i (0..100) { push @{ $$self{states} }, "$i%"; } + push( @{ $$self{states} }, 'on'); + $self->{timer} = new Timer; + $self->get_data(); + return $self; +} + +sub check_for_socket_data { + my ($self) = @_; + +# Other objects use a socket in $Socket_Items? +# $NewCmd = $Socket_Items{$instance}{'socket'}->said; + + my $com_status = "offline"; + 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); + 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]" ) if ( $self->{debug} ); + + 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}) and ($self->{socket_connected})) { + 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} ) { + 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} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } +} + +sub get_data { + my ($self) = @_; + my $com_status = "online"; + + 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. Starting Socket listener..." ); + $active_yeelights{ $self->{host} } = 1; + $self->print_info(); + $self->{init} = 1; + $self->process_data(); + $self->{data_socket}->start(); + } + + } + 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 ) 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} ); + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } + +} + +sub process_check { + my ($self) = @_; + my $com_status = $self->{status}; + + return unless ( defined $self->{poll_process} ); + + if ( $self->{poll_process}->done_now() ) { + + @{ $self->{poll_queue} } = (); #clear the queue since process is done. + + $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 query" ); + 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"; + } + } + + } + + 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} ); + $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" ); + ${ $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 + $com_status = "online"; + + } else { + 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} } ) 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' ); + } + } + } +} + +#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\":["; + $cmdline .= join(',', @{$param_array{$mode}}); + $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(); + $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 { + main::print_log( "[Yeelight:" . $self->{name} . "] Query request already in progress" ) + } +} + +#command process +sub _push_TCP_data { + my ( $self, $mode, @params ) = @_; + + my $cmdline = "{ \"id\":" . $self->{id} . ", \"method\":" . $method{$mode} . ", \"params\":["; + $cmdline .= join(',',@params); + $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); + $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::Time,0]; + + 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::Time,0]; + } + else { + 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' ); +# } +# } +# } + } + } +} + +sub get_ssdp_data { + my ( $self, $id, $timeout ) = @_; + + 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}!"); + $self->{data}->{info} = $data->{$self->{host}}; + + } 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); + 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; + &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+(.*)/; + 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 ) = @_; + + $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" ); + +} + +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} . "] \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} ); + 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 changed + + 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}->{bright} to $self->{data}->{info}->{bright}" ) if ( $self->{loglevel} ); + $self->{previous}->{info}->{bright} = $self->{data}->{info}->{bright}; + $self->set( $self->{data}->{info}->{bright}, 'poll' ); + } + +#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}; + } + + 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 ( $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' ); + } + } + +} + +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 ) { + 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} . "] ------------------------------------------------------------------" ); + +} + +sub purge_command_queue { + my ($self) = @_; + my $commands = scalar @{ $self->{cmd_queue} }; + main::print_log( "Yeelight:" . $self->{name} . "] Purging Command Queue of $commands commands" ); + @{ $self->{cmd_queue} } = (); +} + +#------------ +# User access methods + +sub get_debug { + my ($self) = @_; + 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 ) = @_; + + $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 =~ /^(\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 { + my @params = @{$param_array{"bright"}}; + unshift @params, $1; + $self->_push_TCP_data( 'brightness', @params ); + #} + } + elsif ( $mode =~ /^([-+]\d+)/ ) { + my $value = $self->{info}->{bright} + $1; + 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" ); + return ('0'); + } + return ('1'); + } +} + +sub get_rgb { + my ($self) = @_; + 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 ) = @_; + + 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 restart_socket { + my ($self) = @_; + + $self->{data_socket}->stop(); + $self->{data_socket}->start(); +} + +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 Yeelight $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', 'Yeelight', 'Controller_commands' ); + $i++; + } + + #Evaluate the resulting object generating string + package main; + eval $object_string; + print "Error in Yeelight_item_commands: $@\n" if $@; + + package Yeelight; + } +} + +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', + 'Force Yeelight Status query' => $self->get_object_name . '->query_yeelight', + 'Restart Data Socket connection' => $self->get_object_name . '->restart_socket' + ); + + return \%voice_cmds; +} + +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_ColorTemp; + +@Yeelight_ColorTemp::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object) = @_; + + my $self = new Generic_Item(); + bless $self, $class; + for my $i (1700..6500) { push @{ $$self{states} }, "$i"; } + + $$self{master_object} = $object; + $object->register( $self, 'ct' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( $p_state >= 1700 and $p_state <= 6500 ) { + $$self{master_object}->set_ct($p_state) + + } else { + main::print_log("[Yeelight Color Temp] Error. State out of range (1700-6500): $p_state "); + } + } +} + +package Yeelight_Comm; + +@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 - color support +# v1.2.1 - command retry logic 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/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 diff --git a/lib/handy_utilities.pl b/lib/handy_utilities.pl index b07c6df2a..0b4a96710 100644 --- a/lib/handy_utilities.pl +++ b/lib/handy_utilities.pl @@ -1586,6 +1586,288 @@ sub main::get_idle_item_data { return @results; } +sub main::Groups { + use Storable; + my ($function, $group, $user, $type, $cmd, $cmdperm, $line ) = @_; + +my $file = $::config_parms{'data_dir'} . '/groups.data'; +#my $file = '/home/wayne/groups2'; +my @ugroups; +my @users; +my $UserGroups; + + unless ( $UserGroups ) { + $UserGroups = retrieve($file) if ( -e $file ); + } +#dump($UserGroups); + + + +# 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}->{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; + } + } + 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 $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}->{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 ( $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}->{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; + } +return 0; +} + #print " done\n"; 1; diff --git a/lib/http_server.pl b/lib/http_server.pl index fcfe54b6a..4494eb0de 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 @@ -225,6 +224,11 @@ sub http_process_request { if $Http{Host}; # Drop the port, if present $Http{Client_address} = $Socket_Ports{http}{client_ip_address}; + if( $Http{'X-Real-IP'}){ + $client_ip_address = $Http{'X-Real-IP'}; + print "http: 'X-Real-IP' found, override \$client_ip_address to $client_ip_address\n" if $main::Debug{http}; + } + if ( $Http{Cookie} ) { for my $key_value ( split ';', $Http{Cookie} ) { my ( $key2, $value2 ) = $key_value =~ /(\S+)=(\S+)/; @@ -261,6 +265,7 @@ sub http_process_request { if defined $main::config_parms{'ia7_enable'}; my $mobile_html = 0; my $modern_browser = 0; + $Http{'User-Agent-Full'} = $Http{'User-Agent'}; #Keep the original user agent string as MH modifies it through some legacy code. if ( ( $Http{'User-Agent'} =~ /iPhone/i ) or ( $Http{'User-Agent'} =~ /Android/i ) ) { @@ -328,7 +333,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; @@ -340,14 +345,33 @@ sub http_process_request { if $main::Debug{http}; if ( $req_typ eq "POST" || $req_typ eq "PUT" ) { $http_data =~ s/^(POST|PUT).+?^\R//smi; - my $cl = $Http{'Content-Length'}; - print "http POST query has $cl bytes of args\n" if $main::Debug{http}; + my $cl; my $buf; - $cl = $cl - length($http_data); - read $socket, $buf, $cl; - $buf = $http_data.$buf if $http_data; - $http_data = ''; - + if ($Http{'Content-Length'}) { + $cl = $Http{'Content-Length'}; + print "http POST query has $cl bytes of args\n" if $main::Debug{http}; + $cl = $cl - length($http_data); + read $socket, $buf, $cl; + $buf = $http_data.$buf if $http_data; + $http_data = ''; + } elsif ($Http{'Transfer-Encoding'} && ($Http{'Transfer-Encoding'} eq 'chunked')) { + print "http POST query has chunked Transfer-Encoding\n" if $main::Debug{http}; + # We can't read the post body from the socket, so need to get this from $http_data instead + # Note that this only works with one chunk right now. + print "http_data ->$http_data<-\n" if $main::Debug{http}; + if ($http_data =~ s/^([^\015\012]*)\015\012//) { + my $chunk_head = $1; + print "Chunk header = $chunk_head\n" if $main::Debug{http}; + unless ($chunk_head =~ /^([0-9A-Fa-f]+)/) { + print "Bad chunk header\n" if $main::Debug{http}; + } else { + $cl = hex($chunk_head); + print "Reading chunk size = $cl bytes\n" if $main::Debug{http}; + $buf = substr $http_data, 0, $cl; + $http_data = ''; + } + } + } # Save the body into the global var $HTTP_BODY = $buf; print "http POST buf=$buf get_arg=$get_arg\n" if $main::Debug{http}; @@ -362,13 +386,26 @@ 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. + # + # 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}; + } elsif ($Http{'Transfer-Encoding'} && $HTTP_BODY =~ /^\{/) { + print "[http_server.pl]: posting chunked json data\n" if $main::Debug{http}; } else { - &main::print_log("[http_server.pl]: Warning, invalid argument string detected ($buf)\n"); + &main::print_log("[http_server.pl]: Warning, invalid argument string detected ($buf) ($Http{'Content-Type'}) ($HTTP_BODY)\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 +487,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 +509,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 +530,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 +554,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 +581,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 ) ) { } @@ -678,6 +716,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=/; @@ -752,10 +795,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 $@; @@ -824,35 +867,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 = < + Password: + + - # $html .= qq[\n]; - $html .= qq[
\n]; - - # $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 +899,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 +1989,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 @@ -2141,6 +2180,7 @@ sub mime_header { } else { my ($extention) = $file_or_type =~ /.+\.(\S+)$/; + ($extention) = $file_or_type =~ /.+\.(\S+)\.\S\S$/ if ($file_or_type =~ m/\.gz$/i) ; $mime = $mime_types{ lc $extention } || 'text/html'; my $time = ( stat($file_or_type) )[9]; $date = &time2str($time); @@ -2154,6 +2194,7 @@ sub mime_header { $header = "HTTP/1.1 206 Partial Content\r\n" if $range; $header .= "Server: MisterHouse\r\n"; $header .= "Connection: close\r\n" if &http_close_socket; + $header .= "Content-Encoding: gzip\r\n" if ($file_or_type =~ m/\.gz$/i); $header .= "Date: " . time2str(time) . "\r\n"; $header .= "Content-Type: $mime\r\n"; @@ -2266,34 +2307,32 @@ sub html_page { unless $script =~ / script /i; $html = $script . "\n"; } -$html .= " + $html .= < $style $title - $body -"; - - - $html =~ s/\n/\n\r/g; # Bill S. says this is required to be standards compiliant +EOF - $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"; + $html =~ s/\n/\n\r/g; # Bill S. says this is required to be standards compiliant - 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 { @@ -2304,7 +2343,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"; diff --git a/lib/http_server_aog.pl b/lib/http_server_aog.pl new file mode 100644 index 000000000..cfcde2cde --- /dev/null +++ b/lib/http_server_aog.pl @@ -0,0 +1,327 @@ + +=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 { + 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."); + } + + # 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'}; + + if ( -e $::config_parms{'aog_oauth_tokens_file'} ) { + $oauth_tokens = retrieve( $::config_parms{'aog_oauth_tokens_file'} ); + } + + if ( $main::Debug{'aog'} ) { + print STDERR < + +$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 STDERR "[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/ia7_utilities.pl b/lib/ia7_utilities.pl index e99728029..fad7279ef 100644 --- a/lib/ia7_utilities.pl +++ b/lib/ia7_utilities.pl @@ -57,7 +57,7 @@ sub main::ia7_notify { sub ia7_update_collection { &main::print_log("[IA7_Collection_Updater] : Starting"); - my $ia7_coll_current_ver = 1.4; + my $ia7_coll_current_ver = 1.6; my @collection_files = ( "$main::Pgm_Root/data/web/collections.json", @@ -129,7 +129,45 @@ sub ia7_update_collection { $updated = 1; } } - + if ( $json_data->{meta}->{version} < 1.5 ) { + #IA7 v2.0 required change + #Native trigger support /bin/triggers.pl + my $file_data2 = to_json( $json_data, { utf8 => 1, pretty => 1 } ); + $file_data2 =~ s/\"link\"[\s+]:[\s+]\"\/bin\/triggers\.pl\"/\"link\" : \"\/ia7\/\#path\=triggers\"/g; + eval { + $json_data = decode_json($file_data2); #HP, wrap this in eval to prevent MH crashes + }; + if ($@) { + &main::print_log("[IA7_Collection_Updater] : WARNING: decode_json failed for v1.5 update."); + } else { + $json_data->{meta}->{version} = "1.5"; + &main::print_log("[IA7_Collection_Updater] : Updating $file to version 1.5 (MH 5.1 IA7 v2.0.300 native triggers change)"); + $updated = 1; + } + } + if ( $json_data->{meta}->{version} < 1.6 ) { + + my $setup_key = 0; + my $next_key = 0; + foreach my $key (keys %{$json_data}) { + $setup_key = $key if ($json_data->{$key}->{name} eq 'Setup MrHouse'); + $next_key = $key if (($key > $next_key) and ($key < 500)); + } + if ($setup_key and $next_key) { + $next_key++; + &main::print_log("[IA7_Collection_Updater] : Adding new security button (" . $next_key . ") to Setup MrHouse (" . $setup_key . ")"); + $json_data->{meta}->{version} = "1.6"; + $json_data->{$next_key}->{icon} = "fa-users"; + $json_data->{$next_key}->{name} = "Users and Groups"; + $json_data->{$next_key}->{link} = "/ia7/#path=/security"; + push (@{$json_data->{$setup_key}->{children}},$next_key); + &main::print_log("[IA7_Collection_Updater] : Updating $file to version 1.6 (MH 5.1 IA7 v2.0.600 user management)"); + $updated = 1; + } else { + &main::print_log("[IA7_Collection_Updater] : Could not add security key to Setup MrHouse (" . $next_key . ":" . $setup_key . ")"); + } + + } if ($updated) { my $json_newdata = to_json( $json_data, { utf8 => 1, pretty => 1 } ); my $backup_file = $file . ".t" . int( ::get_tickcount() / 1000 ) . ".backup"; diff --git a/lib/json_server.pl b/lib/json_server.pl index 923877e25..79b747630 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; @@ -175,8 +176,11 @@ sub json_put { eval { $body = decode_json($body); #HP, wrap this in eval to prevent MH crashes }; + if ($@ and !$empty_json) { &main::print_log( "Json_Server.pl: WARNING: decode_json failed for json POST!" ); + &main::print_log( "Json_Server.pl: WARNING: Data is $body" ); + $response_code = "HTTP/1.1 500 Internal Server Error\r\n"; $response_text->{status} = "error"; $response_text->{text} = "Failed to decode JSON file"; @@ -250,6 +254,122 @@ sub json_put { $response_text->{status} = "success"; $response_text->{text} = ""; + } elsif ( $path[0] eq 'security' ) { + + &main::print_log( "Json_Server.pl: security post"); + + print Dumper $body; + + if ($body->{pk} eq 'add_group') { + my $members = join(',',@{$body->{members}}); + print("add_group &Groups('add',$body->{name},$members)\n"); + + &Groups('add',$body->{name},$members); + } + if ($body->{pk} eq 'add_user') { + 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"); + + foreach my $group (@{$body->{groups}}) { + &Groups('add',$group,$body->{name}); + print("add_user &Groups('add',$group,$body->{name})\n"); + } + } + if ($body->{pk} eq 'type') { + if ($body->{value} eq 'delete') { + my ($type,$name) = $body->{name} =~ /^(user_|group_)(.*)/i; + if ($type eq "user_") { + print("can't delete users yet"); + } elsif ($type eq "group_") { + &Groups('delete',$name); + print("delete group &Groups('delete',$name);\n"); + } + } + } + + $response_code = "HTTP/1.1 200 OK\r\n"; + $response_text->{status} = "success"; + $response_text->{text} = ""; + + } elsif ( $path[0] eq 'triggers' ) { + + if (($empty_json or (!defined $body->{name}) or (!defined $body->{value}) or (!defined $body->{pk})) and (defined $body->{pk} and lc $body->{pk} ne 'add')) { + $response_code = "HTTP/1.1 500 Internal Server Error\r\n"; + $response_text->{status} = "error"; + if ($empty_json) { + $response_text->{text} = "Empty JSON string posted"; + } else { + $response_text->{text} = "Bad submitted data"; + } + &main::print_log( "Json_Server.pl: ERROR: Trigger Post:" . $response_text->{text}); + + } elsif (lc $Authorized ne "admin") { + $response_code = "HTTP/1.1 401 Unauthorized\r\n"; + $response_text->{status} = "error"; + $response_text->{text} = "Administative Access required"; + &main::print_log( "Json_Server.pl: ERROR: Unauthorized Trigger update" . $response_text->{text}); + } else { + print Dumper $body; + + my $err = 0; + my $status; + if ($body->{pk} eq 'code') { + $err = trigger_code_flag($body->{value}); + if ($err) { + $status = "Error: Blacklist command found:$err"; + &main::print_log( "Json_Server.pl: Trigger. Blacklist command ($err) found in $body->{name}"); + } else { + $status = &trigger_set_code($body->{name}, $body->{value}); + } + + } elsif ($body->{pk} eq 'name') { + $status = &trigger_rename($body->{name}, $body->{value}); + + } elsif ($body->{pk} eq 'type') { + $status = &trigger_set_type($body->{name}, $body->{value}); + + } elsif ($body->{pk} eq 'trigger') { + $status = &trigger_set_trigger($body->{name}, $body->{value}); + + } elsif ($body->{pk} eq 'add') { + &main::print_log( "Json_Server.pl: adding new trigger $body->{name} "); + my $trigger = ( $body->{trigger1} ) ? "$body->{trigger1} $body->{trigger2}" : $body->{trigger2}; + my $code; + if ( $body->{code1} ) { + unless ( $body->{code1} eq 'set' ) { + $body->{code2} =~ s/\'/\\'/g; + $body->{code2} = "'$body->{code2}'"; + } + $code = "$body->{code1} $body->{code2}"; + } + else { + $code = $body->{code2}; + } + &main::print_log("trigger_set( $trigger, $code, $body->{type}, $body->{name} )"); + + $status = &trigger_set( $trigger, $code, $body->{type}, $body->{name} ); + } + + if ($status =~ m/OK/) { + $response_code = "HTTP/1.1 200 OK\r\n"; + $response_text->{status} = "success"; + my ($txt) = $status =~ /INFO: (.*)/i; + $response_text->{text} = ""; + $response_text->{text} = $txt if ($txt); + &_triggers_save + + } else { + $response_code = "HTTP/1.1 500 Internal Server Error\r\n"; + $response_text->{status} = "error"; + my ($txt) = $status =~ /ERROR: (.*)/i; + $response_text->{text} = $txt; + } + + } + } elsif ( $path[0] eq 'objects' ) { if ($empty_json) { @@ -762,6 +882,81 @@ sub json_get { } } + if ( $path[0] eq 'security' ) { + if (defined $path[1] and $path[1] eq 'authorize') { + # 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 = "fail"; + &main::print_log("json_server.pl: ERROR, authorize attempt with no username"); + } elsif ($args{password} && $args{password}[0] eq "") { + $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); + #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 + 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"; + + 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; + 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}; + } + } + + if ( $path[0] eq 'authorize' ) { + # /json/security/authorize?$user=admin&password=MD5HASH + } + # List subroutines if ( $path[0] eq 'subs' || $path[0] eq '' ) { my $name; @@ -772,6 +967,48 @@ sub json_get { } } + # List triggers + if ( $path[0] eq 'triggers' || $path[0] eq '' ) { + &_triggers_save; #clean up triggers before sending + #group by type + my @dataset = (); + + for my $name ( + sort { + my $t1 = $triggers{$a}{type}; + my $t2 = $triggers{$b}{type}; + $t1 = 0 if $t1 eq 'OneShot'; + $t2 = 0 if $t2 eq 'OneShot'; + $t1 = 1 if $t1 eq 'NoExpire'; + $t2 = 1 if $t2 eq 'NoExpire'; + $t1 = 2 if $t1 eq 'Expired'; + $t2 = 2 if $t2 eq 'Expired'; + $t1 = 3 if $t1 eq 'Disabled'; + $t2 = 3 if $t2 eq 'Disabled'; + $t1 cmp $t2 or lc $a cmp lc $b + } keys %triggers + ) + { + my ( $trigger, $code, $type, $triggered, $trigger_error, $code_error ) = trigger_get($name); + my %data; + $data{name} = $name; + $data{trigger} = $trigger; + $data{type} = $type; + $data{code} = $code; + if ($triggered) { + $data{triggered_ms} = $triggered; + $data{triggered} = &time_date_stamp( 12, $triggered ); + $data{triggered_rel} = &time_date_stamp( 23, $triggered ); + } + push @dataset, \%data; + } + my %data2; + $data2{data} = \@dataset; + $data2{options}{code} = ["speak","play","display","print_log","set","run","run_voice_cmd","net_im_send","net_mail_send"]; + $data2{options}{trigger} = ["time_now","time_cron","time_random","new_second","new_minute","new_hour",'$New_Hour','$New_Day','$New_Week','$New_Month','$New_Year']; + $json_data{'triggers'} = \%data2; + } + # List packages if ( $path[0] eq 'packages' || $path[0] eq '' ) { my $ref = \%::; @@ -1289,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 ) { @@ -1332,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/lib/mqtt.pm b/lib/mqtt.pm index 1cbfeab7f..a76a29838 100644 --- a/lib/mqtt.pm +++ b/lib/mqtt.pm @@ -158,7 +158,6 @@ Notes: reconnect we need to resubscribe. There is no way to do that now (we'll need to resubscribe all the same socket related subscriptions) @FIXME: We're really not checking for ConnAck or SubAck. - @FIXME: there is no reconnect logic @FIXME: No SSL @FIXME: Lots of error checking needs to be done @FIXME: Use of uninitialized value @@ -197,7 +196,7 @@ my $msg_id = 1; my %MQTT_Data; -$main::Debug{mqtt} = 1; +#$main::Debug{mqtt} = 1; # ------------------------------------------------------------------------------ sub dump() { @@ -206,105 +205,13 @@ sub dump() { # ------------------------------------------------------------------------------ -=item - -Okay, I can see this is going to get complicated and require I do a rewrite of -the subscription handling. When we reconnect we want to also resubscribe. -Currently we can't do that. - -=cut - -sub mqtt_reconnect() { - my ($self) = @_; - - ### - ### Do we need to do a clean up on the existing socket before we reconnect? - ### Will a close do that for us ? - ### - $$self{socket}->close(); - - &main::print_log("*** mqtt $$self{instance} mqtt_connect Socket ($$self{host}:$$self{port},$$self{keep_alive_timer}) ") if ( $main::Debug{mqtt} || 1 ); - - ### 1) open a socket (host, port and keepalive - my $socket = IO::Socket::INET->new( - PeerAddr => $self->{host} . ':' . $self->{port}, - Timeout => $self->{keep_alive_timer}, - ); - - # Can't use this at this time - # $socket = new main::Socket_Item(undef, undef, "$host:$port", $instance); - - &main::print_log( "*** mqtt $$self{instance} Socket check #1 ($$self{keep_alive_timer}) [ $! ]: " . ( $self->isConnected() ? "Connected" : "Failed" ) ) - if ( $main::Debug{mqtt} ); - return if ( !defined($socket) ); - - $self->{socket} = $socket; - $self->{got_ping_response} = 1; - $self->{next_ping} = $self->{keep_alive_timer}; - - # -------------------------------------------------------------------------- - ### 2) Send MQTT_CONNECT - $self->send_mqtt_msg( - message_type => MQTT_CONNECT, - keep_alive_timer => $self->{keep_alive_timer}, - user_name => $self->{user_name}, - password => $self->{password} - ); - - ### 3) Check for ACK or fail - &main::print_log( "*** mqtt $$self{instance} Socket check #2 ($$self{keep_alive_timer}) [ $! ]: " . ( $self->isConnected() ? "Connected" : "Failed" ) ) - if ( $main::Debug{mqtt} ); - - my $msg = read_mqtt_msg_timeout( $self, $buf ); - if ( !$msg ) { - &main::print_log("XXX mqtt $$self{instance} No ConnAck "); - - #exit 1; - return; - } - - # We should actually get a SubAck but who is checking (yes, I know I should) - &main::print_log( "*** mqtt $$self{instance} Received: " . $msg->string ) - if ( $main::Debug{mqtt} ); - - ### ------------------------------------------------------------------------ - - ### - ### Here is where we need to make the changes to support multiple - ### subscriptions. - ### - - ### 4) Send a subscribe '#' (we'll have many of these, one for each device) - ### I don't know if this is a good idea or not but that's what I intend to do for now - $self->send_mqtt_msg( - message_type => MQTT_SUBSCRIBE, - message_id => $msg_id++, - topics => [ map { [ $_ => MQTT_QOS_AT_MOST_ONCE ] } $self->{topic} ] - ); - - ### 5) Check for ACK or fail - ### we really should check for a SubAck and that it's the correct SubAck - $msg = $self->read_mqtt_msg_timeout($buf) - or &main::print_log( "*** mqtt $$self{instance} Received: " . "No SubAck" ); - &main::print_log( "*** mqtt $$self{instance} Sub 1 Received: " . "$$msg{string}" ) - if ( $main::Debug{mqtt} ); - - ### ------------------------------------------------------------------------ - - ### 6) check for data - &main::print_log("*** mqtt $$self{instance} Initializing MQTT re_connection ...") - if ( $main::Debug{mqtt} ); -} - -# ------------------------------------------------------------------------------ - =item =cut sub mqtt_connect() { my ($self) = @_; - &main::print_log("*** mqtt mqtt_connect Socket ($$self{host}:$$self{port},$$self{keep_alive_timer}) ") if ( $main::Debug{mqtt} || 1 ); + &main::print_log("*** mqtt mqtt_connect Socket ($$self{host}:$$self{port},$$self{keep_alive_timer}) ") if ( $main::Debug{mqtt} ); ### 1) open a socket (host, port and keepalive my $socket = IO::Socket::INET->new( @@ -315,7 +222,14 @@ sub mqtt_connect() { # Can't use this at this time # $socket = new main::Socket_Item(undef, undef, "$host:$port", $instance); - return if ( !defined($socket) ); + if ( !defined($socket) ) { + if ( $$self{recon_timer}->inactive ) { + ::print_log("*** mqtt connection for $$self{instance} failed, I will try to reconnect in 20 seconds"); + my $inst = $$self{instance}; + $$self{recon_timer}->set( 20, sub { $MQTT_Data{$inst}{self}->mqtt_connect() } ); + return; + } + } $self->{socket} = $socket; $self->{got_ping_response} = 1; @@ -323,12 +237,14 @@ sub mqtt_connect() { # -------------------------------------------------------------------------- ### 2) Send MQTT_CONNECT + # 20-12-2020 added registration of mqtt Last Will and Testament if defined in mh.ini $self->send_mqtt_msg( message_type => MQTT_CONNECT, keep_alive_timer => $self->{keep_alive_timer}, - , - user_name => $self->{user_name}, - password => $self->{password} + user_name => $self->{user_name}, + password => $self->{password}, + will_topic => $::config_parms{mqtt_LWT_topic}, + will_message => $::config_parms{mqtt_LWT_payload} ); ### 3) Check for ACK or fail @@ -381,7 +297,7 @@ sub mqtt_connect() { sub isConnected { my ($self) = @_; - + unless ( defined( $$self{socket} ) ) { return 0 } return $$self{socket}->connected; } @@ -392,7 +308,7 @@ sub isConnected { sub isNotConnected { my ($self) = @_; - + unless ( defined( $$self{socket} ) ) { return 1 } return !$$self{socket}->connected; } @@ -463,13 +379,15 @@ sub new { @{ $$self{command_stack} } = (); - $$self{instance} = $instance; - - $$self{host} = "$host" || "127.0.0.1"; - $$self{port} = $port || 1883; + $$self{instance} = $instance; + $$self{recon_timer} = ::Timer::new(); + $$self{host} = "$host" || "127.0.0.1"; + $$self{port} = $port || 1883; # Use the wildcard here, not in the mqtt_Item - $$self{topic} = "$topic" || "home/ha/#"; + #$$self{topic} = "$topic" || "home/ha/#"; + # 20-12-2020 edit to enable MH to monitor all mqtt topics especially for wildcards e.g. LWT + $$self{topic} = "$topic" || "#"; # Currently not used $$self{user_name} = "$user" || ""; @@ -500,13 +418,13 @@ sub new { ### ------------------------------------------------------------------------ $self->mqtt_connect(); - &main::print_log("\n***\n*** Hmm, this is not good!, can't find myself\n***\n") - unless $self; - return unless $self; + unless ($self) { + &main::print_log("\n***\n*** Hmm, this is not good!, can't find myself\n***\n"); + return; + } # Hey what happens when we fail ? #$MQTT_Data{$instance}{self} = $self; - if ( 1 == scalar( keys %MQTT_Data ) ) { # Add hooks on first call only &main::print_log("*** mqtt added MQTT check_for_data ..."); &::MainLoop_pre_add_hook( \&mqtt::check_for_data, 1 ); @@ -546,10 +464,10 @@ sub check_for_data { ### @FIXME: failed connection if ( 'off' ne $self->{state} ) { - # First say something - &main::print_log("*** mqtt $inst failed ($$self{host}/$$self{port}/$$self{topic})"); - - # Then do something (reconnect) + if ( $$self{recon_timer}->inactive ) { + ::print_log("*** mqtt $inst connection failed ($$self{host}/$$self{port}/$$self{topic}), I will try to reconnect in 20 seconds"); + $$self{recon_timer}->set( 20, sub { $MQTT_Data{$inst}{self}->mqtt_connect() } ); + } # check the state to see if it's off already @@ -709,12 +627,17 @@ sub read_mqtt_msg { # We get no bytes if there is an error or the socket has closed unless ($bytes) { - &main::print_log( "*** mqtt $$self{instance}: read_mqtt_msg Socket closed " . ( defined $bytes ? 'gracefully ' : "with error [ $! ]" ) ); + my $inst = $$self{instance}; + if ( $$self{recon_timer}->inactive ) { + ::print_log( "*** mqtt $$self{instance}: read_mqtt_msg Socket closed " . ( defined $bytes ? 'gracefully ' : "with error [ $! ]" ) ); + ::print_log("*** mqtt instance $$self{instance} will try to reconnect in 20 seconds"); + $$self{recon_timer}->set( 20, sub { $MQTT_Data{$inst}{self}->mqtt_connect() } ); + } # Not a permanent solution just a way to keep debugging - &main::print_log( "*** mqtt deleting $$self{instance}\n" . Dumper( \$self ) ) - if ( $main::Debug{mqtt} ); - delete( $MQTT_Data{ $$self{instance} } ); + #&main::print_log( "*** mqtt deleting $$self{instance}\n" . Dumper( \$self ) ) + # if ( $main::Debug{mqtt} ); + #delete( $MQTT_Data{ $$self{instance} } ); return; } @@ -770,13 +693,10 @@ sub read_mqtt_msg_timeout { sub set { my ( $self, $msg, $set_by ) = @_; - if ( $main::Debug{mqtt} || 1 ) { + if ( $main::Debug{mqtt} ) { my $xStr = defined($msg) ? "($msg)" : "undefined message"; - $xStr .= defined($set_by) ? ", ($set_by)" : ", undefined set_by"; - $xStr .= - ", Obj: " . defined( $$self{object_name} ) - ? ", $$self{object_name}" - : ", undefined object_name"; # @FIXME: Use of uninitialized value + $xStr .= defined($set_by) ? ", ($set_by)" : ", undefined set_by, Obj: "; + $xStr .= defined( $$self{object_name} ) ? ", $$self{object_name}" : ", undefined object_name"; # @FIXME: Use of uninitialized value &main::print_log("*** mqtt mqtt set $$self{instance}: [$xStr]"); &main::print_log( @@ -958,13 +878,46 @@ sub remove_item { sub parse_data_to_obj { my ( $self, $msg, $p_setby ) = @_; + # 20-12-2020 added support for wildcard mqtt devices e.g. in items.mht + # MQTT_DEVICE, MQTT_test_wildcard, , mqtt_1, tele/+/LWT + # or for a multilevel wildcard + # MQTT_DEVICE, MQTT_test_multi_wildcard, , mqtt_1, tele/# + # NOTE, use of multi level wildcards can consume a lot of CPU + # it also exits the loop if it finds a match for speed when there is a large number of mqtt devices + + my ( @split_incoming, @split_device, $counter, $device_topic, $message_topic ); # + $message_topic = "$$msg{topic}"; for my $obj ( @{ $$self{objects} } ) { - if ( "$$obj{topic}" eq "$$msg{topic}" ) { - $obj->set( $$msg{message}, $self, ); + $device_topic = $obj->{topic}; + + # check if this mqtt device is a wildcard, and if so replace the wildcard characters + # with the incoming message topic pieces + if ( index( $device_topic, "\+" ) > 0 + || index( $device_topic, "\#" ) > 0 ) + { + @split_incoming = split( "/", $$msg{topic} ); + @split_device = split( "/", $device_topic ); + $counter = 0; + foreach (@split_device) { + if ( $split_device[$counter] eq "+" ) { + $device_topic =~ s/\+/$split_incoming[$counter]/; + } + if ( $split_device[$counter] eq "#" ) { + $device_topic = substr( $device_topic, 0, index( $device_topic, "#" ) ) . substr( $$msg{topic}, index( $device_topic, "#" ) ); + last; + } + $counter++; + } } - else { - #&main::print_log ("***mqtt mqtt obj ($$obj{topic}) vs ($$msg{topic})"); + + # the edited device topic is now ready to compare with the incoming message topic + if ( $device_topic eq $message_topic ) { + $obj->set( $$msg{message}, $self, ); + $obj->{set_by_topic} = $message_topic; + + # one we have a match, no need to examine any more devices + last; } } @@ -991,9 +944,11 @@ use Data::Dumper; =over - =item name: the 'friendly' name of the squeezebox in squeezecenter. This parameter is used to link this object to the correct status messages in the CLI interface of squeezecenter + =item name: the name of the object seen in Misterhouse + + =item interface: the parent (mqtt) object that holds the connection info. - =item interface: the object that is the CLI interface to assign this player to. + =item interface: the topic that is used to update the object state and/or control a mqtt device =back @@ -1001,11 +956,9 @@ use Data::Dumper; =over - =item amplifier: the object that needs to be enabled and disabled together with the squeezebox - - =item auto_off_time: the time (in minutes) the squeezebox and the optional attached amplifier should be turned off after a playlist has ended + =item retain - =item preheat_time: the time (in seconds) the amplifier should be turned on before a notification is played if the amplifier is off. This enables the amplifier to turn on and enable the speakers before the notification is played. + =item qos =back @@ -1033,7 +986,7 @@ sub new { $$self{topic} = $topic; $$self{message} = ''; $$self{retain} = $retain || 0; - $$self{QOS} = $qos || 0; + $$self{QOS} = $qos || 0; $$self{instance}->add($self); diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 36b0ccfb3..a5ea424c4 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,13 @@ +=head1 B v3.0.4 + +#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 -=head1 B v2.1.1 =head2 SYNOPSIS @@ -32,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 @@ -47,6 +57,10 @@ for specifying controller options; RAZBERRY_CONTROLLER, 10.0.1.1, razberry_controller, zwave, push ,'user=admin,password=bob' +or to poll with authentication + +RAZBERRY_CONTROLLER, 10.0.1.2, razberry_controller2, zwave, ,'user=admin,password=bob' + =head2 DESCRIPTION @@ -58,7 +72,8 @@ Devices need to first included inside the razberry zwave network using the inclu =head3 STATE REPORTED IN MisterHouse The Razberry is polled on a regular basis in order to update local objects. By default, -the razberry is polled every 5 seconds. +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 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. @@ -66,15 +81,15 @@ status every poll. =head3 CHILD OBJECTS Each device class will need a child object, as the controller object is just a gateway -to the hardware. +to the zwave network. There is also a communication object to allow for alerting and monitoring of the razberry controller. =head2 RaZberry v2 AUTHENTICATION -Works and tested 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 @@ -88,9 +103,10 @@ 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.0 +=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%) +URL for updating: http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%,X) +where X is the instance. If ommitted, assume instance 1. If the razberry or mh get out of sync, $controller->poll can be issued to get the latest states. @@ -98,59 +114,28 @@ 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 - -=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. - - -=head2 CHANGELOG -v2.1.0 -- added support for secondary controllers - -v2.0.2 -- added generic_item support for loggers - -v2.0.1 -- added full poll for getting battery data - -v2.0 -- added in authentication method for razberry 2.1.2+ support -- supports a push method when used in conjunction with the HTTPGet automation module -- displays some controller information at startup - -v1.6 -- added in digital blinds, battery item (like a remote) - -v1.5 -- added in binary sensors - -v1.4 -- added in thermostat - -v1.3 -- added in locks -- added in ability to add and remove lock users - -v1.2 -- added in ability to 'ping' device -- added a check to see if the device is 'dead'. If dead it will attempt a ping for - X attempts a Y seconds apart. - +-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 =cut use strict; -our $push_obj; +our $raz_push_obj; +our $raz_instances; package raZberry; @@ -161,7 +146,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'); @@ -201,71 +186,118 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v2.1.1 Controller Initializing..."); + &main::print_log("[raZberry]: v3.0.4 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->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} if ( defined $main::config_parms{raZberry_poll_seconds} ); + $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} = 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; if ( ( defined $poll ) and ( lc $poll eq 'push' ) ) { $self->{push} = 1; - $self->{config}->{poll_seconds} = 1800; #poll the raZberry every 30 minutes if we are using the push method + $self->{config}->{poll_seconds} = 600; #poll the raZberry every 10 minutes if we are using the push method } else { - $self->{config}->{poll_seconds} = $poll if ( defined $poll ); - $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $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 ) ); - if ( ( $push_obj eq "" ) and ( $self->{push} ) ) { - &main::print_log("[raZberry:" . $self->{host} . "]: Push method selected"); - &main::print_log("[raZberry:" . $self->{host} . "]: The HTTPGet Automation module needs to be installed for push to work"); - &main::print_log("[raZberry:" . $self->{host} . "]: URL is http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%)"); - $push_obj = \%{$self}; - } - else { - &main::print_log("[raZberry:" . $self->{host} . ": Push method already in use on other object") if ($push_obj); - &main::print_log("[raZberry:" . $self->{host} . "]: Poll method selected"); + + $self->{instance} = 1; + ( $self->{instance} ) = ( $options =~ /instance\=([0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/instance\=/i ) ); + if ($main::Startup) { + if ( (!defined $raz_push_obj->{$self->{instance}}) && $self->{push} ) { + &main::print_log("[raZberry:" . $self->{host} . "]: Push method selected"); + &main::print_log("[raZberry:" . $self->{host} . "]: The HTTPGet Automation module needs to be installed for push to work"); + &main::print_log("[raZberry:" . $self->{host} . "]: URL is http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%," . $self->{instance} .")"); + $raz_push_obj->{$self->{instance}} = \%{$self}; + } + else { + &main::print_log("[raZberry:" . $self->{host} . "]: Push method already in use on this instance [" . $self->{instance} . "]!") if (defined $raz_push_obj->{$self->{instance}}); + &main::print_log("[raZberry:" . $self->{host} . "]: Poll method selected"); + } + } else { + if ($self->{push}) { + &main::print_log("[raZberry:" . $self->{host} . "]: Push method selected"); + } else { + &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; $self->{controllers}->{failover_threshold} = 120; - $self->get_controllerdata; $self->{timer} = new Timer; - $self->poll; - $self->start_timer; + + $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->{com_warning} = 0; + $self->{com_poll_interval} = undef; + + &::MainLoop_post_add_hook( \&raZberry::process_check, 0, $self ); + $self->{generate_voice_cmds} = 0; - &::Reload_post_add_hook( \&raZberry::generate_voice_commands, 1, $self ); - &main::print_log("[raZberry:" . $self->{host} . "]: Controller Initialization Complete"); + &::Reload_post_add_hook( \&raZberry::generate_voice_commands, 0, $self ); + + $self->get_controllerdata; + return $self; } @@ -289,40 +321,37 @@ 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; &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."); $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"; + $self->{login_attempt} = 0; } } 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) = @_; @@ -386,6 +415,251 @@ sub controller_failback { } +sub process_check { + my ($self) = @_; + my @process_data = (); + 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 + +#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->{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"; + } else { + push @process_data, $json_data; + } + } + if ( $self->{cmd_process}->done_now() ) { + $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") { + 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); + 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"; + #update the retry on the failed item. + $ {$self->{cmd_queue}}[0][3]++; + } else { + push @process_data, $json_data; + 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} }) 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 + 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) > $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() ) )) {#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. Attempt number $retry" ); + } + $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->poll(); #get the first set of data + $self->start_timer; #data has come in, so start the timer. + } + + $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} ); + $com_status = "online"; + $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} ) 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; + 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") { + $self->{com_warning}++; + 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 ) { + 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' ); + } + } + } +} + sub poll { my ( $self, $option ) = @_; @@ -394,7 +668,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} ); @@ -407,105 +681,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} } ) { - &main::print_log("[raZberry:" . $self->{host} . "]: Found:" . $item->{id} . " with level " . $item->{metrics}->{level} . " and updated " . $item->{updateTime} . "." ) - if ( $self->{debug} ); - - #my ($id) = ( split /_/, $item->{id} )[2]; - my ($id) = ( split /_/, $item->{id} )[-1]; #always just get the last element - print "id=$id\n" if ( $self->{debug} > 1 ); - &main::print_log("[raZberry:" . $self->{host} . "]: WARNING: device $id level is undefined") - if ( ( !defined $item->{metrics}->{level} ) or ( lc $item->{metrics}->{level} eq "undefined" ) ); - 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 ); } } @@ -570,107 +762,65 @@ 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); - } + 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 .= "-timeout " . $self->{timeout} . " "; + $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() ) 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; + 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} ); + 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_cmd_queue}) reached! Discarding queued command" ); + #@{ $self->{cmd_queue} } = (); } - 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' ); - } - } - 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 { my ($self) = @_; - $self->{timer}->stop; } sub start_timer { my ($self) = @_; - $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &raZberry::poll($self) }, -1 ); } @@ -695,7 +845,6 @@ sub get_dev_status { if ( defined $self->{data}->{devices}->{$id} ) { return $self->{data}->{devices}->{$id}->{level}; - } else { @@ -712,11 +861,10 @@ sub register { $self->{child_object}->{'comm'} = $object; } else { - #TODO 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 ) { @@ -737,7 +885,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}); @@ -745,52 +893,71 @@ sub deregister { } sub main::razberry_push { - my ( $dev, $level ) = @_; + my ( $dev, $level, $instance ) = @_; my ($id) = ( split /_/, $dev )[-1]; #always just get the last element #Filter out some non-items - return if ( ( $dev =~ m/^InfoWidget_/ ) - or ( $dev =~ m/^BatteryPolling_/ ) ); + return "" if ( (!defined $dev) or ( $dev =~ m/^InfoWidget_/ ) or ( $dev =~ m/^BatteryPolling_/ ) or ( $dev =~ m/^MobileAppSupport/ )); - &main::print_log("[raZberry]: HTTP Push update received for device: $dev, id: $id and level: $level"); + $instance = 1 unless (defined $instance and $instance); - #my $obj = &main::get_object_by_name($object); - if ( $push_obj eq "" ) { - &main::print_log("[raZberry]: ERROR, Push control not enabled on this controller."); + &main::print_log("[raZberry]: HTTP Push update received for instance: $instance, device: $dev, id: $id and level: $level") if ( $main::Debug{razberry}); - } + #my $obj = &main::get_object_by_name($object); + if ( (! defined $raz_push_obj->{$instance}) or ( $raz_push_obj->{$instance} eq "" )) { + &main::print_log("[raZberry]: ERROR, Push control not enabled on this controller instance: $instance."); + } elsif ( $dev =~ m/^ZWayVDev_zway_/ ) { - if ( defined $push_obj->{child_object}->{$id} ) { + + if ( defined $raz_push_obj->{$instance}->{child_object}->{$id} ) { if ( $dev =~ m/\-0\-\50\-\d$/ ) { ( my $subdev ) = ( $dev =~ /\-0\-50\-(\d)$/ ); - &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set_level( ' . $level . ", $subdev );" ); + &main::print_log( '[raZberry]: Calling $raz_push_obj->{$instance}->{child_object}->{' . $id . '}->set_level( ' . $level . ", $subdev );" ) if ( $main::Debug{razberry}); } else { - &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set( ' . $level . ", 'push' );" ); - $push_obj->{child_object}->{$id}->set( $level, 'push' ); + &main::print_log( '[raZberry]: Calling $raz_push_obj->{$instance}->{child_object}->{' . $id . '}->set( ' . $level . ", 'push' );" ) if ( $main::Debug{razberry}); + $raz_push_obj->{$instance}->{child_object}->{$id}->set( $level, '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)"); } } else { &main::print_log("[raZberry]: ERROR, only ZWayVDev devices supported for push"); } - + + #update comm object, If we got a push request, then the razberry's OK + if ( defined $raz_push_obj->{$instance}->{child_object}->{comm} ) { + if (( $raz_push_obj->{$instance}->{status} eq "offline" ) || ($raz_push_obj->{$instance}->{child_object}->{comm}->state() eq "offline")) { + $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' ); + } + } +return ""; } sub print_command_queue { my ($self) = @_; main::print_log( "[raZberry:" . $self->{host} . "]: ------------------------------------------------------------------" ); - my $commands = scalar @{ $self->{cmd_queue} }; - my $name = "$commands commands"; - $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] ); + unless ( defined $self->{cmd_queue} ) { + main::print_log( "[raZberry:" . $self->{host} . "]: Empty Command queue" ); + } else { + my $commands = scalar @{ $self->{cmd_queue} }; + my $name = "$commands commands"; + $name = "empty" if ($commands == 0); + main::print_log( "[raZberry:" . $self->{host} . "]: Current Command Queue: $name" ); + for my $i ( 1 .. $commands ) { + 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} . "]: ------------------------------------------------------------------" ); @@ -851,13 +1018,12 @@ sub get_voice_cmds { 'Print devices to print log' => $self->get_object_name . '->display_all_devices', 'Print Command Queue to print log' => $self->get_object_name . '->print_command_queue', 'Purge Command Queue' => $self->get_object_name . '->purge_command_queue', - 'Initiate controller failover' => $self->get_object_name . '->controller_failover', - 'Initiate controller failback' => $self->get_object_name . '->controller_failback(\"force\")' + 'Poll Controller' => $self->get_object_name . '->poll' ); -# if ($self->{controllers}->{backup}) { -# $voice_cmds{'Initiate controller failover'} => $self->get_object_name . '->controller_failover'; -# $voice_cmds{'Initiate controller failback'} => $self->get_object_name . '->controller_failback(\'force\')'; -# } + if ($self->{controllers}->{backup}) { + $voice_cmds{'Initiate controller failover'} = $self->get_object_name . '->controller_failover'; + $voice_cmds{'Initiate controller failback'} = $self->get_object_name . '->controller_failback(\'force\')'; + } return \%voice_cmds; } @@ -1180,28 +1346,39 @@ 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; - } + if (!defined $self->{battery_level}) { + &main::print_log( "[raZberry_lock] WARNING Battery level undefined. Try again later" ); + return undef; } - 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; + $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"); + } + 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%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 ); + main::eval_with_timer( sub { &raZberry_lock::battery_check($self,1) }, 10 ); + } return $self->{battery_level}; + } sub _battery_timer { @@ -1324,23 +1501,36 @@ 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 (!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 ) ) { + $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%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 ); + main::eval_with_timer( sub { &raZberry_lock::battery_check($self,1) }, 10 ); + } return $self->{battery_level}; + } sub enable_user { @@ -1463,6 +1653,7 @@ sub new { $$self{master_object} = $object; push( @{ $$self{states} }, 'online', 'offline' ); $object->register( $self, 'comm' ); +# $self->SUPER::set('online'); #start online at initialization return $self; } @@ -1496,7 +1687,7 @@ sub new { } else { - push( @{ $$self{states} }, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 16, 27, 28, 29, 30 ); + push( @{ $$self{states} }, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 ); $self->{units} = "C"; $self->{min_temp} = 10; $self->{max_temp} = 30; @@ -1504,6 +1695,9 @@ sub new { $$self{master_object} = $object; $devid = $devid . "-0-67" if ( $devid =~ m/^\d+$/ ); + #check if the thermostat is a subitem? ie xx-0-67-1, which happened on 2.3.5? + my $testdev = $devid . "-1"; + $devid = $testdev if (defined $$self{master_object}->{data}->{devices}->{$testdev}); $$self{devid} = $devid; $$self{type} = "Thermostat"; @@ -1697,8 +1891,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 { @@ -1761,23 +1954,35 @@ 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 (!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 ) ) { + $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%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 ); + main::eval_with_timer( sub { &raZberry_lock::battery_check($self,1) }, 10 ); + } return $self->{battery_level}; + } package raZberry_voltage; @@ -1796,7 +2001,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; @@ -1897,3 +2102,131 @@ sub isfailed { sub update_data { my ( $self, $data ) = @_; } + +package raZberry_motion; +@raZberry_motion::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_brightness::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 . "-0-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} ); +} + +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! + +# 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 + +v2.2.0 +- fixed push not working at reload. Added instance option so that multiple controllers can push updates +- minor bugfixes + +v2.1.0 +- added support for secondary controllers. Given that secondary controllers don't receive + lifeline data (ie updates when device is changed by other controller or local ) this probably isn't useful + +v2.0.2 +- added generic_item support for loggers + +v2.0.1 +- added full poll for getting battery data + +v2.0 +- added in authentication method for razberry 2.1.2+ support +- supports a push method when used in conjunction with the HTTPGet automation module +- displays some controller information at startup + +v1.6 +- added in digital blinds, battery item (like a remote) + +v1.5 +- added in binary sensors + +v1.4 +- added in thermostat + +v1.3 +- added in locks +- added in ability to add and remove lock users + +v1.2 +- added in ability to 'ping' device +- 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 diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 059b251bb..e915acc5a 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1503,11 +1503,14 @@ sub read_table_A { my $poll; ( $address, $name, $grouplist, $poll, @other ) = @item_info; $other = join ',', ( map { "$_" } @other ); # Quote data + $poll = "'" . $poll . "'" if (defined $poll); + $other =~ s/^[\'\"]//; #strip out quotes in case they are included + $other =~ s/[\'\"]$//; if ($other) { - $object = "raZberry('$address','$other')"; + $object = "raZberry('$address', $poll, '$other')"; } else { - $object = "raZberry('$address')"; + $object = "raZberry('$address', $poll)"; } $code .= "use raZberry;\n"; } @@ -1639,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 ]------------------------------------------------------ @@ -1814,7 +1840,100 @@ sub read_table_A { $object = ''; } #-------------- End Alexa Objects ---------------- - + #-------------- BondHome Objects ----------------- + elsif ( $type eq "BONDHOME" ) { + ## + require 'BondHome.pm'; + $code .= '#noloop=start'."\n"; + my ($instance); + ( $name, $instance, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "BondHome('$instance','$other')".';'."\n".'#noloop=stop'."\n"; + } + elsif ( $type eq "BONDHOME_DEVICE" ) { + ## + require 'BondHome.pm'; + $code .= '#noloop=start'."\n"; + my ($instance, $bonddevname); + ( $name, $instance, $bonddevname, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); + $object = "BondHome_Device('$instance','$bonddevname')".';'."\n".'#noloop=stop'."\n"; + $code .= '#noloop=stop'."\n"; + } + elsif ( $type eq "BONDHOME_MANUAL" ) { + ## + require 'BondHome.pm'; + $code .= '#noloop=start'."\n"; + my ($instance); + ( $name, $instance, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); + $object = "BondHome_Manual('$instance')".';'."\n".'#noloop=stop'."\n"; + $code .= '#noloop=stop'."\n"; + } + elsif ( $type eq "BONDHOME_MANUAL_CMD" ) { + ## + $code .= '#noloop=start'."\n"; + my ($parent, $cmdname, $frequency, $modulation, $encoding, $bps, $reps, $data) = @item_info;; + $code .= sprintf "\$%-35s -> addcmd('$cmdname', '$frequency', '$modulation', '$encoding', '$bps', '$reps', '$data');\n", $parent; + $object = ''; + $code .= '#noloop=stop'."\n"; + } + #-------------- End BondHome 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%^&%\\&%; # "&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"; + } + $code .= sprintf "\$%-35s -> add('$realname','$name',$sub,'$on','$off','$statesub',$other);\n", $parent; + $object = ''; + } + #-------------- End AoGSmartHome Objects ---------------- + #-------------- MQTT Objects ----------------- + elsif ( $type eq "MQTT_BROKER" ) { + # there must be one record for the broker above any MQTT_DEVICE definitions + # it takes the following format + # MQTT_BROKER, name_of_broker + # e.g.MQTT_BROKER, mqtt_1 + require 'mqtt.pm'; + ( $name ) = @item_info; + $code .= sprintf( "\n\$%-35s = new mqtt(\"%s\", \$config_parms{mqtt_host}, + \$config_parms{mqtt_server_port}, + \$config_parms{mqtt_topic}, + \$config_parms{mqtt_username}, + \$config_parms{mqtt_password}, 121);\n", + $name, + $name + ); + } + elsif ( $type eq "MQTT_DEVICE" ) { + # there is one record per mqtt device and it must be below the MQTT_BROKER definition + # it takes the following form + # MQTT_DEVICE, name_of_device, groups, name_of_broker, topic + # e.g. MQTT_DEVICE, MQTT_test, Kitchen, mqtt_1, stat/mh_mqtt_test/SENSOR + # if the device is to transmit to MH, its topic must match the + # config parameter mqtt_topic in the mh.ini file + require 'mqtt.pm'; + my ($MQTT_broker_name, $MQTT_topic); + ( $name, $grouplist, $MQTT_broker_name, $MQTT_topic ) = @item_info; + + $code .= sprintf( "\n\$%-35s = new mqtt_Item(\$%s\,\"%s\");\n", + $name, $MQTT_broker_name, $MQTT_topic ); + + } + #-------------- End MQTT Objects ---------------- elsif ( $type =~ /PLCBUS_.*/ ) { #<,PLCBUS_Scene,Address,Name,Groups,Default|Scenes># require PLCBUS; @@ -1831,6 +1950,11 @@ sub read_table_A { &::MainLoop_pre_add_hook( \&Wink::GetDevicesAndStatus, 1 ); } } + elsif ( $type eq "TASMOTA_HTTP_SWITCH" ) { + require Tasmota_HTTP_Item; + ( $address, $name, $grouplist ) = @item_info; + $object = "Tasmota_HTTP::Switch('$address')"; + } else { print "\nUnrecognized .mht entry: $record\n"; return; @@ -1852,6 +1976,11 @@ sub read_table_A { next; } + if ( $group eq ''){ + &::print_log("grouplist '$grouplist' contains empty group!"); + next; + } + if ( $name eq $group ) { &::print_log( "mht object and group name are the same: $name Bad idea!"); diff --git a/lib/site/Geo/WeatherNOAA.pm b/lib/site/Geo/WeatherNOAA.pm index caf4f9fb6..b99f0624d 100644 --- a/lib/site/Geo/WeatherNOAA.pm +++ b/lib/site/Geo/WeatherNOAA.pm @@ -33,7 +33,7 @@ require Exporter; $VERSION = do { my @r = ( q$Revision: 4.40 $ =~ /\d+/g ); sprintf "%d." . "%02d" x $#r, @r }; my $URL_BASE = 'http://forecast.weather.gov/product.php?site='; -my $ZONE_SEARCH_URL = 'http://forecast.weather.gov/zipcity.php'; +my $ZONE_SEARCH_URL = 'https://forecast.weather.gov/zipcity.php'; use vars '$proxy_from_env'; $proxy_from_env = 0; @@ -386,8 +386,7 @@ sub get_zone { $UA->agent("WeatherNOAA/$VERSION"); my $ua = LWP::UserAgent->new(); - my $response = - $ua->post( $URL, { 'inputstring' => $CityState, 'siteid' => 'chr' } ); + my $response = $ua->post( $URL, { 'inputstring' => $CityState, 'Go2' => 'Go' } ); my $location = $response->header('Location'); if ( $location =~ /&site=(...)&/ ) { @@ -396,6 +395,7 @@ sub get_zone { else { return; } + } sub get_url { diff --git a/lib/trigger_code.pl b/lib/trigger_code.pl index d318d18e2..b28a28acd 100644 --- a/lib/trigger_code.pl +++ b/lib/trigger_code.pl @@ -248,8 +248,10 @@ =head1 SUBROUTINES # this routine does the heavy lifting re modifying, renaming, copying triggers sub trigger_set { my ( $trigger, $code, $type, $name, $replace, $triggered, $new_name ) = @_; - - return unless $trigger and $code; + my $message = "OK"; + + return "Error: no trigger" unless $trigger; + return "Error: no code" unless $code; $trigger =~ s/[;\s\r\n]*$//g; # in case trigger file was edited on windows $code =~ s/[;\s\r\n]*$//g; # So we can consistenly add ;\n when used $triggered = 0 unless $triggered; @@ -268,7 +270,8 @@ sub trigger_set { $name =~ s/ \d+$//; my $i = 2; while ( exists $triggers{"$name $i"} ) { $i++; } - print_log "trigger $name already exists, adding '$i' to name"; + $message .= "\nINFO: trigger $name already exists, adding '$i' to name\n"; + &print_log($message); $name = "$name $i"; } print_log "trigger_set: trigger=$trigger code=$code type=$type name=$name @@ -279,7 +282,8 @@ sub trigger_set { eval $trigger; if ($@) { $triggers{$name}{'trigger_error'} = $@; - &print_log("Error: trigger '$name' has an error, disabling"); + $message = "Error: trigger '$name' has an error, disabling"; + &print_log($message); &print_log(" Code = $trigger"); &print_log(" Result = $@"); } @@ -298,7 +302,7 @@ sub trigger_set { } $trigger_write_code_flag++ unless $Reload; - return; + return $message; } =item C @@ -317,6 +321,22 @@ sub trigger_get { $triggers{$name}{trigger_error}, $triggers{$name}{code_error}; } +=item C + +Prevents a blacklist of commands from being entered. + +=cut + +sub trigger_code_flag { + my $code = shift; + my @blacklist = ("`", "unlink", "exec", "fork", "open", "die", "exit", "eval", "system"); + return 0 if (defined $::config_parms{ignore_trigger_code_flag} and $::config_parms{ignore_trigger_code_flag} == 1); + foreach my $item (@blacklist) { + return $item if ($code =~ m/$item/); + } + return 0; +} + =item C Deletes the specified trigger. @@ -357,8 +377,8 @@ sub trigger_rename { my $type = $triggers{$name}{type}; my $replace = 1; my $triggered = $triggers{$name}{triggerd}; - trigger_set( $trigger, $code, $type, $name, $replace, $triggered, $new_name ); - return; + my $status = trigger_set( $trigger, $code, $type, $name, $replace, $triggered, $new_name ); + return $status; } sub trigger_set_trigger { @@ -369,8 +389,8 @@ sub trigger_set_trigger { my $type = $triggers{$name}{type}; my $replace = 1; my $triggered = $triggers{$name}{triggered}; - trigger_set( $trigger, $code, $type, $name, $replace, $triggered ); - return; + my $status = trigger_set( $trigger, $code, $type, $name, $replace, $triggered ); + return $status; } sub trigger_set_code { @@ -381,8 +401,8 @@ sub trigger_set_code { my $type = $triggers{$name}{type}; my $replace = 1; my $triggered = $triggers{$name}{triggered}; - trigger_set( $trigger, $code, $type, $name, $replace, $triggered ); - return; + my $status = trigger_set( $trigger, $code, $type, $name, $replace, $triggered ); + return $status; } sub trigger_set_type { @@ -393,8 +413,8 @@ sub trigger_set_type { my $type = shift; my $replace = 1; my $triggered = $triggers{$name}{triggered}; - trigger_set( $trigger, $code, $type, $name, $replace, $triggered ); - return; + my $status = trigger_set( $trigger, $code, $type, $name, $replace, $triggered ); + return $status; } sub trigger_expire { diff --git a/lib/xAP_Items.pm b/lib/xAP_Items.pm index 984a65760..aa35cd95c 100644 --- a/lib/xAP_Items.pm +++ b/lib/xAP_Items.pm @@ -636,6 +636,9 @@ sub _process_incoming_xap_data { if $o->allow_empty_state() or ( defined $state_value and $state_value ne '' ); } + } else { + print "db1 xap discarded, source is mh: s=$source d=$data\n" + if $main::Debug{xap} and $main::Debug{xap} == 1; } } diff --git a/lib/xml_server.pl b/lib/xml_server.pl index 1034c50da..4e5ea94eb 100644 --- a/lib/xml_server.pl +++ b/lib/xml_server.pl @@ -497,7 +497,6 @@ sub xml_page { my $style; $style = qq|| if $xsl; my $html = < $style 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}; } } diff --git a/web/bin/items.pl b/web/bin/items.pl index 610aa461b..538a4ef28 100644 --- a/web/bin/items.pl +++ b/web/bin/items.pl @@ -175,33 +175,45 @@ sub web_items_list { return &html_page( '', $html ); } -sub AddMhtWebItems { # This takes properly formatted comments from read_table_A.pl and # adds them to the mht web editor list. -my ( $web_lists, %web_lists, @values, $values, $types ); - open TABLE_A, "/opt/misterhouse/mh/lib/read_table_A.pl"; - push @{$$web_lists{web_item_types}}, 'type'; - push @{$$web_lists{web_item_types}}, 0; - push @{$$web_lists{web_item_types}}, ""; - while () { - if (/#<(.*?),(.*?),(.*)>#/) { - $types = $2; - $types = $1 if length($1); - if ( length($3) ) { - $values= $3; - @values = split ',', $values; - } else { - @values = (qw(Address Name Groups Other)); - } - push @{$$web_lists{web_item_types}}, "$types"; - $$web_lists{headers}{$2} = [@values]; - next; - } - - next if (/^\s*$/); # Skip blank lines - } - $$web_lists{headers}{default} = [qw(Address Name Groups Other)]; - return($web_lists); +sub AddMhtWebItems { + my ( $web_lists, %web_lists, @values, $values, $types ); + my $read_table_A_path; + + # Try to find the location of read_table_A.pl + for my $path (@INC) { + if (-f "$path/read_table_A.pl") { + $read_table_A_path = $path; + last; + } + } + + if (open(TABLE_A, "$read_table_A_path/read_table_A.pl") ) { + push @{$$web_lists{web_item_types}}, 'type'; + push @{$$web_lists{web_item_types}}, 0; + push @{$$web_lists{web_item_types}}, ""; + while () { + if (/#<(.*?),(.*?),(.*)>#/) { + $types = $2; + $types = $1 if length($1); + if ( length($3) ) { + $values= $3; + @values = split ',', $values; + } else { + @values = (qw(Address Name Groups Other)); + } + push @{$$web_lists{web_item_types}}, "$types"; + $$web_lists{headers}{$2} = [@values]; + } + } + + close TABLE_A; + } + + $$web_lists{headers}{default} = [qw(Address Name Groups Other)]; + + return $web_lists; } sub web_item_set_field { diff --git a/web/ia7/fonts/glyphicons-halflings-regular.ttf b/web/ia7/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..1413fc609 Binary files /dev/null and b/web/ia7/fonts/glyphicons-halflings-regular.ttf differ diff --git a/web/ia7/fonts/glyphicons-halflings-regular.woff b/web/ia7/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..9e612858f Binary files /dev/null and b/web/ia7/fonts/glyphicons-halflings-regular.woff differ diff --git a/web/ia7/fonts/glyphicons-halflings-regular.woff2 b/web/ia7/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 000000000..64539b54c Binary files /dev/null and b/web/ia7/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/web/ia7/graphics/clear.png b/web/ia7/graphics/clear.png new file mode 100755 index 000000000..580b52a5b Binary files /dev/null and b/web/ia7/graphics/clear.png differ 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 000000000..a0876ec62 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_danger_128.png differ diff --git a/web/ia7/graphics/fp_speaker_danger_32.png b/web/ia7/graphics/fp_speaker_danger_32.png new file mode 100644 index 000000000..ebccb4318 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_danger_32.png differ 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 000000000..7f594d2db Binary files /dev/null and b/web/ia7/graphics/fp_speaker_danger_48.png differ 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 000000000..cae87873e Binary files /dev/null and b/web/ia7/graphics/fp_speaker_danger_64.png differ 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 000000000..1e8084273 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_default_128.png differ 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 000000000..174464b4c Binary files /dev/null and b/web/ia7/graphics/fp_speaker_default_32.png differ 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 000000000..5e58399af Binary files /dev/null and b/web/ia7/graphics/fp_speaker_default_48.png differ diff --git a/web/ia7/graphics/fp_speaker_default_64.png b/web/ia7/graphics/fp_speaker_default_64.png new file mode 100644 index 000000000..ff25dc5b0 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_default_64.png differ 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 000000000..c73e804f0 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_info_128.png differ 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 000000000..c8a076af1 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_info_32.png differ 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 000000000..90e5df630 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_info_48.png differ 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 000000000..0a3c8e739 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_info_64.png differ diff --git a/web/ia7/graphics/fp_speaker_success_128.png b/web/ia7/graphics/fp_speaker_success_128.png new file mode 100644 index 000000000..d70fb775a Binary files /dev/null and b/web/ia7/graphics/fp_speaker_success_128.png differ diff --git a/web/ia7/graphics/fp_speaker_success_32.png b/web/ia7/graphics/fp_speaker_success_32.png new file mode 100644 index 000000000..c1e3fe528 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_success_32.png differ 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 000000000..32a249f8a Binary files /dev/null and b/web/ia7/graphics/fp_speaker_success_48.png differ 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 000000000..84bcb8dbd Binary files /dev/null and b/web/ia7/graphics/fp_speaker_success_64.png differ 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 000000000..9347b8e3a Binary files /dev/null and b/web/ia7/graphics/fp_speaker_warning_128.png differ 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 000000000..d3e543692 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_warning_32.png differ 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 000000000..71d39a511 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_warning_48.png differ 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 000000000..4ca0a9bb6 Binary files /dev/null and b/web/ia7/graphics/fp_speaker_warning_64.png differ diff --git a/web/ia7/graphics/loading.gif b/web/ia7/graphics/loading.gif new file mode 100755 index 000000000..5b33f7e54 Binary files /dev/null and b/web/ia7/graphics/loading.gif differ diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 90939c657..9bb7ffe72 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -6,7 +6,9 @@ OS:
Perl:
User:
- IA7: replace_current_ia7_version + IA7: replace_current_ia7_version
+ Collection: replace_current_collection_version +

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/house/whatsnew.shtml b/web/ia7/house/whatsnew.shtml index 66ccd3d2a..e717af24e 100644 --- a/web/ia7/house/whatsnew.shtml +++ b/web/ia7/house/whatsnew.shtml @@ -1,3 +1,56 @@ +

v5.1

+

Released 2018/xx/xx

+
+ +
+ +
+
+

+ Native Triggers +

+
+
+
+
    +
  • Support for adding, and editing triggers +
  • Controls only visible in expert mode +
+
+
+
+
+ +
+
+
    +
  • 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

Released 2017/10/31


diff --git a/web/ia7/include/bootstrap-theme.3.3.7.min.css b/web/ia7/include/bootstrap-theme.3.3.7.min.css deleted file mode 100644 index 5e3940195..000000000 --- a/web/ia7/include/bootstrap-theme.3.3.7.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -/*# sourceMappingURL=bootstrap-theme.min.css.map */ \ No newline at end of file diff --git a/web/ia7/include/bootstrap-theme.3.3.7.min.css.gz b/web/ia7/include/bootstrap-theme.3.3.7.min.css.gz new file mode 100644 index 000000000..ef741035f Binary files /dev/null and b/web/ia7/include/bootstrap-theme.3.3.7.min.css.gz differ diff --git a/web/ia7/include/bootstrap.3.3.7.min.css.gz b/web/ia7/include/bootstrap.3.3.7.min.css.gz new file mode 100644 index 000000000..685f4d04d Binary files /dev/null and b/web/ia7/include/bootstrap.3.3.7.min.css.gz differ diff --git a/web/ia7/include/bootstrap.3.3.7.min.js.gz b/web/ia7/include/bootstrap.3.3.7.min.js.gz new file mode 100644 index 000000000..9462e6553 Binary files /dev/null and b/web/ia7/include/bootstrap.3.3.7.min.js.gz differ diff --git a/web/ia7/include/bootstrap3-editable.1.5.0.css b/web/ia7/include/bootstrap3-editable.1.5.0.css index 61cf1083b..9085d963e 100644 --- a/web/ia7/include/bootstrap3-editable.1.5.0.css +++ b/web/ia7/include/bootstrap3-editable.1.5.0.css @@ -48,7 +48,7 @@ } .editableform-loading { - background: url('../img/loading.gif') center center no-repeat; + background: url('../graphics/loading.gif') center center no-repeat; height: 25px; width: auto; min-width: 25px; @@ -116,7 +116,7 @@ /* IOS-style clear button for text inputs */ .editable-clear-x { - background: url('../img/clear.png') center center no-repeat; + background: url('../graphics/clear.png') center center no-repeat; display: block; width: 13px; height: 13px; diff --git a/web/ia7/include/font-awesome.4.7.0.min.css b/web/ia7/include/font-awesome.4.7.0.min.css deleted file mode 100644 index 540440ce8..000000000 --- a/web/ia7/include/font-awesome.4.7.0.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/web/ia7/include/font-awesome.4.7.0.min.css.gz b/web/ia7/include/font-awesome.4.7.0.min.css.gz new file mode 100644 index 000000000..29ffd1d78 Binary files /dev/null and b/web/ia7/include/font-awesome.4.7.0.min.css.gz differ diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 908361bc1..747b861db 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,6 @@ -var ia7_ver = "v1.6.700"; +var ia7_ver = "v2.0.820"; +var coll_ver = ""; var entity_store = {}; //global storage of entities var json_store = {}; var updateSocket; @@ -12,18 +13,67 @@ var speech_banner; var audio_init; var audioElement = document.getElementById('sound_element'); var authorized = "false"; +var admin = "false"; var developer = false; var show_tooltips = true; var rrd_refresh_loop; var stats_loop; -var stat_refresh = 60; var fp_popover_close = true ; var dev_changes = 0; var config_modal_loop; +var zm_init = undefined; +var req_errors = {}; + +var stat_refresh = 60; +var error_retry = 10; //seconds to retry if an error was found in ajax request var ctx; //audio context var buf; //audio buffer +//'Dynamic' modules. To enable the main collection screen to display as quick as possible +// load all modules not needed to display until after the collection screen. +var modules = {}; +modules.zoneminder = {}; +modules['zoneminder'].loaded = 0 +modules['zoneminder'].script = ["zm.js"]; + +modules.object = {}; +modules['object'].loaded = 0; +modules['object'].script = ["jqCron.js"]; +modules['object'].css = ["jqCron.css","bootstrap-datepicker3.standalone.min.css"]; +modules['object'].callback = function (){loadModule("object2")}; +modules.object2 = {}; +modules['object2'].loaded = 0; +modules['object2'].script = ["jqCron.en.js","bootstrap-datepicker.min.js"]; + +modules.init = {}; +modules['init'].loaded = 0; +modules['init'].script = ["jquery.alerts.js","jquery.ui.touch-punch.0.2.3.min.js","ia7_prefs.json"]; +modules['init'].css = ["jquery.alerts.css"]; + +modules.edit = {}; +modules['edit'].loaded = 0; +modules['edit'].script = ["bootstrap3-editable.1.5.0.min.js"]; +modules['edit'].css = ["bootstrap3-editable.1.5.0.css"]; + +modules.rrd = {} +modules['rrd'].loaded = 0; +modules['rrd'].script = ["jquery.flot.min.js"]; +modules['rrd'].callback = function (){loadModule("rrd2")}; +modules.rrd2 = {} +modules['rrd2'].loaded = 0; +modules['rrd2'].script = ["jquery.flot.time.min.js","jquery.flot.resize.min.js","jquery.flot.selection.min.js"]; + +modules.tables = {}; +modules['tables'].loaded = 0; +modules['tables'].css = ["tables.css"]; + +modules.developer = {}; +modules['developer'].loaded = 0; +modules['developer'].script = ["fontawesome-iconpicker.min.js"]; +modules['developer'].css = ["fontawesome-iconpicker.min.css"]; + + //Takes the current location and parses the achor element into a hash function URLToHash() { if (location.hash === undefined) return; @@ -48,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"); $('#buffer_page').append("