From e88748aa0e257f258872440a7c636506931c4967 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 2 Mar 2013 14:46:09 +0100 Subject: [PATCH 001/209] Upped version to 2.200 --- VERSION | 1 + bin/mh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..b0bb4f56c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +This is Misterhouse stable release v2-200. diff --git a/bin/mh b/bin/mh index b6a8afcb8..e5bf30b93 100755 --- a/bin/mh +++ b/bin/mh @@ -77,7 +77,7 @@ BEGIN { close (ENTRIES); } - $Version = "mh 2.105"; + $Version = "mh 2.200"; if ($revision) { $Version.=" R${revision}"; } From 236dd90e8f2dc87e517db03a9cba4413b0b55480 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Wed, 13 Mar 2013 21:38:53 +0100 Subject: [PATCH 002/209] Update README.md Ensure the naming is consistent with what we agreed upon on the mailing list. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eff1684c3..8b6692a56 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -misterhouse +MisterHouse =========== Perl open source home automation program. It's fun, it's free, and it's entirely geeky. A quickstart guide is documented [here](https://github.com/hollie/misterhouse/wiki/Getting-started). -The Misterhouse wiki with more information is located here: http://misterhouse.wikispaces.com/ \ No newline at end of file +The Misterhouse wiki with more information is located here: http://misterhouse.wikispaces.com/ From cc7c439239b8caac60b99b107aa17bdc0ed74432 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Wed, 13 Mar 2013 21:56:13 +0100 Subject: [PATCH 003/209] Modified the contents of the VERSION file in preparation of MH depending on this file to know what version it is running --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b0bb4f56c..dd8ca3155 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -This is Misterhouse stable release v2-200. +2.200 From b3ba252c0b0d471ffa4b5ba114dd85f07fc21f41 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Thu, 20 Jun 2013 21:27:26 +0200 Subject: [PATCH 004/209] Upped version to 3.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 76cff7f1f..f398a2061 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -unstable \ No newline at end of file +3.0 \ No newline at end of file From 40dae3204a7662b2bf53f05f5221f87a0dbc2af1 Mon Sep 17 00:00:00 2001 From: Steve Switzer Date: Tue, 1 Oct 2013 13:17:55 -0400 Subject: [PATCH 005/209] First creation of common/weather_wunderground.pl --- code/common/weather_wunderground.pl | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 code/common/weather_wunderground.pl diff --git a/code/common/weather_wunderground.pl b/code/common/weather_wunderground.pl new file mode 100644 index 000000000..bb324a9a6 --- /dev/null +++ b/code/common/weather_wunderground.pl @@ -0,0 +1,123 @@ +# Category = Weather + +#@ Updates live weather variables from http://api.wunderground.com. + +=begin comment +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +File: + weather_wunderground.pl + +Description: + Updates live weather variables from http://api.wunderground.com. + +Author: + Steve Switzer (Pmatis) + steve@switzerny.org + +License: + This free software is licensed under the terms of the GNU public license. + +Requires: + Weather_Common.pl + XML::Twig + +Special Thanks to: + Bruce Winter - MH + Everyone else - GPL examples to learn from + +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=cut +# noloop=start + use Weather_Common; + use XML::Twig; + my $wunderground_getweather_file; + my $wunderground_stationid=$config_parms{wunderground_stationid}; + my $wunderground_url; + $p_weather_wunderground_getweather = new Process_Item(); + + my $wunderground_states = 'getweather,debug'; + $v_wunderground = new Voice_Cmd("wunderground [$wunderground_states]"); +# noloop=stop + +if ($Reload) { + $wunderground_stationid=$config_parms{wunderground_stationid}; + $wunderground_stationid='KNYROCHE41' unless $wunderground_stationid; + $wunderground_getweather_file=$config_parms{data_dir}.'/web/weather_wunderground_getweather.xml'; + $wunderground_url='http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID='.$wunderground_stationid; + + set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_getweather_file"}; + #$p_weather_wunderground_getweather-set_output("$wunderground_getweather_file"); + + &trigger_set('($New_Minute_10) or $Reload', "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') + unless &trigger_get('Update current weather conditions via wunderground'); +} + +my $wunderground_state = 'blank'; +if ($wunderground_state = $v_wunderground ->{said}) { + if ($wunderground_state eq 'getweather'){ + start $p_weather_wunderground_getweather; + } +} + +my(%wunderground_data,@wunderground_keys); +if (done_now $p_weather_wunderground_getweather) { + $Weather{wunderground_obsv_valid} = 0; #Set to not valid unless proven + #my $wunderground_xml=file_read $wunderground_getweather_file; + print_log "wunderground getweather finished."; + my $twig = new XML::Twig; + $twig->parsefile($wunderground_getweather_file); + my $root = $twig->root; + my $channel = $root->first_child("current_observation"); + + my $w_stationid = $root->first_child_text("station_id"); + + print_log "WUnderground: Received stationid: $w_stationid\n"; + + if($config_parms{wunderground_stationid} eq $w_stationid) { + %wunderground_data={}; + @wunderground_keys=[]; + #TempOutdoor + weather_wunderground_addelem($root,'TempOutdoor','temp_f'); + #DewOutdoor + weather_wunderground_addelem($root,'DewOutdoor','dewpoint_f'); + #WindAvgDir + weather_wunderground_addelem($root,'WindAvgDir','wind_dir'); + #WindAvgSpeed + weather_wunderground_addelem($root,'WindAvgSpeed','wind_mph'); + #WindGustDir + #WindGustSpeed + #WindGustTime + #Clouds + #Conditions + #Barom + weather_wunderground_addelem($root,'Barom','pressure_mb'); + #BaromSea + #BaromDelta + #HumidOutdoorMeasured + #HumidOutdoor + weather_wunderground_addelem($root,'HumidOutdoor','relative_humidity'); + #IsRaining + #IsSnowing + #RainTotal + #RainRate + + #use Data::Dumper; + #print Dumper %wunderground_data; + + &Weather_Common::populate_internet_weather(\%wunderground_data, $config_parms{weather_wunderground_elements}); + &Weather_Common::weather_updated; + + } else { + print_log "WUnderground: ERROR! Received a station ID we didn't want. Aborting."; + } + +} + +sub weather_wunderground_addelem { + my ($w_root,$w_dest,$w_src) = @_; + if(my $w_srcval=$w_root->first_child_text("$w_src")) { + $wunderground_data{$w_dest}=$w_srcval; + push(@wunderground_keys, $w_dest); + } +} \ No newline at end of file From bfd6700ec53b9acd5eb927a235db80decdf50ec1 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Tue, 1 Oct 2013 14:24:51 -0400 Subject: [PATCH 006/209] fix weather station change --- code/common/weather_wunderground.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/common/weather_wunderground.pl b/code/common/weather_wunderground.pl index bb324a9a6..fc68efeac 100644 --- a/code/common/weather_wunderground.pl +++ b/code/common/weather_wunderground.pl @@ -46,7 +46,6 @@ $wunderground_getweather_file=$config_parms{data_dir}.'/web/weather_wunderground_getweather.xml'; $wunderground_url='http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID='.$wunderground_stationid; - set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_getweather_file"}; #$p_weather_wunderground_getweather-set_output("$wunderground_getweather_file"); &trigger_set('($New_Minute_10) or $Reload', "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') @@ -56,6 +55,7 @@ my $wunderground_state = 'blank'; if ($wunderground_state = $v_wunderground ->{said}) { if ($wunderground_state eq 'getweather'){ + set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_getweather_file"}; start $p_weather_wunderground_getweather; } } @@ -120,4 +120,4 @@ sub weather_wunderground_addelem { $wunderground_data{$w_dest}=$w_srcval; push(@wunderground_keys, $w_dest); } -} \ No newline at end of file +} From 4f611bb1feca6c14070edcf9c975b306d3c69a41 Mon Sep 17 00:00:00 2001 From: Steve Switzer Date: Tue, 1 Oct 2013 23:56:51 -0400 Subject: [PATCH 007/209] Fix trigger creation, add debug option. --- code/common/weather_wunderground.pl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/common/weather_wunderground.pl b/code/common/weather_wunderground.pl index fc68efeac..a67795197 100644 --- a/code/common/weather_wunderground.pl +++ b/code/common/weather_wunderground.pl @@ -25,6 +25,7 @@ Special Thanks to: Bruce Winter - MH Everyone else - GPL examples to learn from + J. Serack & David Norwood - Weatherbug code template @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut @@ -48,7 +49,7 @@ #$p_weather_wunderground_getweather-set_output("$wunderground_getweather_file"); - &trigger_set('($New_Minute_10) or $Reload', "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') + &trigger_set(qq|time_cron('*/15 * * * *') or \$Reload|, "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') unless &trigger_get('Update current weather conditions via wunderground'); } @@ -72,7 +73,7 @@ my $w_stationid = $root->first_child_text("station_id"); - print_log "WUnderground: Received stationid: $w_stationid\n"; + print_log "WUnderground: Received stationid: $w_stationid" if $Debug{wunderground}; if($config_parms{wunderground_stationid} eq $w_stationid) { %wunderground_data={}; @@ -117,6 +118,7 @@ sub weather_wunderground_addelem { my ($w_root,$w_dest,$w_src) = @_; if(my $w_srcval=$w_root->first_child_text("$w_src")) { + print_log sprintf("WUnderground: Data: %15s = %8s (%s)",$w_dest,$w_srcval,$w_src) if $Debug{wunderground}; $wunderground_data{$w_dest}=$w_srcval; push(@wunderground_keys, $w_dest); } From 475e29373c0f462caad3a3dce90069d2c45467ab Mon Sep 17 00:00:00 2001 From: Pmatis Date: Tue, 8 Oct 2013 12:37:00 -0400 Subject: [PATCH 008/209] Adding weather... --- code/common/weather_wunderground.pl | 101 +++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/code/common/weather_wunderground.pl b/code/common/weather_wunderground.pl index fc68efeac..748177890 100644 --- a/code/common/weather_wunderground.pl +++ b/code/common/weather_wunderground.pl @@ -31,42 +31,85 @@ # noloop=start use Weather_Common; use XML::Twig; - my $wunderground_getweather_file; - my $wunderground_stationid=$config_parms{wunderground_stationid}; - my $wunderground_url; - $p_weather_wunderground_getweather = new Process_Item(); + my $wunderground_file_getweather; + my $wunderground_file_getforecast; + $p_weather_wunderground_getweather = new Process_Item(); + $p_weather_wunderground_getforecast = new Process_Item(); - my $wunderground_states = 'getweather,debug'; + my $wunderground_states = 'getweather,getforecast,debug'; $v_wunderground = new Voice_Cmd("wunderground [$wunderground_states]"); # noloop=stop if ($Reload) { - $wunderground_stationid=$config_parms{wunderground_stationid}; - $wunderground_stationid='KNYROCHE41' unless $wunderground_stationid; - $wunderground_getweather_file=$config_parms{data_dir}.'/web/weather_wunderground_getweather.xml'; - $wunderground_url='http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID='.$wunderground_stationid; + $wunderground_file_getweather=$config_parms{data_dir}.'/web/weather_wunderground_weather.xml'; + $wunderground_file_getforecast=$config_parms{data_dir}.'/web/weather_wunderground_forecast.xml'; + if($config_parms{wunderground_stationid} eq '') { + print_log("Warning: wunderground_stationid is not defined in mh.private.ini."); + } + if($config_parms{wunderground_apikey} eq '') { + print_log("Warning: wunderground_apikey is not defined in mh.private.ini."); + } + if($config_parms{state} eq '') { + print_log("Warning: state is not defined in mh.private.ini."); + } + if($config_parms{wunderground_city} eq '') { + print_log("Warning: wunderground_city is not defined in mh.private.ini."); + } - #$p_weather_wunderground_getweather-set_output("$wunderground_getweather_file"); + #$p_weather_wunderground_getweather-set_output("$wunderground_file_getweather"); &trigger_set('($New_Minute_10) or $Reload', "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') unless &trigger_get('Update current weather conditions via wunderground'); + &trigger_set('($New_Minute && $Minute == 5) or $Reload', "run_voice_cmd 'wunderground getforecast'", 'NoExpire', 'Update forecast via wunderground') + unless &trigger_get('Update forecast via wunderground'); } my $wunderground_state = 'blank'; -if ($wunderground_state = $v_wunderground ->{said}) { - if ($wunderground_state eq 'getweather'){ - set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_getweather_file"}; - start $p_weather_wunderground_getweather; +if ($wunderground_state = $v_wunderground->{said}) { + if ($wunderground_state eq 'debug'){ + #Not implemented + } elsif ($wunderground_state eq 'getweather'){ + if($config_parms{wunderground_stationid} eq '') { + print_log("ERROR: wunderground_stationid is not defined in mh.private.ini."); + } else { + my $wunderground_url='http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID='.$config_parms{wunderground_stationid}; + print_log('Retrieving: '.$wunderground_url); + set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_file_getweather"}; + start $p_weather_wunderground_getweather; + } + } elsif ($wunderground_state eq 'getforecast'){ + my $wundergrounderror=0; + if($config_parms{wunderground_apikey} eq '') { + print_log("ERROR: wunderground_apikey is not defined in mh.private.ini."); + $wundergrounderror++; + } + if($config_parms{state} eq '') { + print_log("ERROR: state is not defined in mh.private.ini."); + $wundergrounderror++; + } + if($config_parms{wunderground_city} eq '') { + print_log("ERROR: wunderground_city is not defined in mh.private.ini."); + $wundergrounderror++; + } + if ($wundergrounderror == 0) { + my $wustate=uc $config_parms{state}; + my $wucity=$config_parms{wunderground_city}; + my $wunderground_url='https://api.wunderground.com/api/'.$config_parms{wunderground_apikey}.'/forecast/q/'.$wustate.'/'.$wucity.'.xml'; + print_log('Retrieving: '.$wunderground_url); + set $p_weather_wunderground_getforecast qq{get_url -quiet "$wunderground_url" "$wunderground_file_getforecast"}; + start $p_weather_wunderground_getforecast; + } } } my(%wunderground_data,@wunderground_keys); + if (done_now $p_weather_wunderground_getweather) { $Weather{wunderground_obsv_valid} = 0; #Set to not valid unless proven - #my $wunderground_xml=file_read $wunderground_getweather_file; + #my $wunderground_xml=file_read $wunderground_file_getweather; print_log "wunderground getweather finished."; my $twig = new XML::Twig; - $twig->parsefile($wunderground_getweather_file); + $twig->parsefile($wunderground_file_getweather); my $root = $twig->root; my $channel = $root->first_child("current_observation"); @@ -114,6 +157,32 @@ } +if (done_now $p_weather_wunderground_getforecast) { + $Weather{wunderground_obsv_valid} = 0; #Set to not valid unless proven + #my $wunderground_xml=file_read $wunderground_file_getweather; + print_log "wunderground getforecast finished."; + my $twig = new XML::Twig; + print_log('Reading file: '.$wunderground_file_getforecast); + $twig->parsefile($wunderground_file_getforecast); + my $root = $twig->root; + my $fcast = $root->first_child("forecast"); + my $txtfcast = $fcast->first_child("txt_forecast"); + my $fcdays = $txtfcast->first_child('forecastdays'); + + my @forecast = $fcdays->children('forecastdays'); + + print "\n\n"; + print $fcdays->text(); + print "\n\n"; + + foreach my $fcday (@forecast) { + print_log($fcday->first_child_text('fcttext')); + } + + use Data::Dumper; + #print Dumper @forecast; +} + sub weather_wunderground_addelem { my ($w_root,$w_dest,$w_src) = @_; if(my $w_srcval=$w_root->first_child_text("$w_src")) { From 5e6d5ae4707cdcbb8e5aa3045b56ec9e4e13442d Mon Sep 17 00:00:00 2001 From: Jon Whitear Date: Sun, 24 Nov 2013 13:14:35 +1100 Subject: [PATCH 009/209] Cbus update to support current CGate --- code/public/cbus.pl | 73 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/code/public/cbus.pl b/code/public/cbus.pl index af913bd00..b6a38ced0 100755 --- a/code/public/cbus.pl +++ b/code/public/cbus.pl @@ -84,6 +84,12 @@ # Changed DEV to DEBUG for commonality. # Monitor and Talker attempt to always run unless in DEBUG state. # +# V3.0.1 2013-11-22 +# Fixed to work with C-Gate Version: v2.9.7 (build 2569), which returns +# cbus addresses in the form NETWORK/APPLICATION/GROUP rather than +# //PROJECT/NETWORK/APPLICATION/GROUP. +# Add logging to aid debugging cbus_builder +# Contributed by Jon Whitear # # How Cgate integrates with MH # @@ -153,7 +159,7 @@ ############################################################################## ############################################################################## ########### ############## -########### Globals, Startup, Menus, Voice COmmands ############## +########### Globals, Startup, Menus, Voice Commands ############## ########### ############## ############################################################################## ############################################################################## @@ -261,9 +267,9 @@ sub cbus_configure { if ($data eq 'Run') { load_def_file(); if (not defined $cbus_def) { - # Their was no cbus def file to load. + # There was no cbus def file to load. # Help out a new user, by auto-building the def file. - # Otherwise, their will be nothing to build. + # Otherwise, there will be nothing to build. print_log "CBus: Builder is initiating scan of CGate"; scan_cgate(); } @@ -315,11 +321,11 @@ sub load_def_file { $cbus_def_filename = $config_parms{code_dir} . "/" . $config_parms{cbus_dat_file}; if (not -e $cbus_def_filename) { - print_log "CBus: Definition file $cbus_def_filename does not exist"; + print_log "CBus: [load_def_file] XML definition file $cbus_def_filename does not exist"; return; } - print_log "CBus: Builder - Loading CBus config from file ". + print_log "CBus: Builder - Loading CBus config from XML file ". $cbus_def_filename; $cbus_def = XMLin($cbus_def_filename, ForceArray => ['mh_group', 'note'], @@ -330,7 +336,7 @@ sub load_def_file { delete $cbus_def->{'Creation_Date'}; delete $cbus_def->{'Version'}; - # print_log Dumper($cbus_def); + #print_log Dumper($cbus_def); } @@ -355,7 +361,7 @@ sub load_def_file { sub scan_cgate { # Initiate scan of CGate data # The scan is controlled by code in the Talker mh main loop code - print_log "CBus: Scanning CGate..."; + print_log "CBus: [scan_cgate] Scanning CGate..."; # Cleanup from any previous scan and initialise flags/counters @cbus_net_list = [ ]; @@ -423,12 +429,16 @@ sub write_def_file { ); # Write the file to disk + print_log "CBus: [write_def_file] Writing XML definition to $cbus_def_filename,"; $xml_file->XMLout($cbus_def, OutputFile => $cbus_def_filename, ); } -#sub dump_cbus_data { +sub dump_cbus_data { + + print_log "CBus: Device list function disabled"; + # # Basic diagnostic routine for dumping the cbus objects hash # my $count = 0; # my $msg = "

CBUS Device Listing


"; @@ -448,7 +458,7 @@ sub write_def_file { # # $msg .= "

List CBus Devices: Listed $count CBus devices

"; # display $msg; -#} +} # @@ -1058,6 +1068,8 @@ sub attempt_level_sync { $msg_code = $2; } +###### Message code 320: Tree information. Returned from the tree command. + if ($msg_code == 320) { if (not $cbus_got_tree_list) { if (not $cbus_units_config) { @@ -1073,6 +1085,7 @@ sub attempt_level_sync { } else { # CGate is listing CBus "groups" if ($cbus_data =~ /end/) { + print_log "CBus: end of CBus scan data, got tree list"; $cbus_got_tree_list = 1; } elsif ($cbus_data =~ /(\/\/.+\/\d+\/\d+\/\d+).+level=(\d+)/) { print_log "CBus: scanned group=$1 at level $2"; @@ -1082,17 +1095,24 @@ sub attempt_level_sync { } } +###### Message code 342: DBGet response (not documented in CGate Server Guide 1.0.) + } elsif ($msg_code == 342) { - if ($cbus_scanning_cgate) { - if ($cbus_data =~ /(\/\/.+\/\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/) { + if ($cbus_scanning_cgate) { + + print_log "CBus: Message 342 response data: $cbus_data"; + + if ($cbus_data =~ /\d+\s+(\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/) { my ($addr, $name) = ($1, $2); + $addr = "//$cbus_project_name/$addr"; + print_log "CBus: Address $addr, name $name"; $cbus_scan_last_addr_seen = $addr; # $name =~ s/ /_/g; Change spaces, depends on user usage... my $addr_type; if ($addr =~ /\/p\/(\d+)/) { # Data is for a CBus device eg. switch, relay, dimmer - $addr_type = 'unit'; + $addr_type = 'unit'; $addr = $1; } else { # Data is for a CBus "group" @@ -1124,9 +1144,12 @@ sub attempt_level_sync { }; } } + print_log "Cbus: end message"; } - } - + } + +###### Message code 300: Object information, for example: 300 1/56/1: level=200 + } elsif ($msg_code == 300) { if ($cbus_data =~ /(sessionID=.+)/) { @@ -1181,9 +1204,13 @@ sub attempt_level_sync { print_log "CBus: UNEXPECTED 300 msg \"$cbus_data\""; } +###### Message code 200: Completed successfully + } elsif ($msg_code == 200) { print_log "CBus: Cmd OK - $cbus_data" if $Debug{cbus}; +###### Message code 201: Service ready + } elsif ($msg_code == 201) { print_log "CBus: Comms established - $cbus_data"; @@ -1204,9 +1231,14 @@ sub attempt_level_sync { eval_with_timer $cmd, 2; } +###### Message code 401: Bad object or device ID + } elsif ($msg_code == 401) { print_log "CBus: $cbus_data"; +###### Message code 408: Indicates that a SET, GET or other method +###### failed for a given object + } elsif ($msg_code == 408) { print_log "CBus: **** Failed Cmd - $cbus_data"; if ($msg_id =~ /\[MisterHouse(\d+)\]/) { @@ -1221,6 +1253,8 @@ sub attempt_level_sync { } } +###### Message code unhandled + } else { print_log "CBus: Cmd port - UNHANDLED: $cbus_data"; } @@ -1251,6 +1285,7 @@ sub attempt_level_sync { } else { # All networks scanned - set completion flag ### FIXME - RichardM test with two networks?? + print_log "Cbus: leaving scanning mode"; $cbus_scanning_cgate = 0; print_log "CBus: CBus server scan complete"; write_def_file(); @@ -1259,17 +1294,19 @@ sub attempt_level_sync { } elsif ($cbus_got_tree_list) { if ($cbus_group_idx < @cbus_group_list) { my $group = $cbus_group_list[$cbus_group_idx++]; + print_log "Cbus: dbget group $group"; set $cbus_talker "dbget $group/TagName"; } elsif ($cbus_unit_idx < @cbus_unit_list) { my $unit = $cbus_unit_list[$cbus_unit_idx++]; + print_log "Cbus: dbget unit $unit"; set $cbus_talker "dbget $unit/TagName"; - } else { - if ($cbus_scan_last_addr_seen eq - $cbus_unit_list[$#cbus_unit_list]) { + } else { + if ($cbus_scan_last_addr_seen eq $cbus_unit_list[$#cbus_unit_list]) { # Tree Scan complete - set tree completion flag - $cbus_scanning_tree = 0; + print_log "Cbus: leaving scanning mode"; + $cbus_scanning_tree = 0; } } From ed6a66548977846487a80d3ae6c4d4a7a5e627a2 Mon Sep 17 00:00:00 2001 From: Jon Whitear Date: Mon, 25 Nov 2013 12:45:07 +1100 Subject: [PATCH 010/209] Cbus update to support new and old message response formats --- code/public/cbus.pl | 112 +++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/code/public/cbus.pl b/code/public/cbus.pl index b6a38ced0..984b96680 100755 --- a/code/public/cbus.pl +++ b/code/public/cbus.pl @@ -85,11 +85,15 @@ # Monitor and Talker attempt to always run unless in DEBUG state. # # V3.0.1 2013-11-22 -# Fixed to work with C-Gate Version: v2.9.7 (build 2569), which returns -# cbus addresses in the form NETWORK/APPLICATION/GROUP rather than -# //PROJECT/NETWORK/APPLICATION/GROUP. -# Add logging to aid debugging cbus_builder -# Contributed by Jon Whitear +# Fixed to work with C-Gate Version: v2.9.7 (build 2569), which returns +# cbus addresses in the form NETWORK/APPLICATION/GROUP rather than +# //PROJECT/NETWORK/APPLICATION/GROUP. +# Add logging to aid debugging cbus_builder +# Contributed by Jon Whitear +# +# V3.0.2 2013-11-25 +# Add support for both formats of return code, i.e. NETWORK/APPLICATION/GROUP +# and //PROJECT/NETWORK/APPLICATION/GROUP. # # How Cgate integrates with MH # @@ -1054,6 +1058,51 @@ sub attempt_level_sync { } } +# +# Add an address or group to the hash +# + +sub add_address_to_hash { + my ($addr, $name) = @_; + my $addr_type; + + if ($addr =~ /\/p\/(\d+)/) { + # Data is for a CBus device eg. switch, relay, dimmer + $addr_type = 'unit'; + $addr = $1; + } else { + # Data is for a CBus "group" + $addr_type = 'group'; + } + + print_log "CBus: Addr $addr is $name of type $addr_type"; + + # Store the CBus name and address in the cbus_def hash + if ($addr_type eq 'group') { + if (not exists $cbus_def->{group}{$addr}) { + print_log "CBus: group not defined yet, ". + "adding $addr, $name"; + $cbus_def->{group}{$addr} = { + name => $name, + note =>["Added by MisterHouse $Date_Now $Time_Now"], + type => 'dimmer', + mh_group => ['CBus'] + }; + # print_log Dumper($cbus_def); + } + } elsif ($addr_type eq 'unit') { + if (not exists $cbus_def->{unit}{$addr}) { + print_log "CBus: unit not defined yet, ". + "adding $addr, $name"; + $cbus_def->{unit}{$addr} = { + name => $name, + note => ["Added by MisterHouse $Date_Now $Time_Now"] + }; + } + } + +} + # # Main MH Loop Code for ***** TALKER ***** # @@ -1103,50 +1152,25 @@ sub attempt_level_sync { print_log "CBus: Message 342 response data: $cbus_data"; if ($cbus_data =~ /\d+\s+(\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/) { + #response matched against "new" format, i.e. network/app/group my ($addr, $name) = ($1, $2); $addr = "//$cbus_project_name/$addr"; - print_log "CBus: Address $addr, name $name"; + $cbus_scan_last_addr_seen = $addr; # $name =~ s/ /_/g; Change spaces, depends on user usage... - - my $addr_type; - if ($addr =~ /\/p\/(\d+)/) { - # Data is for a CBus device eg. switch, relay, dimmer - $addr_type = 'unit'; - $addr = $1; - } else { - # Data is for a CBus "group" - $addr_type = 'group'; - } - - print_log "CBus: Addr $addr is $name of type $addr_type"; - - # Store the CBus name and address in the cbus_def hash - if ($addr_type eq 'group') { - if (not exists $cbus_def->{group}{$addr}) { - print_log "CBus: group not defined yet, ". - "adding $addr, $name"; - $cbus_def->{group}{$addr} = { - name => $name, - note =>["Added by MisterHouse $Date_Now $Time_Now"], - type => 'dimmer', - mh_group => ['CBus'] - }; - # print_log Dumper($cbus_def); - } - } elsif ($addr_type eq 'unit') { - if (not exists $cbus_def->{unit}{$addr}) { - print_log "CBus: unit not defined yet, ". - "adding $addr, $name"; - $cbus_def->{unit}{$addr} = { - name => $name, - note => ["Added by MisterHouse $Date_Now $Time_Now"] - }; - } - } - print_log "Cbus: end message"; + add_address_to_hash($addr, $name); + + } elsif ($cbus_data =~ /(\/\/.+\/\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/) { + #response matched against "old" format, i.e. //project/network/app/group + my ($addr, $name) = ($1, $2); + + $cbus_scan_last_addr_seen = $addr; + # $name =~ s/ /_/g; Change spaces, depends on user usage... + add_address_to_hash($addr, $name); + } - } + print_log "Cbus: end message"; + } ###### Message code 300: Object information, for example: 300 1/56/1: level=200 From b61d5ab20b6dd73b8c10a5c245a83efdb896dd2c Mon Sep 17 00:00:00 2001 From: Jon Whitear Date: Thu, 28 Nov 2013 11:08:18 +1100 Subject: [PATCH 011/209] Cbus update to add debug flag test to logging statements, i.e. reduce logging --- code/public/cbus.pl | 54 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/code/public/cbus.pl b/code/public/cbus.pl index 984b96680..d3948ec54 100755 --- a/code/public/cbus.pl +++ b/code/public/cbus.pl @@ -91,10 +91,13 @@ # Add logging to aid debugging cbus_builder # Contributed by Jon Whitear # -# V3.0.2 2013-11-25 +# V3.0.2 2013-11-25 # Add support for both formats of return code, i.e. NETWORK/APPLICATION/GROUP # and //PROJECT/NETWORK/APPLICATION/GROUP. # +# V3.0.3 2013-11-28 +# Test debug flag for logging statements. +# # How Cgate integrates with MH # # All Cbus objects are defined in a standard XML file (cbus.xml), this file is @@ -247,6 +250,9 @@ sub cbus_configure { $cbus_system_debug = 1; print_log "CBus: DEBUG mode - No CGate communications started"; } + + print_log "CBus: MisterHouse CBus debug mode - additional logging enabled" if $Debug{cbus}; + } @@ -325,11 +331,11 @@ sub load_def_file { $cbus_def_filename = $config_parms{code_dir} . "/" . $config_parms{cbus_dat_file}; if (not -e $cbus_def_filename) { - print_log "CBus: [load_def_file] XML definition file $cbus_def_filename does not exist"; + print_log "CBus: load_def_file() XML definition file $cbus_def_filename does not exist"; return; } - print_log "CBus: Builder - Loading CBus config from XML file ". + print_log "CBus: load_def_file () Loading CBus config from XML file ". $cbus_def_filename; $cbus_def = XMLin($cbus_def_filename, ForceArray => ['mh_group', 'note'], @@ -365,7 +371,7 @@ sub load_def_file { sub scan_cgate { # Initiate scan of CGate data # The scan is controlled by code in the Talker mh main loop code - print_log "CBus: [scan_cgate] Scanning CGate..."; + print_log "CBus: scan_cgate() Scanning CGate..."; # Cleanup from any previous scan and initialise flags/counters @cbus_net_list = [ ]; @@ -380,7 +386,7 @@ sub scan_cgate { if (defined $cbus_project_name) { set $cbus_talker "project load " . $cbus_project_name; set $cbus_talker "project use " . $cbus_project_name; - print_log "CBus: Command - project start " . $cbus_project_name; + print_log "CBus: scan_cgate() Command - project start " . $cbus_project_name; set $cbus_talker "project start " . $cbus_project_name; } @@ -433,7 +439,7 @@ sub write_def_file { ); # Write the file to disk - print_log "CBus: [write_def_file] Writing XML definition to $cbus_def_filename,"; + print_log "CBus: write_def_file() Writing XML definition to $cbus_def_filename,"; $xml_file->XMLout($cbus_def, OutputFile => $cbus_def_filename, ); @@ -486,20 +492,20 @@ sub build_cbus_file { # Setup output filename if ($cbus_build_debug) { - print_log "CBus: Builder - Start CBus build in TEST mode"; + print_log "CBus: build_cbus_file() Start CBus build in TEST mode"; $cbus_file = $config_parms{code_dir} . "/cbus_procedures.pl.test"; } else { - print_log "CBus: Builder - Starting build"; + print_log "CBus: build_cbus_file() Starting build"; $cbus_file = $config_parms{code_dir} . "/cbus_procedures.pl"; } rename ($cbus_file, $cbus_file . '.old') - or print_log "CBus: Builder - Could not backup $cbus_file: $!"; + or print_log "CBus: build_cbus_file() Could not backup $cbus_file: $!"; - print_log "CBus: Builder - Saving CBus configs to $cbus_file"; + print_log "CBus: build_cbus_file() Saving CBus configs to $cbus_file"; open (CF, ">$cbus_file") - or print_log "CBus: Builder - Could not open $cbus_file: $!"; + or print_log "CBus: build_cbus_file() Could not open $cbus_file: $!"; print CF "# Category=CBus_Items\n#\n#\n"; print CF "# Created: $Time_Now, from cbus.xml file: \"$config_parms{cbus_dat_file}\"\n"; @@ -677,9 +683,9 @@ sub build_cbus_file { print CF "#\n#\n# EOF\n#\n#\n"; close (CF) - or print_log "Could not close $cbus_file: $!"; + or print_log "CBbus: build_cbus_file() Could not close $cbus_file: $!"; - print_log "CBus: Builder - Completed CBus build to $cbus_file"; + print_log "CBUs: build_cbus_file() Completed CBus build to $cbus_file"; } @@ -705,6 +711,7 @@ sub build_cbus_file { # Currently set to 5 seconds if ($New_Minute or ($New_Second and $cbus_monitor_retry++ > $CBUS_RETRY_SECS) ) { $cbus_monitor_retry = 0; + print_log "CBus: Restarting CBus Monitor" if $Debug{cbus}; cbus_monitor_start(); } } @@ -908,6 +915,7 @@ sub cbus_monitor_status { # Currently set to 5 seconds if ($New_Minute or ($New_Second and $cbus_talker_retry++ > $CBUS_RETRY_SECS)) { $cbus_talker_retry = 0; + print_log "CBus: Restarting CBus Talker" if $Debug{cbus}; cbus_talker_start(); } } @@ -1075,12 +1083,12 @@ sub add_address_to_hash { $addr_type = 'group'; } - print_log "CBus: Addr $addr is $name of type $addr_type"; + print_log "CBus: add_address_to_hash() Addr $addr is $name of type $addr_type"; # Store the CBus name and address in the cbus_def hash if ($addr_type eq 'group') { if (not exists $cbus_def->{group}{$addr}) { - print_log "CBus: group not defined yet, ". + print_log "CBus: add_address_to_hash() group not defined yet, ". "adding $addr, $name"; $cbus_def->{group}{$addr} = { name => $name, @@ -1092,7 +1100,7 @@ sub add_address_to_hash { } } elsif ($addr_type eq 'unit') { if (not exists $cbus_def->{unit}{$addr}) { - print_log "CBus: unit not defined yet, ". + print_log "CBus: add_address_to_hash() unit not defined yet, ". "adding $addr, $name"; $cbus_def->{unit}{$addr} = { name => $name, @@ -1134,7 +1142,7 @@ sub add_address_to_hash { } else { # CGate is listing CBus "groups" if ($cbus_data =~ /end/) { - print_log "CBus: end of CBus scan data, got tree list"; + print_log "CBus: end of CBus scan data, got tree list" if $Debug{cbus}; $cbus_got_tree_list = 1; } elsif ($cbus_data =~ /(\/\/.+\/\d+\/\d+\/\d+).+level=(\d+)/) { print_log "CBus: scanned group=$1 at level $2"; @@ -1149,7 +1157,7 @@ sub add_address_to_hash { } elsif ($msg_code == 342) { if ($cbus_scanning_cgate) { - print_log "CBus: Message 342 response data: $cbus_data"; + print_log "CBus: Message 342 response data: $cbus_data" if $Debug{cbus}; if ($cbus_data =~ /\d+\s+(\d+\/[a-z\d]+\/\d+)\/TagName=(.+)/) { #response matched against "new" format, i.e. network/app/group @@ -1169,7 +1177,7 @@ sub add_address_to_hash { add_address_to_hash($addr, $name); } - print_log "Cbus: end message"; + print_log "Cbus: end message" if $Debug{cbus}; } ###### Message code 300: Object information, for example: 300 1/56/1: level=200 @@ -1309,7 +1317,7 @@ sub add_address_to_hash { } else { # All networks scanned - set completion flag ### FIXME - RichardM test with two networks?? - print_log "Cbus: leaving scanning mode"; + print_log "Cbus: leaving scanning mode" if $Debug{cbus}; $cbus_scanning_cgate = 0; print_log "CBus: CBus server scan complete"; write_def_file(); @@ -1318,18 +1326,18 @@ sub add_address_to_hash { } elsif ($cbus_got_tree_list) { if ($cbus_group_idx < @cbus_group_list) { my $group = $cbus_group_list[$cbus_group_idx++]; - print_log "Cbus: dbget group $group"; + print_log "Cbus: dbget group $group" if $Debug{cbus}; set $cbus_talker "dbget $group/TagName"; } elsif ($cbus_unit_idx < @cbus_unit_list) { my $unit = $cbus_unit_list[$cbus_unit_idx++]; - print_log "Cbus: dbget unit $unit"; + print_log "Cbus: dbget unit $unit" if $Debug{cbus}; set $cbus_talker "dbget $unit/TagName"; } else { if ($cbus_scan_last_addr_seen eq $cbus_unit_list[$#cbus_unit_list]) { # Tree Scan complete - set tree completion flag - print_log "Cbus: leaving scanning mode"; + print_log "Cbus: leaving scanning mode" if $Debug{cbus}; $cbus_scanning_tree = 0; } } From 55a75ac97b6ad591200d2aa8ef7be73e04405eea Mon Sep 17 00:00:00 2001 From: Michael Stovenour Date: Sun, 22 Dec 2013 09:27:43 -0600 Subject: [PATCH 012/209] Updating download.pod to revision v3.0 --- docs/download.html | 82 ---------------------------------------------- docs/download.pod | 17 +++++++--- 2 files changed, 12 insertions(+), 87 deletions(-) delete mode 100644 docs/download.html diff --git a/docs/download.html b/docs/download.html deleted file mode 100644 index 62b504321..000000000 --- a/docs/download.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - -Download Misterhouse - - - - - - - - - -

- - -

-

-

Download Misterhouse

-

-

-
-

Current

-

Version 2.200 - released on 03/02/2012

- -

-

-
-

Previous Stable

-

Version 2.105 - released on 12/01/2008

- -

-

-
-

Older Versions

- -

-

-
-

Mailing List

-

Subscribe to the Misterhouse release announcement mailing list here: http://lists.sourceforge.net/mailman/listinfo/misterhouse-announce

-

Or join the Misterhouse community mailing list here: http://lists.sourceforge.net/mailman/listinfo/misterhouse-users

-

Last modified 03/02/2013 11:03:55

- - - - diff --git a/docs/download.pod b/docs/download.pod index b22c75b3c..10a9e9aac 100644 --- a/docs/download.pod +++ b/docs/download.pod @@ -1,12 +1,19 @@ +=begin comment + +!!Note that if you change the format or layout of this page you must also +modify the code/common/mh_release.pl file to search the new format!! + +=end comment + =head1 Download Misterhouse -=head1 Current +=head1 Current Stable -B - released on 03/02/2012 +B - released on 06/20/2013 =over -=item * For Linux, Windows and MacOS: L +=item * For Linux, Windows and MacOS: L =item * The Windows only compiled version of Misterhouse is sadly no longer supported. @@ -18,11 +25,11 @@ B - released on 03/02/2012 =head1 Previous Stable -B - released on 12/01/2008 +B - released on 03/02/2012 =over -=item * L +=item * L =back From 24b71a57f26187ffc1dfb54019ec47abaef73ec0 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Thu, 27 Mar 2014 14:10:48 -0700 Subject: [PATCH 013/209] Upped version to v3.1 --- VERSION | 2 +- docs/download.pod | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index f398a2061..06a445799 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0 \ No newline at end of file +3.1 \ No newline at end of file diff --git a/docs/download.pod b/docs/download.pod index f67d43b35..58ea5ed05 100644 --- a/docs/download.pod +++ b/docs/download.pod @@ -9,13 +9,13 @@ modify the code/common/mh_release.pl file to search the new format!! =head1 Current Stable -B - released on 06/20/2013 +B - released on 03/30/2014 =over =item Linux, Windows and MacOS -L +L The Windows only compiled version of Misterhouse is sadly no longer supported. @@ -27,7 +27,7 @@ A large (30MB) zip of optional files (mainly sound files) is here: L - released on 03/02/2012 +B - released on 06/20/2013 =over @@ -36,7 +36,7 @@ B - released on 03/02/2012 =begin HTML -https://api.github.com/repos/hollie/misterhouse/zipball/v2.200 +https://api.github.com/repos/hollie/misterhouse/zipball/v3.0 =end HTML From 61f183946d2d62119395b895bd4bbd0ae238f042 Mon Sep 17 00:00:00 2001 From: KRKeegan Date: Mon, 31 Mar 2014 19:44:57 -0700 Subject: [PATCH 014/209] Fix Release Date --- docs/download.pod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/download.pod b/docs/download.pod index 58ea5ed05..1d402985d 100644 --- a/docs/download.pod +++ b/docs/download.pod @@ -9,7 +9,7 @@ modify the code/common/mh_release.pl file to search the new format!! =head1 Current Stable -B - released on 03/30/2014 +B - released on 03/31/2014 =over From 3b3336b9b9d4a43a4755e085f796fdb5aa226f26 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 12 Oct 2014 20:16:21 +0200 Subject: [PATCH 015/209] Changed link to wiki. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19764f356..b7520e6f0 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Perl open source home automation program. It's fun, it's free, and it's entirely * [Quickstart Guide](https://github.com/hollie/misterhouse/wiki/Getting-started) * [Standard Installation Guide](http://misterhouse.sourceforge.net/install.html) -* [User Wiki](http://misterhouse.wikispaces.com/) +* [User Wiki](https://github.com/hollie/misterhouse/wiki) * [User Mail List misterhouse-users](https://sourceforge.net/p/misterhouse/mailman/misterhouse-users/) * Active Development Repository - [GitHub hollie/misterhouse](https://github.com/hollie/misterhouse) -The MisterHouse wiki with more information is located here: http://misterhouse.wikispaces.com/ \ No newline at end of file +The MisterHouse wiki with more information is located here: http://misterhouse.wikispaces.com/ From fee14bb86050167d5c557b7c21e92c9dc7173756 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Mon, 24 Nov 2014 09:17:55 +0100 Subject: [PATCH 016/209] Removed duplicated link Wikispace is no longer active. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b7520e6f0..76359864d 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,3 @@ Perl open source home automation program. It's fun, it's free, and it's entirely * Active Development Repository - [GitHub hollie/misterhouse](https://github.com/hollie/misterhouse) -The MisterHouse wiki with more information is located here: http://misterhouse.wikispaces.com/ From 033ec7e7e8ae4360ff185c60e2eba6348e3a574a Mon Sep 17 00:00:00 2001 From: rudybrian Date: Tue, 9 Feb 2016 10:59:45 -0800 Subject: [PATCH 017/209] Initial Ecobee module commit Very limited functionality at present and much more needs to be added to make this usable. Currently supports -PIN request and token authentication -Polling for changes to runtime and sensor data Todo -More error checking and appropriate recovery -Add function to update configuration when the revision number changes -Add many more user functions to get/set the data -Add remaining "user friendly" classes to access the data -Clean up and reduce the verbosity of logs when not running in debug mode --- lib/Ecobee.pm | 641 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 lib/Ecobee.pm diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm new file mode 100644 index 000000000..b8055fff0 --- /dev/null +++ b/lib/Ecobee.pm @@ -0,0 +1,641 @@ + +=head1 B + +=head2 SYNOPSIS + +This module allows MisterHouse to communicate with the public Ecobee API which +currently allows interaction with Ecobee's family of thermostats (ecobee3, Smart Si, +EMS, EMS Si and white label Carrier Cor and Bryant Housewise). The current version +of this module only supports the ecobee3, but can be extended to support the other +thermostat models. + +=head2 CONFIGURATION + +Ecobee uses OAuth 2.0 to authorize access to devices associated with your account. +The nice thing is that at any point in the future, you can sign into your Ecobee +account and revoke any access tokens that you have issued. + +To authorize API access for MisterHouse, you will need to perform two steps on +the Ecobee website. + +First: Sign up as a developer on the Ecobee Developer portal +(L). This steps is required to create the +API key used by the application. All thermostats registered with this with this +API key (application) will be visible to MisterHouse, so for security reasons, you +should create your own application and not use someone elses's API key as they will +have access to your thermostats. Once signed up as a developer, the Developer +Dashboard becomes available on the consumer portal +(L) and can be accessed by selecting +I from the three horizontal lines in the upper-right-hand part of the screen. +Create a new application. The name must be globally unique, but is arbitrary for how +MisterHouse interacts with it. Select Authorization Method I, add any other +attributes you desire to include (these are not required), then select I. +Once your application is created, it will be visible in the left-hand part of the screen. +Click on this and it will reveal the API key. Copy this string as it is needed in your +configuration. + +Second: Add the API key string to your mh.private.ini file: + + Ecobee_api_key= + +Create an Ecobee instance in the .mht file, or is user code: + +.mht file: + + CODE, require Ecobee; #noloop + CODE, $ecobee = new Ecobee_Interface(); #noloop + +Explanations of the parameters is contained below in the documentation for each +module. + +=head2 OVERVIEW + +The Ecobee public API is fairly comprehensive and allows access to the majority +of the thermostat's features. Since release, Ecobee has been progressively been +adding and enhancing features. As such, functionality may change and require +updates to this module. + +=head3 ECOBEE_INTERFACE + +This handles the interaction between the Ecobee API servers and MisterHouse. +This is the object that is required. An advanced user could interact with +the Ecobee API solely through this object. + +=head3 ECOBEE_GENERIC + +This provides a generic base for building objects that receive data from the +interface. This object is inherited by all parent and child objects and +in most cases, a user will not need to worry about this object. + +=head3 PARENT ITEMS + +These currently include B and B +This classes provide more specific support for each of the current Ecobee +type of objects. These objects provide all of the access needed to interact +with each of the devices in a user friendly way. + +=head3 CHILD ITEMS + +Currently these are named B.... These are very specific objects +that provide very specific support for individual features on the Ecobee +Thermostats. I have in the past commonly referred to these as child objects. +In general, the state of these objects reports the state of a single parameter +on the thermostat. A few of the objects also offer writable features that allow +changing certain parameters on the thermostat. + +=cut + +package Ecobee; + +# Used solely to provide a consistent logging feature +# +use strict; + +#log levels +my $warn = 1; +my $info = 2; +my $trace = 3; + +sub debug { + my ( $self, $message, $level ) = @_; + $level = 0 if $level eq ''; + my $line = ''; + my @caller = caller(0); + if ( $::Debug{'ecobee'} >= $level || $level == 0 ) { + $line = " at line " . $caller[2] if $::Debug{'ecobee'} >= $trace; + ::print_log( "[" . $caller[0] . "] " . $message . $line ); + } +} + +# +# + +package Ecobee_Interface; + +@Ecobee_Interface::ISA = ('Generic_Item', 'Ecobee'); + +use strict; +use warnings; + +use LWP::UserAgent; +use HTTP::Request::Common qw(POST); +use JSON::XS; +use Data::Dumper; +use URI::Escape; + + +# Notes: +# +# As of 1/31/2016 some API examples using cURL incorrectly show the authorize and token +# endpoints as https://api.ecobee.com/1/authorize and https://api.ecobee.com/1/token. +# However, these are actually https://api.ecobee.com/authorize https://api.ecobee.com/token. +# Other endpoints are versioned and appear to work correctly. + +#todo +# + + +# -------------------- START OF SUBROUTINES -------------------- +# -------------------------------------------------------------- + +our %rest; +$rest{authorize} = "/authorize"; +$rest{token} = "/token"; +$rest{thermostat} = "/1/thermostat"; +$rest{thermostatSummary} = "/1/thermostatSummary"; +$rest{runtimeReport} = "/1/runtimeReport"; +$rest{group} = "/1/group"; + +sub new { + my ( $class, $api_key, $poll_interval, $port_name, $url ) = @_; + my $self = {}; + $port_name = 'Ecobee' if !$port_name; + $url = "https://api.ecobee.com" if !$url; + $poll_interval = 60 if !$poll_interval; + $api_key = $::config_parms{ $port_name . "_api_key" } if !$api_key; + $$self{port_name} = $port_name; + $$self{url} = $url; + $$self{poll_interval} = $poll_interval; + $$self{api_key} = $api_key; + $$self{data} = undef; + $$self{ready} = 0; + $$self{data}->{retry} = 0; + $$self{debug} = 1; + $$self{loglevel} = 1; + $$self{timeout} = 15; + $$self{polling_timer} = new Timer; + $$self{auth_check_timer} = new Timer; + $$self{token_check_timer} = new Timer; + bless $self, $class; + + $self->restore_data('access_token','refresh_token'); + $self->_init(); + return $self; +} + +# This is called every time the timer time elapses +sub _poll_check { + my ($self) = @_; + + main::print_log("[Ecobee] _poll_check initiated"); + #main::run (sub {&Venstar_Colortouch::get_data($self)}); #spawn this off to run in the background + $self->get_data(); +} + +sub get_data { + my ($self) = @_; + + main::print_log("[Ecobee] get_data initiated"); + $self->poll; + $self->process_data; +} + +sub _init { + my ($self) = @_; + + # Start a timer to check if we are authenticated + my $action = sub { $self->_check_auth() }; + $$self{auth_check_timer}->set(1, $action); +} + +# We need to do this asynchronously so we don't block execution that blocks the saved tokens from being restored +sub _check_auth { + my ($self) = @_; + #$$self{access_token} = undef; + #$$self{refresh_token} = undef; + # Check if we have cached access and refresh tokens + if ((defined $$self{access_token}) && (defined $$self{refresh_token})) { + if (($$self{access_token} eq '') || ($$self{refresh_token} eq '')) { + # The access_token or refresh_token are missing. Tell the user and wait + main::print_log( "[Ecobee] Error: Missing tokens. Please reauthenticate the API key with the Ecobee portal"); + $self->_request_pin_auth(); + } else { + # Ok, we have tokens. Make sure they are current, then go get the initial state of the device, then start the time to look for updates + main::print_log( "[Ecobee] We have tokens, lets proceed"); + $self->_thermostat_summary(); + $self->_list_thermostats(); + #$self->print_devices(); + main::print_log( "[Ecobee] Office temp is " . $self->get_temp("Monet Thermostat", "Office") ); + # The basic details should be populated now so we can start to poll + $$self{ready} = 1; + my $action = sub { $self->_poll() }; + $$self{polling_timer}->set($$self{poll_interval}, $action); + } + } else { + # If we don't have tokens, we need the user to request a PIN and we need to wait until they request it + # The access_token or refresh_token are undefined. This is probably the first run. Tell the user and wait + main::print_log( "[Ecobee] Error: Token variables undefined. Please authenticate the PIN with the Ecobee portal. A request for a new PIN will follow this message."); + $self->_request_pin_auth(); + } + # If we have tokens, go get the initial state of the device, then start the time to look for updates + +} + + +# We don't have a valid set of tokens, so request a new PIN +sub _request_pin_auth { + my ($self) = @_; + my ($isSuccessResponse1, $keyparams) = $self->_get_JSON_data("GET", "authorize", "?response_type=ecobeePin&client_id=" . $$self{api_key} . "&scope=smartWrite"); + if (!$isSuccessResponse1) { + # something has gone wrong, we should probably exit + main::print_log( "[Ecobee]: Error, failed to get PIN! Have you entered a valid API key?"); + } else { + # print the PIN to the log so the user knows what to enter + main::print_log( "[Ecobee]: New PIN generated ->" . $keyparams->{ecobeePin} . "<-. You have " . $keyparams->{expires_in} . " minutes to add a new application with this PIN on the Ecobee website!"); + # loop until the user enters the PIN or the code expires + my $action = sub { $self->_wait_for_tokens($keyparams->{ecobeePin},$keyparams->{code}, $keyparams->{interval}) }; + $$self{token_check_timer}->set($keyparams->{interval}, $action); + } +} + +# Poll for tokens +sub _wait_for_tokens { + my ($self, $pin, $code, $interval) = @_; + # check for token + my ($isSuccessResponse1, $tokenparams) = $self->_get_JSON_data("POST", "token", "?grant_type=ecobeePin&code=" . $code . "&client_id=" . $$self{api_key}); + if ($isSuccessResponse1 && defined $tokenparams->{access_token} && defined $tokenparams->{refresh_token}) { + # save tokens + $$self{access_token} = $tokenparams->{access_token}; + $$self{refresh_token} = $tokenparams->{refresh_token}; + $self->_check_auth(); + } + # If expired, get a new PIN and start over + if (defined $tokenparams->{error}) { + if ($tokenparams->{error} eq "authorization_expired") { + main::print_log( "[Ecobee]: Warning, PIN has expired, requesting new PIN." ); + $self->_request_pin_auth(); + } elsif ($tokenparams->{error} eq "authorization_pending") { + main::print_log( "[Ecobee]: Authorization is still pending. Please add a new application with PIN ->" . $pin . "<- on Ecobee website before it expires." ); + my $action = sub { $self->_wait_for_tokens($pin, $code, $interval) }; + $$self{token_check_timer}->set($interval, $action); + } + } +} + +# We need to periodically refresh the tokens when they expire +sub _refresh_tokens { + my ($self) = @_; + main::print_log( "[Ecobee]: Refreshing tokens" ); + my ($isSuccessResponse1, $tokenparams) = $self->_get_JSON_data("POST", "token", "?grant_type=refresh_token&refresh_token=" . $$self{refresh_token} . "&client_id=" . $$self{api_key}); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Refresh token response looks good" ); + $$self{access_token} = $tokenparams->{access_token}; + $$self{refresh_token} = $tokenparams->{refresh_token}; + } else { + # We need to handle the case where the refresh token has expired and start a new PIN authorization request. + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the refresh token request" ); + # It looks like the tokens are FUBAR. We need to re-authenticate + $$self{access_token} = undef; + $$self{refresh_token} = undef; + } +} + + +=item C<_list_thermostats()> + +Collects the initial settings and parameters for each device on the Ecobee account. + +=cut + +sub _list_thermostats { + my ($self) = @_; + main::print_log( "[Ecobee]: Listing thermostats..." ); + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{access_token} + ); + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true","includeSettings":"true","includeEvents":"true","includeRuntime":"true","includeSensors":"true"}}'; + my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Thermostat response looks good." ); + foreach my $device (@{$thermoparams->{thermostatList}}) { + # we need to inspect the runtime and remoteSensors + foreach my $key (keys %{$device->{runtime}}) { + $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; + } + + foreach my $index (@{$device->{remoteSensors}}) { + # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} = $index->{id}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} = $index->{name}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{type} = $index->{type}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} = $index->{inUse}; + foreach my $capability (@{$index->{capability}}) { + # capabilities can vary by sensor type + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{id} = $capability->{id}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{type} = $capability->{type}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; + } + } + # We need to do this for the configs and other stuff as well in this function + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); + } + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" ); + } +} + + +=item C<_get_runtime_with_sensors()> + +Gets the runtime and sensor data + +=cut + +sub _get_runtime_with_sensors { + my ($self) = @_; + main::print_log( "[Ecobee]: Getting runtime and sensor data..." ); + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{access_token} + ); + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":"true","includeSensors":"true"}}'; + my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Runtime response looks good." ); + foreach my $device (@{$thermoparams->{thermostatList}}) { + # we need to inspect the runtime and remoteSensors + foreach my $key (keys %{$device->{runtime}}) { + if ($device->{runtime}{$key} ne $$self{data}{devices}{$device->{identifier}}{runtime}{$key}) { + main::print_log( "[Ecobee]: runtime parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{runtime}{$key} . " to " . $device->{runtime}{$key}); + } + $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; + } + foreach my $index (@{$device->{remoteSensors}}) { + # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} = $index->{id}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} = $index->{name}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{type} = $index->{type}; + if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse}) { + if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} ne $index->{inUse}) { + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} . " to " . $index->{inUse} ); + } + } + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} = $index->{inUse}; + foreach my $capability (@{$index->{capability}}) { + # capabilities can vary by sensor type + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{id} = $capability->{id}; + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{type} = $capability->{type}; + if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value}) { + if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} ne $capability->{value}) { + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ":" . $capability->{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} . " to " . $capability->{value} ); + } + } + $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; + } + } + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); + } + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the runtime and sensor request" ); + } +} + + +=item C<_thermostat_summary()> + +Collects the revision numbers from each device on the Ecobee account. Changes to the revision number tell us that something +has changed, and we need to request an update. + +=cut + +sub _thermostat_summary { + my ($self) = @_; + main::print_log( "[Ecobee]: Getting thermostat summary..." ); + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{access_token} + ); + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeEquipmentStatus":true}}'; + my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostatSummary", + '?json=' . uri_escape($json_body), $headers); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Thermostat response looks good. Found " . $thermoparams->{thermostatCount} . " thermostats" ); + foreach my $device (@{$thermoparams->{revisionList}}) { + # format: + # identifier:name:connected:thermoRev:alertsRev:runtimeRev:intervalRev + # Example line: + # 123456789101:MyStat:true:071223012334:080102000000:080102000000 + my @values = split(':',$device); + $$self{data}{devices}{$values[0]}{identifier} = $values[0]; + $$self{data}{devices}{$values[0]}{name} = $values[1]; + + if (defined $$self{data}{devices}{$values[0]}{connected}) { + if ($$self{data}{devices}{$values[0]}{connected} ne $values[2]) { + # This tells us if we are connected to the Ecobee servers + main::print_log( "[Ecobee]: connected has changed from " . $$self{data}{devices}{$values[0]}{connected} . " to " . $values[2] ); + } + } + $$self{data}{devices}{$values[0]}{connected} = $values[2]; + + if (defined $$self{data}{devices}{$values[0]}{thermoRev}) { + if ($$self{data}{devices}{$values[0]}{thermoRev} != $values[3]) { + # This tells us that the thermostat program, hvac mode, settings or configuration has changed + main::print_log( "[Ecobee]: thermoRev has changed from " . $$self{data}{devices}{$values[0]}{thermoRev} . " to " . $values[3] ); + } + } + $$self{data}{devices}{$values[0]}{thermoRev} = $values[3]; + + if (defined $$self{data}{devices}{$values[0]}{alertsRev}) { + if ($$self{data}{devices}{$values[0]}{alertsRev} != $values[4]) { + # This tells us of a new alert is issued or an alert is modified (acked) + main::print_log( "[Ecobee]: alertsRev has changed from " . $$self{data}{devices}{$values[0]}{alertsRev} . " to " . $values[4] ); + } + } + $$self{data}{devices}{$values[0]}{alertsRev} = $values[4]; + + if (defined $$self{data}{devices}{$values[0]}{runtimeRev}) { + if ($$self{data}{devices}{$values[0]}{runtimeRev} != $values[5]) { + # This tells us when the thermostat has sent a new status message, or the equipment state or remote sensor readings have changed + main::print_log( "[Ecobee]: runtimeRev has changed from " . $$self{data}{devices}{$values[0]}{runtimeRev} . " to " . $values[5] ); + $self->_get_runtime_with_sensors(); + } + } + $$self{data}{devices}{$values[0]}{runtimeRev} = $values[5]; + + if (defined $$self{data}{devices}{$values[0]}{intervalRev}) { + if ($$self{data}{devices}{$values[0]}{intervalRev} != $values[6]) { + # This tells us that the thermostat has sent a new status message (every 15 minutes) + main::print_log( "[Ecobee]: intervalRev has changed from " . $$self{data}{devices}{$values[0]}{intervalRev} . " to " . $values[6] ); + } + } + $$self{data}{devices}{$values[0]}{intervalRev} = $values[6]; + + #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " ID is " . $$self{data}{devices}{$values[0]}{identifier} . + # " connected is " . $$self{data}{devices}{$values[0]}{connected} ); + } + # update the status for each device + foreach my $device (@{$thermoparams->{statusList}}) { + my %default_status = ( + 'heatPump' => 0, + 'heatPump2' => 0, + 'heatPump3' => 0, + 'compCool1' => 0, + 'compCool2' => 0, + 'auxHeat1' => 0, + 'auxHeat2' => 0, + 'auxHeat3' => 0, + 'fan' => 0, + 'humidifier' => 0, + 'dehumidifier' => 0, + 'ventilator' => 0, + 'economizer' => 0, + 'compHotWater' => 0, + 'auxHotWater' => 0 + ); + my @values = split(':',$device); + # we probably need to notify something if one of these changes + $$self{data}{devices}{$values[0]}{status} = \%default_status; + if (scalar @values > 1) { + foreach my $index (1..$#values) { + $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; + } + } + #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " fan is " . $$self{data}{devices}{$values[0]}{status}{fan} ); + } + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" ); + } +} + + +sub _poll { + my ($self) = @_; + if ($$self{ready}) { + $self->_thermostat_summary(); + } + # reset the timer + my $action = sub { $self->_poll() }; + $$self{polling_timer}->set($$self{poll_interval}, $action); +} + +#------------------------------------------------------------------------------------ +sub _get_JSON_data { + my ( $self, $type, $endpoint, $args, $headers) = @_; + + my $ua = new LWP::UserAgent(); + $ua->timeout( $$self{timeout} ); + + my $url = $$self{url}; + + my $request = HTTP::Request->new( $type, $url . $rest{$endpoint} . $args, $headers ); + main::print_log( "[Ecobee]: Full request ->" . $request->as_string . "<-") if $$self{debug}; + + my $responseObj = $ua->request($request); + print $responseObj->content . "\n--------------------\n" if $$self{debug}; + + my $responseCode = $responseObj->code; + print 'Response code: ' . $responseCode . "\n" if $$self{debug}; + my $isSuccessResponse = $responseCode < 400; + + my $response; + eval { $response = JSON::XS->new->decode( $responseObj->content ); }; + + # catch crashes: + if ($@) { + print "[Ecobee]: ERROR! JSON parser crashed! $@\n"; + return ('0'); + } + else { + if ( !$isSuccessResponse ) { + if (($endpoint eq "token") && ($responseCode == 401)) { + # Don't bother printing an error as this is expected, because the user hasn't entered the PIN yet + } elsif (($responseCode == 500) && (defined $response->{status}->{code})) { + if ($response->{status}->{code} == 14) { + # Our tokens have expired, we must refresh them and try again + $self->_refresh_tokens(); + # Update the token in the header first + $headers->header('Authorization' => 'Bearer ' . $$self{access_token}); + return $self->_get_JSON_data($type, $endpoint, $args, $headers); + } else { + main::print_log( "[Ecobee]: Warning, failed to get data. Response code $responseCode, error code " . $response->{status}->{code}); + } + } else { + main::print_log( "[Ecobee]: Warning, failed to get data. Response code $responseCode"); + } + } + else { + main::print_log( "[Ecobee]: Got a valid response."); + } + + return ( $isSuccessResponse, $response ); + } +} + + +sub stop_timer { + my ($self) = @_; + + if ( defined $self->{timer} ) { + $self->{timer}->stop() if ( $self->{timer}->active() ); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Warning, stop_timer called but timer undefined" ); + } +} + +sub start_timer { + my ($self) = @_; + + if ( defined $self->{timer} ) { + $self->{timer}->set( $self->{config}->{poll_seconds}, + sub { &Venstar_Colortouch::_poll_check($self) }, -1 ); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Warning, start_timer called but timer undefined" ); + } +} + + +#------------ +# User access methods + + +=item C + +Prints the name and id of all devices found in the Ecobee account. + +=cut + +sub print_devices { + my ($self) = @_; + my $output = "The list of devices reported by Ecobee is:\n"; + foreach my $key (keys %{ $$self{data}{devices} } ) { + $output .= " Name:" . $$self{data}{devices}{$key}{name} . " ID: " . $$self{data}{devices}{$key}{identifier} . "\n"; + } + $self->debug($output); +} + +sub get_temp { + my ($self,$device,$name) = @_; + # Get the id of the given device + my $d_id; + foreach my $key (keys %{$$self{data}{devices}}) { + if ($$self{data}{devices}{$key}{name} eq $device) { + $d_id = $key; + last; + } + } + if ($d_id) { + foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { + if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { + return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{1}{value}; + } + } + return 0; + } else { + return 0; + } +} + +#------------ +# User control methods + + +1; From c0ed7339316053b859bd2522f7022c472aac60fa Mon Sep 17 00:00:00 2001 From: rudybrian Date: Tue, 9 Feb 2016 12:47:55 -0800 Subject: [PATCH 018/209] More cleanup and added get_humidity() user function Removed most of the unused code from the original module that this was based on --- lib/Ecobee.pm | 89 +++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index b8055fff0..91891b91f 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -173,23 +173,6 @@ sub new { return $self; } -# This is called every time the timer time elapses -sub _poll_check { - my ($self) = @_; - - main::print_log("[Ecobee] _poll_check initiated"); - #main::run (sub {&Venstar_Colortouch::get_data($self)}); #spawn this off to run in the background - $self->get_data(); -} - -sub get_data { - my ($self) = @_; - - main::print_log("[Ecobee] get_data initiated"); - $self->poll; - $self->process_data; -} - sub _init { my ($self) = @_; @@ -215,7 +198,8 @@ sub _check_auth { $self->_thermostat_summary(); $self->_list_thermostats(); #$self->print_devices(); - main::print_log( "[Ecobee] Office temp is " . $self->get_temp("Monet Thermostat", "Office") ); + main::print_log( "[Ecobee] Office temp is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "Office")/10) . " degrees F" ); + main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); # The basic details should be populated now so we can start to poll $$self{ready} = 1; my $action = sub { $self->_poll() }; @@ -565,33 +549,6 @@ sub _get_JSON_data { } -sub stop_timer { - my ($self) = @_; - - if ( defined $self->{timer} ) { - $self->{timer}->stop() if ( $self->{timer}->active() ); - } - else { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, stop_timer called but timer undefined" ); - } -} - -sub start_timer { - my ($self) = @_; - - if ( defined $self->{timer} ) { - $self->{timer}->set( $self->{config}->{poll_seconds}, - sub { &Venstar_Colortouch::_poll_check($self) }, -1 ); - } - else { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, start_timer called but timer undefined" ); - } -} - #------------ # User access methods @@ -612,6 +569,13 @@ sub print_devices { $self->debug($output); } + +=item C + +Returns the temperature of the named temperature sensor registered with the given device (thermostat) + +=cut + sub get_temp { my ($self,$device,$name) = @_; # Get the id of the given device @@ -634,6 +598,41 @@ sub get_temp { } } + +=item C + +Returns the humidity of the named sensor registered with the given device (thermostat). +Currently only the main thermostat device, not the remote sensors have humidity + +=cut + +sub get_humidity { + my ($self,$device,$name) = @_; + # Get the id of the given device + my $d_id; + foreach my $key (keys %{$$self{data}{devices}}) { + if ($$self{data}{devices}{$key}{name} eq $device) { + $d_id = $key; + last; + } + } + if ($d_id) { + foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { + if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { + if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{type} eq "humidity") { + return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{value}; + } else { + # This sensor type doesn't have a humidity sensor. Only the main thermostat unit does + return 0; + } + } + } + return 0; + } else { + return 0; + } +} + #------------ # User control methods From d57f5ae6c159ebe8d899e78a0bafe96e8acb920f Mon Sep 17 00:00:00 2001 From: rudybrian Date: Wed, 10 Feb 2016 13:02:35 -0800 Subject: [PATCH 019/209] Now capturing settings and added more user functions Also added ability to get to the system level properties actualHumidity and actualTemperature which are aggregated from the sensors depending on how things are setup. --- lib/Ecobee.pm | 195 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 173 insertions(+), 22 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 91891b91f..da6729b6d 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -128,7 +128,7 @@ use URI::Escape; # # As of 1/31/2016 some API examples using cURL incorrectly show the authorize and token # endpoints as https://api.ecobee.com/1/authorize and https://api.ecobee.com/1/token. -# However, these are actually https://api.ecobee.com/authorize https://api.ecobee.com/token. +# However, these are actually https://api.ecobee.com/authorize and https://api.ecobee.com/token. # Other endpoints are versioned and appear to work correctly. #todo @@ -197,9 +197,21 @@ sub _check_auth { main::print_log( "[Ecobee] We have tokens, lets proceed"); $self->_thermostat_summary(); $self->_list_thermostats(); + + #### + # Testing functions here. These will be removed eventually + ## #$self->print_devices(); + main::print_log( "[Ecobee] actualTemperature is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "actualTemperature")/10) . " degrees F" ); + main::print_log( "[Ecobee] desiredHeat is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Heat")/10) . " degrees F" ); + main::print_log( "[Ecobee] desiredCool is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Cool")/10) . " degrees F" ); main::print_log( "[Ecobee] Office temp is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "Office")/10) . " degrees F" ); + main::print_log( "[Ecobee] actualHumidity is " . $self->get_humidity("Monet Thermostat", "actualHumidity") . "%"); + main::print_log( "[Ecobee] desiredHumidity is " . $self->get_desired_comfort("Monet Thermostat", "Humidity") . "%"); main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); + main::print_log( "[Ecobee] hvacMode is " . $self->get_setting("Monet Thermostat", "hvacMode") ); + #### + # The basic details should be populated now so we can start to poll $$self{ready} = 1; my $action = sub { $self->_poll() }; @@ -312,7 +324,12 @@ sub _list_thermostats { $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; } } - # We need to do this for the configs and other stuff as well in this function + # We need to do this for the settings as well + foreach my $key (keys %{$device->{settings}}) { + $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; + } + # We also need to get the Alerts and Events + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } } else { @@ -321,6 +338,39 @@ sub _list_thermostats { } +=item C<_get_settings()> + +Gets the current settings + +=cut + +sub _get_settings { + my ($self) = @_; + main::print_log( "[Ecobee]: Getting runtime and sensor data..." ); + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{access_token} + ); + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true"}}'; + my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Settings response looks good." ); + # We just asked for the settings this time + foreach my $device (@{$thermoparams->{thermostatList}}) { + foreach my $key (keys %{$device->{settings}}) { + if ($device->{settings}{$key} ne $$self{data}{devices}{$device->{identifier}}{settings}{$key}) { + main::print_log( "[Ecobee]: settings parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{settings}{$key} . " to " . $device->{settings}{$key}); + } + $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; + } + } + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the settings request" ); + } +} + + =item C<_get_runtime_with_sensors()> Gets the runtime and sensor data @@ -354,7 +404,7 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{type} = $index->{type}; if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse}) { if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} ne $index->{inUse}) { - main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} . " to " . $index->{inUse} ); + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} . " to " . $index->{inUse} ); } } $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} = $index->{inUse}; @@ -364,7 +414,7 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{type} = $capability->{type}; if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value}) { if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} ne $capability->{value}) { - main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ":" . $capability->{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} . " to " . $capability->{value} ); + main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . ", capability " . $capability->{type} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ":" . $capability->{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} . " to " . $capability->{value} ); } } $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; @@ -418,6 +468,7 @@ sub _thermostat_summary { if ($$self{data}{devices}{$values[0]}{thermoRev} != $values[3]) { # This tells us that the thermostat program, hvac mode, settings or configuration has changed main::print_log( "[Ecobee]: thermoRev has changed from " . $$self{data}{devices}{$values[0]}{thermoRev} . " to " . $values[3] ); + $self->_get_settings(); } } $$self{data}{devices}{$values[0]}{thermoRev} = $values[3]; @@ -471,11 +522,38 @@ sub _thermostat_summary { ); my @values = split(':',$device); # we probably need to notify something if one of these changes - $$self{data}{devices}{$values[0]}{status} = \%default_status; - if (scalar @values > 1) { - foreach my $index (1..$#values) { - $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; - } + if (defined $$self{data}{devices}{$values[0]}{status}) { + # This isn't our first run, so see if something has changed + my $matched; + for my $stat (keys %default_status) { + if (scalar @values > 1) { + $matched = 0; + foreach my $index (1..$#values) { + if ($values[$index] eq $stat) { + if ($$self{data}{devices}{$values[0]}{status}{$values[$index]} == 0) { + $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; + $matched = 1; + main::print_log( "[Ecobee]: Status $stat has changed from off to on" ); + } + } + } + if ((!$matched) && ($$self{data}{devices}{$values[0]}{status}{$stat} == 1)) { + main::print_log( "[Ecobee]: Status $stat has changed from on to off" ); + } + } else { + if ($$self{data}{devices}{$values[0]}{status}{$stat} != 0) { + $$self{data}{devices}{$values[0]}{status}{$stat} = 0; + main::print_log( "[Ecobee]: Status $stat has changed from on to off" ); + } + } + } + } else { + $$self{data}{devices}{$values[0]}{status} = \%default_status; + if (scalar @values > 1) { + foreach my $index (1..$#values) { + $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; + } + } } #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " fan is " . $$self{data}{devices}{$values[0]}{status}{fan} ); } @@ -587,12 +665,17 @@ sub get_temp { } } if ($d_id) { - foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { - if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { - return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{1}{value}; - } - } - return 0; + if ($name eq "actualTemperature") { + # This is a special case where we return the runtime actualTemperature property instead of a sensor value + return $$self{data}{devices}{$d_id}{runtime}{actualTemperature}; + } else { + foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { + if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { + return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{1}{value}; + } + } + return 0; + } } else { return 0; } @@ -617,22 +700,90 @@ sub get_humidity { } } if ($d_id) { - foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { - if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { - if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{type} eq "humidity") { - return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{value}; - } else { - # This sensor type doesn't have a humidity sensor. Only the main thermostat unit does - return 0; + if ($name eq "actualHumidity") { + # This is a special case where we return the runtime actualHumidity property instead of a sensor value + return $$self{data}{devices}{$d_id}{runtime}{actualHumidity}; + } else { + foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { + if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { + if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{type} eq "humidity") { + return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{value}; + } else { + # This sensor type doesn't have a humidity sensor. Only the main thermostat unit does + return 0; + } } } + return 0; } + } else { return 0; + } +} + + +=item C + +Returns the given runtime property for desiredX comfort setting. Known properties are: + +desiredHeat => Heat +desiredCool => Cool +desiredHumidity => Humidity +desiredDehumidity => Dehumidity +desiredFanMode => FanMode + +=cut + +sub get_desired_comfort { + my ($self,$device,$name) = @_; + # Get the id of the given device + my $d_id; + foreach my $key (keys %{$$self{data}{devices}}) { + if ($$self{data}{devices}{$key}{name} eq $device) { + $d_id = $key; + last; + } + } + if ($d_id) { + if (defined $$self{data}{devices}{$d_id}{runtime}{"desired" . $name}) { + return $$self{data}{devices}{$d_id}{runtime}{"desired" . $name}; + } else { + return 0; + } + } else { + return 0; + } +} + + +=item C + +Returns the given setting property. + +=cut + +sub get_setting { + my ($self,$device,$name) = @_; + # Get the id of the given device + my $d_id; + foreach my $key (keys %{$$self{data}{devices}}) { + if ($$self{data}{devices}{$key}{name} eq $device) { + $d_id = $key; + last; + } + } + if ($d_id) { + if (defined $$self{data}{devices}{$d_id}{settings}{$name}) { + return $$self{data}{devices}{$d_id}{settings}{$name}; + } else { + return 0; + } } else { return 0; } } + #------------ # User control methods From 41d76f37d8c3047751f3d1a133a1db133f035f71 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Wed, 10 Feb 2016 16:28:34 -0800 Subject: [PATCH 020/209] Added support for alerts --- lib/Ecobee.pm | 107 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index da6729b6d..150c3f673 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -210,6 +210,12 @@ sub _check_auth { main::print_log( "[Ecobee] desiredHumidity is " . $self->get_desired_comfort("Monet Thermostat", "Humidity") . "%"); main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); main::print_log( "[Ecobee] hvacMode is " . $self->get_setting("Monet Thermostat", "hvacMode") ); + my $alerts = $self->get_alert("Monet Thermostat"); + if ($alerts) { + foreach my $key (keys %{$alerts}) { + main::print_log( "[Ecobee] Alert " . $key . ": (\"" . $alerts->{$key}{text} . "\")" ); + } + } #### # The basic details should be populated now so we can start to poll @@ -328,7 +334,13 @@ sub _list_thermostats { foreach my $key (keys %{$device->{settings}}) { $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; } - # We also need to get the Alerts and Events + # Get the Alerts (provided as a JSON array) + foreach my $index (@{$device->{alerts}}) { + foreach my $key (keys %{$index}) { + $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}}{$key} = $index->{$key}; + } + } + # We also need to get the Events main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } @@ -346,7 +358,7 @@ Gets the current settings sub _get_settings { my ($self) = @_; - main::print_log( "[Ecobee]: Getting runtime and sensor data..." ); + main::print_log( "[Ecobee]: Getting settings..." ); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -371,6 +383,56 @@ sub _get_settings { } +=item C<_get_alerts()> + +Gets the current alerts + +=cut + +sub _get_alerts { + my ($self) = @_; + main::print_log( "[Ecobee]: Getting alerts..." ); + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{access_token} + ); + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true"}}'; + my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Alerts response looks good." ); + # We just asked for the settings this time + foreach my $device (@{$thermoparams->{thermostatList}}) { + # Look for events that have been acked and are no longer in the array + foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{alertsHash}}) { + my $matched = 0; + foreach my $index (@{$device->{alerts}}) { + if ($key eq $index->{acknowledgeRef}) { + $matched = 1; + } + } + if (!$matched) { + # Alert has been acked + main::print_log( "[Ecobee]: Alert $key: (\"" . $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}{text} . "\") has been acked." ); + $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key} = undef; + } + } + foreach my $index (@{$device->{alerts}}) { + if (defined $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}}) { + # Do we need to see if something has changed? All of the alert properties should be static and the alert dissappears from the JSON array once acked. + } else { + # This is a new alert + main::print_log( "[Ecobee]: A new alert " . $index->{acknowledgeRef} . ": (\"" . $index->{text} . "\") has been generated." ); + $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}} = $index; + } + } + } + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the alerts request" ); + } +} + + =item C<_get_runtime_with_sensors()> Gets the runtime and sensor data @@ -477,6 +539,7 @@ sub _thermostat_summary { if ($$self{data}{devices}{$values[0]}{alertsRev} != $values[4]) { # This tells us of a new alert is issued or an alert is modified (acked) main::print_log( "[Ecobee]: alertsRev has changed from " . $$self{data}{devices}{$values[0]}{alertsRev} . " to " . $values[4] ); + $self->_get_alerts(); } } $$self{data}{devices}{$values[0]}{alertsRev} = $values[4]; @@ -763,7 +826,7 @@ Returns the given setting property. =cut sub get_setting { - my ($self,$device,$name) = @_; + my ($self,$device,$setting) = @_; # Get the id of the given device my $d_id; foreach my $key (keys %{$$self{data}{devices}}) { @@ -773,8 +836,8 @@ sub get_setting { } } if ($d_id) { - if (defined $$self{data}{devices}{$d_id}{settings}{$name}) { - return $$self{data}{devices}{$d_id}{settings}{$name}; + if (defined $$self{data}{devices}{$d_id}{settings}{$setting}) { + return $$self{data}{devices}{$d_id}{settings}{$setting}; } else { return 0; } @@ -784,6 +847,40 @@ sub get_setting { } +=item C + +Returns either the given alert by the given $id, or all of them if $id is undefined + +=cut + +sub get_alert { + my ($self,$device,$id) = @_; + # Get the id of the given device + my $d_id; + foreach my $key (keys %{$$self{data}{devices}}) { + if ($$self{data}{devices}{$key}{name} eq $device) { + $d_id = $key; + last; + } + } + if ($d_id) { + if (defined $id) { + if (defined $$self{data}{devices}{$d_id}{alertsHash}{$id}) { + return $$self{data}{devices}{$d_id}{alertsHash}{$id}; + } else { + # Normal return is a hashref, so this is probably unwise + return 0; + } + } else { + return $$self{data}{devices}{$d_id}{alertsHash}; + } + } else { + # Normal return is a hashref, so this is probably unwise + return 0; + } +} + + #------------ # User control methods From afb31fd47ad6a0a01c0bfbb62399ffb26680307e Mon Sep 17 00:00:00 2001 From: rudybrian Date: Thu, 11 Feb 2016 23:58:08 -0800 Subject: [PATCH 021/209] Added support for events (holds, vacation, etc) Also fixed a bug related to deleting alerts --- lib/Ecobee.pm | 78 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 150c3f673..908638679 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -216,6 +216,13 @@ sub _check_auth { main::print_log( "[Ecobee] Alert " . $key . ": (\"" . $alerts->{$key}{text} . "\")" ); } } + my $events = $self->get_event("Monet Thermostat"); + if ($events) { + foreach my $key (keys %{$events}) { + main::print_log( "[Ecobee] Event: $key" ); + } + } + #### # The basic details should be populated now so we can start to poll @@ -250,6 +257,7 @@ sub _request_pin_auth { } } + # Poll for tokens sub _wait_for_tokens { my ($self, $pin, $code, $interval) = @_; @@ -285,10 +293,12 @@ sub _refresh_tokens { $$self{refresh_token} = $tokenparams->{refresh_token}; } else { # We need to handle the case where the refresh token has expired and start a new PIN authorization request. - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the refresh token request" ); + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the refresh token request. Flushing tokens and requesting a new PIN" ); # It looks like the tokens are FUBAR. We need to re-authenticate $$self{access_token} = undef; $$self{refresh_token} = undef; + $$self{ready} = 0; # This disables polling until the issue can be corrected + $self->_request_pin_auth(); } } @@ -340,7 +350,10 @@ sub _list_thermostats { $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}}{$key} = $index->{$key}; } } - # We also need to get the Events + # We also need to get the Events (provided as a JSON array, but does not contain a unique ID so we have to create our own to make this a hash) + foreach my $index (@{$device->{events}}) { + $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + } main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } @@ -358,12 +371,12 @@ Gets the current settings sub _get_settings { my ($self) = @_; - main::print_log( "[Ecobee]: Getting settings..." ); + main::print_log( "[Ecobee]: Getting settings and events..." ); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true"}}'; + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true","includeEvents":"true"}}'; my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { @@ -376,6 +389,26 @@ sub _get_settings { } $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; } + # create a temporary hash to compare current and previous events + my %temp_events; + foreach my $index (@{$device->{events}}) { + $temp_events{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + } + # Look for new events + foreach my $key (keys %temp_events) { + unless (defined $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}) { + main::print_log( "[Ecobee]: New event added: $key" ); + $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key} = \$temp_events{$key}; + } + } + # Look for deleted events + foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{eventsHash}}) { + unless (defined $temp_events{$key}) { + main::print_log( "[Ecobee]: Event deleted: $key" ); + # This doesn't seem to work (the hashref is still there) + delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}; + } + } } } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the settings request" ); @@ -414,7 +447,7 @@ sub _get_alerts { if (!$matched) { # Alert has been acked main::print_log( "[Ecobee]: Alert $key: (\"" . $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}{text} . "\") has been acked." ); - $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key} = undef; + delete $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}; } } foreach my $index (@{$device->{alerts}}) { @@ -881,6 +914,41 @@ sub get_alert { } +=item C + +Returns either the given event by the given $id, or all of them if $id is undefined + +=cut + +sub get_event { + my ($self,$device,$id) = @_; + # Get the id of the given device + my $d_id; + foreach my $key (keys %{$$self{data}{devices}}) { + if ($$self{data}{devices}{$key}{name} eq $device) { + $d_id = $key; + last; + } + } + if ($d_id) { + if (defined $id) { + if (defined $$self{data}{devices}{$d_id}{eventsHash}{$id}) { + return $$self{data}{devices}{$d_id}{eventsHash}{$id}; + } else { + # Normal return is a hashref, so this is probably unwise + return 0; + } + } else { + return $$self{data}{devices}{$d_id}{eventsHash}; + } + } else { + # Normal return is a hashref, so this is probably unwise + return 0; + } +} + + + #------------ # User control methods From fac514911873275088a965e152d3cbf3910210f4 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Mon, 15 Feb 2016 13:01:08 -0800 Subject: [PATCH 022/209] Added initial support for Ecobee_Generic and Ecobee_Thermostat classes Functions to populate and check $monitor_hash are not complete/working yet --- lib/Ecobee.pm | 297 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 292 insertions(+), 5 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 908638679..8a3012878 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -44,6 +44,7 @@ Create an Ecobee instance in the .mht file, or is user code: CODE, require Ecobee; #noloop CODE, $ecobee = new Ecobee_Interface(); #noloop + CODE, $ecobee_thermo = new Ecobee_Thermostat('First floor', $ecobee); #noloop Explanations of the parameters is contained below in the documentation for each module. @@ -195,8 +196,9 @@ sub _check_auth { } else { # Ok, we have tokens. Make sure they are current, then go get the initial state of the device, then start the time to look for updates main::print_log( "[Ecobee] We have tokens, lets proceed"); - $self->_thermostat_summary(); - $self->_list_thermostats(); + $self->_thermostat_summary(); # This populates the revision numbers and operating state + $self->_list_thermostats(); # This gets the full initial state of each thermostat + $self->_get_groups(); # This gets the groups, their settings and the associated thermostats in each group #### # Testing functions here. These will be removed eventually @@ -222,7 +224,6 @@ sub _check_auth { main::print_log( "[Ecobee] Event: $key" ); } } - #### # The basic details should be populated now so we can start to poll @@ -231,7 +232,7 @@ sub _check_auth { $$self{polling_timer}->set($$self{poll_interval}, $action); } } else { - # If we don't have tokens, we need the user to request a PIN and we need to wait until they request it + # If we don't have tokens, we need to get a PIN and wait until they user registers it # The access_token or refresh_token are undefined. This is probably the first run. Tell the user and wait main::print_log( "[Ecobee] Error: Token variables undefined. Please authenticate the PIN with the Ecobee portal. A request for a new PIN will follow this message."); $self->_request_pin_auth(); @@ -405,7 +406,6 @@ sub _get_settings { foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{eventsHash}}) { unless (defined $temp_events{$key}) { main::print_log( "[Ecobee]: Event deleted: $key" ); - # This doesn't seem to work (the hashref is still there) delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}; } } @@ -466,6 +466,34 @@ sub _get_alerts { } +=item C<_get_groups()> + +Gets the group and grouping data for thermostats + +=cut + +sub _get_groups { + my ($self) = @_; + main::print_log( "[Ecobee]: Getting groups..." ); + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{access_token} + ); + my $json_body = '{"selection":{"selectionType":"registered"}}'; + my ($isSuccessResponse1, $groupparams) = $self->_get_JSON_data("GET", "group", + '?format=json&body=' . uri_escape($json_body), $headers); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Groups response looks good." ); + foreach my $group (@{$groupparams->{groups}}) { + # groupRef is the unique ID + $$self{data}{groups}{$group->{groupRef}} = $group; + } + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the groups request" ); + } +} + + =item C<_get_runtime_with_sensors()> Gets the runtime and sensor data @@ -723,6 +751,52 @@ sub _get_JSON_data { } +=item C + +Used to register actions to be run if a specific value changes. + + $parent - The parent object on which the value should be monitored + (thermostat, remote sensor, group) + $value - The parameter to monitor for changes + $action - A Code Reference to run when the value changes. The code reference + will be passed two arguments, the parameter name and value. + +=cut + +sub register { + my ( $self, $parent, $value, $action ) = @_; + push( @{ $$self{register} }, [ $parent, $value, $action ] ); +} + + +# Walk through the JSON hash and looks for changes from previous json hash if a +# change is found, looks for children to notify and notifies them. + +# This needs to be enhanced a bit from the original Nest version as we are dealing +# with remote sensors that have the same set of properties that are associated with +# the same thermostat and the parameter names are not globally unique. As such, the +# monitor hash structure will be provided as a delimited string that must be parsed +# to get to the same key,value pair for comparison. + +sub compare_json { + my ( $self, $json, $prev_json, $monitor_hash ) = @_; + while ( my ( $key, $value ) = each %{$json} ) { + # Use empty hash reference is it doesn't exist + my $prev_value = {}; + $prev_value = $$prev_json{$key} if exists $$prev_json{$key}; + my $monitior_value = {}; + $monitior_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; + if ( 'HASH' eq ref $value ) { + $self->compare_json( $value, $prev_value, $monitior_value ); + } + elsif ( $value ne $prev_value && ref $monitior_value eq 'ARRAY' ) { + for my $action ( @{$monitior_value} ) { + &$action( $key, $value ); + } + } + } +} + #------------ # User access methods @@ -953,4 +1027,217 @@ sub get_event { # User control methods +package Ecobee_Generic; + +=back + +=head1 B + +=head2 SYNOPSIS + +This is a generic module primarily meant to be inherited by higher level more +user friendly modules. The average user should just ignore this module. + +=cut + +use strict; + +=head2 INHERITS + +C + +=cut + +@Ecobee_Generic::ISA = ( 'Generic_Item', 'Ecobee' ); + +=head2 METHODS + +=over + +=item C + +Creates a new Ecobee_Generic. + $interface - The Ecobee_Interface through which this device can be found. + $parent - The parent interface of this object, if not specified the + the parent will be set to Self. + $monitor_hash - A hash ref, {$value => $action}, where $value is the JSON + value that should be monitored with $action equal to the code + reference that should be run on changes. The hash ref can + contain an infinite number of key value pairs. If no action + is specified, it will use the default data_changed routine. + +=cut + +sub new { + my ( $class, $interface, $parent, $monitor_hash ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{interface} = $interface; + $$self{parent} = $parent; + $$self{parent} = $self if ( $$self{parent} eq '' ); + while ( my ( $monitor_value, $action ) = each %{$monitor_hash} ) { + my $action = sub { $self->data_changed(@_); } if $action eq ''; + $$self{interface}->register( $$self{parent}, $monitor_value, $action ); + } + return $self; +} + + +=item C + +The default action to be called when the JSON data has changed. In most cases +we can ignore the value name and just set the state of the child to new_value. +More sophisticated children can hijack this method to do more complex tasks. + +=cut + +sub data_changed { + my ( $self, $value_name, $new_value ) = @_; + my ( $setby, $response ); + $self->debug( "Data changed called $value_name, $new_value", $info ); + if ( defined $$self{parent}{state_pending}{$value_name} ) { + ( $setby, $response ) = @{ $$self{parent}{state_pending}{$value_name} }; + delete $$self{parent}{state_pending}{$value_name}; + } + else { + $setby = $$self{interface}; + } + $self->set_receive( $new_value, $setby, $response ); +} + + +=item C + +Handles setting the state of the object inside MisterHouse + +=cut + +sub set_receive { + my ( $self, $p_state, $p_setby, $p_response ) = @_; + $self->SUPER::set( $p_state, $p_setby, $p_response ); +} + + +=item C + +Returns the device_id of an object. + +=cut + +sub device_id { + my ($self) = @_; + my $type_hash; + my $parent = $$self{parent}; + for my $device_id ( keys %{$$self{interface}{data}{devices}} ) { + if ( $$parent{name} eq $$self{interface}{data}{devices}{$device_id}{name} ) { + return $device_id; + } + } + $self->debug( + "ERROR, no device by the name " . $$parent{name} . " was found." ); + return 0; +} + + +=item C + +Returns the data contained in value for this device. + +=cut + +sub get_value { + my ( $self, $value ) = @_; + my $device_id = $self->device_id; + if (defined $$self{interface}{data}{devices}{$device_id}{$value}) { + return $$self{interface}{data}{devices}{$device_id}{$value}; + } else { + $self->debug("ERROR, no value for $value on device $device_id was found." ); + return 0; + } +} + + +package Ecobee_Thermostat; + +=back + +=head1 B + +=head2 SYNOPSIS + +This is a high level module for interacting with the Ecobee Thermostat. It is +generally user friendly and contains many functions which are similar to other +thermostat modules. + +The state of this object will be the ambient temperature reported by the +thermostat. This object does not accept set commands. You can use all of the +remaining C including c, c, c to +interact with this object. + +=head2 CONFIGURATION + +Create an Ecobee thermostat instance in the .mht file: + +.mht file: + + CODE, $ecobee_thermo = new Ecobee_Thermostat('Entryway', $ecobee); #noloop + +The arguments: + + 1. The first argument is the I. + The name must be the exact verbatim name as listed on the Ecobee + website. + 2. The second argument is the interface object + +=cut + +use strict; + +=head2 INHERITS + +C + +=cut + +@Ecobee_Thermostat::ISA = ('Ecobee_Generic'); + +=head2 METHODS + +=over + +=item C + +Creates a new Ecobee_Generic. + + $name - The name of the Thermostat + $interface - The interface object + +=cut + +sub new { + my ( $class, $name, $interface ) = @_; + my $monitor_value = "ambient_temperature_"; # This will not work as-is + my $self = new Ecobee_Generic( $interface, '', { $monitor_value => '' } ); + bless $self, $class; + $$self{class} = 'devices', + $$self{type} = 'thermostats', + $$self{name} = $name; + return $self; +} + + +=item C + +Returns the current ambient temperature. + +=cut + +sub get_temp { + my ($self) = @_; + my $runtime = $self->get_value( "runtime"); # This returns a hashref with all the runtime properties + #$self->debug("The actualTemperature is " . $runtime->{actualTemperature} ); + return $runtime->{actualTemperature}; +} + + 1; From 5a8eae686fd2dc2ebee18047e05b42225ed82b4f Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Wed, 17 Feb 2016 21:19:46 +0100 Subject: [PATCH 023/209] Upped version to v4.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 06a445799..5186d0706 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1 \ No newline at end of file +4.0 From 218b9d656f151334b50263c8eb6da7d45c6546ec Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 21 Feb 2016 18:18:12 +0100 Subject: [PATCH 024/209] Upped version to 4.0.1 after merging user feedback on v4.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5186d0706..1454f6ed4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0 +4.0.1 From 9e20c5d34c0bfc99ce39bf92c903522aa3548238 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 21 Feb 2016 20:10:23 +0100 Subject: [PATCH 025/209] Upped to version 4.1 after consensus on mailing list --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1454f6ed4..7d5c902e7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.1 +4.1 From 96be6af6dd81af700f95785eac8c85c04dd25b3d Mon Sep 17 00:00:00 2001 From: rudybrian Date: Tue, 15 Mar 2016 10:19:17 -0700 Subject: [PATCH 026/209] Test code that updates the state of Ecobee_Thermostat objects when the actualTemperature property changes This still needs a lot of work. --- lib/Ecobee.pm | 163 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 140 insertions(+), 23 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 8a3012878..7a0a3022a 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -123,7 +123,7 @@ use HTTP::Request::Common qw(POST); use JSON::XS; use Data::Dumper; use URI::Escape; - +use Storable 'dclone'; # Notes: # @@ -227,6 +227,7 @@ sub _check_auth { #### # The basic details should be populated now so we can start to poll + $self->convert_to_ids( $$self{monitor} ); $$self{ready} = 1; my $action = sub { $self->_poll() }; $$self{polling_timer}->set($$self{poll_interval}, $action); @@ -384,12 +385,21 @@ sub _get_settings { main::print_log( "[Ecobee]: Settings response looks good." ); # We just asked for the settings this time foreach my $device (@{$thermoparams->{thermostatList}}) { + # Save the previous settings + $$self{prev_data}{devices}{$device->{identifier}}{settings} = $$self{data}{devices}{$device->{identifier}}{settings}; + foreach my $key (keys %{$device->{settings}}) { if ($device->{settings}{$key} ne $$self{data}{devices}{$device->{identifier}}{settings}{$key}) { main::print_log( "[Ecobee]: settings parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{settings}{$key} . " to " . $device->{settings}{$key}); } $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; } + # Compare the old with the new + #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{settings}, $$self{prev_data}{devices}{$device->{identifier}}{settings}, $$self{monitor}{settings} ); + + # Save the previous events + $$self{prev_data}{devices}{$device->{identifier}}{eventsHash} = $$self{data}{devices}{$device->{identifier}}{eventsHash}; + # create a temporary hash to compare current and previous events my %temp_events; foreach my $index (@{$device->{events}}) { @@ -409,6 +419,8 @@ sub _get_settings { delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}; } } + # Compare the old with the new + #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{eventsHash}, $$self{prev_data}{devices}{$device->{identifier}}{eventsHash}, $$self{monitor}{eventsHash} ); } } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the settings request" ); @@ -436,6 +448,8 @@ sub _get_alerts { main::print_log( "[Ecobee]: Alerts response looks good." ); # We just asked for the settings this time foreach my $device (@{$thermoparams->{thermostatList}}) { + # Save the previous alerts + $$self{prev_data}{devices}{$device->{identifier}}{alertsHash} = dclone $$self{data}{devices}{$device->{identifier}}{alertsHash}; # Look for events that have been acked and are no longer in the array foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{alertsHash}}) { my $matched = 0; @@ -459,6 +473,8 @@ sub _get_alerts { $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}} = $index; } } + # Compare the old with the new + #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{alertsHash}, $$self{prev_data}{devices}{$device->{identifier}}{alertsHash}, $$self{monitor}{alertsHash} ); } } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the alerts request" ); @@ -512,7 +528,10 @@ sub _get_runtime_with_sensors { '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { main::print_log( "[Ecobee]: Runtime response looks good." ); - foreach my $device (@{$thermoparams->{thermostatList}}) { + + foreach my $device (@{$thermoparams->{thermostatList}}) { + # Save the previous runtime + $$self{prev_data}{devices}{$device->{identifier}}{runtime} = dclone $$self{data}{devices}{$device->{identifier}}{runtime}; # we need to inspect the runtime and remoteSensors foreach my $key (keys %{$device->{runtime}}) { if ($device->{runtime}{$key} ne $$self{data}{devices}{$device->{identifier}}{runtime}{$key}) { @@ -520,6 +539,16 @@ sub _get_runtime_with_sensors { } $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; } + # Compare the old with the new + main::print_log( "[Ecobee]: runtime monitor -pre-"); + print "*** Object *** \n"; + print Data::Dumper::Dumper( \$self->{monitor}); + print "*** Object *** \n"; + #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}}{runtime} ); + $self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}} ); + + # Save the previous remoteSensorsHash + $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash} = dclone $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}; foreach my $index (@{$device->{remoteSensors}}) { # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} = $index->{id}; @@ -542,6 +571,8 @@ sub _get_runtime_with_sensors { } $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; } + # Compare the old with the new, but we need to handle this differently since it is nested (a child of a child) + # $self->compare_data( $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{monitor}{remoteSensorsHash} ); } main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } @@ -647,6 +678,8 @@ sub _thermostat_summary { my @values = split(':',$device); # we probably need to notify something if one of these changes if (defined $$self{data}{devices}{$values[0]}{status}) { + # Save the previous status + $$self{prev_data}{devices}{$values[0]}{status} = dclone $$self{data}{devices}{$values[0]}{status}; # This isn't our first run, so see if something has changed my $matched; for my $stat (keys %default_status) { @@ -662,15 +695,17 @@ sub _thermostat_summary { } } if ((!$matched) && ($$self{data}{devices}{$values[0]}{status}{$stat} == 1)) { - main::print_log( "[Ecobee]: Status $stat has changed from on to off" ); + main::print_log( "[Ecobee]: C1 Status $stat has changed from on to off" ); } } else { if ($$self{data}{devices}{$values[0]}{status}{$stat} != 0) { $$self{data}{devices}{$values[0]}{status}{$stat} = 0; - main::print_log( "[Ecobee]: Status $stat has changed from on to off" ); + main::print_log( "[Ecobee]: C2 Status $stat has changed from on to off" ); } } - } + } + # Compare the old with the new. This may not work as-is. We also need to consider the initial value. + #$self->compare_data( $$self{data}{devices}{$values[0]}{status}, $$self{prev_data}{devices}{$values[0]}{status}, $$self{monitor}{status} ); } else { $$self{data}{devices}{$values[0]}{status} = \%default_status; if (scalar @values > 1) { @@ -765,38 +800,75 @@ Used to register actions to be run if a specific value changes. sub register { my ( $self, $parent, $value, $action ) = @_; + main::print_log( "[Ecobee]: interface registering value $value"); push( @{ $$self{register} }, [ $parent, $value, $action ] ); } -# Walk through the JSON hash and looks for changes from previous json hash if a +# Walk through the data hash and looks for changes from previous hash if a # change is found, looks for children to notify and notifies them. -# This needs to be enhanced a bit from the original Nest version as we are dealing +# This needs to be enhanced a bit from the original Nest version as we are also dealing # with remote sensors that have the same set of properties that are associated with # the same thermostat and the parameter names are not globally unique. As such, the -# monitor hash structure will be provided as a delimited string that must be parsed -# to get to the same key,value pair for comparison. - -sub compare_json { - my ( $self, $json, $prev_json, $monitor_hash ) = @_; - while ( my ( $key, $value ) = each %{$json} ) { +# monitor hash structure will be provided as a nested hash that is traversed with the +# data hash. Unlike the Nest, different data comes in at different times and would false +# trigger if run at the top level of the data hash each time as it would compare previously +# evaluated data each run + +sub compare_data { + my ( $self, $data, $prev_data, $monitor_hash ) = @_; + main::print_log( "[Ecobee]: starting execution within compare_data()"); + while ( my ( $key, $value ) = each %{$data} ) { # Use empty hash reference is it doesn't exist my $prev_value = {}; - $prev_value = $$prev_json{$key} if exists $$prev_json{$key}; - my $monitior_value = {}; - $monitior_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; - if ( 'HASH' eq ref $value ) { - $self->compare_json( $value, $prev_value, $monitior_value ); + $prev_value = $$prev_data{$key} if exists $$prev_data{$key}; + my $monitor_value = {}; + $monitor_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; + if ($key eq "actualTemperature") { + main::print_log( "[Ecobee]: --AT-- I am evaluating key $key, value $value, prev_value $prev_value ref monitor_value " . ref $monitor_value ); + main::print_log( "[Ecobee]: --AT2-- monitor_value = $monitor_value"); + } + if ( ref $value eq 'HASH') { + $self->compare_data( $value, $prev_value, $monitor_value ); } - elsif ( $value ne $prev_value && ref $monitior_value eq 'ARRAY' ) { - for my $action ( @{$monitior_value} ) { + elsif ( ($value ne $prev_value) && (ref $monitor_value eq 'ARRAY') ) { + for my $action ( @{$monitor_value} ) { + main::print_log( "[Ecobee]: I am running action for key $key, value $value"); &$action( $key, $value ); } } } } +# Converts the names in the register hash to IDs, and then puts them into +# the monitor hash. +sub convert_to_ids { + my ($self) = @_; + for my $array_ref ( @{ $$self{register} } ) { + my ( $parent, $value, $action ) = @{$array_ref}; + $self->debug( "Ecobee Initial data load convert_to_ids " . $value ); + my $device_id = $parent->device_id(); + if ( $$parent{type} eq 'sensor') { + push( + @{ + $$self{monitor}{$device_id}{ $parent->sensor_id() }{$value} + }, + $action + ); + } + else { + push( + @{ + $$self{monitor}{$device_id}{$value} + }, + $action + ); + } + } + delete $$self{register}; +} + #------------ # User access methods @@ -1076,6 +1148,7 @@ sub new { $$self{parent} = $parent; $$self{parent} = $self if ( $$self{parent} eq '' ); while ( my ( $monitor_value, $action ) = each %{$monitor_hash} ) { + main::print_log( "[Ecobee]: new Ecobee_Generic registering monitor_value $monitor_value"); my $action = sub { $self->data_changed(@_); } if $action eq ''; $$self{interface}->register( $$self{parent}, $monitor_value, $action ); } @@ -1139,6 +1212,31 @@ sub device_id { } +=item C + +Returns the sensor_id of an object. + +=cut + +sub sensor_id { + my ($self) = @_; + my $type_hash; + my $parent = $$self{parent}; + for my $device_id ( keys %{$$self{interface}{data}{devices}} ) { + if ( $$parent{name} eq $$self{interface}{data}{devices}{$device_id}{name} ) { + foreach my $sensor_id ( keys %{$$self{interface}{data}{devices}{$device_id}{remoteSensorsHash}} ) { + if ($$parent{sensor_name} eq $$self{interface}{data}{devices}{$device_id}{remoteSensorsHash}{$sensor_id}{name}) { + return $sensor_id; + } + } + } + } + $self->debug( + "ERROR, no sensor by the name " . $$parent{sensor_name} . " was found on device " . $$parent{name} . "." ); + return 0; +} + + =item C Returns the data contained in value for this device. @@ -1216,8 +1314,9 @@ Creates a new Ecobee_Generic. sub new { my ( $class, $name, $interface ) = @_; - my $monitor_value = "ambient_temperature_"; # This will not work as-is - my $self = new Ecobee_Generic( $interface, '', { $monitor_value => '' } ); + #my $monitor_value; + #my $self = new Ecobee_Generic( $interface, '', {$monitor_value->{runtime}{actualTemperature} => ''} ); + my $self = new Ecobee_Generic( $interface, '', {"actualTemperature" => ''} ); bless $self, $class; $$self{class} = 'devices', $$self{type} = 'thermostats', @@ -1240,4 +1339,22 @@ sub get_temp { } -1; +=back + +=head1 AUTHOR + +Brian Rudy + +=head1 SEE ALSO + +https://www.ecobee.com/developers/ + +=head1 LICENSE + +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +=cut From 7b646d3dd85f27a0347f91b6d57f5f7622e338f3 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Fri, 18 Mar 2016 14:49:52 -0700 Subject: [PATCH 027/209] Now supports nested hashes in the monitor_hash This allows children to register for changes for any property at any depth in the data hash --- lib/Ecobee.pm | 113 +++++++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 7a0a3022a..877ef65a0 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -227,7 +227,7 @@ sub _check_auth { #### # The basic details should be populated now so we can start to poll - $self->convert_to_ids( $$self{monitor} ); + $self->populate_monitor_hash( $$self{monitor} ); $$self{ready} = 1; my $action = sub { $self->_poll() }; $$self{polling_timer}->set($$self{poll_interval}, $action); @@ -540,12 +540,11 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; } # Compare the old with the new - main::print_log( "[Ecobee]: runtime monitor -pre-"); - print "*** Object *** \n"; - print Data::Dumper::Dumper( \$self->{monitor}); - print "*** Object *** \n"; - #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}}{runtime} ); - $self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}} ); + #main::print_log( "[Ecobee]: runtime monitor -pre-"); + #print "*** Object *** \n"; + #print Data::Dumper::Dumper( \$self->{monitor}); + #print "*** Object *** \n"; + $self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}}{runtime} ); # Save the previous remoteSensorsHash $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash} = dclone $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}; @@ -786,22 +785,22 @@ sub _get_JSON_data { } -=item C +=item C Used to register actions to be run if a specific value changes. $parent - The parent object on which the value should be monitored (thermostat, remote sensor, group) - $value - The parameter to monitor for changes - $action - A Code Reference to run when the value changes. The code reference + $value - The nested hash to monitor for changes. The assigned value is a + Code Reference to run when the value changes. The code reference will be passed two arguments, the parameter name and value. =cut sub register { - my ( $self, $parent, $value, $action ) = @_; - main::print_log( "[Ecobee]: interface registering value $value"); - push( @{ $$self{register} }, [ $parent, $value, $action ] ); + my ( $self, $parent, $value ) = @_; + #main::print_log( "[Ecobee]: interface registering value $value"); + push( @{ $$self{register} }, [ $parent, $value ] ); } @@ -825,10 +824,10 @@ sub compare_data { $prev_value = $$prev_data{$key} if exists $$prev_data{$key}; my $monitor_value = {}; $monitor_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; - if ($key eq "actualTemperature") { - main::print_log( "[Ecobee]: --AT-- I am evaluating key $key, value $value, prev_value $prev_value ref monitor_value " . ref $monitor_value ); - main::print_log( "[Ecobee]: --AT2-- monitor_value = $monitor_value"); - } + #if ($key eq "actualTemperature") { + # main::print_log( "[Ecobee]: --AT-- I am evaluating key $key, value $value, prev_value $prev_value ref monitor_value " . ref $monitor_value ); + # main::print_log( "[Ecobee]: --AT2-- monitor_value = $monitor_value"); + #} if ( ref $value eq 'HASH') { $self->compare_data( $value, $prev_value, $monitor_value ); } @@ -841,35 +840,47 @@ sub compare_data { } } -# Converts the names in the register hash to IDs, and then puts them into -# the monitor hash. -sub convert_to_ids { +# Populate the monitor_hash with device IDs and monitor values in the register for each device +sub populate_monitor_hash { my ($self) = @_; for my $array_ref ( @{ $$self{register} } ) { - my ( $parent, $value, $action ) = @{$array_ref}; - $self->debug( "Ecobee Initial data load convert_to_ids " . $value ); + my ( $parent, $value ) = @{$array_ref}; + #print "***value***\n"; + #print Data::Dumper::Dumper( \$value); + #print "***value***\n"; + #$self->debug( "Ecobee Initial data load convert_to_ids " . $value ); my $device_id = $parent->device_id(); if ( $$parent{type} eq 'sensor') { - push( - @{ - $$self{monitor}{$device_id}{ $parent->sensor_id() }{$value} - }, - $action - ); + my $sensor_id = $parent->sensor_id(); + $$self{monitor}{$device_id}{$sensor_id} = $self->_merge($value, $$self{monitor}{$device_id}{$sensor_id}); } else { - push( - @{ - $$self{monitor}{$device_id}{$value} - }, - $action - ); + $$self{monitor}{$device_id} = $self->_merge($value, $$self{monitor}{$device_id}); } } delete $$self{register}; } +# Merge the source into the dest. This is used to populate the monitor hash. +sub _merge { + my ($self,$source,$dest) = @_; + for my $key (keys %{$source}) { + if ('ARRAY' eq ref $dest->{$key}) { + $self->debug( "Ecobee: adding array element " . $source->{$key} ); + push @{$dest->{$key}}, $source->{$key}; + } elsif ('HASH' eq ref $dest->{$key}) { + $self->debug( "Ecobee: merging $key"); + $self->_merge($source->{$key},$dest->{$key}); + } else { + $self->debug( "Ecobee: assigning value " . $source->{$key} . " to key " . $key); + $dest->{$key} = $source->{$key}; + } + } + return $dest; +} + + #------------ # User access methods @@ -1147,15 +1158,33 @@ sub new { $$self{interface} = $interface; $$self{parent} = $parent; $$self{parent} = $self if ( $$self{parent} eq '' ); - while ( my ( $monitor_value, $action ) = each %{$monitor_hash} ) { - main::print_log( "[Ecobee]: new Ecobee_Generic registering monitor_value $monitor_value"); - my $action = sub { $self->data_changed(@_); } if $action eq ''; - $$self{interface}->register( $$self{parent}, $monitor_value, $action ); - } + + my $monitor_value = $self->_delve($monitor_hash); + $$self{interface}->register( $$self{parent}, $monitor_value); return $self; } +=item C<_delve()> + +Internal function to help populate the monitor_hash with action functions + +=cut + +sub _delve { + my ( $self, $datahash) = @_; + while (my ($key, $value) = each %{$datahash}) { + if (ref $value eq 'HASH') { + # We need to go deeper + $self->_delve($datahash->{$key}); + } else { + $value = [sub { $self->data_changed(@_); }] if $value eq ''; + $datahash->{$key} = $value; + } + } + return $datahash; +} + =item C The default action to be called when the JSON data has changed. In most cases @@ -1314,9 +1343,9 @@ Creates a new Ecobee_Generic. sub new { my ( $class, $name, $interface ) = @_; - #my $monitor_value; - #my $self = new Ecobee_Generic( $interface, '', {$monitor_value->{runtime}{actualTemperature} => ''} ); - my $self = new Ecobee_Generic( $interface, '', {"actualTemperature" => ''} ); + my $monitor_value; + $monitor_value->{runtime}{actualTemperature} = ''; + my $self = new Ecobee_Generic( $interface, '', $monitor_value ); bless $self, $class; $$self{class} = 'devices', $$self{type} = 'thermostats', From f35b2a7950f9d1f31b2743b3c568a60e60fabe12 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Sun, 20 Mar 2016 13:15:08 -0700 Subject: [PATCH 028/209] Added Ecobee_Thermo_Humidity child object This child object allows you to track the state of the actualHumidity property to be utilized within user code. --- lib/Ecobee.pm | 58 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 877ef65a0..14810ddb9 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -45,6 +45,7 @@ Create an Ecobee instance in the .mht file, or is user code: CODE, require Ecobee; #noloop CODE, $ecobee = new Ecobee_Interface(); #noloop CODE, $ecobee_thermo = new Ecobee_Thermostat('First floor', $ecobee); #noloop + CODE, $thermo_humid = new Ecobee_Thermo_Humidity($ecobee_thermo); #noloop Explanations of the parameters is contained below in the documentation for each module. @@ -540,10 +541,10 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; } # Compare the old with the new - #main::print_log( "[Ecobee]: runtime monitor -pre-"); - #print "*** Object *** \n"; - #print Data::Dumper::Dumper( \$self->{monitor}); - #print "*** Object *** \n"; + main::print_log( "[Ecobee]: runtime monitor -pre-"); + print "*** Object *** \n"; + print Data::Dumper::Dumper( \$self->{monitor}); + print "*** Object *** \n"; $self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}}{runtime} ); # Save the previous remoteSensorsHash @@ -867,13 +868,13 @@ sub _merge { my ($self,$source,$dest) = @_; for my $key (keys %{$source}) { if ('ARRAY' eq ref $dest->{$key}) { - $self->debug( "Ecobee: adding array element " . $source->{$key} ); + #$self->debug( "Ecobee: adding array element " . $source->{$key} ); push @{$dest->{$key}}, $source->{$key}; } elsif ('HASH' eq ref $dest->{$key}) { - $self->debug( "Ecobee: merging $key"); + #$self->debug( "Ecobee: merging $key"); $self->_merge($source->{$key},$dest->{$key}); } else { - $self->debug( "Ecobee: assigning value " . $source->{$key} . " to key " . $key); + #$self->debug( "Ecobee: assigning value " . $source->{$key} . " to key " . $key); $dest->{$key} = $source->{$key}; } } @@ -1368,11 +1369,54 @@ sub get_temp { } +package Ecobee_Thermo_Humidity; + +=head1 B + +=head2 SYNOPSIS + +This is a very high level module for viewing with the Ecobee Thermostat Humidity value (actualHumidity) +This type of object is often referred to as a child device. It displays the +current humidity. The object inherits all of the C methods, +including c, c, c. + +=head2 CONFIGURATION + +.mht file: + + CODE, $thermo_humid = new Ecobee_Thermo_Humidity($ecobee_thermo); #noloop + +The only argument required is the thermostat object. + +=head2 INHERITS + +C + +=cut + + +use strict; + +@Ecobee_Thermo_Humidity::ISA = ('Ecobee_Generic'); + +sub new { + my ( $class, $parent ) = @_; + my $monitor_value; + $monitor_value->{runtime}{actualHumidity} = ''; + my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + bless $self, $class; + return $self; +} + + + + =back =head1 AUTHOR Brian Rudy +Originally based on Nest.pm by Kevin Robert Keegan =head1 SEE ALSO From 14584d30e537ee6811c712d220369fb5eb513d30 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Mon, 21 Mar 2016 13:58:44 -0700 Subject: [PATCH 029/209] Added Ecobee_Thermo_HVAC_Status child object This child object allows you to track the status of the thermostat (by means of a vector) to be utilized within user code. --- lib/Ecobee.pm | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 14810ddb9..b569c11aa 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -46,6 +46,7 @@ Create an Ecobee instance in the .mht file, or is user code: CODE, $ecobee = new Ecobee_Interface(); #noloop CODE, $ecobee_thermo = new Ecobee_Thermostat('First floor', $ecobee); #noloop CODE, $thermo_humid = new Ecobee_Thermo_Humidity($ecobee_thermo); #noloop + CODE, $thermo_hvac_status = new Ecobee_Thermo_HVAC_Status($ecobee_thermo); #noloop Explanations of the parameters is contained below in the documentation for each module. @@ -675,7 +676,48 @@ sub _thermostat_summary { 'compHotWater' => 0, 'auxHotWater' => 0 ); + my $status_bit_LUT = { + 'heatPump' => 0x0001, + 'heatPump2' => 0x0002, + 'heatPump3' => 0x0004, + 'compCool1' => 0x0008, + 'compCool2' => 0x0010, + 'auxHeat1' => 0x0020, + 'auxHeat2' => 0x0040, + 'auxHeat3' => 0x0080, + 'fan' => 0x0100, + 'humidifier' => 0x0200, + 'dehumidifier' => 0x0400, + 'ventilator' => 0x0800, + 'economizer' => 0x1000, + 'compHotWater' => 0x2000, + 'auxHotWater' => 0x4000 + }; my @values = split(':',$device); + + # populate the status vector + my $statusvec = 0x0000; + foreach my $index (1..$#values) { + $statusvec |= $status_bit_LUT->{$values[$index]}; + } + if (exists $$self{data}{devices}{$values[0]}{statusvec}{status}) { + $$self{prev_data}{devices}{$values[0]}{statusvec} = dclone $$self{data}{devices}{$values[0]}{statusvec}; + # Since state_now doesn't fire for values of 0, we need to create this artificial "all off" state + if ($statusvec == 0) { + $$self{data}{devices}{$values[0]}{statusvec}{status} = 0x8000; + } else { + $$self{data}{devices}{$values[0]}{statusvec}{status} = $statusvec; + } + $self->compare_data( $$self{data}{devices}{$values[0]}{statusvec}, $$self{prev_data}{devices}{$values[0]}{statusvec}, $$self{monitor}{$values[0]}{statusvec} ); + } else { + # Since state_now doesn't fire for values of 0, we need to create this artificial "all off" state + if ($statusvec == 0) { + $$self{data}{devices}{$values[0]}{statusvec}{status} = 0x8000; + } else { + $$self{data}{devices}{$values[0]}{statusvec}{status} = $statusvec; + } + } + # we probably need to notify something if one of these changes if (defined $$self{data}{devices}{$values[0]}{status}) { # Save the previous status @@ -687,9 +729,9 @@ sub _thermostat_summary { $matched = 0; foreach my $index (1..$#values) { if ($values[$index] eq $stat) { + $matched = 1; if ($$self{data}{devices}{$values[0]}{status}{$values[$index]} == 0) { $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; - $matched = 1; main::print_log( "[Ecobee]: Status $stat has changed from off to on" ); } } @@ -1409,6 +1451,45 @@ sub new { } +package Ecobee_Thermo_HVAC_Status; + +=head1 B + +=head2 SYNOPSIS + +This is a very high level module for viewing the Ecobee Thermostat operating status (state). +This type of object is often referred to as a child device. It displays the +current status (fan, auxHeat1, ventilator, etc) as a vector (bitfield) to convey all the +status data as a single state value. The object inherits all of the C methods, +including c, c, c. + +=head2 CONFIGURATION + +.mht file: + + CODE, $thermo_hvac_status = new Ecobee_Thermo_HVAC_Status($ecobee_thermo); #noloop + +The only argument required is the thermostat object. + +=head2 INHERITS + +C + +=cut + +use strict; + +@Ecobee_Thermo_HVAC_Status::ISA = ('Ecobee_Generic'); + +sub new { + my ( $class, $parent ) = @_; + my $monitor_value; + $monitor_value->{statusvec}{status} = ''; + my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + bless $self, $class; + return $self; +} + =back From 14f7f18a3d5baeba104b389b12392260634a03e7 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Wed, 23 Mar 2016 12:43:49 -0700 Subject: [PATCH 030/209] Added Ecobee_Thermo_Mode child object This child object allows you to track and change the mode of the thermostat within user code. --- lib/Ecobee.pm | 96 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index b569c11aa..63a976395 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -47,6 +47,7 @@ Create an Ecobee instance in the .mht file, or is user code: CODE, $ecobee_thermo = new Ecobee_Thermostat('First floor', $ecobee); #noloop CODE, $thermo_humid = new Ecobee_Thermo_Humidity($ecobee_thermo); #noloop CODE, $thermo_hvac_status = new Ecobee_Thermo_HVAC_Status($ecobee_thermo); #noloop + CODE, $thermo_mode = new Ecobee_Thermo_Mode($ecobee_thermo); #noloop Explanations of the parameters is contained below in the documentation for each module. @@ -388,7 +389,7 @@ sub _get_settings { # We just asked for the settings this time foreach my $device (@{$thermoparams->{thermostatList}}) { # Save the previous settings - $$self{prev_data}{devices}{$device->{identifier}}{settings} = $$self{data}{devices}{$device->{identifier}}{settings}; + $$self{prev_data}{devices}{$device->{identifier}}{settings} = dclone $$self{data}{devices}{$device->{identifier}}{settings}; foreach my $key (keys %{$device->{settings}}) { if ($device->{settings}{$key} ne $$self{data}{devices}{$device->{identifier}}{settings}{$key}) { @@ -397,7 +398,7 @@ sub _get_settings { $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; } # Compare the old with the new - #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{settings}, $$self{prev_data}{devices}{$device->{identifier}}{settings}, $$self{monitor}{settings} ); + $self->compare_data( $$self{data}{devices}{$device->{identifier}}{settings}, $$self{prev_data}{devices}{$device->{identifier}}{settings}, $$self{monitor}{$device->{identifier}}{settings} ); # Save the previous events $$self{prev_data}{devices}{$device->{identifier}}{eventsHash} = $$self{data}{devices}{$device->{identifier}}{eventsHash}; @@ -776,7 +777,7 @@ sub _poll { #------------------------------------------------------------------------------------ sub _get_JSON_data { - my ( $self, $type, $endpoint, $args, $headers) = @_; + my ( $self, $type, $endpoint, $args, $headers, $content) = @_; my $ua = new LWP::UserAgent(); $ua->timeout( $$self{timeout} ); @@ -784,6 +785,7 @@ sub _get_JSON_data { my $url = $$self{url}; my $request = HTTP::Request->new( $type, $url . $rest{$endpoint} . $args, $headers ); + $request->content($content) if defined $content; main::print_log( "[Ecobee]: Full request ->" . $request->as_string . "<-") if $$self{debug}; my $responseObj = $ua->request($request); @@ -1410,6 +1412,46 @@ sub get_temp { return $runtime->{actualTemperature}; } +=item C + +Sets the mode to $state, must be [heat,auxHeatOnly,cool,auto,off] + +=cut + +sub set_hvac_mode { + my ( $self, $state, $p_setby, $p_response ) = @_; + main::print_log( "[Ecobee]: Attempting to set the thermostat mode to $state" ); + $$self{interface}{polling_timer}->pause; + $state = lc($state); + if ( $state ne 'heat' + && $state ne 'auxHeatOnly' + && $state ne 'cool' + && $state ne 'auto' + && $state ne 'off' ) + { + $self->debug( + "set_hvac_mode must be one of: heat, auxHeatOnly, cool, auto, or off. Not $state." + ); + return; + } + $$self{state_pending}{hvacMode} = [ $p_setby, $p_response ]; + # Send the new mode to the API + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{interface}{access_token} + ); + # Note: this will change all thermostats on the account to this mode. The selection needs to be more specific to control just one. + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":""},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; + #my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; + my ($isSuccessResponse1, $modeparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Mode change response looks good" ); + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the mode change request" ); + } + $$self{interface}{polling_timer}->resume; +} + package Ecobee_Thermo_Humidity; @@ -1491,6 +1533,54 @@ sub new { } +package Ecobee_Thermo_Mode; + +=head1 B + +=head2 SYNOPSIS + +This is a very high level module for interacting with the Ecobee Thermostat Mode. +This type of object is often referred to as a child device. It displays the +mode of the thermostat and allows for setting the modes. The object inherits +all of the C methods, including c, c, c, +c. + +=head2 CONFIGURATION + +.mht file: + + CODE, $thermo_mode = new Ecobee_Thermo_Mode($ecobee_thermo); #noloop + +The only argument required is the thermostat object. + +=head2 INHERITS + +C + +=cut + +use strict; + +@Ecobee_Thermo_Mode::ISA = ('Ecobee_Generic'); + +sub new { + my ( $class, $parent ) = @_; + my $monitor_value; + $monitor_value->{settings}{hvacMode} = ''; + my $self = + new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + $$self{states} = [ 'heat', 'auxHeatOnly', 'cool', 'auto', 'off' ]; + bless $self, $class; + return $self; +} + +sub set { + my ( $self, $p_state, $p_setby, $p_response ) = @_; + $self->debug( "Setting $p_state, $p_setby, $p_response", $info ); + $$self{parent}->set_hvac_mode( $p_state, $p_setby, $p_response ); +} + + =back From a0a84634e32a30fabd085af7864fa71989411af0 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Fri, 25 Mar 2016 12:26:32 -0700 Subject: [PATCH 031/209] Added Ecobee_Thermo_Climate child object This child object allows you to track the current climate value (away/home/sleep) of the thermostat and use it within user code. --- lib/Ecobee.pm | 69 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 63a976395..4ae8c7c8b 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -48,6 +48,7 @@ Create an Ecobee instance in the .mht file, or is user code: CODE, $thermo_humid = new Ecobee_Thermo_Humidity($ecobee_thermo); #noloop CODE, $thermo_hvac_status = new Ecobee_Thermo_HVAC_Status($ecobee_thermo); #noloop CODE, $thermo_mode = new Ecobee_Thermo_Mode($ecobee_thermo); #noloop + CODE, $thermo_climate = new Ecobee_Thermo_Climate($ecobee_thermo); #noloop Explanations of the parameters is contained below in the documentation for each module. @@ -321,7 +322,7 @@ sub _list_thermostats { 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true","includeSettings":"true","includeEvents":"true","includeRuntime":"true","includeSensors":"true"}}'; + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true","includeSettings":"true","includeEvents":"true","includeRuntime":"true","includeSensors":"true","includeProgram":"true"}}'; my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { @@ -359,6 +360,9 @@ sub _list_thermostats { foreach my $index (@{$device->{events}}) { $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; } + # Save the program information as well. Note: the schedule and climate info are stored in arrays. Since we need to use this same format to + # modify a schedule or climate, they will be retained in this format + $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } @@ -370,18 +374,18 @@ sub _list_thermostats { =item C<_get_settings()> -Gets the current settings +Gets the current settings, events and program =cut sub _get_settings { my ($self) = @_; - main::print_log( "[Ecobee]: Getting settings and events..." ); + main::print_log( "[Ecobee]: Getting settings, events and programs..." ); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true","includeEvents":"true"}}'; + my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true","includeEvents":"true","includeProgram":"true"}}'; my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { @@ -423,7 +427,16 @@ sub _get_settings { } } # Compare the old with the new - #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{eventsHash}, $$self{prev_data}{devices}{$device->{identifier}}{eventsHash}, $$self{monitor}{eventsHash} ); + #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{eventsHash}, $$self{prev_data}{devices}{$device->{identifier}}{eventsHash}, $$self{monitor}{$device->{identifier}}{eventsHash} ); + + # Save the previous program data + $$self{prev_data}{devices}{$device->{identifier}}{program} = dclone $$self{data}{devices}{$device->{identifier}}{program}; + + # Save the new program data + $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; + + # Compare the old with the new + $self->compare_data( $$self{data}{devices}{$device->{identifier}}{program}, $$self{prev_data}{devices}{$device->{identifier}}{program}, $$self{monitor}{$device->{identifier}}{program} ); } } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the settings request" ); @@ -1441,8 +1454,9 @@ sub set_hvac_mode { 'Authorization' => 'Bearer ' . $$self{interface}{access_token} ); # Note: this will change all thermostats on the account to this mode. The selection needs to be more specific to control just one. - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":""},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; - #my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; + #my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":""},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; + # Note: This will only change the specific device (or devices if more than one is given in CSV format) + my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; my ($isSuccessResponse1, $modeparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); if ($isSuccessResponse1) { main::print_log( "[Ecobee]: Mode change response looks good" ); @@ -1567,8 +1581,7 @@ sub new { my ( $class, $parent ) = @_; my $monitor_value; $monitor_value->{settings}{hvacMode} = ''; - my $self = - new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); $$self{states} = [ 'heat', 'auxHeatOnly', 'cool', 'auto', 'off' ]; bless $self, $class; return $self; @@ -1581,6 +1594,44 @@ sub set { } +package Ecobee_Thermo_Climate; + +=head1 B + +=head2 SYNOPSIS + +This is a very high level module for interacting with the Ecobee Thermostat Climate. +This type of object is often referred to as a child device. It displays the +climate value of the thermostat. The object inherits all of the C +methods, including c, c, c, c. + +=head2 CONFIGURATION + +.mht file: + + CODE, $thermo_climate = new Ecobee_Thermo_Climate($ecobee_thermo); #noloop + +The only argument required is the thermostat object. + +=head2 INHERITS + +C + +=cut + +use strict; + +@Ecobee_Thermo_Climate::ISA = ('Ecobee_Generic'); + +sub new { + my ( $class, $parent ) = @_; + my $monitor_value; + $monitor_value->{program}{currentClimateRef} = ''; + my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + bless $self, $class; + return $self; +} + =back From 6b00005ba1ade154e7b641772962e19ca841ec2a Mon Sep 17 00:00:00 2001 From: rudybrian Date: Thu, 31 Mar 2016 11:27:40 -0700 Subject: [PATCH 032/209] Added/moved todo list --- lib/Ecobee.pm | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 4ae8c7c8b..a41ee840a 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -90,6 +90,30 @@ changing certain parameters on the thermostat. =cut + +# Notes: +# +# As of 1/31/2016 some API examples using cURL incorrectly show the authorize and token +# endpoints as https://api.ecobee.com/1/authorize and https://api.ecobee.com/1/token. +# However, these are actually https://api.ecobee.com/authorize and https://api.ecobee.com/token. +# Other endpoints are versioned and appear to work correctly. + +#todo +# +# -Child items that are registered for state changes are currently only updated after the +# state changes post-initialization, and are are left at a default value on startup until +# this happens +# +# -Add support for creating and cancelling holds +# +# -Add home/away child item. +# +# -Add support for creating and cancelling vacations +# +# -Add support for creating/deleting/modifying climate settings +# + + package Ecobee; # Used solely to provide a consistent logging feature @@ -129,17 +153,6 @@ use Data::Dumper; use URI::Escape; use Storable 'dclone'; -# Notes: -# -# As of 1/31/2016 some API examples using cURL incorrectly show the authorize and token -# endpoints as https://api.ecobee.com/1/authorize and https://api.ecobee.com/1/token. -# However, these are actually https://api.ecobee.com/authorize and https://api.ecobee.com/token. -# Other endpoints are versioned and appear to work correctly. - -#todo -# - - # -------------------- START OF SUBROUTINES -------------------- # -------------------------------------------------------------- From de1f72608af72a54af2ff8c894c66550e99da13e Mon Sep 17 00:00:00 2001 From: rudybrian Date: Thu, 31 Mar 2016 13:16:08 -0700 Subject: [PATCH 033/209] Fixed bug with runtime status parsing. We were not handling the CSV correctly when more than one state was active (e.g. auxHeat1 and fan) --- lib/Ecobee.pm | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index a41ee840a..db5bdc8e3 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -721,11 +721,16 @@ sub _thermostat_summary { 'auxHotWater' => 0x4000 }; my @values = split(':',$device); + my @states; # populate the status vector my $statusvec = 0x0000; - foreach my $index (1..$#values) { - $statusvec |= $status_bit_LUT->{$values[$index]}; + if (scalar @values > 1) { + @states = split(',',$values[1]); + foreach my $index (0..$#states) { + $statusvec |= $status_bit_LUT->{$states[$index]}; + main::print_log( "[Ecobee]: Statusvec=$statusvec, key=" . $states[$index] . ", LUT=" . $status_bit_LUT->{$states[$index]}); + } } if (exists $$self{data}{devices}{$values[0]}{statusvec}{status}) { $$self{prev_data}{devices}{$values[0]}{statusvec} = dclone $$self{data}{devices}{$values[0]}{statusvec}; @@ -754,11 +759,11 @@ sub _thermostat_summary { for my $stat (keys %default_status) { if (scalar @values > 1) { $matched = 0; - foreach my $index (1..$#values) { - if ($values[$index] eq $stat) { + foreach my $index (0..$#states) { + if ($states[$index] eq $stat) { $matched = 1; - if ($$self{data}{devices}{$values[0]}{status}{$values[$index]} == 0) { - $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; + if ($$self{data}{devices}{$values[0]}{status}{$states[$index]} == 0) { + $$self{data}{devices}{$values[0]}{status}{$states[$index]} = 1; main::print_log( "[Ecobee]: Status $stat has changed from off to on" ); } } @@ -778,8 +783,8 @@ sub _thermostat_summary { } else { $$self{data}{devices}{$values[0]}{status} = \%default_status; if (scalar @values > 1) { - foreach my $index (1..$#values) { - $$self{data}{devices}{$values[0]}{status}{$values[$index]} = 1; + foreach my $index (0..$#states) { + $$self{data}{devices}{$values[0]}{status}{$states[$index]} = 1; } } } From 92d29a5e71f3186cfc72494a717fc28a29fc5872 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Fri, 1 Apr 2016 17:00:49 -0700 Subject: [PATCH 034/209] Added functions to set and clear holds --- lib/Ecobee.pm | 111 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index db5bdc8e3..5d4c94245 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -112,7 +112,7 @@ changing certain parameters on the thermostat. # # -Add support for creating/deleting/modifying climate settings # - +# -Reduce logging verbosity and use consistant logging format package Ecobee; @@ -371,7 +371,11 @@ sub _list_thermostats { } # We also need to get the Events (provided as a JSON array, but does not contain a unique ID so we have to create our own to make this a hash) foreach my $index (@{$device->{events}}) { - $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + if (exists $index->{holdClimateRef}) { + $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + } else { + $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name}} = $index; + } } # Save the program information as well. Note: the schedule and climate info are stored in arrays. Since we need to use this same format to # modify a schedule or climate, they will be retained in this format @@ -423,7 +427,11 @@ sub _get_settings { # create a temporary hash to compare current and previous events my %temp_events; foreach my $index (@{$device->{events}}) { - $temp_events{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + if (exists $index->{holdClimateRef}) { + $temp_events{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + } else { + $temp_events{$index->{type} . "-" . $index->{name}} = $index; + } } # Look for new events foreach my $key (keys %temp_events) { @@ -900,10 +908,6 @@ sub compare_data { $prev_value = $$prev_data{$key} if exists $$prev_data{$key}; my $monitor_value = {}; $monitor_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; - #if ($key eq "actualTemperature") { - # main::print_log( "[Ecobee]: --AT-- I am evaluating key $key, value $value, prev_value $prev_value ref monitor_value " . ref $monitor_value ); - # main::print_log( "[Ecobee]: --AT2-- monitor_value = $monitor_value"); - #} if ( ref $value eq 'HASH') { $self->compare_data( $value, $prev_value, $monitor_value ); } @@ -921,10 +925,6 @@ sub populate_monitor_hash { my ($self) = @_; for my $array_ref ( @{ $$self{register} } ) { my ( $parent, $value ) = @{$array_ref}; - #print "***value***\n"; - #print Data::Dumper::Dumper( \$value); - #print "***value***\n"; - #$self->debug( "Ecobee Initial data load convert_to_ids " . $value ); my $device_id = $parent->device_id(); if ( $$parent{type} eq 'sensor') { my $sensor_id = $parent->sensor_id(); @@ -943,13 +943,10 @@ sub _merge { my ($self,$source,$dest) = @_; for my $key (keys %{$source}) { if ('ARRAY' eq ref $dest->{$key}) { - #$self->debug( "Ecobee: adding array element " . $source->{$key} ); push @{$dest->{$key}}, $source->{$key}; } elsif ('HASH' eq ref $dest->{$key}) { - #$self->debug( "Ecobee: merging $key"); $self->_merge($source->{$key},$dest->{$key}); } else { - #$self->debug( "Ecobee: assigning value " . $source->{$key} . " to key " . $key); $dest->{$key} = $source->{$key}; } } @@ -1443,6 +1440,24 @@ sub get_temp { return $runtime->{actualTemperature}; } + +=item C + +Returns the current events. + +=cut + +sub get_events { + my ($self) = @_; + my $eventshash = $self->get_value( "eventsHash"); # This returns a hashref with all the events (including holds) + if (scalar keys %{$eventshash} > 0) { + return $eventshash; + } else { + return; + } +} + + =item C Sets the mode to $state, must be [heat,auxHeatOnly,cool,auto,off] @@ -1477,7 +1492,7 @@ sub set_hvac_mode { my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; my ($isSuccessResponse1, $modeparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Mode change response looks good" ); + main::print_log( "[Ecobee]: Mode change response looks good" ); } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the mode change request" ); } @@ -1485,6 +1500,72 @@ sub set_hvac_mode { } +=item C + +Sets a hold for the properties defined in $state. $state format can be either a temperature hold or a +climate hold. Temperature holds are in the format temperature___ and +climate holds are in the format climate__ + +=cut + +sub set_hold { + my ( $self, $state, $p_setby, $p_response ) = @_; + main::print_log( "[Ecobee]: Attempting to set a thermostat hold to $state" ); + my @s_params = split('_', $state); + my $json_body; + if (($s_params[0] eq 'climate') && (scalar @s_params == 3)) { + # climate hold + $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"setHold","params":{"holdType":"' . $s_params[1] . '","holdClimateRef":' . $s_params[2] . '}}]}'; + } elsif (($s_params[0] eq 'temperature') && (scalar @s_params == 4)) { + # temperature hold + $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"setHold","params":{"holdType":"' . $s_params[1] . '","heatHoldTemp":' . $s_params[2] . ',"coolHoldTemp":' . $s_params[3] . '}}]}'; + } else { + # format is wrong + $self->debug("set_hold state \"$state\" is invalid"); + return; + } + + $$self{state_pending}{hold} = [ $p_setby, $p_response ]; + $$self{interface}{polling_timer}->pause; + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{interface}{access_token} + ); + my ($isSuccessResponse1, $holdparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Set hold response looks good" ); + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the set hold request" ); + } + $$self{interface}{polling_timer}->resume; +} + + +=item C + +Clears the current thermostat hold. + +=cut + +sub clear_hold { + my ($self) = @_; + main::print_log( "[Ecobee]: Attempting to clear thermostat hold" ); + $$self{interface}{polling_timer}->pause; + my $headers = HTTP::Headers->new( + 'Content-Type' => 'text/json', + 'Authorization' => 'Bearer ' . $$self{interface}{access_token} + ); + my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"resumeProgram","params":{"resumeAll":false}}]}'; + my ($isSuccessResponse1, $holdparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); + if ($isSuccessResponse1) { + main::print_log( "[Ecobee]: Clear hold response looks good" ); + } else { + main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the clear hold request" ); + } + $$self{interface}{polling_timer}->resume; +} + + package Ecobee_Thermo_Humidity; =head1 B From 02d9dfbd1d68a6e5488fb21436cb352b78467173 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Wed, 20 Apr 2016 15:21:50 -0700 Subject: [PATCH 035/209] Updated ClimateRef tracking and fixed Ecobee_Thermo_Climate --- lib/Ecobee.pm | 93 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 5d4c94245..2eef3be53 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -104,10 +104,6 @@ changing certain parameters on the thermostat. # state changes post-initialization, and are are left at a default value on startup until # this happens # -# -Add support for creating and cancelling holds -# -# -Add home/away child item. -# # -Add support for creating and cancelling vacations # # -Add support for creating/deleting/modifying climate settings @@ -377,6 +373,11 @@ sub _list_thermostats { $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name}} = $index; } } + # set to empty so we don't crash on dclone + if (scalar @{$device->{events}} == 0) { + my $non_event = {'none' => {'holdClimateRef' => 'none'}}; + $$self{data}{devices}{$device->{identifier}}{eventsHash} = $non_event; + } # Save the program information as well. Note: the schedule and climate info are stored in arrays. Since we need to use this same format to # modify a schedule or climate, they will be retained in this format $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; @@ -422,33 +423,43 @@ sub _get_settings { $self->compare_data( $$self{data}{devices}{$device->{identifier}}{settings}, $$self{prev_data}{devices}{$device->{identifier}}{settings}, $$self{monitor}{$device->{identifier}}{settings} ); # Save the previous events - $$self{prev_data}{devices}{$device->{identifier}}{eventsHash} = $$self{data}{devices}{$device->{identifier}}{eventsHash}; + $$self{prev_data}{devices}{$device->{identifier}}{eventsHash} = dclone $$self{data}{devices}{$device->{identifier}}{eventsHash}; # create a temporary hash to compare current and previous events - my %temp_events; + my $temp_events; foreach my $index (@{$device->{events}}) { if (exists $index->{holdClimateRef}) { - $temp_events{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; + $temp_events->{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; } else { - $temp_events{$index->{type} . "-" . $index->{name}} = $index; + $temp_events->{$index->{type} . "-" . $index->{name}} = $index; } } # Look for new events - foreach my $key (keys %temp_events) { + foreach my $key (keys %{$temp_events}) { unless (defined $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}) { main::print_log( "[Ecobee]: New event added: $key" ); - $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key} = \$temp_events{$key}; + $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key} = $temp_events->{$key}; + if (exists $$self{data}{devices}{$device->{identifier}}{eventsHash}{'none'}) { + # delete the none event if there is a real one + delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{'none'}; + } } } # Look for deleted events foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{eventsHash}}) { - unless (defined $temp_events{$key}) { + unless (defined $temp_events->{$key} || ($key eq 'none')) { main::print_log( "[Ecobee]: Event deleted: $key" ); delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}; } } + # set to null so we don't crash on dclone + if (scalar @{$device->{events}} == 0) { + my $non_event = {'none' => {'holdClimateRef' => 'none'}}; + $$self{data}{devices}{$device->{identifier}}{eventsHash} = $non_event; + } + # Compare the old with the new - #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{eventsHash}, $$self{prev_data}{devices}{$device->{identifier}}{eventsHash}, $$self{monitor}{$device->{identifier}}{eventsHash} ); + $self->compare_data( $$self{data}{devices}{$device->{identifier}}{eventsHash}, $$self{prev_data}{devices}{$device->{identifier}}{eventsHash}, $$self{monitor}{$device->{identifier}}{eventsHash} ); # Save the previous program data $$self{prev_data}{devices}{$device->{identifier}}{program} = dclone $$self{data}{devices}{$device->{identifier}}{program}; @@ -456,6 +467,10 @@ sub _get_settings { # Save the new program data $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; + if ($$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ne $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef}) { + main::print_log( "[Ecobee]: currentClimateRef has changed from " . $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef} . " to " . $$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ); + } + # Compare the old with the new $self->compare_data( $$self{data}{devices}{$device->{identifier}}{program}, $$self{prev_data}{devices}{$device->{identifier}}{program}, $$self{monitor}{$device->{identifier}}{program} ); } @@ -908,6 +923,7 @@ sub compare_data { $prev_value = $$prev_data{$key} if exists $$prev_data{$key}; my $monitor_value = {}; $monitor_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; + #main::print_log( "[Ecobee]: key is $key, value is $value, prev_value is $prev_value, monitor_value is $monitor_value"); if ( ref $value eq 'HASH') { $self->compare_data( $value, $prev_value, $monitor_value ); } @@ -1269,7 +1285,7 @@ More sophisticated children can hijack this method to do more complex tasks. sub data_changed { my ( $self, $value_name, $new_value ) = @_; my ( $setby, $response ); - $self->debug( "Data changed called $value_name, $new_value", $info ); + $self->debug( "Data changed called $value_name, $new_value"); if ( defined $$self{parent}{state_pending}{$value_name} ) { ( $setby, $response ) = @{ $$self{parent}{state_pending}{$value_name} }; delete $$self{parent}{state_pending}{$value_name}; @@ -1457,6 +1473,19 @@ sub get_events { } } +=item C + +Returns the current programs. + +=cut + +sub get_programs { + my ($self) = @_; + my $programs = $self->get_value("program"); # This returns a hashref with all the programs + return $programs; +} + + =item C @@ -1701,8 +1730,9 @@ package Ecobee_Thermo_Climate; This is a very high level module for interacting with the Ecobee Thermostat Climate. This type of object is often referred to as a child device. It displays the -climate value of the thermostat. The object inherits all of the C -methods, including c, c, c, c. +climate value of the thermostat either by the active schedule, or by the ClimateRef +of an overriding hold. The object inherits all of the C +methods, including c, c, c. =head2 CONFIGURATION @@ -1726,11 +1756,44 @@ sub new { my ( $class, $parent ) = @_; my $monitor_value; $monitor_value->{program}{currentClimateRef} = ''; + $monitor_value->{eventsHash}{'hold-auto-home'}{holdClimateRef} = ''; + $monitor_value->{eventsHash}{'hold-auto-away'}{holdClimateRef} = ''; + $monitor_value->{eventsHash}{'none'}{holdClimateRef} = ''; my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); bless $self, $class; return $self; } +# Holds with a holdClimateRef override the value in currentClimateRef +sub data_changed { + my ( $self, $value_name, $new_value ) = @_; + $self->debug( "Data changed called $value_name, $new_value"); + my $state = ''; + if ($value_name eq 'holdClimateRef') { + if ($new_value eq 'none') { + # A hold ws cleared, so we need to set the value back to the currentClimateRef + my $programs = $$self{parent}->get_programs; + $state = $programs->{currentClimateRef}; + } else { + $state = $new_value; + } + } else { + # We need to check if there are any active holds with a holdClimateRef before changing the state + my $events = $$self{parent}->get_events; + if (!exists $events->{'none'}) { + main::print_log( "[Ecobee]: Not setting the state to $new_value because there is still an active hold" ); + return; + } else { + $state = $new_value; + } + } + if ($self->{state} ne $state) { + $self->set_receive($state, $$self{parent}{interface}); + } else { + $self->debug( "Not setting the state to $state because that is already the current value" ); + } +} + =back From b7c96fb13e7e3a7267d6b7e7a3f752f8e3e7231e Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 21 Apr 2016 10:47:31 -0400 Subject: [PATCH 036/209] Allows the control of WGL rain8Net serial sprinkler control modules (see http://www.wgldesigns.com/rain8pc.html) --- lib/Rain8Net.pm | 801 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 801 insertions(+) create mode 100644 lib/Rain8Net.pm diff --git a/lib/Rain8Net.pm b/lib/Rain8Net.pm new file mode 100644 index 000000000..96599ad97 --- /dev/null +++ b/lib/Rain8Net.pm @@ -0,0 +1,801 @@ +=begin comment +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +Rain8Net.pm v1.1 +by Marco Maddalena (mhcoder@nowheremail.com) + +Description: + +Allows the control of WGL rain8Net serial sprinkler control modules +(see http://www.wgldesigns.com/rain8pc.html) +Each unit can control 8 zones, and 8 units can be daisy-chained on 1 serial connection. + + +Methods + status_zone - returns the zone status (1=on,0=off). + Parms: Unit (required) : Numeric - Unit number + Zone (required) : numeric - Zone number + + + + cmd - send Rain8Net Cmd to the unit (some function below alias these commands) + Parms cmd (required) : string - See list + Unit (optional) : numeric - Unit number IF required by the cmd + Zone (optional) : numeric - Zone number IF required by the cmd + + Supported commands are + "COMCheck" - Check communications link. No parameters. + "StatusRequest" - Request an update of zone statuses. Unit number required. + (Note: automatic status request happens via RAIN8NET_AutoStatusRequestTimer value) + "ZoneON" - Turn zone on. Requires unit and Zone number + "ZoneOFF" - Turn zone off. Requires unit and Zone number + "AllModuleOFF" - Turn all zones of a unit off. Requires unit number. + "AllGlobalOff" - Turns all zones of all units. No paramters + + ZoneOn - send the ZoneOn command to Rain8Net + Parms Unit (required) : numeric - Unit number + Zone (required) : numeric - Zone number + + ZoneOff - send the ZoneOff/AllUnitOff/AllGlobalOff command to Rain8Net + Parms Unit (optional) : numeric - Unit number (if not passed, then the equivalent to AllGlobalOff is performed) + Zone (optional) : numeric - Zone number (if not passed, but unit was, the equivalent to AllModuleOff is performed) + + + + + +Examples + + use Rain8Net; + $Rain8 = new Rain8Net; + + + if (time_now("22:00")) + { + print_log "Sprinkler ON for unit 1, Zone 2 [" . $Rain8->get_name(1,2) . "]"; + $Rain8->cmd('ZoneON',1,2); + } + + if (time_now("01:00")) + { + print_log "Sprinkler OFF for unit 1, Zone 2 [" . $Rain8->get_name(1,2) . "]"; + $Rain8->cmd('ZoneOFF',1,2); + } + + if ((time_now("23:00")) && (Rain8->status_zone(1,5))) + { + print_log "Sprinkler OFF for unit 1, Zone 5 [" . $Rain8->get_name(1,5) . "]"; + $Rain8->cmd('ZoneOFF',1,5); + } + + + + +mh.ini parameter Values + +RAIN8NET_serial_port + Required + Serial port where Rain8Net module is connected. + ie /dev/ttySx + + +RAIN8NET_Units + Optional (default 1) + Number of Rain8Net units linked on 1 serial connection. rain8Net support 1-8 + Valid Values: 1 - 8 + + +RAIN8NET_AutoStatusRequestTimer + Optional (default 30) + Number of seconds between auto executions of StatusRequests + Valid Values: 0 - 999999 + If 0, no automatic Status request are performed. + + +RAIN8NET_unit_[U]_zone_[Z] + Optional (no default) + String name of a Unit/Zone (see zone_name method) + Allows for zones to be named. ie "RAIN8NET_unit_1_zone_2=Front Yard Sprinklers" names the zone 2 of unit 1 + + +RAIN8NET_ModuleDebug + Optional (default 0) + if 1, echo debug messasges of the Rain8Net.pm module into the MH log + Valid Values: 0,1 + + + +Notes: + +This is my 1st Perl module and I am a novice, so I welcome comments and criticism but please be gentle ... I bruise easily ;) +This module was heavily derived from the DSC5401.pm module by Jocelyn Brouillard and Gaetan lord. Thanks to them + + +Changes + +v1.0 2015-07-14 initial module construction +v1.1 2015-12-11 Fixes and more documentation + + +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=cut + +use strict; + +package Rain8Net; + + +# Rain8Net Serial parameters +use constant RAIN8NET_BAUDRATE => 4800; +use constant RAIN8NET_DATABITS => 8; +use constant RAIN8NET_PARITY => "none"; +use constant RAIN8NET_STOPBITS => 1; +use constant RAIN8NET_HANDSHAKE => 'none'; +use constant RAIN8NET_MAXUNITS => 8; +use constant RAIN8NET_MAXZONES => 8; + +use constant SUBCOMMAND_STATUSREQUEST => 1; +use constant SUBCOMMAND_ZONEON => 2; +use constant SUBCOMMAND_ZONEOFF => 4; +use constant SUBCOMMAND_ALLUNITOFF => 8; + + +@Rain8Net::ISA = ('Generic_Item'); + +my %CmdMsg; +my %CmdMsgRev; +my @Rain8Net_Objects = (); +my $IncompleteCmd; +my $ModuleDebug = 0; + + + +# --------------------------------------------------------------------------- +# Method: new (public) +# Desc: Instantiate the new object +sub new +{ + my ($class) = @_; + my $self = {}; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [new]...") if $ModuleDebug; + + bless $self, $class; + + $$self{state} = 'Unknown'; + $$self{said} = ''; + $$self{MaxUnits} = 0; + $$self{LastCmdSent} = ''; + $$self{LastSubCmdSent} = -1; + $$self{LastCmdSentUnit} = -1; + $$self{LastCmdSentZone} = -1; + $$self{LastResponseRaw} = ''; + $$self{LastCmdResponse} = ''; + $$self{TimerInterval} = 0; + + # Module-level reference to self + push @Rain8Net_Objects, $self; + + # read event message hash + _DefineCmdMsg(); + + # test if ModuleDebug is on + $ModuleDebug = 1 if (exists $main::config_parms{RAIN8NET_ModuleDebug}); + + &main::print_log("Rain8Net Starting interface module"); + + # Call Startup to initialize serial port + $self->startup(); + + select(undef, undef, undef, 0.250); # wait 250 millseconds + + # We send a COMCheck, which is really a NOP. + $self->cmd('COMCheck'); # request an initial COMCheck + + select(undef, undef, undef, 0.250); # wait 250 millseconds to avoid overrunning RS-232 receive buffer on panel + + return $self; +} + + + + +# --------------------------------------------------------------------------- +# Method: _init_serial_port (private) +# Description: serial port configuration +sub _init_serial_port +{ + my ($self, $serial_port) = @_; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_init_serial_port]...") if $ModuleDebug; + + $serial_port->error_msg(1); + + $serial_port->databits(RAIN8NET_DATABITS); + $serial_port->parity(RAIN8NET_PARITY); + $serial_port->stopbits(RAIN8NET_STOPBITS); + $serial_port->handshake(RAIN8NET_HANDSHAKE); + $serial_port->datatype('raw'); + $serial_port->dtr_active(1); + $serial_port->rts_active(1); + + $serial_port->debug(1) if ($ModuleDebug eq 1); + + select( undef, undef, undef, .100 ); # Sleep a bit +} + + + +# --------------------------------------------------------------------------- +# Method: startup (public) +# Description: Called by MH on startup +sub startup +{ + my ($self) = @_; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_startup]...") if $ModuleDebug; + + my $port; + + if ($::config_parms{'RAIN8NET_serial_port'} ) + { + $port = $::config_parms{'RAIN8NET_serial_port'}; + &main::print_log("Rain8Net.pm - MODULE DEBUG : mh.ini [RAIN8NET_serial_port] read, value is [" . $port ."]") if ($ModuleDebug); + if (not (-c "$port")) + { + &main::print_log("Rain8Net.pm Error : Invalid port [" . $port ."] defined in mh.ini [RAIN8NET_serial_port]. Cannot create object."); + $$self{state} = "Error"; + return; + } + } + else + { + &main::print_log("Rain8Net.pm Error : Port not defined in mh.ini [RAIN8NET_serial_port]. Cannot create object."); + $$self{state} = "Error"; + return; + } + + + # If here, validate other settings + $$self{MaxUnits} = ( defined $::config_parms{RAIN8NET_Units} ) ? $main::config_parms{RAIN8NET_Units} : 1; + if ((($$self{MaxUnits} + 0) eq $$self{MaxUnits}) && (($$self{MaxUnits} +0 ) => 1) && (($$self{MaxUnits} + 0) <= RAIN8NET_MAXUNITS)) + { + $$self{MaxUnits} = ($$self{MaxUnits} + 0); # convert numeric + &main::print_log("Rain8Net.pm - MODULE DEBUG : mh.ini [RAIN8NET_Units] read, value is [" . $$self{MaxUnits} ."]") if ($ModuleDebug); + } + else + { + &main::print_log("Rain8Net.pm Error : Invalid value for mh.ini [RAIN8NET_Units], ignoring and setting to 1"); + $$self{MaxUnits} = 1; + } + + + # initialize state of all units/zones as off + for (my $j = 1; $j <= $$self{MaxUnits}; $j++) + { + for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) + { + $$self{zone_status}{$j}{$i} = 0; + } + } + + + # create the Serial port item + if ( &main::serial_port_create( 'Rain8Net', $port, RAIN8NET_BAUDRATE, 'none', 'raw' ) ) + { + $self->_init_serial_port( $::Serial_Ports{Rain8Net}{object}, $port ); + &main::print_log("Rain8Net.pm initializing port $port at " . RAIN8NET_BAUDRATE . " baud") if $ModuleDebug; + &::MainLoop_pre_add_hook( \&Rain8Net::_check_for_data, 1 ) if $main::Serial_Ports{Rain8Net}{object}; + $$self{state} = "Active"; + } + else + { + &main::print_log("Rain8Net.pm Error : Unable to open serial port [$port]. Cannot create object."); + $$self{state} = "Error"; + return; + } + + + # Get the Auto Status request value and validate + my $timerinterval = ( defined $::config_parms{RAIN8NET_AutoStatusRequestTimer} ) ? $main::config_parms{RAIN8NET_AutoStatusRequestTimer} : 30; + if ((($timerinterval + 0) eq $timerinterval) && (($timerinterval+0) >= 0) && (($timerinterval+0) <= 9999)) + { + $$self{TimerInterval} = ($timerinterval + 0); # convert numeric + &main::print_log("Rain8Net.pm - MODULE DEBUG : mh.ini [RAIN8NET_AutoStatusRequestTimer] read, value is [" . $$self{TimerInterval} ."]") if ($ModuleDebug); + } + else + { + &main::print_log("Rain8Net.pm Error : Invalid value for mh.ini [RAIN8NET_AutoStatusRequestTimer], ignoring and setting to 0"); + $$self{TimerInterval} = 0; + } +} + + + +# --------------------------------------------------------------------------- +# Method: _check_for_data (private) +# Description: hooked routine that checks data on port +sub _check_for_data +{ + # &main::print_log("Rain8Net.pm - MODULE DEBUG : entering init [_check_for_data]...") if $ModuleDebug; + + $main::Serial_Ports{'Rain8Net'}{data} = ''; + &main::check_for_generic_serial_data('Rain8Net'); + #-- my $NewCmd = $main::Serial_Ports{'Rain8Net'}{data}; + + my $NewCmd = $main::Serial_Ports{'Rain8Net'}{data}; + #-- $main::Serial_Ports{'Rain8Net'}{data} = ''; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : Received the following [$NewCmd]") if (($NewCmd) && ($ModuleDebug)); + + # we need to buffer the information received, because many command could be include in a single pass + $NewCmd = $IncompleteCmd . $NewCmd if $IncompleteCmd; + return if !$NewCmd; + + my $self = $Rain8Net_Objects[0]; + + # Flush last buffered response + $$self{LastResponseRaw} = ''; + + # Rain8Net commands are 3 bytes + if (length($NewCmd) ge 3) + { + $IncompleteCmd = substr($NewCmd,3); # Keep rest of Command + &_CheckCmd($self,$NewCmd); + } +} + + +# --------------------------------------------------------------------------- +# Method: _SetTimers (private) +# Description: set up auto status request timers for all units +sub _SetTimers +{ + my ($self) = @_; + + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_SetTimers]...") if $ModuleDebug; + + # set a timer to perform auto Statusrequest every 30 + if (($$self{object_name}) && ($$self{TimerInterval} > 0)) + { + &::print_log("Rain8Net.pm - MODULE DEBUG : creating timer(s) for object [" . $$self{object_name} . "]") if $ModuleDebug; + for (my $count = 1; $count <= $$self{MaxUnits}; $count++) + { + my $timername = 'statusrequest_timer' . $count; + + if (not ($$self{$timername})) + { + &::print_log("Rain8Net.pm - MODULE DEBUG : creating timer [$timername]") if $ModuleDebug; + $$self{$timername} = new Timer; + $$self{$timername}->set($$self{TimerInterval}, "$$self{object_name}->cmd('StatusRequest',$count)", -1); + } + } + } + +} + +# --------------------------------------------------------------------------- +# Method: _CheckCmd (private) +# Description: validate in incoming command string from the Rain8 unit and apply the data updates +sub _CheckCmd +{ + my ($self,$CmdStr) = @_; + + my $UnitNumber = -1; + my $ZoneNumber = -1; + my $ZoneBitFlag = 0; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_CheckCmd]...") if $ModuleDebug; + + &_SetTimers($self); + + &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd - cmdstr is ($CmdStr)") if $ModuleDebug; + + return if !$CmdStr; + + $$self{LastResponseRaw} = $CmdStr; + + my $l = length($CmdStr); + my $code = unpack("H*", substr( $CmdStr, 0, 1 )); + my $arg1 = unpack("H*", substr( $CmdStr, 1, 1 )); + my $arg2 = unpack("H*", substr( $CmdStr, 2, 1 )); + my $hexcode = sprintf("0x%d",$code); + + + &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd Raw input is [$code] [$arg1] [$arg2], length [$l]") if $ModuleDebug; + + # test received command is valid + if (not ( exists $CmdMsgRev{$hexcode} )) + { + &::print_log("Unknown rain8Net response code recieved : code [$code] with arguments [$arg1] , [$arg2]"); + $$self{LastSubCmdSent} = 0; + return; + } + + my $CmdName; + + + # Since StatusRequest and ZoneOn and ZoneOFF all return the same god-damn 40 hex, use our flag + if (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_STATUSREQUEST)) + { + $CmdName = "StatusRequest"; + } + elsif (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_ZONEON)) + { + $CmdName = "ZoneON"; + } + elsif (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_ZONEOFF)) + { + $CmdName = "ZoneOFF"; + } + elsif (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_ALLUNITOFF)) + { + $CmdName = "AllModuleOFF"; + } + else + { + $CmdName = ($CmdMsgRev{$hexcode}); + } + + &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd Command recived is = [" . $CmdName . "]") if $ModuleDebug; + + # At this point process the response...-------------- + + # If Status request response, the last byte is a bit pattern of active zones + if ($CmdName eq "StatusRequest") + { + $UnitNumber = hex($arg1); + $ZoneBitFlag = hex($arg2); + my $match = 1; + for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) + { + $$self{zone_status}{$UnitNumber}{$i} = ( $ZoneBitFlag & $match ) ? 1 : 0; + $match = $match << 1; + } + } + + + # Echo from ZoneOFF + if ($CmdName eq "ZoneOFF") + { + $UnitNumber = hex($arg1); + my $UpperNible = hex($arg2) & hex('0x40'); + my $LowerNible = hex($arg2) & hex('0x08'); + + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub _checkCmd - ZONE OFF, Unit [$UnitNumber], uppernibble [$UpperNible], lowerninble [$LowerNible]") if $ModuleDebug; + + $$self{zone_status}{$UnitNumber}{$LowerNible} = 0; + } + + # Echo from ZoneON + if ($CmdName eq "ZoneON") + { + $UnitNumber = hex($arg1); + my $UpperNible = hex($arg2) & hex("0x30"); + my $LowerNible = hex($arg2) & hex('0x08'); + + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub _checkCmd - ZONE ON, Unit [$UnitNumber], uppernibble [$UpperNible], lowerninble [$LowerNible]") if $ModuleDebug; + + $$self{zone_status}{$UnitNumber}{$LowerNible} = 1; + } + + # Echo from AllModuleOff + if ($CmdName eq "AllModuleOFF") + { + $UnitNumber = hex($arg1); + + if (hex($arg2) eq hex("0x55")) + { + for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) + { + $$self{zone_status}{$UnitNumber}{$i} = 0; + } + } + } + + + # Dump debug + if (($ModuleDebug) && ($UnitNumber >= 1)) + { + &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd dump status -->"); + for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) + { + &main::print_log(" Unit [$UnitNumber], zone [$i] is [" . ($$self{zone_status}{$UnitNumber}{$i}) . "]"); + } + } + + + # Reset the last subcommand + $$self{LastSubCmdSent} = 0; + + return; +} + + + + +#}}} +# Define hash with DSC command {{{ +sub _DefineCmdMsg { + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_DefineCmdMsg]...") if $ModuleDebug; + + %CmdMsg = ( + lc("COMCheck") => "0x70", + lc("StatusRequest") => "0x40", + lc("ZoneON") => "0x40", + lc("ZoneOFF") => "0x40", + lc("AllModuleOFF") => "0x40", + lc("AllGlobalOFF") => "0x20", + lc("ReadRainSwitchStatus") => "0x50", + lc("ReadFlowMeterCounter") => "0x50", + lc("ClearFlowMeterCounter") => "0x50" + ); + + %CmdMsgRev = reverse %CmdMsg; + return; +} + + + + + +#}}} +# Sending command to Rain8Net +sub cmd +{ + + my ( $self, $cmd, $arg1, $arg2 ) = @_; + my $CmdName; + + if ($ModuleDebug) + { + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [cmd]..."); + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - cmd is [" . $cmd . "]"); + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - arg1 is [" . $arg1 . "]"); + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - arg2 is [" . $arg2 . "]"); + } + + $cmd = lc($cmd); + if (not (exists $CmdMsg{$cmd}) ) + { + &::print_log("Rain8Net.pm ERROR - Invalid command : ($cmd) with arguments $arg1 , $arg2"); + return; + } + + my $CmdByte = hex($CmdMsg{$cmd}); + my $CmdStr = chr($CmdByte); + my $UnitNumber = 0; + my $ZoneNumber = 0; + + if ($cmd eq lc("COMCheck")) + { + # pad command with 2 extra bytes, does not matter which + $CmdStr .= "xx"; + } + elsif ($cmd eq lc("StatusRequest")) + { + $UnitNumber = $self->_ConvertUnit($cmd, $arg1); + return if ( $UnitNumber lt 0 ); + + # Second arg is "all status code" + $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0xf0")); + $$self{LastSubCmdSent} = SUBCOMMAND_STATUSREQUEST; + } + elsif ($cmd eq lc("ZoneON")) + { + $UnitNumber = $self->_ConvertUnit($cmd, $arg1); + return if ( $UnitNumber lt 0 ); + + $ZoneNumber = $self->_ConvertZone($cmd, $arg2); + return if ( $ZoneNumber lt 0 ); + + # Second arg is 3 upper nibble, zone - lower nibble + $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0x3" . "$ZoneNumber")); + $$self{LastSubCmdSent} = SUBCOMMAND_ZONEON; + $$self{zone_status}{$UnitNumber}{$ZoneNumber} = 1; + } + elsif ($cmd eq lc("ZoneOFF")) + { + $UnitNumber = $self->_ConvertUnit($cmd, $arg1); + return if ( $UnitNumber lt 0 ); + + $ZoneNumber = $self->_ConvertZone($cmd, $arg2); + return if ( $ZoneNumber lt 0 ); + + # Second arg is 4 upper nibble, zone - lower nibble + $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0x4" . "$ZoneNumber")); + $$self{LastSubCmdSent} = SUBCOMMAND_ZONEOFF; + $$self{zone_status}{$UnitNumber}{$ZoneNumber} = 0; + } + elsif ($cmd eq lc("AllModuleOFF")) + { + $UnitNumber = $self->_ConvertUnit($cmd, $arg1); + return if ( $UnitNumber lt 0 ); + + $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0x55")); + $$self{LastSubCmdSent} = SUBCOMMAND_ALLUNITOFF; + for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) + { + $$self{zone_status}{$UnitNumber}{$i} = 0; + } + + + } + elsif ($cmd eq lc("AllGlobalOff")) + { + $$self{LastSubCmdSent} = 0; + $CmdStr .= chr(hex("0x55")) . chr(hex("0x55")); + for (my $i = 1; $i <= $$self{MaxUnits}; $i++) + { + for (my $j = 1; $j <= RAIN8NET_MAXZONES; $j++) + { + $$self{zone_status}{$i}{$j} = 0; + } + } + } + + + +# &::print_log("Sending to Rain8Net panel ($cmd) with argument ($arg1, $arg2)"); + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - Writing (" . $CmdStr . ")") if $ModuleDebug; + if ($main::Serial_Ports{Rain8Net}{object}->write($CmdStr)) + { + &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - Command (" . $CmdStr . ") successfully written to serial port.") if $ModuleDebug; + $$self{LastCmdSent} = $cmd; + $$self{LastCmdSentUnit} = $UnitNumber; + $$self{LastCmdSentZone} = $ZoneNumber; + } + + + return "Sent to Rain8Net panel: ($cmd) with argument ($arg1,$arg2) - Successful"; + +} + + + + +#}}} +# validate and Convert Unit Number to value +sub _ConvertUnit +{ + my ($self, $cmd, $Unit) = @_; + + my $UnitNumber = $Unit + 0; + if ( ($UnitNumber >= 1) && ($UnitNumber <= $$self{MaxUnits}) ) + { + # $UnitNumber is in the range + return ($UnitNumber); + } + else + { + &::print_log("Rain8Net Error - Invalid Unit number on command : [$cmd], Unit [$Unit]"); + return -1; + } +} + +#}}} +# validate and Convert Zone Number to numeric value. +# retrun -1 if invalid +sub _ConvertZone +{ + + my ($self, $cmd, $Zone) = @_; + + my $ZoneNumber = $Zone + 0; + + if ( ($ZoneNumber >= 1) && ($ZoneNumber <= RAIN8NET_MAXZONES) ) + { + # $ZoneNumber is in the range + return ($ZoneNumber); + } + else + { + &::print_log("Rain8Net Error - Invalid zone number on command : [$cmd], zone [$Zone])"); + return -1; + } +} + + + + + +#}}} +# user call from MH {{{ +# most of them are replicate to hash value, easier to code + +sub status_zone +{ + my ( $self, $unit, $zone ) = @_; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [status_zone]...") if $ModuleDebug; + + my $UnitNumber = $self->_ConvertUnit("method: status_zone",$unit); + return -1 if ($UnitNumber < 1); + + my $ZoneNumber = $self->_ConvertZone("method: status_zone",$zone); + return -1 if ($ZoneNumber < 1); + + return $$self{zone_status}{$UnitNumber}{$ZoneNumber} if defined $$self{zone_status}{$UnitNumber}{$ZoneNumber}; +} + +sub zone_name +{ + my ( $self, $unit, $zone ) = @_; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [zone_name]...") if $ModuleDebug; + + my $UnitNumber = $self->_ConvertUnit("method: zone_name",$unit); + return -1 if ($UnitNumber < 1); + + my $ZoneNumber = $self->_ConvertZone("method: zone_name",$zone); + return -1 if ($ZoneNumber < 1); + + my $unitstr = sprintf "%d", $UnitNumber; + my $zonestr = sprintf "%d", $ZoneNumber; + + my $cfgparmname = "RAIN8NET_unit_" . $unitstr . "_zone_" . $zonestr; + + my $ZoneName = $main::config_parms{$cfgparmname} if exists $main::config_parms{$cfgparmname}; + + return $ZoneName if $ZoneName; + + return $ZoneNumber; +} + + + +sub ZoneOn +{ + my ( $self, $unit, $zone ) = @_; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [ZoneOn]...") if $ModuleDebug; + + my $UnitNumber = $self->_ConvertUnit("method: ZoneOn",$unit); + return -1 if ($UnitNumber < 1); + + my $ZoneNumber = $self->_ConvertZone("method: ZoneOn",$zone); + return -1 if ($ZoneNumber < 1); + + $self->cmd("ZoneOn", $UnitNumber, $ZoneNumber); +} + + +sub ZoneOff +{ + my ( $self, $unit, $zone ) = @_; + + &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [ZoneOff]...") if $ModuleDebug; + + # If no unit or zone, all global off!!!! + if (@_ < 2) + { + &main::print_log("Rain8Net.pm - MODULE DEBUG : [ZoneOff] missing unit/zone - calling AllGlobalOff...") if $ModuleDebug; + $self->cmd("AllGlobalOff"); + return; + } + + my $UnitNumber = $self->_ConvertUnit("method: ZoneOff",$unit); + return -1 if ($UnitNumber < 1); + + # If unit but no zone + if (@_ < 3) + { + &main::print_log("Rain8Net.pm - MODULE DEBUG : [ZoneOff] missing zone - calling AllModuleOFF...") if $ModuleDebug; + $self->cmd("AllModuleOFF",$UnitNumber); + return; + } + + # If Here ... a particular unit/zone is to be turned off + + my $ZoneNumber = $self->_ConvertZone("method: ZoneOff",$zone); + return -1 if ($ZoneNumber < 1); + + &main::print_log("Rain8Net.pm - MODULE DEBUG : [ZoneOff] call ZoneOff...") if $ModuleDebug; + $self->cmd("ZoneOff", $UnitNumber, $ZoneNumber); +} + + +1; + From fff2dbe3f58f631bfa4a9bed1b6cc0aa347e1d7c Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 29 Apr 2016 10:45:18 -0400 Subject: [PATCH 037/209] Added HTML message format to API (as per Pushover v2.3+) --- lib/Pushover.pm | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/Pushover.pm b/lib/Pushover.pm index 0f30cef37..806922a69 100644 --- a/lib/Pushover.pm +++ b/lib/Pushover.pm @@ -12,6 +12,7 @@ Configure the required pushover settings in your mh.private.ini file: Pushover_token = Pushover_user = Pushover_priority = [-1 | 0 | 1 | 2] Default message priority, defaults to 0. + Pushover_html = [ 0 | 1 ] Support HTML messages, see https://pushover.net/api#html. Defaults to 0. Pushover_title = "MisterHouse" Default title for messages if none provided Pushover_disable = 1 Disable notifications. Messages will still be logged @@ -39,6 +40,9 @@ values provided on initialization. See the method documentation for below more $push->notify( "Some important message", { title => 'Security Alert', priority => 2 }); +Or with HTML formmatting + $push->notify( "Some important message", { title => 'Security Alert', priority => 2, html => 1 }); + =head2 DESCRIPTION The Pushover instance establishes the defaults for messages and acts as a rudimentary rate limiter for notifications. @@ -90,7 +94,8 @@ Creates a new Pushover object. The parameter hash is optional. Defaults will be B my $push = Pushover->new( { priority => 0, # Set default Message priority, -1, 0, 1, 2 - retry => 60, # Set default retry priority 2 notification every 60 seconds + html => 1, # Support HTML formatting of message ...see https://pushover.net/api#html + retry => 60, # Set default retry priority 2 notification every 60 seconds expire => 3600, # Set default expration of the retry timer title => "Some title", # Set default title for messages token => "xxxx...", # Set the API Token @@ -122,11 +127,12 @@ sub new { $params = {} unless defined $params; my $self = {}; - $self->{priority} = 0; # Priority zero - honor quite times - $self->{speak} = 1; # Speak notifications and acknowledgments + $self->{priority} = 0; # Priority zero - honour quite times + $self->{speak} = 1; # Speak notifications and acknowledgements + $self->{html} = 0; # Default html off # Merge the mh.private.ini defaults into the object - foreach (qw( token user priority title server retry expire speak disable)) { + foreach (qw( token user priority html title server retry expire speak disable)) { $self->{$_} = $params->{$_}; $self->{$_} = $::config_parms{"Pushover_$_"} unless defined $self->{$_}; } @@ -163,7 +169,8 @@ information for the notification. The list is not exclusive. Additional parame in the POST to Pushover.net. This allows support of any API parameter as defined at https://pushover.net/api $push->notify("Some urgent message", { priority => 2, # Message priority, -1, 0, 1, 2 - retry => 60, # Retry priority 2 notification every 60 seconds + html => 1, # HTML formatting of message 0-off, 1-on ... see https://pushover.net/api#html + retry => 60, # Retry priority 2 notification every 60 seconds expire => 3600, # Give up if not ack of priority 2 notify after 1 hour title => "Some title", # Override title of message token => "xxxx...", # Override the API Token - probably not useful @@ -210,7 +217,7 @@ sub notify { } # Merge in the message defaults, They can be overridden - foreach (qw( token user priority title url url_title sound retry expire )) { + foreach (qw( token user priority html title url url_title sound retry expire )) { next unless ( defined $self->{$_} ); $callparms->{$_} = $self->{$_} unless defined $callparms->{$_}; } @@ -354,6 +361,11 @@ sub _checkReceipt { George Clark +=head2 MODIFICATIONS + +2016/04/29 Marc mhcoder@nowheremail.com + Added html support on API call + =head2 SEE ALSO http://Pushover.net/ @@ -368,3 +380,4 @@ You should have received a copy of the GNU General Public License along with thi =cut + From a5cc6d2a5037813087b8d0512661b9f857516a2b Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 29 Apr 2016 14:09:38 -0400 Subject: [PATCH 038/209] Added device to API call --- lib/Pushover.pm | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/Pushover.pm b/lib/Pushover.pm index 806922a69..0501be78d 100644 --- a/lib/Pushover.pm +++ b/lib/Pushover.pm @@ -11,6 +11,7 @@ Configure the required pushover settings in your mh.private.ini file: Pushover_token = Pushover_user = + Pushover_device = Pushover_priority = [-1 | 0 | 1 | 2] Default message priority, defaults to 0. Pushover_html = [ 0 | 1 ] Support HTML messages, see https://pushover.net/api#html. Defaults to 0. Pushover_title = "MisterHouse" Default title for messages if none provided @@ -132,7 +133,7 @@ sub new { $self->{html} = 0; # Default html off # Merge the mh.private.ini defaults into the object - foreach (qw( token user priority html title server retry expire speak disable)) { + foreach (qw( token user priority html title device server retry expire speak disable)) { $self->{$_} = $params->{$_}; $self->{$_} = $::config_parms{"Pushover_$_"} unless defined $self->{$_}; } @@ -169,12 +170,13 @@ information for the notification. The list is not exclusive. Additional parame in the POST to Pushover.net. This allows support of any API parameter as defined at https://pushover.net/api $push->notify("Some urgent message", { priority => 2, # Message priority, -1, 0, 1, 2 - html => 1, # HTML formatting of message 0-off, 1-on ... see https://pushover.net/api#html - retry => 60, # Retry priority 2 notification every 60 seconds - expire => 3600, # Give up if not ack of priority 2 notify after 1 hour - title => "Some title", # Override title of message - token => "xxxx...", # Override the API Token - probably not useful - user => "xxxx...", # Override the target user/group + html => 1, # HTML formatting of message 0-off, 1-on ... see https://pushover.net/api#html + retry => 60, # Retry priority 2 notification every 60 seconds + expire => 3600, # Give up if not ack of priority 2 notify after 1 hour + title => "Some title", # Override title of message + device => "nexus5,iphone" # Device or device-list + token => "xxxx...", # Override the API Token - probably not useful + user => "xxxx...", # Override the target user/group }); Notify will record the last message sent along with a timestamp. If the duplicate message is sent within @@ -217,7 +219,7 @@ sub notify { } # Merge in the message defaults, They can be overridden - foreach (qw( token user priority html title url url_title sound retry expire )) { + foreach (qw( token user priority html title device url url_title sound retry expire )) { next unless ( defined $self->{$_} ); $callparms->{$_} = $self->{$_} unless defined $callparms->{$_}; } @@ -232,6 +234,11 @@ sub notify { $callparms->{expire} = 86400 if ( $callparms->{expire} > 86400 ); } + # remove html if off + if ( $callparms->{html} != 1 ) { + delete $callparms->{html}; + } + &::print_log( "[Pushover] Notify parameters: " . Data::Dumper::Dumper( \$callparms ) ) if TRACE; @@ -364,7 +371,7 @@ George Clark =head2 MODIFICATIONS 2016/04/29 Marc mhcoder@nowheremail.com - Added html support on API call + Added html and device support on API call =head2 SEE ALSO From d40b0850649b345f6787c451c420368eef153357 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Thu, 12 May 2016 22:29:43 +0200 Subject: [PATCH 039/209] Revert "Allows the control of WGL rain8Net serial sprinkler control modules" --- lib/Pushover.pm | 40 +-- lib/Rain8Net.pm | 801 ------------------------------------------------ 2 files changed, 10 insertions(+), 831 deletions(-) delete mode 100644 lib/Rain8Net.pm diff --git a/lib/Pushover.pm b/lib/Pushover.pm index 0501be78d..0f30cef37 100644 --- a/lib/Pushover.pm +++ b/lib/Pushover.pm @@ -11,9 +11,7 @@ Configure the required pushover settings in your mh.private.ini file: Pushover_token = Pushover_user = - Pushover_device = Pushover_priority = [-1 | 0 | 1 | 2] Default message priority, defaults to 0. - Pushover_html = [ 0 | 1 ] Support HTML messages, see https://pushover.net/api#html. Defaults to 0. Pushover_title = "MisterHouse" Default title for messages if none provided Pushover_disable = 1 Disable notifications. Messages will still be logged @@ -41,9 +39,6 @@ values provided on initialization. See the method documentation for below more $push->notify( "Some important message", { title => 'Security Alert', priority => 2 }); -Or with HTML formmatting - $push->notify( "Some important message", { title => 'Security Alert', priority => 2, html => 1 }); - =head2 DESCRIPTION The Pushover instance establishes the defaults for messages and acts as a rudimentary rate limiter for notifications. @@ -95,8 +90,7 @@ Creates a new Pushover object. The parameter hash is optional. Defaults will be B my $push = Pushover->new( { priority => 0, # Set default Message priority, -1, 0, 1, 2 - html => 1, # Support HTML formatting of message ...see https://pushover.net/api#html - retry => 60, # Set default retry priority 2 notification every 60 seconds + retry => 60, # Set default retry priority 2 notification every 60 seconds expire => 3600, # Set default expration of the retry timer title => "Some title", # Set default title for messages token => "xxxx...", # Set the API Token @@ -128,12 +122,11 @@ sub new { $params = {} unless defined $params; my $self = {}; - $self->{priority} = 0; # Priority zero - honour quite times - $self->{speak} = 1; # Speak notifications and acknowledgements - $self->{html} = 0; # Default html off + $self->{priority} = 0; # Priority zero - honor quite times + $self->{speak} = 1; # Speak notifications and acknowledgments # Merge the mh.private.ini defaults into the object - foreach (qw( token user priority html title device server retry expire speak disable)) { + foreach (qw( token user priority title server retry expire speak disable)) { $self->{$_} = $params->{$_}; $self->{$_} = $::config_parms{"Pushover_$_"} unless defined $self->{$_}; } @@ -170,13 +163,11 @@ information for the notification. The list is not exclusive. Additional parame in the POST to Pushover.net. This allows support of any API parameter as defined at https://pushover.net/api $push->notify("Some urgent message", { priority => 2, # Message priority, -1, 0, 1, 2 - html => 1, # HTML formatting of message 0-off, 1-on ... see https://pushover.net/api#html - retry => 60, # Retry priority 2 notification every 60 seconds - expire => 3600, # Give up if not ack of priority 2 notify after 1 hour - title => "Some title", # Override title of message - device => "nexus5,iphone" # Device or device-list - token => "xxxx...", # Override the API Token - probably not useful - user => "xxxx...", # Override the target user/group + retry => 60, # Retry priority 2 notification every 60 seconds + expire => 3600, # Give up if not ack of priority 2 notify after 1 hour + title => "Some title", # Override title of message + token => "xxxx...", # Override the API Token - probably not useful + user => "xxxx...", # Override the target user/group }); Notify will record the last message sent along with a timestamp. If the duplicate message is sent within @@ -219,7 +210,7 @@ sub notify { } # Merge in the message defaults, They can be overridden - foreach (qw( token user priority html title device url url_title sound retry expire )) { + foreach (qw( token user priority title url url_title sound retry expire )) { next unless ( defined $self->{$_} ); $callparms->{$_} = $self->{$_} unless defined $callparms->{$_}; } @@ -234,11 +225,6 @@ sub notify { $callparms->{expire} = 86400 if ( $callparms->{expire} > 86400 ); } - # remove html if off - if ( $callparms->{html} != 1 ) { - delete $callparms->{html}; - } - &::print_log( "[Pushover] Notify parameters: " . Data::Dumper::Dumper( \$callparms ) ) if TRACE; @@ -368,11 +354,6 @@ sub _checkReceipt { George Clark -=head2 MODIFICATIONS - -2016/04/29 Marc mhcoder@nowheremail.com - Added html and device support on API call - =head2 SEE ALSO http://Pushover.net/ @@ -387,4 +368,3 @@ You should have received a copy of the GNU General Public License along with thi =cut - diff --git a/lib/Rain8Net.pm b/lib/Rain8Net.pm deleted file mode 100644 index 96599ad97..000000000 --- a/lib/Rain8Net.pm +++ /dev/null @@ -1,801 +0,0 @@ -=begin comment -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - -Rain8Net.pm v1.1 -by Marco Maddalena (mhcoder@nowheremail.com) - -Description: - -Allows the control of WGL rain8Net serial sprinkler control modules -(see http://www.wgldesigns.com/rain8pc.html) -Each unit can control 8 zones, and 8 units can be daisy-chained on 1 serial connection. - - -Methods - status_zone - returns the zone status (1=on,0=off). - Parms: Unit (required) : Numeric - Unit number - Zone (required) : numeric - Zone number - - - - cmd - send Rain8Net Cmd to the unit (some function below alias these commands) - Parms cmd (required) : string - See list - Unit (optional) : numeric - Unit number IF required by the cmd - Zone (optional) : numeric - Zone number IF required by the cmd - - Supported commands are - "COMCheck" - Check communications link. No parameters. - "StatusRequest" - Request an update of zone statuses. Unit number required. - (Note: automatic status request happens via RAIN8NET_AutoStatusRequestTimer value) - "ZoneON" - Turn zone on. Requires unit and Zone number - "ZoneOFF" - Turn zone off. Requires unit and Zone number - "AllModuleOFF" - Turn all zones of a unit off. Requires unit number. - "AllGlobalOff" - Turns all zones of all units. No paramters - - ZoneOn - send the ZoneOn command to Rain8Net - Parms Unit (required) : numeric - Unit number - Zone (required) : numeric - Zone number - - ZoneOff - send the ZoneOff/AllUnitOff/AllGlobalOff command to Rain8Net - Parms Unit (optional) : numeric - Unit number (if not passed, then the equivalent to AllGlobalOff is performed) - Zone (optional) : numeric - Zone number (if not passed, but unit was, the equivalent to AllModuleOff is performed) - - - - - -Examples - - use Rain8Net; - $Rain8 = new Rain8Net; - - - if (time_now("22:00")) - { - print_log "Sprinkler ON for unit 1, Zone 2 [" . $Rain8->get_name(1,2) . "]"; - $Rain8->cmd('ZoneON',1,2); - } - - if (time_now("01:00")) - { - print_log "Sprinkler OFF for unit 1, Zone 2 [" . $Rain8->get_name(1,2) . "]"; - $Rain8->cmd('ZoneOFF',1,2); - } - - if ((time_now("23:00")) && (Rain8->status_zone(1,5))) - { - print_log "Sprinkler OFF for unit 1, Zone 5 [" . $Rain8->get_name(1,5) . "]"; - $Rain8->cmd('ZoneOFF',1,5); - } - - - - -mh.ini parameter Values - -RAIN8NET_serial_port - Required - Serial port where Rain8Net module is connected. - ie /dev/ttySx - - -RAIN8NET_Units - Optional (default 1) - Number of Rain8Net units linked on 1 serial connection. rain8Net support 1-8 - Valid Values: 1 - 8 - - -RAIN8NET_AutoStatusRequestTimer - Optional (default 30) - Number of seconds between auto executions of StatusRequests - Valid Values: 0 - 999999 - If 0, no automatic Status request are performed. - - -RAIN8NET_unit_[U]_zone_[Z] - Optional (no default) - String name of a Unit/Zone (see zone_name method) - Allows for zones to be named. ie "RAIN8NET_unit_1_zone_2=Front Yard Sprinklers" names the zone 2 of unit 1 - - -RAIN8NET_ModuleDebug - Optional (default 0) - if 1, echo debug messasges of the Rain8Net.pm module into the MH log - Valid Values: 0,1 - - - -Notes: - -This is my 1st Perl module and I am a novice, so I welcome comments and criticism but please be gentle ... I bruise easily ;) -This module was heavily derived from the DSC5401.pm module by Jocelyn Brouillard and Gaetan lord. Thanks to them - - -Changes - -v1.0 2015-07-14 initial module construction -v1.1 2015-12-11 Fixes and more documentation - - -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -=cut - -use strict; - -package Rain8Net; - - -# Rain8Net Serial parameters -use constant RAIN8NET_BAUDRATE => 4800; -use constant RAIN8NET_DATABITS => 8; -use constant RAIN8NET_PARITY => "none"; -use constant RAIN8NET_STOPBITS => 1; -use constant RAIN8NET_HANDSHAKE => 'none'; -use constant RAIN8NET_MAXUNITS => 8; -use constant RAIN8NET_MAXZONES => 8; - -use constant SUBCOMMAND_STATUSREQUEST => 1; -use constant SUBCOMMAND_ZONEON => 2; -use constant SUBCOMMAND_ZONEOFF => 4; -use constant SUBCOMMAND_ALLUNITOFF => 8; - - -@Rain8Net::ISA = ('Generic_Item'); - -my %CmdMsg; -my %CmdMsgRev; -my @Rain8Net_Objects = (); -my $IncompleteCmd; -my $ModuleDebug = 0; - - - -# --------------------------------------------------------------------------- -# Method: new (public) -# Desc: Instantiate the new object -sub new -{ - my ($class) = @_; - my $self = {}; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [new]...") if $ModuleDebug; - - bless $self, $class; - - $$self{state} = 'Unknown'; - $$self{said} = ''; - $$self{MaxUnits} = 0; - $$self{LastCmdSent} = ''; - $$self{LastSubCmdSent} = -1; - $$self{LastCmdSentUnit} = -1; - $$self{LastCmdSentZone} = -1; - $$self{LastResponseRaw} = ''; - $$self{LastCmdResponse} = ''; - $$self{TimerInterval} = 0; - - # Module-level reference to self - push @Rain8Net_Objects, $self; - - # read event message hash - _DefineCmdMsg(); - - # test if ModuleDebug is on - $ModuleDebug = 1 if (exists $main::config_parms{RAIN8NET_ModuleDebug}); - - &main::print_log("Rain8Net Starting interface module"); - - # Call Startup to initialize serial port - $self->startup(); - - select(undef, undef, undef, 0.250); # wait 250 millseconds - - # We send a COMCheck, which is really a NOP. - $self->cmd('COMCheck'); # request an initial COMCheck - - select(undef, undef, undef, 0.250); # wait 250 millseconds to avoid overrunning RS-232 receive buffer on panel - - return $self; -} - - - - -# --------------------------------------------------------------------------- -# Method: _init_serial_port (private) -# Description: serial port configuration -sub _init_serial_port -{ - my ($self, $serial_port) = @_; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_init_serial_port]...") if $ModuleDebug; - - $serial_port->error_msg(1); - - $serial_port->databits(RAIN8NET_DATABITS); - $serial_port->parity(RAIN8NET_PARITY); - $serial_port->stopbits(RAIN8NET_STOPBITS); - $serial_port->handshake(RAIN8NET_HANDSHAKE); - $serial_port->datatype('raw'); - $serial_port->dtr_active(1); - $serial_port->rts_active(1); - - $serial_port->debug(1) if ($ModuleDebug eq 1); - - select( undef, undef, undef, .100 ); # Sleep a bit -} - - - -# --------------------------------------------------------------------------- -# Method: startup (public) -# Description: Called by MH on startup -sub startup -{ - my ($self) = @_; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_startup]...") if $ModuleDebug; - - my $port; - - if ($::config_parms{'RAIN8NET_serial_port'} ) - { - $port = $::config_parms{'RAIN8NET_serial_port'}; - &main::print_log("Rain8Net.pm - MODULE DEBUG : mh.ini [RAIN8NET_serial_port] read, value is [" . $port ."]") if ($ModuleDebug); - if (not (-c "$port")) - { - &main::print_log("Rain8Net.pm Error : Invalid port [" . $port ."] defined in mh.ini [RAIN8NET_serial_port]. Cannot create object."); - $$self{state} = "Error"; - return; - } - } - else - { - &main::print_log("Rain8Net.pm Error : Port not defined in mh.ini [RAIN8NET_serial_port]. Cannot create object."); - $$self{state} = "Error"; - return; - } - - - # If here, validate other settings - $$self{MaxUnits} = ( defined $::config_parms{RAIN8NET_Units} ) ? $main::config_parms{RAIN8NET_Units} : 1; - if ((($$self{MaxUnits} + 0) eq $$self{MaxUnits}) && (($$self{MaxUnits} +0 ) => 1) && (($$self{MaxUnits} + 0) <= RAIN8NET_MAXUNITS)) - { - $$self{MaxUnits} = ($$self{MaxUnits} + 0); # convert numeric - &main::print_log("Rain8Net.pm - MODULE DEBUG : mh.ini [RAIN8NET_Units] read, value is [" . $$self{MaxUnits} ."]") if ($ModuleDebug); - } - else - { - &main::print_log("Rain8Net.pm Error : Invalid value for mh.ini [RAIN8NET_Units], ignoring and setting to 1"); - $$self{MaxUnits} = 1; - } - - - # initialize state of all units/zones as off - for (my $j = 1; $j <= $$self{MaxUnits}; $j++) - { - for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) - { - $$self{zone_status}{$j}{$i} = 0; - } - } - - - # create the Serial port item - if ( &main::serial_port_create( 'Rain8Net', $port, RAIN8NET_BAUDRATE, 'none', 'raw' ) ) - { - $self->_init_serial_port( $::Serial_Ports{Rain8Net}{object}, $port ); - &main::print_log("Rain8Net.pm initializing port $port at " . RAIN8NET_BAUDRATE . " baud") if $ModuleDebug; - &::MainLoop_pre_add_hook( \&Rain8Net::_check_for_data, 1 ) if $main::Serial_Ports{Rain8Net}{object}; - $$self{state} = "Active"; - } - else - { - &main::print_log("Rain8Net.pm Error : Unable to open serial port [$port]. Cannot create object."); - $$self{state} = "Error"; - return; - } - - - # Get the Auto Status request value and validate - my $timerinterval = ( defined $::config_parms{RAIN8NET_AutoStatusRequestTimer} ) ? $main::config_parms{RAIN8NET_AutoStatusRequestTimer} : 30; - if ((($timerinterval + 0) eq $timerinterval) && (($timerinterval+0) >= 0) && (($timerinterval+0) <= 9999)) - { - $$self{TimerInterval} = ($timerinterval + 0); # convert numeric - &main::print_log("Rain8Net.pm - MODULE DEBUG : mh.ini [RAIN8NET_AutoStatusRequestTimer] read, value is [" . $$self{TimerInterval} ."]") if ($ModuleDebug); - } - else - { - &main::print_log("Rain8Net.pm Error : Invalid value for mh.ini [RAIN8NET_AutoStatusRequestTimer], ignoring and setting to 0"); - $$self{TimerInterval} = 0; - } -} - - - -# --------------------------------------------------------------------------- -# Method: _check_for_data (private) -# Description: hooked routine that checks data on port -sub _check_for_data -{ - # &main::print_log("Rain8Net.pm - MODULE DEBUG : entering init [_check_for_data]...") if $ModuleDebug; - - $main::Serial_Ports{'Rain8Net'}{data} = ''; - &main::check_for_generic_serial_data('Rain8Net'); - #-- my $NewCmd = $main::Serial_Ports{'Rain8Net'}{data}; - - my $NewCmd = $main::Serial_Ports{'Rain8Net'}{data}; - #-- $main::Serial_Ports{'Rain8Net'}{data} = ''; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : Received the following [$NewCmd]") if (($NewCmd) && ($ModuleDebug)); - - # we need to buffer the information received, because many command could be include in a single pass - $NewCmd = $IncompleteCmd . $NewCmd if $IncompleteCmd; - return if !$NewCmd; - - my $self = $Rain8Net_Objects[0]; - - # Flush last buffered response - $$self{LastResponseRaw} = ''; - - # Rain8Net commands are 3 bytes - if (length($NewCmd) ge 3) - { - $IncompleteCmd = substr($NewCmd,3); # Keep rest of Command - &_CheckCmd($self,$NewCmd); - } -} - - -# --------------------------------------------------------------------------- -# Method: _SetTimers (private) -# Description: set up auto status request timers for all units -sub _SetTimers -{ - my ($self) = @_; - - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_SetTimers]...") if $ModuleDebug; - - # set a timer to perform auto Statusrequest every 30 - if (($$self{object_name}) && ($$self{TimerInterval} > 0)) - { - &::print_log("Rain8Net.pm - MODULE DEBUG : creating timer(s) for object [" . $$self{object_name} . "]") if $ModuleDebug; - for (my $count = 1; $count <= $$self{MaxUnits}; $count++) - { - my $timername = 'statusrequest_timer' . $count; - - if (not ($$self{$timername})) - { - &::print_log("Rain8Net.pm - MODULE DEBUG : creating timer [$timername]") if $ModuleDebug; - $$self{$timername} = new Timer; - $$self{$timername}->set($$self{TimerInterval}, "$$self{object_name}->cmd('StatusRequest',$count)", -1); - } - } - } - -} - -# --------------------------------------------------------------------------- -# Method: _CheckCmd (private) -# Description: validate in incoming command string from the Rain8 unit and apply the data updates -sub _CheckCmd -{ - my ($self,$CmdStr) = @_; - - my $UnitNumber = -1; - my $ZoneNumber = -1; - my $ZoneBitFlag = 0; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_CheckCmd]...") if $ModuleDebug; - - &_SetTimers($self); - - &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd - cmdstr is ($CmdStr)") if $ModuleDebug; - - return if !$CmdStr; - - $$self{LastResponseRaw} = $CmdStr; - - my $l = length($CmdStr); - my $code = unpack("H*", substr( $CmdStr, 0, 1 )); - my $arg1 = unpack("H*", substr( $CmdStr, 1, 1 )); - my $arg2 = unpack("H*", substr( $CmdStr, 2, 1 )); - my $hexcode = sprintf("0x%d",$code); - - - &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd Raw input is [$code] [$arg1] [$arg2], length [$l]") if $ModuleDebug; - - # test received command is valid - if (not ( exists $CmdMsgRev{$hexcode} )) - { - &::print_log("Unknown rain8Net response code recieved : code [$code] with arguments [$arg1] , [$arg2]"); - $$self{LastSubCmdSent} = 0; - return; - } - - my $CmdName; - - - # Since StatusRequest and ZoneOn and ZoneOFF all return the same god-damn 40 hex, use our flag - if (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_STATUSREQUEST)) - { - $CmdName = "StatusRequest"; - } - elsif (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_ZONEON)) - { - $CmdName = "ZoneON"; - } - elsif (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_ZONEOFF)) - { - $CmdName = "ZoneOFF"; - } - elsif (($hexcode eq "0x40") && ($$self{LastSubCmdSent} eq SUBCOMMAND_ALLUNITOFF)) - { - $CmdName = "AllModuleOFF"; - } - else - { - $CmdName = ($CmdMsgRev{$hexcode}); - } - - &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd Command recived is = [" . $CmdName . "]") if $ModuleDebug; - - # At this point process the response...-------------- - - # If Status request response, the last byte is a bit pattern of active zones - if ($CmdName eq "StatusRequest") - { - $UnitNumber = hex($arg1); - $ZoneBitFlag = hex($arg2); - my $match = 1; - for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) - { - $$self{zone_status}{$UnitNumber}{$i} = ( $ZoneBitFlag & $match ) ? 1 : 0; - $match = $match << 1; - } - } - - - # Echo from ZoneOFF - if ($CmdName eq "ZoneOFF") - { - $UnitNumber = hex($arg1); - my $UpperNible = hex($arg2) & hex('0x40'); - my $LowerNible = hex($arg2) & hex('0x08'); - - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub _checkCmd - ZONE OFF, Unit [$UnitNumber], uppernibble [$UpperNible], lowerninble [$LowerNible]") if $ModuleDebug; - - $$self{zone_status}{$UnitNumber}{$LowerNible} = 0; - } - - # Echo from ZoneON - if ($CmdName eq "ZoneON") - { - $UnitNumber = hex($arg1); - my $UpperNible = hex($arg2) & hex("0x30"); - my $LowerNible = hex($arg2) & hex('0x08'); - - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub _checkCmd - ZONE ON, Unit [$UnitNumber], uppernibble [$UpperNible], lowerninble [$LowerNible]") if $ModuleDebug; - - $$self{zone_status}{$UnitNumber}{$LowerNible} = 1; - } - - # Echo from AllModuleOff - if ($CmdName eq "AllModuleOFF") - { - $UnitNumber = hex($arg1); - - if (hex($arg2) eq hex("0x55")) - { - for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) - { - $$self{zone_status}{$UnitNumber}{$i} = 0; - } - } - } - - - # Dump debug - if (($ModuleDebug) && ($UnitNumber >= 1)) - { - &main::print_log("Rain8Net.pm - MODULE DEBUG : _CheckCmd dump status -->"); - for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) - { - &main::print_log(" Unit [$UnitNumber], zone [$i] is [" . ($$self{zone_status}{$UnitNumber}{$i}) . "]"); - } - } - - - # Reset the last subcommand - $$self{LastSubCmdSent} = 0; - - return; -} - - - - -#}}} -# Define hash with DSC command {{{ -sub _DefineCmdMsg { - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [_DefineCmdMsg]...") if $ModuleDebug; - - %CmdMsg = ( - lc("COMCheck") => "0x70", - lc("StatusRequest") => "0x40", - lc("ZoneON") => "0x40", - lc("ZoneOFF") => "0x40", - lc("AllModuleOFF") => "0x40", - lc("AllGlobalOFF") => "0x20", - lc("ReadRainSwitchStatus") => "0x50", - lc("ReadFlowMeterCounter") => "0x50", - lc("ClearFlowMeterCounter") => "0x50" - ); - - %CmdMsgRev = reverse %CmdMsg; - return; -} - - - - - -#}}} -# Sending command to Rain8Net -sub cmd -{ - - my ( $self, $cmd, $arg1, $arg2 ) = @_; - my $CmdName; - - if ($ModuleDebug) - { - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [cmd]..."); - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - cmd is [" . $cmd . "]"); - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - arg1 is [" . $arg1 . "]"); - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - arg2 is [" . $arg2 . "]"); - } - - $cmd = lc($cmd); - if (not (exists $CmdMsg{$cmd}) ) - { - &::print_log("Rain8Net.pm ERROR - Invalid command : ($cmd) with arguments $arg1 , $arg2"); - return; - } - - my $CmdByte = hex($CmdMsg{$cmd}); - my $CmdStr = chr($CmdByte); - my $UnitNumber = 0; - my $ZoneNumber = 0; - - if ($cmd eq lc("COMCheck")) - { - # pad command with 2 extra bytes, does not matter which - $CmdStr .= "xx"; - } - elsif ($cmd eq lc("StatusRequest")) - { - $UnitNumber = $self->_ConvertUnit($cmd, $arg1); - return if ( $UnitNumber lt 0 ); - - # Second arg is "all status code" - $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0xf0")); - $$self{LastSubCmdSent} = SUBCOMMAND_STATUSREQUEST; - } - elsif ($cmd eq lc("ZoneON")) - { - $UnitNumber = $self->_ConvertUnit($cmd, $arg1); - return if ( $UnitNumber lt 0 ); - - $ZoneNumber = $self->_ConvertZone($cmd, $arg2); - return if ( $ZoneNumber lt 0 ); - - # Second arg is 3 upper nibble, zone - lower nibble - $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0x3" . "$ZoneNumber")); - $$self{LastSubCmdSent} = SUBCOMMAND_ZONEON; - $$self{zone_status}{$UnitNumber}{$ZoneNumber} = 1; - } - elsif ($cmd eq lc("ZoneOFF")) - { - $UnitNumber = $self->_ConvertUnit($cmd, $arg1); - return if ( $UnitNumber lt 0 ); - - $ZoneNumber = $self->_ConvertZone($cmd, $arg2); - return if ( $ZoneNumber lt 0 ); - - # Second arg is 4 upper nibble, zone - lower nibble - $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0x4" . "$ZoneNumber")); - $$self{LastSubCmdSent} = SUBCOMMAND_ZONEOFF; - $$self{zone_status}{$UnitNumber}{$ZoneNumber} = 0; - } - elsif ($cmd eq lc("AllModuleOFF")) - { - $UnitNumber = $self->_ConvertUnit($cmd, $arg1); - return if ( $UnitNumber lt 0 ); - - $CmdStr .= chr(hex("0x0" . $UnitNumber)) . chr(hex("0x55")); - $$self{LastSubCmdSent} = SUBCOMMAND_ALLUNITOFF; - for (my $i = 1; $i <= RAIN8NET_MAXZONES; $i++) - { - $$self{zone_status}{$UnitNumber}{$i} = 0; - } - - - } - elsif ($cmd eq lc("AllGlobalOff")) - { - $$self{LastSubCmdSent} = 0; - $CmdStr .= chr(hex("0x55")) . chr(hex("0x55")); - for (my $i = 1; $i <= $$self{MaxUnits}; $i++) - { - for (my $j = 1; $j <= RAIN8NET_MAXZONES; $j++) - { - $$self{zone_status}{$i}{$j} = 0; - } - } - } - - - -# &::print_log("Sending to Rain8Net panel ($cmd) with argument ($arg1, $arg2)"); - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - Writing (" . $CmdStr . ")") if $ModuleDebug; - if ($main::Serial_Ports{Rain8Net}{object}->write($CmdStr)) - { - &main::print_log("Rain8Net.pm - MODULE DEBUG : sub cmd - Command (" . $CmdStr . ") successfully written to serial port.") if $ModuleDebug; - $$self{LastCmdSent} = $cmd; - $$self{LastCmdSentUnit} = $UnitNumber; - $$self{LastCmdSentZone} = $ZoneNumber; - } - - - return "Sent to Rain8Net panel: ($cmd) with argument ($arg1,$arg2) - Successful"; - -} - - - - -#}}} -# validate and Convert Unit Number to value -sub _ConvertUnit -{ - my ($self, $cmd, $Unit) = @_; - - my $UnitNumber = $Unit + 0; - if ( ($UnitNumber >= 1) && ($UnitNumber <= $$self{MaxUnits}) ) - { - # $UnitNumber is in the range - return ($UnitNumber); - } - else - { - &::print_log("Rain8Net Error - Invalid Unit number on command : [$cmd], Unit [$Unit]"); - return -1; - } -} - -#}}} -# validate and Convert Zone Number to numeric value. -# retrun -1 if invalid -sub _ConvertZone -{ - - my ($self, $cmd, $Zone) = @_; - - my $ZoneNumber = $Zone + 0; - - if ( ($ZoneNumber >= 1) && ($ZoneNumber <= RAIN8NET_MAXZONES) ) - { - # $ZoneNumber is in the range - return ($ZoneNumber); - } - else - { - &::print_log("Rain8Net Error - Invalid zone number on command : [$cmd], zone [$Zone])"); - return -1; - } -} - - - - - -#}}} -# user call from MH {{{ -# most of them are replicate to hash value, easier to code - -sub status_zone -{ - my ( $self, $unit, $zone ) = @_; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [status_zone]...") if $ModuleDebug; - - my $UnitNumber = $self->_ConvertUnit("method: status_zone",$unit); - return -1 if ($UnitNumber < 1); - - my $ZoneNumber = $self->_ConvertZone("method: status_zone",$zone); - return -1 if ($ZoneNumber < 1); - - return $$self{zone_status}{$UnitNumber}{$ZoneNumber} if defined $$self{zone_status}{$UnitNumber}{$ZoneNumber}; -} - -sub zone_name -{ - my ( $self, $unit, $zone ) = @_; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [zone_name]...") if $ModuleDebug; - - my $UnitNumber = $self->_ConvertUnit("method: zone_name",$unit); - return -1 if ($UnitNumber < 1); - - my $ZoneNumber = $self->_ConvertZone("method: zone_name",$zone); - return -1 if ($ZoneNumber < 1); - - my $unitstr = sprintf "%d", $UnitNumber; - my $zonestr = sprintf "%d", $ZoneNumber; - - my $cfgparmname = "RAIN8NET_unit_" . $unitstr . "_zone_" . $zonestr; - - my $ZoneName = $main::config_parms{$cfgparmname} if exists $main::config_parms{$cfgparmname}; - - return $ZoneName if $ZoneName; - - return $ZoneNumber; -} - - - -sub ZoneOn -{ - my ( $self, $unit, $zone ) = @_; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [ZoneOn]...") if $ModuleDebug; - - my $UnitNumber = $self->_ConvertUnit("method: ZoneOn",$unit); - return -1 if ($UnitNumber < 1); - - my $ZoneNumber = $self->_ConvertZone("method: ZoneOn",$zone); - return -1 if ($ZoneNumber < 1); - - $self->cmd("ZoneOn", $UnitNumber, $ZoneNumber); -} - - -sub ZoneOff -{ - my ( $self, $unit, $zone ) = @_; - - &main::print_log("Rain8Net.pm - MODULE DEBUG : entering [ZoneOff]...") if $ModuleDebug; - - # If no unit or zone, all global off!!!! - if (@_ < 2) - { - &main::print_log("Rain8Net.pm - MODULE DEBUG : [ZoneOff] missing unit/zone - calling AllGlobalOff...") if $ModuleDebug; - $self->cmd("AllGlobalOff"); - return; - } - - my $UnitNumber = $self->_ConvertUnit("method: ZoneOff",$unit); - return -1 if ($UnitNumber < 1); - - # If unit but no zone - if (@_ < 3) - { - &main::print_log("Rain8Net.pm - MODULE DEBUG : [ZoneOff] missing zone - calling AllModuleOFF...") if $ModuleDebug; - $self->cmd("AllModuleOFF",$UnitNumber); - return; - } - - # If Here ... a particular unit/zone is to be turned off - - my $ZoneNumber = $self->_ConvertZone("method: ZoneOff",$zone); - return -1 if ($ZoneNumber < 1); - - &main::print_log("Rain8Net.pm - MODULE DEBUG : [ZoneOff] call ZoneOff...") if $ModuleDebug; - $self->cmd("ZoneOff", $UnitNumber, $ZoneNumber); -} - - -1; - From 4b24e0393720b6230aac494d7dce4253ab7ec0e8 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Mon, 11 Jul 2016 16:36:07 -0700 Subject: [PATCH 040/209] Logging cleanup Reduced verbosity of logging when verbose logging is disabled in MH. --- lib/Ecobee.pm | 157 ++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 81 deletions(-) diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 2eef3be53..5e224276b 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -204,11 +204,11 @@ sub _check_auth { if ((defined $$self{access_token}) && (defined $$self{refresh_token})) { if (($$self{access_token} eq '') || ($$self{refresh_token} eq '')) { # The access_token or refresh_token are missing. Tell the user and wait - main::print_log( "[Ecobee] Error: Missing tokens. Please reauthenticate the API key with the Ecobee portal"); + $self->debug("Error: Missing tokens. Please reauthenticate the API key with the Ecobee portal"); $self->_request_pin_auth(); } else { # Ok, we have tokens. Make sure they are current, then go get the initial state of the device, then start the time to look for updates - main::print_log( "[Ecobee] We have tokens, lets proceed"); + $self->debug("We have tokens, lets proceed"); $self->_thermostat_summary(); # This populates the revision numbers and operating state $self->_list_thermostats(); # This gets the full initial state of each thermostat $self->_get_groups(); # This gets the groups, their settings and the associated thermostats in each group @@ -217,26 +217,26 @@ sub _check_auth { # Testing functions here. These will be removed eventually ## #$self->print_devices(); - main::print_log( "[Ecobee] actualTemperature is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "actualTemperature")/10) . " degrees F" ); - main::print_log( "[Ecobee] desiredHeat is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Heat")/10) . " degrees F" ); - main::print_log( "[Ecobee] desiredCool is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Cool")/10) . " degrees F" ); - main::print_log( "[Ecobee] Office temp is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "Office")/10) . " degrees F" ); - main::print_log( "[Ecobee] actualHumidity is " . $self->get_humidity("Monet Thermostat", "actualHumidity") . "%"); - main::print_log( "[Ecobee] desiredHumidity is " . $self->get_desired_comfort("Monet Thermostat", "Humidity") . "%"); - main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); - main::print_log( "[Ecobee] hvacMode is " . $self->get_setting("Monet Thermostat", "hvacMode") ); - my $alerts = $self->get_alert("Monet Thermostat"); - if ($alerts) { - foreach my $key (keys %{$alerts}) { - main::print_log( "[Ecobee] Alert " . $key . ": (\"" . $alerts->{$key}{text} . "\")" ); - } - } - my $events = $self->get_event("Monet Thermostat"); - if ($events) { - foreach my $key (keys %{$events}) { - main::print_log( "[Ecobee] Event: $key" ); - } - } + #main::print_log( "[Ecobee] actualTemperature is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "actualTemperature")/10) . " degrees F" ); + #main::print_log( "[Ecobee] desiredHeat is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Heat")/10) . " degrees F" ); + #main::print_log( "[Ecobee] desiredCool is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Cool")/10) . " degrees F" ); + #main::print_log( "[Ecobee] Office temp is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "Office")/10) . " degrees F" ); + #main::print_log( "[Ecobee] actualHumidity is " . $self->get_humidity("Monet Thermostat", "actualHumidity") . "%"); + #main::print_log( "[Ecobee] desiredHumidity is " . $self->get_desired_comfort("Monet Thermostat", "Humidity") . "%"); + #main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); + #main::print_log( "[Ecobee] hvacMode is " . $self->get_setting("Monet Thermostat", "hvacMode") ); + #my $alerts = $self->get_alert("Monet Thermostat"); + #if ($alerts) { + # foreach my $key (keys %{$alerts}) { + # main::print_log( "[Ecobee] Alert " . $key . ": (\"" . $alerts->{$key}{text} . "\")" ); + # } + #} + #my $events = $self->get_event("Monet Thermostat"); + #if ($events) { + # foreach my $key (keys %{$events}) { + # main::print_log( "[Ecobee] Event: $key" ); + # } + #} #### # The basic details should be populated now so we can start to poll @@ -248,7 +248,7 @@ sub _check_auth { } else { # If we don't have tokens, we need to get a PIN and wait until they user registers it # The access_token or refresh_token are undefined. This is probably the first run. Tell the user and wait - main::print_log( "[Ecobee] Error: Token variables undefined. Please authenticate the PIN with the Ecobee portal. A request for a new PIN will follow this message."); + $self->debug("Error: Token variables undefined. Please authenticate the PIN with the Ecobee portal. A request for a new PIN will follow this message."); $self->_request_pin_auth(); } # If we have tokens, go get the initial state of the device, then start the time to look for updates @@ -300,10 +300,10 @@ sub _wait_for_tokens { # We need to periodically refresh the tokens when they expire sub _refresh_tokens { my ($self) = @_; - main::print_log( "[Ecobee]: Refreshing tokens" ); + $self->debug("Refreshing tokens"); my ($isSuccessResponse1, $tokenparams) = $self->_get_JSON_data("POST", "token", "?grant_type=refresh_token&refresh_token=" . $$self{refresh_token} . "&client_id=" . $$self{api_key}); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Refresh token response looks good" ); + $self->debug("Refresh token response looks good"); $$self{access_token} = $tokenparams->{access_token}; $$self{refresh_token} = $tokenparams->{refresh_token}; } else { @@ -326,7 +326,7 @@ Collects the initial settings and parameters for each device on the Ecobee accou sub _list_thermostats { my ($self) = @_; - main::print_log( "[Ecobee]: Listing thermostats..." ); + $self->debug("Listing thermostats..."); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -335,7 +335,7 @@ sub _list_thermostats { my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Thermostat response looks good." ); + $self->debug("Thermostat response looks good."); foreach my $device (@{$thermoparams->{thermostatList}}) { # we need to inspect the runtime and remoteSensors foreach my $key (keys %{$device->{runtime}}) { @@ -382,7 +382,7 @@ sub _list_thermostats { # modify a schedule or climate, they will be retained in this format $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; - main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); + $self->debug($$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" ); @@ -398,7 +398,7 @@ Gets the current settings, events and program sub _get_settings { my ($self) = @_; - main::print_log( "[Ecobee]: Getting settings, events and programs..." ); + $self->debug("Getting settings, events and programs..."); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -407,7 +407,7 @@ sub _get_settings { my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Settings response looks good." ); + $self->debug("Settings response looks good."); # We just asked for the settings this time foreach my $device (@{$thermoparams->{thermostatList}}) { # Save the previous settings @@ -415,7 +415,7 @@ sub _get_settings { foreach my $key (keys %{$device->{settings}}) { if ($device->{settings}{$key} ne $$self{data}{devices}{$device->{identifier}}{settings}{$key}) { - main::print_log( "[Ecobee]: settings parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{settings}{$key} . " to " . $device->{settings}{$key}); + $self->debug("Settings parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{settings}{$key} . " to " . $device->{settings}{$key}); } $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; } @@ -437,7 +437,7 @@ sub _get_settings { # Look for new events foreach my $key (keys %{$temp_events}) { unless (defined $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}) { - main::print_log( "[Ecobee]: New event added: $key" ); + $self->debug("New event added: $key"); $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key} = $temp_events->{$key}; if (exists $$self{data}{devices}{$device->{identifier}}{eventsHash}{'none'}) { # delete the none event if there is a real one @@ -448,7 +448,7 @@ sub _get_settings { # Look for deleted events foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{eventsHash}}) { unless (defined $temp_events->{$key} || ($key eq 'none')) { - main::print_log( "[Ecobee]: Event deleted: $key" ); + $self->debug("Event deleted: $key"); delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}; } } @@ -468,7 +468,7 @@ sub _get_settings { $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; if ($$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ne $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef}) { - main::print_log( "[Ecobee]: currentClimateRef has changed from " . $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef} . " to " . $$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ); + $self->debug("currentClimateRef has changed from " . $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef} . " to " . $$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ); } # Compare the old with the new @@ -488,7 +488,7 @@ Gets the current alerts sub _get_alerts { my ($self) = @_; - main::print_log( "[Ecobee]: Getting alerts..." ); + $self->debug("Getting alerts..."); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -497,7 +497,7 @@ sub _get_alerts { my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Alerts response looks good." ); + $self->debug("Alerts response looks good."); # We just asked for the settings this time foreach my $device (@{$thermoparams->{thermostatList}}) { # Save the previous alerts @@ -512,7 +512,7 @@ sub _get_alerts { } if (!$matched) { # Alert has been acked - main::print_log( "[Ecobee]: Alert $key: (\"" . $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}{text} . "\") has been acked." ); + $self->debug("Alert $key: (\"" . $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}{text} . "\") has been acked."); delete $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}; } } @@ -521,7 +521,7 @@ sub _get_alerts { # Do we need to see if something has changed? All of the alert properties should be static and the alert dissappears from the JSON array once acked. } else { # This is a new alert - main::print_log( "[Ecobee]: A new alert " . $index->{acknowledgeRef} . ": (\"" . $index->{text} . "\") has been generated." ); + $self->debug("A new alert " . $index->{acknowledgeRef} . ": (\"" . $index->{text} . "\") has been generated."); $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}} = $index; } } @@ -542,7 +542,7 @@ Gets the group and grouping data for thermostats sub _get_groups { my ($self) = @_; - main::print_log( "[Ecobee]: Getting groups..." ); + $self->debug("Getting groups..."); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -551,7 +551,7 @@ sub _get_groups { my ($isSuccessResponse1, $groupparams) = $self->_get_JSON_data("GET", "group", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Groups response looks good." ); + $self->debug("Groups response looks good."); foreach my $group (@{$groupparams->{groups}}) { # groupRef is the unique ID $$self{data}{groups}{$group->{groupRef}} = $group; @@ -570,7 +570,7 @@ Gets the runtime and sensor data sub _get_runtime_with_sensors { my ($self) = @_; - main::print_log( "[Ecobee]: Getting runtime and sensor data..." ); + $self->debug("Getting runtime and sensor data..."); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -579,7 +579,7 @@ sub _get_runtime_with_sensors { my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", '?format=json&body=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Runtime response looks good." ); + $self->debug("Runtime response looks good."); foreach my $device (@{$thermoparams->{thermostatList}}) { # Save the previous runtime @@ -592,10 +592,10 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; } # Compare the old with the new - main::print_log( "[Ecobee]: runtime monitor -pre-"); - print "*** Object *** \n"; - print Data::Dumper::Dumper( \$self->{monitor}); - print "*** Object *** \n"; + #main::print_log( "[Ecobee]: runtime monitor -pre-"); + #print "*** Object *** \n"; + #print Data::Dumper::Dumper( \$self->{monitor}); + #print "*** Object *** \n"; $self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}}{runtime} ); # Save the previous remoteSensorsHash @@ -607,7 +607,7 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{type} = $index->{type}; if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse}) { if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} ne $index->{inUse}) { - main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} . " to " . $index->{inUse} ); + $self->debug($$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} . " to " . $index->{inUse} ); } } $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} = $index->{inUse}; @@ -617,7 +617,7 @@ sub _get_runtime_with_sensors { $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{type} = $capability->{type}; if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value}) { if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} ne $capability->{value}) { - main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . ", capability " . $capability->{type} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ":" . $capability->{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} . " to " . $capability->{value} ); + $self->debug($$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . ", capability " . $capability->{type} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ":" . $capability->{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} . " to " . $capability->{value} ); } } $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; @@ -625,7 +625,7 @@ sub _get_runtime_with_sensors { # Compare the old with the new, but we need to handle this differently since it is nested (a child of a child) # $self->compare_data( $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{monitor}{remoteSensorsHash} ); } - main::print_log( "[Ecobee]: " . $$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); + $self->debug($$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); } } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the runtime and sensor request" ); @@ -642,7 +642,7 @@ has changed, and we need to request an update. sub _thermostat_summary { my ($self) = @_; - main::print_log( "[Ecobee]: Getting thermostat summary..." ); + $self->debug("Getting thermostat summary..."); my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} @@ -651,7 +651,7 @@ sub _thermostat_summary { my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostatSummary", '?json=' . uri_escape($json_body), $headers); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Thermostat response looks good. Found " . $thermoparams->{thermostatCount} . " thermostats" ); + $self->debug("Thermostat response looks good. Found " . $thermoparams->{thermostatCount} . " thermostats"); foreach my $device (@{$thermoparams->{revisionList}}) { # format: # identifier:name:connected:thermoRev:alertsRev:runtimeRev:intervalRev @@ -664,7 +664,7 @@ sub _thermostat_summary { if (defined $$self{data}{devices}{$values[0]}{connected}) { if ($$self{data}{devices}{$values[0]}{connected} ne $values[2]) { # This tells us if we are connected to the Ecobee servers - main::print_log( "[Ecobee]: connected has changed from " . $$self{data}{devices}{$values[0]}{connected} . " to " . $values[2] ); + $self->debug("connected has changed from " . $$self{data}{devices}{$values[0]}{connected} . " to " . $values[2] ); } } $$self{data}{devices}{$values[0]}{connected} = $values[2]; @@ -672,7 +672,7 @@ sub _thermostat_summary { if (defined $$self{data}{devices}{$values[0]}{thermoRev}) { if ($$self{data}{devices}{$values[0]}{thermoRev} != $values[3]) { # This tells us that the thermostat program, hvac mode, settings or configuration has changed - main::print_log( "[Ecobee]: thermoRev has changed from " . $$self{data}{devices}{$values[0]}{thermoRev} . " to " . $values[3] ); + $self->debug("thermoRev has changed from " . $$self{data}{devices}{$values[0]}{thermoRev} . " to " . $values[3] ); $self->_get_settings(); } } @@ -681,7 +681,7 @@ sub _thermostat_summary { if (defined $$self{data}{devices}{$values[0]}{alertsRev}) { if ($$self{data}{devices}{$values[0]}{alertsRev} != $values[4]) { # This tells us of a new alert is issued or an alert is modified (acked) - main::print_log( "[Ecobee]: alertsRev has changed from " . $$self{data}{devices}{$values[0]}{alertsRev} . " to " . $values[4] ); + $self->debug("alertsRev has changed from " . $$self{data}{devices}{$values[0]}{alertsRev} . " to " . $values[4] ); $self->_get_alerts(); } } @@ -690,7 +690,7 @@ sub _thermostat_summary { if (defined $$self{data}{devices}{$values[0]}{runtimeRev}) { if ($$self{data}{devices}{$values[0]}{runtimeRev} != $values[5]) { # This tells us when the thermostat has sent a new status message, or the equipment state or remote sensor readings have changed - main::print_log( "[Ecobee]: runtimeRev has changed from " . $$self{data}{devices}{$values[0]}{runtimeRev} . " to " . $values[5] ); + $self->debug("runtimeRev has changed from " . $$self{data}{devices}{$values[0]}{runtimeRev} . " to " . $values[5] ); $self->_get_runtime_with_sensors(); } } @@ -699,7 +699,7 @@ sub _thermostat_summary { if (defined $$self{data}{devices}{$values[0]}{intervalRev}) { if ($$self{data}{devices}{$values[0]}{intervalRev} != $values[6]) { # This tells us that the thermostat has sent a new status message (every 15 minutes) - main::print_log( "[Ecobee]: intervalRev has changed from " . $$self{data}{devices}{$values[0]}{intervalRev} . " to " . $values[6] ); + $self->debug("intervalRev has changed from " . $$self{data}{devices}{$values[0]}{intervalRev} . " to " . $values[6] ); } } $$self{data}{devices}{$values[0]}{intervalRev} = $values[6]; @@ -752,7 +752,7 @@ sub _thermostat_summary { @states = split(',',$values[1]); foreach my $index (0..$#states) { $statusvec |= $status_bit_LUT->{$states[$index]}; - main::print_log( "[Ecobee]: Statusvec=$statusvec, key=" . $states[$index] . ", LUT=" . $status_bit_LUT->{$states[$index]}); + $self->debug("Statusvec=$statusvec, key=" . $states[$index] . ", LUT=" . $status_bit_LUT->{$states[$index]}); } } if (exists $$self{data}{devices}{$values[0]}{statusvec}{status}) { @@ -787,17 +787,17 @@ sub _thermostat_summary { $matched = 1; if ($$self{data}{devices}{$values[0]}{status}{$states[$index]} == 0) { $$self{data}{devices}{$values[0]}{status}{$states[$index]} = 1; - main::print_log( "[Ecobee]: Status $stat has changed from off to on" ); + $self->debug("Status $stat has changed from off to on"); } } } if ((!$matched) && ($$self{data}{devices}{$values[0]}{status}{$stat} == 1)) { - main::print_log( "[Ecobee]: C1 Status $stat has changed from on to off" ); + $self->debug("C1 Status $stat has changed from on to off"); } } else { if ($$self{data}{devices}{$values[0]}{status}{$stat} != 0) { $$self{data}{devices}{$values[0]}{status}{$stat} = 0; - main::print_log( "[Ecobee]: C2 Status $stat has changed from on to off" ); + $self->debug("C2 Status $stat has changed from on to off"); } } } @@ -840,13 +840,13 @@ sub _get_JSON_data { my $request = HTTP::Request->new( $type, $url . $rest{$endpoint} . $args, $headers ); $request->content($content) if defined $content; - main::print_log( "[Ecobee]: Full request ->" . $request->as_string . "<-") if $$self{debug}; + $self->debug("Full request ->" . $request->as_string . "<-"); my $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" if $$self{debug}; + $self->debug($responseObj->content . "\n--------------------"); my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if $$self{debug}; + $self->debug("Response code: " . $responseCode); my $isSuccessResponse = $responseCode < 400; my $response; @@ -854,7 +854,7 @@ sub _get_JSON_data { # catch crashes: if ($@) { - print "[Ecobee]: ERROR! JSON parser crashed! $@\n"; + main::print_log("ERROR! JSON parser crashed! $@"); return ('0'); } else { @@ -876,7 +876,7 @@ sub _get_JSON_data { } } else { - main::print_log( "[Ecobee]: Got a valid response."); + $self->debug("Got a valid response."); } return ( $isSuccessResponse, $response ); @@ -916,7 +916,7 @@ sub register { sub compare_data { my ( $self, $data, $prev_data, $monitor_hash ) = @_; - main::print_log( "[Ecobee]: starting execution within compare_data()"); + $self->debug("Starting execution within compare_data()"); while ( my ( $key, $value ) = each %{$data} ) { # Use empty hash reference is it doesn't exist my $prev_value = {}; @@ -929,7 +929,7 @@ sub compare_data { } elsif ( ($value ne $prev_value) && (ref $monitor_value eq 'ARRAY') ) { for my $action ( @{$monitor_value} ) { - main::print_log( "[Ecobee]: I am running action for key $key, value $value"); + $self->debug("I am running action for key $key, value $value"); &$action( $key, $value ); } } @@ -1324,8 +1324,7 @@ sub device_id { return $device_id; } } - $self->debug( - "ERROR, no device by the name " . $$parent{name} . " was found." ); + $self->debug("ERROR, no device by the name " . $$parent{name} . " was found." ); return 0; } @@ -1349,8 +1348,7 @@ sub sensor_id { } } } - $self->debug( - "ERROR, no sensor by the name " . $$parent{sensor_name} . " was found on device " . $$parent{name} . "." ); + $self->debug("ERROR, no sensor by the name " . $$parent{sensor_name} . " was found on device " . $$parent{name} . "." ); return 0; } @@ -1502,11 +1500,8 @@ sub set_hvac_mode { && $state ne 'auxHeatOnly' && $state ne 'cool' && $state ne 'auto' - && $state ne 'off' ) - { - $self->debug( - "set_hvac_mode must be one of: heat, auxHeatOnly, cool, auto, or off. Not $state." - ); + && $state ne 'off' ) { + $self->debug("set_hvac_mode must be one of: heat, auxHeatOnly, cool, auto, or off. Not $state."); return; } $$self{state_pending}{hvacMode} = [ $p_setby, $p_response ]; @@ -1521,7 +1516,7 @@ sub set_hvac_mode { my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; my ($isSuccessResponse1, $modeparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Mode change response looks good" ); + $self->debug("Mode change response looks good" ); } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the mode change request" ); } @@ -1539,7 +1534,7 @@ climate holds are in the format climate__ sub set_hold { my ( $self, $state, $p_setby, $p_response ) = @_; - main::print_log( "[Ecobee]: Attempting to set a thermostat hold to $state" ); + $self->debug("Attempting to set a thermostat hold to $state" ); my @s_params = split('_', $state); my $json_body; if (($s_params[0] eq 'climate') && (scalar @s_params == 3)) { @@ -1562,7 +1557,7 @@ sub set_hold { ); my ($isSuccessResponse1, $holdparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Set hold response looks good" ); + $self->debug("Set hold response looks good" ); } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the set hold request" ); } @@ -1578,7 +1573,7 @@ Clears the current thermostat hold. sub clear_hold { my ($self) = @_; - main::print_log( "[Ecobee]: Attempting to clear thermostat hold" ); + $self->debug("Attempting to clear thermostat hold"); $$self{interface}{polling_timer}->pause; my $headers = HTTP::Headers->new( 'Content-Type' => 'text/json', @@ -1587,7 +1582,7 @@ sub clear_hold { my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"resumeProgram","params":{"resumeAll":false}}]}'; my ($isSuccessResponse1, $holdparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); if ($isSuccessResponse1) { - main::print_log( "[Ecobee]: Clear hold response looks good" ); + $self->debug("Clear hold response looks good"); } else { main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the clear hold request" ); } From 9ac2270c5d114c2a868fdf973abdd98ef5f23cfe Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Mon, 25 Jul 2016 10:49:58 +0200 Subject: [PATCH 041/209] Revert "Ia7 v1.2.301" --- code/common/ia7_collection_upgrader.pl | 51 - code/common/ia7_notifications.pl | 45 - data/web/collections.json | 13 +- data/web/ia7_config.json | 4 +- lib/ajax.pm | 5 +- lib/http_server.pl | 154 +- lib/json_server.pl | 233 +-- web/bin/code_select.pl | 12 +- web/bin/code_unselect.pl | 2 +- web/bin/iniedit.pl | 4 +- web/bin/items.pl | 29 +- web/bin/set_func.pl | 5 +- web/bin/triggers.pl | 15 +- web/ia7/graphics/help.gif | Bin 1582 -> 0 bytes web/ia7/graphics/important.gif | Bin 1492 -> 0 bytes web/ia7/graphics/info.gif | Bin 1487 -> 0 bytes web/ia7/graphics/title.gif | Bin 317 -> 0 bytes web/ia7/house/main.shtml | 19 +- .../include/bootstrap-custom-button.3.3.5.css | 29 - web/ia7/include/bootstrap-theme.3.0.2.min.css | 9 + web/ia7/include/bootstrap.3.0.2.min.css | 9 + web/ia7/include/bootstrap.3.0.2.min.js | 9 + web/ia7/include/font-awesome.4.0.3.min.css | 4 + web/ia7/include/javascript.js | 1488 +++-------------- web/ia7/include/jquery.alerts.css | 57 - web/ia7/include/jquery.alerts.js | 237 --- web/ia7/include/tables.css | 8 +- web/ia7/index.shtml | 50 +- web/organizer/contacts.pl | 2 +- 29 files changed, 441 insertions(+), 2052 deletions(-) delete mode 100644 code/common/ia7_collection_upgrader.pl delete mode 100644 code/common/ia7_notifications.pl delete mode 100755 web/ia7/graphics/help.gif delete mode 100755 web/ia7/graphics/important.gif delete mode 100755 web/ia7/graphics/info.gif delete mode 100755 web/ia7/graphics/title.gif delete mode 100644 web/ia7/include/bootstrap-custom-button.3.3.5.css create mode 100644 web/ia7/include/bootstrap-theme.3.0.2.min.css create mode 100644 web/ia7/include/bootstrap.3.0.2.min.css create mode 100644 web/ia7/include/bootstrap.3.0.2.min.js create mode 100644 web/ia7/include/font-awesome.4.0.3.min.css delete mode 100755 web/ia7/include/jquery.alerts.css delete mode 100644 web/ia7/include/jquery.alerts.js diff --git a/code/common/ia7_collection_upgrader.pl b/code/common/ia7_collection_upgrader.pl deleted file mode 100644 index b8c8697db..000000000 --- a/code/common/ia7_collection_upgrader.pl +++ /dev/null @@ -1,51 +0,0 @@ -# Category = IA7 - -#@ IA7 v1.2 : This is a helper utility that can find and update collections.json files -#@ if any structural changes are required. -#@ -#@ v1.2 - add in new login system id 700 - -my $ia7_coll_current_ver = 1.2; - -if ($Startup) { - - my @collection_files = ("$Pgm_Root/data/web/collections.json", - "$config_parms{data_dir}/web/collections.json", - "$config_parms{ia7_data_dir}/collections.json"); - for my $file (@collection_files) { - if (-e $file) { - print_log "[IA7_Collection_Updater] : Checking $file to current version $ia7_coll_current_ver"; - my $json_data; - my $file_data; - eval { - $file_data = file_read($file); - $json_data = decode_json($file_data); #HP, wrap this in eval to prevent MH crashes - }; - - if ($@) { - print_log "[IA7_Collection_Updater] : WARNING: decode_json failed for $file. Please check this file!"; - } else { - my $updated = 0; - - if ((! defined $json_data->{meta}->{version}) or ($json_data->{meta}->{version} < 1.2)) { #IA7 v1.2 required change - $json_data->{700}->{user} = '$Authorized' unless (defined $json_data->{700}->{user}); - my $found = 0; - foreach my $i (@{$json_data->{500}->{children}}) { - $found = 1 if ($i == 700); - } - push (@{$json_data->{500}->{children}},700) unless ($found); - $json_data->{meta}->{version} = "1.2"; - print_log "[IA7_Collection_Updater] : Checking $file to version $ia7_coll_current_ver"; - $updated = 1; - } - if ($updated) { - my $json_newdata = to_json($json_data, {utf8 => 1, pretty => 1}); - my $backup_file = $file . ".v" . $version . ".backup"; - file_write($backup_file,$file_data); - print_log "[IA7_Collection_Updater] : Saved backup " . $file . ".v" . $version . ".backup"; - file_write($file,$json_newdata); - } - } - } - } -} \ No newline at end of file diff --git a/code/common/ia7_notifications.pl b/code/common/ia7_notifications.pl deleted file mode 100644 index 1739e51bd..000000000 --- a/code/common/ia7_notifications.pl +++ /dev/null @@ -1,45 +0,0 @@ -# Category = IA7 - -#@ IA7 v1.1 : Enables speech notifications to browsers. -#@ also includes some sample code of how to use other notifications - -if ($Startup or $Reload) { - if (! defined $Info{IPAddress_local}) { - print_log "json_server.pl: \$Info{IPAddress_local} not defined. Json speech disabled"; - } else { - print_log "IA7 Speech Notifications enabled"; - &Speak_parms_add_hook(\&json_speech_enable); - } -} - -$v_ia7_test_sound = new Voice_Cmd ("Test playing a sound"); -$v_ia7_test_banner = new Voice_Cmd("Test [blue,green,yellow,red] Banner Notification"); - -if (my $said = said $v_ia7_test_banner) { - my %data; - $data{text} = "This is a test of the IA7 notification"; - $data{color} = $said; - &json_notification("banner",{%data}); -} - -if (said $v_ia7_test_sound) { - my %data; - $data{url} = "http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/misc/tellme_welcome.wav"; - &json_notification("sound",{%data}); -} - -sub json_speech_enable { - my ($parms) = @_; - push @{$parms->{web_hook}},\&file_ready_for_ia7; -} - -sub file_ready_for_ia7 { - my (%parms) = @_; - my %data; - $data{mode} = $parms{mode}; - $data{url} = "http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/" . $parms{web_file}; - $data{text} = $parms{raw_text}; - $data{client} = $parms{requestor}; - &json_notification("speech",{%data}); -} - diff --git a/data/web/collections.json b/data/web/collections.json index 2d0618eb1..7efe8a56c 100644 --- a/data/web/collections.json +++ b/data/web/collections.json @@ -395,7 +395,6 @@ "28" : { "link" : "/bin/set_parm_tv_provider.pl", "name" : "Setup TV Provider", - "mode" : "advanced", "icon" : "fa-desktop" }, "5" : { @@ -488,7 +487,6 @@ "26" : { "link" : "/ia5/house/irman.shtml", "name" : "Program IRMAN", - "mode" : "advanced", "icon" : "fa-rss" }, "57" : { @@ -709,7 +707,6 @@ "27" : { "icon" : "fa-wrench", "name" : "Header Control", - "mode" : "advanced", "link" : "/bin/headercontrol.pl" }, "78" : { @@ -720,7 +717,7 @@ "102" : { "name" : "Picture Frame", "icon" : "fa-desktop", - "external" : "$config_parms{html_dir}/misc/photos.shtml" + "external" : "http://home.krkeegan.com:8081/misc/photos.shtml" }, "105" : { "link" : "/ia5/calendar/main.shtml", @@ -775,12 +772,14 @@ "icon" : "fa-home", "name" : "Gear Settings", "children" : [ - 700 + 501 ] }, - "700" : { - "user" : "$Authorized" + "501" : { + "link" : "/UNSET_PASSWORD?user=admin", + "name" : "Authorize", + "icon" : "fa-lock" }, "600" : { "link" : "/ia7/#path=/objects&parents=ia7_status_items", diff --git a/data/web/ia7_config.json b/data/web/ia7_config.json index fec693a36..2fb7e729e 100644 --- a/data/web/ia7_config.json +++ b/data/web/ia7_config.json @@ -7,9 +7,7 @@ "fp_icon_size" : "32", "fp_state_popovers" : "yes", "substate_percentages" : "20", - "disable_current_state" : "yes", - "notifications" : "yes", - "speech_default" : "banner" + "disable_current_state" : "yes" }, "objects" : { "example_object" : { diff --git a/lib/ajax.pm b/lib/ajax.pm index 0d44b1ff0..dd7819c30 100644 --- a/lib/ajax.pm +++ b/lib/ajax.pm @@ -101,10 +101,7 @@ sub checkForUpdate { &main::print_log("checkForUpdate sub ${$$self{sub}} returned $xml") if $main::Debug{ajax}; &::print_socket_fork( ${ $$self{waitingSocket} }, $xml ); - &main::print_log( "Closing Socket " . ${ $$self{waitingSocket} } ) - if $main::Debug{ajax}; - ${ $$self{waitingSocket} }->shutdown(2) - ; #Changed this from close() to shutdown(2). In some cases, the parent port wasn't being closed -- ie. speech events + ${ $$self{waitingSocket} }->close; ${ $$self{changed} } = 1; } else { diff --git a/lib/http_server.pl b/lib/http_server.pl index 5f8100d55..8bb6ef8ea 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -9,8 +9,6 @@ use Text::ParseWords; require 'http_utils.pl'; -#use Data::Dumper; -#$main::Debug{http} = 4; #no warnings 'uninitialized'; # These seem to always show up. Dang, will not work with 5.0 use vars @@ -184,23 +182,13 @@ sub http_process_request { unless ($header) { # Ignore empty requests, like from 'check the http server' command - print "http: Error, not header request. header=[$temp]\n" + print "http: Error, not header request. header=$temp\n" if $main::Debug{http} and $temp; - my ( $type, $file ) = ( $temp =~ /^(\S*)\s(.*)\sHTTP/ ); - print "t=[$type] f=[$file]\n" if $main::Debug{http2}; - if ( $type eq "HEAD" ) { - print "yes\n"; - print $socket &mime_header( $file, 1, 4592100 ); - } return; } $Socket_Ports{http}{data_record} = $header; - print "http: Header = $header\n" if $main::Debug{http}; - #print Dumper %Http if $main::Debug{http}; - print "http: Range Header $Http{Range} encountered for $header" - if ( defined $Http{Range} ); $Http{loop} = $Loop_Count; # Track which pass we last processes a web request $Http{request} = $header; @@ -429,16 +417,13 @@ sub http_process_request { # Prompt for password (SET_PASSWORD) and allow for UNSET_PASSWORD if ( $get_req =~ /SET_PASSWORD$/ ) { - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - if ( $config_parms{password_menu} eq 'html' ) { - my $html = &html_authorized; if ( $get_req =~ /^\/UNSET_PASSWORD$/ ) { $Authorized = 0; $Cookie .= "Set-Cookie: password=xyz ; ; path=/;\n"; - $html .= ""; } - $html .= "
Refresh: Main Page\n" unless (lc $mode eq "ia7"); + my $html = &html_authorized; + $html .= "
Refresh: Main Page\n"; $html .= &html_password('') . '
'; print $socket &html_page( undef, $html, undef, undef, undef, undef ); @@ -446,7 +431,6 @@ sub http_process_request { else { my $html = &html_authorized; $html .= "
Refresh: Main Page\n"; - $html .= "
set_password else Referrer: $Http{Referer}\n"; # $html .= &html_reload_link('/', 'Refresh Main Page'); # Does not force reload? my ( $name, $name_short ) = &net_domain_name('http'); @@ -474,15 +458,14 @@ sub http_process_request { # Process the html password form elsif ( $get_req =~ /\/SET_PASSWORD_FORM$/ ) { - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); 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'); $html .= &html_password(''); if ($password_crypted) { @@ -510,9 +493,7 @@ sub http_process_request { # &speak("app=admin Password NOT set by $name_short"); } - print $socket &html_page( undef, $html ); - return; } @@ -798,23 +779,24 @@ sub http_process_request { sub html_password { my ($menu) = @_; $menu = $config_parms{password_menu} unless $menu; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); # return $html_unauthorized unless $Authorized; my $html; if ( $menu eq 'html' ) { - $html = qq[\n] unless (lc $mode eq "ia7"); + $html = + qq[\n]; - # $html .= qq[\n]; - $html .= qq[
\n]; + # $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]; -# } + # $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]; } else { $html = qq[HTTP/1.0 401 Unauthorized\n]; @@ -826,20 +808,13 @@ sub html_password { } sub html_authorized { - my $html = "Status: "; if ($Authorized) { - $html .= ""; - $html .= "Logged In as $Authorized"; - $html .= ""; - $html .= "
"; + return + "Status: Logged In as $Authorized
"; } else { - $html .= ""; - $html .= "Not Logged In"; - $html .= ""; - $html .= "
"; + return "Status: Not Logged In
"; } - return $html; } sub html_unauthorized { @@ -1107,7 +1082,7 @@ sub html_sub { } # Allow for &sub1 and &sub1(args) - if ( ( ( $sub_name, $sub_arg ) = $data =~ /^\&(\S+?)\((.*)\)$/ ) + if ( ( ( $sub_name, $sub_arg ) = $data =~ /\&([^\&]+?)\((.*)\)$/ ) or ( ($sub_name) = $data =~ /^\&(\S+)$/ ) ) { $sub_arg = '' unless defined $sub_arg; # Avoid uninit warninng @@ -1180,8 +1155,8 @@ sub html_response { # $leave_socket_open_action = "&speak_log_last(1)"; # Only show the last spoken text # $leave_socket_open_action = "&Voice_Text::last_spoken(1)"; # Only show the last spoken text } - elsif ( $h_response =~ /^http:\S+$/i or $h_response =~ /^reff?erer/i ) { - + elsif ( $h_response =~ /^https?:\S+$/i or $h_response =~ /^reff?erer/i ) + { # Allow to use just the base part of the referer # - some browsers (audrey) do not return full referer url :( # so allow for referer(url)... @@ -1578,10 +1553,7 @@ sub html_error_log { # These html_form functions are used by mh/web/bin/*.pl scrips sub html_form_input_set_func { my ( $func, $resp, $var1, $var2 ) = @_; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - my $id = ""; - #$id = "id='mhresponse'" if ($mode eq 'ia7'); - my $html .= qq|
\n|; + my $html .= qq|\n|; $html .= qq|\n|; $html .= qq|\n|; $html .= qq|\n|; @@ -1589,21 +1561,18 @@ sub html_form_input_set_func { $size = 10 if $size < 10; $size = 30 if $size > 30; $html .= qq|\n|; - $html .= qq|
\n|; + $html .= qq|\n|; return $html; } sub html_form_input_set_var { my ( $var, $resp, $default ) = @_; $default = HTML::Entities::encode($default); - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - my $id = ""; - #$id = "id='mhresponse'" if ($mode eq 'ia7'); - my $html .= qq|
\n|; + my $html .= qq|\n|; $html .= qq|\n|; $html .= qq|\n|; $html .= qq|\n|; - $html .= qq|
\n|; + $html .= qq|\n|; return $html; } @@ -1626,11 +1595,7 @@ sub html_form_select { sub html_form_select_set_func { my ( $func, $resp, $var1, $default, @values ) = @_; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - my $id = ""; - #$id = "id='mhresponse'" if ($mode eq 'ia7'); -# my $form .= qq|
\n|; - my $form .= qq|\n|; + my $form .= qq|\n|; $form .= qq|\n|; $form .= qq|\n|; $form .= qq|\n|; @@ -1643,20 +1608,16 @@ sub html_form_select_set_func { $form .= qq|\n|; } $form .= "
\n"; -# $form .= "\n"; return $form; } sub html_form_select_set_var { my ( $var, $default, @values ) = @_; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - my $id = ""; - #$id = "id='mhresponse'" if ($mode eq 'ia7'); - my $html = "
\n"; + my $html = "\n"; $html .= qq|\n|; $html .= qq|\n|; $html .= - &html_form_select( 'value', 1, $default, @values ) . "
\n"; + &html_form_select( 'value', 1, $default, @values ) . "\n"; return $html; } @@ -1666,10 +1627,8 @@ sub html_file { # Do not cach shtml files my ($cache) = ( $file =~ /\.shtm?l?$/ or $file =~ /\.vxml?$/ ) ? 0 : 1; - ($cache) = ( defined $Http{Range} ) ? 0 : 1; - # Return right away if the file has not changed, don't return a cache entry if there is - # a http Range header though... + # Return right away if the file has not changed #http: header key=If-Modified-Since value=Sat, 27 Mar 2004 02:49:29 GMT; length=1685. if ( $cache and $Http{'If-Modified-Since'} @@ -1678,7 +1637,7 @@ sub html_file { my $time2 = &str2time($1); my $time3 = ( stat($file) )[9]; print "db web file cache check: f=$file t=$time2/$time3\n" - if $main::Debug{http}; + if $main::Debug{http3}; if ( $time3 <= $time2 ) { return "HTTP/1.0 304 Not Modified\nServer: MisterHouse\n"; } @@ -1693,8 +1652,6 @@ sub html_file { return; } - print "http: processing file\n" if $main::Debug{http}; - # Allow for 'server side include' directives # if ( $file =~ /\.shtm?l?$/ @@ -1714,7 +1671,6 @@ sub html_file { # Note: These differ from classic .cgi in that they return # the results, rather than print them to stdout. elsif ( $file =~ /\.(pl|cgi)$/ ) { - print "Processing perl or CGI file: $file\n" if $main::Debug{http}; my $code = join( '', ); # Check if authorized @@ -1758,7 +1714,6 @@ sub html_file { # print "Http_server .pl file results:$html.\n" if $main::Debug{http}; } else { - print "http: Reading file: $file\n" if $main::Debug{http}; binmode HTML; # my $data = join '', ; @@ -1768,22 +1723,7 @@ sub html_file { local $/ = undef; $data = join( '', ); } - my $full_length = length $data; - - # yes, this is somewhat inefficient to read in the whole file only to then - # take a subset of the requested bytes. There are more effective means however - # since this is only in place to stream wav speak files to safari clients, it - # minimizes the overall change to http_server. - if ( defined $Http{Range} ) { - my ( $start, $end ) = $Http{Range} =~ /bytes=(\d*)-(\d*)/; - $end = ( length $data ) - $start if ( $end eq "" ); - print "http:start=$start end=$end\n" if ( $main::Debug{http} ); - my $tmpdata = substr $data, $start, $end; - $data = $tmpdata; - } - $html = - &mime_header( $file, 1, length $data, $Http{Range}, $full_length ) - unless $no_header; + $html = &mime_header( $file, 1, length $data ) unless $no_header; $html .= $data; } close HTML; @@ -1954,7 +1894,7 @@ sub html_cgi { } sub mime_header { - my ( $file_or_type, $cache, $length, $range, $full_length ) = @_; + my ( $file_or_type, $cache, $length ) = @_; # Allow for passing filename or filetype my ( $mime, $date ); @@ -1971,9 +1911,8 @@ sub mime_header { } # print "dbx2 m=$mime f=$file_or_type\n"; - my $code = "HTTP/1.0 200 OK"; - $code = "HTTP/1.1 206 Partial Content" if $range; - my $header = "$code\nServer: MisterHouse\nContent-Type: $mime\n"; + + my $header = "HTTP/1.0 200 OK\nServer: MisterHouse\nContent-type: $mime\n"; # $header .= ($cache) ? "Cache-Control: max-age=1000000\n" : "Cache-Control: no-cache\n"; if ($cache) { @@ -1986,25 +1925,6 @@ sub mime_header { # Allow for a length header, as this allows for faster 'persistant' connections $header .= "Content-Length: $length\n" if $length; - #(my $range_bytes) = $range =~ /bytes=(.*)/; - my ( $start, $end ) = $range =~ /bytes=(\d*)-(\d*)/; - $end = ($full_length) - $start - 1 if ( $end eq "" ); - - #$header .= "Content-Range: bytes " . $range_bytes . "/" . $full_length . "\n" if $range_bytes; - #print "http: Server responds: bytes " . $range_bytes . "/" . $full_length . "\n" if $range_bytes; - $header .= - "Content-Range: bytes " . $start . "-" . $end . "/" . $full_length . "\n" - if $range; - print "http: Server responds: HTTP/1.1 206; bytes " - . $start . "-" - . $end . "/" - . $full_length . "\n" - if $range; - - $header .= "Accept-Ranges: bytes\n"; - - print "returned header = $header\n" if ( $main::Debug{http} ); - return $header . "\n"; #Expires: Mon, 01 Jul 2002 08:00:00 GMT @@ -3184,16 +3104,10 @@ sub print_socket_fork { } } else { - my $keep_alive = 0; - $keep_alive = 1 - if ( ( defined $Http{Connection} ) - and ( $Http{Connection} eq "keep-alive" ) ); &print_socket_fork_unix( $socket, $html ); } } else { - print "http: printing with regular socket l=$length s=$socket\n" - if $main::Debug{http}; print $socket $html; } } diff --git a/lib/json_server.pl b/lib/json_server.pl index 8dc59fd71..1d5965fbd 100644 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -45,7 +45,6 @@ =head2 METHODS use JSON qw(decode_json); use IO::Compress::Gzip qw(gzip); use vars qw(%json_table); -my @json_notifications = (); #noloop sub json { my ( $request_type, $path_str, $arguments, $body ) = @_; @@ -171,9 +170,8 @@ sub json_get { eval { my $json_collections = file_read($collection_file); - $json_collections =~ s/\$config_parms\{(.+?)\}/$config_parms{$1}/gs; - $json_collections =~ s/\$Authorized/$Authorized/gs; # needed for including current "Authorize" status - $json_data{'collections'} = decode_json($json_collections); #HP, wrap this in eval to prevent MH crashes + $json_data{'collections'} = decode_json($json_collections) + ; #HP, wrap this in eval to prevent MH crashes }; if ($@) { print_log @@ -181,7 +179,7 @@ sub json_get { $json_data{'collections'} = decode_json('{ "0" : { "name" : "error" } }') ; #write a blank collection - config_checker($collection_file); + } } @@ -204,29 +202,7 @@ sub json_get { $json_data{'ia7_config'} = decode_json('{ "prefs" : { "status" : "error" } }') ; #write a blank collection - config_checker($prefs_file); - } - # Look at the client ip overrides, and replace any pref key with the client_ip specific item - if ( - defined $json_data{'ia7_config'}->{clients} - ->{ $Http{Client_address} } ) - { - print_log - "Json_Server.pl: Client override section for $Http{Client_address} found"; - for my $key ( - keys $json_data{'ia7_config'}->{clients} - ->{ $Http{Client_address} } ) - { - print_log - "Json_Server.pl: Client key=$key, value = $json_data{'ia7_config'}->{clients}->{$Http{Client_address}}->{$key}"; - print_log - "Json_Server.pl: Master value = $json_data{'ia7_config'}->{prefs}->{$key}"; - $json_data{'ia7_config'}->{prefs}->{$key} = - $json_data{'ia7_config'}->{clients} - ->{ $Http{Client_address} }->{$key}; - } - delete $json_data{'ia7_config'}->{clients}; } } @@ -260,11 +236,6 @@ sub json_get { my $default_cf = "AVERAGE"; $default_cf = $json_data{'rrd_config'}->{'prefs'}->{'default_cf'} if ( defined $json_data{'rrd_config'}->{'prefs'}->{'default_cf'} ); - my $default_timestamp = "true"; - $default_timestamp = - $json_data{'rrd_config'}->{'prefs'}->{'get_last_update'} - if ( - defined $json_data{'rrd_config'}->{'prefs'}->{'get_last_update'} ); my @dss = (); my @defs = (); @@ -275,7 +246,6 @@ sub json_get { my @type = (); my $celsius = 0; my $arg_time = 0; - my $xml_info; $arg_time = int( $args{time}[0] ) if ( defined int( $args{time}[0] ) ); $celsius = 1 if ( $config_parms{weather_uom_temp} eq 'C' ); $celsius = 1 @@ -360,10 +330,13 @@ sub json_get { } push @defs, @xports; + + #print "defs=" . join (',',@defs) . "\n"; + #print "start=$start end=$end\n"; + my $rrd = RRDTool::Rawish->new( rrdfile => "$path/$rrd_file" ); my $xml = $rrd->xport( [@defs], { '--start' => $start, '--end' => $end, } ); - $xml_info = $rrd->info if ( $default_timestamp eq "true" ); my @lines = split /\n/, $xml; foreach my $line (@lines) { @@ -380,23 +353,40 @@ sub json_get { } my ($time) = $line =~ /\\(\d*)\<\/t\>/; $time = $time * 1000; #javascript is in milliseconds + + #print "time=$time, $arg_time\n"; next if ( $arg_time > int($time) ); #only return new items my (@values) = $line =~ /\(-?[e.+-\d]*|NaN)\<\/v\>/g; if ($time) { + + #print "line=$line\n"; + #print "[$time"; my $index = 0; foreach my $value (@values) { my $value1 = sprintf( "%.10g", $value ); + + #print "index=$index,value=$value,value1=$value1,"; $value1 = ( $value1 - 32 ) * ( 5 / 9 ) if ( ($celsius) and ( lc $type[$index] eq "temperature" ) ); $value1 = sprintf( "%." . $round[$index] . "f", $value1 ) if ( defined $round[$index] ); + + #print "value1=$value1"; $value1 =~ s/\.0*$// unless ( $value1 == 0 ) ; #remove unneccessary trailing decimals $value1 = "null" if ( lc $value1 eq "nan" ); + + #print "value1=$value1\n"; + #my @array = (); + #$array[0] = $time; + #$array[1] = $value1; + ##push @{$dataset[$index]->{data}},"$time,$value1"; ###Need to get a 2d array here####*** push @{ $dataset[$index]->{data} }, [ $time, $value1 ]; + + #push @{$dataset[$index]->{data}},@array; $index++; } } @@ -407,9 +397,11 @@ sub json_get { if ( defined $json_data{'rrd_config'}->{'options'} ); $data{'periods'} = $json_data{'rrd_config'}->{'periods'} if ( defined $json_data{'rrd_config'}->{'periods'} ); - $data{'last_update'} = $xml_info->{'last_update'} * 1000 - if ( defined $xml_info->{'last_update'} ); + + #$json_data{'rrd'} = \@dataset; $json_data{'rrd'} = \%data; + + #print Dumper %data; } # List objects @@ -541,31 +533,12 @@ sub json_get { $json_data{vars} = \%json_vars; } - if ( $path[0] eq 'notifications' ) { - - for my $i ( 0 .. $#json_notifications ) { - my $n_time = int( $json_notifications[$i]{time} ); - my $x = $args{time}[0] - ; #Weird, does nothing, but notifications doesn't work if removed... - if ( ($n_time) - and - ( ( defined $args{time} && int( $args{time}[0] ) < $n_time ) ) ) - { - push( - @{ $json_data{'notifications'} }, - $json_notifications[$i] - ); - } - else { - #if older than X minutes, then remove the array values to keep things tidy - } - } - } - if ( $path[0] eq 'table_data' ) { if ( $args{var} ) { my $length = $#{ $json_table{ $args{var}[0] }->{data} } + 1; + #print "json_db: length = $length start=" . $args{start}[0] . " records=" . $args{records}[0] . "\n"; + #need to check if vars and keys exist my $start = 0; @@ -576,13 +549,30 @@ sub json_get { if ( defined $json_table{ $args{var}[0] }{page} ); $records = $args{records}[0] if ( $args{records}[0] ); - # TODO: At some point have a hook that pulls in more data into the table if it's missing - # ie read a file - + # if ($length < ($start + $records)) { + # print "db: will have to request data, $length, $start, $records\n"; + # print "&" . $json_table{$args{var}[0]}{hook} . "($start,$records)\n"; + # my $hook = $json_table{$args{var}[0]}{hook} . "($start,$records)"; + # + # #eval (&get_inbound_data($start,$records)); + # eval ("&$hook"); + # if ($@) { + # print_log "Json_Server.pl: WARNING: fetch data failed for " . $args{var}[0] . " " . $json_table{$args{var}[0]}{hook} . "!"; + # } else { + # $page++ if (scalar @{$json_table{$args{var}[0]}->{data}} > $json_table{$args{var}[0]}{page_size}); + # } + # #$json_table{$args{var}[0]}{page} = $page; + # } + #if requesting data beyond what's available, fetch it. + #test bad table + #remove data from hash my $jt_time = int( $json_table{ $args{var}[0] }{time} ); + + #print "arg time = " . int($args{time}[0]) . " table time = " .$jt_time . "\n"; if ( ( $args{time} && int( $args{time}[0] ) < $jt_time ) or ( !$args{time} ) ) { + #$json_data->{'table_data'} = $json_table{$args{var}[0]}; #need to copy all the data since we can adjust starts and records $json_data{'table_data'}{exist} = @@ -603,6 +593,8 @@ sub json_get { if ( $args{records}[0] ); $json_data{'table_data'}{records} = scalar @{ $json_data{'table_data'}->{data} }; + + #print "db=$json_data{'table_data'}{records}\n"; } } } @@ -626,14 +618,6 @@ sub json_get { } } - if ( $path[0] eq 'fp_icon_sets' ){ - my $p = "../web/ia7/graphics/*default_".$args{px}[0].".png"; - my @icons = glob($p); - s/^..\/web// for @icons; - $json_data{'icon_sets'} = []; - push( @{ $json_data{'fp_icon_sets'} }, @icons); - } - # List speak phrases if ( $path[0] eq 'print_speaklog' || $path[0] eq '' ) { my ( @log, @tmp ); @@ -645,6 +629,7 @@ sub json_get { @log = ::print_speaklog_since( $args{time}[0] ); push @log, '' ; #TODO HP - Kludge, the javascript seems to want an extra line in the array for some reason + #print "db/json: " . join(", ",@log) . "\n"; } elsif ( !$args{time} ) { @log = ::print_speaklog_since(); @@ -672,16 +657,20 @@ sub json_get { # Insert Data or Error Message if ($output_ref) { $json{data} = $output_ref; + + # foreach my $key (sort (keys(%{$output_ref}))) { + # print "db:key = $key\n"; + # $json{data}{$key} = $output_ref->{$key}; + # } } else { $json{error}{msg} = 'No data, or path does not exist.'; } #Insert Meta Data fields - $json{meta}{time} = $output_time; - $json{meta}{path} = \@path; - $json{meta}{args} = \%args; - $json{meta}{client_ip} = $Http{Client_address}; + $json{meta}{time} = $output_time; + $json{meta}{path} = \@path; + $json{meta}{args} = \%args; my $json_raw = JSON->new->allow_nonref; @@ -1032,8 +1021,6 @@ sub filter_object { sub json_page { my ($json_raw) = @_; my $json; - - #utf8::encode( $json_raw ); #may need to wrap gzip in an eval and encode it if errors develop. It crashes if a < is in the text gzip \$json_raw => \$json; my $output = "HTTP/1.0 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; @@ -1266,104 +1253,6 @@ sub json_table_purge_data { my ( $key, $posx, $records ) = @_; } -sub json_notification { - my ( $type, $data ) = @_; - $data->{type} = $type; - $data->{time} = &::get_tickcount; - for my $i ( 0 .. $#json_notifications ) { - - #clean up any old notifications, or empty entries (ie less than 5 seconds old) - my $n_time = int( $json_notifications[$i]{time} ); - if ( ( &get_tickcount > $n_time + 5000 ) - or ( !defined $json_notifications[$i]{time} ) ) - { - splice @json_notifications, $i, 1; - } - } - push @json_notifications, $data; -} - -sub config_checker { - my ($file) = @_; - - my (%collections, $key, $output, $temp); - my @data = file_read($file); - - - foreach my $row (@data) { - $key = $1 if $row =~ /\"(\d+?)\" \:/; - $row =~ /\"(.+?)\" \: \"(.+?)\"/ ; - $collections{$key}{$1} = $2 if $1; - } - - foreach my $row (@data) { - $key = $1 if $row =~ /\"(\d+?)\" \:/; - if ($row =~ /(\d+?)(\,|\n)/) { - my $sub_key = $1; - my $comma = $2; - $collections{$key}{children} .= "\t$1: " ; - $collections{$key}{children} .= "$collections{$sub_key}{name}"; - $collections{$key}{children} .= "\n"; - } - } - - - #foreach $key (sort check_numerically keys %collections) { - # $output .= "$key: $collections{$key}{name}:$collections{$key}{link}$collections{$key}{external}$collections{$key}{iframe}:$collections{$key}{comment}:$collections{$key}{mode}:$collections{$key}{children}"; - #} - - my ($row, $show_row, $bracket_errors, $comma_errors1, $comma_errors2, %brackets, $curly, $square); - $curly = 'closed'; - $square = 'closed'; - - while ($row < @data) { - $show_row = $row + 1; - $data[$row] =~ s/\s+$//; # remove trailing spaces - if ($data[$row] =~ /\{/ and $data[$row] !~ /iframe|external/) { - $brackets{open_curly} ++; - $bracket_errors .= "Repeated open curly bracket in line $show_row: '$data[$row]'\n" if $curly and $curly eq 'open'; - $curly = 'open'; - } - if ($data[$row] =~ /\}/ and $data[$row] !~ /iframe|external/) { - $brackets{close_curly} ++; - $bracket_errors .= "Repeated close curly bracket in line $show_row: '$data[$row]'\n" if $curly eq 'closed'; - $curly = 'closed'; - } - if ($data[$row] =~ /\[/) { - $brackets{open_square} ++; - $bracket_errors .= "Repeated open square bracket in line $show_row: '$data[$row]'\n" if $square eq 'open'; - $square = 'open'; - } - if ($data[$row] =~ /\]/) { - $brackets{close_square} ++; - $bracket_errors .= "Repeated close square bracket in line $show_row: '$data[$row]'\n" if $square eq 'closed'; - $square = 'closed'; - } - - $comma_errors1 .= $row + 1 . ": '$data[$row]'\n" if $data[$row] !~ /\, *$/ - and $data[$row + 1] !~ /(\}|\])/ - and $data[$row] !~ /\: (\{|\[)/ - and $data[$row] !~ /^\{/ - and $data[$row] !~ /\}$/; - - $comma_errors2 .= $row + 1 . ": '$data[$row]'\n" if $data[$row] =~ /\,/ and $data[$row + 1] =~ /(\}|\])\,/; - - $row ++; - } - - $output .= "Possible bracket errors:\n$bracket_errors\n" if $bracket_errors; - $output .= "The following lines should possibly have a comma at the end:\n$comma_errors1\n" if $comma_errors1; - $output .= "The following lines should possibly not have a comma at the end:\n$comma_errors2\n" if $comma_errors2; - - $output .= "There are $brackets{open_square} '[' and $brackets{close_square} ']'.\n"; - $output .= "There are $brackets{open_curly} '{' and $brackets{close_curly} '}'.\n"; - - print_log $output; -} - -sub check_numerically { $a <=> $b } - - return 1; # Make require happy =back diff --git a/web/bin/code_select.pl b/web/bin/code_select.pl index aa375efda..de47f06fa 100644 --- a/web/bin/code_select.pl +++ b/web/bin/code_select.pl @@ -38,12 +38,14 @@ sub select_code_form { The following are standardized MisterHouse code files which need no modifications, but which may require settings in your ini file to activate properly.|; - $html .= qq| Simply check those that you'd like to run and they'll be automatically activated within MisterHouse.| if $Authorized eq 'admin'; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - - $html .= qq|
Read-Only: Login as admin to edit| unless $Authorized eq 'admin'; + $html .= qq| Simply check those that you'd like to run and +they'll be automatically activated within MisterHouse.| + if $Authorized eq 'admin'; + $html .= + qq|
Read-Only: Login as admin to edit| + unless $Authorized eq 'admin'; $html .= qq| -
+
\n"; + $HTTP = $HTTP . "\n"; } } $HTTP = $HTTP . "
Search (file or description):   |; diff --git a/web/bin/code_unselect.pl b/web/bin/code_unselect.pl index 1269e5253..805ffa244 100644 --- a/web/bin/code_unselect.pl +++ b/web/bin/code_unselect.pl @@ -42,7 +42,7 @@ sub select_code_form { $html .= qq|Simply uncheck files you want to disable or check to re-enable.| if $Authorized eq 'admin'; $html .= qq| -
+
Search (file or description):   |; diff --git a/web/bin/iniedit.pl b/web/bin/iniedit.pl index 33ecb1591..cce5a3e8b 100644 --- a/web/bin/iniedit.pl +++ b/web/bin/iniedit.pl @@ -175,7 +175,7 @@ sub edit { Note: Commit will resort and filter out comments.' if $Authorized eq 'admin'; $data .= ' - + '; $data .= ' @@ -264,7 +264,7 @@ sub edit { sub edit_list { my $data = ' - + '; $data .= ' diff --git a/web/bin/items.pl b/web/bin/items.pl index 630288ba6..f93a91951 100644 --- a/web/bin/items.pl +++ b/web/bin/items.pl @@ -64,7 +64,7 @@ sub web_items_list { # Create a form to pick which file $html .= - "
+ $html_calls .= " diff --git a/web/bin/phone_out2.pl b/web/bin/phone_out2.pl index dec378154..6d0c3e813 100644 --- a/web/bin/phone_out2.pl +++ b/web/bin/phone_out2.pl @@ -10,8 +10,7 @@ for my $r (@calls) { my ( $time, $num, $name, $line, $type, $dur, $ext, $color, $coloroff ); $coloroff = ""; - ( $time, $num, $name, $line, $type ) = $r =~ - /date=(.+\d+:\d+:\d+) number=(\S+) +name=(.*?) line=(\S*) type=(\S*)/; + ( $time, $num, $name, $line, $type ) = $r =~ /date=(.+\d+:\d+:\d+) number=(\S+) +name=(.*?) line=(\S*) type=(\S*)/; ( $dur, $ext ) = $r =~ / dur=(\S*) ext=(\S*)/; $name = '' unless $name; next unless $num; @@ -33,8 +32,7 @@ # $color = "" if ( $name ne 'NA' ) ; - $html_calls .= - " + $html_calls .= " diff --git a/web/bin/phone_search.pl b/web/bin/phone_search.pl index 139fc2d66..1556f7820 100644 --- a/web/bin/phone_search.pl +++ b/web/bin/phone_search.pl @@ -23,8 +23,7 @@ $results =~ s/[\r\f]//g; $results =~ s/^results:(.+) matched (.+)\n/$1 matched "$2".<\/font><\/td><\/tr>\n"; + $html .= "\n"; $html .= "\n"; return $html; @@ -371,21 +329,17 @@ sub track_edit { $selected{$_}++; } } - my $html = - &html_header("Misterhouse mp3Rip Step 3/$numsteps: Finalize Naming"); - my ( $error, $cddbid, $track_numbers, $track_lengths, $total_seconds ) = - &verify_cdinfo( 'track_edit', $html ); + my $html = &html_header("Misterhouse mp3Rip Step 3/$numsteps: Finalize Naming"); + my ( $error, $cddbid, $track_numbers, $track_lengths, $total_seconds ) = &verify_cdinfo( 'track_edit', $html ); if ($error) { - return &html_page( - "Misterhouse mp3Rip Step 3/$numsteps: Finalize Naming", $error ); + return &html_page( "Misterhouse mp3Rip Step 3/$numsteps: Finalize Naming", $error ); } my %combined; my $entrycount = -1; $max_track_count = $#{@$track_numbers} + 1; foreach my $disc ( &mp3Rip_get_cddb_discs() ) { if ( $selected{ $disc->[1] } ) { - my ( $genre, $artist, $album, @tracks ) = - &mp3Rip_get_disc_details($disc); + my ( $genre, $artist, $album, @tracks ) = &mp3Rip_get_disc_details($disc); my $trackcount = 0; $entrycount++; $combined{'genre'}->[$entrycount] = $genre; @@ -397,8 +351,7 @@ sub track_edit { } } } - $html .= - "

For each row, type in or modify the value in the text box or make a selection from the list, if applicable.\n"; + $html .= "

For each row, type in or modify the value in the text box or make a selection from the list, if applicable.\n"; $html .= "

Disc Info\n"; $html .= "\n"; - $html .= - "

Which .mht file to edit?\n"; + "\n"; @@ -99,17 +99,17 @@ sub web_items_list { #form action='/bin/items.pl?add' method=post> $html .= qq| - + + + + + + | if $Authorized eq 'admin'; # Parse table data @@ -122,7 +122,7 @@ sub web_items_list { # Do not list comments unless ( $record =~ /^\s*\#/ or $record =~ /^\s*$/ - or $record =~ /^Format *=/i ) + or $record =~ /^Format *=/ ) { $record =~ s/#.*//; # Ignore comments $record =~ s/,? *$//; @@ -179,7 +179,6 @@ sub web_items_list { INSTEON_TRIGGERLINC => [qw(Address Name Groups)], INSTEON_ICONTROLLER => [qw(Address Name Groups)], SCENE_MEMBER => [qw(MemberName LinkName OnLevel RampRate)], - CODE => [qw(Code)], default => [qw(Address Name Groups Other)] ); @@ -188,12 +187,11 @@ sub web_items_list { my @headers = ( $headers{$type} ) ? @{ $headers{$type} } : @{ $headers{default} }; - my $headers = 2 + @headers; + my $headers = 1 + @headers; - $html .= "
Which .mht file to edit?\n"; $html .= &html_form_select( 'file', 1, $web_item_file_name, @file_paths ) . "
+
$form_type - - - - - -
\n"; logit( $logi, $index, 0 ); } @@ -373,27 +349,23 @@ sub check_accounts { # If new mail is age based, need to reset latest.html every time unlink("$config_parms{data_dir}/email/latest.html") if $config_parms{net_mail_scan_age}; - logit( "$config_parms{data_dir}/email/latest.html", $msg_latest, - 0 ); + logit( "$config_parms{data_dir}/email/latest.html", $msg_latest, 0 ); } # $msgcnt_flag .= sprintf("%3d", ($config_parms{net_mail_scan_age})?$msg_inbox_total:$msgcnt); # gmail can have thousands of messages, instead, just add a space... - $message_counts .= " " - . ( $config_parms{net_mail_scan_age} ? $msg_inbox_total : $msgcnt ); + $message_counts .= " " . ( $config_parms{net_mail_scan_age} ? $msg_inbox_total : $msgcnt ); my $temp = &make_name_list( $account, @{ $email_prev{$account} } ); $results_unread .= $temp; # Create html version with a link - $temp =~ - s|^(.+) has |   $1 has |; + $temp =~ s|^(.+) has |   $1 has |; $results_unread2 .= $temp . "
" if $temp; # Save parsed data, so we don't have to re-read next time - $email_file_data .= - ( join( $;, $account, @{ $email_prev{$account} } ) ) . "\n" + $email_file_data .= ( join( $;, $account, @{ $email_prev{$account} } ) ) . "\n" if $email_prev{$account} and @{ $email_prev{$account} }; } @@ -420,8 +392,7 @@ sub make_name_list { my $account2 = $account; $account2 =~ tr/_/ /; # Make speakable - @list = grep !/^filtered/i, - @list; # Delete blank names (filtered out from the above get_email_rule) + @list = grep !/^filtered/i, @list; # Delete blank names (filtered out from the above get_email_rule) my $cnt = @list; return unless $cnt; @@ -434,11 +405,7 @@ sub make_name_list { # $_ =~ s/\@/ At /g ; # ...change \@ to the word "At" # } - return ("$account2 has " - . plural( $cnt, 'new message' ) - . " from " - . &speakify_list(@list) - . ".\n" ); + return ( "$account2 has " . plural( $cnt, 'new message' ) . " from " . &speakify_list(@list) . ".\n" ); # return ("Email account $account2 has " . plural($cnt, 'new email message') . diff --git a/bin/get_email_rule_example.pl b/bin/get_email_rule_example.pl index f5e2045a4..f25f81cc3 100755 --- a/bin/get_email_rule_example.pl +++ b/bin/get_email_rule_example.pl @@ -8,8 +8,7 @@ sub get_email_rule { $from = 'The perl guys' if $to =~ /Perl-Win32-Users/; $from = 'The phone guys' if $to =~ /ktx/ or $subject =~ /kx-t/i; $from = 'junk mail' - if $from =~ /\S+[0-9]{3,}/ - ; # If we get a joe#### type address, assume it is junk mail. + if $from =~ /\S+[0-9]{3,}/; # If we get a joe#### type address, assume it is junk mail. return if $from =~ /X10 Newsletter/; # These are not needed if using the MS TTS engine (it pronounces fred@placed.com ok) diff --git a/bin/get_mp3_data b/bin/get_mp3_data index 8891f9f24..8d3e98609 100755 --- a/bin/get_mp3_data +++ b/bin/get_mp3_data @@ -7,8 +7,7 @@ use IO::File; my ( $Pgm_Path, $Pgm_Name, $Version ); BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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; } @@ -49,8 +48,7 @@ for my $dir (@ARGV) { print "\nTraversing dir $dir\n"; &read_mp3_dir($dir); } -print - "\nRead $counts{dir} directories and $counts{file} files.\n - Found $counts{tag} files with TAG data\n"; +print "\nRead $counts{dir} directories and $counts{file} files.\n - Found $counts{tag} files with TAG data\n"; my @tags = qw(title artist album year comment genre file); for ( 0 .. 6 ) { printf " - Found %4d files with a $tags[$_] tag\n", $counts{$_} @@ -130,8 +128,7 @@ sub GetOggInfo ($) { #Find out if the Ogg Stream uses Vorbis version 0 encoding. return - if unpack( 'H22', OggRead( $fh, \$temp, 11 ) ) ne - '01766f7262697300000000'; + if unpack( 'H22', OggRead( $fh, \$temp, 11 ) ) ne '01766f7262697300000000'; #Skip the next 18 bytes of the identification header. $buf = OggRead( $fh, \$temp, 18 ); @@ -183,80 +180,31 @@ sub read_mp3_dir { close MP3DIR; my @mp3_genres = ( - 'Blues', 'Classic Rock', - 'Country', 'Dance', - 'Disco', 'Funk', - 'Grunge', 'Hip-Hop', - 'Jazz', 'Metal', - 'New Age', 'Oldies', - 'Other', 'Pop', - 'R&B', 'Rap', - 'Reggae', 'Rock', - 'Techno', 'Industrial', - 'Alternative', 'Ska', - 'Death Metal', 'Pranks', - 'Soundtrack', 'Euro-Techno', - 'Ambient', 'Trip-Hop', - 'Vocal', 'Jazz+Funk', - 'Fusion', 'Trance', - 'Classical', 'Instrumental', - 'Acid', 'House', - 'Game', 'Sound Clip', - 'Gospel', 'Noise', - 'Alt. Rock', 'Bass', - 'Soul', 'Punk', - 'Space', 'Meditative', - 'Instrumental Pop', 'Instrumental Rock', - 'Ethnic', 'Gothic', - 'Darkwave', 'Techno-Industrial', - 'Electronic', 'Pop-Folk', - 'Eurodance', 'Dream', - 'Southern Rock', 'Comedy', - 'Cult', 'Gangsta Rap', - 'Top 40', 'Christian Rap', - 'Pop/Funk', 'Jungle', - 'Native American', 'Cabaret', - 'New Wave', 'Psychedelic', - 'Rave', 'Showtunes', - 'Trailer', 'Lo-Fi', - 'Tribal', 'Acid Punk', - 'Acid Jazz', 'Polka', - 'Retro', 'Musical', - 'Rock & Roll', 'Hard Rock', - 'Folk', 'Folk/Rock', - 'National Folk', 'Swing', - 'Fast-Fusion', 'Bebob', - 'Latin', 'Revival', - 'Celtic', 'Bluegrass', - 'Avantgarde', 'Gothic Rock', - 'Progressive Rock', 'Psychedelic Rock', - 'Symphonic Rock', 'Slow Rock', - 'Big Band', 'Chorus', - 'Easy Listening', 'Acoustic', - 'Humour', 'Speech', - 'Chanson', 'Opera', - 'Chamber Music', 'Sonata', - 'Symphony', 'Booty Bass', - 'Primus', 'Porn Groove', - 'Satire', 'Slow Jam', - 'Club', 'Tango', - 'Samba', 'Folklore', - 'Ballad', 'Power Ballad', - 'Rhythmic Soul', 'Freestyle', - 'Duet', 'Punk Rock', - 'Drum Solo', 'A Cappella', - 'Euro-House', 'Dance Hall', - 'Goa', 'Drum & Bass', - 'Club-House', 'Hardcore', - 'Terror', 'Indie', - 'BritPop', 'Negerpunk', - 'Polsk Punk', 'Beat', - 'Christian Gangsta Rap', 'Heavy Metal', - 'Black Metal', 'Crossover', - 'Contemporary Christian', 'Christian Rock', - 'Merengue', 'Salsa', - 'Thrash Metal', 'Anime', - 'JPop', 'Synthpop' + 'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', + 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', + 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', + 'Techno', 'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks', + 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', 'Jazz+Funk', + 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', + 'Game', 'Sound Clip', 'Gospel', 'Noise', 'Alt. Rock', 'Bass', + 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', + 'Ethnic', 'Gothic', 'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk', + 'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta Rap', + 'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', 'Cabaret', + 'New Wave', 'Psychedelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', + 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', + 'Rock & Roll', 'Hard Rock', 'Folk', 'Folk/Rock', 'National Folk', 'Swing', + 'Fast-Fusion', 'Bebob', 'Latin', 'Revival', 'Celtic', 'Bluegrass', + 'Avantgarde', 'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', + 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', + 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', + 'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', + 'Samba', 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle', + 'Duet', 'Punk Rock', 'Drum Solo', 'A Cappella', 'Euro-House', 'Dance Hall', + 'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', 'Terror', 'Indie', + 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', 'Heavy Metal', + 'Black Metal', 'Crossover', 'Contemporary Christian', 'Christian Rock', 'Merengue', 'Salsa', + 'Thrash Metal', 'Anime', 'JPop', 'Synthpop' ); for my $file ( sort @files ) { @@ -328,7 +276,7 @@ sub read_mp3_dir { if defined $tag_data[$i]; } - $tag_data[$i] =~ tr/\x20-\x7e//cd; # Drop non-ascii characters + $tag_data[$i] =~ tr/\x20-\x7e//cd; # Drop non-ascii characters $tag_data[$i] =~ s/^\s+//; $tag_data[$i] =~ s/\s+$//; if ( $i == 1 ) { diff --git a/bin/get_tv_grid b/bin/get_tv_grid index 1d1922359..1bfcdbd0d 100755 --- a/bin/get_tv_grid +++ b/bin/get_tv_grid @@ -42,36 +42,32 @@ use strict; my ( $Pgm_Path, $Pgm_Name, $Version ); BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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 } require "RedirAgent.pm"; #require "SMSAgent.pm"; -require 'handy_utilities.pl'; # For read_mh_opts -require 'handy_net_utilities.pl' - ; # For net_mail_send <-- causes probs in perl 5.6.1 with -w flag! +require 'handy_utilities.pl'; # For read_mh_opts +require 'handy_net_utilities.pl'; # For net_mail_send <-- causes probs in perl 5.6.1 with -w flag! use Getopt::Long; use LWP::Simple; use LWP::UserAgent; use HTTP::Cookies; use HTTP::Request::Common; -use vars '%config_parms'; # Not a my, as called from handy_net_utils +use vars '%config_parms'; # Not a my, as called from handy_net_utils #====================================================================== # Variable declarations and other init #====================================================================== # Globals # -my ( %parms, $didLogIn, $infile, $outfile, %channels_skip, %channels_keep, - $channel_data ); -my ( $url, $ua, $cookie_jar, $req_get1, $req_get2, $req_post, $logged_in ); +my ( %parms, $didLogIn, $infile, $outfile, %channels_skip, %channels_keep, $channel_data ); +my ( $url, $ua, $cookie_jar, $req_get1, $req_get2, $req_post, $logged_in ); my ( @hours, %providers, @uas ); my ( $dbm_file, $dbm_file2 ); my ( %DBM, %DBM2 ); @@ -96,22 +92,10 @@ my $dbgSubmitRequest = 1; # Set to 0 to turn off URL requests #====================================================================== if ( !&GetOptions( - \%parms, "h", - "help", "infile=s", - "outfile=s", "outdir=s", - "reget", "redo", - "db=s", "name=s", - "preserveRaw", "keep=s", - "skip=s", "channel_max=s", - "channel_min=s", "zip:s", - "debug", "label=s", - "keep_old", "mail_to=s", - "provider:s", "provider_name:s", - "get_providers", "mail_server=s", - "mail_baseref=s", "purge=s", - "mail_baseref=s", "include_footer", - "days=s", "day=s", - "hour=s", "tableChannels=s", + \%parms, "h", "help", "infile=s", "outfile=s", "outdir=s", "reget", "redo", + "db=s", "name=s", "preserveRaw", "keep=s", "skip=s", "channel_max=s", "channel_min=s", "zip:s", + "debug", "label=s", "keep_old", "mail_to=s", "provider:s", "provider_name:s", "get_providers", "mail_server=s", + "mail_baseref=s", "purge=s", "mail_baseref=s", "include_footer", "days=s", "day=s", "hour=s", "tableChannels=s", "timebars=s" ) or @ARGV @@ -213,8 +197,7 @@ sub days_from_now { my ( $day_time, $days ) = @_; my $day_time2 = $day_time + $days * 60 * 60 * 24; my ( $day, $month, $year, $down ) = ( localtime($day_time2) )[ 3, 4, 5, 6 ]; - my $dow = - (qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday))[$down]; + my $dow = (qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday))[$down]; $month++; $year += 1900; @@ -250,7 +233,7 @@ sub setup { $parms{tableChannels} = 40 unless $parms{tableChannels}; $parms{days} = 1 unless $parms{days}; $parms{db} = 'tv' unless $parms{db}; - $parms{outdir} = "$config_parms{html_dir}/$parms{db}" + $parms{outdir} = "$config_parms{html_dir}/$parms{db}" unless $parms{outdir}; $parms{zip} = $config_parms{zip_code} unless $parms{zip}; $parms{proxy} = $ENV{http_proxy} unless $parms{proxy}; @@ -261,9 +244,9 @@ sub setup { unless $parms{provider}; $parms{provider_name} = $config_parms{ $parms{db} . '_provider_name' } unless $parms{provider_name}; - $parms{get_providers} = 0 unless ( $parms{get_providers} ); - $parms{hour} = $config_parms{ $parms{db} . '_hours' } unless $parms{hour}; - $parms{skip} = $config_parms{ $parms{db} . '_channels_skip' } + $parms{get_providers} = 0 unless ( $parms{get_providers} ); + $parms{hour} = $config_parms{ $parms{db} . '_hours' } unless $parms{hour}; + $parms{skip} = $config_parms{ $parms{db} . '_channels_skip' } if ( $config_parms{ $parms{db} . '_channels_skip' } and !$parms{skip} ); $parms{keep} = $config_parms{ $parms{db} . '_channels_keep' } if ( $config_parms{ $parms{db} . '_channels_keep' } and !$parms{keep} ); @@ -307,8 +290,8 @@ sub setup { || die "Missing provider!"; $parms{channel_min} = '1' unless $parms{channel_min}; $parms{channel_max} = '99999' unless $parms{channel_max}; - $parms{label} = "VCR" unless $parms{label}; # This can also be an image link - $parms{days} = 1 unless $parms{days}; + $parms{label} = "VCR" unless $parms{label}; # This can also be an image link + $parms{days} = 1 unless $parms{days}; $parms{redo} = 1 if $parms{reget}; $parms{reget} = 1 if $parms{get_providers}; $parms{duration} = 6 unless $parms{duration}; @@ -409,9 +392,8 @@ sub submitRequest { my $res; # Reponse $request->content_type('application/x-www-form-urlencoded'); $request->header( - 'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0)', - 'Referer' => - ( $referer ? $referer : 'http://tvlistings.zap2it.com/grid.asp' ), + 'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0)', + 'Referer' => ( $referer ? $referer : 'http://tvlistings.zap2it.com/grid.asp' ), 'Accept' => '*/*', 'Accept-Language' => 'en', ); @@ -423,9 +405,7 @@ sub submitRequest { } # Submit request - print "Request: [" - . ( $request->as_string ) - . ( $cookie_jar->as_string ) . "]\n" + print "Request: [" . ( $request->as_string ) . ( $cookie_jar->as_string ) . "]\n" if ( $parms{debug} ); if ($outFile) { print "Submitting request with output file: $outFile \n" @@ -452,9 +432,7 @@ sub submitRequest { or die "missing location: ", $res->as_string; print "is redirect to $ur\n" if ( $parms{debug} ); $request = HTTP::Request->new( GET => $url . $ur ); - print "Request: [" - . ( $request->as_string ) - . ( $cookie_jar->as_string ) . "]\n" + print "Request: [" . ( $request->as_string ) . ( $cookie_jar->as_string ) . "]\n" if ( $parms{debug} ); $res = $ua->request( $request, $outFile ); return 1; @@ -506,17 +484,11 @@ sub login { # Second submission, to submit our zip code #------------------------------------------------------------ - my $req_zip = - HTTP::Request->new( - POST => $url . "zipcode.asp?partner_id=national&zipcode=$parms{zip}" ); - $req_zip->content( "zipcode=$parms{zip}&" - . "partner_id=national&" - . "FormName=zipcode.asp&" - . "submit1=Continue" ); + my $req_zip = HTTP::Request->new( POST => $url . "zipcode.asp?partner_id=national&zipcode=$parms{zip}" ); + $req_zip->content( "zipcode=$parms{zip}&" . "partner_id=national&" . "FormName=zipcode.asp&" . "submit1=Continue" ); $req_zip->content_type('application/x-www-form-urlencoded'); print "2 of 4\n" if $parms{debug}; - $submitResult = - submitRequest( $req_zip, "$config_parms{data_dir}/tv_providers.html" ); + $submitResult = submitRequest( $req_zip, "$config_parms{data_dir}/tv_providers.html" ); if ( 1 != $submitResult ) { return -1; } @@ -547,17 +519,10 @@ sub login { } if ( !defined $this_provider ) { - ($this_provider) = split( /,\s*/, $parms{provider} ) - ; # first time through, take the first one + ($this_provider) = split( /,\s*/, $parms{provider} ); # first time through, take the first one } - my $req_prov = - HTTP::Request->new( - POST => $url . "system.asp?partner_id=national&zipcode=$parms{zip}" ); - $req_prov->content( "provider=$this_provider&" - . "saveProvider=See%20Listings&" - . "zipCode=$parms{zip}&" - . "FormName=system.asp&" - . "page_from=" ); + my $req_prov = HTTP::Request->new( POST => $url . "system.asp?partner_id=national&zipcode=$parms{zip}" ); + $req_prov->content( "provider=$this_provider&" . "saveProvider=See%20Listings&" . "zipCode=$parms{zip}&" . "FormName=system.asp&" . "page_from=" ); $req_prov->content_type('application/x-www-form-urlencoded'); #submitRequest( $req_prov, "out3.txt" ); @@ -573,8 +538,7 @@ sub login { # so we have to do that with this step before asking for specific # dates/times #------------------------------------------------------------ - my $req_allChans = - HTTP::Request->new( GET => $url . 'listings_redirect.asp?spp=0' ); + my $req_allChans = HTTP::Request->new( GET => $url . 'listings_redirect.asp?spp=0' ); print "4 of 4\n" if $parms{debug}; $submitResult = submitRequest($req_allChans); if ( 1 != $submitResult ) { @@ -646,8 +610,7 @@ sub fetchDataToFile { } if ( !@uas ) { @providers = split( /,\s*/, $parms{provider} ); - $this_provider = - shift @providers; # what we just logged-into + $this_provider = shift @providers; # what we just logged-into } push( @uas, @@ -657,8 +620,7 @@ sub fetchDataToFile { 'provider' => $this_provider } ); - if ( $this_provider = shift @providers ) - { # prepare for next request + if ( $this_provider = shift @providers ) { # prepare for next request $ua = new RedirAgent(); $cookie_jar = HTTP::Cookies->new; $ua->cookie_jar($cookie_jar); @@ -669,8 +631,7 @@ sub fetchDataToFile { #------------------------------------------------------------ # Retrieve the file we need #------------------------------------------------------------ - print - "Requesting data for ${startDay} at $startHour for $parms{duration} hours\n"; + print "Requesting data for ${startDay} at $startHour for $parms{duration} hours\n"; # $startHour -= 2; my $provnum; @@ -678,8 +639,7 @@ sub fetchDataToFile { $ua = $_->{ua}; $cookie_jar = $_->{cookie_jar}; my $ofile = providerFilename( $outfile, $provnum ); - my $loopReq = - HTTP::Request->new( POST => $url . 'listings_redirect.asp' ); + my $loopReq = HTTP::Request->new( POST => $url . 'listings_redirect.asp' ); $loopReq->content( "displayType=Grid&" . "duration=$parms{duration}&" . "startDay=${startDay}&" @@ -690,8 +650,7 @@ sub fetchDataToFile { $loopReq->content_type('application/x-www-form-urlencoded'); my $submitResult = submitRequest( $loopReq, $ofile ); - my $loopReq = - HTTP::Request->new( GET => $url . 'listings_redirect.asp?spp=0' ); + my $loopReq = HTTP::Request->new( GET => $url . 'listings_redirect.asp?spp=0' ); my $submitResult = submitRequest( $loopReq, $ofile ); if ( 1 != $submitResult ) { return -1; @@ -734,9 +693,7 @@ sub prov_count { # SUB: processRawFile #====================================================================== sub processRawFile { - my ( $day_time, $hour, $down, $rawFile, $outfile, $dow, $month, $day, - $year, $tomorrow_month, $tomorrow_day ) - = @_; + my ( $day_time, $hour, $down, $rawFile, $outfile, $dow, $month, $day, $year, $tomorrow_month, $tomorrow_day ) = @_; print "Processing $rawFile to $outfile\n" if ( $parms{debug} ); #------------------------------------------------------------ @@ -766,21 +723,17 @@ eof #------------------------------------------------------------ # Create & initialize local variables for processing #------------------------------------------------------------ - my ( - $record, $record_prev, $script, $loop_phase, - $count1, $count2, $count3 - ); + my ( $record, $record_prev, $script, $loop_phase, $count1, $count2, $count3 ); my ( $pgm_desc, $min_start, $min_end, $min_pgm ) = 0; $count1 = $count2 = $count3 = 0; - my $channel_number = ''; - my $channel_name = ''; - my $pgm_name = ''; - my $channelRowsSaved = 0; - my $current_time_bar = ''; - my $rowOfLastTimeBar = 0; - my $tableStartText = - qq|
$type\n"; + $html .= "\n"; - $headers--; - + $html .= ""; for my $header ( '', 'Type', @headers ) { $html .= @@ -328,8 +326,6 @@ sub web_item_add { # write out new record to mht file $file_data[@file_data] = sprintf( "%-20s%-20s%-20s%-20s%-20s%s", $type, $address, $name, $group, $other1, $other2 ); - - #&main::print_log("DB: in webitem, $type, $address, $name, $group, $other1, $other2"); &mht_item_file_write( $web_item_file_name, \@file_data ); return 0; @@ -360,7 +356,6 @@ sub web_item_help { Options => 'List the device options separated by | (e.g. preset, resume=80)', FloorPlan => 'Floor Plan location', - Code => 'Perl code executed at startup', Other => 'Other stuff :)' ); diff --git a/web/bin/set_func.pl b/web/bin/set_func.pl index 8fd77d5c7..e746376b4 100644 --- a/web/bin/set_func.pl +++ b/web/bin/set_func.pl @@ -11,7 +11,7 @@ use strict; -#&main::print_log("db a=@ARGV\n"); +#print "db a=@ARGV\n"; # Process form if ( @ARGV > 2 ) { @@ -35,12 +35,9 @@ } } $func = "$func($args)"; - #&main::print_log("func=$func($args)"); my $html = eval $func; print "\nError in set_func.pl: $@\n" if $@; -#print_log "html=$html"; return $html if $html; # Allow function to override response -#print_log "redir=" &http_redirect($resp). return &http_redirect($resp); } diff --git a/web/bin/triggers.pl b/web/bin/triggers.pl index c55cfbd3f..78d022c61 100644 --- a/web/bin/triggers.pl +++ b/web/bin/triggers.pl @@ -12,7 +12,6 @@ $^W = 0; # Avoid redefined sub msgs my ( $function, @parms ) = @ARGV; -my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); #print "dbx a=@ARGV.\n"; @@ -62,12 +61,12 @@ sub web_trigger_list { triggers, change the type to "Disabled". If you delete it, the trigger will be recreated the next time Misterhouse is restarted. - - + + $form_trigger - + $form_code - +
$type\n"; $html .= "(back to top)
\n|; + $html .= qq|
\n|; $html .= "\n"; } @@ -150,8 +149,6 @@ sub web_trigger_list { my $triggered_date = &time_date_stamp( 7, $triggered ) if $triggered; $html .= "\n"; - } else { - $html .= "\n"; #put in a blank cell. } $html .= "\n\n"; @@ -203,7 +200,7 @@ sub web_trigger_add { # Create form else { my $html = - "Add a trigger:\n"; + "Add a trigger:\n"; $html .= qq|
Name \n|; $html .= qq|
Trigger\n|; diff --git a/web/ia7/graphics/help.gif b/web/ia7/graphics/help.gif deleted file mode 100755 index 3b514253179abc826410a1c983b15d1a3239c95d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1582 zcmV+}2GRLPNk%w1VITk?0QUd@a-QcgY19By)8y*(0Y$ncH=K#G^kS3d07=4csQ(&X z(dz8=8bqk<@%PQv>NrNC3roHrT+OS`|3i4w09o7{SPmH8rU=6$U5 zMPst`_4yGqq#iGHmg0uan%>SCf`vOS5jJNqidEQZl;Q%?Y ztGwPVWyuXrz`n%cMU3+(aP}i%+HtEt+2HOraMl|! zn^S|*dwaXt;_(1P!4Xl&E^*#cknR8d^Zxtw2NsV%dfauD&`EvT|Ns5}|M^IL)&2eZ z|Nr#;`}hDvwp@+nTZYdCKdlf$yB{T!PHeVYT&yKu&^lVK4^GH5Jf0mZn*aa*|Nr~{ z|Nb|7=5&R@I(*{>K(26|>;O8fK863=8{ zH*(v0pxsQ4@ob^$S%AuJo!^$k{qXJR2qmB#Adiur&5NhlnWN8`sME;N=+58sB`cMp zrO{rc|7o4=ZlLf1JEyq5-e!)|T#)Vh`}lvW@sg6pXqV}l#{X)agfOd`FNMm5h<1R z`1ygA$yko*A^8LV00000EC2ui03ZM$000R804E6?NU)&6g9sBUT!?UuF@KD>Fkm2q z!zFwj7#?iJq22@@S~h55_HPCQF2sgYrUxLwtfsB5#$0uv--5?~qiV1-5pTm;?9 zkt9eP1Jck)lePn0x^qP2_*m#bMwT2Bk|Y7Z%g>oByi`y#aR6F|5+0@^T)@Q}(I82% zoq_h*8ix=^CTXfneSym_Gtu3>Stvl1n990Tl%Yr?1YN*D1{$OaDGDeIp@SN|WJE=p(SxeMqJTgbuc)DeA;=bL0}wDk5YHm25LC$&^XQ^MwZWEgj2?uf0cEn_ zCi2b?bR2|ADO8*?M6h_0SO;rBs7H>0hFMCu{v1e4GuPt07Nq|WOB#I(=6nS9Hm&|$|?kq@Wd0FeA`SS z5WFJ_AKYkkfe0R)fQKtO;2;GA5J-W8Jn{U13@9s2vc?OM{E$KfAAF!d1@R~X%p!Cw g7q&nRgi*u;1&ASrB#|@=^1?cE2QK*F1O)^DJ1*7%H2?qr diff --git a/web/ia7/graphics/important.gif b/web/ia7/graphics/important.gif deleted file mode 100755 index 41d49438fd08230095c699b2d6f80593f3aa8667..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1492 zcmV;_1uObTNk%w1VITk?0QUd@^z`)Eb7#)h*Tlrc=H}-7{QT+Z>E~xw>oytxN=v#C z3-#mU{-vh>s;bCCJ^wW|q?nlc`ugH}ZMPa1&LtPsD;WRS*!ZHK+gwrqwYC4TvBhg^ zqXGcbMn1c{yVS9*{=vO_dwcy@Sjj9U$;rv&L~^IWt5= zM5_q}|GvK6G#$bN0Nc2<|H{g(9vk)k{@U8w`1ttSot^B0e5|FVW(Ee+76ZFkR;(2h zqzw$63klpE0J9DYg(oNe{QS?Zs?W~O#yK*krlz_L1@rp)*5c&N9i9aO|9g7%`uTEkaaAfRt0 z?d0R*%M}IX6ad2!1-UCH|7K>T7Zt%XF2ouU7#J8WEiM25|MTcXUs@IaVRM0_xRkb ztpAmk{XjlpY1P%$^OuqLvaHYE;aNXF_uAF|kd4B?!1dPF{+AXN@BixR|Lp6qXJ_a!4y`OKc~@6t5fQfn0E|CB+|J9= zkdXi6v@A~@3sHklsBGl!@aXdN z?(y;L?Ci{TcE30@xH~(7WMqh7V2^5Q@moRY9{~T`+kHt%{M_8`NiF}PqSlOkuti0e zetx@8PnwE~A^8LV00000EC2ui03ZM$000R80R0FYNU)&6g9sBUT*xrVib2r;F`Ve< z&=d(6qKu=sunIgGz|4&}Muua;P*YmjOQGZpgaRfF`WmFk9uX8{uuwS?vtY9cAe;fo zkfCKim_7aUA`~D65LyiwbOA@@N>V@WVt_!QYZkXcr(Qsr2OuV(1y4#4(Xh?P76vtH zI8Zd8=LS6>#%Mq!5FZ*e-Bid6#1Rk=C?!xdu(wFfj5G}JWa{t*7so388~|yc!4JAO zZ>|8tBSJ);d+jKOd%{Xr7Ylx}fL;ZtOM(PFzATw&Vvvk6`Z84U zDi{kNC3$i|Lm_1XVZ7->*pI$i4ZYl!y?K{gf=*6F~S$|{9%waDqw&>9TJqX0{{`g6oEtqL;wIgIyg{< z4K5TTi$PAjLr)7qAhV1CJN!`q436y(!5=#q(8dQi{6Ikt#7shvIG!9JObh#P0mB>y zxH$u89z@WN0dv40h!bTfFaZ<02~-6?-(1oH7`C{=MHp?YbBh3~aMA`e>#z}qAe$Ic z1_yiO;01Din3Bf_^QO}Uy|%CWX}LS zx=MZBAY0FAlG>-figF^zQENt?d+U$A{>91UbfWH5TB`W=^hGS|Tt^qu% z7G~lG3WESr(EviUK92ffi`M-8{+-GH09M*wkLCbd=~s&3MS0W_Q^sYL=>t&Ap2hs? z>FIucz1`#S07b(>chAw->kd%E3o4rb|NJ6GqAq6Bk+0$mOu(AO{}ep17Fy0yhS|^3 z=qD_c09DpIgYN(R_9YP|8tAP4^GEfisVsIr95%h zYHG1X^U!{P^)1R>=%c$uUl){QUYrc-#O+$4E$^ z;^OdgpXvxZqg9RFS%=tMh}QG>`(JUkI&jR};O{A0z$akXFnj+bXxS=h)CNer?Ca}v zqVsU2`FN)A^7;Ou%>RC||0ZkWcb3v(p8xai=(5rFGI!%QcI(^f`;)r&EoRh#tnUC< z-~~UeP?Gdip8r~y|1@vk3slJpNwo!3&_09zCvEV%%H{w&r~o>#A70>{!~31X{9tjm zgs}9c!t6|p`aq5MOONxBy7>xB!3<5r*y8g5FsR<-@;h`?34KVST z(7J?4UllJ|wlH#6t;ME<_Bh0m;a1%%b|x;3qJ^nJpg@ck;21Zoz!8Kg1`Mr{j)kcW z4a%iS<6sG7$%DQTdy{08!6s_<$*`c#^M)SFcrn?tWQ!xz6B3X^S3r)zOqMRVfnmD` z61)#?+;|}Q-h?%pB0QVa$6$&K-9)|rx^|n-9yTl3kRY+7)}ZT}7_`yA+F&99A+*%- z(1XYp6ZE8EfE{VfFu?@a#5a!%aO7};L8_2Kg%(;AfdmhSpuhzUN__AFFW9VchkR4G z&_xb&7(|JL0R*586d0@$3MmfQ;bAIw+!4z$Q%vE+i76Je2n6r67yuS@prV5@2T0%{ zK4-}DNRKW&(FFh}{8LaK!h{fj2w>77#R#@EiOdUSC@^J-PCTK4B!U%k`MDfW%zbFEaKT?c2 z!U$v>P(cTrlyQy=ZSLS^C%2@mRgf7iOi+h7qmEF50_~j22{84vy2BZjL=i+ndZ2L% zBwv)^tR^%_(?JC*3xz?-?poQUC?GG(^BA1^l2%F>rJ-Pb87dBa024L_x^B4jqw10h7Fu p!^Z$fB19Co1YyNW0?E-w5+}?t#|eUn@yW?3*KG66I2RNU06WRi?&Sag diff --git a/web/ia7/graphics/title.gif b/web/ia7/graphics/title.gif deleted file mode 100755 index f92b59665df2fd0d1c5e798af0945d14daa880bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 zcmXw!J4?e*0EI8wN-32h7TQhgQjvln@ljoL@C8l|R^8kj6#5fHry?#Er!G$Z0e^zs zG`(qJ;%%Db=Jw|1)uw4iH`{{Wa5x+e2hQQ~!B*)i4zfT7Ac~^laMaX1K}04b%WX}Ye94MPwF)JDW42FALk8HT|avmJZ1Lc3i`yOJcip7+J`vMh_D zG|8yn^VidsEfmtRdOgm? IU-!VyACo}9@&Et; diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index fbb530279..f958fb5db 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -1,22 +1,12 @@
-
+

Version:
Modified:
OS:
Perl:
User:

-
@@ -82,10 +72,7 @@

MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - developed the v4 web interface, updates by H.Plato. IA7 v1.2.350 Font Awesome by Dave Gandy - http://fontawesome.io

+ developed the v4 web interface, updates by H.Plato. IA7 v1.0.800 Font Awesome by Dave Gandy - http://fontawesome.io

-
- \ No newline at end of file +
\ No newline at end of file diff --git a/web/ia7/include/bootstrap-custom-button.3.3.5.css b/web/ia7/include/bootstrap-custom-button.3.3.5.css deleted file mode 100644 index 5d329999f..000000000 --- a/web/ia7/include/bootstrap-custom-button.3.3.5.css +++ /dev/null @@ -1,29 +0,0 @@ -.btn-purple { - color: #fff; - background-color: #834087; - border-color: #834087; -} -.btn-purple:hover, -.btn-purple:focus, -.btn-purple:active, -.btn-purple.active { - color: #fff !important; - background-color: #723876; - border-color: #613064; -} -.btn-purple.disabled:hover, -.btn-purple.disabled:focus, -.btn-purple.disabled:active, -.btn-purple.disabled.active, -.btn-purple[disabled]:hover, -.btn-purple[disabled]:focus, -.btn-purple[disabled]:active, -.btn-purple[disabled].active, -fieldset[disabled] .btn-purple:hover, -fieldset[disabled] .btn-purple:focus, -fieldset[disabled] .btn-purple:active, -fieldset[disabled] .btn-purple.active { - color: #fff; - background-color: #834087; - border-color: #834087; -} diff --git a/web/ia7/include/bootstrap-theme.3.0.2.min.css b/web/ia7/include/bootstrap-theme.3.0.2.min.css new file mode 100644 index 000000000..916427705 --- /dev/null +++ b/web/ia7/include/bootstrap-theme.3.0.2.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v3.0.2 by @fat and @mdo + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + */ + +.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e0e0e0));background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-moz-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#2d6ca2));background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-moz-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#419641));background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#eb9316));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c12e2a));background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#2aabd2));background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f3f3f3));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#222),to(#282828));background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-moz-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} \ No newline at end of file diff --git a/web/ia7/include/bootstrap.3.0.2.min.css b/web/ia7/include/bootstrap.3.0.2.min.css new file mode 100644 index 000000000..3deec3488 --- /dev/null +++ b/web/ia7/include/bootstrap.3.0.2.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v3.0.2 by @fat and @mdo + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + */ + +/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#c09853}.text-warning:hover{color:#a47e3c}.text-danger{color:#b94a48}.text-danger:hover{color:#953b39}.text-success{color:#468847}.text-success:hover{color:#356635}.text-info{color:#3a87ad}.text-info:hover{color:#2d6987}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small{display:block;line-height:1.428571429;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Monaco,Menlo,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.container{width:750px}.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.container{width:970px}.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.container{width:1170px}.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:45px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:45px;line-height:45px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#c09853}.has-warning .form-control{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.has-warning .input-group-addon{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#b94a48}.has-error .form-control{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.has-error .input-group-addon{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#468847}.has-success .form-control{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.has-success .input-group-addon{color:#468847;background-color:#dff0d8;border-color:#468847}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-xs{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid #000;border-right:4px solid transparent;border-bottom:0 dotted;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0 dotted;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-default .caret{border-top-color:#333}.btn-primary .caret,.btn-success .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret{border-top-color:#fff}.dropup .btn-default .caret{border-bottom-color:#333}.dropup .btn-primary .caret,.dropup .btn-success .caret,.dropup .btn-warning .caret,.dropup .btn-danger .caret,.dropup .btn-info .caret{border-bottom-color:#fff}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:5px 10px;padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified .btn{display:table-cell;float:none;width:1%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group.col{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:45px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:45px;line-height:45px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .open>a .caret,.nav .open>a:hover .caret,.nav .open>a:focus .caret{border-top-color:#2a6496;border-bottom-color:#2a6496}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-pills>li.active>a .caret,.nav-pills>li.active>a:hover .caret,.nav-pills>li.active>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav .caret{border-top-color:#428bca;border-bottom-color:#428bca}.nav a:hover .caret{border-top-color:#2a6496;border-bottom-color:#2a6496}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:auto}.navbar-collapse .navbar-nav.navbar-left:first-child{margin-left:-15px}.navbar-collapse .navbar-nav.navbar-right:last-child{margin-right:-15px}.navbar-collapse .navbar-text:last-child{margin-right:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-text{float:left;margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{margin-right:15px;margin-left:15px}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.dropdown>a:hover .caret,.navbar-default .navbar-nav>.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.open>a .caret,.navbar-default .navbar-nav>.open>a:hover .caret,.navbar-default .navbar-nav>.open>a:focus .caret{border-top-color:#555;border-bottom-color:#555}.navbar-default .navbar-nav>.dropdown>a .caret{border-top-color:#777;border-bottom-color:#777}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.dropdown>a:hover .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-nav>.dropdown>a .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .navbar-nav>.open>a .caret,.navbar-inverse .navbar-nav>.open>a:hover .caret,.navbar-inverse .navbar-nav>.open>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.btn .badge{position:relative;top:-1px}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.thumbnail{display:inline-block;display:block;height:auto;max-width:100%;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#356635}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#2d6987}.alert-warning{color:#c09853;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#a47e3c}.alert-danger{color:#b94a48;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#953b39}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-heading>.dropdown .caret{border-color:#333 transparent}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-heading>.dropdown .caret{border-color:#fff transparent}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading>.dropdown .caret{border-color:#468847 transparent}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#c09853;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading>.dropdown .caret{border-color:#c09853 transparent}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#b94a48;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading>.dropdown .caret{border-color:#b94a48 transparent}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading>.dropdown .caret{border-color:#3a87ad transparent}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;padding:10px;margin-right:auto;margin-left:auto}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;padding-top:30px;padding-bottom:30px}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-gradient(linear,0 top,100% top,from(rgba(0,0,0,0.5)),to(rgba(0,0,0,0.0001)));background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:-moz-linear-gradient(left,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-gradient(linear,0 top,100% top,from(rgba(0,0,0,0.0001)),to(rgba(0,0,0,0.5)));background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:-moz-linear-gradient(left,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}} \ No newline at end of file diff --git a/web/ia7/include/bootstrap.3.0.2.min.js b/web/ia7/include/bootstrap.3.0.2.min.js new file mode 100644 index 000000000..0e668e85c --- /dev/null +++ b/web/ia7/include/bootstrap.3.0.2.min.js @@ -0,0 +1,9 @@ +/*! + * Bootstrap v3.0.2 by @fat and @mdo + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + */ + +if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); \ No newline at end of file diff --git a/web/ia7/include/font-awesome.4.0.3.min.css b/web/ia7/include/font-awesome.4.0.3.min.css new file mode 100644 index 000000000..449d6ac55 --- /dev/null +++ b/web/ia7/include/font-awesome.4.0.3.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.0.3 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.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;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.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.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-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-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{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:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.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-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-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc: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-reply-all:before{content:"\f122"}.fa-mail-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{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"} \ No newline at end of file diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index ef70b9a34..417dd00e9 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,21 +1,14 @@ -// v1.2 +// Optimization opportunity +// add print_errorlog +// updateStaticPage has lots of copy paste + var entity_store = {}; //global storage of entities var json_store = {}; var updateSocket; -var updateSocketN; //Second socket for notifications var display_mode; if (display_mode == undefined) display_mode = "simple"; -var notifications; -var speech_sound; -var speech_banner; -var audio_init; -var audioElement = document.getElementById('sound_element'); -var authorized = "false"; -var developer = false; -var ctx; //audio context -var buf; //audio buffer //Takes the current location and parses the achor element into a hash function URLToHash() { @@ -119,6 +112,7 @@ function getJSONDataByPath (path){ return returnJSON; } + //Called anytime the page changes function changePage (){ var URLHash = URLToHash(); @@ -128,7 +122,10 @@ function changePage (){ URLHash.path = "collections"; } if (getJSONDataByPath("ia7_config") === undefined){ - // Load all the specific preferences + // We need at minimum the basic collections data to render all pages + // (the breadcrumb) + // NOTE may want to think about how to handle dynamic changes to the + // collections list $.ajax({ type: "GET", url: "/json/ia7_config", @@ -139,34 +136,11 @@ function changePage (){ } }); } else { - if (json_store.ia7_config.prefs.header_button == "no") $("#mhstatus").remove(); - if (json_store.ia7_config.prefs.audio_controls !== undefined && json_store.ia7_config.prefs.audio_controls == "yes") { - $("#sound_element").attr("controls", "controls"); //Show audio Controls - } - if (json_store.ia7_config.prefs.substate_percentages === undefined) json_store.ia7_config.prefs.substate_percentages = 20; - if (json_store.ia7_config.prefs.developer !== undefined) developer = json_store.ia7_config.prefs.developer; - // First time loading, set the default speech notifications - if (speech_sound === undefined) { - if ((json_store.ia7_config.prefs.speech_default !== undefined) && (json_store.ia7_config.prefs.speech_default.search("audio") >= 0 )) { - speech_sound = "yes"; - } else { - speech_sound = "no"; - } - } - if (speech_banner === undefined) { - if ((json_store.ia7_config.prefs.speech_default !== undefined) && (json_store.ia7_config.prefs.speech_default.search("banner") >= 0 )) { - speech_banner = "yes"; - } else { - speech_banner = "no"; - } - } - if ((json_store.ia7_config.prefs.notifications == undefined) || ((json_store.ia7_config.prefs.notifications !== undefined) && (json_store.ia7_config.prefs.notifications == "no" ))) { - notifications = "disabled"; - speech_sound = "no"; - speech_banner = "no"; - } else { - notifications = "enabled"; + //console.log("x "+json_store.ia7_config.prefs.substate_percentages); + if (json_store.ia7_config.prefs.header_button == "no") { + $("#mhstatus").remove(); } + if (json_store.ia7_config.prefs.substate_percentages === undefined) json_store.ia7_config.prefs.substate_percentages = 20; } if (getJSONDataByPath("collections") === undefined){ // We need at minimum the basic collections data to render all pages @@ -184,14 +158,9 @@ function changePage (){ }); } else { - // Check for authorize - authDetails(); // Clear Options Entity by Default $("#toolButton").attr('entity', ''); - // Remove the RRD Last Updated - $('#Last_updated').remove(); - //Trim leading and trailing slashes from path var path = URLHash.path.replace(/^\/|\/$/g, ""); if (path.indexOf('objects') === 0){ @@ -200,11 +169,6 @@ function changePage (){ else if (path.indexOf('vars') === 0){ loadVars(); } - else if (path.indexOf('prefs') === 0){ - var pref_name = path.replace(/\prefs\/?/,''); - console.log("loadprefs() "+pref_name); - loadPrefs(pref_name); - } else if(URLHash._request == 'page'){ var link = URLHash.link.replace(/\?+.*/,''); //HP for some reason, this often has the first arg with no value, ie ?bob var args = HashPathArgs(URLHash); @@ -212,13 +176,16 @@ function changePage (){ args = args.replace(/\=undefined/img,''); //HP sometimes arguments are just items and not key=value... link += "?"+args; } - + //alert("link="+link); + //$.get(URLHash.link, function( data ) { $.get(link, function( data ) { - + data = data.replace(/]*>/img, ''); //Remove stylesheets + data = data.replace(/]*>((\r|\n|.)*?)<\/title[^>]*>/img, ''); //Remove title + data = data.replace(/]*>/img, ''); //Remove meta refresh + data = data.replace(/]*>/img, ''); //Remove base target tags $('#list_content').html("
"); $('#buffer_page').append("
"); - parseLinkData(link,data); //remove css & fix up Mr.House setup stuff - + $('#row_page').html(data); }); } else if(path.indexOf('print_log') === 0){ @@ -230,7 +197,7 @@ function changePage (){ else if(path.indexOf('display_table') === 0){ var path_arg = path.split('?'); display_table(path_arg[1]); - } + } else if(path.indexOf('floorplan') === 0){ var path_arg = path.split('?'); floorplan(path_arg[1]); @@ -281,207 +248,6 @@ function changePage (){ } } -function loadPrefs (config_name){ //show ia7 prefs, args ia7_prefs, ia7_rrd_prefs if no arg then both - - $('#list_content').html("
"); - $('#prefs_table').append("
"); - var html = "
NameTrigger EventAction CodeTypeLast Run
$triggered_date
"; - var config_data; - if (config_name === undefined || config_name === '') config_name="ia7"; - if (config_name == "ia7") { - config_data = json_store.ia7_config; - } else if (config_name == "ia7_rrd") { - $.ajax({ - type: "GET", - async: false, //async is always better, but since this is the only point of the sub, it's OK - url: "/json/rrd_config", - dataType: "json", - success: function( json ) { - config_data = json.data; - } - }); - } - html += ""; - console.log(config_data); - console.log("in prefs="+config_data.length); - for (var i in config_data){ - if ( typeof config_data[i] === 'object') { - console.log("i "+i+":"); - html += ""; - for (var j in config_data[i]) { - if ( typeof config_data[i][j] === 'object') { - //console.log("j "+j+":"); - html += ""; - - for (var k in config_data[i][j]){ - //console.log("k "+k+" = "+json_store.ia7_config[i][j][k]); - html += ""; - } - //html +=""; - } else { - //console.log("j "+j+" = "+json_store.ia7_config[i][j]); - html += "" - } - } - //html +=""; - } else { - //console.log("i "+i+" : "+json_store.ia7_config[i]); - //html += ""; - } - } - - html += "
"+ config_name + "_config.json
"+ i + "
"+ j + "
"+k+" = "+config_data[i][j][k]+"
"+j+" = "+config_data[i][j]+"
"+ String(json_store.ia7_config[i]) + "
"; - $('#prtable').html(html); - -} - -function parseLinkData (link,data) { - - data = data.replace(/]*>/img, ''); //Remove stylesheets - data = data.replace(/]*>((\r|\n|.)*?)<\/title[^>]*>/img, ''); //Remove title - data = data.replace(/]*>/img, ''); //Remove meta refresh - data = data.replace(/]*>/img, ''); //Remove base target tags - - if (link == "/bin/code_select.pl" || link == "/bin/code_unselect.pl") { //fix links in the code select / unselect modules - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - data = data.replace(/href=\/bin\/browse.pl(.*?)>/img, function (path,r1) { - return 'href=/ia7/#_request=page&link=/bin/browse.pl'+r1+'&'+coll_key+',>'; - }); - data = data.replace(/\(back to top<\/a>\)/img, ''); - data = data.replace(/Category Index:/img,''); - data = data.replace(/.*?<\/a>/img,''); - } - if (link == "/bin/items.pl") { - console.log("items data="+data); - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - data = data.replace(/href=\/bin\/items.pl/img, 'onclick="changePage()"'); - data = data.replace(/\(back to top<\/a>\)/img, ''); - data = data.replace(/Item Index:/img,''); - data = data.replace(/.*?<\/a>/img,''); - data = data.replace(/input name='resp' value="\/bin\/items.pl"/img, 'input name=\'resp\' value=\"/ia7/#_request=page&link=/bin/items.pl&'+coll_key+'\"'); - - } - if (link == "/bin/iniedit.pl") { - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - data = data.replace(//img, ''); - data = data.replace(//img,''); - data = data.replace(/Back<\/a>/img,'Back<\/a>'); - - //replace the back button with a reload - } - if (link == "/bin/triggers.pl") { //fix links in the triggers modules - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - //data = data.replace(/href=\/bin\/triggers.pl/img, 'href=/ia7/#_request=page&link=/bin/triggers.pl&'+coll_key); - data = data.replace(/href=\/bin\/triggers.pl/img, 'onclick="changePage()"'); - data = data.replace(/\(back to top<\/a>\)/img, ''); - data = data.replace(/Trigger Index:/img,''); - data = data.replace(/.*?<\/a>/img,''); - //data = data.replace(/onChange=\'form.submit\(\)\'/img,'onChange=\'this\.form\.submit\(\)\''); - data = data.replace(/input name='resp' value="\/bin\/triggers.pl"/img, 'input name=\'resp\' value=\"/ia7/#_request=page&link=/bin/triggers.pl&'+coll_key+'\"'); - //console.log(data); - } - if (link == "/ia5/news/main.shtml") { //fix links in the email module 1 - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - data = data.replace(/Latest emails<\/a>/img,''); - data = data.replace(/href=\/email\/(.*?)>/img, function (path,r1) { - return 'href=/ia7/#_request=page&link=/email/'+r1+'&'+coll_key+',>'; - }); - data = data.replace(/(.*?)<\/a>/img, function (path,r1,r2) { - return r1; - }); - data = data.replace(/href='RUN;\/ia5\/news\/main.shtml\?Check_for_e_mail'/img, 'class="btn-voice-cmd" voice_cmd="Check_for_e_mail"'); - } - if (link.indexOf('/email/') === 0) { //fix links in the email module 2 - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - data = data.replace(/Previous<\/a>.*?
/img, ''); - data = data.replace(/
Back to Index<\/a>.*?/img,''); - data = data.replace(/href='#\d+'/img,''); - } - if (link.indexOf('/comics/') === 0) { //fix links in the comics module - var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - data = data.replace(/(.*?)<\/a>/img,function (path,r1,r2) { - return ''+r2+''; - }); - data = data.replace(/]*>/img, ''); //Remove stylesheets - data = data.replace(/]*>((\r|\n|.)*?)<\/title[^>]*>/img, ''); //Remove title - data = data.replace(/]*>/img, ''); //Remove meta refresh - data = data.replace(/]*>/img, ''); //Remove base target tags - - var start = data.toLowerCase().indexOf('') + 6; - var end = data.toLowerCase().indexOf(''); - - if (form.attr('action') === "/bin/triggers.pl?add" && ! data.match(/Not authorized to make updates/)) { - //location.reload(); - changePage(); - } else if (form.attr('action') === "/bin/iniedit.pl") { -// var pdata = parseLinkData("/bin/iniedit.pl",data); - parseLinkData("/bin/iniedit.pl",data); -// $('#row_page').html(pdata); -//TODO parse data - } else { - $('#lastResponse').find('.modal-body').html(data.substring(start, end)); - $('#lastResponse').modal({ - show: true - }); - } - } - }); -// } - }); - $('#mhresponse :input:not(:text)').change(function() { -//TODO - don't submit when a text field changes - console.log("in input not text"); - $('#mhresponse').submit(); - }); - $('#mhexec a').click( function (e) { - e.preventDefault(); - var url = $(this).attr('href'); - url = url.replace(/;(.*?)\?/,'?'); -// console.log("MHExec " + url); - $.get( url, function(data) { -// var start = data.toLowerCase().indexOf('') + 6; -// var end = data.toLowerCase().indexOf(''); -// $('#lastResponse').find('.modal-body').html(data.substring(start, end)); -// $('#lastResponse').modal({ -// show: true -// }); - }); - changePage(); - }); -} - - function loadVars (){ //variables list var URLHash = URLToHash(); $.ajax({ @@ -586,8 +352,6 @@ var loadList = function() { // Sort that list if a sort exists, probably exists a shorter way to // write the sort - // Sorting code removed. Original design idea that buttons could be moved - // Around by the end user. Possible function for the future. // if (sort_list !== undefined){ // entity_list = sortArrayByArray(entity_list, sort_list); // } @@ -598,7 +362,7 @@ var loadList = function() { // This is not an entity, likely a value of the root obj continue; } - if (json_store.objects[entity].hidden !== undefined){ + if (json_store.objects[entity].hidden != undefined){ // This is an entity with the hidden property, so skip it continue; } @@ -620,8 +384,8 @@ var loadList = function() { button_html += '
'; button_html += ''; button_html += '
'; - button_html += ''; button_html += ''; $('#rrd-periods').append(dropdown_html); - - var last_timestamp = "unavailable"; - if (json.data.last_update !== undefined) { - last_timestamp = new Date(json.data.last_update); - } - //Update the footer database updated time - console.log ("Last Updated:"+last_timestamp); - $('#Last_updated').remove(); - $('#footer_stuff').prepend("
RRD Database Last Updated:"+last_timestamp+"
"); - $('.dropdown').on('click', '.dropdown-menu li a', function(e){ e.stopPropagation(); @@ -1516,6 +1100,7 @@ var graph_rrd = function(start,group,time) { // put the selection list on the side. for (var i = 0; i < json.data.data.length; i++){ + //console.log("selection="+json.data.data[i].label); var legli = $('
  • ').appendTo('#rrd-legend'); $('').appendTo(legli); $(''+ - ''; - if (coords !== ""){ - $('#graphic').append(html); - } - else { - $('#fp_positionless_items').append(html); - } - } - var E = $('#'+entityId); - E.bind("dragstart", noDragDrop); - var image = get_fp_image(json.data[entity]); - E.attr('src',"/ia7/graphics/"+image); - if (show_pos) - E.css("border","1px solid black"); - - return E; -}; - -var fp_resize_floorplan_image = function(){ - var floor_width = $("#fp_graphic").width(); - $("#fp_graphic").attr("width", "1px"); - - fp_display_width = $("#graphic").width(); - console.log("FP: resize "+ floor_width + " => " + fp_display_width); - $('#fp_graphic').attr("width",fp_display_width+"px"); - fp_display_height = $("#fp_graphic").height(); -}; - -var fp_reposition_entities = function(){ - var t0 = performance.now(); - var offset = $("#fp_graphic").offset(); - var width = fp_display_width; - var hight = fp_display_height; - var onePercentWidthInPx = width/100; - var onePercentHeightInPx = hight/100; - var fp_get_offset_from_location = function(item) { - var y = item[0]; - var x = item[1]; - var newy = offset.top + y * onePercentHeightInPx; - var newx = offset.left + x * onePercentWidthInPx; - return { - "top": newy, - "left": newx - }; - }; - var nwidth = $("#fp_graphic").get(0).naturalWidth; - fp_scale = Math.round( width/nwidth * 100); - - // update the location of all the objects... - $(".floorplan_item").each(function(index) { - var classstr = $(this).attr("class"); - var coords = classstr.split(/coords=/)[1]; - $(this).width(fp_scale + "%"); - - if (coords.length === 0){ - return; - } - var fp_location = coords.split(/x/); - var fp_offset = fp_get_offset_from_location(fp_location); - - // this seems to make the repositioning slow - // ~ 300+ms on my nexus7 firefox-beta vs <100ms with this code commented out - // var baseimg_width = $("#fp_graphic").width(); - // if (baseimg_width < 500) { - // $(this).attr('src',$(this).attr('src').replace('48.png','32.png')); - // } else { - // $(this).attr('src',$(this).attr('src').replace('32.png','48.png')); - // } - - var adjust = $(this).width()/2; - var fp_off_center = { - "top": fp_offset.top - adjust, - "left": fp_offset.left - adjust - }; - fp_set_pos($(this).attr('id'), fp_off_center); - }); - - $('.icon_select img').each(function(){ - $(this).width(fp_scale + "%"); - }); - var t1 = performance.now(); - console.log("FP: reposition and scale: " +Math.round(t1 - t0) + "ms "); -}; - -var fp_set_pos = function(id, offset){ - var item = $('#' + id); - // do not move the span, this make the popup to narrow somehow - // item.closest("span").offset(offset); - item.offset(offset); -}; - -var fp_is_point_on_fp = function (p){ - var offset = $("#fp_graphic").offset(); - var width = $("#fp_graphic").width(); - var height = $("#fp_graphic").height(); - if (p.top < offset.top) - return false; - if (p.top > offset.top + height) - return false; - if (p.left < offset.left) - return false; - if (p.left > offset.left + width) - return false; - - return true; -}; - var floorplan = function(group,time) { - var URLHash = URLToHash(); - var baseimg_width; - if (typeof time === 'undefined'){ - //var window_width = $(window).width(); - $('#list_content').html("
    "); - if (developer){ - // add elememnts to show current position on floorplan - $('#floorplan').append("
      " + - "
    1. grab icon and drop it on apropriate position on the flooplan
    2. " + - "
    3. right click item to select another iconset
    4. "+ - "
    5. to remove the item from the perl code drop it besides the fp background image
    6. "+ - "
    7. repeat 1/2/3 for all items you'd like to change
    8. "+ - "
    9. copy the generated perl code into your usercode file
    10. " + - "
    " + - "
    y,x = " + - "
    "); - } - $('#floorplan').append("
    "); - time = 0; - $('#graphic').prepend('
    '); - if (URLHash.show_pos){ - $('#fp_graphic').css("border","1px solid black"); - $('#list_content').append("
    "); - $('#list_content').append("
    ");
    -        }
    -        $('#fp_graphic').bind("load", function () {
    -            console.log("FP: background loaded.");
    -            fp_resize_floorplan_image();
    -            floorplan(group, time);
    -        });
    -        var base_img_dir = '/ia7/graphics/floorplan';
    -		if (json_store.ia7_config.prefs.floorplan_basedir !== undefined) base_img_dir = json_store.ia7_config.prefs.floorplan_basedir;
    -        $('#fp_graphic').attr("src", base_img_dir+'-'+group+'.png');
    -        return;
    -    }
    -
    -    if (updateSocket !== undefined && updateSocket.readyState !== 4){
    -        // Only allow one update thread to run at once
    -        updateSocket.abort();
    -    }
    -
    -    if (developer){
    -        // update positon
    -
    -        $(document).mousemove(function(e){
    -            var offset = $("#fp_graphic").offset();
    -            var width = $("#fp_graphic").width();
    -            var hight = $("#fp_graphic").height();
    -            var  l = e.pageX - offset.left;
    -            var  t = e.pageY - offset.top;
    -
    -            //var pos =   Math.round((t/hight) *100) +"," + Math.round((l/width)*100);
    -            var pos =  (t/hight) *100 +"," + (l/width)*100;
    -            //console.log("floorplanpos: " + pos );
    -            $('#debug_pos').text(pos);
    -            if (fp_grabbed_entity !== null){
    -                //var itemCenterOffset = Math.round(fp_grabbed_entity.width/2);
    -                var itemCenterOffset = fp_grabbed_entity.width/2;
    -                var newPos = {
    -                    "top": e.pageY - itemCenterOffset,
    -                    "left": e.pageX - itemCenterOffset
    -                };
    -                fp_set_pos(fp_grabbed_entity.id, newPos);
    -                //console.log(fp_grabbed_entity.id +" pos: " +newPos.top + " x " + newPos.left);
    -                //fp_grabbed_entity.class.replace("coords=.*", "coords="+pos);
    -            }
    -        });
    +	var URLHash = URLToHash();
    +	var baseimg_width;
    +	if (typeof time === 'undefined'){
    +  		$('#list_content').html("
    "); + $('#floorplan').append("
    "); + time = 0; + $('#graphic').prepend('
    '); + baseimg_width = $(window).width(); + if (baseimg_width > 990) baseimg_width = 800; + $('#fp_graphic').attr("width",baseimg_width+"px"); + } + + if (updateSocket !== undefined && updateSocket.readyState != 4){ + // Only allow one update thread to run at once + updateSocket.abort(); + } + //resize window if changed + window.onresize = function(){ + baseimg_width = $(window).width(); + if (baseimg_width > 990) baseimg_width = 800; + $('#fp_graphic').attr("width",baseimg_width+"px"); + // update the location of all the objects... + $(".floorplan_item").each(function(index) { + var classstr = $(this).attr("class"); + var coords = classstr.split(/coords=/)[1]; + var fp_location = coords.split(/x/); + var location = get_fp_location(fp_location,0); + //alert("fp_location="+fp_location+" location="+location); + $(this).attr("style",location); +//iphone scale + var baseimg_width = $(window).width(); + if (baseimg_width < 500) { + $(this).attr('src',$(this).attr('src').replace('48.png','32.png')) + } else { + $(this).attr('src',$(this).attr('src').replace('32.png','48.png')) + } - $(window).mousedown(function(e){ - if (e.which === 1 && e.target.id.indexOf("entity_") >= 0){ - fp_grabbed_entity = e.target; - e.stopPropagation(); - return true; - } - }); + }); + } - $(window).mouseup(function(e){ - if (fp_grabbed_entity === null) - return; + var path_str = "/objects"; + var arg_str = "parents="+group+"&fields=fp_location,state,states,state_log,fp_icons,fp_icon_set,img,link,label,type&long_poll=true&time="+time; + + updateSocket = $.ajax({ + type: "GET", + url: "/LONG_POLL?json('GET','"+path_str+"','"+arg_str+"')", + dataType: "json", + success: function( json, statusText, jqXHR ) { + var requestTime = time; + if (jqXHR.status == 200) { + JSONStore(json); + for (var entity in json.data) { + for (var i=0 ; i < json.data[entity].fp_location.length-1; i=i+2){ //allow for multiple graphics + //alert("length="+json.data[entity].fp_location.length+" i="+i+" x="+json.data[entity].fp_location[i]+" y="+json.data[entity].fp_location[i+1]+" ent_length="+$('#entity_'+entity).length); + var location = get_fp_location(json.data[entity].fp_location,i); + var popover = 0; + if ((json.data[entity].type == "FPCamera_Item") || + (json_store.ia7_config.prefs.fp_state_popovers == "yes")) popover = 1 + //console.log("popover="+popover+" config="+json_store.ia7_config.prefs.fp_state_popovers) + var popover_html = ""; + if (popover) popover_html = 'data-toggle="popover" data-trigger="focus" tabindex="0"' + + var image = get_fp_image(json.data[entity]); + if ($('#entity_'+entity+'_'+i).length > 0) { + $('#entity_'+entity+'_'+i).attr('src',"/ia7/graphics/"+image); + } else { + $('#graphic').append(''); + //create_state_modal('#entity_'+entity+i,entity); + } + // create unique popovers for Camera items + if (json.data[entity].type == "FPCamera_Item") { + var name = entity; + if (json.data[entity].label !== undefined) name = json.data[entity].label + var a_start = ""; + var a_end = ""; + if (json.data[entity].link !== undefined) { + a_start = '' + a_end = ''; + } + //console.log("name="+entity+" label = "+json.data[entity].label); + //console.log("link = "+json.data[entity].get_img); + $('[data-toggle="popover"]').popover({ + placement : 'auto bottom', //placement of the popover. also can use top, bottom, left or right + title : name, + html: 'true', //needed to show html of course + content : '
    '+a_start+''+a_end+'
    ' + }); + } else { + if (popover) { + + $('[data-toggle="popover"]').popover({ + placement : 'auto bottom', //placement of the popover. also can use top, bottom, left or right + title : function() { + var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. + var name = fp_entity; + if (json_store.objects[fp_entity].label !== undefined) name = json_store.objects[fp_entity].label; + return name+" - "+json_store.objects[fp_entity].state; + }, + html: 'true', //needed to show html of course + content : function() { + var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. + var po_states = json_store.objects[fp_entity].states; + var html = '
    '; + // HP need to have at least 2 states to be a controllable object... + if (po_states.length > 1) { + html = '
    '; + var buttons = 0; + var stategrp = 0; + for (var i = 0; i < po_states.length; i++){ + if (filterSubstate(po_states[i]) == 1) { + continue + } else { + buttons++ + //} + if (buttons > 2) { + stategrp++; + html += "
    "; + buttons = 1; + } + //console.log ("name="+fp_entity+" buttons="+buttons) + + var color = getButtonColor(po_states[i]) +//TODO disabled override + var disabled = "" + if (po_states[i] == json_store.objects[fp_entity].state) { + disabled = "disabled"; + } + html += "
    " + //console.log("html="+html) + return html + } - set_set_coordinates_from_offset(fp_grabbed_entity.id); - fp_reposition_entities(); - fp_grabbed_entity = null; - }); + }); + } else { + $('#entity_'+entity+'_'+i).click( function () { + //var fp_entity = $(this).attr("id").split(/entity_/)[1]; // + var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. + //alert("entity="+fp_entity); + create_state_modal(fp_entity); + }); + } + $('#entity_'+entity+'_'+i).mayTriggerLongClicks().on( 'longClick', function() { + var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. + create_state_modal(fp_entity); + }); + } + } + } + requestTime = json.meta.time; + } + if (jqXHR.status == 200 || jqXHR.status == 204) { + //Call update again, if page is still here + //KRK best way to handle this is likely to check the URL hash + if ($('#floorplan').length !== 0){ + //If the floorplan page is still active request more data + floorplan(group,requestTime); + } + } + } + }); +}; - } +//have to figure out height, and the jumps at 991 and 1200 +var get_fp_location = function(item,index) { + var baseimg_width = $(window).width(); + //var baseimg_height = $(window).height() - 60; + var baseimg_height = $('#fp_graphic').height(); + var tmargin = 0; + var lmargin = 0; + //console.log("baseimg_width="+baseimg_width+"baseimg_height="+baseimg_height+" h2="+baseimg_height2); + if (baseimg_width > 990) { + lmargin = (baseimg_width - 980) / 2; + tmargin = 20; + if (baseimg_width > 1200) { + lmargin = (baseimg_width - 1180) / 2; + } + baseimg_width = 800; - var set_set_coordinates_from_offset = function (id) - { - var E = $('#'+id); - var offsetE = E.offset(); - offsetE.top += E.width()/2; - offsetE.left += E.width()/2; - var offsetP = $("#fp_graphic").offset(); - var width = fp_display_width; - var hight = fp_display_height; - var onePercentWidthInPx = width/100; - var onePercentHeightInPx = hight/100; - - var newy = (offsetE.top - offsetP.top) / onePercentHeightInPx; - var newx = (offsetE.left - offsetP.left) / onePercentWidthInPx; - var coords = newy+"x"+newx; - var name = id.match(/entity_(.*)_(\d)+$/)[1]; - var codeLines = $("#fp_pos_perl_code").text().split('\n'); - var newCode = ""; - if (fp_is_point_on_fp(offsetE) === false){ - E.attr("class", "entity="+id+" floorplan_item coords="); - E.attr("src", "/ia7/graphics/fp_unknown_info_48.png"); - for (var i = 0; i< codeLines.length; i++) - { - var line = codeLines[i]; - if (line.startsWith("$"+name) === false && line !== "") - { - newCode += line + "\n"; - } - } - } - else{ - E.attr("class", "entity="+id+" floorplan_item coords="+coords); - var coordIdx = id.match(/entity_(.*)_(\d)+$/)[2]; - - var itemUpdated = false; - for (var i = 0; i< codeLines.length; i++) - { - var line = codeLines[i]; - if (line.startsWith("$"+name+"->set_fp_location")) - { - var m = line.match(/.*\((.*)\).*/); - oldCoords = m[1].split(","); - oldCoords[+coordIdx] = newy; - oldCoords[+coordIdx+1] = newx; - var newline = "$" + name + "->set_fp_location("+ oldCoords.join(",") + ");\n"; - newCode += newline; - itemUpdated = true; - } - else if (line !== "") - { - newCode += line + "\n"; - } - } - if (itemUpdated === false) - { - var newline = "$" + name + "->set_fp_location("+ newy +","+ newx + ");\n"; - newCode += newline; - } - } - newCode = newCode.split('\n').sort().join('\n'); - $("#fp_pos_perl_code").text(newCode); - }; - - // reposition on window size change - window.onresize = function(){ - if ($('#floorplan').length === 0) - { - window.onresize = null; - return; - } + } + var location = "position: absolute; "; + var top = parseInt((baseimg_height * item[index] / 100)+tmargin); + var left = parseInt((baseimg_width * item[index+1] / 100)+lmargin); + //console.log("baseimg_width="+baseimg_width+" top="+top+" left="+left); + location += "top: "+top+"px;"; + location += "left: "+left+"px"; + return location; +} - console.log("FP: window resized"); - fp_resize_floorplan_image(); - fp_reposition_entities(); - }; - - var path_str = "/objects"; - var fields = "fields=fp_location,state,states,fp_icons,fp_icon_set,img,link,label,type"; - if (json_store.ia7_config.prefs.state_log_show === "yes") - fields += ",state_log"; - - var arg_str = "parents="+group+"&"+fields+"&long_poll=true&time="+time; - - updateSocket = $.ajax({ - type: "GET", - url: "/LONG_POLL?json('GET','"+path_str+"','"+arg_str+"')", - dataType: "json", - error: function(xhr, textStatus, errorThrown){ - // console.log('FP: request failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"'); - }, - success: function( json, statusText, jqXHR ) { - // console.log('FP: request succeeded: "' + statusText + '" "'+JSON.stringify(jqXHR, undefined,2)+'"'); - var requestTime = time; - if (jqXHR.status === 200) { - var t0 = performance.now(); - JSONStore(json); - for (var entity in json.data) { - if (URLHash.show_pos && requestTime === 0){ - perl_pos_coords = ""; - } - for (var i=0 ; i < json.data[entity].fp_location.length-1; i=i+2){ //allow for multiple graphics - var popover = 0; - if ((json.data[entity].type === "FPCamera_Item") || (json_store.ia7_config.prefs.fp_state_popovers === "yes")) - popover = 1; - - if (URLHash.show_pos && requestTime === 0){ - if (perl_pos_coords.length !== 0){ - perl_pos_coords += ", "; - } - perl_pos_coords += "" + json.data[entity].fp_location[i]+','+json.data[entity].fp_location[i+1]; - } - - var coords= json.data[entity].fp_location[i]+'x'+json.data[entity].fp_location[i+1]; - var E = fp_getOrCreateIcon(json, entity, i, coords, URLHash.show_pos); - - if (URLHash.show_pos === undefined) - { - // create unique popovers for Camera items - if (json.data[entity].type === "FPCamera_Item") { - var name = entity; - if (json.data[entity].label !== undefined) - name = json.data[entity].label; - - var a_start = ""; - var a_end = ""; - if (json.data[entity].link !== undefined) { - a_start = ''; - a_end = ''; - } - - $('[data-toggle="popover"]').popover({ - placement : 'auto bottom', //placement of the popover. also can use top, bottom, left or right - title : name, - html: 'true', //needed to show html of course - content : '
    '+a_start+''+a_end+'
    ' - }); - } else { - if (popover) { - - $('[data-toggle="popover"]').popover({ - placement : 'auto bottom', //placement of the popover. also can use top, bottom, left or right - title : function() { - var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. - var name = fp_entity; - if (json_store.objects[fp_entity].label !== undefined) name = json_store.objects[fp_entity].label; - var ackt = E.offset(); - return name+ " - "+json_store.objects[fp_entity].state; - }, - html: 'true', //needed to show html of course - content : function() { - var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. - var po_states = json_store.objects[fp_entity].states; - var html = '
    '; - // HP need to have at least 2 states to be a controllable object... - if (po_states.length > 1) { - html = '
    '; - var buttons = 0; - var stategrp = 0; - for (var i = 0; i < po_states.length; i++){ - if (filterSubstate(po_states[i]) !== 1) { - buttons++; - if (buttons > 2) { - stategrp++; - html += "
    "; - buttons = 1; - } - - var color = getButtonColor(po_states[i]); - //TODO disabled override - var disabled = ""; - if (po_states[i] === json_store.objects[fp_entity].state) { - disabled = "disabled"; - } - html += "
    "; - //console.log("html="+html) - } - return html; - } - }); - } else { - E.click( function () { - //var fp_entity = $(this).attr("id").split(/entity_/)[1]; // - var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. - //alert("entity="+fp_entity); - create_state_modal(fp_entity); - }); - } - E.mayTriggerLongClicks().on( 'longClick', function() { - var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. - create_state_modal(fp_entity); - }); - } - } - } - - if (URLHash.show_pos && requestTime === 0){ - if (perl_pos_coords.length===0) - { - fp_getOrCreateIcon(json, entity, 0, "", URLHash.show_pos); - } - else{ - var oldCode = $('#fp_pos_perl_code').text(); - if (oldCode.length !== 0) - oldCode += "\n"; - - var perl_pos_code = ""; - var iconset = json.data[entity].fp_icon_set; - if (iconset !== undefined){ - perl_pos_code += '$' + entity + '->set_fp_icon_set("'; - perl_pos_code += iconset + '");\n'; - } - perl_pos_code += "$" + entity + "->set_fp_location("; - perl_pos_code += perl_pos_coords + ");"; - perl_pos_code = oldCode + perl_pos_code; - perl_pos_code = perl_pos_code.split('\n').sort().join('\n'); - $('#fp_pos_perl_code').text(perl_pos_code); - } - } - } - fp_reposition_entities(); - if (requestTime === 0 && URLHash.show_pos){ - $('#list_content').append("

     

    "); - $.ajax({ - type: "GET", - url: "/LONG_POLL?json('GET','fp_icon_sets','px=48')", - dataType: "json", - error: function(xhr, textStatus, errorThrown){ - console.log('FP: request iconsets failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"'); - }, - success: function( json, statusText, jqXHR ) { - console.log('FP: request iconsets: "' + statusText + '" "'+JSON.stringify(jqXHR, undefined,2)+'"'); - var requestTime = time; - if (jqXHR.status === 200) { - var iconlist = '\n"; - $('#list_content').append(iconlist); - - // Trigger action when the contexmenu is about to be shown - $(".floorplan_item").bind("contextmenu", function (event) { - - event.preventDefault(); - - fp_icon_select_item_id = $(this).attr('id'); - $(".icon_select").finish().toggle(100); - $(".icon_select").offset({ - top: event.pageY, - left: event.pageX - }); - }); - - - // If the document is clicked somewhere - $(document).bind("mousedown", function (e) { - if ($(e.target).parents(".icon_select").length === 0) { - $(".icon_select").hide(100); - fp_icon_select_item_id = null; - } - }); - - - // If the menu element is clicked - $(".icon_select img").click(function(){ - var img = $(this).attr("src"); - $('#'+fp_icon_select_item_id).attr('src', img); - var name = fp_icon_select_item_id.match(/entity_(.*)_(\d)+$/)[1]; - - var codeLines = $("#fp_pos_perl_code").text().split('\n'); - var newCode = ""; - - var icon_set_name = img.match(/.*fp_(.*)_(.*)_48.png/)[1]; - var itemUpdated = false; - for (var i = 0; i< codeLines.length; i++) - { - var line = codeLines[i]; - if (line.startsWith("$"+name+"->set_fp_icon_set")) - { - var newline = "$" + name + '->set_fp_icon_set("'+ icon_set_name+ '");\n'; - newCode += newline; - itemUpdated = true; - } - else if (line !== "") - { - newCode += line + "\n"; - } - } - if (itemUpdated === false) - { - var newline = "$" + name + '->set_fp_icon_set("'+ icon_set_name +'");\n'; - newCode += newline; - } - newCode = newCode.split('\n').sort().join('\n'); - $("#fp_pos_perl_code").text(newCode); - $(".icon_select").hide(100); - fp_icon_select_item_id = null; - }); - } - } - }); - } - requestTime = json.meta.time; - var t1 = performance.now(); - console.log("FP: long poll " +Math.round(t1 - t0) + "ms"); - } - if (jqXHR.status === 200 || jqXHR.status === 204) { - //Call update again, if page is still here - //KRK best way to handle this is likely to check the URL hash - if ($('#floorplan').length !== 0){ - //If the floorplan page is still active request more data - // and we are not editing the fp - if (URLHash.show_pos === undefined) - floorplan(group,requestTime); - } - } - } - }); -}; var get_fp_image = function(item,size,orientation) { var image_name; - var image_color = getButtonColor(item.state); + var image_color = getButtonColor(item.state) var baseimg_width = $(window).width(); - var image_size = "48"; - // if (baseimg_width < 500) image_size = "32" // iphone scaling - //kvar image_size = "32" + var image_size = "48" + if (baseimg_width < 500) image_size = "32" // iphone scaling if (item.fp_icons !== undefined) { + //alert("Has a button defined state="+item.fp_icons[item.state]); if (item.fp_icons[item.state] !== undefined) return item.fp_icons[item.state]; } if (item.fp_icon_set !== undefined) { + //alert("Has a button defined state="+item.fp_icons[item.state]); return "fp_"+item.fp_icon_set+"_"+image_color+"_"+image_size+".png"; } // if item.fp_icons.return item.fp_icons[state]; - if(item.type === "Light_Item" || item.type === "Fan_Light" || - item.type === "Insteon_Device" || item.type === "UPB_Link" || - item.type === "Insteon::SwitchLinc" || item.type === "Insteon::SwitchLincRelay" || - item.type === "Insteon::KeyPadLinc" || - item.type === "EIB_Item" || item.type === "EIB1_Item" || - item.type === "EIB2_Item" || item.type === "EIO_Item" || - item.type === "UIO_Item" || item.type === "X10_Item" || - item.type === "xPL_Plugwise" || item.type === "X10_Appliance") { + if(item.type == "Light_Item" || item.type == "Fan_Light" || + item.type == "Insteon_Device" || item.type == "UPB_Link" || + item.type == "Insteon::SwitchLinc" || item.type == "Insteon::SwitchLincRelay" || + item.type == "Insteon::KeyPadLinc" || + item.type == "EIB_Item" || item.type == "EIB1_Item" || + item.type == "EIB2_Item" || item.type == "EIO_Item" || + item.type == "UIO_Item" || item.type == "X10_Item" || + item.type == "xPL_Plugwise" || item.type == "X10_Appliance") { return "fp_light_"+image_color+"_"+image_size+".png"; } - if(item.type === "Motion_Item" || item.type === "X10_Sensor" || - item.type === "Insteon::MotionSensor" ) { + if(item.type == "Motion_Item" || item.type == "X10_Sensor" || + item.type == "Insteon::MotionSensor" ) { return "fp_motion_"+image_color+"_"+image_size+".png"; } - if(item.type === "Door_Item" || item.type === "Insteon::IOLinc_door") { + if(item.type == "Door_Item" || item.type == "Insteon::IOLinc_door") { return "fp_door_"+image_color+"_"+image_size+".png"; } - if(item.type === "FPCamera_Item" ) { + if(item.type == "FPCamera_Item" ) { return "fp_camera_default_"+image_size+".png"; } return "fp_unknown_info_"+image_size+".png"; -}; +} var create_img_popover = function(entity) { } @@ -2250,6 +1483,7 @@ var create_state_modal = function(entity) { grid_buttons = 4; group_buttons = 3; } + //console.log("display buttons="+display_buttons+" grid_buttons="+grid_buttons+" group_buttons="+group_buttons); for (var i = 0; i < modal_states.length; i++){ if (filterSubstate(modal_states[i]) == 1) { @@ -2313,73 +1547,6 @@ var create_state_modal = function(entity) { }); } -var authorize_modal = function(user) { - - //alert(user); - var changed = "false"; - var af = ""; - var html = '
    '; - if (user !== "0") html += "Currently logged in as "+user+"

    "; - html += ''; - html += ''; - html += '
    '; - html += ''; - -//create footer buttons - $('#loginModal').find('.modal-footer').html(''); - - if (user == "0") { - $('#loginModal').find('.modal-footer').prepend(''); - $('#loginModal').find('.modal-footer').prepend(''); - } - $('#loginModal').find('.modal-body').html(html); - $('#loginModal').modal({ - show: true - }); - $('#loginModal').on('shown.bs.modal', function () { - if (user == "0") $('#password').focus(); - }); - $('#loginModal').on('hide.bs.modal', function () { - if (changed == "true") location.reload(); - }); - $('.btn-login-logoff').click( function () { - $.get ("/UNSET_PASSWORD"); - console.log("in logoff"); - location.reload(); - $('#loginModal').modal('hide'); - }); - $('#LoginModalpw').submit( function (e) { - e.preventDefault(); - //console.log("Custom submit function"); - $.ajax({ - type: "POST", - url: "/SET_PASSWORD_FORM", - data: $(this).serialize(), - success: function(data){ - console.log(data) - var status=data.match(/\(.*)\<\/b\>/gm); - //console.log("match="+status[2]); //3rd match is password status - if (status[2] == "Password was incorrect") { - //alert("Password was incorrect"); - $('#loginModal').find('#pwstatus').html("Password was incorrect"); - $("#loginModal").find('#password').val(''); - if (user !== "0") { - user = "0"; - authorized = "false"; - changed = "true"; - $("#currentuser").html(""); - //location.reload(); - } - } else { - location.reload(); - $('#loginModal').modal('hide'); - } - } - }); - }); -} //Outputs the list of triggers var trigger = function() { @@ -2450,27 +1617,14 @@ $(document).ready(function() { $(window).bind('hashchange', function() { changePage(); }); - $("#mhstatus").click( function () { var link = json_store.collections[600].link; link = buildLink (link, "0,600"); + // window.location.href = "/ia7/#path=/objects&parents=group1&_collection_key=0,1,17,$group1"; window.location.href = link; }); - - // Load up 'globals' -- notification and the status - updateItem("ia7_status"); - get_notifications(); - + updateItem("ia7_status"); $("#toolButton").click( function () { - // Need a 'click' event to turn on sound for mobile devices - if (mobile_device() == "ios" ) { - if (audio_init === undefined) { - audioElement = document.getElementById('sound_element'); - audioElement.play(); - } - audio_init = 1; - } - // var entity = $("#toolButton").attr('entity'); $('#optionsModal').modal('show'); $('#optionsModal').find('.object-title').html("Mr.House Options"); @@ -2479,8 +1633,6 @@ $(document).ready(function() { $('#optionsModal').find('.modal-body').html('
    '); var simple_active = "active"; var simple_checked = "checked"; - var develop_active = ""; - var develop_checked = ""; var advanced_active = ""; var advanced_checked = "" if (display_mode == "advanced") { @@ -2488,87 +1640,13 @@ $(document).ready(function() { simple_checked = ""; advanced_active = "active"; advanced_checked = "checked" - develop_active = ""; - develop_checked = ""; } - if (display_mode == "advanced" && developer == true) { - simple_active = ""; - simple_checked = ""; - advanced_active = ""; - advanced_checked = "" - develop_active = "active"; - developed_checked = "checked" - } - - $('#optionsModal').find('.modal-body').find('.btn-group').append(""); - $('#optionsModal').find('.modal-body').find('.btn-group').append(""); - $('#optionsModal').find('.modal-body').find('.btn-group').append(""); - + $('#optionsModal').find('.modal-body').find('.btn-group').append(""); + $('#optionsModal').find('.modal-body').find('.btn-group').append(""); $('.mhmode').on('click', function(){ - if ($(this).find('input').attr('id') == "developer") { - display_mode = "advanced"; - developer = true; - } else { - display_mode = $(this).find('input').attr('id'); - developer = false; - } + display_mode = $(this).find('input').attr('id'); changePage(); }); - - var sound_active = ""; - var banner_active = ""; - var off_active = "active"; - var sound_label = "Sound"; - var banner_label = "Banner"; - if ($(window).width() <= 768) { // make icons for mobile - sound_label = ""; - banner_label = ""; - } - if (speech_banner === "yes") { - banner_active = "active"; - off_active = ""; - } - if (speech_sound === "yes") { - sound_active = "active"; - off_active = ""; - } - if (notifications === "disabled") { - sound_active = "active"; - off_active = "active"; - banner_active = "active"; - } - // if notifications disabled then disable all the buttons - $('#optionsModal').find('.modal-body').append('
    '); - $('#optionsModal').find('.modal-body').find('.btn-notifications').append(""); - $('#optionsModal').find('.modal-body').find('.btn-notifications').append(""); - $('#optionsModal').find('.modal-body').find('.btn-notifications').append(""); - $('#optionsModal').find('.modal-body').find('.btn-notifications').append(""); - $('.mhnotify').on('click', function(){ - var speech_mode = $(this).find('input').attr('id'); - var button_active = $(this).hasClass('active'); - if (speech_mode == "off") { - $('.mhnotifyopt').removeClass('active'); - speech_sound = "no"; - speech_banner = "no"; - } else { - if (speech_mode === "sound") { - if (button_active === true) { - speech_sound = "no"; - } else { - speech_sound = "yes"; - } - } else if (speech_mode === "banner") { - if (button_active === true) { - speech_banner = "no"; - } else { - speech_banner = "yes"; - } - } - $('.mhnotifyoff').removeClass('active'); - if ((speech_banner === "no") && (speech_sound === "no")) $('.mhnotifyoff').addClass('active'); - } - //if off, then unselect others - }); // parse the collection ID 500 and build a list of buttons var opt_collection_keys = 0; var opt_entity_html = ""; @@ -2590,42 +1668,14 @@ $(document).ready(function() { if (json_store.collections[collection].external !== undefined) { link = json_store.collections[collection].external; } - //Check to see if this is the login button - if (json_store.collections[collection].user !== undefined) { -// if (name == undefined) { -// authDetails(); -// } - opt_entity_html += ""; - } else { - opt_entity_html += ""+name+""; - } + opt_entity_html += ""+name+""; } } - $('#optionsModal').find('.modal-body').append(opt_entity_html); - $('.btn-login-modal').click( function () { - $('#optionsModal').modal('hide'); - var user = $(this).attr('user') - authorize_modal(user); - }); + $('#optionsModal').find('.modal-body').append(opt_entity_html); $('#optionsModal').find('.btn-list').click(function (){ $('#optionsModal').modal('hide'); }); }); - -//TODO remove me? - $('#mhresponse').click( function (e) { - e.preventDefault(); - $form = $(this); - console.log("MHResponse Custom submit function "+ form.attr('action')); - //$.ajax({ - // type: "POST", - // url: "/SET_PASSWORD_FORM", - // data: $(this).serialize(), - // success: function(data){ - // console.log(data) - // } - // }); - }); }); // diff --git a/web/ia7/include/jquery.alerts.css b/web/ia7/include/jquery.alerts.css deleted file mode 100755 index 98efe1e0b..000000000 --- a/web/ia7/include/jquery.alerts.css +++ /dev/null @@ -1,57 +0,0 @@ -#popup_container { - font-family: Arial, sans-serif; - font-size: 12px; - min-width: 300px; /* Dialog will be no smaller than this */ - max-width: 600px; /* Dialog will wrap after this width */ - background: #FFF; - border: solid 5px #999; - color: #000; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; -} - -#popup_title { - font-size: 14px; - font-weight: bold; - text-align: center; - line-height: 1.75em; - color: #666; - background: #CCC url(/ia7/graphics/title.gif) top repeat-x; - border: solid 1px #FFF; - border-bottom: solid 1px #999; - cursor: default; - padding: 0em; - margin: 0em; -} - -#popup_content { - background: 16px 16px no-repeat url(/ia7/graphics/info.gif); - padding: 1em 1.75em; - margin: 0em; -} - -#popup_content.alert { - background-image: url(/ia7/graphics/info.gif); -} - -#popup_content.confirm { - background-image: url(/ia7/graphics/important.gif); -} - -#popup_content.prompt { - background-image: url(/ia7/graphics/help.gif); -} - -#popup_message { - padding-left: 48px; -} - -#popup_panel { - text-align: center; - margin: 1em 0em 0em 1em; -} - -#popup_prompt { - margin: .5em 0em; -} diff --git a/web/ia7/include/jquery.alerts.js b/web/ia7/include/jquery.alerts.js deleted file mode 100644 index efd1e7d3e..000000000 --- a/web/ia7/include/jquery.alerts.js +++ /dev/null @@ -1,237 +0,0 @@ -// jQuery Alert Dialogs Plugin -// -// Version 1.1 -// -// Cory S.N. LaViska -// A Beautiful Site (http://abeautifulsite.net/) -// 14 May 2009 -// -// Visit http://abeautifulsite.net/notebook/87 for more information -// -// Usage: -// jAlert( message, [title, callback] ) -// jConfirm( message, [title, callback] ) -// jPrompt( message, [value, title, callback] ) -// -// History: -// -// 1.00 - Released (29 December 2008) -// -// 1.01 - Fixed bug where unbinding would destroy all resize events -// -// License: -// -// This plugin is dual-licensed under the GNU General Public License and the MIT License and -// is copyright 2008 A Beautiful Site, LLC. -// -(function($) { - - $.alerts = { - - // These properties can be read/written by accessing $.alerts.propertyName from your scripts at any time - - verticalOffset: -75, // vertical offset of the dialog from center screen, in pixels - horizontalOffset: 0, // horizontal offset of the dialog from center screen, in pixels/ - repositionOnResize: true, // re-centers the dialog on window resize - overlayOpacity: .01, // transparency level of overlay - overlayColor: '#FFF', // base color of overlay - draggable: true, // make the dialogs draggable (requires UI Draggables plugin) - okButton: ' OK ', // text for the OK button - cancelButton: ' Cancel ', // text for the Cancel button - dialogClass: null, // if specified, this class will be applied to all dialogs - - // Public methods - - alert: function(message, title, callback) { - if( title == null ) title = 'Alert'; - $.alerts._show(title, message, null, 'alert', function(result) { - if( callback ) callback(result); - }); - }, - - confirm: function(message, title, callback) { - if( title == null ) title = 'Confirm'; - $.alerts._show(title, message, null, 'confirm', function(result) { - if( callback ) callback(result); - }); - }, - - prompt: function(message, value, title, callback) { - if( title == null ) title = 'Prompt'; - $.alerts._show(title, message, value, 'prompt', function(result) { - if( callback ) callback(result); - }); - }, - - // Private methods - - _show: function(title, msg, value, type, callback) { - - $.alerts._hide(); - $.alerts._overlay('show'); - - $("BODY").append( - ''); - - if( $.alerts.dialogClass ) $("#popup_container").addClass($.alerts.dialogClass); - - // IE6 Fix - //var pos = ($.browser.msie && parseInt($.browser.version) <= 6 ) ? 'absolute' : 'fixed'; - var pos = 'fixed'; - - $("#popup_container").css({ - position: pos, - zIndex: 99999, - padding: 0, - margin: 0 - }); - - $("#popup_title").text(title); - $("#popup_content").addClass(type); - $("#popup_message").text(msg); - $("#popup_message").html( $("#popup_message").text().replace(/\n/g, '
    ') ); - - $("#popup_container").css({ - minWidth: $("#popup_container").outerWidth(), - maxWidth: $("#popup_container").outerWidth() - }); - - $.alerts._reposition(); - $.alerts._maintainPosition(true); - - switch( type ) { - case 'alert': - $("#popup_message").after(''); - $("#popup_ok").click( function() { - $.alerts._hide(); - callback(true); - }); - $("#popup_ok").focus().keypress( function(e) { - if( e.keyCode == 13 || e.keyCode == 27 ) $("#popup_ok").trigger('click'); - }); - break; - case 'confirm': - $("#popup_message").after(''); - $("#popup_ok").click( function() { - $.alerts._hide(); - if( callback ) callback(true); - }); - $("#popup_cancel").click( function() { - $.alerts._hide(); - if( callback ) callback(false); - }); - $("#popup_ok").focus(); - $("#popup_ok, #popup_cancel").keypress( function(e) { - if( e.keyCode == 13 ) $("#popup_ok").trigger('click'); - if( e.keyCode == 27 ) $("#popup_cancel").trigger('click'); - }); - break; - case 'prompt': - $("#popup_message").append('
    ').after(''); - $("#popup_prompt").width( $("#popup_message").width() ); - $("#popup_ok").click( function() { - var val = $("#popup_prompt").val(); - $.alerts._hide(); - if( callback ) callback( val ); - }); - $("#popup_cancel").click( function() { - $.alerts._hide(); - if( callback ) callback( null ); - }); - $("#popup_prompt, #popup_ok, #popup_cancel").keypress( function(e) { - if( e.keyCode == 13 ) $("#popup_ok").trigger('click'); - if( e.keyCode == 27 ) $("#popup_cancel").trigger('click'); - }); - if( value ) $("#popup_prompt").val(value); - $("#popup_prompt").focus().select(); - break; - } - - // Make draggable - if( $.alerts.draggable ) { - try { - $("#popup_container").draggable({ handle: $("#popup_title") }); - $("#popup_title").css({ cursor: 'move' }); - } catch(e) { /* requires jQuery UI draggables */ } - } - }, - - _hide: function() { - $("#popup_container").remove(); - $.alerts._overlay('hide'); - $.alerts._maintainPosition(false); - }, - - _overlay: function(status) { - switch( status ) { - case 'show': - $.alerts._overlay('hide'); - $("BODY").append(''); - $("#popup_overlay").css({ - position: 'absolute', - zIndex: 99998, - top: '0px', - left: '0px', - width: '100%', - height: $(document).height(), - background: $.alerts.overlayColor, - opacity: $.alerts.overlayOpacity - }); - break; - case 'hide': - $("#popup_overlay").remove(); - break; - } - }, - - _reposition: function() { - var top = (($(window).height() / 2) - ($("#popup_container").outerHeight() / 2)) + $.alerts.verticalOffset; - var left = (($(window).width() / 2) - ($("#popup_container").outerWidth() / 2)) + $.alerts.horizontalOffset; - if( top < 0 ) top = 0; - if( left < 0 ) left = 0; - - // IE6 fix - //if( $.browser.msie && parseInt($.browser.version) <= 6 ) top = top + $(window).scrollTop(); - top = top + $(window).scrollTop(); - - $("#popup_container").css({ - top: top + 'px', - left: left + 'px' - }); - $("#popup_overlay").height( $(document).height() ); - }, - - _maintainPosition: function(status) { - if( $.alerts.repositionOnResize ) { - switch(status) { - case true: - $(window).bind('resize', $.alerts._reposition); - break; - case false: - $(window).unbind('resize', $.alerts._reposition); - break; - } - } - } - - } - - // Shortuct functions - jAlert = function(message, title, callback) { - $.alerts.alert(message, title, callback); - } - - jConfirm = function(message, title, callback) { - $.alerts.confirm(message, title, callback); - }; - - jPrompt = function(message, value, title, callback) { - $.alerts.prompt(message, value, title, callback); - }; - -})(jQuery); diff --git a/web/ia7/include/tables.css b/web/ia7/include/tables.css index 9ed17528e..29e0cf13b 100644 --- a/web/ia7/include/tables.css +++ b/web/ia7/include/tables.css @@ -29,13 +29,7 @@ border-radius: 0 0 6px 0; } -#prtable table { - width: 97%; - margin-top: 8px; - margin-left: 15px; - margin-right: 15px; -} - + #rtable p { margin: 20px 0; } diff --git a/web/ia7/index.shtml b/web/ia7/index.shtml index f5fcfc9a8..f1e9b26b1 100644 --- a/web/ia7/index.shtml +++ b/web/ia7/index.shtml @@ -26,7 +26,6 @@ - @@ -38,10 +37,6 @@ - - - - @@ -137,7 +132,7 @@ } .rrd-period-dropdown { - margin-left:45px; + margin-left:55px; } .rrd-legend-class { @@ -155,18 +150,6 @@ float:left; margin-left:-10px; } - .alerts { - position: absolute; - top: 3px; - left: 17px; - right: 17px; - } - .mobile-alert { - margin-top:-22px !important; - } - .dropdown-caret { - margin-left:-5px !important; - } @@ -184,26 +167,19 @@ -
    -
    -
    -
    - +
    - - -
    -
    + \ No newline at end of file diff --git a/web/organizer/contacts.pl b/web/organizer/contacts.pl index 844cc0ff7..c2ebfbc00 100644 --- a/web/organizer/contacts.pl +++ b/web/organizer/contacts.pl @@ -156,7 +156,7 @@ BEGIN my ($command) = $objCGI->param('vsCOM') || ""; my ($idNum) = $objCGI->param('vsID') || ""; my ($scriptName) = $ENV{'SCRIPT_NAME'} || "contacts.pl"; -$scriptName = $ia7_prefix . "/organizer/contacts.pl" if ( $web_mode eq "IA7" ); +$scriptName = $ia7_prefix . "/organizer/tasks.pl" if ( $web_mode eq "IA7" ); my ($filePath) = $ENV{"CWD"} . "/" . $fileName; $filePath = "$config_parms{organizer_dir}/$fileName"; my ($activePage) = $objCGI->param('vsAP') || "1"; From 2cd2349d13fa76b6c5336107bbf840dc0820e255 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 18 Sep 2016 13:41:09 -0400 Subject: [PATCH 042/209] Updating to version that I'm using, cleaned up code, add api_key detection. --- code/common/weather_wunderground.pl | 176 +++++++++++----------------- 1 file changed, 68 insertions(+), 108 deletions(-) diff --git a/code/common/weather_wunderground.pl b/code/common/weather_wunderground.pl index 5e08cf240..ee3dca9c8 100644 --- a/code/common/weather_wunderground.pl +++ b/code/common/weather_wunderground.pl @@ -1,8 +1,7 @@ # Category = Weather - #@ Updates live weather variables from http://api.wunderground.com. -=begin comment +=begin @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ File: @@ -29,165 +28,126 @@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut + # noloop=start use Weather_Common; + use Data::Dumper; use XML::Twig; - my $wunderground_file_getweather; - my $wunderground_file_getforecast; - $p_weather_wunderground_getweather = new Process_Item(); - $p_weather_wunderground_getforecast = new Process_Item(); + my $wunderground_getweather_file; + my $wunderground_stationid=$config_parms{wunderground_stationid}; + my $wunderground_apikey=$config_parms{wunderground_apikey}; + my $wunderground_url; + my %wunderground_data; + my @wunderground_keys; + $p_weather_wunderground_getweather = new Process_Item(); - my $wunderground_states = 'getweather,getforecast,debug'; + my $wunderground_states = 'getweather,parsefile,debug'; $v_wunderground = new Voice_Cmd("wunderground [$wunderground_states]"); # noloop=stop if ($Reload) { - $wunderground_file_getweather=$config_parms{data_dir}.'/web/weather_wunderground_weather.xml'; - $wunderground_file_getforecast=$config_parms{data_dir}.'/web/weather_wunderground_forecast.xml'; - if($config_parms{wunderground_stationid} eq '') { - print_log("Warning: wunderground_stationid is not defined in mh.private.ini."); - } + $wunderground_stationid=$config_parms{wunderground_stationid}; + $wunderground_apikey=$config_parms{wunderground_apikey}; + $wunderground_stationid='' unless $wunderground_stationid; + $wunderground_getweather_file=$config_parms{data_dir}.'/web/weather_wunderground_getweather.xml'; if($config_parms{wunderground_apikey} eq '') { - print_log("Warning: wunderground_apikey is not defined in mh.private.ini."); - } - if($config_parms{state} eq '') { - print_log("Warning: state is not defined in mh.private.ini."); - } - if($config_parms{wunderground_city} eq '') { - print_log("Warning: wunderground_city is not defined in mh.private.ini."); - } - - #$p_weather_wunderground_getweather-set_output("$wunderground_file_getweather"); + print_log("[WUnderground] ERROR: wunderground_apikey is not defined in mh.private.ini."); + } else { + if($wunderground_stationid ne '') { + print_log "[WUnderground] Using Weather underground StationID: $wunderground_stationid" if $Debug{weather}; + $wunderground_url='http://api.wunderground.com/api/'.$wunderground_apikey.'/conditions/q/pws:'.$wunderground_stationid.'.xml'; + } elsif($config_parms{zip_code} ne '') { + print_log "[WUnderground] Using ZIP Code: " . $config_parms{zip_code} if $Debug{weather}; + $wunderground_url='http://api.wunderground.com/api/'.$wunderground_apikey.'/conditions/q/'.$config_parms{zip_code}.'.xml'; + } else { + print_log "[WUnderground] WARNING: wunderground_stationid and zip_core are both blank in mh.private.ini. Leaving it to Weather underground to guess your location by IP address..." if $Debug{weather}; + $wunderground_url='http://api.wunderground.com/api/'.$wunderground_apikey.'/conditions/q/autoip.xml'; + } + + print_log "[WUnderground] Using URL: '".$wunderground_url if $Debug{weather}; - &trigger_set(qq|time_cron('*/15 * * * *') or \$Reload|, "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') - unless &trigger_get('Update current weather conditions via wunderground'); - &trigger_set('($New_Minute && $Minute == 5) or $Reload', "run_voice_cmd 'wunderground getforecast'", 'NoExpire', 'Update forecast via wunderground') - unless &trigger_get('Update forecast via wunderground'); + &trigger_set(qq|time_cron('*/15 * * * *') or \$Reload|, "run_voice_cmd 'wunderground getweather'", 'NoExpire', 'Update current weather conditions via wunderground') + unless &trigger_get('Update current weather conditions via wunderground'); + } } my $wunderground_state = 'blank'; -if ($wunderground_state = $v_wunderground->{said}) { - if ($wunderground_state eq 'debug'){ - #Not implemented - } elsif ($wunderground_state eq 'getweather'){ - if($config_parms{wunderground_stationid} eq '') { - print_log("ERROR: wunderground_stationid is not defined in mh.private.ini."); - } else { - my $wunderground_url='http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID='.$config_parms{wunderground_stationid}; - print_log('Retrieving: '.$wunderground_url); - set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_file_getweather"}; - start $p_weather_wunderground_getweather; - } - } elsif ($wunderground_state eq 'getforecast'){ - my $wundergrounderror=0; - if($config_parms{wunderground_apikey} eq '') { - print_log("ERROR: wunderground_apikey is not defined in mh.private.ini."); - $wundergrounderror++; - } - if($config_parms{state} eq '') { - print_log("ERROR: state is not defined in mh.private.ini."); - $wundergrounderror++; - } - if($config_parms{wunderground_city} eq '') { - print_log("ERROR: wunderground_city is not defined in mh.private.ini."); - $wundergrounderror++; - } - if ($wundergrounderror == 0) { - my $wustate=uc $config_parms{state}; - my $wucity=$config_parms{wunderground_city}; - my $wunderground_url='https://api.wunderground.com/api/'.$config_parms{wunderground_apikey}.'/forecast/q/'.$wustate.'/'.$wucity.'.xml'; - print_log('Retrieving: '.$wunderground_url); - set $p_weather_wunderground_getforecast qq{get_url -quiet "$wunderground_url" "$wunderground_file_getforecast"}; - start $p_weather_wunderground_getforecast; - } +if ($wunderground_state = $v_wunderground ->{said}) { + if ($wunderground_state eq 'getweather'){ + print_log "[WUnderground] Getting data from $wunderground_url" if $Debug{weather}; + set $p_weather_wunderground_getweather qq{get_url -quiet "$wunderground_url" "$wunderground_getweather_file"}; + start $p_weather_wunderground_getweather; } } -my(%wunderground_data,@wunderground_keys); - -if (done_now $p_weather_wunderground_getweather) { +if (done_now $p_weather_wunderground_getweather or 'parsefile' eq said $v_wunderground) { $Weather{wunderground_obsv_valid} = 0; #Set to not valid unless proven - #my $wunderground_xml=file_read $wunderground_file_getweather; - print_log "wunderground getweather finished."; - my $twig = new XML::Twig; - $twig->parsefile($wunderground_file_getweather); + #my $wunderground_xml=file_read $wunderground_getweather_file; + print_log "[WUnderground] getweather finished, data written to $wunderground_getweather_file" if $Debug{weather}; + my $twig = new XML::Twig(); + $twig->parsefile($wunderground_getweather_file); + $twig->print if $Debug{weather} >= 5; my $root = $twig->root; my $channel = $root->first_child("current_observation"); + + print_log "[WUnderground] " . Dumper $channel if $Debug{weather} >= 5; my $w_stationid = $root->first_child_text("station_id"); - print_log "WUnderground: Received stationid: $w_stationid" if $Debug{wunderground}; - + print_log "[WUnderground] Received stationid: $w_stationid" if $Debug{weather}; + + $config_parms{wunderground_stationid} = $w_stationid; + if($config_parms{wunderground_stationid} eq $w_stationid) { %wunderground_data={}; @wunderground_keys=[]; #TempOutdoor - weather_wunderground_addelem($root,'TempOutdoor','temp_f'); + weather_wunderground_addelem($channel,'TempOutdoor','temp_f'); #DewOutdoor - weather_wunderground_addelem($root,'DewOutdoor','dewpoint_f'); + weather_wunderground_addelem($channel,'DewOutdoor','dewpoint_f'); #WindAvgDir - weather_wunderground_addelem($root,'WindAvgDir','wind_dir'); + weather_wunderground_addelem($channel,'WindAvgDir','wind_dir'); #WindAvgSpeed - weather_wunderground_addelem($root,'WindAvgSpeed','wind_mph'); + weather_wunderground_addelem($channel,'WindAvgSpeed','wind_mph'); #WindGustDir #WindGustSpeed + weather_wunderground_addelem($channel,'WindGustSpeed','wind_gust_mph'); #WindGustTime #Clouds #Conditions + weather_wunderground_addelem($channel,'Conditions','weather'); #Barom - weather_wunderground_addelem($root,'Barom','pressure_mb'); + weather_wunderground_addelem($channel,'Barom','pressure_mb'); #BaromSea #BaromDelta #HumidOutdoorMeasured #HumidOutdoor - weather_wunderground_addelem($root,'HumidOutdoor','relative_humidity'); + weather_wunderground_addelem($channel,'HumidOutdoor','relative_humidity'); #IsRaining #IsSnowing #RainTotal + weather_wunderground_addelem($channel,'RainTotal','precip_today_in'); #RainRate - #use Data::Dumper; - #print Dumper %wunderground_data; + print_log "[WUnderground] " . Dumper %wunderground_data if $Debug{weather} >= 5; + print_log "[WUnderground] Using elements: $config_parms{weather_wunderground_elements}" if $Debug{weather}; &Weather_Common::populate_internet_weather(\%wunderground_data, $config_parms{weather_wunderground_elements}); &Weather_Common::weather_updated; - + } else { - print_log "WUnderground: ERROR! Received a station ID we didn't want. Aborting."; - } - -} - -if (done_now $p_weather_wunderground_getforecast) { - $Weather{wunderground_obsv_valid} = 0; #Set to not valid unless proven - #my $wunderground_xml=file_read $wunderground_file_getweather; - print_log "wunderground getforecast finished."; - my $twig = new XML::Twig; - print_log('Reading file: '.$wunderground_file_getforecast); - $twig->parsefile($wunderground_file_getforecast); - my $root = $twig->root; - my $fcast = $root->first_child("forecast"); - my $txtfcast = $fcast->first_child("txt_forecast"); - my $fcdays = $txtfcast->first_child('forecastdays'); - - my @forecast = $fcdays->children('forecastdays'); - - print "\n\n"; - print $fcdays->text(); - print "\n\n"; - - foreach my $fcday (@forecast) { - print_log($fcday->first_child_text('fcttext')); + print_log "[WUnderground] ERROR! Received a station ID we didn't want. Aborting."; } - use Data::Dumper; - #print Dumper @forecast; } sub weather_wunderground_addelem { - my ($w_root,$w_dest,$w_src) = @_; - if(my $w_srcval=$w_root->first_child_text("$w_src")) { - print_log sprintf("WUnderground: Data: %15s = %8s (%s)",$w_dest,$w_srcval,$w_src) if $Debug{wunderground}; + my ($w_twigroot,$w_dest,$w_src) = @_; + print_log "[WUnderground] Looking for $w_src" if $Debug{weather}; + + if(my $w_srcval=$w_twigroot->first_child_text("$w_src")) { + $w_srcval =~ s/%$//; + print_log sprintf("WUnderground: Data: %15s = %8s (%s)",$w_dest,$w_srcval,$w_src) if $Debug{weather}; $wunderground_data{$w_dest}=$w_srcval; push(@wunderground_keys, $w_dest); } From d2e76bbab98083fe41df6c57da233e7cb90925b0 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Sun, 18 Sep 2016 13:49:13 -0400 Subject: [PATCH 043/209] . --- code/common/weather_wunderground.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/code/common/weather_wunderground.pl b/code/common/weather_wunderground.pl index ee3dca9c8..412bc0771 100644 --- a/code/common/weather_wunderground.pl +++ b/code/common/weather_wunderground.pl @@ -50,6 +50,7 @@ $wunderground_apikey=$config_parms{wunderground_apikey}; $wunderground_stationid='' unless $wunderground_stationid; $wunderground_getweather_file=$config_parms{data_dir}.'/web/weather_wunderground_getweather.xml'; + if($config_parms{wunderground_apikey} eq '') { print_log("[WUnderground] ERROR: wunderground_apikey is not defined in mh.private.ini."); } else { From f868c298a4a8c270b6042af45238f218637d2d6d Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 18 Sep 2016 20:07:27 -0600 Subject: [PATCH 044/209] Steve's merged in changes, plus some additions for 4.08. Humidity workaround still todo --- lib/Venstar_Colortouch.pm | 142 +++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 60c2e7c2d..b0e80a1e2 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -21,10 +21,15 @@ use Data::Dumper; # v1.1 - added in schedule and humidity control. # v1.2 - added communication tracker object & timeout control # v1.3 - added check for timer defined +# v1.4 - support for API v5. It seems like v5 +# "hum_setpoint": is the current humidity and +# "dehum_setpoint": is the humidity setpoint +# "hum" doesn't return anything anymore. # Notes: # - Best to use firmware at least 3.14 released Nov 2014. This fixes issues with both # schedule and humidity/dehumidify control. +# - 4.08 is API5, seems to have better wifi, but humidity control is messed up. #todo # - temp setpoint bounds checking @@ -55,6 +60,7 @@ sub new { my $self = {}; bless $self, $class; $self->{data} = undef; + $self->{api_ver} = 0; $self->{child_object} = undef; $self->{config}->{cache_time} = 30; #TODO fix cache timeouts $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} @@ -68,7 +74,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{host} = $host; - $self->{debug} = 0; + $self->{debug} = 5; $self->{loglevel} = 1; $self->{status} = ""; $self->{timeout} = 15; #300; @@ -106,11 +112,11 @@ sub _init { my ( $isSuccessResponse1, $stat ) = $self->_get_JSON_data('api'); if ($isSuccessResponse1) { - - if ( ( $stat->{api_ver} > 3 ) and ( $stat->{type} eq "residential" ) ) { + $self->{api_ver} = $stat->{api_ver}; + if ( ( $self->{api_ver} > 3 ) and ( $stat->{type} eq "residential" ) ) { main::print_log( - "[Venstar Colortouch] Residental Venstar ColorTouch found with api level $stat->{api_ver}" + "[Venstar Colortouch] Residental Venstar ColorTouch found with api level $self->{api_ver}" ); if ( $self->poll() ) { main::print_log( "[Venstar Colortouch:" @@ -305,7 +311,8 @@ sub _push_JSON_data { if ( $type eq 'settings' ) { #tempunits=0&away=0&schedule=0&hum_setpoint=0&dehum_setpoint=0 - + # with v4.08 firmware, hum and dehum setpoints need to be at least 4 % points different + my ( $newunits, $newaway, $newsched, $newhumsp, $newdehumsp ); my ($units) = $params =~ /tempunits=(\d+)/; my ($away) = $params =~ /away=(\d+)/; @@ -314,7 +321,7 @@ sub _push_JSON_data { my ($humsp) = $params =~ /hum_setpoint=(\d+)/; my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/ ; #need to add in dehumidifier stuff at some point - + my $humidity_change = 0; my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; @@ -324,10 +331,10 @@ sub _push_JSON_data { $units = 0 if ( ( $units eq "F" ) or ( $units eq "f" ) ); $away = $caway if ( not defined $away ); - $sched = $csched if ( not defined $sched ); + $sched = $csched if ( not defined $sched ); $hum = $chum if ( not defined $hum ); $humsp = $chumsp if ( not defined $humsp ); - $dehumsp = $cdehumsp if ( not defined $dehumsp ); + $dehumsp = $cdehumsp if ( not defined $dehumsp ); if ( $cunits ne $units ) { main::print_log( "[Venstar Colortouch:" @@ -362,8 +369,11 @@ sub _push_JSON_data { if ( $chumsp ne $humsp ) { print - "[Venstar Colortouch] Changing Humidity Setpoint from $chumsp to $humsp\n"; + "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing Humidity Setpoint from $chumsp to $humsp\n"; $newhumsp = $humsp; + $humidity_change = 1; } else { $newhumsp = $chumsp; @@ -371,17 +381,25 @@ sub _push_JSON_data { if ( $cdehumsp ne $dehumsp ) { print - "[Venstar Colortouch] Changing Dehumidity Setpoint from $cdehumsp to $dehumsp\n"; + "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing (De)humidity Setpoint from $cdehumsp to $dehumsp\n"; $newdehumsp = $dehumsp; } else { $newdehumsp = $cdehumsp; } - $cmd = - "tempunits=$newunits&away=$newaway&schedule=$newsched&hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp"; - main::print_log( "Sending Settings command $cmd to " . $self->{host} ) - if $self->{debug}; + # v4.08 needs 6 % point delta on humidity and dehumidity. + # if humidify is more then dehumidify, then set dehumidify to 6 + humidity + $newdehumsp = $newhumsp + 6 if ((($newhumsp >= $newdehumsp) or (($newdehumsp - $newhumsp) < 6))); + if ($self->{api_ver} >=5) { + $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched"; #&dehum_setpoint=$newhumsp&hum_setpoint=" . ($newhumsp + 1); + $cmd = "hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp" if ($humidity_change); + } else { + $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched&hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp"; + } + main::print_log( "Sending Settings command [$cmd] to " . $self->{host} ) if $self->{debug}; } elsif ( $type eq 'control' ) { @@ -485,8 +503,7 @@ sub _push_JSON_data { $cmd = "mode=$newmode&fan=$newfan&heattemp=$newheatsp&cooltemp=$newcoolsp"; - main::print_log( "Sending Control command $cmd to " . $self->{host} ) - ; # if $self->{debug}; + main::print_log( "Sending Control command $cmd to " . $self->{host} ) if $self->{debug}; } else { @@ -653,7 +670,7 @@ sub print_info { . "] Device " . $self->{data}->{name} . " is " . $type - . " Thermostat" ); + . " Thermostat with API level " . $self->{api_ver} ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} @@ -700,7 +717,7 @@ sub print_info { . $self->{data}->{name} . "] Current Humidity:" . $self->{data}->{info}->{hum} - . "%" ); + . "%" ) if ($self->{api_ver} < 5); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} @@ -723,18 +740,20 @@ sub print_info { . "$unit\t" . $self->{data}->{info}->{cooltempmax} . "$unit" ); + my $hum_value = $self->{data}->{info}->{hum_setpoint}; + $hum_value = $self->{data}->{info}->{dehum_setpoint} if ($self->{api_ver} >= 5); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidity:\t" - . $self->{data}->{info}->{hum_setpoint} + . $hum_value . "%" ) - unless ( $self->{data}->{info}->{hum_setpoint} == 99 ); + unless ( $hum_value == 99 ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Dehumidity:" . $self->{data}->{info}->{dehum_setpoint} . "%" ) - unless ( $self->{data}->{info}->{dehum_setpoint} == 99 ); + unless (( $self->{data}->{info}->{dehum_setpoint} == 99 ) or ($self->{api_ver} >= 5)); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} @@ -839,7 +858,7 @@ sub process_data { ) if ( $self->{loglevel} ); $self->{previous}->{info}->{fan} = $self->{data}->{info}->{fan}; if ( defined $self->{child_object}->{fanstate} ) { - main::print_log "Child object found. Updating..." + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Fan Child object found. Updating..." if ( $self->{loglevel} ); $self->{child_object}->{fanstate} ->set_mode( $fan[ $self->{data}->{info}->{fan} ] ); @@ -856,7 +875,7 @@ sub process_data { $self->{previous}->{info}->{fanstate} = $self->{data}->{info}->{fanstate}; if ( defined $self->{child_object}->{fanstate} ) { - main::print_log "Child object found. Updating..." + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Fanstate Child object found. Updating..." if ( $self->{loglevel} ); $self->{child_object}->{fanstate} ->set( $fanstate[ $self->{data}->{info}->{fanstate} ], 'poll' ); @@ -898,7 +917,7 @@ sub process_data { $self->{previous}->{info}->{schedule} = $self->{data}->{info}->{schedule}; if ( defined $self->{child_object}->{sched} ) { - main::print_log "Child object found. Updating..." + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Schedule Child object found. Updating..." if ( $self->{loglevel} ); $self->{child_object}->{sched} ->set( $sched[ $self->{data}->{info}->{schedule} ], 'poll' ); @@ -937,7 +956,7 @@ sub process_data { $self->{previous}->{info}->{spacetemp} = $self->{data}->{info}->{spacetemp}; if ( defined $self->{child_object}->{temp} ) { - main::print_log "Child object found. Updating..." + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Temperature Sensor Child object found. Updating..." if ( $self->{loglevel} ); $self->{child_object}->{temp} ->set( $self->{data}->{info}->{spacetemp}, 'poll' ); @@ -1010,44 +1029,35 @@ sub process_data { $self->{data}->{info}->{cooltempmax}; } - if ( $self->{previous}->{info}->{dehum_setpoint} != - $self->{data}->{info}->{dehum_setpoint} ) - { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Thermostat dehumidity setpoint changed from $self->{previous}->{info}->{dehum_setpoint} to $self->{data}->{info}->{dehum_setpoint}" - ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{dehum_setpoint} = - $self->{data}->{info}->{dehum_setpoint}; - if ( defined $self->{child_object}->{dehum_sp} ) { - main::print_log "Child object found. Updating..." - if ( $self->{loglevel} ); - $self->{child_object}->{dehum_sp} - ->set( $self->{data}->{info}->{dehum_setpoint}, 'poll' ); + if ( $self->{previous}->{info}->{dehum_setpoint} != $self->{data}->{info}->{dehum_setpoint} ) { + if ($self->{api_ver} >=5) { #v5, dehum_setpoint is now the humidity setpoint? + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat humidity setpoint changed from $self->{previous}->{info}->{dehum_setpoint} to $self->{data}->{info}->{dehum_setpoint}") if ( $self->{loglevel} ); + $self->{previous}->{info}->{dehum_setpoint} = $self->{data}->{info}->{dehum_setpoint}; + if ( defined $self->{child_object}->{hum_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidify Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{hum_sp}->set( $self->{data}->{info}->{dehum_setpoint}, 'poll' ); + } + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat dehumidity setpoint changed from $self->{previous}->{info}->{dehum_setpoint} to $self->{data}->{info}->{dehum_setpoint}") if ( $self->{loglevel} ); + $self->{previous}->{info}->{dehum_setpoint} = $self->{data}->{info}->{dehum_setpoint}; + if ( defined $self->{child_object}->{dehum_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Dehumidify Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{dehum_sp}->set( $self->{data}->{info}->{dehum_setpoint}, 'poll' ); + } } } - if ( $self->{previous}->{info}->{hum_setpoint} != - $self->{data}->{info}->{hum_setpoint} ) - { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Thermostat humidity setpoint changed from $self->{previous}->{info}->{hum_setpoint} to $self->{data}->{info}->{hum_setpoint}" - ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{hum_setpoint} = - $self->{data}->{info}->{hum_setpoint}; - if ( defined $self->{child_object}->{hum_sp} ) { - main::print_log "Child object found. Updating..." - if ( $self->{loglevel} ); - $self->{child_object}->{hum_sp} - ->set( $self->{data}->{info}->{hum_setpoint}, 'poll' ); - } + if ( $self->{previous}->{info}->{hum_setpoint} != $self->{data}->{info}->{hum_setpoint} ) { + if ($self->{api_ver} < 5) { #v5, hum_setpoint is now the humidity sensor? + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat humidity setpoint changed from $self->{previous}->{info}->{hum_setpoint} to $self->{data}->{info}->{hum_setpoint}") if ( $self->{loglevel} ); + $self->{previous}->{info}->{hum_setpoint} = $self->{data}->{info}->{hum_setpoint}; + if ( defined $self->{child_object}->{hum_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidify Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{hum_sp} ->set( $self->{data}->{info}->{hum_setpoint}, 'poll' ); + } + } } - #if ($self->{previous}->{info}->{hum} != $self->{data}->{info}->{hum}) { - # main::print_log("[Venstar Colortouch:". $self->{data}->{name} . "] Thermostat humidity changed from $self->{previous}->{info}->{hum} to $self->{data}->{info}->{hum}") if ($self->{loglevel}); - # $self->{previous}->{info}->{hum} = $self->{data}->{info}->{hum}; - #} if ( $self->{previous}->{info}->{setpointdelta} != $self->{data}->{info}->{setpointdelta} ) @@ -1070,7 +1080,7 @@ sub process_data { $self->{previous}->{sensors}->{sensors}[0]->{hum} = $self->{data}->{sensors}->{sensors}[0]->{hum}; if ( defined $self->{child_object}->{hum} ) { - main::print_log "Child object found. Updating..." + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidity Sensor Child object found. Updating..." if ( $self->{loglevel} ); $self->{child_object}->{hum} ->set( $self->{data}->{sensors}->{sensors}[0]->{hum}, 'poll' ); @@ -1108,6 +1118,12 @@ sub print_runtimes { #------------ # User access methods +sub get_apiver { + my ($self) = @_; + + return ($self->{api_ver}); +} + sub get_mode { my ($self) = @_; my @mode; @@ -1181,11 +1197,14 @@ sub get_sp_cool { sub get_sp_hum { my ($self) = @_; - return ( $self->{data}->{info}->{hum_setpoint} ); + my $value = $self->{data}->{info}->{hum_setpoint}; + $value = $self->{data}->{info}->{dehum_setpoint} if ($self->{api_ver} >= 5); + return ( $value ); } sub get_sp_dehum { my ($self) = @_; + main::print_log("[Venstar_Colortouch]: WARNING, api v5 humidity settings are questionable.") if ($self->{api_ver} >= 5); return ( $self->{data}->{info}->{dehum_setpoint} ); } @@ -1462,6 +1481,7 @@ sub set_mode { sub set_away { + #I don't use this. Shouldn't just be this simple though. #($isSuccessResponse3,$status) = push_JSON_data($host,'settings','away=0&schedule=1'); } @@ -1506,8 +1526,6 @@ sub set_fan { sub set_units { - #($isSuccessResponse3,$status) = push_JSON_data($host,'settings','away=0&schedule=1'); - } sub set { From c413d4cb843bbb24ec305499fd9f6afd8d62320c Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 19 Sep 2016 10:08:06 -0400 Subject: [PATCH 045/209] Typo correction for examples --- lib/Venstar_Colortouch.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index b0e80a1e2..6aebcbe1e 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -15,7 +15,7 @@ use Data::Dumper; # $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); # $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); # $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); -# $stat_upper_sched = new Venstar_Colortouch_Sched($stat_upper); +# $stat_upper_sched = new Venstar_Colortouch_Schedule($stat_upper); # $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); # v1.1 - added in schedule and humidity control. From 81f903529cf2131c93dc9344c984c0f9f6047ca3 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 19 Sep 2016 10:18:33 -0400 Subject: [PATCH 046/209] Add mode, heat, cool setpoint child objects --- lib/Venstar_Colortouch.pm | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 6aebcbe1e..dbfaf6fc7 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -11,7 +11,10 @@ use Data::Dumper; # Venstar::Colortouch # $stat_upper = new Venstar_Colortouch('192.168.0.100'); # +# $stat_upper_mode = new Venstar_Colortouch_Mode($stat_upper); # $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); +# $stat_upper_heat_sp = new Venstar_Colortouch_Heat_sp($stat_upper); +# $stat_upper_cool_sp = new Venstar_Colortouch_Cool_sp($stat_upper); # $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); # $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); # $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); @@ -888,6 +891,12 @@ sub process_data { . "] Mode changed from $mode[$self->{previous}->{info}->{mode}] to $mode[$self->{data}->{info}->{mode}]" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{mode} = $self->{data}->{info}->{mode}; + if ( defined $self->{child_object}->{mode} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Mode Child object found. Updating..." + if ( $self->{loglevel} ); + $self->{child_object}->{mode} + ->set( $mode[$self->{data}->{info}->{mode}] ); + } } if ( $self->{previous}->{info}->{state} != $self->{data}->{info}->{state} ) @@ -972,6 +981,12 @@ sub process_data { ) if ( $self->{loglevel} ); $self->{previous}->{info}->{heattemp} = $self->{data}->{info}->{heattemp}; + if ( defined $self->{child_object}->{heat_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Heat Setpoint Child object found. Updating..." + if ( $self->{loglevel} ); + $self->{child_object}->{heat_sp} + ->set( $self->{data}->{info}->{heattemp}, 'poll' ); + } } if ( $self->{previous}->{info}->{heattempmin} != @@ -1005,6 +1020,12 @@ sub process_data { ) if ( $self->{loglevel} ); $self->{previous}->{info}->{cooltemp} = $self->{data}->{info}->{cooltemp}; + if ( defined $self->{child_object}->{cool_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Cooling Setpoint Child object found. Updating..." + if ( $self->{loglevel} ); + $self->{child_object}->{cool_sp} + ->set( $self->{data}->{info}->{cooltemp}, 'poll' ); + } } if ( $self->{previous}->{info}->{cooltempmin} != @@ -1742,4 +1763,112 @@ sub set { } } +package Venstar_Colortouch_Mode; + +@Venstar_Colortouch_Mode::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + + $object->register( $self, 'mode' ); + $self->set( $object->get_mode, 'poll' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( ( lc $p_state eq "cooling" ) or ( lc $p_state eq "heating" ) or ( lc $p_state eq "cool" ) or ( lc $p_state eq "heat" ) or ( lc $p_state eq "auto" ) or ( lc $p_state eq "off" ) ) { + $$self{master_object}->set_mode($p_state); + } + else { + main::print_log( + "[Venstar Colortouch Mode] Error. Unknown set state $p_state" + ); + } + } +} + +package Venstar_Colortouch_Heat_sp; + +@Venstar_Colortouch_Heat_sp::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + + $object->register( $self, 'heat_sp' ); + $self->set( $object->get_sp_heat, 'poll' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( ( $p_state >= 0 ) and ( $p_state <= 98 ) ) { + $$self{master_object}->set_heat_sp($p_state); + } + else { + main::print_log( + "[Venstar Colortouch Heat_SP] Error. Unknown set state $p_state" + ); + } + } +} + +package Venstar_Colortouch_Cool_sp; + +@Venstar_Colortouch_Cool_sp::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + + $object->register( $self, 'cool_sp' ); + $self->set( $object->get_sp_cool, 'poll' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( ( $p_state >= 0 ) and ( $p_state <= 98 ) ) { + $$self{master_object}->set_cool_sp($p_state); + } + else { + main::print_log( + "[Venstar Colortouch Cool_SP] Error. Unknown set state $p_state" + ); + } + } +} + 1; From abffc4ac4814fdeacf98bfc94197a70aa1b8fda7 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 19 Sep 2016 10:20:03 -0400 Subject: [PATCH 047/209] Neaten examples --- lib/Venstar_Colortouch.pm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index dbfaf6fc7..fa41ba403 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -9,16 +9,16 @@ use JSON::XS; use Data::Dumper; # Venstar::Colortouch -# $stat_upper = new Venstar_Colortouch('192.168.0.100'); +# $stat_upper = new Venstar_Colortouch('192.168.0.100'); # # $stat_upper_mode = new Venstar_Colortouch_Mode($stat_upper); -# $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); +# $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); # $stat_upper_heat_sp = new Venstar_Colortouch_Heat_sp($stat_upper); # $stat_upper_cool_sp = new Venstar_Colortouch_Cool_sp($stat_upper); -# $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); -# $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); -# $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); -# $stat_upper_sched = new Venstar_Colortouch_Schedule($stat_upper); +# $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); +# $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); +# $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); +# $stat_upper_sched = new Venstar_Colortouch_Schedule($stat_upper); # $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); # v1.1 - added in schedule and humidity control. From 914d40074b44b45549021daa1e2e889ca2a3c9b4 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 19 Sep 2016 10:24:05 -0400 Subject: [PATCH 048/209] Add debug control --- lib/Venstar_Colortouch.pm | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index fa41ba403..6b88ca1ba 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -59,7 +59,7 @@ $rest{control} = "/control"; $rest{settings} = "/settings"; sub new { - my ( $class, $host, $poll ) = @_; + my ( $class, $host, $poll, $debug ) = @_; my $self = {}; bless $self, $class; $self->{data} = undef; @@ -77,7 +77,10 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{host} = $host; - $self->{debug} = 5; + $self->{debug} = 0; + $self->{debug} = $debug if ($debug); + $self->{debug} = 0 + if ( $self->{debug} < 0 ); $self->{loglevel} = 1; $self->{status} = ""; $self->{timeout} = 15; #300; @@ -1310,6 +1313,11 @@ sub get_mode_humid { } +sub get_debug { + my ($self) = @_; + return $self->{debug}; +} + #------------ # User control methods #tempunits=0&away=0&schedule=0&hum_setpoint=0&dehum_setpoint=0 @@ -1549,6 +1557,14 @@ sub set_units { } + +sub set_debug { + my ( $self, $debug ) = @_; + $self->{debug} = $debug if ($debug); + $self->{debug} = 0 + if ( $self->{debug} < 0 ); +} + sub set { my ( $self, $p_state, $p_setby ) = @_; From 604618955dddcadda396d91c533325932ad42080 Mon Sep 17 00:00:00 2001 From: Pmatis Date: Mon, 19 Sep 2016 10:32:10 -0400 Subject: [PATCH 049/209] Add support for commercial versions andthe values that only commercial versions have --- lib/Venstar_Colortouch.pm | 326 +++++++++++++++++++++++++++++++++++--- 1 file changed, 308 insertions(+), 18 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 6b88ca1ba..91d81ff2b 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -58,6 +58,7 @@ $rest{alerts} = "query/alerts"; $rest{control} = "/control"; $rest{settings} = "/settings"; + sub new { my ( $class, $host, $poll, $debug ) = @_; my $self = {}; @@ -119,10 +120,13 @@ sub _init { if ($isSuccessResponse1) { $self->{api_ver} = $stat->{api_ver}; - if ( ( $self->{api_ver} > 3 ) and ( $stat->{type} eq "residential" ) ) { + + if ( ( $stat->{api_ver} > 3 ) and ( $stat->{type} eq "residential" or $stat->{type} eq "commercial" ) ) { + + $self->{type} = $stat->{type}; main::print_log( - "[Venstar Colortouch] Residental Venstar ColorTouch found with api level $self->{api_ver}" + "[Venstar Colortouch] $stat->{type} Venstar ColorTouch found with api level $stat->{api_ver}" ); if ( $self->poll() ) { main::print_log( "[Venstar Colortouch:" @@ -140,8 +144,8 @@ sub _init { $self->{previous}->{sensors}->{sensors}[0]->{hum} = $self->{data}->{sensors}->{sensors}[0]->{hum}; ## set states based on available mode - $self->print_info(); $self->set( $state[ $self->{data}->{info}->{state} ], 'poll' ); + $self->print_info(); } else { @@ -282,7 +286,7 @@ sub _get_JSON_data { . $self->{data}->{name} . "] Warning, not fetching data due to operation in progress" ); return ('0'); - } + } sub _push_JSON_data { @@ -318,17 +322,44 @@ sub _push_JSON_data { #tempunits=0&away=0&schedule=0&hum_setpoint=0&dehum_setpoint=0 # with v4.08 firmware, hum and dehum setpoints need to be at least 4 % points different - - my ( $newunits, $newaway, $newsched, $newhumsp, $newdehumsp ); + + my ( $isSuccessResponse, $thedata ) = $self->_get_setting_params; + my %info = %$thedata; + my %cinfo = %$thedata; + my @changearr; + + while ($params =~ /(\w+)=(\d+)/g) { + print "[Venstar Colortouch:" + . $self->{data}->{name} + . "] _push_JSON_data match: $1 = $2\n"; + $info{$1} = $2; + if ($info{$1} ne $cinfo{$1}) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing $1 from $cinfo{$1} to $info{$1}" ); + push(@changearr, "$1=$info{$1}"); + } + } + + $cmd = join ('&', @changearr); + main::print_log( "Sending Settings command $cmd to " . $self->{host} ) if $cmd + ; # if $self->{debug}; + + my ( $newunits, $newaway, $newholiday, $newsched, $newhumsp, $newdehumsp ); my ($units) = $params =~ /tempunits=(\d+)/; my ($away) = $params =~ /away=(\d+)/; + my ($holiday) = $params =~ /holiday=(\d+)/; + my ($override) = $params =~ /override=(\d+)/; + my ($overridetime) = $params =~ /overridetime=(\d+)/; + my ($forceunocc) = $params =~ /forceunocc=(\d+)/; my ($sched) = $params =~ /schedule=(\d+)/; my $hum; my ($humsp) = $params =~ /hum_setpoint=(\d+)/; my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/ ; #need to add in dehumidifier stuff at some point my $humidity_change = 0; - my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, + + my ( $isSuccessResponse, $cunits, $caway, $choliday, $coverride, $coverridetime, $cforceunocc, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; @@ -337,10 +368,14 @@ sub _push_JSON_data { $units = 0 if ( ( $units eq "F" ) or ( $units eq "f" ) ); $away = $caway if ( not defined $away ); - $sched = $csched if ( not defined $sched ); + $holiday = $choliday if ( not defined $holiday ); + $override = $coverride if ( not defined $override ); + $overridetime = $coverridetime if ( not defined $overridetime ); + $forceunocc = $cforceunocc if ( not defined $forceunocc ); + $sched = $csched if ( not defined $sched ); $hum = $chum if ( not defined $hum ); $humsp = $chumsp if ( not defined $humsp ); - $dehumsp = $cdehumsp if ( not defined $dehumsp ); + $dehumsp = $cdehumsp if ( not defined $dehumsp ); if ( $cunits ne $units ) { main::print_log( "[Venstar Colortouch:" @@ -362,6 +397,47 @@ sub _push_JSON_data { $newaway = $caway; } + if ( $choliday ne $holiday ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing Away from $choliday to $holiday" ); + $newholiday = $holiday; + } + else { + $newholiday = $choliday; + } + + if ( $caway ne $away ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing Away from $caway to $away" ); + $newaway = $away; + } + else { + $newaway = $caway; + } + + if ( $caway ne $away ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing Away from $caway to $away" ); + $newaway = $away; + } + else { + $newaway = $caway; + } + + if ( $caway ne $away ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Changing Away from $caway to $away" ); + $newaway = $away; + } + else { + $newaway = $caway; + } + + if ( $csched ne $sched ) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} @@ -452,7 +528,7 @@ sub _push_JSON_data { if ( $cfan ne $fan ) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} - . "] Changing fan from $fanstate[$cfan] to $fanstate[$fan]" ); + . "] Changing fan from $fan[$cfan] to $fan[$fan]" ); $newfan = $fan; } else { @@ -601,9 +677,9 @@ sub _get_control_params { sub _get_setting_params { my ($self) = @_; my ( $isSuccessResponse, $info ) = $self->_get_JSON_data('info'); - return ( $isSuccessResponse, $info->{tempunits}, $info->{away}, - $info->{schedule}, $info->{hum}, $info->{hum_setpoint}, - $info->{dehum_setpoint} ); + return ( $isSuccessResponse, $info); #->{tempunits}, $info->{away}, +# $info->{schedule}, $info->{hum}, $info->{hum_setpoint}, +# $info->{dehum_setpoint} ); } sub stop_timer { @@ -900,6 +976,8 @@ sub process_data { $self->{child_object}->{mode} ->set( $mode[$self->{data}->{info}->{mode}] ); } + + } if ( $self->{previous}->{info}->{state} != $self->{data}->{info}->{state} ) @@ -947,17 +1025,59 @@ sub process_data { $self->{data}->{info}->{schedulepart}; } - if ( $self->{previous}->{info}->{away} != $self->{data}->{info}->{away} ) { + if ( $self->{type} eq "residential" and $self->{previous}->{info}->{away} != $self->{data}->{info}->{away} ) { my $away = "home mode"; $away = "away mode" if ( $self->{data}->{info}->{away} ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} - . "] Thermostat occupency changed to" + . "] Thermostat occupancy changed to" . $away ) if ( $self->{loglevel} ); $self->{previous}->{info}->{away} = $self->{data}->{info}->{away}; } + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{holiday} != $self->{data}->{info}->{holiday} ) { + my $holiday = "observing holiday"; + $holiday = "no holiday" if ( $self->{data}->{info}->{holiday} ); + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat holiday changed to" + . $holiday ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{holiday} = $self->{data}->{info}->{holiday}; + } + + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{override} != $self->{data}->{info}->{override} ) { + my $override = "off"; + $override = "on" if ( $self->{data}->{info}->{override} ); + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat override changed to" + . $override ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{override} = $self->{data}->{info}->{override}; + } + + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{overridetime} != $self->{data}->{info}->{overridetime} ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat overridetime changed to" + . $self->{data}->{info}->{overridetime} ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{overridetime} = $self->{data}->{info}->{overridetime}; + } + + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{forceunocc} != $self->{data}->{info}->{forceunocc} ) { + my $forceunocc = "off"; + $forceunocc = "on" if ( $self->{data}->{info}->{forceunocc} ); + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat forceunocc changed to" + . $forceunocc ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{forceunocc} = $self->{data}->{info}->{forceunocc}; + } + if ( $self->{previous}->{info}->{spacetemp} != $self->{data}->{info}->{spacetemp} ) { @@ -1082,6 +1202,10 @@ sub process_data { } } + #if ($self->{previous}->{info}->{hum} != $self->{data}->{info}->{hum}) { + # main::print_log("[Venstar Colortouch:". $self->{data}->{name} . "] Thermostat humidity changed from $self->{previous}->{info}->{hum} to $self->{data}->{info}->{hum}") if ($self->{loglevel}); + # $self->{previous}->{info}->{hum} = $self->{data}->{info}->{hum}; + #} if ( $self->{previous}->{info}->{setpointdelta} != $self->{data}->{info}->{setpointdelta} ) @@ -1509,10 +1633,175 @@ sub set_mode { } sub set_away { + my ( $self, $value ) = @_; + return unless $self->{type} eq "residential"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown away mode $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'control', "away=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set away to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} - #I don't use this. Shouldn't just be this simple though. - #($isSuccessResponse3,$status) = push_JSON_data($host,'settings','away=0&schedule=1'); +sub set_holiday { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown holiday $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "holiday=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set holiday to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} +sub set_override { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown override $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "override=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set override to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} + +sub set_overridetime { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "overridetime=$value" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set overridetime to $value" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} + +sub set_forceunocc { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown forceunocc mode $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "forceunocc=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set forceunocc to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } } sub set_fan { @@ -1555,8 +1844,9 @@ sub set_fan { sub set_units { -} + #($isSuccessResponse3,$status) = push_JSON_data($host,'settings','away=0&schedule=1'); +} sub set_debug { my ( $self, $debug ) = @_; From c7a9812f0318dc21dd676a28765f9bf2e1e8136d Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Sun, 25 Sep 2016 14:49:49 -0600 Subject: [PATCH 050/209] Fixed jqCron needing 1-7 for DOW. added in conversion subroutine. Included Tobi's fixes for datepicker. --- web/ia7/include/javascript.js | 122 ++++++++++++++++++++++------------ web/ia7/include/jqCron.en.js | 4 +- 2 files changed, 82 insertions(+), 44 deletions(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 36c896fae..c06d61b55 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1690,8 +1690,10 @@ var object_history = function(items,start,days,time) { $('.update_history').click(function() { console.log ("start="+$('.hist_start').val()+" end="+$('.hist_end').val()); - var new_start = new Date($('.hist_start').val()).getTime(); - var new_end = new Date($('.hist_end').val()).getTime(); +// var new_start = new Date($('.hist_start').val()).getTime(); +// var new_end = new Date($('.hist_end').val()).getTime(); + var new_start = new Date($('.hist_start').val().split('-')).getTime(); + var new_end = new Date($('.hist_end').val().split('-')).getTime(); var end_days = (new_start - new_end) / (24 * 60 * 60 * 1000) new_start = new_start / 1000; object_history(items,new_start,end_days); @@ -1722,15 +1724,18 @@ var object_history = function(items,start,days,time) { for (var i = 0; i < json.data.data.length; i++) { if (json.data.data[i].label == key) { data.push(json.data.data[i]); - return true; + return true; } } }); // take away the border so that it looks better and span the graph from start to end. json.data.options.grid.borderWidth = 0; - json.data.options.xaxis.min = new Date($('.hist_end').val()).getTime(); - json.data.options.xaxis.max = new Date($('.hist_start').val()).getTime() + (24 * 60 * 60 * 1000); +// json.data.options.xaxis.min = new Date($('.hist_end').val()).getTime(); +// json.data.options.xaxis.max = new Date($('.hist_start').val()).getTime() + (24 * 60 * 60 * 1000); + json.data.options.xaxis.min = new Date($('.hist_end').val().split('-')).getTime(); + json.data.options.xaxis.max = new Date($('.hist_start').val().split('-')).getTime() + (24 * 60 * 60 * 1000); + //console.log("data="+JSON.stringify(data)); //console.log("xmin="+json.data.options.xaxis.min+" xmax="+json.data.options.xaxis.max); $.plot($("#hist-graph"), data, json.data.options); @@ -1833,12 +1838,13 @@ var fp_display_height=0; // updated by fp_resize_floorplan_image var fp_scale = 100; // updated by fp_reposition_entities var fp_grabbed_entity = null; // store item for drag & drop var fp_icon_select_item_id = null; // store item id on right click for icon set selection +var fp_icon_image_size = 48; var noDragDrop = function() { return false; }; -var fp_getOrCreateIcon = function (json, entity, i, coords, show_pos){ +var fp_getOrCreateIcon = function (json, entity, i, coords){ var popover = 0; if ((json.data[entity].type === "FPCamera_Item") || (json_store.ia7_config.prefs.fp_state_popovers === "yes")) popover = 1; @@ -1853,7 +1859,7 @@ var fp_getOrCreateIcon = function (json, entity, i, coords, show_pos){ ''+ + '>'+ ''; if (coords !== ""){ $('#graphic').append(html); @@ -1866,7 +1872,7 @@ var fp_getOrCreateIcon = function (json, entity, i, coords, show_pos){ E.bind("dragstart", noDragDrop); var image = get_fp_image(json.data[entity]); E.attr('src',"/ia7/graphics/"+image); - if (show_pos) + if (developer) E.css("border","1px solid black"); return E; @@ -1884,7 +1890,8 @@ var fp_resize_floorplan_image = function(){ var fp_reposition_entities = function(){ var t0 = performance.now(); - var offset = $("#fp_graphic").offset(); + var fp_graphic_offset = $("#fp_graphic").offset(); + console.log("fp_graphic_offset: "+ JSON.stringify(fp_graphic_offset)); var width = fp_display_width; var hight = fp_display_height; var onePercentWidthInPx = width/100; @@ -1892,8 +1899,8 @@ var fp_reposition_entities = function(){ var fp_get_offset_from_location = function(item) { var y = item[0]; var x = item[1]; - var newy = offset.top + y * onePercentHeightInPx; - var newx = offset.left + x * onePercentWidthInPx; + var newy = fp_graphic_offset.top + y * onePercentHeightInPx; + var newx = fp_graphic_offset.left + x * onePercentWidthInPx; return { "top": newy, "left": newx @@ -1918,21 +1925,22 @@ var fp_reposition_entities = function(){ } else { nwidth = 790; } - - fp_scale = Math.round( width/nwidth * 100); + var fp_scale = width/nwidth; + var fp_scale_percent = Math.round( fp_scale * 100); - console.log("width="+width+" nwidth="+nwidth+" scale="+fp_scale); + console.log("width="+width+" nwidth="+nwidth+" scale="+fp_scale_percent); // update the location of all the objects... $(".floorplan_item").each(function(index) { var classstr = $(this).attr("class"); var coords = classstr.split(/coords=/)[1]; - $(this).width(fp_scale + "%"); + $(this).width(fp_scale_percent + "%"); if (coords.length === 0){ return; } var fp_location = coords.split(/x/); var fp_offset = fp_get_offset_from_location(fp_location); + console.log("coords="+coords); // this seems to make the repositioning slow // ~ 300+ms on my nexus7 firefox-beta vs <100ms with this code commented out @@ -1942,13 +1950,14 @@ var fp_reposition_entities = function(){ // } else { // $(this).attr('src',$(this).attr('src').replace('32.png','48.png')); // } - - var adjust = $(this).width()/2; + var element_id = $(this).attr('id'); + var adjust = fp_icon_image_size*fp_scale/2; + console.log("adjust="+adjust+" fp_offset.top="+fp_offset.top+" fp_offset.left="+fp_offset.left); var fp_off_center = { "top": fp_offset.top - adjust, "left": fp_offset.left - adjust }; - fp_set_pos($(this).attr('id'), fp_off_center); + fp_set_pos(element_id, fp_off_center); }); $('.icon_select img').each(function(){ @@ -1962,7 +1971,23 @@ var fp_set_pos = function(id, offset){ var item = $('#' + id); // do not move the span, this make the popup to narrow somehow // item.closest("span").offset(offset); + var left11 = item.css("left"); + var left12 = item[0].style.left; + var top11 = item.css("top"); + var top12 = item[0].style.top; + var before = item.offset(); + var init = false + if (item.css("left") == "auto") { + console.log("auto found, fixing left property"); + offset.left = 0 - fp_icon_image_size/2; + } item.offset(offset); + var after = item.offset(); + var left21 = item.css("left"); + var left22 = item[0].style.left; + console.log("offset.top="+offset.top+" offset.left="+offset.left+" before.top="+before.top+" before.left="+before.left+" after.top="+after.top+" after.left="+after.left); + console.log("top11="+top11+" top12="+top12); + console.log("left11="+left11+" left12="+left12+" left21="+left21+" left22="+left22); }; var fp_is_point_on_fp = function (p){ @@ -2002,7 +2027,7 @@ var floorplan = function(group,time) { $('#floorplan').append("
    "); time = 0; $('#graphic').prepend('
    '); - if (URLHash.show_pos){ + if (developer){ $('#fp_graphic').css("border","1px solid black"); $('#list_content').append("
    "); $('#list_content').append("
    ");
    @@ -2062,14 +2087,14 @@ var floorplan = function(group,time) {
                 if (fp_grabbed_entity === null)
                     return;
     
    -            set_set_coordinates_from_offset(fp_grabbed_entity.id);
    +            set_coordinates_from_offset(fp_grabbed_entity.id);
                 fp_reposition_entities();
                 fp_grabbed_entity = null;
             });
     
         }
     
    -    var set_set_coordinates_from_offset = function (id)
    +    var set_coordinates_from_offset = function (id)
         {
             var E = $('#'+id);
             var offsetE = E.offset();
    @@ -2166,7 +2191,7 @@ var floorplan = function(group,time) {
                     var t0 = performance.now();
                     JSONStore(json);
                     for (var entity in json.data) {
    -                    if (URLHash.show_pos && requestTime === 0){
    +                    if (developer && requestTime === 0){
                             perl_pos_coords = "";
                         }
                         for (var i=0 ; i < json.data[entity].fp_location.length-1; i=i+2){ //allow for multiple graphics
    @@ -2174,7 +2199,7 @@ var floorplan = function(group,time) {
                             if ((json.data[entity].type === "FPCamera_Item") || (json_store.ia7_config.prefs.fp_state_popovers === "yes"))
                                 popover = 1;
     
    -                        if (URLHash.show_pos && requestTime === 0){
    +                        if (developer && requestTime === 0){
                                 if (perl_pos_coords.length !== 0){
                                     perl_pos_coords += ", ";
                                 }
    @@ -2182,9 +2207,9 @@ var floorplan = function(group,time) {
                             }
     
                             var coords= json.data[entity].fp_location[i]+'x'+json.data[entity].fp_location[i+1];
    -                        var E = fp_getOrCreateIcon(json, entity, i, coords, URLHash.show_pos);
    +                        var E = fp_getOrCreateIcon(json, entity, i, coords, developer);
     
    -                        if (URLHash.show_pos === undefined)
    +                        if (developer === false)
                             {
                                 // create unique popovers for Camera items
                                 if (json.data[entity].type === "FPCamera_Item") {
    @@ -2271,10 +2296,10 @@ var floorplan = function(group,time) {
                             }
                         }
     
    -                    if (URLHash.show_pos && requestTime === 0){
    +                    if (developer && requestTime === 0){
                             if (perl_pos_coords.length===0)
                             {
    -                            fp_getOrCreateIcon(json, entity, 0, "", URLHash.show_pos);
    +                            fp_getOrCreateIcon(json, entity, 0, "");
                             }
                             else{
                                 var oldCode = $('#fp_pos_perl_code').text();
    @@ -2296,7 +2321,7 @@ var floorplan = function(group,time) {
                         }
                     }
                     fp_reposition_entities();
    -                if (requestTime === 0 && URLHash.show_pos){
    +                if (requestTime === 0 && developer){
                         $('#list_content').append("

     

    "); $.ajax({ type: "GET", @@ -2392,7 +2417,7 @@ var floorplan = function(group,time) { if ($('#floorplan').length !== 0){ //If the floorplan page is still active request more data // and we are not editing the fp - if (URLHash.show_pos === undefined) + if (developer === false) floorplan(group,requestTime); } } @@ -2403,14 +2428,13 @@ var get_fp_image = function(item,size,orientation) { var image_name; var image_color = getButtonColor(item.state); var baseimg_width = $(window).width(); - var image_size = "48"; - // if (baseimg_width < 500) image_size = "32" // iphone scaling - //kvar image_size = "32" + // if (baseimg_width < 500) fp_icon_image_size = "32" // iphone scaling + //kvar fp_icon_image_size = "32" if (item.fp_icons !== undefined) { if (item.fp_icons[item.state] !== undefined) return item.fp_icons[item.state]; } if (item.fp_icon_set !== undefined) { - return "fp_"+item.fp_icon_set+"_"+image_color+"_"+image_size+".png"; + return "fp_"+item.fp_icon_set+"_"+image_color+"_"+fp_icon_image_size+".png"; } // if item.fp_icons.return item.fp_icons[state]; if(item.type === "Light_Item" || item.type === "Fan_Light" || @@ -2422,25 +2446,25 @@ var get_fp_image = function(item,size,orientation) { item.type === "UIO_Item" || item.type === "X10_Item" || item.type === "xPL_Plugwise" || item.type === "X10_Appliance") { - return "fp_light_"+image_color+"_"+image_size+".png"; + return "fp_light_"+image_color+"_"+fp_icon_image_size+".png"; } if(item.type === "Motion_Item" || item.type === "X10_Sensor" || item.type === "Insteon::MotionSensor" ) { - return "fp_motion_"+image_color+"_"+image_size+".png"; + return "fp_motion_"+image_color+"_"+fp_icon_image_size+".png"; } if(item.type === "Door_Item" || item.type === "Insteon::IOLinc_door") { - return "fp_door_"+image_color+"_"+image_size+".png"; + return "fp_door_"+image_color+"_"+fp_icon_image_size+".png"; } if(item.type === "FPCamera_Item" ) { - return "fp_camera_default_"+image_size+".png"; + return "fp_camera_default_"+fp_icon_image_size+".png"; } - return "fp_unknown_info_"+image_size+".png"; + return "fp_unknown_info_"+fp_icon_image_size+".png"; }; var create_img_popover = function(entity) { @@ -2530,7 +2554,18 @@ var create_state_modal = function(entity) { $('#control').find('.modal-footer').find('.sched_submit').remove(); // Unique Schedule Data here if (json_store.objects[entity].schedule !== undefined) { - + + var modify_jqcon_dow = function(cronstr,offset) { + var cron = cronstr.split(/\s+/); + console.log("dow="+cron[cron.length-1]); + cron[cron.length-1] = cron[cron.length-1].replace(/\d/gi, function adjust(x) { + console.log("x="+x+" offset="+offset); + return parseInt(x) + parseInt(offset); + });; + console.log("dow="+cron[cron.length-1]); + return cron.join(" "); + } + var add_schedule = function(index,cron,label,state_sets) { if (cron === null) return; if (label == undefined) label = index; @@ -2609,7 +2644,7 @@ var create_state_modal = function(entity) { console.log("schedule.length="+json_store.objects[entity].schedule.length); for (var i = 1; i < json_store.objects[entity].schedule.length; i++){ var sched_index = json_store.objects[entity].schedule[i][0]; - var sched_cron = json_store.objects[entity].schedule[i][1]; + var sched_cron = modify_jqcon_dow(json_store.objects[entity].schedule[i][1],1); var sched_label = json_store.objects[entity].schedule[i][2]; console.log("si="+sched_index+",sc="+sched_cron+",sl="+sched_label+",ss="+sched_states); add_schedule(sched_index,sched_cron,sched_label,sched_states); @@ -2633,7 +2668,10 @@ var create_state_modal = function(entity) { if ($(this).hasClass("disabled")) return; var string = ""; $('.mhsched').each(function(index,value) { - string += $( this ).attr("id") + ',"' + $( this ).text() + '",' + $( this ).attr("label") + ','; + console.log("string="+string); +// string += $( this ).attr("id") + ',"' + $( this ).text() + '",' + $( this ).attr("label") + ','; + string += $( this ).attr("id") + ',"' + modify_jqcon_dow($(this).text(),"-1") + '",' + $( this ).attr("label") + ','; + console.log("string="+string); }); string = string.replace(/,\s*$/, ""); //remove the last comma var url="/SUB?ia7_update_schedule"+encodeURI("("+$(this).parents('.control-dialog').attr("entity")+","+string+")"); @@ -3019,4 +3057,4 @@ $(document).ready(function() { // // 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. -// +// \ No newline at end of file diff --git a/web/ia7/include/jqCron.en.js b/web/ia7/include/jqCron.en.js index 779d36836..9823c5bd4 100644 --- a/web/ia7/include/jqCron.en.js +++ b/web/ia7/include/jqCron.en.js @@ -34,7 +34,7 @@ jqCronDefaultSettings.texts.en = { error2: 'Bad number of elements', error3: 'The jquery_element should be set into jqCron settings', error4: 'Unrecognized expression', - weekdays_short: ['M','Tu','W','Th','F','Sa','Su'], - weekdays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], + weekdays_short: ['Su','M','Tu','W','Th','F','Sa'], + weekdays: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'], months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] }; From 30e303a3003cb92a38f931d03330631b974d84b9 Mon Sep 17 00:00:00 2001 From: hplato Date: Sun, 25 Sep 2016 15:03:28 -0600 Subject: [PATCH 051/209] Added in workaround for firmware 4.08 humidity bug --- lib/Venstar_Colortouch.pm | 194 +++++++++++++++++++------------------- 1 file changed, 99 insertions(+), 95 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 91d81ff2b..289182992 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,4 +1,5 @@ package Venstar_Colortouch; +# v1.4.1 use strict; use warnings; @@ -19,7 +20,7 @@ use Data::Dumper; # $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); # $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); # $stat_upper_sched = new Venstar_Colortouch_Schedule($stat_upper); -# $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); +# $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); # v1.1 - added in schedule and humidity control. # v1.2 - added communication tracker object & timeout control @@ -28,6 +29,7 @@ use Data::Dumper; # "hum_setpoint": is the current humidity and # "dehum_setpoint": is the humidity setpoint # "hum" doesn't return anything anymore. +# v1.4.1 - API v5, working schedule, humidity setpoints # Notes: # - Best to use firmware at least 3.14 released Nov 2014. This fixes issues with both @@ -40,8 +42,8 @@ use Data::Dumper; # - figure out timezone w/ DST # - changing heating/cooling setpoints for heating/cooling only stats does not need both setpoints # - add decimals for setpoints -# # make the data poll non-blocking, turn off timer -# +# - make the data poll non-blocking, turn off timer + # State can only be set by stat. Set mode will change the mode. @Venstar_Colortouch::ISA = ('Generic_Item'); @@ -286,13 +288,13 @@ sub _get_JSON_data { . $self->{data}->{name} . "] Warning, not fetching data due to operation in progress" ); return ('0'); - + } } sub _push_JSON_data { my ( $self, $type, $params ) = @_; - my ( @fan, @fanstate, @modename, @statename, @schedule, @home ); + my ( @fan, @fanstate, @modename, @statename, @schedule, @home, @schedulestat ); $fan[0] = "auto"; $fan[1] = "on"; $fanstate[0] = "off"; @@ -313,6 +315,10 @@ sub _push_JSON_data { $schedule[2] = "evening (occupied3)"; $schedule[3] = "night (occupied4)"; $schedule[255] = "inactive"; + $schedulestat[0] = "off"; + $schedulestat[1] = "on"; + +#4.08, schedulepart is now the schedule type. schedule 0 is off, my $cmd; @@ -320,30 +326,29 @@ sub _push_JSON_data { $self->stop_timer; #stop timer to prevent a clash of updates if ( $type eq 'settings' ) { - #tempunits=0&away=0&schedule=0&hum_setpoint=0&dehum_setpoint=0 - # with v4.08 firmware, hum and dehum setpoints need to be at least 4 % points different - - my ( $isSuccessResponse, $thedata ) = $self->_get_setting_params; - my %info = %$thedata; - my %cinfo = %$thedata; - my @changearr; - - while ($params =~ /(\w+)=(\d+)/g) { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] _push_JSON_data match: $1 = $2\n"; - $info{$1} = $2; - if ($info{$1} ne $cinfo{$1}) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing $1 from $cinfo{$1} to $info{$1}" ); - push(@changearr, "$1=$info{$1}"); - } - } + #for testing purposes, curl is: + #curl --data "tempunits=1&away=0&schedule=0&hum_setpoint=30&dehum_setpoint=35" http://ip/settings - $cmd = join ('&', @changearr); - main::print_log( "Sending Settings command $cmd to " . $self->{host} ) if $cmd - ; # if $self->{debug}; +# my ( $isSuccessResponse, $thedata ) = $self->_get_setting_params; +# my %info = %$thedata; +# my %cinfo = %$thedata; +# my @changearr; +# +# while ($params =~ /(\w+)=(\d+)/g) { +# print "[Venstar Colortouch:" +# . $self->{data}->{name} +# . "] _push_JSON_data match: $1 = $2\n"; +# $info{$1} = $2; +# if ($info{$1} ne $cinfo{$1}) { +# main::print_log( "[Venstar Colortouch:" +# . $self->{data}->{name} +# . "] Changing $1 from $cinfo{$1} to $info{$1}" ); +# push(@changearr, "$1=$info{$1}"); +# } +# } + +# $cmd = join ('&', @changearr); +# main::print_log( "Sending Settings command $cmd to " . $self->{host} ) if $cmd; # if $self->{debug}; my ( $newunits, $newaway, $newholiday, $newsched, $newhumsp, $newdehumsp ); my ($units) = $params =~ /tempunits=(\d+)/; @@ -359,10 +364,9 @@ sub _push_JSON_data { ; #need to add in dehumidifier stuff at some point my $humidity_change = 0; - my ( $isSuccessResponse, $cunits, $caway, $choliday, $coverride, $coverridetime, $cforceunocc, $csched, $chum, $chumsp, - $cdehumsp ) - = $self->_get_setting_params; - + my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; +#check why is this called twice? + my ($choliday,$coverride,$coverridetime,$cforceunocc); $units = $cunits if ( not defined $units ); $units = 1 if ( ( $units eq "C" ) or ( $units eq "c" ) ); $units = 0 if ( ( $units eq "F" ) or ( $units eq "f" ) ); @@ -374,8 +378,21 @@ sub _push_JSON_data { $forceunocc = $cforceunocc if ( not defined $forceunocc ); $sched = $csched if ( not defined $sched ); $hum = $chum if ( not defined $hum ); - $humsp = $chumsp if ( not defined $humsp ); - $dehumsp = $cdehumsp if ( not defined $dehumsp ); + #v4.08, dehum_sp is humidify, and hum_sp is dehumidify. + if ($self->{api_ver} >=5) { + $humsp = $cdehumsp if ( not defined $humsp ); + } else { + $humsp = $chumsp if ( not defined $humsp ); + } + if ($self->{api_ver} >=5) { + $dehumsp = $chumsp if ( not defined $dehumsp ); + } else { + $dehumsp = $cdehumsp if ( not defined $dehumsp ); + } +print "venstar db: params = $params\n"; +print "units=$units, away=$away, sched=$sched, hum=$hum, humsp=$humsp, dehumsp=$dehumsp\n"; +print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, cdehumsp=$cdehumsp\n"; + if ( $cunits ne $units ) { main::print_log( "[Venstar Colortouch:" @@ -417,70 +434,57 @@ sub _push_JSON_data { $newaway = $caway; } - if ( $caway ne $away ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Away from $caway to $away" ); - $newaway = $away; - } - else { - $newaway = $caway; - } - - if ( $caway ne $away ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Away from $caway to $away" ); - $newaway = $away; - } - else { - $newaway = $caway; - } - - if ( $csched ne $sched ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Schedule from $schedule[$csched] to $schedule[$sched]" - ); + if ($self->{api_ver} >=5) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Schedule from $schedulestat[$csched] to $schedulestat[$sched]"); + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Schedule from $schedule[$csched] to $schedule[$sched]"); + } $newsched = $sched; } else { $newsched = $csched; } - - if ( $chumsp ne $humsp ) { - print - "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Humidity Setpoint from $chumsp to $humsp\n"; - $newhumsp = $humsp; - $humidity_change = 1; - } - else { - $newhumsp = $chumsp; - } - - if ( $cdehumsp ne $dehumsp ) { - print - "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing (De)humidity Setpoint from $cdehumsp to $dehumsp\n"; - $newdehumsp = $dehumsp; - } - else { - $newdehumsp = $cdehumsp; - } - - # v4.08 needs 6 % point delta on humidity and dehumidity. - # if humidify is more then dehumidify, then set dehumidify to 6 + humidity - $newdehumsp = $newhumsp + 6 if ((($newhumsp >= $newdehumsp) or (($newdehumsp - $newhumsp) < 6))); + if ($self->{api_ver} >=5) { + if ( $cdehumsp ne $humsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Humidity Setpoint from $cdehumsp to $humsp\n" ); + $newhumsp = $humsp; + $humidity_change = 1; + } else { + $newhumsp = $cdehumsp; + } + + if ( $chumsp ne $dehumsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] *Changing Dehumidity Setpoint from $chumsp to $dehumsp\n" ); + $newdehumsp = $dehumsp; + } else { + $newdehumsp = $chumsp; + } + } else { + if ( $chumsp ne $humsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Humidity Setpoint from $chumsp to $humsp\n" ); + $newhumsp = $humsp; + $humidity_change = 1; + } else { + $newhumsp = $chumsp; + } + + if ( $cdehumsp ne $dehumsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Dehumidity Setpoint from $cdehumsp to $dehumsp\n" ); + $newdehumsp = $dehumsp; + } else { + $newdehumsp = $cdehumsp; + } + } + +## to set v4.08, humidity to 32%, this is needed "tempunits=1&away=0&schedule=0&hum_setpoint=32&dehum_setpoint=38" +## so humidity setpoint hasn't changed? if ($self->{api_ver} >=5) { - $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched"; #&dehum_setpoint=$newhumsp&hum_setpoint=" . ($newhumsp + 1); - $cmd = "hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp" if ($humidity_change); - } else { - $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched&hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp"; - } + # v4.08 has changed humidification settings + # - need 6 % point delta on humidity and dehumidity. + $newdehumsp = $newhumsp + 6 if ((($newhumsp >= $newdehumsp) or (($newdehumsp - $newhumsp) < 6))); + } + $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched&hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp"; main::print_log( "Sending Settings command [$cmd] to " . $self->{host} ) if $self->{debug}; } @@ -677,9 +681,9 @@ sub _get_control_params { sub _get_setting_params { my ($self) = @_; my ( $isSuccessResponse, $info ) = $self->_get_JSON_data('info'); - return ( $isSuccessResponse, $info); #->{tempunits}, $info->{away}, -# $info->{schedule}, $info->{hum}, $info->{hum_setpoint}, -# $info->{dehum_setpoint} ); + return ( $isSuccessResponse, $info->{tempunits}, $info->{away}, + $info->{schedule}, $info->{hum}, $info->{hum_setpoint}, + $info->{dehum_setpoint} ); } sub stop_timer { @@ -2177,4 +2181,4 @@ sub set { } } -1; +1; \ No newline at end of file From cc40b8d7b33ec882c4e0e86de90daec941c4d60a Mon Sep 17 00:00:00 2001 From: hplato Date: Mon, 26 Sep 2016 20:14:09 -0600 Subject: [PATCH 052/209] v2.0 - Initial background support Added ability to background web requests to reduce pauses. --- lib/Venstar_Colortouch.pm | 332 ++++++++++++++++++++++---------------- 1 file changed, 189 insertions(+), 143 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 289182992..37172d999 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,5 +1,5 @@ package Venstar_Colortouch; -# v1.4.1 +# v2.0 use strict; use warnings; @@ -30,6 +30,7 @@ use Data::Dumper; # "dehum_setpoint": is the humidity setpoint # "hum" doesn't return anything anymore. # v1.4.1 - API v5, working schedule, humidity setpoints +# v2.0 - Background process # Notes: # - Best to use firmware at least 3.14 released Nov 2014. This fixes issues with both @@ -65,31 +66,36 @@ sub new { my ( $class, $host, $poll, $debug ) = @_; my $self = {}; bless $self, $class; - $self->{data} = undef; - $self->{api_ver} = 0; - $self->{child_object} = undef; - $self->{config}->{cache_time} = 30; #TODO fix cache timeouts - $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} - if defined $::config_params{venstar_config_cache_time}; - $self->{config}->{tz} = - $::config_params{time_zone}; #TODO Need to figure out DST for print runtimes + $self->{data} = undef; + $self->{api_ver} = 0; + $self->{child_object} = undef; + $self->{config}->{cache_time} = 30; #TODO fix cache timeouts + $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} if defined $::config_params{venstar_config_cache_time}; + $self->{config}->{tz} =$::config_params{time_zone}; #TODO Need to figure out DST for print runtimes $self->{config}->{poll_seconds} = 60; $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 - if ( $self->{config}->{poll_seconds} < 1 ); - $self->{updating} = 0; - $self->{data}->{retry} = 0; - $self->{host} = $host; - $self->{debug} = 0; - $self->{debug} = $debug if ($debug); - $self->{debug} = 0 - if ( $self->{debug} < 0 ); - $self->{loglevel} = 1; - $self->{status} = ""; - $self->{timeout} = 15; #300; - + $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{updating} = 0; + $self->{data}->{retry} = 0; + $self->{host} = $host; + $self->{debug} = 5; + $self->{debug} = $debug if ($debug); + $self->{debug} = 0 if ( $self->{debug} < 0 ); + $self->{loglevel} = 1; + $self->{status} = ""; + $self->{timeout} = 15; #300; + $self->{background} = 1; + $self->{max_poll_queue} = 3; + if ($self->{background}) { + @{$self->{command_queue}} = (); + $self->{poll_data_file} = "$::config_parms{data_dir}/venstar_poll_" . $self->{host} . ".data"; + unlink "$::config_parms{data_dir}/venstar_poll_" . $self->{host} . ".data"; + $self->{poll_process} = new Process_Item; + $self->{poll_process}->set_output($self->{poll_data_file}); + &::MainLoop_post_add_hook( \&Venstar_Colortouch::process_check, 0, $self ); + } + $self->{timer} = new Timer; $self->_init; - $self->{timer} = new Timer; $self->start_timer; return $self; } @@ -107,7 +113,7 @@ sub get_data { #main::print_log("[Venstar Colortouch] get_data initiated"); $self->poll; - $self->process_data; + $self->process_data unless ($self->{background}); #for background tasks, data will be processed when process completed. } sub _init { @@ -118,7 +124,7 @@ sub _init { $state[2] = "cooling"; $state[3] = "lockout"; $state[4] = "error"; - my ( $isSuccessResponse1, $stat ) = $self->_get_JSON_data('api'); + my ( $isSuccessResponse1, $stat ) = $self->_get_JSON_data('api',"direct"); if ($isSuccessResponse1) { $self->{api_ver} = $stat->{api_ver}; @@ -127,63 +133,60 @@ sub _init { $self->{type} = $stat->{type}; - main::print_log( - "[Venstar Colortouch] $stat->{type} Venstar ColorTouch found with api level $stat->{api_ver}" - ); - if ( $self->poll() ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Data Successfully Retrieved" ); + main::print_log("[Venstar Colortouch] $stat->{type} Venstar ColorTouch found with api level $stat->{api_ver}"); + if ( $self->poll("direct") ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Data Successfully Retrieved" ); $self->{active} = 1; $self->{previous}->{tempunits} = $self->{data}->{tempunits}; $self->{previous}->{name} = $self->{data}->{name}; foreach my $key1 ( keys $self->{data}->{info} ) { - $self->{previous}->{info}->{$key1} = - $self->{data}->{info}->{$key1}; + $self->{previous}->{info}->{$key1} = $self->{data}->{info}->{$key1}; } - $self->{previous}->{sensors}->{sensors}[0]->{temp} = - $self->{data}->{sensors}->{sensors}[0]->{temp}; - $self->{previous}->{sensors}->{sensors}[0]->{hum} = - $self->{data}->{sensors}->{sensors}[0]->{hum}; + $self->{previous}->{sensors}->{sensors}[0]->{temp} = $self->{data}->{sensors}->{sensors}[0]->{temp}; + $self->{previous}->{sensors}->{sensors}[0]->{hum} = $self->{data}->{sensors}->{sensors}[0]->{hum}; ## set states based on available mode - $self->set( $state[ $self->{data}->{info}->{state} ], 'poll' ); +### Strange, if this set is here, then the timer is not defined. + #print "db: set " . $state[ $self->{data}->{info}->{state} ] . "=" . $self->{data}->{info}->{state} . "\n"; + #$self->set( $state[ $self->{data}->{info}->{state} ], 'poll' ); $self->print_info(); } else { - main::print_log( - "[Venstar Colortouch] Problem retrieving initial data"); + main::print_log("[Venstar Colortouch] Problem retrieving initial data"); $self->{active} = 0; return ('1'); } } else { - main::print_log( - "[Venstar Colortouch] Unknown device " . $self->{host} ); + main::print_log("[Venstar Colortouch] Unknown device " . $self->{host} ); $self->{active} = 0; return ('1'); } } else { - main::print_log( "[Venstar Colortouch] Error. Unable to connect to " - . $self->{host} ); + main::print_log( "[Venstar Colortouch] Error. Unable to connect to " . $self->{host} ); $self->{active} = 0; return ('1'); } } sub poll { - my ($self) = @_; - - main::print_log("[Venstar Colortouch] Polling initiated") - if ( $self->{debug} ); - - my ( $isSuccessResponse1, $info ) = $self->_get_JSON_data('info'); - my ( $isSuccessResponse2, $sensors ) = $self->_get_JSON_data('sensors'); + my ($self,$method) = @_; + $method = "" unless (defined $method); + if (($self->{background}) and (lc $method ne "direct")) { + main::print_log("[Venstar Colortouch] Background Polling initiated") if ( $self->{debug} ); + } else { + main::print_log("[Venstar Colortouch] Direct Polling initiated") if ( $self->{debug} ); + } + #spawn off info. + #might had to add into the main loop to check if the object's data has changed. + #$self->{process_pid}->{{$self->{process}->pid} = "poll_info"; + my ( $isSuccessResponse1, $info ) = $self->_get_JSON_data('info',$method); + my ( $isSuccessResponse2, $sensors ) = $self->_get_JSON_data('sensors',$method); - if ( $isSuccessResponse1 and $isSuccessResponse2 ) { + if (( $isSuccessResponse1 and $isSuccessResponse2 ) and ((!$self->{background}) or (lc $method eq "direct"))) { $self->{data}->{tempunits} = $info->{tempunits}; $self->{data}->{name} = $info->{name}; $self->{data}->{info} = $info; @@ -191,103 +194,139 @@ sub poll { $self->{data}->{timestamp} = time; $self->{data}->{retry} = 0; - #if (defined $self->{child_object}->{comm}) { - # if ($self->{child_object}->{comm}->state() ne "online") { - # main::print_log "[Venstar Colortouch:". $self->{data}->{name} . "]] 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'); - - #} else { - # main::print_log("[Venstar Colortouch:". $self->{data}->{name} . "] Problem retrieving poll data from " . $self->{host}); - # $self->{data}->{retry}++; - # if (defined $self->{child_object}->{comm}) { - # if ($self->{child_object}->{comm}->state() ne "offline") { - # main::print_log "[Venstar Colortouch:". $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ($self->{loglevel}); - # $self->{child_object}->{comm}->set("offline",'poll'); - # } - # } - # return ('0'); } } +sub process_check { + my ($self) = @_; + + return unless (defined $self->{poll_process}); + if ($self->{poll_process}->done_now()) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background, " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); + + my $file_data = &main::file_read($self->{poll_data_file}); + #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 $data; + eval { $data = JSON::XS->new->decode( $json_data ); }; + # catch crashes: + if ($@) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; + } else { + if (keys $data) { +# if ($self->{poll_process_pid}->{$self->{poll_process}->pid()} eq "info") { +# need to check for valid data + if ($self->{poll_process_mode} eq "info") { + $self->{data}->{tempunits} = $data->{tempunits}; + $self->{data}->{name} = $data->{name}; + $self->{data}->{info} = $data; + $self->{data}->{timestamp} = time; + } elsif ($self->{poll_process_mode} eq "sensors") { + $self->{data}->{sensors} = $data; + $self->{data}->{timestamp} = time; + } + $self->process_data(); + } else { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; + } + } + if (scalar @{$self->{command_queue}}) { + my $cmd_string = shift @{$self->{command_queue}}; + my ($mode,$cmd) = split/\|/,$cmd_string; + $self->{poll_process}->set($cmd); + $self->{poll_process}->start(); + $self->{poll_process_pid}->{$self->{poll_process}->pid()}=$mode; #capture the type of information requested in order to parse; + $self->{poll_process_mode} = $mode; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Command Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd") if ($self->{debug}); + + } + } +} + #------------------------------------------------------------------------------------ sub _get_JSON_data { - my ( $self, $mode ) = @_; - - unless ( $self->{updating} ) { - - $self->{updating} = 1; - my $ua = new LWP::UserAgent( keep_alive => 1 ); - $ua->timeout( $self->{timeout} ); - - my $host = $self->{host}; - - my $request = HTTP::Request->new( POST => "http://$host/$rest{$mode}" ); - $request->content_type("application/x-www-form-urlencoded"); - - my $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" - if $self->{debug}; + my ( $self, $mode, $method ) = @_; - my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if $self->{debug}; - my $isSuccessResponse = $responseCode < 400; - $self->{updating} = 0; - if ( !$isSuccessResponse ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, failed to get data. Response code $responseCode" - ); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} eq "online" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } - return ('0'); - } - else { - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} eq "offline" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to online..." - if ( $self->{loglevel} ); - $self->{status} = "online"; - $self->{child_object}->{comm}->set( "online", 'poll' ); - } - } - } - my $response; - eval { $response = JSON::XS->new->decode( $responseObj->content ); }; + if (($self->{background}) and (lc $method ne "direct")) { + + my $cmd = 'get_url "http://' . $self->{host} . "/$rest{$mode}" . '"'; + 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( "[Venstar Colortouch:" . $self->{data}->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd") if ($self->{debug}); + + } else { + if (scalar @{$self->{command_queue}} < $self->{max_poll_queue}) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{$self->{command_queue}} . ". Queing command $mode, $cmd") if ($self->{debug}); + push @{$self->{command_queue}}, "$mode|$cmd"; +#TODO: queue shouldn't grow for polling. Since a poll is 2 queries, the queue should only be 3 items. Otherwise it will grow every poll +# if there are device issues. + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Command discarded."); + } + } + } else { + unless ( $self->{updating} ) { + + $self->{updating} = 1; + my $ua = new LWP::UserAgent( keep_alive => 1 ); + $ua->timeout( $self->{timeout} ); + + my $host = $self->{host}; + + my $request = HTTP::Request->new( POST => "http://$host/$rest{$mode}" ); + $request->content_type("application/x-www-form-urlencoded"); + + my $responseObj = $ua->request($request); + print $responseObj->content . "\n--------------------\n" if $self->{debug}; + + my $responseCode = $responseObj->code; + print 'Response code: ' . $responseCode . "\n" if $self->{debug}; + my $isSuccessResponse = $responseCode < 400; + $self->{updating} = 0; + if ( !$isSuccessResponse ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, failed to get data. Response code $responseCode"); +print "Venstar. status=" . $self->{status}; +if (defined $self->{child_object}->{comm}) { + print " Tracker defined\n"; +} else { + print " Tracker UNDEFINED\n"; +} + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} eq "online" ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } + return ('0'); + } else { + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} eq "offline" ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to online..." if ( $self->{loglevel} ); + $self->{status} = "online"; + $self->{child_object}->{comm}->set( "online", 'poll' ); + } + } + } + my $response; + eval { $response = JSON::XS->new->decode( $responseObj->content ); }; - # catch crashes: - if ($@) { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR! JSON parser crashed! $@\n"; - return ('0'); - } - else { - return ( $isSuccessResponse, $response ); - } - } - else { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, not fetching data due to operation in progress" ); - return ('0'); + # catch crashes: + if ($@) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON parser crashed! $@\n"; + return ('0'); + } else { + return ( $isSuccessResponse, $response ); + } + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, not fetching data due to operation in progress" ); + return ('0'); + } } } @@ -365,7 +404,6 @@ sub _push_JSON_data { my $humidity_change = 0; my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; -#check why is this called twice? my ($choliday,$coverride,$coverridetime,$cforceunocc); $units = $cunits if ( not defined $units ); $units = 1 if ( ( $units eq "C" ) or ( $units eq "c" ) ); @@ -617,6 +655,12 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, failed to push data. Response code $responseCode" ); +print "Venstar. status=" . $self->{status}; +if (defined $self->{child_object}->{comm}) { + print " Tracker defined\n"; +} else { + print " Tracker UNDEFINED\n"; +} if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} eq "online" ) { main::print_log "[Venstar Colortouch:" @@ -701,7 +745,7 @@ sub stop_timer { sub start_timer { my ($self) = @_; - + print "db: timer has started\n"; if ( defined $self->{timer} ) { $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &Venstar_Colortouch::_poll_check($self) }, -1 ); @@ -1863,9 +1907,11 @@ sub set { my ( $self, $p_state, $p_setby ) = @_; if ( $p_setby eq 'poll' ) { + print "db: in super set\n"; $self->SUPER::set($p_state); } else { + print "db: in mode set\n"; $self->set_mode($p_state); } } From 8bb3cf053aee38cfe9ed6781449cacf76a19c416 Mon Sep 17 00:00:00 2001 From: hplato Date: Tue, 27 Sep 2016 16:22:26 -0600 Subject: [PATCH 053/209] Working active commands. --- lib/Venstar_Colortouch.pm | 235 +++++++++++++++++++++++++++----------- 1 file changed, 166 insertions(+), 69 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 37172d999..202770380 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -9,7 +9,7 @@ use HTTP::Request::Common qw(POST); use JSON::XS; use Data::Dumper; -# Venstar::Colortouch +# Venstar::Colortouch Objects # $stat_upper = new Venstar_Colortouch('192.168.0.100'); # # $stat_upper_mode = new Venstar_Colortouch_Mode($stat_upper); @@ -22,6 +22,7 @@ use Data::Dumper; # $stat_upper_sched = new Venstar_Colortouch_Schedule($stat_upper); # $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); +# Version History # v1.1 - added in schedule and humidity control. # v1.2 - added communication tracker object & timeout control # v1.3 - added check for timer defined @@ -32,20 +33,26 @@ use Data::Dumper; # v1.4.1 - API v5, working schedule, humidity setpoints # v2.0 - Background process -# Notes: +# Notes +# - State can only be set by stat. Set mode will change the mode. # - Best to use firmware at least 3.14 released Nov 2014. This fixes issues with both # schedule and humidity/dehumidify control. -# - 4.08 is API5, seems to have better wifi, but humidity control is messed up. +# - 4.08 brings API5, and a workaround to a humidity bug. -#todo -# - temp setpoint bounds checking +# Issues +#2 # - log runtimes. Maybe into a dbm file? log_runtimes method with destination. # - figure out timezone w/ DST +#1 +# - temp setpoint bounds checking # - changing heating/cooling setpoints for heating/cooling only stats does not need both setpoints +# - allow for a humidity value of 0 # - add decimals for setpoints -# - make the data poll non-blocking, turn off timer - -# State can only be set by stat. Set mode will change the mode. +# - add in communication tracker for background requests +# - verify command sets work both with existing poll data, and have a poll data gap. +# - verify that network issues don't escalate CPU usage (by _push_json_data being continually called) +# - add in the commercial stat fields +# - incorporate Steve's approach for more efficient detection of changed values. @Venstar_Colortouch::ISA = ('Generic_Item'); @@ -58,8 +65,8 @@ $rest{api} = ""; $rest{sensors} = "query/sensors"; $rest{runtimes} = "query/runtimes"; $rest{alerts} = "query/alerts"; -$rest{control} = "/control"; -$rest{settings} = "/settings"; +$rest{control} = "control"; +$rest{settings} = "settings"; sub new { @@ -69,10 +76,10 @@ sub new { $self->{data} = undef; $self->{api_ver} = 0; $self->{child_object} = undef; - $self->{config}->{cache_time} = 30; #TODO fix cache timeouts + $self->{config}->{cache_time} = 30; #is this still necessary? $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} if defined $::config_params{venstar_config_cache_time}; - $self->{config}->{tz} =$::config_params{time_zone}; #TODO Need to figure out DST for print runtimes - $self->{config}->{poll_seconds} = 60; + $self->{config}->{tz} = $::config_params{time_zone}; #TODO Need to figure out DST for print runtimes + $self->{config}->{poll_seconds} = 10; $self->{config}->{poll_seconds} = $poll if ($poll); $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); $self->{updating} = 0; @@ -85,13 +92,21 @@ sub new { $self->{status} = ""; $self->{timeout} = 15; #300; $self->{background} = 1; - $self->{max_poll_queue} = 3; + $self->{poll_data_timestamp} = 0; + $self->{max_poll_queue} = 3; + $self->{max_cmd_queue} = 5; + $self->{cmd_process_retry_limit}= 6; if ($self->{background}) { - @{$self->{command_queue}} = (); + @{$self->{poll_queue}} = (); $self->{poll_data_file} = "$::config_parms{data_dir}/venstar_poll_" . $self->{host} . ".data"; unlink "$::config_parms{data_dir}/venstar_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}/venstar_cmd_" . $self->{host} . ".data"; + unlink "$::config_parms{data_dir}/venstar_cmd_" . $self->{host} . ".data"; + $self->{cmd_process} = new Process_Item; + $self->{cmd_process}->set_output($self->{cmd_data_file}); &::MainLoop_post_add_hook( \&Venstar_Colortouch::process_check, 0, $self ); } $self->{timer} = new Timer; @@ -204,7 +219,7 @@ sub process_check { return unless (defined $self->{poll_process}); if ($self->{poll_process}->done_now()) { - main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background, " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); my $file_data = &main::file_read($self->{poll_data_file}); #for some reason get_url adds garbage to the output. Clean out the characters before and after the json @@ -216,8 +231,7 @@ sub process_check { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; } else { if (keys $data) { -# if ($self->{poll_process_pid}->{$self->{poll_process}->pid()} eq "info") { -# need to check for valid data + $self->{poll_data_timestamp} = time; if ($self->{poll_process_mode} eq "info") { $self->{data}->{tempunits} = $data->{tempunits}; $self->{data}->{name} = $data->{name}; @@ -232,17 +246,57 @@ sub process_check { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; } } - if (scalar @{$self->{command_queue}}) { - my $cmd_string = shift @{$self->{command_queue}}; + if (scalar @{$self->{poll_queue}}) { + my $cmd_string = shift @{$self->{poll_queue}}; my ($mode,$cmd) = split/\|/,$cmd_string; $self->{poll_process}->set($cmd); $self->{poll_process}->start(); $self->{poll_process_pid}->{$self->{poll_process}->pid()}=$mode; #capture the type of information requested in order to parse; $self->{poll_process_mode} = $mode; - main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Command Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd") if ($self->{debug}); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Poll Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd") if ($self->{debug}); + } + } + return unless (defined $self->{cmd_process}); + if ($self->{cmd_process}->done_now()) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background Command " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); + + my $file_data = &main::file_read($self->{cmd_data_file}); + #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 $data; + eval { $data = JSON::XS->new->decode( $json_data ); }; + # catch crashes: + if ($@) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; + } else { + 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; + $self->poll; + } else { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING Issued command was unsuccessful (success=" . $data->{success} .") , retrying..."; + if ($self->{cmd_process_retry} > $self->{cmd_process_retry_limit}) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..."; + shift @{$self->{cmd_queue}}; + $self->{cmd_process_retry} = 0; + } else { + $self->{cmd_process_retry}++; + } + } + } else { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; + } + } + if (scalar @{$self->{cmd_queue}}) { + my $cmd = @{$self->{cmd_queue}}[0]; #grab the first command, but don't take it off. + $self->{poll_process}->set($cmd); + $self->{poll_process}->start(); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Command Queue " . $self->{poll_process}->pid() . " cmd=$cmd") if ($self->{debug}); } } + } #------------------------------------------------------------------------------------ @@ -260,9 +314,9 @@ sub _get_JSON_data { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd") if ($self->{debug}); } else { - if (scalar @{$self->{command_queue}} < $self->{max_poll_queue}) { - main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{$self->{command_queue}} . ". Queing command $mode, $cmd") if ($self->{debug}); - push @{$self->{command_queue}}, "$mode|$cmd"; + if (scalar @{$self->{poll_queue}} < $self->{max_poll_queue}) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{$self->{poll_queue}} . ". Queing command $mode, $cmd") if ($self->{debug}); + push @{$self->{poll_queue}}, "$mode|$cmd"; #TODO: queue shouldn't grow for polling. Since a poll is 2 queries, the queue should only be 3 items. Otherwise it will grow every poll # if there are device issues. } else { @@ -331,7 +385,7 @@ if (defined $self->{child_object}->{comm}) { } sub _push_JSON_data { - my ( $self, $type, $params ) = @_; + my ( $self, $type, $params, $method ) = @_; my ( @fan, @fanstate, @modename, @statename, @schedule, @home, @schedulestat ); $fan[0] = "auto"; @@ -360,9 +414,26 @@ sub _push_JSON_data { #4.08, schedulepart is now the schedule type. schedule 0 is off, my $cmd; + $method = "" unless (defined $method); + +#For background tasks, we want up to date data, ie returned within the last poll period. +#recursively calling the same subroutine might be a memory or performance hog, but need to effectively +#'suspend' the data push until we get valid data. + + if (($self->{background}) and (lc $method ne "direct")) { + if ($self->{poll_data_timestamp} + $self->{config}->{poll_seconds} < &main::tickcount()) { + $self->poll() if (scalar @{$self->{poll_queue}} < $self->{max_poll_queue}); #once max reached, no sense adding more + if ($self->{poll_data_timestamp} + 300 > &main::tickcount()) { #give up after 5 minutes of trying + $self->_push_json_data($type,$params); + } else { + return ('1'); + } + } + } + #print "VCT DB: $params\n"; - $self->stop_timer; #stop timer to prevent a clash of updates + $self->stop_timer unless ($self->{background}); #stop timer to prevent a clash of updates if ( $type eq 'settings' ) { #for testing purposes, curl is: @@ -399,12 +470,23 @@ sub _push_JSON_data { my ($sched) = $params =~ /schedule=(\d+)/; my $hum; my ($humsp) = $params =~ /hum_setpoint=(\d+)/; - my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/ - ; #need to add in dehumidifier stuff at some point + my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/; #need to add in dehumidifier stuff at some point my $humidity_change = 0; - my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; + my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ); my ($choliday,$coverride,$coverridetime,$cforceunocc); + + + if (($self->{background}) and (lc $method ne "direct")) { + $cunits = $self->{data}->{info}->{tempunits}; + $caway = $self->{data}->{info}->{away}; + $csched = $self->{data}->{info}->{schedule}; + $chumsp = $self->{data}->{info}->{hum_setpoint}; + $cdehumsp = $self->{data}->{info}->{dehum_setpoint}; + } else { + ($isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; + #($choliday,$coverride,$coverridetime,$cforceunocc); + } $units = $cunits if ( not defined $units ); $units = 1 if ( ( $units eq "C" ) or ( $units eq "c" ) ); $units = 0 if ( ( $units eq "F" ) or ( $units eq "f" ) ); @@ -433,9 +515,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, if ( $cunits ne $units ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Units from $cunits to $units" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Units from $cunits to $units" ); $newunits = $units; } else { @@ -443,9 +523,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( $caway ne $away ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Away from $home[$caway] to $home[$away]" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $home[$caway] to $home[$away]" ); $newaway = $away; } else { @@ -453,9 +531,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( $choliday ne $holiday ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Away from $choliday to $holiday" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $choliday to $holiday" ); $newholiday = $holiday; } else { @@ -463,9 +539,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( $caway ne $away ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Away from $caway to $away" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $caway to $away" ); $newaway = $away; } else { @@ -538,12 +612,22 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, my ($heattemp) = $params =~ /heattemp=(\d+)/; #need decimals my ($cooltemp) = $params =~ /cooltemp=(\d+)/; - my ( - $isSuccessResponse, $cmode, $cfan, - $cheattemp, $ccooltemp, $setpointdelta, - $minheat, $maxheat, $mincool, - $maxcool - ) = $self->_get_control_params; + my ($isSuccessResponse, $cmode, $cfan, $cheattemp, $ccooltemp, $setpointdelta, $minheat, $maxheat, $mincool, $maxcool); + + if (($self->{background}) and (lc $method ne "direct")) { + $cmode = $self->{data}->{info}->{mode}; + $cfan = $self->{data}->{info}->{fan}; + $cheattemp = $self->{data}->{info}->{heattemp}; + $ccooltemp = $self->{data}->{info}->{cooltemp}; + $setpointdelta = $self->{data}->{info}->{setpointdelta}; + $minheat = $self->{data}->{info}->{heattempmin}; + $maxheat = $self->{data}->{info}->{heattempmax}; + $mincool = $self->{data}->{info}->{cooltempmin}; + $maxcool = $self->{data}->{info}->{cooltempmax}; + + } else { + ($isSuccessResponse, $cmode, $cfan, $cheattemp, $ccooltemp, $setpointdelta, $minheat, $maxheat, $mincool, $maxcool) = $self->_get_control_params; + } $mode = $cmode if ( not defined $mode ); $fan = $cfan if ( not defined $fan ); @@ -636,25 +720,42 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, return ( '1', 'error' ); } - my $ua = new LWP::UserAgent( keep_alive => 1 ); - $ua->timeout( $self->{timeout} ); + if (($self->{background}) and (lc $method ne "direct")) { + + my $cmd = 'get_url -post "' .$cmd . '" "http://' . $self->{host} . "/$rest{$type}" . '"'; + push @{$self->{cmd_queue}}, "$cmd"; + if ($self->{cmd_process}->done()) { + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + $self->{cmd_process_retry} = 0; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Backgrounding " . $self->{cmd_process}->pid() . " command $cmd") if ($self->{debug}); + + } else { + if (scalar @{$self->{cmd_queue}} < $self->{max_cmd_queue}) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{$self->{cmd_queue}} . ". Queing command $cmd") if ($self->{debug}); + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded."); + } + } + } else { - my $host = $self->{host}; + my $ua = new LWP::UserAgent( keep_alive => 1 ); + $ua->timeout( $self->{timeout} ); - my $request = HTTP::Request->new( POST => "http://$host/$rest{$type}" ); - $request->content_type("application/x-www-form-urlencoded"); - $request->content($cmd) if $cmd; + my $host = $self->{host}; - my $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" if $self->{debug}; + my $request = HTTP::Request->new( POST => "http://$host/$rest{$type}" ); + $request->content_type("application/x-www-form-urlencoded"); + $request->content($cmd) if $cmd; - my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if $self->{debug}; - my $isSuccessResponse = $responseCode < 400; - if ( !$isSuccessResponse ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, failed to push data. Response code $responseCode" ); + my $responseObj = $ua->request($request); + print $responseObj->content . "\n--------------------\n" if $self->{debug}; + + my $responseCode = $responseObj->code; + print 'Response code: ' . $responseCode . "\n" if $self->{debug}; + my $isSuccessResponse = $responseCode < 400; + if ( !$isSuccessResponse ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, failed to push data. Response code $responseCode" ); print "Venstar. status=" . $self->{status}; if (defined $self->{child_object}->{comm}) { print " Tracker defined\n"; @@ -663,12 +764,7 @@ if (defined $self->{child_object}->{comm}) { } if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} eq "online" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); $self->{status} = "offline"; $self->{child_object}->{comm}->set( "offline", 'poll' ); } @@ -697,6 +793,7 @@ if (defined $self->{child_object}->{comm}) { $self->poll if ( $response eq "success" ); $self->start_timer; return ( $isSuccessResponse, $response ); + } } sub register { @@ -724,7 +821,7 @@ sub _get_control_params { sub _get_setting_params { my ($self) = @_; - my ( $isSuccessResponse, $info ) = $self->_get_JSON_data('info'); + my ( $isSuccessResponse, $info ) = $self->_get_JSON_data('info',"direct"); return ( $isSuccessResponse, $info->{tempunits}, $info->{away}, $info->{schedule}, $info->{hum}, $info->{hum_setpoint}, $info->{dehum_setpoint} ); From eead01054a00c61d906f78c207e6ec7c000ed2e2 Mon Sep 17 00:00:00 2001 From: hplato Date: Tue, 27 Sep 2016 20:42:26 -0600 Subject: [PATCH 054/209] Fixed timeout setting --- bin/get_url | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/get_url b/bin/get_url index 549d83f63..1efcd0fb3 100755 --- a/bin/get_url +++ b/bin/get_url @@ -103,7 +103,7 @@ sub use_ua { $ua->proxy( [ 'http', 'ftp' ] => $config_parms{proxy} ) if $config_parms{proxy}; - $ua->timeout( [120] ); # Time out after 60 seconds + $ua->timeout(60); # Time out after 60 seconds $ua->env_proxy(); $ua->agent( $config_parms{get_url_ua} ) if $config_parms{get_url_ua}; From 42a658ac38eedea112d75e21f2084df3e9a56536 Mon Sep 17 00:00:00 2001 From: hplato Date: Tue, 27 Sep 2016 20:43:01 -0600 Subject: [PATCH 055/209] Fixed some cosmetic errors with v2.0 --- lib/Venstar_Colortouch.pm | 61 ++++++++++++--------------------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 202770380..9beeee4fa 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -216,7 +216,7 @@ sub poll { sub process_check { my ($self) = @_; - +#Need to catch error 500's 403's and update communication tracker and success:false messages to write to log return unless (defined $self->{poll_process}); if ($self->{poll_process}->done_now()) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); @@ -421,7 +421,7 @@ sub _push_JSON_data { #'suspend' the data push until we get valid data. if (($self->{background}) and (lc $method ne "direct")) { - if ($self->{poll_data_timestamp} + $self->{config}->{poll_seconds} < &main::tickcount()) { + if ($self->{poll_data_timestamp} + $self->{config}->{poll_seconds} < &main::get_tickcount()) { $self->poll() if (scalar @{$self->{poll_queue}} < $self->{max_poll_queue}); #once max reached, no sense adding more if ($self->{poll_data_timestamp} + 300 > &main::tickcount()) { #give up after 5 minutes of trying $self->_push_json_data($type,$params); @@ -509,9 +509,9 @@ sub _push_JSON_data { } else { $dehumsp = $cdehumsp if ( not defined $dehumsp ); } -print "venstar db: params = $params\n"; -print "units=$units, away=$away, sched=$sched, hum=$hum, humsp=$humsp, dehumsp=$dehumsp\n"; -print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, cdehumsp=$cdehumsp\n"; +#print "venstar db: params = $params\n"; +#print "units=$units, away=$away, sched=$sched, humsp=$humsp, dehumsp=$dehumsp\n"; +#print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, cdehumsp=$cdehumsp\n"; if ( $cunits ne $units ) { @@ -530,7 +530,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, $newaway = $caway; } - if ( $choliday ne $holiday ) { + if ( ($self->{type} eq "commercial") and ($choliday ne $holiday) ) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $choliday to $holiday" ); $newholiday = $holiday; } @@ -634,17 +634,9 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, $heattemp = $cheattemp if ( !$heattemp ); $cooltemp = $ccooltemp if ( !$cooltemp ); - main::print_log( - "data1=$isSuccessResponse,$cmode,$cfan,$cheattemp,$ccooltemp,$setpointdelta" - ) if $self->{debug}; #TODO pass object to get debug - main::print_log("data2=$mode,$fan,$heattemp,$cooltemp") - if $self->{debug}; #TODO debug if ( $cmode ne $mode ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing mode from $modename[$cmode] to $modename[$mode]" - ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing mode from $modename[$cmode] to $modename[$mode]"); $newmode = $mode; } else { @@ -652,9 +644,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( $cfan ne $fan ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing fan from $fan[$cfan] to $fan[$fan]" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing fan from $fan[$cfan] to $fan[$fan]" ); $newfan = $fan; } else { @@ -662,9 +652,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( $heattemp ne $cheattemp ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing heat setpoint from $cheattemp to $heattemp" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing heat setpoint from $cheattemp to $heattemp" ); $newheatsp = $heattemp; } else { @@ -672,9 +660,7 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( $cooltemp ne $ccooltemp ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing cool setpoint from $ccooltemp to $cooltemp" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing cool setpoint from $ccooltemp to $cooltemp" ); $newcoolsp = $cooltemp; } else { @@ -682,35 +668,23 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if ( ( $newcoolsp > $maxcool ) or ( $newcoolsp < $mincool ) ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Error: New cooling setpoint $newcoolsp out of bounds $mincool - $maxcool" - ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Error: New cooling setpoint $newcoolsp out of bounds $mincool - $maxcool"); $newcoolsp = $ccooltemp; } if ( ( $newheatsp > $maxheat ) or ( $newheatsp < $minheat ) ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Error: New heating setpoint $newheatsp out of bounds $minheat - $maxheat" - ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Error: New heating setpoint $newheatsp out of bounds $minheat - $maxheat"); $newheatsp = $cheattemp; } if ( ( $newheatsp - $newcoolsp ) > $setpointdelta ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Error: Cooling ($newcoolsp) and Heating ($newheatsp) setpoints need to be less than setpoint $setpointdelta" - ); - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Not setting setpoints" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Error: Cooling ($newcoolsp) and Heating ($newheatsp) setpoints need to be less than setpoint $setpointdelta"); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Not setting setpoints" ); $newcoolsp = $ccooltemp; $newheatsp = $cheattemp; } - $cmd = - "mode=$newmode&fan=$newfan&heattemp=$newheatsp&cooltemp=$newcoolsp"; + $cmd = "mode=$newmode&fan=$newfan&heattemp=$newheatsp&cooltemp=$newcoolsp"; main::print_log( "Sending Control command $cmd to " . $self->{host} ) if $self->{debug}; } @@ -721,7 +695,8 @@ print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, } if (($self->{background}) and (lc $method ne "direct")) { - + $isSuccessResponse = 1; #set these to successful, since the process_data will indicate if a setting was unsuccessful. + $response = "success"; my $cmd = 'get_url -post "' .$cmd . '" "http://' . $self->{host} . "/$rest{$type}" . '"'; push @{$self->{cmd_queue}}, "$cmd"; if ($self->{cmd_process}->done()) { @@ -792,8 +767,8 @@ if (defined $self->{child_object}->{comm}) { print "response=$response\n" if $self->{debug}; $self->poll if ( $response eq "success" ); $self->start_timer; - return ( $isSuccessResponse, $response ); } + return ( $isSuccessResponse, $response ); } sub register { From 13f9535f4edd56649d67488324a58ad0de2d0c33 Mon Sep 17 00:00:00 2001 From: hplato Date: Fri, 7 Oct 2016 16:34:13 -0600 Subject: [PATCH 056/209] v2.0.3 - tracker, setpoints, decimals fixed active commands, verified setpoints --- lib/Venstar_Colortouch.pm | 125 +++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 9beeee4fa..e0c0dc52e 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,5 +1,5 @@ package Venstar_Colortouch; -# v2.0 +# v2.0.3 use strict; use warnings; @@ -14,7 +14,7 @@ use Data::Dumper; # # $stat_upper_mode = new Venstar_Colortouch_Mode($stat_upper); # $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); -# $stat_upper_heat_sp = new Venstar_Colortouch_Heat_sp($stat_upper); +# $stat_upper_heat_sp = new Venstar_Colortouch_Heat_sp($stat_upper,"F"); #F for Fahrenheit # $stat_upper_cool_sp = new Venstar_Colortouch_Cool_sp($stat_upper); # $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); # $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); @@ -44,10 +44,8 @@ use Data::Dumper; # - log runtimes. Maybe into a dbm file? log_runtimes method with destination. # - figure out timezone w/ DST #1 -# - temp setpoint bounds checking -# - changing heating/cooling setpoints for heating/cooling only stats does not need both setpoints -# - allow for a humidity value of 0 -# - add decimals for setpoints +# - TEST temp setpoint bounds checking +# - changing heating/cooling setpoints for heating/cooling only stats does not need both setpoints? # - add in communication tracker for background requests # - verify command sets work both with existing poll data, and have a poll data gap. # - verify that network issues don't escalate CPU usage (by _push_json_data being continually called) @@ -85,7 +83,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{host} = $host; - $self->{debug} = 5; + $self->{debug} = 0; $self->{debug} = $debug if ($debug); $self->{debug} = 0 if ( $self->{debug} < 0 ); $self->{loglevel} = 1; @@ -219,31 +217,40 @@ sub process_check { #Need to catch error 500's 403's and update communication tracker and success:false messages to write to log return unless (defined $self->{poll_process}); if ($self->{poll_process}->done_now()) { + my $com_status = "online"; main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); my $file_data = &main::file_read($self->{poll_data_file}); #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 =~ /({.*})/; + print "debug: json_data=$json_data\n" if ($self->{debug}); + unless ($file_data) { + main::print_log("[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! no data returned by poll"); + return; + } my $data; eval { $data = JSON::XS->new->decode( $json_data ); }; # catch crashes: if ($@) { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; + $com_status = "offline"; } else { if (keys $data) { - $self->{poll_data_timestamp} = time; + $self->{poll_data_timestamp} = &main::get_tickcount(); if ($self->{poll_process_mode} eq "info") { $self->{data}->{tempunits} = $data->{tempunits}; $self->{data}->{name} = $data->{name}; $self->{data}->{info} = $data; - $self->{data}->{timestamp} = time; + $self->{data}->{timestamp} = &main::get_tickcount(); } elsif ($self->{poll_process_mode} eq "sensors") { $self->{data}->{sensors} = $data; - $self->{data}->{timestamp} = time; + $self->{data}->{timestamp} = &main::get_tickcount(); } $self->process_data(); } else { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; + $com_status = "offline"; } } if (scalar @{$self->{poll_queue}}) { @@ -255,13 +262,25 @@ sub process_check { $self->{poll_process_mode} = $mode; main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Poll Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd") if ($self->{debug}); - } + } + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } } return unless (defined $self->{cmd_process}); if ($self->{cmd_process}->done_now()) { + my $com_status = "online"; main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background Command " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); my $file_data = &main::file_read($self->{cmd_data_file}); + unless ($file_data) { + main::print_log("[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! no data returned by command"); + return; + } #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 $data; @@ -269,6 +288,8 @@ sub process_check { # catch crashes: if ($@) { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; + $com_status = "offline"; + } else { if (keys $data) { if ($data->{success} eq "true") { @@ -280,13 +301,15 @@ sub process_check { if ($self->{cmd_process_retry} > $self->{cmd_process_retry_limit}) { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..."; shift @{$self->{cmd_queue}}; - $self->{cmd_process_retry} = 0; + $self->{cmd_process_retry} = 0; + $com_status = "offline"; } else { $self->{cmd_process_retry}++; } } } else { print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; + $com_status = "offline"; } } if (scalar @{$self->{cmd_queue}}) { @@ -294,7 +317,14 @@ sub process_check { $self->{poll_process}->set($cmd); $self->{poll_process}->start(); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Command Queue " . $self->{poll_process}->pid() . " cmd=$cmd") if ($self->{debug}); - } + } + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } } } @@ -321,6 +351,13 @@ sub _get_JSON_data { # if there are device issues. } else { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Command discarded."); + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne "offline" ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } } } } else { @@ -419,13 +456,15 @@ sub _push_JSON_data { #For background tasks, we want up to date data, ie returned within the last poll period. #recursively calling the same subroutine might be a memory or performance hog, but need to effectively #'suspend' the data push until we get valid data. - + print "db: poll_timestamp=" . $self->{poll_data_timestamp} . " poll_seconds = " . $self->{config}->{poll_seconds} . " get_tickcount=" . &main::get_tickcount() . "\n" if ($self->{debug}); if (($self->{background}) and (lc $method ne "direct")) { - if ($self->{poll_data_timestamp} + $self->{config}->{poll_seconds} < &main::get_tickcount()) { + if (($self->{poll_data_timestamp} + ($self->{config}->{poll_seconds} * 1000)) < &main::get_tickcount()) { $self->poll() if (scalar @{$self->{poll_queue}} < $self->{max_poll_queue}); #once max reached, no sense adding more - if ($self->{poll_data_timestamp} + 300 > &main::tickcount()) { #give up after 5 minutes of trying + if ($self->{poll_data_timestamp} + 300000 > &main::get_tickcount()) { #give up after 5 minutes of trying + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING: retrying command attempt due to stale poll data!" ); $self->_push_json_data($type,$params); } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR: Abandoning command attempt due to stale poll data!" ); return ('1'); } } @@ -609,8 +648,8 @@ sub _push_JSON_data { my ( $newmode, $newfan, $newcoolsp, $newheatsp ); my ($mode) = $params =~ /mode=(\d+)/; my ($fan) = $params =~ /fan=(\d+)/; - my ($heattemp) = $params =~ /heattemp=(\d+)/; #need decimals - my ($cooltemp) = $params =~ /cooltemp=(\d+)/; + my ($heattemp) = $params =~ /heattemp=(\d+\.?\d?)/; #need decimals + my ($cooltemp) = $params =~ /cooltemp=(\d+\.?\d?)/; my ($isSuccessResponse, $cmode, $cfan, $cheattemp, $ccooltemp, $setpointdelta, $minheat, $maxheat, $mincool, $maxcool); @@ -693,7 +732,8 @@ sub _push_JSON_data { main::print_log("Unknown mode!"); return ( '1', 'error' ); } - + my $isSuccessResponse; + my $response; if (($self->{background}) and (lc $method ne "direct")) { $isSuccessResponse = 1; #set these to successful, since the process_data will indicate if a setting was unsuccessful. $response = "success"; @@ -728,7 +768,7 @@ sub _push_JSON_data { my $responseCode = $responseObj->code; print 'Response code: ' . $responseCode . "\n" if $self->{debug}; - my $isSuccessResponse = $responseCode < 400; + $isSuccessResponse = $responseCode < 400; if ( !$isSuccessResponse ) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, failed to push data. Response code $responseCode" ); print "Venstar. status=" . $self->{status}; @@ -758,12 +798,11 @@ if (defined $self->{child_object}->{comm}) { $self->{child_object}->{comm}->set( "online", 'poll' ); } } - } - #my $response = JSON::XS->new->decode ($responseObj->content); + ($response) = $responseObj->content =~ /\{\"(.*)\":/; + } #print Dumper $response if $self->{debug}; - my ($response) = $responseObj->content =~ /\{\"(.*)\":/; print "response=$response\n" if $self->{debug}; $self->poll if ( $response eq "success" ); $self->start_timer; @@ -2105,6 +2144,8 @@ sub new { bless $self, $class; $$self{master_object} = $object; + push( @{ $$self{states} }, '0','5','10','15','20','25','30','35','40' ); + $object->register( $self, 'hum_sp' ); $self->set( $object->get_sp_hum, 'poll' ); @@ -2232,12 +2273,21 @@ package Venstar_Colortouch_Heat_sp; @Venstar_Colortouch_Heat_sp::ISA = ('Generic_Item'); sub new { - my ( $class, $object ) = @_; + my ( $class, $object, $scale ) = @_; my $self = {}; bless $self, $class; $$self{master_object} = $object; + if (lc $scale eq "F") { + push( @{ $$self{states} }, '65','66','67','68','69','70','71','72','73','74','75','76','77','78','79','80' ); + $self->{lower_limit} = 65; + $self->{upper_limit} = 80; + } else { + push( @{ $$self{states} }, '17','17.5','18','18.5','19','19.5','20','20.5','21','21.5','22','22.5','23','23.5','24','24.5','25','25.5','26' ); + $self->{lower_limit} = 17; + $self->{upper_limit} = 26; + } $object->register( $self, 'heat_sp' ); $self->set( $object->get_sp_heat, 'poll' ); @@ -2253,7 +2303,11 @@ sub set { } else { if ( ( $p_state >= 0 ) and ( $p_state <= 98 ) ) { - $$self{master_object}->set_heat_sp($p_state); + if (($p_state >= $self->{lower_limit}) and ($p_state <= $self->{upper_limit})) { + $$self{master_object}->set_heat_sp($p_state); + } else { + main::print_log("[Venstar Colortouch Heat_SP] Error. $p_state out of limits (" . $self->{lower_limit} . " to " . $self->{upper_limit} . ")"); + } } else { main::print_log( @@ -2268,12 +2322,21 @@ package Venstar_Colortouch_Cool_sp; @Venstar_Colortouch_Cool_sp::ISA = ('Generic_Item'); sub new { - my ( $class, $object ) = @_; + my ( $class, $object, $scale ) = @_; my $self = {}; bless $self, $class; $$self{master_object} = $object; + if (lc $scale eq "F") { + push( @{ $$self{states} }, '58','59','60','61','62','63','64','65','66','67','68','69','70','71','72','73','74','75','76','77','78','79','80' ); + $self->{lower_limit} = 58; + $self->{upper_limit} = 80; + } else { + push( @{ $$self{states} }, '17','17.5','18','18.5','19','19.5','20','20.5','21','21.5','22','22.5','23','23.5','24','24.5','25','25.5','26' ); + $self->{lower_limit} = 17; + $self->{upper_limit} = 26; + } $object->register( $self, 'cool_sp' ); $self->set( $object->get_sp_cool, 'poll' ); @@ -2289,12 +2352,14 @@ sub set { } else { if ( ( $p_state >= 0 ) and ( $p_state <= 98 ) ) { - $$self{master_object}->set_cool_sp($p_state); + if (($p_state >= $self->{lower_limit}) and ($p_state <= $self->{upper_limit})) { + $$self{master_object}->set_cool_sp($p_state); + } else { + main::print_log("[Venstar Colortouch Cool_SP] Error. $p_state out of limits (" . $self->{lower_limit} . " to " . $self->{upper_limit} . ")"); + } } else { - main::print_log( - "[Venstar Colortouch Cool_SP] Error. Unknown set state $p_state" - ); + main::print_log("[Venstar Colortouch Cool_SP] Error. Unknown set state $p_state"); } } } From f8ce00b3223bc8a2dc3a494dfa6f491d742e0972 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Sun, 9 Oct 2016 16:58:54 -0600 Subject: [PATCH 057/209] IA7 v1.3.580 - workaround for floorplan initial icon locations --- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 70 +++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 7f917f592..001535670 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - developed the v4 web interface, updates by H.Plato. IA7 v1.3.560 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.3.580 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index c06d61b55..5d365cd7c 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1883,7 +1883,7 @@ var fp_resize_floorplan_image = function(){ $("#fp_graphic").attr("width", "1px"); fp_display_width = $("#graphic").width(); - console.log("FP: resize "+ floor_width + " => " + fp_display_width); +// console.log("FP: resize "+ floor_width + " => " + fp_display_width); $('#fp_graphic').attr("width",fp_display_width+"px"); fp_display_height = $("#fp_graphic").height(); }; @@ -1891,7 +1891,7 @@ var fp_resize_floorplan_image = function(){ var fp_reposition_entities = function(){ var t0 = performance.now(); var fp_graphic_offset = $("#fp_graphic").offset(); - console.log("fp_graphic_offset: "+ JSON.stringify(fp_graphic_offset)); +// console.log("fp_graphic_offset: "+ JSON.stringify(fp_graphic_offset)); var width = fp_display_width; var hight = fp_display_height; var onePercentWidthInPx = width/100; @@ -1928,7 +1928,7 @@ var fp_reposition_entities = function(){ var fp_scale = width/nwidth; var fp_scale_percent = Math.round( fp_scale * 100); - console.log("width="+width+" nwidth="+nwidth+" scale="+fp_scale_percent); +// console.log("width="+width+" nwidth="+nwidth+" scale="+fp_scale_percent); // update the location of all the objects... $(".floorplan_item").each(function(index) { var classstr = $(this).attr("class"); @@ -1940,7 +1940,7 @@ var fp_reposition_entities = function(){ } var fp_location = coords.split(/x/); var fp_offset = fp_get_offset_from_location(fp_location); - console.log("coords="+coords); +// console.log("coords="+coords); // this seems to make the repositioning slow // ~ 300+ms on my nexus7 firefox-beta vs <100ms with this code commented out @@ -1952,7 +1952,7 @@ var fp_reposition_entities = function(){ // } var element_id = $(this).attr('id'); var adjust = fp_icon_image_size*fp_scale/2; - console.log("adjust="+adjust+" fp_offset.top="+fp_offset.top+" fp_offset.left="+fp_offset.left); +// console.log("adjust="+adjust+" fp_offset.top="+fp_offset.top+" fp_offset.left="+fp_offset.left); var fp_off_center = { "top": fp_offset.top - adjust, "left": fp_offset.left - adjust @@ -1961,33 +1961,40 @@ var fp_reposition_entities = function(){ }); $('.icon_select img').each(function(){ - $(this).width(fp_scale + "%"); + $(this).width(fp_scale_percent + "%"); }); var t1 = performance.now(); console.log("FP: reposition and scale: " +Math.round(t1 - t0) + "ms "); }; +var fp_show_all_icons = function() { + + $(".floorplan_item").each(function(index) { + $(this).show(); + }); +}; + var fp_set_pos = function(id, offset){ var item = $('#' + id); // do not move the span, this make the popup to narrow somehow // item.closest("span").offset(offset); - var left11 = item.css("left"); - var left12 = item[0].style.left; - var top11 = item.css("top"); - var top12 = item[0].style.top; - var before = item.offset(); - var init = false - if (item.css("left") == "auto") { - console.log("auto found, fixing left property"); - offset.left = 0 - fp_icon_image_size/2; - } +// var left11 = item.css("left"); +// var left12 = item[0].style.left; +// var top11 = item.css("top"); +// var top12 = item[0].style.top; +// var before = item.offset(); +// var init = false +// if (item.css("left") == "auto") { + // console.log("auto found, fixing left property"); +// offset.left = 0 - fp_icon_image_size/2; +// } item.offset(offset); - var after = item.offset(); - var left21 = item.css("left"); - var left22 = item[0].style.left; - console.log("offset.top="+offset.top+" offset.left="+offset.left+" before.top="+before.top+" before.left="+before.left+" after.top="+after.top+" after.left="+after.left); - console.log("top11="+top11+" top12="+top12); - console.log("left11="+left11+" left12="+left12+" left21="+left21+" left22="+left22); +// var after = item.offset(); +// var left21 = item.css("left"); +// var left22 = item[0].style.left; +// console.log("offset.top="+offset.top+" offset.left="+offset.left+" before.top="+before.top+" before.left="+before.left+" after.top="+after.top+" after.left="+after.left); +// console.log("top11="+top11+" top12="+top12); +// console.log("left11="+left11+" left12="+left12+" left21="+left21+" left22="+left22); }; var fp_is_point_on_fp = function (p){ @@ -2033,7 +2040,7 @@ var floorplan = function(group,time) { $('#list_content').append("
    ");
             }
             $('#fp_graphic').bind("load", function () {
    -            console.log("FP: background loaded.");
    +//            console.log("FP: background loaded.");
                 fp_resize_floorplan_image();
                 floorplan(group, time);
             });
    @@ -2165,7 +2172,7 @@ var floorplan = function(group,time) {
                 return;
             }
     
    -        console.log("FP: window resized");
    +//        console.log("FP: window resized");
             fp_resize_floorplan_image();
             fp_reposition_entities();
         };
    @@ -2328,10 +2335,10 @@ var floorplan = function(group,time) {
                             url: "/LONG_POLL?json('GET','fp_icon_sets','px=48')",
                             dataType: "json",
                             error: function(xhr, textStatus, errorThrown){
    -                            console.log('FP: request iconsets failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"');
    +//                            console.log('FP: request iconsets failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"');
                             },
                             success: function( json, statusText, jqXHR ) {
    -                            console.log('FP: request iconsets: "' + statusText + '" "'+JSON.stringify(jqXHR, undefined,2)+'"');
    +//                            console.log('FP: request iconsets: "' + statusText + '" "'+JSON.stringify(jqXHR, undefined,2)+'"');
                                 var requestTime = time;
                                 if (jqXHR.status === 200) {
                                     var iconlist = '
    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 4e3126d1c..5058dd93c 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1803,13 +1803,15 @@ var object_history = function(items,start,days,time) { $('#hist-legend').find("li").eq(i).prepend('    '); }); } else { + // table var html = ""; html += ""; html += ""; if (json.data.data !== undefined) { //If no data, at least show the header + json.data.data.reverse(); for (var i = 0; i < json.data.data.length; i++){ html +=""; - html += ""; + html += ""; html += ""; html += ""; html += ""; From 1a2dafc4c1d52e2fac59b53d04e37d5a50cfcb80 Mon Sep 17 00:00:00 2001 From: H Plato Date: Tue, 18 Oct 2016 20:34:43 -0600 Subject: [PATCH 059/209] v2.0.11 - working hybrid model --- lib/Venstar_Colortouch.pm | 1258 ++++++++++++++++++++++++++++--------- 1 file changed, 961 insertions(+), 297 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index b0e80a1e2..0a4795712 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,4 +1,6 @@ package Venstar_Colortouch; +# v2.0.11 +# background > 2 for polling use strict; use warnings; @@ -8,16 +10,20 @@ use HTTP::Request::Common qw(POST); use JSON::XS; use Data::Dumper; -# Venstar::Colortouch -# $stat_upper = new Venstar_Colortouch('192.168.0.100'); +# Venstar::Colortouch Objects +# $stat_upper = new Venstar_Colortouch('192.168.0.100'); # -# $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); -# $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); -# $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); -# $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); -# $stat_upper_sched = new Venstar_Colortouch_Sched($stat_upper); -# $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); - +# $stat_upper_mode = new Venstar_Colortouch_Mode($stat_upper); +# $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); +# $stat_upper_heat_sp = new Venstar_Colortouch_Heat_sp($stat_upper,"F"); #F for Fahrenheit +# $stat_upper_cool_sp = new Venstar_Colortouch_Cool_sp($stat_upper); +# $stat_upper_fan = new Venstar_Colortouch_Fan($stat_upper); +# $stat_upper_hum = new Venstar_Colortouch_Humidity($stat_upper); +# $stat_upper_hum_sp = new Venstar_Colortouch_Humidity_sp($stat_upper); +# $stat_upper_sched = new Venstar_Colortouch_Schedule($stat_upper); +# $stat_upper_comm = new Venstar_Colortouch_Comm($stat_upper); + +# Version History # v1.1 - added in schedule and humidity control. # v1.2 - added communication tracker object & timeout control # v1.3 - added check for timer defined @@ -25,21 +31,27 @@ use Data::Dumper; # "hum_setpoint": is the current humidity and # "dehum_setpoint": is the humidity setpoint # "hum" doesn't return anything anymore. +# v1.4.1 - API v5, working schedule, humidity setpoints +# v2.0 - Background process -# Notes: +# Notes +# - State can only be set by stat. Set mode will change the mode. # - Best to use firmware at least 3.14 released Nov 2014. This fixes issues with both # schedule and humidity/dehumidify control. -# - 4.08 is API5, seems to have better wifi, but humidity control is messed up. +# - 4.08 brings API5, and a workaround to a humidity bug. -#todo -# - temp setpoint bounds checking +# Issues +#2 # - log runtimes. Maybe into a dbm file? log_runtimes method with destination. # - figure out timezone w/ DST -# - changing heating/cooling setpoints for heating/cooling only stats does not need both setpoints -# - add decimals for setpoints -# # make the data poll non-blocking, turn off timer -# -# State can only be set by stat. Set mode will change the mode. +#1 +# - TEST temp setpoint bounds checking +# - changing heating/cooling setpoints for heating/cooling only stats does not need both setpoints? +# - add in communication tracker for background requests +# - verify command sets work both with existing poll data, and have a poll data gap. +# - verify that network issues don't escalate CPU usage (by _push_json_data being continually called) +# - add in the commercial stat fields +# - incorporate Steve's approach for more efficient detection of changed values. @Venstar_Colortouch::ISA = ('Generic_Item'); @@ -52,35 +64,52 @@ $rest{api} = ""; $rest{sensors} = "query/sensors"; $rest{runtimes} = "query/runtimes"; $rest{alerts} = "query/alerts"; -$rest{control} = "/control"; -$rest{settings} = "/settings"; +$rest{control} = "control"; +$rest{settings} = "settings"; + sub new { - my ( $class, $host, $poll ) = @_; + my ( $class, $host, $poll, $debug ) = @_; my $self = {}; bless $self, $class; - $self->{data} = undef; - $self->{api_ver} = 0; - $self->{child_object} = undef; - $self->{config}->{cache_time} = 30; #TODO fix cache timeouts - $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} - if defined $::config_params{venstar_config_cache_time}; - $self->{config}->{tz} = - $::config_params{time_zone}; #TODO Need to figure out DST for print runtimes - $self->{config}->{poll_seconds} = 60; + $self->{data} = undef; + $self->{api_ver} = 0; + $self->{child_object} = undef; + $self->{config}->{cache_time} = 30; #is this still necessary? + $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} if defined $::config_params{venstar_config_cache_time}; + $self->{config}->{tz} = $::config_params{time_zone}; #TODO Need to figure out DST for print runtimes + $self->{config}->{poll_seconds} = 10; $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 - if ( $self->{config}->{poll_seconds} < 1 ); - $self->{updating} = 0; - $self->{data}->{retry} = 0; - $self->{host} = $host; - $self->{debug} = 5; - $self->{loglevel} = 1; - $self->{status} = ""; - $self->{timeout} = 15; #300; - + $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{updating} = 0; + $self->{data}->{retry} = 0; + $self->{host} = $host; + $self->{debug} = 5; + $self->{debug} = $debug if ($debug); + $self->{debug} = 0 if ( $self->{debug} < 0 ); + $self->{loglevel} = 1; + $self->{status} = ""; + $self->{timeout} = 15; #300; + $self->{background} = 1; #0 for direct, 1 for set commands, 2 for poll and set commands + $self->{poll_data_timestamp} = 0; + $self->{max_poll_queue} = 3; + $self->{max_cmd_queue} = 5; + $self->{cmd_process_retry_limit}= 6; + if ($self->{background}) { + @{$self->{poll_queue}} = (); + $self->{poll_data_file} = "$::config_parms{data_dir}/venstar_poll_" . $self->{host} . ".data"; + unlink "$::config_parms{data_dir}/venstar_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}/venstar_cmd_" . $self->{host} . ".data"; + unlink "$::config_parms{data_dir}/venstar_cmd_" . $self->{host} . ".data"; + $self->{cmd_process} = new Process_Item; + $self->{cmd_process}->set_output($self->{cmd_data_file}); + &::MainLoop_post_add_hook( \&Venstar_Colortouch::process_check, 0, $self ); + } + $self->{timer} = new Timer; $self->_init; - $self->{timer} = new Timer; $self->start_timer; return $self; } @@ -88,7 +117,7 @@ sub new { sub _poll_check { my ($self) = @_; - #main::print_log("[Venstar Colortouch] _poll_check initiated"); + main::print_log("[Venstar Colortouch] _poll_check initiated"); #main::run (sub {&Venstar_Colortouch::get_data($self)}); #spawn this off to run in the background $self->get_data(); } @@ -96,9 +125,9 @@ sub _poll_check { sub get_data { my ($self) = @_; - #main::print_log("[Venstar Colortouch] get_data initiated"); + main::print_log("[Venstar Colortouch] get_data initiated"); $self->poll; - $self->process_data; + $self->process_data unless ($self->{background} > 1); #for background tasks, data will be processed when process completed. } sub _init { @@ -109,69 +138,70 @@ sub _init { $state[2] = "cooling"; $state[3] = "lockout"; $state[4] = "error"; - my ( $isSuccessResponse1, $stat ) = $self->_get_JSON_data('api'); + my ( $isSuccessResponse1, $stat ) = $self->_get_JSON_data('api',"direct"); if ($isSuccessResponse1) { $self->{api_ver} = $stat->{api_ver}; - if ( ( $self->{api_ver} > 3 ) and ( $stat->{type} eq "residential" ) ) { - main::print_log( - "[Venstar Colortouch] Residental Venstar ColorTouch found with api level $self->{api_ver}" - ); - if ( $self->poll() ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Data Successfully Retrieved" ); + if ( ( $stat->{api_ver} > 3 ) and ( $stat->{type} eq "residential" or $stat->{type} eq "commercial" ) ) { + + $self->{type} = $stat->{type}; + + main::print_log("[Venstar Colortouch] $stat->{type} Venstar ColorTouch found with api level $stat->{api_ver}"); + if ( $self->poll("direct") ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Data Successfully Retrieved" ); $self->{active} = 1; $self->{previous}->{tempunits} = $self->{data}->{tempunits}; $self->{previous}->{name} = $self->{data}->{name}; foreach my $key1 ( keys $self->{data}->{info} ) { - $self->{previous}->{info}->{$key1} = - $self->{data}->{info}->{$key1}; + $self->{previous}->{info}->{$key1} = $self->{data}->{info}->{$key1}; } - $self->{previous}->{sensors}->{sensors}[0]->{temp} = - $self->{data}->{sensors}->{sensors}[0]->{temp}; - $self->{previous}->{sensors}->{sensors}[0]->{hum} = - $self->{data}->{sensors}->{sensors}[0]->{hum}; + $self->{previous}->{sensors}->{sensors}[0]->{temp} = $self->{data}->{sensors}->{sensors}[0]->{temp}; + $self->{previous}->{sensors}->{sensors}[0]->{hum} = $self->{data}->{sensors}->{sensors}[0]->{hum}; ## set states based on available mode +### Strange, if this set is here, then the timer is not defined. + #print "db: set " . $state[ $self->{data}->{info}->{state} ] . "=" . $self->{data}->{info}->{state} . "\n"; + #$self->set( $state[ $self->{data}->{info}->{state} ], 'poll' ); $self->print_info(); - $self->set( $state[ $self->{data}->{info}->{state} ], 'poll' ); } else { - main::print_log( - "[Venstar Colortouch] Problem retrieving initial data"); + main::print_log("[Venstar Colortouch] Problem retrieving initial data"); $self->{active} = 0; return ('1'); } } else { - main::print_log( - "[Venstar Colortouch] Unknown device " . $self->{host} ); + main::print_log("[Venstar Colortouch] Unknown device " . $self->{host} ); $self->{active} = 0; return ('1'); } } else { - main::print_log( "[Venstar Colortouch] Error. Unable to connect to " - . $self->{host} ); + main::print_log( "[Venstar Colortouch] Error. Unable to connect to " . $self->{host} ); $self->{active} = 0; return ('1'); } } sub poll { - my ($self) = @_; - - main::print_log("[Venstar Colortouch] Polling initiated") - if ( $self->{debug} ); - - my ( $isSuccessResponse1, $info ) = $self->_get_JSON_data('info'); - my ( $isSuccessResponse2, $sensors ) = $self->_get_JSON_data('sensors'); - - if ( $isSuccessResponse1 and $isSuccessResponse2 ) { + my ($self,$method) = @_; + $method = "" unless (defined $method); + if (($self->{background} > 1) and (lc $method ne "direct")) { + main::print_log("[Venstar Colortouch] Background Polling initiated") if ( $self->{debug} ); + } else { + main::print_log("[Venstar Colortouch] Direct Polling initiated") if ( $self->{debug} ); + } + #spawn off info. + #might had to add into the main loop to check if the object's data has changed. + #$self->{process_pid}->{{$self->{process}->pid} = "poll_info"; + my ( $isSuccessResponse1, $info ) = $self->_get_JSON_data('info',$method); + my ( $isSuccessResponse2, $sensors ) = $self->_get_JSON_data('sensors',$method); + + if (( $isSuccessResponse1 and $isSuccessResponse2 ) and (($self->{background} < 2) or (lc $method eq "direct"))) { + $self->{poll_data_timestamp} = &main::get_tickcount(); $self->{data}->{tempunits} = $info->{tempunits}; $self->{data}->{name} = $info->{name}; $self->{data}->{info} = $info; @@ -179,110 +209,234 @@ sub poll { $self->{data}->{timestamp} = time; $self->{data}->{retry} = 0; - #if (defined $self->{child_object}->{comm}) { - # if ($self->{child_object}->{comm}->state() ne "online") { - # main::print_log "[Venstar Colortouch:". $self->{data}->{name} . "]] 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'); - - #} else { - # main::print_log("[Venstar Colortouch:". $self->{data}->{name} . "] Problem retrieving poll data from " . $self->{host}); - # $self->{data}->{retry}++; - # if (defined $self->{child_object}->{comm}) { - # if ($self->{child_object}->{comm}->state() ne "offline") { - # main::print_log "[Venstar Colortouch:". $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ($self->{loglevel}); - # $self->{child_object}->{comm}->set("offline",'poll'); - # } - # } - # return ('0'); } } -#------------------------------------------------------------------------------------ -sub _get_JSON_data { - my ( $self, $mode ) = @_; - - unless ( $self->{updating} ) { - - $self->{updating} = 1; - my $ua = new LWP::UserAgent( keep_alive => 1 ); - $ua->timeout( $self->{timeout} ); - - my $host = $self->{host}; - - my $request = HTTP::Request->new( POST => "http://$host/$rest{$mode}" ); - $request->content_type("application/x-www-form-urlencoded"); - - my $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" - if $self->{debug}; - - my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if $self->{debug}; - my $isSuccessResponse = $responseCode < 400; - $self->{updating} = 0; - if ( !$isSuccessResponse ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, failed to get data. Response code $responseCode" - ); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} eq "online" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } - return ('0'); - } - else { - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} eq "offline" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to online..." - if ( $self->{loglevel} ); - $self->{status} = "online"; - $self->{child_object}->{comm}->set( "online", 'poll' ); - } - } - } - my $response; - eval { $response = JSON::XS->new->decode( $responseObj->content ); }; - +sub process_check { + my ($self) = @_; +#Need to catch error 500's 403's and update communication tracker and success:false messages to write to log +# as a safety measure, just check that the timer's active + if ( defined $self->{timer} ) { + if ($self->{timer}->inactive()) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] INFO, timer is not active. ") if (($self->{debug}) or ($self->{loglevel} > 2)); + #$self->start_timer(); + } + } else { +# main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! timer is not defined!"); + } +# $self->start_timer if ($self->{timer}->inactive()); + return unless (defined $self->{poll_process}); + if ($self->{poll_process}->done_now()) { + my $com_status = "online"; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background poll " . $self->{poll_process_mode} . " process completed") if ($self->{debug}); + + my $file_data = &main::file_read($self->{poll_data_file}); + #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 =~ /({.*})/; + print "debug: json_data=$json_data\n" if ($self->{debug}); + unless ($file_data) { + main::print_log("[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! no data returned by poll"); + return; + } + my $data; + eval { $data = JSON::XS->new->decode( $json_data ); }; # catch crashes: if ($@) { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR! JSON parser crashed! $@\n"; - return ('0'); + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; + $com_status = "offline"; + } else { + if (keys $data) { + $self->{poll_data_timestamp} = &main::get_tickcount(); + if ($self->{poll_process_mode} eq "info") { + $self->{data}->{tempunits} = $data->{tempunits}; + $self->{data}->{name} = $data->{name}; + $self->{data}->{info} = $data; + $self->{data}->{timestamp} = &main::get_tickcount(); + } elsif ($self->{poll_process_mode} eq "sensors") { + $self->{data}->{sensors} = $data; + $self->{data}->{timestamp} = &main::get_tickcount(); + } + $self->process_data(); + } else { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; + $com_status = "offline"; + } } - else { - return ( $isSuccessResponse, $response ); + if (scalar @{$self->{poll_queue}}) { + my $cmd_string = shift @{$self->{poll_queue}}; + my ($mode,$cmd) = split/\|/,$cmd_string; + $self->{poll_process}->set($cmd); + $self->{poll_process}->start(); + $self->{poll_process_pid}->{$self->{poll_process}->pid()}=$mode; #capture the type of information requested in order to parse; + $self->{poll_process_mode} = $mode; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Poll Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd") if ($self->{debug}); + + } + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } + return unless (defined $self->{cmd_process}); + if ($self->{cmd_process}->done_now()) { + my $com_status = "online"; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background Command " . $self->{cmd_process_name} . " process completed") if ($self->{debug}); + + my $file_data = &main::file_read($self->{cmd_data_file}); + unless ($file_data) { + main::print_log("[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! no data returned by command"); + return; + } + #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 $data; + eval { $data = JSON::XS->new->decode( $json_data ); }; + # catch crashes: + if ($@) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n"; + $com_status = "offline"; + + } else { + 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; + $self->poll; + } else { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING Issued command was unsuccessful (success=" . $data->{success} .") , retrying..."; + if ($self->{cmd_process_retry} > $self->{cmd_process_retry_limit}) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..."; + shift @{$self->{cmd_queue}}; + $self->{cmd_process_retry} = 0; + $com_status = "offline"; + } else { + $self->{cmd_process_retry}++; + } + } + } else { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..."; + $com_status = "offline"; + } } - } - else { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, not fetching data due to operation in progress" ); - return ('0'); - } + if (scalar @{$self->{cmd_queue}}) { + my $cmd = @{$self->{cmd_queue}}[0]; #grab the first command, but don't take it off. + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd") if ($self->{debug}); + } + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to " . $com_status . "..." if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } + +} + +#------------------------------------------------------------------------------------ +sub _get_JSON_data { + my ( $self, $mode, $method ) = @_; + + if (($self->{background} > 1) and (lc $method ne "direct")) { + + my $cmd = 'get_url "http://' . $self->{host} . "/$rest{$mode}" . '"'; + 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( "[Venstar Colortouch:" . $self->{data}->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd") if ($self->{debug}); + + } else { + if (scalar @{$self->{poll_queue}} < $self->{max_poll_queue}) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{$self->{poll_queue}} . ". Queing command $mode, $cmd") if ($self->{debug}); + push @{$self->{poll_queue}}, "$mode|$cmd"; +#TODO: queue shouldn't grow for polling. Since a poll is 2 queries, the queue should only be 3 items. Otherwise it will grow every poll +# if there are device issues. + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Command discarded."); + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne "offline" ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } + } + } + } else { + unless ( $self->{updating} ) { + + $self->{updating} = 1; + my $ua = new LWP::UserAgent( keep_alive => 1 ); + $ua->timeout( $self->{timeout} ); + + my $host = $self->{host}; + + my $request = HTTP::Request->new( POST => "http://$host/$rest{$mode}" ); + $request->content_type("application/x-www-form-urlencoded"); + + my $responseObj = $ua->request($request); + print $responseObj->content . "\n--------------------\n" if $self->{debug}; + + my $responseCode = $responseObj->code; + print 'Response code: ' . $responseCode . "\n" if $self->{debug}; + my $isSuccessResponse = $responseCode < 400; + $self->{updating} = 0; + if ( !$isSuccessResponse ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, failed to get data. Response code $responseCode"); +print "Venstar. status=" . $self->{status}; +if (defined $self->{child_object}->{comm}) { + print " Tracker defined\n"; +} else { + print " Tracker UNDEFINED\n"; +} + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} eq "online" ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } + return ('0'); + } else { + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} eq "offline" ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to online..." if ( $self->{loglevel} ); + $self->{status} = "online"; + $self->{child_object}->{comm}->set( "online", 'poll' ); + } + } + } + my $response; + eval { $response = JSON::XS->new->decode( $responseObj->content ); }; + + # catch crashes: + if ($@) { + print "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON parser crashed! $@\n"; + return ('0'); + } else { + return ( $isSuccessResponse, $response ); + } + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, not fetching data due to operation in progress" ); + return ('0'); + } + } } sub _push_JSON_data { - my ( $self, $type, $params ) = @_; + my ( $self, $type, $params, $method ) = @_; - my ( @fan, @fanstate, @modename, @statename, @schedule, @home ); + my ( @fan, @fanstate, @modename, @statename, @schedule, @home, @schedulestat ); $fan[0] = "auto"; $fan[1] = "on"; $fanstate[0] = "off"; @@ -303,43 +457,119 @@ sub _push_JSON_data { $schedule[2] = "evening (occupied3)"; $schedule[3] = "night (occupied4)"; $schedule[255] = "inactive"; + $schedulestat[0] = "off"; + $schedulestat[1] = "on"; + +#4.08, schedulepart is now the schedule type. schedule 0 is off, my $cmd; + $method = "" unless (defined $method); + +#For background tasks, we want up to date data, ie returned within the last poll period. +#recursively calling the same subroutine might be a memory or performance hog, but need to effectively +#'suspend' the data push until we get valid data. + print "venstar, command: poll_timestamp=" . $self->{poll_data_timestamp} . " poll_seconds = " . $self->{config}->{poll_seconds} . " get_tickcount=" . &main::get_tickcount() . "\n";# if ($self->{debug}); + print "venstar, command: issuing poll due to old data\n" if (($self->{poll_data_timestamp} + ($self->{config}->{poll_seconds} * 1000)) < &main::get_tickcount()); + print "venstar, command: retrying command\n" if ($self->{poll_data_timestamp} + 300000 > &main::get_tickcount()); + + if (($self->{background} > 1) and (lc $method ne "direct")) { + if (($self->{poll_data_timestamp} + ($self->{config}->{poll_seconds} * 1000)) < &main::get_tickcount()) { + $self->poll() if (scalar @{$self->{poll_queue}} < $self->{max_poll_queue}); #once max reached, no sense adding more + if ($self->{poll_data_timestamp} + 300000 > &main::get_tickcount()) { #give up after 5 minutes of trying + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING: retrying command attempt due to stale poll data!" ); + &Venstar_Colortouch::_push_json_data($self,$type,$params); + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR: Abandoning command attempt due to stale poll data!" ); + return ('1'); + } + } + } + #print "VCT DB: $params\n"; - $self->stop_timer; #stop timer to prevent a clash of updates + $self->stop_timer unless ($self->{background} > 1); #stop timer to prevent a clash of updates if ( $type eq 'settings' ) { - #tempunits=0&away=0&schedule=0&hum_setpoint=0&dehum_setpoint=0 - # with v4.08 firmware, hum and dehum setpoints need to be at least 4 % points different - - my ( $newunits, $newaway, $newsched, $newhumsp, $newdehumsp ); + #for testing purposes, curl is: + #curl --data "tempunits=1&away=0&schedule=0&hum_setpoint=30&dehum_setpoint=35" http://ip/settings + +# my ( $isSuccessResponse, $thedata ) = $self->_get_setting_params; +# my %info = %$thedata; +# my %cinfo = %$thedata; +# my @changearr; +# +# while ($params =~ /(\w+)=(\d+)/g) { +# print "[Venstar Colortouch:" +# . $self->{data}->{name} +# . "] _push_JSON_data match: $1 = $2\n"; +# $info{$1} = $2; +# if ($info{$1} ne $cinfo{$1}) { +# main::print_log( "[Venstar Colortouch:" +# . $self->{data}->{name} +# . "] Changing $1 from $cinfo{$1} to $info{$1}" ); +# push(@changearr, "$1=$info{$1}"); +# } +# } + +# $cmd = join ('&', @changearr); +# main::print_log( "Sending Settings command $cmd to " . $self->{host} ) if $cmd; # if $self->{debug}; + + my ( $newunits, $newaway, $newholiday, $newsched, $newhumsp, $newdehumsp ); my ($units) = $params =~ /tempunits=(\d+)/; my ($away) = $params =~ /away=(\d+)/; + my ($holiday) = $params =~ /holiday=(\d+)/; + my ($override) = $params =~ /override=(\d+)/; + my ($overridetime) = $params =~ /overridetime=(\d+)/; + my ($forceunocc) = $params =~ /forceunocc=(\d+)/; my ($sched) = $params =~ /schedule=(\d+)/; my $hum; my ($humsp) = $params =~ /hum_setpoint=(\d+)/; - my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/ - ; #need to add in dehumidifier stuff at some point + my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/; #need to add in dehumidifier stuff at some point my $humidity_change = 0; - my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, - $cdehumsp ) - = $self->_get_setting_params; + my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ); + my ($choliday,$coverride,$coverridetime,$cforceunocc); + + + if (($self->{background} > 1) and (lc $method ne "direct")) { + $cunits = $self->{data}->{info}->{tempunits}; + $caway = $self->{data}->{info}->{away}; + $csched = $self->{data}->{info}->{schedule}; + $chumsp = $self->{data}->{info}->{hum_setpoint}; + $cdehumsp = $self->{data}->{info}->{dehum_setpoint}; + } else { + ($isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ) = $self->_get_setting_params; + #($choliday,$coverride,$coverridetime,$cforceunocc); + } $units = $cunits if ( not defined $units ); $units = 1 if ( ( $units eq "C" ) or ( $units eq "c" ) ); $units = 0 if ( ( $units eq "F" ) or ( $units eq "f" ) ); $away = $caway if ( not defined $away ); - $sched = $csched if ( not defined $sched ); + $holiday = $choliday if ( not defined $holiday ); + $override = $coverride if ( not defined $override ); + $overridetime = $coverridetime if ( not defined $overridetime ); + $forceunocc = $cforceunocc if ( not defined $forceunocc ); + $sched = $csched if ( not defined $sched ); $hum = $chum if ( not defined $hum ); - $humsp = $chumsp if ( not defined $humsp ); - $dehumsp = $cdehumsp if ( not defined $dehumsp ); + #v4.08, dehum_sp is humidify, and hum_sp is dehumidify. + if ($self->{api_ver} >=5) { + $humsp = $cdehumsp if ( not defined $humsp ); + } else { + $humsp = $chumsp if ( not defined $humsp ); + } + if ($self->{api_ver} >=5) { + $dehumsp = $chumsp if ( not defined $dehumsp ); + } else { + $dehumsp = $cdehumsp if ( not defined $dehumsp ); + } +#print "venstar db: params = $params\n"; +#print "units=$units, away=$away, sched=$sched, humsp=$humsp, dehumsp=$dehumsp\n"; +#print "cunits=$cunits, caway=$caway, csched=$csched, chum=$chum, chumsp=$chumsp, cdehumsp=$cdehumsp\n"; + if ( $cunits ne $units ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Units from $cunits to $units" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Units from $cunits to $units" ); $newunits = $units; } else { @@ -347,58 +577,80 @@ sub _push_JSON_data { } if ( $caway ne $away ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Away from $home[$caway] to $home[$away]" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $home[$caway] to $home[$away]" ); $newaway = $away; } else { $newaway = $caway; } - if ( $csched ne $sched ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Schedule from $schedule[$csched] to $schedule[$sched]" - ); - $newsched = $sched; + if ( ($self->{type} eq "commercial") and ($choliday ne $holiday) ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $choliday to $holiday" ); + $newholiday = $holiday; } else { - $newsched = $csched; + $newholiday = $choliday; } - if ( $chumsp ne $humsp ) { - print - "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing Humidity Setpoint from $chumsp to $humsp\n"; - $newhumsp = $humsp; - $humidity_change = 1; + if ( $caway ne $away ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Away from $caway to $away" ); + $newaway = $away; } else { - $newhumsp = $chumsp; + $newaway = $caway; } - if ( $cdehumsp ne $dehumsp ) { - print - "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing (De)humidity Setpoint from $cdehumsp to $dehumsp\n"; - $newdehumsp = $dehumsp; + if ( $csched ne $sched ) { + if ($self->{api_ver} >=5) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Schedule from $schedulestat[$csched] to $schedulestat[$sched]"); + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Schedule from $schedule[$csched] to $schedule[$sched]"); + } + $newsched = $sched; } else { - $newdehumsp = $cdehumsp; + $newsched = $csched; + } + if ($self->{api_ver} >=5) { + if ( $cdehumsp ne $humsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Humidity Setpoint from $cdehumsp to $humsp\n" ); + $newhumsp = $humsp; + $humidity_change = 1; + } else { + $newhumsp = $cdehumsp; + } + + if ( $chumsp ne $dehumsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] *Changing Dehumidity Setpoint from $chumsp to $dehumsp\n" ); + $newdehumsp = $dehumsp; + } else { + $newdehumsp = $chumsp; + } + } else { + if ( $chumsp ne $humsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Humidity Setpoint from $chumsp to $humsp\n" ); + $newhumsp = $humsp; + $humidity_change = 1; + } else { + $newhumsp = $chumsp; + } + + if ( $cdehumsp ne $dehumsp ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing Dehumidity Setpoint from $cdehumsp to $dehumsp\n" ); + $newdehumsp = $dehumsp; + } else { + $newdehumsp = $cdehumsp; + } } - # v4.08 needs 6 % point delta on humidity and dehumidity. - # if humidify is more then dehumidify, then set dehumidify to 6 + humidity - $newdehumsp = $newhumsp + 6 if ((($newhumsp >= $newdehumsp) or (($newdehumsp - $newhumsp) < 6))); +## to set v4.08, humidity to 32%, this is needed "tempunits=1&away=0&schedule=0&hum_setpoint=32&dehum_setpoint=38" +## so humidity setpoint hasn't changed? if ($self->{api_ver} >=5) { - $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched"; #&dehum_setpoint=$newhumsp&hum_setpoint=" . ($newhumsp + 1); - $cmd = "hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp" if ($humidity_change); - } else { - $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched&hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp"; - } + # v4.08 has changed humidification settings + # - need 6 % point delta on humidity and dehumidity. + $newdehumsp = $newhumsp + 6 if ((($newhumsp >= $newdehumsp) or (($newdehumsp - $newhumsp) < 6))); + } + $cmd = "tempunits=$newunits&away=$newaway&schedule=$newsched&hum_setpoint=$newhumsp&dehum_setpoint=$newdehumsp"; main::print_log( "Sending Settings command [$cmd] to " . $self->{host} ) if $self->{debug}; } @@ -411,32 +663,34 @@ sub _push_JSON_data { my ( $newmode, $newfan, $newcoolsp, $newheatsp ); my ($mode) = $params =~ /mode=(\d+)/; my ($fan) = $params =~ /fan=(\d+)/; - my ($heattemp) = $params =~ /heattemp=(\d+)/; #need decimals - my ($cooltemp) = $params =~ /cooltemp=(\d+)/; - - my ( - $isSuccessResponse, $cmode, $cfan, - $cheattemp, $ccooltemp, $setpointdelta, - $minheat, $maxheat, $mincool, - $maxcool - ) = $self->_get_control_params; + my ($heattemp) = $params =~ /heattemp=(\d+\.?\d?)/; #need decimals + my ($cooltemp) = $params =~ /cooltemp=(\d+\.?\d?)/; + + my ($isSuccessResponse, $cmode, $cfan, $cheattemp, $ccooltemp, $setpointdelta, $minheat, $maxheat, $mincool, $maxcool); + + if (($self->{background} > 1) and (lc $method ne "direct")) { + $cmode = $self->{data}->{info}->{mode}; + $cfan = $self->{data}->{info}->{fan}; + $cheattemp = $self->{data}->{info}->{heattemp}; + $ccooltemp = $self->{data}->{info}->{cooltemp}; + $setpointdelta = $self->{data}->{info}->{setpointdelta}; + $minheat = $self->{data}->{info}->{heattempmin}; + $maxheat = $self->{data}->{info}->{heattempmax}; + $mincool = $self->{data}->{info}->{cooltempmin}; + $maxcool = $self->{data}->{info}->{cooltempmax}; + + } else { + ($isSuccessResponse, $cmode, $cfan, $cheattemp, $ccooltemp, $setpointdelta, $minheat, $maxheat, $mincool, $maxcool) = $self->_get_control_params; + } $mode = $cmode if ( not defined $mode ); $fan = $cfan if ( not defined $fan ); $heattemp = $cheattemp if ( !$heattemp ); $cooltemp = $ccooltemp if ( !$cooltemp ); - main::print_log( - "data1=$isSuccessResponse,$cmode,$cfan,$cheattemp,$ccooltemp,$setpointdelta" - ) if $self->{debug}; #TODO pass object to get debug - main::print_log("data2=$mode,$fan,$heattemp,$cooltemp") - if $self->{debug}; #TODO debug if ( $cmode ne $mode ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing mode from $modename[$cmode] to $modename[$mode]" - ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing mode from $modename[$cmode] to $modename[$mode]"); $newmode = $mode; } else { @@ -444,9 +698,7 @@ sub _push_JSON_data { } if ( $cfan ne $fan ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing fan from $fanstate[$cfan] to $fanstate[$fan]" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing fan from $fan[$cfan] to $fan[$fan]" ); $newfan = $fan; } else { @@ -454,9 +706,7 @@ sub _push_JSON_data { } if ( $heattemp ne $cheattemp ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing heat setpoint from $cheattemp to $heattemp" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing heat setpoint from $cheattemp to $heattemp" ); $newheatsp = $heattemp; } else { @@ -464,9 +714,7 @@ sub _push_JSON_data { } if ( $cooltemp ne $ccooltemp ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Changing cool setpoint from $ccooltemp to $cooltemp" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Changing cool setpoint from $ccooltemp to $cooltemp" ); $newcoolsp = $cooltemp; } else { @@ -474,35 +722,23 @@ sub _push_JSON_data { } if ( ( $newcoolsp > $maxcool ) or ( $newcoolsp < $mincool ) ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Error: New cooling setpoint $newcoolsp out of bounds $mincool - $maxcool" - ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Error: New cooling setpoint $newcoolsp out of bounds $mincool - $maxcool"); $newcoolsp = $ccooltemp; } if ( ( $newheatsp > $maxheat ) or ( $newheatsp < $minheat ) ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Error: New heating setpoint $newheatsp out of bounds $minheat - $maxheat" - ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Error: New heating setpoint $newheatsp out of bounds $minheat - $maxheat"); $newheatsp = $cheattemp; } if ( ( $newheatsp - $newcoolsp ) > $setpointdelta ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Error: Cooling ($newcoolsp) and Heating ($newheatsp) setpoints need to be less than setpoint $setpointdelta" - ); - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Not setting setpoints" ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Error: Cooling ($newcoolsp) and Heating ($newheatsp) setpoints need to be less than setpoint $setpointdelta"); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Not setting setpoints" ); $newcoolsp = $ccooltemp; $newheatsp = $cheattemp; } - $cmd = - "mode=$newmode&fan=$newfan&heattemp=$newheatsp&cooltemp=$newcoolsp"; + $cmd = "mode=$newmode&fan=$newfan&heattemp=$newheatsp&cooltemp=$newcoolsp"; main::print_log( "Sending Control command $cmd to " . $self->{host} ) if $self->{debug}; } @@ -511,34 +747,55 @@ sub _push_JSON_data { main::print_log("Unknown mode!"); return ( '1', 'error' ); } + my $isSuccessResponse; + my $response; + if (($self->{background}) and (lc $method ne "direct")) { + $isSuccessResponse = 1; #set these to successful, since the process_data will indicate if a setting was unsuccessful. + $response = "success"; + my $cmd = 'get_url -post "' .$cmd . '" "http://' . $self->{host} . "/$rest{$type}" . '"'; + push @{$self->{cmd_queue}}, "$cmd"; + if ($self->{cmd_process}->done()) { + $self->{cmd_process}->set($cmd); + $self->{cmd_process_name} = $cmd; + $self->{cmd_process}->start(); + $self->{cmd_process_retry} = 0; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Backgrounding " . $self->{cmd_process}->pid() . " command $cmd") if ($self->{debug}); - my $ua = new LWP::UserAgent( keep_alive => 1 ); - $ua->timeout( $self->{timeout} ); - - my $host = $self->{host}; - - my $request = HTTP::Request->new( POST => "http://$host/$rest{$type}" ); - $request->content_type("application/x-www-form-urlencoded"); - $request->content($cmd) if $cmd; - - my $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" if $self->{debug}; - - my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if $self->{debug}; - my $isSuccessResponse = $responseCode < 400; - if ( !$isSuccessResponse ) { - main::print_log( "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Warning, failed to push data. Response code $responseCode" ); + } else { + if (scalar @{$self->{cmd_queue}} < $self->{max_cmd_queue}) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{$self->{cmd_queue}} . ". Queing command $cmd") if ($self->{debug}); + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded."); + } + } + } else { + + my $ua = new LWP::UserAgent( keep_alive => 1 ); + $ua->timeout( $self->{timeout} ); + + my $host = $self->{host}; + + my $request = HTTP::Request->new( POST => "http://$host/$rest{$type}" ); + $request->content_type("application/x-www-form-urlencoded"); + $request->content($cmd) if $cmd; + + my $responseObj = $ua->request($request); + print $responseObj->content . "\n--------------------\n" if $self->{debug}; + + my $responseCode = $responseObj->code; + print 'Response code: ' . $responseCode . "\n" if $self->{debug}; + $isSuccessResponse = $responseCode < 400; + if ( !$isSuccessResponse ) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Warning, failed to push data. Response code $responseCode" ); +print "Venstar. status=" . $self->{status}; +if (defined $self->{child_object}->{comm}) { + print " Tracker defined\n"; +} else { + print " Tracker UNDEFINED\n"; +} if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} eq "online" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); $self->{status} = "offline"; $self->{child_object}->{comm}->set( "offline", 'poll' ); } @@ -557,15 +814,20 @@ sub _push_JSON_data { $self->{child_object}->{comm}->set( "online", 'poll' ); } } - } - #my $response = JSON::XS->new->decode ($responseObj->content); - + ($response) = $responseObj->content =~ /\{\"(.*)\":/; + } + } #print Dumper $response if $self->{debug}; - my ($response) = $responseObj->content =~ /\{\"(.*)\":/; print "response=$response\n" if $self->{debug}; - $self->poll if ( $response eq "success" ); - $self->start_timer; + +# remove this poll since the stat pauses after being set? +# if ( $response eq "success" ) { +# $self->poll(); +# $self->process_data() unless ($self->{background} >1); +# } + $self->start_timer unless ($self->{background} > 1); + return ( $isSuccessResponse, $response ); } @@ -594,7 +856,7 @@ sub _get_control_params { sub _get_setting_params { my ($self) = @_; - my ( $isSuccessResponse, $info ) = $self->_get_JSON_data('info'); + my ( $isSuccessResponse, $info ) = $self->_get_JSON_data('info',"direct"); return ( $isSuccessResponse, $info->{tempunits}, $info->{away}, $info->{schedule}, $info->{hum}, $info->{hum_setpoint}, $info->{dehum_setpoint} ); @@ -615,7 +877,6 @@ sub stop_timer { sub start_timer { my ($self) = @_; - if ( defined $self->{timer} ) { $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &Venstar_Colortouch::_poll_check($self) }, -1 ); @@ -627,6 +888,19 @@ sub start_timer { } } +sub background_enable { + my ($self,$level) = @_; + $level = 1 unless (defined $level); + $self->{background} = $level; + main::print_log("[Venstar Colortouch:" . $self->{data}->{name} . "] Background mode enabled (level " . $level . ")"); +} + +sub background_disable { + my ($self) = @_; + $self->{background} = 0; + main::print_log("[Venstar Colortouch:" . $self->{data}->{name} . "] Background mode disabled. Now in direct mode."); +} + sub print_info { my ($self) = @_; @@ -672,6 +946,16 @@ sub print_info { . $type . " Thermostat with API level " . $self->{api_ver} ); + if ($self->{background}) { + if ($self->{background} > 1) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Background mode enabled"); + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Hybrid mode enabled (Background commands, direct polling)"); + } + } else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Direct mode enabled"); + } + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Fan mode is set to " @@ -792,6 +1076,8 @@ sub process_data { # set state of self for state # for any registered child selfs, update their state if + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Processing Data...") if ($self->{debug}); + my ( @fan, @fanstate, @mode, @state, @schedule ); $fan[0] = "auto"; $fan[1] = "on"; @@ -888,6 +1174,13 @@ sub process_data { . "] Mode changed from $mode[$self->{previous}->{info}->{mode}] to $mode[$self->{data}->{info}->{mode}]" ) if ( $self->{loglevel} ); $self->{previous}->{info}->{mode} = $self->{data}->{info}->{mode}; + if ( defined $self->{child_object}->{mode} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Mode Child object found. Updating..." + if ( $self->{loglevel} ); + $self->{child_object}->{mode} + ->set( $mode[$self->{data}->{info}->{mode}] ); + } + } if ( $self->{previous}->{info}->{state} != $self->{data}->{info}->{state} ) @@ -935,17 +1228,59 @@ sub process_data { $self->{data}->{info}->{schedulepart}; } - if ( $self->{previous}->{info}->{away} != $self->{data}->{info}->{away} ) { + if ( $self->{type} eq "residential" and $self->{previous}->{info}->{away} != $self->{data}->{info}->{away} ) { my $away = "home mode"; $away = "away mode" if ( $self->{data}->{info}->{away} ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} - . "] Thermostat occupency changed to" + . "] Thermostat occupancy changed to" . $away ) if ( $self->{loglevel} ); $self->{previous}->{info}->{away} = $self->{data}->{info}->{away}; } + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{holiday} != $self->{data}->{info}->{holiday} ) { + my $holiday = "observing holiday"; + $holiday = "no holiday" if ( $self->{data}->{info}->{holiday} ); + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat holiday changed to" + . $holiday ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{holiday} = $self->{data}->{info}->{holiday}; + } + + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{override} != $self->{data}->{info}->{override} ) { + my $override = "off"; + $override = "on" if ( $self->{data}->{info}->{override} ); + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat override changed to" + . $override ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{override} = $self->{data}->{info}->{override}; + } + + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{overridetime} != $self->{data}->{info}->{overridetime} ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat overridetime changed to" + . $self->{data}->{info}->{overridetime} ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{overridetime} = $self->{data}->{info}->{overridetime}; + } + + if ( $self->{type} eq "commercial" and $self->{previous}->{info}->{forceunocc} != $self->{data}->{info}->{forceunocc} ) { + my $forceunocc = "off"; + $forceunocc = "on" if ( $self->{data}->{info}->{forceunocc} ); + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Thermostat forceunocc changed to" + . $forceunocc ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{forceunocc} = $self->{data}->{info}->{forceunocc}; + } + if ( $self->{previous}->{info}->{spacetemp} != $self->{data}->{info}->{spacetemp} ) { @@ -972,6 +1307,12 @@ sub process_data { ) if ( $self->{loglevel} ); $self->{previous}->{info}->{heattemp} = $self->{data}->{info}->{heattemp}; + if ( defined $self->{child_object}->{heat_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Heat Setpoint Child object found. Updating..." + if ( $self->{loglevel} ); + $self->{child_object}->{heat_sp} + ->set( $self->{data}->{info}->{heattemp}, 'poll' ); + } } if ( $self->{previous}->{info}->{heattempmin} != @@ -1005,6 +1346,12 @@ sub process_data { ) if ( $self->{loglevel} ); $self->{previous}->{info}->{cooltemp} = $self->{data}->{info}->{cooltemp}; + if ( defined $self->{child_object}->{cool_sp} ) { + main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Cooling Setpoint Child object found. Updating..." + if ( $self->{loglevel} ); + $self->{child_object}->{cool_sp} + ->set( $self->{data}->{info}->{cooltemp}, 'poll' ); + } } if ( $self->{previous}->{info}->{cooltempmin} != @@ -1058,6 +1405,10 @@ sub process_data { } } + #if ($self->{previous}->{info}->{hum} != $self->{data}->{info}->{hum}) { + # main::print_log("[Venstar Colortouch:". $self->{data}->{name} . "] Thermostat humidity changed from $self->{previous}->{info}->{hum} to $self->{data}->{info}->{hum}") if ($self->{loglevel}); + # $self->{previous}->{info}->{hum} = $self->{data}->{info}->{hum}; + #} if ( $self->{previous}->{info}->{setpointdelta} != $self->{data}->{info}->{setpointdelta} ) @@ -1289,6 +1640,11 @@ sub get_mode_humid { } +sub get_debug { + my ($self) = @_; + return $self->{debug}; +} + #------------ # User control methods #tempunits=0&away=0&schedule=0&hum_setpoint=0&dehum_setpoint=0 @@ -1480,10 +1836,175 @@ sub set_mode { } sub set_away { + my ( $self, $value ) = @_; + return unless $self->{type} eq "residential"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown away mode $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'control', "away=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set away to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} - #I don't use this. Shouldn't just be this simple though. - #($isSuccessResponse3,$status) = push_JSON_data($host,'settings','away=0&schedule=1'); +sub set_holiday { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown holiday $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "holiday=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set holiday to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} +sub set_override { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown override $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "override=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set override to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} + +sub set_overridetime { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "overridetime=$value" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set overridetime to $value" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } +} + +sub set_forceunocc { + my ( $self, $value ) = @_; + return unless $self->{type} eq "commercial"; + my $num; + if ( lc $value eq "off" ) { + $num = 0; + } + elsif ( lc $value eq lc "on" ) { + $num = 1; + } + else { + main::print_log( "Venstar Colortouch:" + . $self->{data}->{name} + . "] Error, unknown forceunocc mode $value" ); + return ('0'); + } + my ( $isSuccessResponse, $status ) = + $self->_push_JSON_data( 'settings', "forceunocc=$num" ); + if ($isSuccessResponse) { + if ( $status eq "success" ) { + return (1); + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not set forceunocc to $num" ); + return (0); + } + } + else { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] Error. Could not send data to Thermostat" ); + return (0); + } } sub set_fan { @@ -1526,6 +2047,15 @@ sub set_fan { sub set_units { + #($isSuccessResponse3,$status) = push_JSON_data($host,'settings','away=0&schedule=1'); + +} + +sub set_debug { + my ( $self, $debug ) = @_; + $self->{debug} = $debug if ($debug); + $self->{debug} = 0 + if ( $self->{debug} < 0 ); } sub set { @@ -1656,6 +2186,8 @@ sub new { bless $self, $class; $$self{master_object} = $object; + push( @{ $$self{states} }, '0','5','10','15','20','25','30','35','40' ); + $object->register( $self, 'hum_sp' ); $self->set( $object->get_sp_hum, 'poll' ); @@ -1742,4 +2274,136 @@ sub set { } } -1; +package Venstar_Colortouch_Mode; + +@Venstar_Colortouch_Mode::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + + $object->register( $self, 'mode' ); + $self->set( $object->get_mode, 'poll' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( ( lc $p_state eq "cooling" ) or ( lc $p_state eq "heating" ) or ( lc $p_state eq "cool" ) or ( lc $p_state eq "heat" ) or ( lc $p_state eq "auto" ) or ( lc $p_state eq "off" ) ) { + $$self{master_object}->set_mode($p_state); + } + else { + main::print_log( + "[Venstar Colortouch Mode] Error. Unknown set state $p_state" + ); + } + } +} + +package Venstar_Colortouch_Heat_sp; + +@Venstar_Colortouch_Heat_sp::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $scale ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + if ((defined $scale) and (lc $scale eq "F")) { + push( @{ $$self{states} }, '65','66','67','68','69','70','71','72','73','74','75','76','77','78','79','80' ); + $self->{lower_limit} = 65; + $self->{upper_limit} = 80; + } else { + push( @{ $$self{states} }, '17','17.5','18','18.5','19','19.5','20','20.5','21','21.5','22','22.5','23','23.5','24','24.5','25','25.5','26' ); + $self->{lower_limit} = 17; + $self->{upper_limit} = 26; + } + + $object->register( $self, 'heat_sp' ); + $self->set( $object->get_sp_heat, 'poll' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( ( $p_state >= 0 ) and ( $p_state <= 98 ) ) { + if (($p_state >= $self->{lower_limit}) and ($p_state <= $self->{upper_limit})) { + $$self{master_object}->set_heat_sp($p_state); + } else { + main::print_log("[Venstar Colortouch Heat_SP] Error. $p_state out of limits (" . $self->{lower_limit} . " to " . $self->{upper_limit} . ")"); + } + } + else { + main::print_log( + "[Venstar Colortouch Heat_SP] Error. Unknown set state $p_state" + ); + } + } +} + +package Venstar_Colortouch_Cool_sp; + +@Venstar_Colortouch_Cool_sp::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $scale ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + if ((defined $scale) and (lc $scale eq "F")) { + push( @{ $$self{states} }, '58','59','60','61','62','63','64','65','66','67','68','69','70','71','72','73','74','75','76','77','78','79','80' ); + $self->{lower_limit} = 58; + $self->{upper_limit} = 80; + } else { + push( @{ $$self{states} }, '17','17.5','18','18.5','19','19.5','20','20.5','21','21.5','22','22.5','23','23.5','24','24.5','25','25.5','26' ); + $self->{lower_limit} = 17; + $self->{upper_limit} = 26; + } + + $object->register( $self, 'cool_sp' ); + $self->set( $object->get_sp_cool, 'poll' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + if ( ( $p_state >= 0 ) and ( $p_state <= 98 ) ) { + if (($p_state >= $self->{lower_limit}) and ($p_state <= $self->{upper_limit})) { + $$self{master_object}->set_cool_sp($p_state); + } else { + main::print_log("[Venstar Colortouch Cool_SP] Error. $p_state out of limits (" . $self->{lower_limit} . " to " . $self->{upper_limit} . ")"); + } + } + else { + main::print_log("[Venstar Colortouch Cool_SP] Error. Unknown set state $p_state"); + } + } +} + +1; \ No newline at end of file From d0adef9715aa9684f61ada2f9908c521a47fb220 Mon Sep 17 00:00:00 2001 From: H Plato Date: Tue, 18 Oct 2016 20:38:58 -0600 Subject: [PATCH 060/209] Homebridge v3 - supports IOS10 and Home app --- code/common/homebridge.pl | 327 +++++++++++++++++++++++++++ code/common/homebridge_gen_config.pl | 172 -------------- 2 files changed, 327 insertions(+), 172 deletions(-) create mode 100644 code/common/homebridge.pl delete mode 100644 code/common/homebridge_gen_config.pl diff --git a/code/common/homebridge.pl b/code/common/homebridge.pl new file mode 100644 index 000000000..c24c7e8f0 --- /dev/null +++ b/code/common/homebridge.pl @@ -0,0 +1,327 @@ +# Category = HomeKit Integration + +#@ This module generates a config.json to be used by the homebridge system +#@ To use several groups need to be set up: +#@ HB__ where type is LIGHT, LOCK, FAN, GARAGEDOOR, BLINDS, SWITCH, THERMOSTAT +#@ Thermostat control only tested with a few models. + + +my $hb_debug = 1; +my $port = $config_parms{homebridge_port}; +$port = 51826 unless ($port); +my $name = $config_parms{homebridge_name}; +$name = "Homebridge" unless ($name); +my $pin = $config_parms{homebridge_pin}; +$pin = "031-45-154" unless ($pin); +my $username = $config_parms{homebridge_username}; +$username = "CC:22:3D:E3:CE:30" unless ($username); +my $version = "3"; +my $filepath = $config_parms{data_dir} . "/homebridge_config.json"; +$filepath = $config_parms{homebridge_config_dir} . "/config.json" if (defined $config_parms{homebridge_config_dir}); +my $acc_count; +$v_generate_hb_config = new Voice_Cmd("Generate new Homebridge config.json file"); +$v_restart_hb_server = new Voice_Cmd("[start,stop,restart] Homebridge Server"); +my $units = "C"; +$units = $config_parms{homebridge_temp_units} if (defined $config_parms{homebridge_temp_units}); + +if (my $action = said $v_restart_hb_server) { + if (defined $config_parms{homebridge_service_path}) { + print_log "[Homebridge]: " . $action . "ing the Homebridge Server..."; + my $cmd = $config_parms{homebridge_service_path} . " " . $action; + my $r = system ($cmd); + if ($r != 0) { + print_log "[Homebridge]: Warning, couldn't control homebridge service: $r" + } + } else { + print_log "[Homebridge]: Error, homebridge service path not defined" + } +} + +if (said $v_generate_hb_config) { + my $config_json = "{\n\t\"bridge\": {\n"; + $config_json .= "\t\t\"name\": \"" . $name . "\",\n"; + $config_json .= "\t\t\"username\": \"" . $username . "\",\n"; + $config_json .= "\t\t\"port\": " . $port . ",\n"; + $config_json .= "\t\t\"pin\": \"" . $pin . "\"\n\t},\n"; + $config_json .= "\t\"description\": \"MH Generated HomeKit Configuration v" . $version . " " . &time_date_stamp(17) . "\",\n"; + + $config_json .= "\n\t\"accessories\": [\n"; + $acc_count = 0; + $config_json .= add_group("fan"); + $config_json .= add_group("switch"); + $config_json .= add_group("light"); + $config_json .= add_group("lock"); + $config_json .= add_group("garagedoor"); + $config_json .= add_group("blinds"); + $config_json .= add_group("thermostat"); + + $config_json .= "\t\t}\n\t]\n}\n"; + print_log "[Homebridge]: Writing configuration to $filepath..."; + #print_log $config_json; + file_write($filepath, $config_json); +} + + +sub add_group { + my ($type) = @_; + my %url_types; + $url_types{lock}{on} = "lock"; + $url_types{lock}{off} = "unlock"; + $url_types{blinds}{on} = "up"; + $url_types{blinds}{off} = "down"; + $url_types{garagedoor}{on} = "up"; + $url_types{garagedoor}{off} = "down"; + my $groupname = "HB__" . (uc $type); + my $group = &get_object_by_name($groupname); + print_log "gn=$groupname"; + return unless ($group); + my $text = ""; + for my $member (list $group) { + $text .= "\t\t},\n" if ($acc_count > 0 ); + $acc_count++; + $text .= "\t\t{\n"; + $text .= "\t\t\"accessory\": \"HttpMulti\",\n"; + my $name = $member->{object_name}; + $name =~ s/_/ /g; + $name =~ s/\$//g; + $name = $member->{label} if (defined $member->{label}); + $text .= "\t\t\"name\": \"" . $name . "\",\n"; + if ($type eq "thermostat") { + my $name2 = $member->{object_name}; + $name2 =~ s/\$//g; + $text .= "\t\t\"setpoint_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/sub?hb_thermo_setpoint(%27" . $name2 . "%27,%VALUE%)\",\n"; + } else { + my $on = "on"; + $on = $url_types{$type}{on} if (defined $url_types{$type}{on}); + my $off = "off"; + $off = $url_types{$type}{off} if (defined $url_types{$type}{off}); + $text .= "\t\t\"" . $on . "_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=" .$on . "\",\n"; + $text .= "\t\t\"" . $off . "_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=" .$off . "\",\n"; + $text .= "\t\t\"brightness_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=%VALUE%\",\n" if ($type eq "light"); + $text .= "\t\t\"speed_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=%VALUE%\",\n" if ($type eq "fan"); + if ($type eq "thermostat") { + $text .= "\t\t\"mode_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_set_state%28" . $member->{object_name} .",%VALUE%%29\",\n"; + $text .= "\t\t\"status_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_get_state%28" . $member->{object_name} ."," . $type . "%29\",\n"; + $text .= "\t\t\"setpoint_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_set_setpoint%28" . $member->{object_name} . "%29\",\n"; + $text .= "\t\t\"gettemp_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_get_setpoint%28" . $member->{object_name} . "%29\",\n"; + $text .= "\t\t\"unit_type\": \"" . $units . "\",\n"; + } else { + my $obj_name = $member->{object_name}; + $obj_name =~ s/^\$//; #remove $ since the web sub system doesn't seem to like it. + $text .= "\t\t\"status_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SUB?hb_status%28" . $obj_name ."%29\",\n"; + } + + } + $text .= "\t\t\"deviceType\": \"" . $type . "\"\n"; + } + return $text; +} + +#curl "http://127.0.0.1:8080/sub?hb_status%28test_light%29" #() +sub hb_status { + my ($item,$type) = @_; + my $object = &get_object_by_name($item); + my $data = ""; + unless (defined $object) { + print_log "[Homebridge: hb_status]: Error, unknown object $item"; + } else { + my $state = lc $object->state; + if (($state eq "on") or ($state =~ /^lock/i) or ($state =~ /^close/)) { + $data = "1"; + } elsif (($state eq "off") or ($state =~ /^unlock/i) or ($state =~ /^open/) or ($state =~ /^down/)) { + $data = "0"; + } elsif ($state =~ /^low/) { + $data = "20"; + } elsif ($state =~ /^med/) { + $data = "50"; + } elsif ($state =~ /^high/) { + $data = "70"; + } elsif ($state =~ /^up/) { + $data = "100"; + } else { + ($data) = $state =~ /(\d+)/; + } + print_log "[Homebridge]: Warning, no state data to return!" if ($data eq ""); + print_log "[Homebridge]: Status request: item=$item state=$state status=[$data]\n" if ($hb_debug); + } + return <get_mode(); + print_log "[Homebridge]: Thermostat Venstar Colortouch with mode " . $mode . " found"; + if ($mode =~ /^auto/i) { + $data = "3"; + } elsif ($mode =~ /^cool/i) { + $data = "2"; + } elsif ($mode =~ /^heat/i) { + $data = "1"; + } else { + $data = "0"; + } + } + print_log "[Homebridge]: Thermostat State request: item=$item mode=$mode status=[$data]\n" if ($hb_debug); + } + return <get_sched() eq "on") { + print_log "[Homebridge]: Thermostat on a schedule, turning off schedule for override"; + $object -> set_schedule("off"); + $sp_delay = 5; + } + my $mode = "off"; + if ($value == 1) { + $mode = "heat"; + } elsif ($value == 2) { + $mode = "cool"; + } elsif ($value == 3) { + $mode = "auto"; + } + print_log "[Homebridge]: Setting thermostat to $mode"; + if ($sp_delay) { + eval_with_timer '$' . $item . '->set_mode(' . $mode . ');', $sp_delay; + } else { + $object -> set_mode($mode); + } + + } elsif (UNIVERSAL::isa($object,'Nest_Thermostat')) { + print_log "[Homebridge]: Nest Thermostat found"; + + } elsif (UNIVERSAL::isa($object,'Insteon::Thermostat')) { + print_log "[Homebridge]: Insteon Thermostat found"; + + } else { + print_log "Unsupported Thermostat type"; + } +} + +sub hb_thermo_get_setpoint { + my ($item) = @_; + print_log "[Homebridge]: Get Thermostat Current setpoint"; + my $data = "0"; + my $mode = "off"; + my $object = &get_object_by_name($item); + unless (defined $object) { + print_log "[Homebridge: hb_thermo_state]: Error, unknown object $object"; + } else { + if (UNIVERSAL::isa($object,'Venstar_Colortouch')) { + $object->get_mode(); + print_log "[Homebridge]: Thermostat Venstar Colortouch with mode " . $mode . " found"; + if ($mode =~ /^cool/) { + $data = $object -> get_cool_sp(); + } else { + $data = $object -> get_heat_sp(); + } + } + } + print_log "[Homebridge]: Thermostat State request: item=$item mode=$mode status=[$data]\n" if ($hb_debug); + return <get_sched() eq "on") { + print_log "[Homebridge]: Thermostat on a schedule, turning off schedule for override"; + $object -> set_schedule("off"); + $sp_delay = 5; + } + my $auto_mode = ""; + $auto_mode = &calc_auto_mode($value,$object->get_temp()) if ($object->get_mode() eq "auto"); + print_log "[Homebridge]: Thermostat calc mode is $auto_mode" if ($auto_mode); + if (($object->get_mode() eq "cooling") or ($auto_mode eq "cool")) { + if ($sp_delay) { + eval_with_timer '$' . $item . '->set_cool_sp(' . $value . ');', $sp_delay; + } else { + $object -> set_cool_sp($value); + } + } else { + if ($sp_delay) { + eval_with_timer '$' . $item . '->set_heat_sp(' . $value . ');', $sp_delay; + } else { + $object -> set_heat_sp($value); + } + } + + } elsif (UNIVERSAL::isa($object,'Nest_Thermostat')) { + print_log "[Homebridge]: Nest Thermostat found"; + $object -> set_target_temp($value); + + } elsif (UNIVERSAL::isa($object,'Insteon::Thermostat')) { + print_log "[Homebridge]: Insteon Thermostat found"; + my $auto_mode = ""; + $auto_mode = &calc_auto_mode($value) if ($object->get_mode() eq "auto"); + print_log "[Homebridge]: Thermostat calc mode is $auto_mode" if ($auto_mode); + if (($object->get_mode() eq "cool") or ($auto_mode eq "cool")) { + $object -> cool_setpoint($value); + } else { + $object -> heat_setpoint($value); + } + } else { + print_log "Unsupported Thermostat type"; + } +} + +sub calc_auto_mode { + my ($value,$intemp,$outtemp) = @_; + + my $mode = "heat"; + my $cool_threshold = 8; #set to cool if outside less + my $outside = ""; + $outside = $Weather{Outdoor} if (defined $Weather{TempOutdoor}); + $outside = $outtemp if (defined $outtemp); + my $inside = ""; + $inside = $Weather{Inside} if (defined $Weather{TempInside}); + $inside = $intemp if (defined $intemp); + $mode = "cool" if ($value < $inside); + $mode = "heat" if (($value - $cool_threshold) > $outside); + + return $mode; +} + + + diff --git a/code/common/homebridge_gen_config.pl b/code/common/homebridge_gen_config.pl deleted file mode 100644 index 6bb2b1998..000000000 --- a/code/common/homebridge_gen_config.pl +++ /dev/null @@ -1,172 +0,0 @@ -# Category = HomeKit Integration - -#@ This module generates a config.json to be used by the homebridge system -#@ To use several groups need to be set up: -#@ HB__ where type is LIGHT, LOCK, FAN, GARAGEDOOR, BLINDS, SWITCH, THERMOSTAT -#@ Thermostat control only tested with a few models. -#@ requires homebridge-httpmulti accessory: https://github.com/hplato/homebridge-httpmulti - -# TODO: -# Status Calls: determine if an object is on or off - -# read_url = "http://mh/sub?hb_status('$item')"; - -my $port = $config_parms{homebridge_port}; -$port = 51826 unless ($port); -my $name = $config_parms{homebridge_name}; -$name = "Homebridge" unless ($name); -my $pin = $config_parms{homebridge_pin}; -$pin = "031-45-154" unless ($pin); -my $username = $config_parms{homebridge_username}; -$username = "CC:22:3D:E3:CE:30" unless ($username); -my $version = "2"; -my $filepath = $config_parms{data_dir} . "/homebridge_config.json"; -my $acc_count; -$v_generate_hb_config = new Voice_Cmd("Generate new Homebridge config.json file"); - -if (said $v_generate_hb_config) { - my $config_json = "{\n\t\"bridge\": {\n"; - $config_json .= "\t\t\"name\": \"" . $name . "\",\n"; - $config_json .= "\t\t\"username\": \"" . $username . "\",\n"; - $config_json .= "\t\t\"port\": " . $port . ",\n"; - $config_json .= "\t\t\"pin\": \"" . $pin . "\"\n\t},\n"; - $config_json .= "\t\"description\": \"MH Generated HomeKit Configuration v" . $version . " " . &time_date_stamp(17) . "\",\n"; - - $config_json .= "\n\t\"accessories\": [\n"; - $acc_count = 0; - $config_json .= add_group("fan"); - $config_json .= add_group("switch"); - $config_json .= add_group("light"); - $config_json .= add_group("lock"); - $config_json .= add_group("garagedoor"); - $config_json .= add_group("blinds"); - $config_json .= add_group("thermostat"); - - $config_json .= "\t\t}\n\t]\n}\n"; - print_log "Writing configuration to $filepath..."; - #print_log $config_json; - file_write($filepath, $config_json); -} - - -sub add_group { - my ($type) = @_; - my %url_types; - $url_types{lock}{on} = "locked"; - $url_types{lock}{off} = "unlocked"; - $url_types{blind}{on} = "up"; - $url_types{blind}{off} = "down"; - $url_types{garagedoor}{on} = "open"; - $url_types{garagedoor}{off} = "close"; - my $groupname = "HB__" . (uc $type); - my $group = &get_object_by_name($groupname); - print_log "gn=$groupname"; - return unless ($group); - my $text = ""; - for my $member (list $group) { - $text .= "\t\t},\n" if ($acc_count > 0 ); - $acc_count++; - $text .= "\t\t{\n"; - $text .= "\t\t\"accessory\": \"HttpMulti\",\n"; - my $name = $member->{object_name}; - $name =~ s/_/ /g; - $name =~ s/\$//g; - $name = $member->{label} if (defined $member->{label}); - $text .= "\t\t\"name\": \"" . $name . "\",\n"; - if ($type eq "thermostat") { - my $name2 = $member->{object_name}; - $name2 =~ s/\$//g; - $text .= "\t\t\"setpoint_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/sub?hb_thermo_setpoint(%27" . $name2 . "%27,%VALUE%)\",\n"; - } else { - my $on = "on"; - $on = $url_types{$type}{on} if (defined $url_types{$type}{on}); - my $off = "off"; - $off = $url_types{$type}{off} if (defined $url_types{$type}{off}); - $text .= "\t\t\"" . $on . "_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=" .$on . "\",\n"; - $text .= "\t\t\"" . $off . "_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=" .$off . "\",\n"; - $text .= "\t\t\"brightness_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=%VALUE%\",\n" if ($type eq "light"); - $text .= "\t\t\"speed_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=%VALUE%\",\n" if ($type eq "fan"); - } - $text .= "\t\t\"deviceType\": \"" . $type . "\"\n"; - } - return $text; -} - -sub hb_status { - my ($item) = @_; - my $object = &get_object_by_name($item); - my $state = $object->state; - my $status = "1 "; - $status = "0 " if (lc $state eq "off"); - print_log "Homebridge: Status request: item=$item status=$status\n"; - return "$status"; -} - -sub hb_thermo_setpoint { - my ($item,$value) = @_; - print_log "Homebridge: Temperature change request for $item to $value"; - my $object = &get_object_by_name($item); - - if (UNIVERSAL::isa($object,'Venstar_Colortouch')) { - print_log "Homebridge: Thermostat Venstar Colortouch found"; - my $sp_delay = 0; - if ($object->get_sched() eq "on") { - print_log "Thermostat on a schedule, turning off schedule for override"; - $object -> set_schedule("off"); - $sp_delay = 5; - } - my $auto_mode = ""; - $auto_mode = &calc_auto_mode($value,$object->get_temp()) if ($object->get_mode() eq "auto"); - print_log "Homebridge: Thermostat calc mode is $auto_mode" if ($auto_mode); - if (($object->get_mode() eq "cooling") or ($auto_mode eq "cool")) { - if ($sp_delay) { - eval_with_timer '$' . $item . '->set_cool_sp(' . $value . ');', $sp_delay; - } else { - $object -> set_cool_sp($value); - } - } else { - if ($sp_delay) { - eval_with_timer '$' . $item . '->set_heat_sp(' . $value . ');', $sp_delay; - } else { - $object -> set_heat_sp($value); - } - } - - } elsif (UNIVERSAL::isa($object,'Nest_Thermostat')) { - print_log "Homebridge: Nest Thermostat found"; - $object -> set_target_temp($value); - - } elsif (UNIVERSAL::isa($object,'Insteon::Thermostat')) { - print_log "Homebridge: Insteon Thermostat found"; - my $auto_mode = ""; - $auto_mode = &calc_auto_mode($value) if ($object->get_mode() eq "auto"); - print_log "Homebridge: Thermostat calc mode is $auto_mode" if ($auto_mode); - if (($object->get_mode() eq "cool") or ($auto_mode eq "cool")) { - $object -> cool_setpoint($value); - } else { - $object -> heat_setpoint($value); - } - } else { - print_log "Unsupported Thermostat type"; - } -} - -sub calc_auto_mode { - my ($value,$intemp,$outtemp) = @_; - - my $mode = "heat"; - my $cool_threshold = 8; #set to cool if outside less - $cool_threshold = $config_parms{homebridge_auto_sp_threshold} if (defined $config_parms{homebridge_auto_sp_threshold}); - my $outside = ""; - $outside = $Weather{Outdoor} if (defined $Weather{TempOutdoor}); - $outside = $outtemp if (defined $outtemp); - my $inside = ""; - $inside = $Weather{Inside} if (defined $Weather{TempInside}); - $inside = $intemp if (defined $intemp); - $mode = "cool" if ($value < $inside); - $mode = "heat" if (($value - $cool_threshold) > $outside); - - return $mode; -} - - From 39a395be125c82d4e792da7a31240850cbfa8cdd Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Thu, 20 Oct 2016 20:07:45 -0600 Subject: [PATCH 061/209] IA7 v1.3.600 -- updated staticpage to include direct control. Some cleanup of console.log's and comments --- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 90 +++++++++-------------------------- 2 files changed, 23 insertions(+), 69 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 8a28a6a19..d66a35d99 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.3.590 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.3.600 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 5058dd93c..9090772de 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,4 @@ -// v1.3 +// v1.3.600 var entity_store = {}; //global storage of entities var json_store = {}; @@ -202,7 +202,6 @@ function changePage (){ } else if (path.indexOf('prefs') === 0){ var pref_name = path.replace(/\prefs\/?/,''); - console.log("loadprefs() "+pref_name); loadPrefs(pref_name); } else if(URLHash._request == 'page'){ @@ -308,31 +307,20 @@ function loadPrefs (config_name){ //show ia7 prefs, args ia7_prefs, ia7_rrd_pref }); } html += "
    "; - console.log(config_data); - console.log("in prefs="+config_data.length); for (var i in config_data){ if ( typeof config_data[i] === 'object') { console.log("i "+i+":"); html += ""; for (var j in config_data[i]) { if ( typeof config_data[i][j] === 'object') { - //console.log("j "+j+":"); html += ""; - for (var k in config_data[i][j]){ - //console.log("k "+k+" = "+json_store.ia7_config[i][j][k]); html += ""; } - //html +=""; } else { - //console.log("j "+j+" = "+json_store.ia7_config[i][j]); html += "" } } - //html +=""; - } else { - //console.log("i "+i+" : "+json_store.ia7_config[i]); - //html += ""; } } @@ -358,7 +346,6 @@ function parseLinkData (link,data) { data = data.replace(/.*?<\/a>/img,''); } if (link == "/bin/items.pl") { - console.log("items data="+data); var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) data = data.replace(/href=\/bin\/items.pl/img, 'onclick="changePage()"'); data = data.replace(/\(back to top<\/a>\)/img, ''); @@ -377,14 +364,11 @@ function parseLinkData (link,data) { } if (link == "/bin/triggers.pl") { //fix links in the triggers modules var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) - //data = data.replace(/href=\/bin\/triggers.pl/img, 'href=/ia7/#_request=page&link=/bin/triggers.pl&'+coll_key); data = data.replace(/href=\/bin\/triggers.pl/img, 'onclick="changePage()"'); data = data.replace(/\(back to top<\/a>\)/img, ''); data = data.replace(/Trigger Index:/img,''); data = data.replace(/.*?<\/a>/img,''); - //data = data.replace(/onChange=\'form.submit\(\)\'/img,'onChange=\'this\.form\.submit\(\)\''); data = data.replace(/input name='resp' value="\/bin\/triggers.pl"/img, 'input name=\'resp\' value=\"/ia7/#_request=page&link=/bin/triggers.pl&'+coll_key+'\"'); - //console.log(data); } if (link == "/ia5/news/main.shtml") { //fix links in the email module 1 var coll_key = window.location.href.substr(window.location.href.indexOf('_collection_key')) @@ -427,12 +411,6 @@ function parseLinkData (link,data) { if (btn.attr('name') !== undefined) { form_data.push({name : btn.attr('name'), value : btn.attr('value')}); } - console.log("MHResponse Custom submit function "+ form.attr('action')); - console.log( $(this).serializeArray() ); - console.log( "btn: "+btn.attr('name')+"="+btn.attr('value')); -// if (btn.attr('value') !== undefined) { -// console.log("executing data!"); -// unless the btn attribute has a name, then don't push the data (prevent text fields $.ajax({ type: "POST", url: form.attr('action'), @@ -448,12 +426,9 @@ function parseLinkData (link,data) { var end = data.toLowerCase().indexOf(''); if (form.attr('action') === "/bin/triggers.pl?add" && ! data.match(/Not authorized to make updates/)) { - //location.reload(); changePage(); } else if (form.attr('action') === "/bin/iniedit.pl") { -// var pdata = parseLinkData("/bin/iniedit.pl",data); parseLinkData("/bin/iniedit.pl",data); -// $('#row_page').html(pdata); //TODO parse data } else { $('#lastResponse').find('.modal-body').html(data.substring(start, end)); @@ -463,7 +438,6 @@ function parseLinkData (link,data) { } } }); -// } }); $('#mhresponse :input:not(:text)').change(function() { //TODO - don't submit when a text field changes @@ -911,7 +885,6 @@ var updateItem = function(item,link,time) { //URLHash.time = json_store.meta.time; if (updateSocket !== undefined && updateSocket.readyState != 4){ // Only allow one update thread to run at once - //console.log("updateItem: Aborting update. updateSocket="+updateSocket.readyState); updateSocket.abort(); } if (time === undefined) { @@ -1003,8 +976,26 @@ var updateStaticPage = function(link,time) { //don't run this if stategrp0 exists if (states_loaded == 0) { $(".btn-state-cmd").click( function () { - var entity = $(this).attr("entity"); - create_state_modal(entity); + var entity = $(this).attr("entity"); + if (json_store.ia7_config.objects !== undefined && json_store.ia7_config.objects[entity] !== undefined) { + if (json_store.ia7_config.objects[entity].direct_control !== undefined && json_store.ia7_config.objects[entity].direct_control == "yes") { + var new_state = ""; + var possible_states = 0; + for (var i = 0; i < json_store.objects[entity].states.length; i++){ + if (filterSubstate(json_store.objects[entity].states[i]) == 1) continue; + possible_states++; + if (json_store.objects[entity].states[i] !== json_store.objects[entity].state) new_state = json_store.objects[entity].states[i]; + + } + if ((possible_states > 2) || (new_state == "")) alert("Check configuration of "+entity+". "+possible_states+" states detected for direct control object. State is "+new_state); + url= '/SET;none?select_item='+entity+'&select_state='+new_state; + $.get( url); + } else { + create_state_modal(entity); + } + } else { + create_state_modal(entity); + } }); } } @@ -1028,7 +1019,6 @@ function authDetails() { alert("Warning, Collection ID 700: Authorize, is not defined in your collections.json!"); } else { if (json_store.collections[700].user !== undefined) { -// console.log ("user found "+json_store.collections[700].user+"."); if (json_store.collections[700].user == "0") { json_store.collections[700].name = "Log in"; json_store.collections[700].icon = "fa-lock"; @@ -1058,17 +1048,10 @@ var loadCollection = function(collection_keys) { if (entity_sort.length <= 0){ entity_arr.push("Childless Collection"); } -// if (json_store.collections[700] == undefined) { -// alert("Warning, Collection ID 700: Authorize, is not defined in your collections.json!"); -// } else { -// authDetails(); -// } for (var i = 0; i < entity_sort.length; i++){ var collection = entity_sort[i]; -// console.log ("col="+collection); if (!(collection in json_store.collections)) continue; -// console.log ("starting"); var link = json_store.collections[collection].link; var icon = json_store.collections[collection].icon; @@ -1199,7 +1182,6 @@ function fixIA7Nav() { var url = $(location).attr('href'); var collid = url.split("_collection_key="); - console.log("fixing nav..."+url+" "+collid[1]); $('a').each(function() { if ($(this).attr('href').match("^/ia7/")) { this.href += '&_collection_key='+collid[1]+','; @@ -1327,7 +1309,6 @@ var mobile_device = function() { function audio_play(audioElement,srcUrl) { - //console.log ("in audio_play:"+srcUrl); audioElement.pause(); audioElement.src=''; //force playback to stop and quit buffering. Not sure if this is strictly necessary. $("#sound_element2").attr("src", srcUrl); //needed for mobile @@ -1341,7 +1322,6 @@ function playWhenReady() {//wait for media element to be ready, then play audioElement=document.getElementById('sound_element'); var audioReady=audioElement.readyState; - //console.log("playWhenReady = "+audioReady); if(audioReady>2) { audioElement.play(); } else if(audioElement.error) { @@ -1689,9 +1669,6 @@ var object_history = function(items,start,days,time) { }); $('.update_history').click(function() { - console.log ("start="+$('.hist_start').val()+" end="+$('.hist_end').val()); -// var new_start = new Date($('.hist_start').val()).getTime(); -// var new_end = new Date($('.hist_end').val()).getTime(); var new_start = new Date($('.hist_start').val().split('-')).getTime(); var new_end = new Date($('.hist_end').val().split('-')).getTime(); var end_days = (new_start - new_end) / (24 * 60 * 60 * 1000) @@ -1731,13 +1708,9 @@ var object_history = function(items,start,days,time) { // take away the border so that it looks better and span the graph from start to end. json.data.options.grid.borderWidth = 0; -// json.data.options.xaxis.min = new Date($('.hist_end').val()).getTime(); -// json.data.options.xaxis.max = new Date($('.hist_start').val()).getTime() + (24 * 60 * 60 * 1000); json.data.options.xaxis.min = new Date($('.hist_end').val().split('-')).getTime(); json.data.options.xaxis.max = new Date($('.hist_start').val().split('-')).getTime() + (24 * 60 * 60 * 1000); -//console.log("data="+JSON.stringify(data)); -//console.log("xmin="+json.data.options.xaxis.min+" xmax="+json.data.options.xaxis.max); $.plot($("#hist-graph"), data, json.data.options); $('.legend').hide(); } @@ -1885,7 +1858,6 @@ var fp_resize_floorplan_image = function(){ $("#fp_graphic").attr("width", "1px"); fp_display_width = $("#graphic").width(); -// console.log("FP: resize "+ floor_width + " => " + fp_display_width); $('#fp_graphic').attr("width",fp_display_width+"px"); fp_display_height = $("#fp_graphic").height(); }; @@ -1930,8 +1902,6 @@ var fp_reposition_entities = function(){ var fp_scale = width/nwidth; var fp_scale_percent = Math.round( fp_scale * 100); -// console.log("width="+width+" nwidth="+nwidth+" scale="+fp_scale_percent); - // update the location of all the objects... $(".floorplan_item").each(function(index) { var classstr = $(this).attr("class"); @@ -1943,19 +1913,9 @@ var fp_reposition_entities = function(){ } var fp_location = coords.split(/x/); var fp_offset = fp_get_offset_from_location(fp_location); -// console.log("coords="+coords); - - // this seems to make the repositioning slow - // ~ 300+ms on my nexus7 firefox-beta vs <100ms with this code commented out - // var baseimg_width = $("#fp_graphic").width(); - // if (baseimg_width < 500) { - // $(this).attr('src',$(this).attr('src').replace('48.png','32.png')); - // } else { - // $(this).attr('src',$(this).attr('src').replace('32.png','48.png')); - // } + var element_id = $(this).attr('id'); var adjust = fp_icon_image_size*fp_scale/2; -// console.log("adjust="+adjust+" fp_offset.top="+fp_offset.top+" fp_offset.left="+fp_offset.left); var fp_off_center = { "top": fp_offset.top - adjust, "left": fp_offset.left - adjust @@ -2270,16 +2230,13 @@ var floorplan = function(group,time) { } } html += ""; - //console.log("html="+html) } return html; } }); } else { E.click( function () { - //var fp_entity = $(this).attr("id").split(/entity_/)[1]; // var fp_entity = $(this).attr("id").match(/entity_(.*)_\d+$/)[1]; //strip out entity_ and ending _X ... item names can have underscores in them. - //alert("entity="+fp_entity); create_state_modal(fp_entity); }); } @@ -2323,10 +2280,8 @@ var floorplan = function(group,time) { url: "/LONG_POLL?json('GET','fp_icon_sets','px=48')", dataType: "json", error: function(xhr, textStatus, errorThrown){ -// console.log('FP: request iconsets failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"'); }, success: function( json, statusText, jqXHR ) { -// console.log('FP: request iconsets: "' + statusText + '" "'+JSON.stringify(jqXHR, undefined,2)+'"'); var requestTime = time; if (jqXHR.status === 200) { var iconlist = '\n"; -# $form .= "\n"; + + # $form .= "\n"; return $form; } sub html_form_select_set_var { my ( $var, $default, @values ) = @_; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); + my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); my $id = ""; - #$id = "id='mhresponse'" if ($mode eq 'ia7'); + + #$id = "id='mhresponse'" if ($mode eq 'ia7'); my $html = ""; for (var i in config_data){ if ( typeof config_data[i] === 'object') { - console.log("i "+i+":"); html += ""; for (var j in config_data[i]) { if ( typeof config_data[i][j] === 'object') { @@ -416,7 +414,6 @@ function parseLinkData (link,data) { url: form.attr('action'), data: form_data, success: function(data){ - console.log(data) data = data.replace(/]*>/img, ''); //Remove stylesheets data = data.replace(/]*>((\r|\n|.)*?)<\/title[^>]*>/img, ''); //Remove title data = data.replace(/]*>/img, ''); //Remove meta refresh @@ -1477,7 +1474,6 @@ var graph_rrd = function(start,group,time) { last_timestamp = new Date(json.data.last_update); } //Update the footer database updated time - console.log ("Last Updated:"+last_timestamp); $('#Last_updated').remove(); $('#footer_stuff').prepend("
    RRD Database Last Updated:"+last_timestamp+"
    "); @@ -1672,8 +1668,9 @@ var object_history = function(items,start,days,time) { }); $('.update_history').click(function() { - var new_start = new Date($('.hist_start').val().split('-')).getTime(); - var new_end = new Date($('.hist_end').val().split('-')).getTime(); + //var new_start = new Date($('.hist_start').val().split('-')).getTime(); + var new_start = new Date($('.hist_start').val().replace(/-/g, "/")).getTime(); + var new_end = new Date($('.hist_end').val().replace(/-/g, "/")).getTime(); var end_days = (new_start - new_end) / (24 * 60 * 60 * 1000) new_start = new_start / 1000; object_history(items,new_start,end_days); @@ -1930,7 +1927,7 @@ var fp_reposition_entities = function(){ $(this).width(fp_scale_percent + "%"); }); var t1 = performance.now(); - console.log("FP: reposition and scale: " +Math.round(t1 - t0) + "ms "); + //console.log("FP: reposition and scale: " +Math.round(t1 - t0) + "ms "); }; var fp_show_all_icons = function() { @@ -2376,9 +2373,9 @@ var floorplan = function(group,time) { if (time === 0){ // hack to fix initial positions of the items var wait = 50; - console.log("FP: calling fp in " +wait+ "ms"); + //console.log("FP: calling fp in " +wait+ "ms"); setTimeout(function(){ - console.log("FP: calling fp after " +wait+ "ms"); + //console.log("FP: calling fp after " +wait+ "ms"); fp_reposition_entities(); }, wait); } @@ -2467,7 +2464,6 @@ var create_state_modal = function(entity) { } for (var i = 0; i < modal_states.length; i++){ -console.log("Creating state buttons. "); if (filterSubstate(modal_states[i]) == 1) { advanced_html += ""; continue @@ -2518,12 +2514,9 @@ console.log("Creating state buttons. "); var modify_jqcon_dow = function(cronstr,offset) { var cron = cronstr.split(/\s+/); - console.log("dow="+cron[cron.length-1]); cron[cron.length-1] = cron[cron.length-1].replace(/\d/gi, function adjust(x) { - console.log("x="+x+" offset="+offset); return parseInt(x) + parseInt(offset); });; - console.log("dow="+cron[cron.length-1]); return cron.join(" "); } @@ -2602,12 +2595,10 @@ console.log("Creating state buttons. "); $('#control').find('.modal-body').append("

    Schedule Control

    "); var sched_states = json_store.objects[entity].schedule[0][3]; - console.log("schedule.length="+json_store.objects[entity].schedule.length); for (var i = 1; i < json_store.objects[entity].schedule.length; i++){ var sched_index = json_store.objects[entity].schedule[i][0]; var sched_cron = modify_jqcon_dow(json_store.objects[entity].schedule[i][1],1); var sched_label = json_store.objects[entity].schedule[i][2]; - console.log("si="+sched_index+",sc="+sched_cron+",sl="+sched_label+",ss="+sched_states); add_schedule(sched_index,sched_cron,sched_label,sched_states); } @@ -2618,7 +2609,6 @@ console.log("Creating state buttons. "); if (isNaN(newid)) newid=1; var newlabel = newid; if (sched_states[0] !== null) newlabel=sched_states[0]; - console.log("add new schedule, index should be "+newid+" states are"+sched_states); add_schedule(newid,'0 0 * * 1-7',newlabel,sched_states); $('.sched_submit').removeClass('disabled'); $('.sched_submit').removeClass('btn-default'); @@ -2629,14 +2619,10 @@ console.log("Creating state buttons. "); if ($(this).hasClass("disabled")) return; var string = ""; $('.mhsched').each(function(index,value) { - console.log("string="+string); -// string += $( this ).attr("id") + ',"' + $( this ).text() + '",' + $( this ).attr("label") + ','; string += $( this ).attr("id") + ',"' + modify_jqcon_dow($(this).text(),"-1") + '",' + $( this ).attr("label") + ','; - console.log("string="+string); }); string = string.replace(/,\s*$/, ""); //remove the last comma var url="/SUB?ia7_update_schedule"+encodeURI("("+$(this).parents('.control-dialog').attr("entity")+","+string+")"); - console.log("url="+url); $.get(url); $('.sched_submit').addClass('disabled'); $('.sched_submit').removeClass('btn-success'); @@ -2686,7 +2672,6 @@ console.log("Creating state buttons. "); }); $('.logger_data').on('click',function() { $('#control').modal('hide'); - console.log('url='+$(location).attr('href')); }); } @@ -2723,7 +2708,6 @@ var authorize_modal = function(user) { }); $('.btn-login-logoff').click( function () { $.get ("/UNSET_PASSWORD"); - console.log("in logoff"); location.reload(); $('#loginModal').modal('hide'); }); @@ -2735,7 +2719,6 @@ var authorize_modal = function(user) { url: "/SET_PASSWORD_FORM", data: $(this).serialize(), success: function(data){ - console.log(data) var status=data.match(/\(.*)\<\/b\>/gm); //console.log("match="+status[2]); //3rd match is password status if (status[2] == "Password was incorrect") { @@ -2993,7 +2976,6 @@ $(document).ready(function() { $('#mhresponse').click( function (e) { e.preventDefault(); $form = $(this); - console.log("MHResponse Custom submit function "+ form.attr('action')); //$.ajax({ // type: "POST", // url: "/SET_PASSWORD_FORM", From 2ecc0580e4abd7538bd30ec8ffa527f2d228bc2a Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 2 Mar 2017 18:04:02 -0700 Subject: [PATCH 135/209] Remove Socket/Multicast.pm for Wayne --- lib/site/IO/Socket/Multicast.pm | 426 -------------------------------- 1 file changed, 426 deletions(-) delete mode 100644 lib/site/IO/Socket/Multicast.pm diff --git a/lib/site/IO/Socket/Multicast.pm b/lib/site/IO/Socket/Multicast.pm deleted file mode 100644 index d69edb17a..000000000 --- a/lib/site/IO/Socket/Multicast.pm +++ /dev/null @@ -1,426 +0,0 @@ -package IO::Socket::Multicast; - -use 5.005; -use strict; -use Carp 'croak'; -use Exporter (); -use DynaLoader (); -use IO::Socket; -BEGIN { - eval "use IO::Interface 0.94 'IFF_MULTICAST';"; -} -use vars qw(@ISA @EXPORT_OK @EXPORT %EXPORT_TAGS $VERSION); -BEGIN { - my @functions = qw( - mcast_add - mcast_drop - mcast_if - mcast_loopback - mcast_ttl - mcast_dest - mcast_send - ); - $VERSION = '1.12'; - @ISA = qw( - Exporter - DynaLoader - IO::Socket::INET - ); - @EXPORT = ( ); - %EXPORT_TAGS = ( - 'all' => \@functions, - 'functions' => \@functions, - ); - @EXPORT_OK = @{ $EXPORT_TAGS{'all'} }; -} - -my $IP = '\d+\.\d+\.\d+\.\d+'; - -sub import { - Socket->export_to_level(1,@_); - IO::Socket::Multicast->export_to_level(1,@_); -} - -sub new { - my $class = shift; - unshift @_,(Proto => 'udp') unless @_; - $class->SUPER::new(@_); -} - -sub configure { - my($self,$arg) = @_; - $arg->{Proto} ||= 'udp'; - $self->SUPER::configure($arg); -} - -sub mcast_add { - my $sock = shift; - my $group = shift || croak 'usage: $sock->mcast_add($mcast_addr [,$interface])'; - $group = inet_ntoa($group) unless $group =~ /^$IP$/o; - my $interface = get_if_addr($sock,shift); - return $sock->_mcast_add($group,$interface); -} - -sub mcast_drop { - my $sock = shift; - my $group = shift || croak 'usage: $sock->mcast_add($mcast_addr [,$interface])'; - $group = inet_ntoa($group) unless $group =~ /^$IP$/o; - my $interface = get_if_addr($sock,shift); - return $sock->_mcast_drop($group,$interface); -} - -sub mcast_if { - my $sock = shift; - - my $previous = $sock->_mcast_if; - $previous = $sock->addr_to_interface($previous) - if $sock->can('addr_to_interface'); - return $previous unless @_; - - my $interface = get_if_addr($sock,shift); - return $sock->_mcast_if($interface) ? $previous : undef; -} - -sub get_if_addr { - my $sock = shift; - return '0.0.0.0' unless defined (my $interface = shift); - return $interface if $interface =~ /^$IP$/; - return $interface if length $interface == 16; - croak "IO::Interface module not available; use IP addr for interface" - unless $sock->can('if_addr'); - croak "unknown or unconfigured interace $interface" - unless my $addr = $sock->if_addr($interface); - croak "interface is not multicast capable" - unless $interface eq 'any' or ($sock->if_flags($interface) & IFF_MULTICAST()); - return $addr; -} - -sub mcast_dest { - my $sock = shift; - my $prev = ${*$sock}{'io_socket_mcast_dest'}; - if (my $dest = shift) { - $dest = sockaddr_in($2,inet_aton($1)) if $dest =~ /^($IP):(\d+)$/; - croak "invalid destination address" unless length($dest) == 16; - ${*$sock}{'io_socket_mcast_dest'} = $dest; - } - return $prev; -} - -sub mcast_send { - my $sock = shift; - my $data = shift || croak 'usage: $sock->mcast_send($data [,$address])'; - $sock->mcast_dest(shift) if @_; - my $dest = $sock->mcast_dest || croak "no destination specified with mcast_send() or mcast_dest()"; - return send($sock,$data,0,$dest); -} - -bootstrap IO::Socket::Multicast $VERSION; - -1; - -__END__ - -=pod - -=head1 NAME - -IO::Socket::Multicast - Send and receive multicast messages - -=head1 SYNOPSIS - - use IO::Socket::Multicast; - - # create a new UDP socket ready to read datagrams on port 1100 - my $s = IO::Socket::Multicast->new(LocalPort=>1100); - - # Add a multicast group - $s->mcast_add('225.0.1.1'); - - # Add a multicast group to eth0 device - $s->mcast_add('225.0.0.2','eth0'); - - # now receive some multicast data - $s->recv($data,1024); - - # Drop a multicast group - $s->mcast_drop('225.0.0.1'); - - # Set outgoing interface to eth0 - $s->mcast_if('eth0'); - - # Set time to live on outgoing multicast packets - $s->mcast_ttl(10); - - # Turn off loopbacking - $s->mcast_loopback(0); - - # Multicast a message to group 225.0.0.1 - $s->mcast_send('hello world!','225.0.0.1:1200'); - $s->mcast_set('225.0.0.2:1200'); - $s->mcast_send('hello again!'); - -=head1 DESCRIPTION - -The IO::Socket::Multicast module subclasses IO::Socket::INET to enable -you to manipulate multicast groups. With this module (and an -operating system that supports multicasting), you will be able to -receive incoming multicast transmissions and generate your own -outgoing multicast packets. - -This module requires IO::Interface version 0.94 or higher. - -=head2 INTRODUCTION - -Multicasting is designed for streaming multimedia applications and for -conferencing systems in which one transmitting machines needs to -distribute data to a large number of clients. - -IP addresses in the range 224.0.0.0 and 239.255.255.255 are reserved -for multicasting. These addresses do not correspond to individual -machines, but to multicast groups. Messages sent to these addresses -will be delivered to a potentially large number of machines that have -registered their interest in receiving transmissions on these groups. -They work like TV channels. A program tunes in to a multicast group -to receive transmissions to it, and tunes out when it no longer -wishes to receive the transmissions. - -To receive transmissions B a multicast group, you will use -IO::Socket::Multicast->new() to create a UDP socket and bind it to a local -network port. You will then subscribe one or more multicast groups -using the mcast_add() method. Subsequent calls to the standard recv() -method will now receive messages incoming messages transmitted to the -subscribed groups using the selected port number. - -To send transmissions B a multicast group, you can use the -standard send() method to send messages to the multicast group and -port of your choice. The mcast_set() and mcast_send() methods are -provided as convenience functions. Mcast_set() will set a default -multicast destination for messages which you then send with -mcast_send(). - -To set the number of hops (routers) that outgoing multicast messages -will cross, call mcast_ttl(). To activate or deactivate the looping -back of multicast messages (in which a copy of the transmitted -messages is received by the local machine), call mcast_loopback(). - -=head2 CONSTRUCTORS - -=over 4 - -=item $socket = IO::Socket::Multicast->new([LocalPort=>$port,...]) - -The new() method is the constructor for the IO::Socket::Multicast -class. It takes the same arguments as IO::Socket::INET, except that -the B argument, rather than defaulting to "tcp", will default -to "udp", which is more appropriate for multicasting. - -To create a UDP socket suitable for sending outgoing multicast -messages, call new() without arguments (or with -C'udp'>). To create a UDP socket that can also receive -incoming multicast transmissions on a specific port, call new() with -the B argument. - -If you plan to run the client and server on the same machine, you may -wish to set the IO::Socket B argument to a true value. -This allows multiple multicast sockets to bind to the same address. - -=back - -=head2 METHODS - -=over 4 - -=item $success = $socket->mcast_add($multicast_address [,$interface]) - -The mcast_add() method will add the provided multicast address to the -list of subscribed multicast groups. The address may be provided -either as a dotted-quad decimal, or as a packed IP address (such as -produced by the inet_aton() function). On success, the method will -return a true value. - -The optional $interface argument can be used to specify on which -network interface to listen for incoming multicast messages. If the -IO::Interface module is installed, you may use the device name for the -interface (e.g. "tu0"). Otherwise, you must use the IP address of the -desired network interface. Either dotted quad form or packed IP -address is acceptable. If no interface is specified, then the -multicast group is joined on INADDR_ANY, meaning that multicast -transmissions received on B of the host's network interfaces will -be forwarded to the socket. - -Note that mcast_add() operates on the underlying interface(s) and not -on the socket. If you have multiple sockets listening on a port, and -you mcast_add() a group to one of those sockets, subsequently B -the sockets will receive mcast messages on this group. To filter -messages that can be received by a socket so that only those sent to a -particular multicast address are received, pass the B -option to the socket at the time you create it: - - my $socket = IO::Socket::Multicast->new(LocalPort=>2000, - LocalAddr=>226.1.1.2', - ReuseAddr=>1); - $socket->mcast_add('226.1.1.2'); - -By combining this technique with IO::Select, you can write -applications that listen to multiple multicast groups and distinguish -which group a message was addressed to by identifying which socket it -was received on. - -=item $success = $socket->mcast_drop($multicast_address) - -This reverses the action of mcast_add(), removing the indicated -multicast address from the list of subscribed groups. - -=item $loopback = $socket->mcast_loopback - -=item $previous = $socket->mcast_loopback($new) - -The mcast_loopback() method controls whether the socket will receive -its own multicast transmissions (default yes). Called without -arguments, the method returns the current state of the loopback -flag. Called with a boolean argument, the method will set the loopback -flag, and return its previous value. - -=item $ttl = $socket->mcast_ttl - -=item $previous = $socket->mcast_ttl($new) - -The mcast_ttl() method examines or sets the time to live (TTL) for -outgoing multicast messages. The TTL controls the numbers of routers -the packet can cross before being expired. The default TTL is 1, -meaning that the message is confined to the local area network. -Values between 0 and 255 are valid. - -Called without arguments, this method returns the socket's current -TTL. Called with a value, this method sets the TTL and returns its -previous value. - -=item $interface = $socket->mcast_if - -=item $previous = $socket->mcast_if($new) - -By default, the OS will pick the network interface to use for outgoing -multicasts automatically. You can control this process by using the -mcast_if() method to set the outgoing network interface explicitly. -Called without arguments, returns the current interface. Called with -the name of an interface, sets the outgoing interface and returns its -previous value. - -You can use the device name for the interface (e.g. "tu0") if the -IO::Interface module is present. Otherwise, you must use the -interface's dotted IP address. - -B: To set the interface used for B multicasts, use the -mcast_add() method. - -=item $dest = $socket->mcast_dest - -=item $previous = $socket->mcast_dest($new) - -The mcast_dest() method is a convenience function that allows you to -set the default destination group for outgoing multicasts. Called -without arguments, returns the current destination as a packed binary -sockaddr_in data structure. Called with a new destination address, -the method sets the default destination and returns the previous one, -if any. - -Destination addresses may be provided as packed sockaddr_in -structures, or in the form "XX.XX.XX.XX:YY" where the first part is -the IP address, and the second the port number. - -=item $bytes = $socket->mcast_send($data [,$dest]) - -Mcast_send() is a convenience function that simplifies the sending of -multicast messages. C<$data> is the message contents, and C<$dest> is -an optional destination group. You can use either the dotted IP form -of the destination address and its port number, or a packed -sockaddr_in structure. If the destination is not supplied, it will -default to the most recent value set in mcast_dest() or a previous -call to mcast_send(). - -The method returns the number of bytes successfully queued for -delivery. - -As a side-effect, the method will call mcast_dest() to remember the -destination address. - -Example: - - $socket->mcast_send('Hi there group members!','225.0.1.1:1900') || die; - $socket->mcast_send("How's the weather?") || die; - -Note that you may still call IO::Socket::Multicast->new() with a -B, and IO::Socket::INET will perform a connect(), creating a -default destination for calls to send(). - -=back - -=head1 EXAMPLE - -The following is an example of a multicast server. Every 10 seconds -it transmits the current time and the list of logged-in users to the -local network using multicast group 226.1.1.2, port 2000 (these are -chosen arbitrarily). - - #!/usr/bin/perl - # server - use strict; - use IO::Socket::Multicast; - - use constant DESTINATION => '226.1.1.2:2000'; - my $sock = IO::Socket::Multicast->new(Proto=>'udp',PeerAddr=>DESTINATION); - - while (1) { - my $message = localtime; - $message .= "\n" . `who`; - $sock->send($message) || die "Couldn't send: $!"; - } continue { - sleep 10; - } - -This is the corresponding client. It listens for transmissions on -group 226.1.1.2, port 2000, and echoes the messages to standard -output. - - #!/usr/bin/perl - # client - - use strict; - use IO::Socket::Multicast; - - use constant GROUP => '226.1.1.2'; - use constant PORT => '2000'; - - my $sock = IO::Socket::Multicast->new(Proto=>'udp',LocalPort=>PORT); - $sock->mcast_add(GROUP) || die "Couldn't set group: $!\n"; - - while (1) { - my $data; - next unless $sock->recv($data,1024); - print $data; - } - -=head2 EXPORT - -None by default. However, if you wish to call mcast_add(), -mcast_drop(), mcast_if(), mcast_loopback(), mcast_ttl, mcast_dest() -and mcast_send() as functions you may import them explicitly on the -B line or by importing the tag ":functions". - -=head2 BUGS - -The mcast_if(), mcast_ttl() and mcast_loopback() methods will cause a -crash on versions of Linux earlier than 2.2.0 because of a kernel bug -in the implementation of the multicast socket options. - -=head1 AUTHOR - -Lincoln Stein, lstein@cshl.org. - -This module is distributed under the same terms as Perl itself. - -=head1 SEE ALSO - -perl(1), IO::Socket(3), IO::Socket::INET(3). - -=cut From 44078b643051e7b3f72a8aff7861cd64ecc58d6b Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 5 Mar 2017 21:30:30 +0100 Subject: [PATCH 136/209] Removes IO::Socket::Multicast again --- lib/site/IO/Socket/Multicast.pm | 426 -------------------------------- 1 file changed, 426 deletions(-) delete mode 100644 lib/site/IO/Socket/Multicast.pm diff --git a/lib/site/IO/Socket/Multicast.pm b/lib/site/IO/Socket/Multicast.pm deleted file mode 100644 index d69edb17a..000000000 --- a/lib/site/IO/Socket/Multicast.pm +++ /dev/null @@ -1,426 +0,0 @@ -package IO::Socket::Multicast; - -use 5.005; -use strict; -use Carp 'croak'; -use Exporter (); -use DynaLoader (); -use IO::Socket; -BEGIN { - eval "use IO::Interface 0.94 'IFF_MULTICAST';"; -} -use vars qw(@ISA @EXPORT_OK @EXPORT %EXPORT_TAGS $VERSION); -BEGIN { - my @functions = qw( - mcast_add - mcast_drop - mcast_if - mcast_loopback - mcast_ttl - mcast_dest - mcast_send - ); - $VERSION = '1.12'; - @ISA = qw( - Exporter - DynaLoader - IO::Socket::INET - ); - @EXPORT = ( ); - %EXPORT_TAGS = ( - 'all' => \@functions, - 'functions' => \@functions, - ); - @EXPORT_OK = @{ $EXPORT_TAGS{'all'} }; -} - -my $IP = '\d+\.\d+\.\d+\.\d+'; - -sub import { - Socket->export_to_level(1,@_); - IO::Socket::Multicast->export_to_level(1,@_); -} - -sub new { - my $class = shift; - unshift @_,(Proto => 'udp') unless @_; - $class->SUPER::new(@_); -} - -sub configure { - my($self,$arg) = @_; - $arg->{Proto} ||= 'udp'; - $self->SUPER::configure($arg); -} - -sub mcast_add { - my $sock = shift; - my $group = shift || croak 'usage: $sock->mcast_add($mcast_addr [,$interface])'; - $group = inet_ntoa($group) unless $group =~ /^$IP$/o; - my $interface = get_if_addr($sock,shift); - return $sock->_mcast_add($group,$interface); -} - -sub mcast_drop { - my $sock = shift; - my $group = shift || croak 'usage: $sock->mcast_add($mcast_addr [,$interface])'; - $group = inet_ntoa($group) unless $group =~ /^$IP$/o; - my $interface = get_if_addr($sock,shift); - return $sock->_mcast_drop($group,$interface); -} - -sub mcast_if { - my $sock = shift; - - my $previous = $sock->_mcast_if; - $previous = $sock->addr_to_interface($previous) - if $sock->can('addr_to_interface'); - return $previous unless @_; - - my $interface = get_if_addr($sock,shift); - return $sock->_mcast_if($interface) ? $previous : undef; -} - -sub get_if_addr { - my $sock = shift; - return '0.0.0.0' unless defined (my $interface = shift); - return $interface if $interface =~ /^$IP$/; - return $interface if length $interface == 16; - croak "IO::Interface module not available; use IP addr for interface" - unless $sock->can('if_addr'); - croak "unknown or unconfigured interace $interface" - unless my $addr = $sock->if_addr($interface); - croak "interface is not multicast capable" - unless $interface eq 'any' or ($sock->if_flags($interface) & IFF_MULTICAST()); - return $addr; -} - -sub mcast_dest { - my $sock = shift; - my $prev = ${*$sock}{'io_socket_mcast_dest'}; - if (my $dest = shift) { - $dest = sockaddr_in($2,inet_aton($1)) if $dest =~ /^($IP):(\d+)$/; - croak "invalid destination address" unless length($dest) == 16; - ${*$sock}{'io_socket_mcast_dest'} = $dest; - } - return $prev; -} - -sub mcast_send { - my $sock = shift; - my $data = shift || croak 'usage: $sock->mcast_send($data [,$address])'; - $sock->mcast_dest(shift) if @_; - my $dest = $sock->mcast_dest || croak "no destination specified with mcast_send() or mcast_dest()"; - return send($sock,$data,0,$dest); -} - -bootstrap IO::Socket::Multicast $VERSION; - -1; - -__END__ - -=pod - -=head1 NAME - -IO::Socket::Multicast - Send and receive multicast messages - -=head1 SYNOPSIS - - use IO::Socket::Multicast; - - # create a new UDP socket ready to read datagrams on port 1100 - my $s = IO::Socket::Multicast->new(LocalPort=>1100); - - # Add a multicast group - $s->mcast_add('225.0.1.1'); - - # Add a multicast group to eth0 device - $s->mcast_add('225.0.0.2','eth0'); - - # now receive some multicast data - $s->recv($data,1024); - - # Drop a multicast group - $s->mcast_drop('225.0.0.1'); - - # Set outgoing interface to eth0 - $s->mcast_if('eth0'); - - # Set time to live on outgoing multicast packets - $s->mcast_ttl(10); - - # Turn off loopbacking - $s->mcast_loopback(0); - - # Multicast a message to group 225.0.0.1 - $s->mcast_send('hello world!','225.0.0.1:1200'); - $s->mcast_set('225.0.0.2:1200'); - $s->mcast_send('hello again!'); - -=head1 DESCRIPTION - -The IO::Socket::Multicast module subclasses IO::Socket::INET to enable -you to manipulate multicast groups. With this module (and an -operating system that supports multicasting), you will be able to -receive incoming multicast transmissions and generate your own -outgoing multicast packets. - -This module requires IO::Interface version 0.94 or higher. - -=head2 INTRODUCTION - -Multicasting is designed for streaming multimedia applications and for -conferencing systems in which one transmitting machines needs to -distribute data to a large number of clients. - -IP addresses in the range 224.0.0.0 and 239.255.255.255 are reserved -for multicasting. These addresses do not correspond to individual -machines, but to multicast groups. Messages sent to these addresses -will be delivered to a potentially large number of machines that have -registered their interest in receiving transmissions on these groups. -They work like TV channels. A program tunes in to a multicast group -to receive transmissions to it, and tunes out when it no longer -wishes to receive the transmissions. - -To receive transmissions B a multicast group, you will use -IO::Socket::Multicast->new() to create a UDP socket and bind it to a local -network port. You will then subscribe one or more multicast groups -using the mcast_add() method. Subsequent calls to the standard recv() -method will now receive messages incoming messages transmitted to the -subscribed groups using the selected port number. - -To send transmissions B a multicast group, you can use the -standard send() method to send messages to the multicast group and -port of your choice. The mcast_set() and mcast_send() methods are -provided as convenience functions. Mcast_set() will set a default -multicast destination for messages which you then send with -mcast_send(). - -To set the number of hops (routers) that outgoing multicast messages -will cross, call mcast_ttl(). To activate or deactivate the looping -back of multicast messages (in which a copy of the transmitted -messages is received by the local machine), call mcast_loopback(). - -=head2 CONSTRUCTORS - -=over 4 - -=item $socket = IO::Socket::Multicast->new([LocalPort=>$port,...]) - -The new() method is the constructor for the IO::Socket::Multicast -class. It takes the same arguments as IO::Socket::INET, except that -the B argument, rather than defaulting to "tcp", will default -to "udp", which is more appropriate for multicasting. - -To create a UDP socket suitable for sending outgoing multicast -messages, call new() without arguments (or with -C'udp'>). To create a UDP socket that can also receive -incoming multicast transmissions on a specific port, call new() with -the B argument. - -If you plan to run the client and server on the same machine, you may -wish to set the IO::Socket B argument to a true value. -This allows multiple multicast sockets to bind to the same address. - -=back - -=head2 METHODS - -=over 4 - -=item $success = $socket->mcast_add($multicast_address [,$interface]) - -The mcast_add() method will add the provided multicast address to the -list of subscribed multicast groups. The address may be provided -either as a dotted-quad decimal, or as a packed IP address (such as -produced by the inet_aton() function). On success, the method will -return a true value. - -The optional $interface argument can be used to specify on which -network interface to listen for incoming multicast messages. If the -IO::Interface module is installed, you may use the device name for the -interface (e.g. "tu0"). Otherwise, you must use the IP address of the -desired network interface. Either dotted quad form or packed IP -address is acceptable. If no interface is specified, then the -multicast group is joined on INADDR_ANY, meaning that multicast -transmissions received on B of the host's network interfaces will -be forwarded to the socket. - -Note that mcast_add() operates on the underlying interface(s) and not -on the socket. If you have multiple sockets listening on a port, and -you mcast_add() a group to one of those sockets, subsequently B -the sockets will receive mcast messages on this group. To filter -messages that can be received by a socket so that only those sent to a -particular multicast address are received, pass the B -option to the socket at the time you create it: - - my $socket = IO::Socket::Multicast->new(LocalPort=>2000, - LocalAddr=>226.1.1.2', - ReuseAddr=>1); - $socket->mcast_add('226.1.1.2'); - -By combining this technique with IO::Select, you can write -applications that listen to multiple multicast groups and distinguish -which group a message was addressed to by identifying which socket it -was received on. - -=item $success = $socket->mcast_drop($multicast_address) - -This reverses the action of mcast_add(), removing the indicated -multicast address from the list of subscribed groups. - -=item $loopback = $socket->mcast_loopback - -=item $previous = $socket->mcast_loopback($new) - -The mcast_loopback() method controls whether the socket will receive -its own multicast transmissions (default yes). Called without -arguments, the method returns the current state of the loopback -flag. Called with a boolean argument, the method will set the loopback -flag, and return its previous value. - -=item $ttl = $socket->mcast_ttl - -=item $previous = $socket->mcast_ttl($new) - -The mcast_ttl() method examines or sets the time to live (TTL) for -outgoing multicast messages. The TTL controls the numbers of routers -the packet can cross before being expired. The default TTL is 1, -meaning that the message is confined to the local area network. -Values between 0 and 255 are valid. - -Called without arguments, this method returns the socket's current -TTL. Called with a value, this method sets the TTL and returns its -previous value. - -=item $interface = $socket->mcast_if - -=item $previous = $socket->mcast_if($new) - -By default, the OS will pick the network interface to use for outgoing -multicasts automatically. You can control this process by using the -mcast_if() method to set the outgoing network interface explicitly. -Called without arguments, returns the current interface. Called with -the name of an interface, sets the outgoing interface and returns its -previous value. - -You can use the device name for the interface (e.g. "tu0") if the -IO::Interface module is present. Otherwise, you must use the -interface's dotted IP address. - -B: To set the interface used for B multicasts, use the -mcast_add() method. - -=item $dest = $socket->mcast_dest - -=item $previous = $socket->mcast_dest($new) - -The mcast_dest() method is a convenience function that allows you to -set the default destination group for outgoing multicasts. Called -without arguments, returns the current destination as a packed binary -sockaddr_in data structure. Called with a new destination address, -the method sets the default destination and returns the previous one, -if any. - -Destination addresses may be provided as packed sockaddr_in -structures, or in the form "XX.XX.XX.XX:YY" where the first part is -the IP address, and the second the port number. - -=item $bytes = $socket->mcast_send($data [,$dest]) - -Mcast_send() is a convenience function that simplifies the sending of -multicast messages. C<$data> is the message contents, and C<$dest> is -an optional destination group. You can use either the dotted IP form -of the destination address and its port number, or a packed -sockaddr_in structure. If the destination is not supplied, it will -default to the most recent value set in mcast_dest() or a previous -call to mcast_send(). - -The method returns the number of bytes successfully queued for -delivery. - -As a side-effect, the method will call mcast_dest() to remember the -destination address. - -Example: - - $socket->mcast_send('Hi there group members!','225.0.1.1:1900') || die; - $socket->mcast_send("How's the weather?") || die; - -Note that you may still call IO::Socket::Multicast->new() with a -B, and IO::Socket::INET will perform a connect(), creating a -default destination for calls to send(). - -=back - -=head1 EXAMPLE - -The following is an example of a multicast server. Every 10 seconds -it transmits the current time and the list of logged-in users to the -local network using multicast group 226.1.1.2, port 2000 (these are -chosen arbitrarily). - - #!/usr/bin/perl - # server - use strict; - use IO::Socket::Multicast; - - use constant DESTINATION => '226.1.1.2:2000'; - my $sock = IO::Socket::Multicast->new(Proto=>'udp',PeerAddr=>DESTINATION); - - while (1) { - my $message = localtime; - $message .= "\n" . `who`; - $sock->send($message) || die "Couldn't send: $!"; - } continue { - sleep 10; - } - -This is the corresponding client. It listens for transmissions on -group 226.1.1.2, port 2000, and echoes the messages to standard -output. - - #!/usr/bin/perl - # client - - use strict; - use IO::Socket::Multicast; - - use constant GROUP => '226.1.1.2'; - use constant PORT => '2000'; - - my $sock = IO::Socket::Multicast->new(Proto=>'udp',LocalPort=>PORT); - $sock->mcast_add(GROUP) || die "Couldn't set group: $!\n"; - - while (1) { - my $data; - next unless $sock->recv($data,1024); - print $data; - } - -=head2 EXPORT - -None by default. However, if you wish to call mcast_add(), -mcast_drop(), mcast_if(), mcast_loopback(), mcast_ttl, mcast_dest() -and mcast_send() as functions you may import them explicitly on the -B line or by importing the tag ":functions". - -=head2 BUGS - -The mcast_if(), mcast_ttl() and mcast_loopback() methods will cause a -crash on versions of Linux earlier than 2.2.0 because of a kernel bug -in the implementation of the multicast socket options. - -=head1 AUTHOR - -Lincoln Stein, lstein@cshl.org. - -This module is distributed under the same terms as Perl itself. - -=head1 SEE ALSO - -perl(1), IO::Socket(3), IO::Socket::INET(3). - -=cut From 0986cc87d2c93dbf611e2b6fbe9219e3d8dbc710 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 5 Mar 2017 21:36:43 +0100 Subject: [PATCH 137/209] Removes the deugging prints as requested by @hplato --- lib/Generic_Item.pm | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/Generic_Item.pm b/lib/Generic_Item.pm index 5cda0668c..1dab04fa8 100644 --- a/lib/Generic_Item.pm +++ b/lib/Generic_Item.pm @@ -1386,27 +1386,27 @@ sub get_logger_data { my $data = ""; $epoch = $epoch - ( $days * 60 * 60 * 24 ); for ( my $i = 0; $i <= $days; $i++ ) { - print "db i=$i, days=$days, epoch=$epoch\n"; + #print "db i=$i, days=$days, epoch=$epoch\n"; my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime( $epoch + ( $i * 60 * 60 * 24 ) ); - print "Epoch: $epoch is " - . $mday . "/" - . ( $mon + 1 ) . "/" - . ( $year + 1900 ) . "\n"; - print "Checking " - . $::config_parms{data_dir} - . "/object_logs/" - . $object_name . "/" - . ( $year + 1900 ) . "/" - . ( $mon + 1 ) . "/" - . $mday . "\n"; - print "Reading " - . $::config_parms{data_dir} - . "/object_logs/" - . $object_name . "/" - . ( $year + 1900 ) . "/" - . ( $mon + 1 ) . "/" - . $mday . "\n" + #print "Epoch: $epoch is " + # . $mday . "/" + # . ( $mon + 1 ) . "/" + # . ( $year + 1900 ) . "\n"; + #print "Checking " + # . $::config_parms{data_dir} + # . "/object_logs/" + # . $object_name . "/" + # . ( $year + 1900 ) . "/" + # . ( $mon + 1 ) . "/" + # . $mday . "\n"; + #print "Reading " + # . $::config_parms{data_dir} + # . "/object_logs/" + # . $object_name . "/" + # . ( $year + 1900 ) . "/" + # . ( $mon + 1 ) . "/" + # . $mday . "\n" if (-e $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" From 07fdb887dfb834884b685be772fcc697912df5bc Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 5 Mar 2017 21:42:57 +0100 Subject: [PATCH 138/209] Comments the last debug line completely --- lib/Generic_Item.pm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Generic_Item.pm b/lib/Generic_Item.pm index 1dab04fa8..a8b82362f 100644 --- a/lib/Generic_Item.pm +++ b/lib/Generic_Item.pm @@ -1407,13 +1407,13 @@ sub get_logger_data { # . ( $year + 1900 ) . "/" # . ( $mon + 1 ) . "/" # . $mday . "\n" - if (-e $::config_parms{data_dir} - . "/object_logs/" - . $object_name . "/" - . ( $year + 1900 ) . "/" - . ( $mon + 1 ) . "/" - . $mday - . ".log" ); + # if (-e $::config_parms{data_dir} + # . "/object_logs/" + # . $object_name . "/" + # . ( $year + 1900 ) . "/" + # . ( $mon + 1 ) . "/" + # . $mday + # . ".log" ); $data .= ::file_read( $::config_parms{data_dir} . "/object_logs/" From 67b4e36de6fccfb159c6b04c2f3e4b238ad23b5b Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Tue, 7 Mar 2017 12:14:15 -0700 Subject: [PATCH 139/209] v4.2 pre-release - run perltidy --- bin/alpha_page | 23 +- bin/authors | 6 +- bin/backup_data | 33 +- bin/dailystrips | 255 +- bin/dailystrips-clean | 11 +- bin/display | 5 +- bin/display_callers | 119 +- bin/find_files | 23 +- bin/find_programs | 19 +- bin/get_earthquakes | 68 +- bin/get_email | 89 +- bin/get_email_rule_example.pl | 3 +- bin/get_mp3_data | 110 +- bin/get_tv_grid | 215 +- bin/get_tv_grid_ge | 160 +- bin/get_tv_grid_xmltv | 252 +- bin/get_tv_info | 78 +- bin/get_tv_info_ge | 149 +- bin/get_url | 20 +- bin/get_weather | 15 +- bin/get_weather_ca | 17 +- bin/githook-perltidy | 20 +- bin/h2ph | 133 +- bin/holical | 19 +- bin/ical2vsdb | 139 +- bin/ical_load | 49 +- bin/image_index | 3 +- bin/image_resize | 13 +- bin/mh | 1417 ++++------- bin/mh_users_table.pl | 9 +- bin/mhsend | 14 +- bin/monitor_weblog | 26 +- bin/mp3Rip.pl | 12 +- bin/net_ftp | 13 +- bin/outlook_read | 42 +- bin/perltidyall | 23 +- bin/pocketsphinx | 17 +- bin/read_email | 13 +- bin/report_weblog | 40 +- bin/send_email | 11 +- bin/send_verizon | 6 +- bin/set_clock | 43 +- bin/set_password | 57 +- bin/sun_time | 28 +- bin/terminalmenu-client.pl | 3 +- bin/update_docs | 34 +- bin/vv_prescript.pl | 3 +- bin/vv_tts.pl | 65 +- bin/vv_tts_simple.pl | 3 +- bin/weather_rrd_update_graphs | 775 +++--- bin/xAP-bluetooth.pl | 19 +- bin/xAP-festival.pl | 15 +- bin/xAP-info.pl | 49 +- bin/xAP-speech.pl | 12 +- code/bruce/audrey.pl | 27 +- code/bruce/backup.pl | 20 +- code/bruce/curtains.pl | 23 +- code/bruce/date_reminders.pl | 11 +- code/bruce/door_monitor.pl | 11 +- code/bruce/email_scan.pl | 6 +- code/bruce/get_email_rule.pl | 15 +- code/bruce/heat_cool.pl | 55 +- code/bruce/iButton_bruce.pl | 16 +- code/bruce/iridium_check_events.pl | 22 +- code/bruce/items.pl | 11 +- code/bruce/lcd.pl | 11 +- code/bruce/lights.pl | 12 +- code/bruce/mailbox.pl | 3 +- code/bruce/mh_zip.pl | 8 +- code/bruce/monitor_amazon.pl | 3 +- code/bruce/monitor_dj.pl | 5 +- code/bruce/monitor_game_time.pl | 6 +- code/bruce/monitor_lan.pl | 8 +- code/bruce/monitor_programs.pl | 15 +- code/bruce/monitor_router_bruce.pl | 15 +- code/bruce/monitor_shoutcast.pl | 64 +- code/bruce/monitors.pl | 3 +- code/bruce/mp3_control.pl | 3 +- code/bruce/phone_weeder.pl | 20 +- code/bruce/reports.pl | 10 +- code/bruce/school_clock.pl | 3 +- code/bruce/school_closing.pl | 12 +- code/bruce/sensors.pl | 11 +- code/bruce/startup.pl | 8 +- code/bruce/test_code_hooks.pl | 3 +- code/bruce/test_stuff.pl | 18 +- code/bruce/test_weather.pl | 3 +- code/bruce/test_web_functions.pl | 3 +- code/bruce/test_x10.pl | 3 +- code/bruce/test_x10_light.pl | 20 +- code/bruce/timers2.pl | 9 +- code/bruce/tk_frames_big.pl | 12 +- code/bruce/tk_widgets_bruce.pl | 66 +- code/bruce/tracking_aprs.pl | 175 +- code/bruce/voice_ms.pl | 7 +- code/bruce/wakeup.pl | 16 +- code/bruce/weather.pl | 25 +- code/bruce/weather_log.pl | 24 +- code/bruce/weather_monitor_bruce.pl | 3 +- code/bruce/weatherpoint.pl | 16 +- code/bruce/web_sub.pl | 3 +- code/bruce/web_sub_ron.pl | 17 +- code/bruce/webcam_light.pl | 9 +- code/bruce/webcam_monitor.pl | 3 +- code/bruce/wxserver_server.pl | 3 +- code/common/Ecobee.pl | 3 +- code/common/LitterFree.pl | 15 +- code/common/UIRT2_learning.pl | 109 +- code/common/USB_UIRT_learning.pl | 90 +- code/common/android_server.pl | 36 +- code/common/aqualink.pl | 15 +- code/common/audrey_control.pl | 44 +- code/common/audrey_control2.pl | 70 +- code/common/audrey_monitor.pl | 12 +- code/common/audreyspeak.pl | 3 +- code/common/audreyspeak_volume.pl | 9 +- code/common/azureus_info.pl | 242 +- code/common/barcode_inventory.pl | 3 +- code/common/barcode_scan.pl | 22 +- code/common/barcode_web.pl | 28 +- code/common/benchmarks.pl | 41 +- code/common/bsc.pl | 8 +- code/common/calc_eto.pl | 1497 ++++++------ code/common/callerid.pl | 42 +- code/common/camera_gphoto2.pl | 39 +- code/common/clock_map.pl | 3 +- code/common/clock_map2.pl | 6 +- code/common/cm11_control.pl | 3 +- code/common/cm11_monitor.pl | 9 +- code/common/comic_dailystrips.pl | 29 +- code/common/computer_disk_space.pl | 37 +- code/common/deep_thought.pl | 12 +- code/common/disable_code.pl | 15 +- code/common/display_alpha.pl | 211 +- code/common/display_mythtv.pl | 9 +- code/common/display_osd232.pl | 6 +- code/common/display_slimserver.pl | 7 +- code/common/dvd_player.pl | 15 +- code/common/dvd_releases.pl | 35 +- code/common/eBay.pl | 388 +-- code/common/eliza_server.pl | 6 +- code/common/email_marketplace_sales.pl | 68 +- code/common/email_motion.pl | 14 +- code/common/event_sounds.pl | 3 +- code/common/froggyrita.pl | 33 +- code/common/games_bingo.pl | 7 +- code/common/goofy.pl | 22 +- code/common/headercontroller.pl | 10 +- code/common/hello_print.pl | 3 +- code/common/holiday_ical.pl | 7 +- code/common/homebridge.pl | 58 +- code/common/iButton.pl | 12 +- code/common/ia7_notifications.pl | 51 +- code/common/ia7_phonelogs.pl | 10 +- code/common/ical.pl | 14 +- code/common/idle_items.pl | 20 +- code/common/internet_connect_check.pl | 20 +- code/common/internet_dialup.pl | 16 +- code/common/internet_earthquakes.pl | 54 +- code/common/internet_earthquakes_cal.pl | 73 +- code/common/internet_im.pl | 82 +- code/common/internet_irc.pl | 167 +- code/common/internet_iridium.pl | 31 +- code/common/internet_mail.pl | 73 +- code/common/internet_slashdot.pl | 9 +- code/common/internet_starshine.pl | 38 +- code/common/internet_time.pl | 6 +- code/common/internet_top10.pl | 26 +- code/common/internet_weather.pl | 125 +- code/common/internet_weather_noaa.pl | 34 +- code/common/menu.pl | 9 +- code/common/mh_control.pl | 106 +- code/common/mh_release.pl | 73 +- code/common/mh_sound.pl | 26 +- code/common/mhmedia_xine.pl | 24 +- code/common/mhmms_services.pl | 10 +- code/common/mhsend_server.pl | 12 +- code/common/monitor_actiontec_mi424wr.pl | 91 +- code/common/monitor_memory.pl | 25 +- code/common/monitor_router.pl | 40 +- code/common/monitor_router_lingo.pl | 24 +- code/common/monitor_server.pl | 10 +- code/common/monitor_versalink.pl | 32 +- code/common/mp3.pl | 35 +- code/common/mp3Rip_code.pl | 246 +- code/common/mp3_alsaplayer.pl | 6 +- code/common/mp3_dj.pl | 32 +- code/common/mp3_mpd.pl | 12 +- code/common/mp3_slimserver.pl | 12 +- code/common/mp3_winamp.pl | 116 +- code/common/mythtv_osd.pl | 3 +- code/common/news_ap_breaking.pl | 47 +- code/common/news_email_breaking.pl | 3 +- code/common/news_mquote_oftd.pl | 35 +- code/common/news_onthisday.pl | 27 +- code/common/news_physics_web.pl | 6 +- code/common/news_yahoo.pl | 51 +- code/common/nma_notification.pl | 10 +- code/common/opensprinkler_external.pl | 37 +- code/common/organizer.pl | 163 +- code/common/outlook.pl | 6 +- code/common/pa_control.pl | 14 +- code/common/phone.pl | 22 +- code/common/phone_logs.pl | 44 +- code/common/phone_minutes.pl | 100 +- code/common/phone_pcs_messaging.pl | 11 +- code/common/phone_vcop.pl | 34 +- code/common/photo_index.pl | 36 +- code/common/pocketsphinx_control.pl | 18 +- code/common/power_Xantrex.pl | 63 +- code/common/proxy_client_server.pl | 44 +- code/common/reject_caller_list.pl | 4 +- code/common/robot_esra.pl | 3 +- code/common/robot_robosapien.pl | 33 +- code/common/rss_subscriber.pl | 58 +- code/common/speak_chime.pl | 3 +- code/common/speak_insult.pl | 11 +- code/common/speak_voices.pl | 6 +- code/common/speech_clash.pl | 8 +- code/common/stocks.pl | 76 +- code/common/tattler.pl | 6 +- code/common/telnet.pl | 104 +- code/common/terminal_menu.pl | 15 +- code/common/test_speak.pl | 21 +- code/common/test_xap.pl | 27 +- code/common/time_info.pl | 44 +- code/common/timers.pl | 24 +- code/common/tk_eye.pl | 3 +- code/common/tk_frames.pl | 31 +- code/common/tk_photos.pl | 21 +- code/common/tk_widgets.pl | 3 +- code/common/trivia.pl | 12 +- code/common/tv_btvwin32.pl | 27 +- code/common/tv_grid.pl | 43 +- code/common/tv_info.pl | 64 +- code/common/viavoice_control.pl | 26 +- code/common/vocp_callerid.pl | 85 +- code/common/voice_server.pl | 8 +- code/common/vr_match.pl | 4 +- code/common/weather_chance_of_rain.pl | 52 +- code/common/weather_iB_OWW_client.pl | 32 +- code/common/weather_iB_OWW_txt_client.pl | 36 +- code/common/weather_metar.pl | 56 +- code/common/weather_monitor.pl | 132 +- code/common/weather_pollen.pl | 39 +- code/common/weather_rrd_update.pl | 149 +- code/common/weather_summary.pl | 4 +- code/common/weather_tides.pl | 20 +- code/common/weather_upload.pl | 36 +- code/common/weather_warnings.pl | 14 +- code/common/weather_weatherbug.pl | 302 +-- code/common/weather_wunderground.pl | 245 +- code/common/x10_item_commands.pl | 35 +- code/common/x10_reset.pl | 10 +- code/common/xAP_command.pl | 43 +- code/common/xAP_news.pl | 5 +- code/common/xAP_send.pl | 30 +- code/common/xPL_test.pl | 21 +- code/common/xbmc_notification.pl | 9 +- code/common/xosd.pl | 3 +- code/examples/DSC5401/DSC5401-2nd_approach.pl | 36 +- code/examples/DSC5401/DSC_Alert_Controls.pl | 9 +- code/examples/DSC5401/DSC_Clock.pl | 8 +- code/examples/DSC5401/DSCmotion.pl | 11 +- code/examples/ISDN.pl | 32 +- code/examples/Insteon_thermostat.pl | 6 +- code/examples/Voice_Cmd_enumeration.pl | 3 +- code/examples/hello_print.pl | 3 +- code/examples/iButton_weather_station.pl | 54 +- code/examples/list_groups.pl | 3 +- code/examples/ncpuxa_example.pl | 9 +- code/examples/network_items_web.pl | 3 +- code/examples/ocelot_infrared.pl | 20 +- code/examples/ocelot_sprinklers.pl | 24 +- code/examples/onewire_xap.pl | 11 +- code/examples/organizer_vmode.pl | 16 +- code/examples/play_midi_windows.pl | 3 +- code/examples/sendkey_browser.pl | 7 +- code/examples/serial_interface_comfort.pl | 12 +- code/examples/socket_test2.pl | 18 +- code/examples/telephone_logger.pl | 5 +- code/examples/test_code_hooks.pl | 3 +- code/examples/test_dates.pl | 3 +- code/examples/test_display_image.pl | 3 +- code/examples/test_ftp.pl | 6 +- code/examples/test_idle.pl | 3 +- code/examples/test_keysend.pl | 3 +- code/examples/test_process.pl | 6 +- code/examples/test_speak.pl | 3 +- code/examples/test_tie.pl | 6 +- code/examples/test_time_add.pl | 3 +- code/examples/test_timer.pl | 5 +- code/examples/test_volume.pl | 3 +- code/examples/test_web_styles.pl | 10 +- code/examples/tie_example_bill.pl | 58 +- code/examples/timer_remarks.pl | 6 +- code/examples/timer_reminder.pl | 3 +- code/examples/tk_examples.pl | 3 +- code/examples/volume_level_reset_httpq.pl | 7 +- code/examples/win_registry_set_sound.pl | 6 +- code/examples/x10_cycle_light1.pl | 31 +- code/examples/x10_motion_sensors.pl | 3 +- code/examples/x10_preset_dim.pl | 23 +- code/examples/x10_set_group.pl | 17 +- code/proxy/proxy_server.pl | 27 +- code/public/AudioControl.pl | 97 +- code/public/Brian/grlevel3.pl | 17 +- code/public/Brian/klier.pl | 25 +- code/public/Brian/phonestuff.pl | 3 +- code/public/Brian/radar-wx.pl | 6 +- code/public/Brian/stormwarning.pl | 9 +- code/public/Brian/tracking.pl | 485 ++-- code/public/Brian/xpl.pl | 3 +- code/public/Compool.pl | 173 +- code/public/Compool_test.pl | 74 +- code/public/Danal/Cat_Box.pl | 9 +- code/public/Danal/Christmas_Lights.pl | 4 +- code/public/Danal/Garage_Door.pl | 51 +- code/public/Danal/ISDN.pl | 40 +- code/public/Danal/ISDN_ip.pl | 13 +- code/public/Danal/Master_Bedroom.pl | 9 +- code/public/Danal/Pool_Maint.pl | 15 +- code/public/Danal/Satellite_TLE.pl | 9 +- code/public/Danal/alarm.pl | 6 +- code/public/Danal/aqualink.pl | 27 +- code/public/Danal/pa_control.pl | 13 +- code/public/Danal/pa_test.pl | 6 +- code/public/Danal/test_volume.pl | 3 +- code/public/Danal/timer_kitchen.pl | 47 +- code/public/Esensor_EM01.pl | 6 +- code/public/NetCallerID.pl | 43 +- code/public/Nick/mh_control.pl | 17 +- code/public/Nick/mhsend_server.pl | 6 +- code/public/Nick/mp3_control.pl | 23 +- code/public/Nick/tk_frames.pl | 3 +- code/public/Nick/tk_widgets.pl | 12 +- code/public/Nick/wakeup.pl | 3 +- code/public/Owfs_hvac.pl | 78 +- code/public/Pushover-test.pl | 9 +- code/public/Pushsafer-test.pl | 4 +- code/public/RCSs.pl | 3 +- code/public/Ricardo/bin/get_earthquakes_sp | 12 +- code/public/Ricardo/bin/get_news_sp | 3 +- code/public/Ricardo/bin/get_tv_com | 30 +- code/public/Ricardo/bin/get_tv_grid_sp | 95 +- code/public/Ricardo/bin/get_tv_info_sp | 53 +- code/public/Ricardo/bin/get_weather_ca | 25 +- code/public/Ricardo/bin/get_weather_inm | 15 +- code/public/Ricardo/bin/get_weather_meteored | 28 +- code/public/Ricardo/bin/get_weather_sp | 18 +- .../Ricardo/code/internet_earthquakes_sp.pl | 64 +- .../Ricardo/code/internet_iridium_sp.pl | 17 +- code/public/Ricardo/code/internet_iss_sp.pl | 13 +- code/public/Ricardo/code/news_sp.pl | 16 +- code/public/Ricardo/code/time_info.pl | 22 +- code/public/Ricardo/code/tv_com.pl | 3 +- code/public/Ricardo/code/tv_grid_sp.pl | 4 +- code/public/Ricardo/code/tv_info_sp.pl | 33 +- code/public/Ricardo/code/wakeup.pl | 15 +- code/public/Ricardo/code/wakeup_sc.pl | 4 +- code/public/Ricardo/code/weather_inm.pl | 45 +- code/public/Ricardo/code/weather_meteored.pl | 47 +- code/public/Ricardo/code/weather_sp.pl | 45 +- .../Ricardo/code/weather_station_3600.pl | 40 +- code/public/Roger/ahub.pl | 480 ++-- code/public/Roger/aprsfunc.pl | 85 +- code/public/Roger/aprsin.pl | 34 +- code/public/Roger/aprsinit.pl | 9 +- code/public/Roger/aprsmsg.pl | 35 +- code/public/Roger/vvall.pl | 98 +- code/public/Roger/vvaprs.pl | 47 +- code/public/Roger/weather_se.pl | 161 +- code/public/Xantech_test.pl | 9 +- code/public/Xmms_jukebox.pl | 15 +- code/public/alarm_concept.pl | 15 +- code/public/alarmclock_david.pl | 7 +- code/public/alarmclock_evan.pl | 3 +- code/public/asterisk_gregg.pl | 24 +- code/public/asterisk_jason_mhcommand.agi | 13 +- code/public/asterisk_robert.pl | 15 +- code/public/asterisk_robert_misterhouse.agi | 6 +- code/public/audrey_cid.pl | 10 +- code/public/caddx.pl | 40 +- code/public/caddx_build_parse.pl | 3 +- code/public/caddx_parse.pm | 816 ++----- code/public/callerid_doug.pl | 9 +- code/public/callerid_ncid.pl | 10 +- code/public/callerisdn.pl | 11 +- code/public/calllog.pl | 48 +- code/public/cbus.pl | 97 +- code/public/cd_player.pl | 3 +- code/public/chart_xl.pl | 30 +- code/public/concept.pl | 100 +- code/public/copycode.pl | 31 +- code/public/csv2rrd_weather.pl | 3 +- code/public/cwitte/tar_cvs_sync.pl | 6 +- code/public/date_functions.pl | 31 +- code/public/dialup_unix.pl | 16 +- code/public/dsc_pc5401.pl | 14 +- code/public/dss_interface.pl | 4 +- code/public/eio1.pl | 53 +- code/public/games_chess.pl | 3 +- code/public/garage_door_code.pl | 6 +- code/public/gas_prices.pl | 6 +- code/public/hvac_brian.pl | 19 +- code/public/hvac_brian_newhvac.pl | 265 +-- code/public/hvac_craig.pl | 14 +- code/public/hvac_david.pl | 173 +- code/public/hvac_upb_thermostat.pl | 3 +- code/public/iButton_DS2450.pl | 26 +- code/public/iButton_locks.pl | 5 +- code/public/iButton_temps.pl | 3 +- code/public/iButton_ws.pl | 47 +- code/public/iButton_ws_client.pl | 6 +- code/public/iButton_ws_ernie.pl | 138 +- code/public/ical.pl | 7 +- code/public/internet_ip_update.pl | 23 +- code/public/internet_ip_update_craig.pl | 3 +- code/public/internet_ip_update_hn.pl | 8 +- code/public/internet_ip_update_larry.pl | 6 +- code/public/internet_speed_check.pl | 39 +- code/public/internet_usgs.pl | 36 +- code/public/iphone.pl | 80 +- code/public/irman.pl | 12 +- code/public/irvs | 15 +- code/public/lcd_ian_test.pl | 3 +- code/public/mirror_directory.pl | 3 +- code/public/monitor_dj.pl | 8 +- code/public/monitor_im_status.pl | 6 +- code/public/monitor_ipchainlog.pl | 10 +- code/public/monitor_mbm.pl | 13 +- code/public/monitor_mbm2.pl | 60 +- code/public/monitor_mh.pl | 3 +- code/public/monitor_occupancy_brian.pl | 129 +- code/public/monitor_occupancy_jason.pl | 3 +- code/public/mp3_control_GQmpeg.pl | 6 +- code/public/mp3_control_mrMP3.pl | 7 +- code/public/mp3_control_xmms.pl | 20 +- code/public/mp3_playlist_xmms.pl | 19 +- code/public/mr26_vdr.pl | 3 +- code/public/news_drudge_report.pl | 3 +- code/public/news_onion.pl | 3 +- code/public/news_star_telegram.pl | 3 +- code/public/news_starnews.pl | 4 +- code/public/news_starnews2.pl | 3 +- code/public/news_starnews4.pl | 4 +- code/public/old/NetCallerID.pl | 43 +- code/public/omnistat.pl | 6 +- code/public/organizer_axel.pl | 29 +- code/public/owfs.pl | 6 +- code/public/pa_control_evan.pl | 13 +- code/public/pa_control_vv_tts.pl | 6 +- code/public/palm_calendar.pl | 49 +- code/public/palm_calendar_monitor.pl | 6 +- code/public/pha_k256.pl | 25 +- code/public/phone_identifier.pl | 100 +- code/public/phone_logs_kieran.pl | 18 +- code/public/phone_merlin.pl | 24 +- code/public/pictures_files1.pl | 12 +- code/public/printer_control.pl | 5 +- code/public/process_weather1.pl | 97 +- code/public/rcs.pl | 18 +- code/public/rcstx15_old.pl | 19 +- code/public/redrat.pl | 237 +- code/public/robot_er1.pl | 333 ++- code/public/rrd.pl | 19 +- code/public/rrd_create_graph.pl | 2 +- code/public/rrd_graph | 17 +- code/public/rrd_graph_web.pl | 24 +- code/public/school_clock.pl | 3 +- code/public/school_closing.pl | 12 +- code/public/schoolday.pl | 17 +- code/public/send_alpha_page.pl | 3 +- code/public/send_numeric_page.pl | 9 +- code/public/siteplayer.pl | 7 +- code/public/slinke_build_IR_database.pl | 46 +- code/public/slinke_play_IR_command.pl | 9 +- code/public/sms.pl | 35 +- code/public/speak_mbrola.pl | 8 +- code/public/speak_server.pl | 34 +- code/public/sports_scores_bball.pl | 9 +- code/public/sprinklers_bill.pl | 19 +- code/public/sprinklers_brian.pl | 60 +- code/public/sprinklers_kyle.pl | 12 +- code/public/test_homebase.pl | 24 +- code/public/time_info_swiss.pl | 26 +- code/public/tivo_direct.pl | 13 +- code/public/tv_info_clive.pl | 66 +- code/public/tv_info_ge.pl | 43 +- code/public/v4l_pvr.pl | 65 +- code/public/v4l_radio.pl | 17 +- code/public/video_inline.pl | 12 +- code/public/vocp_func.pl | 19 +- code/public/vocp_sean.pl | 39 +- code/public/voice_client.pl | 8 +- code/public/voicemodem.pl | 22 +- code/public/wakeup_tom.pl | 28 +- code/public/weather.pl | 25 +- code/public/weather_com.pl | 42 +- code/public/weather_ec.pl | 50 +- code/public/weather_ec_web.pl | 44 +- code/public/weather_email_breaking.pl | 17 +- code/public/weather_monitor_ultimeter2000.pl | 190 +- code/public/weather_monitor_wmr968.pl | 50 +- code/public/weather_mos_forecast.pl | 50 +- code/public/weather_sbweather.pl | 13 +- code/public/weather_vw.pl | 25 +- code/public/weather_warnings.pl | 48 +- code/public/weather_wrtv.pl | 3 +- code/public/webcam_lite.pl | 20 +- code/public/weeder_doorbell_evan.pl | 54 +- code/public/weeder_init.pl | 13 +- code/public/whole_house_audio_musica.pl | 176 +- code/public/whole_house_audio_speech.pl | 88 +- code/public/winamp_control.pl | 84 +- code/public/winlirc_client.pl | 74 +- code/public/wintv_radio.pl | 4 +- code/public/wintvcapture.pl | 3 +- code/public/wintvpvr_grid.pl | 30 +- code/public/wxserver_client.pl | 26 +- code/public/wxserver_server.pl | 3 +- code/public/x10_priority.pl | 48 +- code/public/x10_video_security.pl | 48 +- code/public/yahoo_traffic.pl | 45 +- .../hai_web/omnistat_sched_web.pl | 34 +- .../hai_web/omnistat_setup_web.pl | 41 +- code/support/hai-omnistat/omnistat.pl | 76 +- code/test/test_mh.pl | 54 +- lib/AD2.pm | 2085 +++++++++-------- lib/Acid.pm | 14 +- lib/AlexaBridge.pm | 1588 ++++++------- lib/AlsaPlayer.pm | 149 +- lib/AnalogSensor_Item.pm | 98 +- lib/Android_Item.pm | 28 +- lib/Audible_Menu.pm | 5 +- lib/AudiotronPlayer.pm | 40 +- lib/Audrey_Play.pm | 7 +- lib/BSC.pm | 231 +- lib/Base_Item.pm | 15 +- lib/CCNet_Monitor.pm | 26 +- lib/CID_Announce.pm | 21 +- lib/CID_Log.pm | 21 +- lib/CID_Lookup.pm | 20 +- lib/CID_Server.pm | 13 +- lib/Caller_ID.pm | 55 +- lib/Clipsal_CBus.pm | 22 +- lib/Clipsal_CBus/CGate.pm | 181 +- lib/Clipsal_CBus/Group.pm | 12 +- lib/Clipsal_CBus/TriggerGroup.pm | 15 +- lib/Compool.pm | 701 ++---- lib/Concept.pm | 38 +- lib/DOORBIRD.pm | 56 +- lib/DSC5401.pm | 447 ++-- lib/DSC_Alarm.pm | 27 +- lib/DVDPlayer.pm | 48 +- lib/Device_Item.pm | 11 +- lib/Display.pm | 48 +- lib/Display_Alpha.pm | 76 +- lib/Display_osd232.pm | 61 +- lib/Door_Item.pm | 12 +- lib/Dummy_Interface.pm | 10 +- lib/EIB_Device.pm | 20 +- lib/EIB_Items.pm | 153 +- lib/Ecobee.pm | 629 ++--- lib/Eliza.pm | 69 +- lib/Fan_Control.pm | 9 +- lib/File_Item.pm | 8 +- lib/FroggyRita.pm | 53 +- lib/Generic_Item.pm | 177 +- lib/Group.pm | 97 +- lib/HARMON.pm | 92 +- lib/HVweb_Item.pm | 19 +- lib/HomeBase.pm | 50 +- lib/Homevision.pm | 74 +- lib/IR_Item.pm | 15 +- lib/IR_Utils.pm | 54 +- lib/Insteon.pm | 180 +- lib/Insteon/AllLinkDatabase.pm | 751 ++---- lib/Insteon/BaseInsteon.pm | 597 ++--- lib/Insteon/BaseInterface.pm | 241 +- lib/Insteon/Controller.pm | 30 +- lib/Insteon/EZIO8SA.pm | 240 +- lib/Insteon/Energy.pm | 70 +- lib/Insteon/IOLinc.pm | 96 +- lib/Insteon/Irrigation.pm | 103 +- lib/Insteon/Lighting.pm | 195 +- lib/Insteon/Message.pm | 76 +- lib/Insteon/MessageDecoder.pm | 333 +-- lib/Insteon/PLMTerminal.pl | 16 +- lib/Insteon/Security.pm | 73 +- lib/Insteon/Thermostat.pm | 291 +-- lib/Insteon_PLM.pm | 330 +-- lib/Irrigation_Item.pm | 11 +- lib/K8055.pm | 11 +- lib/LCD.pm | 6 +- lib/Light_Item.pm | 242 +- lib/Light_Restriction_Item.pm | 24 +- lib/Light_Switch_Item.pm | 12 +- lib/Logger.pm | 5 +- lib/Lynx10PLC.pm | 137 +- lib/MBM_mh.pm | 11 +- lib/MBM_sensors.pm | 58 +- lib/MSN.pm | 46 +- lib/Marrick.pm | 47 +- lib/Motion_Item.pm | 14 +- lib/Motion_Tracker.pm | 3 +- lib/Mp3Player.pm | 13 +- lib/Musica.pm | 562 ++--- lib/MySensors.pm | 233 +- lib/Nest.pm | 97 +- lib/Network_Item.pm | 8 +- lib/Numbered_Menu.pm | 24 +- lib/Occupancy_Monitor.pm | 607 +---- lib/Omnistat.pm | 167 +- lib/OneWire_xAP.pm | 24 +- lib/OpenSprinkler.pm | 418 ++-- lib/Owfs_Item.pm | 316 +-- lib/Owfs_Thermostat.pm | 139 +- lib/PAobj.pm | 78 +- lib/PLCBUS.pm | 614 +++-- lib/Parport_Item.pm | 9 +- lib/Philips_Hue.pm | 31 +- lib/Photocell_Item.pm | 7 +- lib/PocketSphinx.pm | 97 +- lib/Presence_Monitor.pm | 56 +- lib/Process_Item.pm | 65 +- lib/Pushbullet.pm | 40 +- lib/Pushover.pm | 84 +- lib/Pushsafer.pm | 62 +- lib/RCS_Item.pm | 74 +- lib/RCSs.pm | 8 +- lib/RCSsTR40.pm | 86 +- lib/RF_Item.pm | 5 +- lib/Rain8Net.pm | 911 ++++--- lib/RedRat.pm | 4 +- lib/RollFileHandle.pm | 8 +- lib/SCHEDULE.pm | 837 +++---- lib/Scene.pm | 40 +- lib/SchoolDays.pm | 25 +- lib/Serial_Item.pm | 96 +- lib/Slinke.pm | 121 +- lib/SoapServer.pm | 26 +- lib/Socket_Item.pm | 65 +- lib/SqueezeboxCLI.pm | 78 +- lib/Stargate.pm | 215 +- lib/Stargate485.pm | 72 +- lib/SunTime_mh.pm | 4 +- lib/SysDiag_xAP.pm | 22 +- lib/TED.pm | 126 +- lib/TI103.pm | 40 +- lib/Telephone_logger.pm | 2 +- lib/Telephony_DTMF.pm | 3 +- lib/Telephony_Identifier.pm | 14 +- lib/Telephony_Interface.pm | 48 +- lib/Telephony_xAP.pm | 40 +- lib/Terminal_Menu.pm | 134 +- lib/Text_Cmd.pm | 4 +- lib/Timer.pm | 45 +- lib/Tivo_Control.pm | 7 +- lib/UIRT2.pm | 61 +- lib/UPBPIM.pm | 28 +- lib/UPB_Device.pm | 20 +- lib/UPB_Rain8.pm | 4 +- lib/UPB_Thermostat.pm | 39 +- lib/USB_UIRT.pm | 144 +- lib/Ultimeter.pm | 37 +- lib/Venstar_Colortouch.pm | 1041 +++----- lib/Video_Inline.pm | 6 +- lib/VirtualAudio.pm | 102 +- lib/Voice_Cmd.pm | 149 +- lib/Voice_Text.pm | 195 +- lib/Weather_Common.pm | 79 +- lib/Weather_Item.pm | 26 +- lib/Weather_davisvantageproii.pm | 90 +- lib/Weather_daviswm2.pm | 67 +- lib/Weather_vw.pm | 6 +- lib/Weather_wmr968.pm | 131 +- lib/Weather_wx200.pm | 22 +- lib/WebServices.pm | 6 +- lib/Weeder_Light.pm | 15 +- lib/Wink.pm | 591 ++--- lib/X10_BX24.pm | 64 +- lib/X10_CMxx.pm | 33 +- lib/X10_Interface.pm | 12 +- lib/X10_Items.pm | 249 +- lib/X10_JR21A.pm | 9 +- lib/X10_MR26.pm | 3 +- lib/X10_RF.pm | 23 +- lib/X10_RF_digimax.pm | 8 +- lib/X10_RF_powerline.pm | 45 +- lib/X10_RF_rfxsensor.pm | 73 +- lib/X10_RF_security.pm | 23 +- lib/X10_RF_tv_remote.pm | 18 +- lib/X10_RedRat.pm | 65 +- lib/X10_Scene.pm | 83 +- lib/X10_W800.pm | 23 +- lib/X10_Wish.pm | 6 +- lib/X10_iplcs.pm | 29 +- lib/Xantech.pm | 38 +- lib/ZWave_Items.pm | 51 +- lib/Zmtrigger_Item.pm | 27 +- lib/ZoneMinder_xAP.pm | 72 +- lib/ajax.pm | 21 +- lib/caddx.pm | 6 +- lib/console_utils.pl | 23 +- lib/dsc.pm | 1649 ++++++------- lib/dss_interface.pm | 9 +- lib/eto.pm | 1221 +++++----- lib/example_interface.pm | 4 +- lib/floorplan.pl | 70 +- lib/handy_net_utilities.pl | 210 +- lib/handy_tk_utilities.pl | 210 +- lib/handy_utilities.pl | 192 +- lib/http_server.pl | 684 ++---- lib/http_utils.pl | 33 +- lib/iButton.pm | 36 +- lib/ia7_utilities.pl | 48 +- lib/imap_utils.pl | 24 +- lib/json_server.pl | 233 +- lib/menu_code.pl | 156 +- lib/mh_perl2exe_list.pl | 3 +- lib/mqtt.pm | 122 +- lib/ncpuxa.pm | 17 +- lib/ncpuxa_mh.pm | 35 +- lib/net_gmail_utils.pl | 8 +- lib/read_table_A.pl | 35 +- lib/read_table_xml.pl | 67 +- lib/table_A2XML.pm | 9 +- lib/tk_widgets.pl | 17 +- lib/trigger_code.pl | 21 +- lib/vsDB.pm | 28 +- lib/vsEmail.pm | 69 +- lib/vsLock.pm | 19 +- lib/xAP_Items.pm | 199 +- lib/xPL_Irrigation.pm | 131 +- lib/xPL_Items.pm | 194 +- lib/xPL_Lighting.pm | 55 +- lib/xPL_Plugwise.pm | 36 +- lib/xPL_Security.pm | 109 +- lib/xPL_Squeezebox.pm | 21 +- lib/xml_server.pl | 26 +- web/bin/DSC5401_web_content.pl | 102 +- web/bin/ListManager.pl | 287 +-- web/bin/button.pl | 3 +- web/bin/button2.pl | 164 +- web/bin/callerid.pl | 36 +- web/bin/code_search.pl | 3 +- web/bin/code_select.pl | 50 +- web/bin/code_unselect.pl | 45 +- web/bin/command_search.pl | 10 +- web/bin/counter.pl | 6 +- web/bin/dbmedit.cgi | 46 +- web/bin/display_map.pl | 3 +- web/bin/flight_status.pl | 6 +- web/bin/floorplan.pl | 73 +- web/bin/floorplan_svg.pl | 14 +- web/bin/headercontrol.pl | 12 +- web/bin/hvac.pl | 3 +- web/bin/icon_auth.pl | 5 +- web/bin/icon_mode.pl | 3 +- web/bin/iniedit.pl | 61 +- web/bin/items.pl | 149 +- web/bin/k8055.pl | 15 +- web/bin/list_buttons.pl | 8 +- web/bin/list_buttons2.pl | 3 +- web/bin/list_categories.pl | 13 +- web/bin/list_items.pl | 11 +- web/bin/list_widgets.pl | 12 +- web/bin/menu.pl | 51 +- web/bin/menu_old.pl | 9 +- web/bin/mh_usage.pl | 6 +- web/bin/mp3_applet_playlist.pl | 18 +- web/bin/mp3_search.pl | 104 +- web/bin/mrwiki.pl | 505 ++-- web/bin/phone_in.pl | 6 +- web/bin/phone_in_old.pl | 3 +- web/bin/phone_list.pl | 14 +- web/bin/phone_out.pl | 6 +- web/bin/phone_out2.pl | 6 +- web/bin/phone_search.pl | 3 +- web/bin/photo_search.pl | 8 +- web/bin/photos.pl | 18 +- web/bin/photos_slideshow.pl | 3 +- web/bin/rejectcall.pl | 13 +- web/bin/rss_logs.pl | 26 +- web/bin/runit.pl | 3 +- web/bin/set_cookie.pl | 6 +- web/bin/set_cookie2.pl | 3 +- web/bin/set_func.pl | 9 +- web/bin/set_parm_tv_provider.pl | 12 +- web/bin/set_parm_weather_local.pl | 19 +- web/bin/shopping_list.pl | 67 +- web/bin/soapcgi.pl | 3 +- web/bin/status_line.pl | 35 +- web/bin/status_panel.pl | 5 +- web/bin/triggers.pl | 65 +- web/bin/uptime.pl | 3 +- web/bin/voicemail.pl | 13 +- web/bin/wc_settings.pl | 7 +- web/bin/weather_graph.pl | 20 +- web/bin/weather_graph_zoom.pl | 118 +- web/bin/webcam_dirs.pl | 36 +- web/bin/webcam_shows.pl | 42 +- web/comics/dailystrips/dailystrips-update | 31 +- web/comics/dailystrips/install.pl | 17 +- web/ia4/web_sub.pl | 29 +- web/ia5/modes/house_mode.pl | 3 +- web/ia5/phone/voicemail.pl | 13 +- web/ia5/pictures/getcomics.pl | 35 +- web/ia6/date.pl | 10 +- web/ia6/weatherpoint.pl | 8 +- web/misc/mp3Rip.pl | 227 +- web/music/MP3_WebCtrl.pl | 30 +- web/music/MP3_WebPlaylist.pl | 15 +- web/music/xmms/MP3_WebXmmsCtrl.pl | 33 +- web/music/xmms/MP3_WebXmmsPlaylist.pl | 15 +- web/organizer/calendar.pl | 204 +- web/organizer/contacts.pl | 67 +- web/organizer/tasks.pl | 123 +- web/test/test1.pl | 12 +- 820 files changed, 19049 insertions(+), 36017 deletions(-) diff --git a/bin/alpha_page b/bin/alpha_page index d6d271c58..62bd83124 100755 --- a/bin/alpha_page +++ b/bin/alpha_page @@ -27,26 +27,21 @@ my ( $Pgm_Path, $Pgm_Name, $Version ); use vars '$Pgm_Root'; # So we can see it in eval var subs in read_parms BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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; $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 } use Getopt::Long; my %parms; -if ( - !&GetOptions( \%parms, "h", "help", "message=s", "name:s", "pin:i", - "service:s" ) +if ( !&GetOptions( \%parms, "h", "help", "message=s", "name:s", "pin:i", "service:s" ) or @ARGV or $parms{h} or $parms{help} or !$parms{message} - or !$parms{service} - ) + or !$parms{service} ) { print < $parms{size} KBytes\n", - $counts{size_size} / 10**6, $counts{count_size}; - $msg .= - sprintf - " - Skipped %5.1f MB of data from %4d files with name = $parms{skip}\n", - $counts{size_skip} / 10**6, $counts{count_skip}; - $msg .= - sprintf - " - Skipped %5.1f MB of data from %4d files with age > $parms{age}\n", - $counts{size_age} / 10**6, $counts{count_age}; + $msg .= sprintf " - Storing %5.1f MB of data from %4d files\n", $counts{size} / 10**6, $counts{file}; + $msg .= sprintf " - Skipped %5.1f MB of data from %4d files with size > $parms{size} KBytes\n", $counts{size_size} / 10**6, $counts{count_size}; + $msg .= sprintf " - Skipped %5.1f MB of data from %4d files with name = $parms{skip}\n", $counts{size_skip} / 10**6, $counts{count_skip}; + $msg .= sprintf " - Skipped %5.1f MB of data from %4d files with age > $parms{age}\n", $counts{size_age} / 10**6, $counts{count_age}; print $msg; $log .= $msg; } @@ -156,8 +140,7 @@ sub tar_files { } } my ( $size, $date ) = ( stat $parms{file} )[ 7, 9 ]; - printf "\nFile stats: %s %s %s\n\n", $parms{file}, $size, - scalar localtime $date; + printf "\nFile stats: %s %s %s\n\n", $parms{file}, $size, scalar localtime $date; } sub read_dir { diff --git a/bin/dailystrips b/bin/dailystrips index 8b30a3d5f..62caad0ca 100755 --- a/bin/dailystrips +++ b/bin/dailystrips @@ -25,13 +25,10 @@ use File::Copy; # Variables my ( - %options, $version, $time_today, - @localtime_today, @localtime_yesterday, @localtime_tomorrow, - $long_date, $short_date, $short_date_yesterday, - $short_date_tomorrow, @get, @strips, - %defs, $known_strips, %groups, - $known_groups, %classes, $val, - $link_tomorrow, $no_dateparse, @base_dirparts + %options, $version, $time_today, @localtime_today, @localtime_yesterday, @localtime_tomorrow, + $long_date, $short_date, $short_date_yesterday, $short_date_tomorrow, @get, @strips, + %defs, $known_strips, %groups, $known_groups, %classes, $val, + $link_tomorrow, $no_dateparse, @base_dirparts ); $version = "1.0.28"; @@ -40,15 +37,10 @@ $time_today = time; # Get options GetOptions( - \%options, 'quiet|q', 'verbose', 'output=s', - 'lite', 'local|l', 'noindex', 'archive|a', - 'dailydir|d', 'stripdir', 'save|s', 'nostale', - 'date=s', 'new|n', 'defs=s', 'nopersonal', - 'basedir=s', 'list', 'proxy=s', 'proxyauth=s', - 'noenvproxy', 'nospaces', 'useragent=s', 'version|v', - 'help|h', 'avantgo', 'random', 'nosystem', - 'stripnav', 'nosymlinks', 'titles=s', 'retries=s', - 'clean=s', 'updates=s', 'noupdates' + \%options, 'quiet|q', 'verbose', 'output=s', 'lite', 'local|l', 'noindex', 'archive|a', 'dailydir|d', 'stripdir', + 'save|s', 'nostale', 'date=s', 'new|n', 'defs=s', 'nopersonal', 'basedir=s', 'list', 'proxy=s', 'proxyauth=s', + 'noenvproxy', 'nospaces', 'useragent=s', 'version|v', 'help|h', 'avantgo', 'random', 'nosystem', 'stripnav', 'nosymlinks', + 'titles=s', 'retries=s', 'clean=s', 'updates=s', 'noupdates' ) or exit 1; # Process options: @@ -266,8 +258,7 @@ if ( $options{'dailydir'} and $options{'stripdir'} ) { if ( $options{'proxy'} ) { $options{'proxy'} =~ /^(http:\/\/)?(.*?):(.+?)\/?$/i; unless ( $2 and $3 ) { - die - "Error: incorrectly formatted proxy server ('http://server:port' expected)\n"; + die "Error: incorrectly formatted proxy server ('http://server:port' expected)\n"; } $options{'proxy'} = "http://$2:$3"; @@ -276,8 +267,7 @@ if ( $options{'proxy'} ) { if ( !$options{'noenvproxy'} and !$options{'proxy'} and $ENV{'http_proxy'} ) { $ENV{'http_proxy'} =~ /(http:\/\/)?(.*?):(.+?)\/?$/i; unless ( $2 and $3 ) { - die - "Error: incorrectly formatted proxy server environment variable\n('http://server:port' expected)\n"; + die "Error: incorrectly formatted proxy server environment variable\n('http://server:port' expected)\n"; } $options{'proxy'} = "http://$2:$3"; @@ -285,8 +275,7 @@ if ( !$options{'noenvproxy'} and !$options{'proxy'} and $ENV{'http_proxy'} ) { if ( $options{'proxyauth'} ) { unless ( $options{'proxyauth'} =~ /^.+?:.+?$/ ) { - die - "Error: incorrectly formatted proxy credentials ('user:pass' expected)\n"; + die "Error: incorrectly formatted proxy credentials ('user:pass' expected)\n"; } } @@ -357,15 +346,13 @@ if ( $options{'local'} ) { # any issues with masks and Win32? unless ( mkdir( $short_date, 0755 ) ) { - die - "Error: could not create today's directory ($short_date/)\n"; + die "Error: could not create today's directory ($short_date/)\n"; } } } unless ( open( STDOUT, ">dailystrips-$short_date.html" ) ) { - die - "Error: could not open HTML file (dailystrips-$short_date.html) for writing\n"; + die "Error: could not open HTML file (dailystrips-$short_date.html) for writing\n"; } unless ( $options{'date'} ) { @@ -417,10 +404,7 @@ if ( $options{'local'} ) { unless ( grep( //, @archive ) ) { for (@archive) { - if ( - s/()/$1\n$long_date<\/a>
    / - ) - { + if (s/()/$1\n
    $long_date<\/a>
    /) { unless ( open( ARCHIVE, ">archive.html" ) ) { die "Error: could not open archive.html for writing\n"; } @@ -443,10 +427,7 @@ if ( $options{'local'} ) { my $match_count; for (@previous_page) { - if ( - s// |
    Next day<\/a>/ - ) - { + if (s// | Next day<\/a>/) { $match_count++; last if ( $match_count == 2 ); } @@ -457,18 +438,15 @@ if ( $options{'local'} ) { close(PREVIOUS); } else { - warn - "Warning: could not open dailystrips-$short_date_yesterday.html for writing\n"; + warn "Warning: could not open dailystrips-$short_date_yesterday.html for writing\n"; } } else { - warn - "Warning: did not find any tag in previous day's file to make today's link\n"; + warn "Warning: did not find any tag in previous day's file to make today's link\n"; } } else { - warn - "Warning: could not open dailystrips-$short_date_yesterday.html for reading\n"; + warn "Warning: could not open dailystrips-$short_date_yesterday.html for reading\n"; } } @@ -478,8 +456,7 @@ elsif ( $options{'output'} ) { } unless ( open( STDOUT, ">$options{'output'}" ) ) { - die - "Error: Could not open output file ($options{'output'}) for writing\n"; + die "Error: Could not open output file ($options{'output'}) for writing\n"; } } @@ -506,8 +483,7 @@ unless ( $options{'quiet'} ) { } if ( -e "dailystrips-$short_date_tomorrow.html" ) { - $link_tomorrow = - " | Next day"; + $link_tomorrow = " | Next day"; } else { $link_tomorrow = ""; @@ -515,8 +491,7 @@ else { # Generate HTML page if ( $options{'lite'} ) { - print - "$options{'titles'}dailystrips for $long_date

    \n"; + print "$options{'titles'}dailystrips for $long_date

    \n"; } else { my $topanchor; @@ -572,17 +547,13 @@ for (@strips) { my ( $strip, $name, $homepage, $img_addr, $referer, $prefetch, $artist ) = split( /;/, $_ ); my ( - $img_line, $local_name, - $local_name_dir, $local_name_file, - $local_name_ext, $image, - $ext, $local_name_yesterday, - $local_name_yesterday_dir, $local_name_yesterday_file, - $local_name_yesterday_ext + $img_line, $local_name, $local_name_dir, $local_name_file, + $local_name_ext, $image, $ext, $local_name_yesterday, + $local_name_yesterday_dir, $local_name_yesterday_file, $local_name_yesterday_ext ); if ( $options{'verbose'} and $options{'local'} ) { - warn "Downloading strip file for " - . lc( ( split( /;/, $_ ) )[0] ) . "\n"; + warn "Downloading strip file for " . lc( ( split( /;/, $_ ) )[0] ) . "\n"; } if ( $img_addr =~ "^unavail" ) { @@ -612,8 +583,7 @@ for (@strips) { $local_name_ext = "$ext"; } elsif ( $options{'dailydir'} ) { - $local_name_yesterday = - "$short_date_yesterday/$name-$short_date_yesterday$ext"; + $local_name_yesterday = "$short_date_yesterday/$name-$short_date_yesterday$ext"; $local_name_yesterday_dir = "$short_date_yesterday/"; $local_name_yesterday_file = "$name-$short_date_yesterday"; $local_name_yesterday_ext = "$ext"; @@ -668,8 +638,7 @@ for (@strips) { $img_addr = $local_name; $img_addr =~ s/ /\%20/go; if ( $options{'stripnav'} ) { - $img_line = - "\"$name\"
    Return to top"; + $img_line = "\"$name\"
    Return to top"; } else { $img_line = "\"$name\""; @@ -729,8 +698,7 @@ for (@strips) { $img_addr = $local_name; $img_addr =~ s/ /\%20/go; if ( $options{'stripnav'} ) { - $img_line = - "\"$name\"
    Return to top"; + $img_line = "\"$name\"
    Return to top"; } else { $img_line = "\"$name\""; @@ -743,57 +711,41 @@ for (@strips) { print IMAGE $image; close(IMAGE); - if ( - -e $local_name - and system( - "diff \"$local_name\" \"$local_name.tmp\" >/dev/null 2>&1" - ) == 0 - ) + if ( -e $local_name + and system("diff \"$local_name\" \"$local_name.tmp\" >/dev/null 2>&1") == 0 ) { # already downloaded the same strip earlier today unlink("$local_name.tmp"); if ( $options{'avantgo'} ) { - $img_line = - &make_avantgo_table( $local_name, $ext ); + $img_line = &make_avantgo_table( $local_name, $ext ); } else { $img_addr = $local_name; $img_addr =~ s/ /\%20/go; if ( $options{'stripnav'} ) { - $img_line = - "\"$name\"
    Return to top"; + $img_line = "\"$name\"
    Return to top"; } else { - $img_line = - "\"$name\""; + $img_line = "\"$name\""; } } } - elsif ( - system( - "diff \"$local_name_yesterday\" \"$local_name.tmp\" >/dev/null 2>&1" - ) == 0 - ) - { + elsif ( system("diff \"$local_name_yesterday\" \"$local_name.tmp\" >/dev/null 2>&1") == 0 ) { + # same strip as yesterday if ( $options{'nosymlinks'} ) { - system( "mv", "$local_name.tmp", - "$local_name" ); + system( "mv", "$local_name.tmp", "$local_name" ); } else { unlink("$local_name.tmp"); if ( $options{'stripdir'} or $options{'dailydir'} ) { - system( - "ln -s \"../$local_name_yesterday\" \"$local_name\" >/dev/null 2>&1" - ); + system("ln -s \"../$local_name_yesterday\" \"$local_name\" >/dev/null 2>&1"); } else { - system( - "ln -s \"$local_name_yesterday\" \"$local_name\" >/dev/null 2>&1" - ); + system("ln -s \"$local_name_yesterday\" \"$local_name\" >/dev/null 2>&1"); } } @@ -805,12 +757,10 @@ for (@strips) { $img_addr = $local_name; $img_addr =~ s/ /\%20/go; if ( $options{'stripnav'} ) { - $img_line = - "\"$name\"
    Return to top"; + $img_line = "\"$name\"
    Return to top"; } else { - $img_line = - "\"$name\""; + $img_line = "\"$name\""; } } } @@ -823,21 +773,17 @@ for (@strips) { system( "mv", "$local_name.tmp", "$local_name" ); if ( $options{'avantgo'} ) { - &make_avantgo_files( $local_name, - $local_name_ext ); - $img_line = - &make_avantgo_table( $local_name, $ext ); + &make_avantgo_files( $local_name, $local_name_ext ); + $img_line = &make_avantgo_table( $local_name, $ext ); } else { $img_addr = $local_name; $img_addr =~ s/ /\%20/go; if ( $options{'stripnav'} ) { - $img_line = - "\"$name\"
    Return to top"; + $img_line = "\"$name\"
    Return to top"; } else { - $img_line = - "\"$name\""; + $img_line = "\"$name\""; } } } @@ -849,8 +795,7 @@ for (@strips) { else { # regular mode - just give addresses to strips on their webserver if ( $options{'stripnav'} ) { - $img_line = - "\"$name\"
    Return to top"; + $img_line = "\"$name\"
    Return to top"; } else { $img_line = "\"$name\""; @@ -863,8 +808,7 @@ for (@strips) { } if ( $options{'lite'} ) { - print - "$name$artist
    + print "$name$artist
    $img_line

    "; @@ -972,8 +916,7 @@ sub http_get { if ( $response->is_error() ) { if ( $options{'verbose'} ) { - warn - "Warning: could not download $url: $status (attempt $_ of $options{'retries'})\n"; + warn "Warning: could not download $url: $status (attempt $_ of $options{'retries'})\n"; } } else { @@ -1003,8 +946,7 @@ sub get_strip { if ( $page =~ /^ERROR/ ) { if ( $options{'verbose'} ) { - warn - "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-server"; @@ -1020,8 +962,7 @@ sub get_strip { unless ( ${ $defs{$strip}{'matchpart'} } ) { if ( $options{'verbose'} ) { - warn - "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-nomatch"; @@ -1035,10 +976,7 @@ sub get_strip { $addr =~ s/\$match/$match/ge; } else { - $addr = - $defs{$strip}{'baseurl'} - . $match - . $defs{$strip}{'urlsuffix'}; + $addr = $defs{$strip}{'baseurl'} . $match . $defs{$strip}{'urlsuffix'}; } } } @@ -1052,8 +990,7 @@ sub get_strip { if ( $page =~ /^ERROR/ ) { if ( $options{'verbose'} ) { - warn - "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-server"; @@ -1070,8 +1007,7 @@ sub get_strip { unless ( $regexmatch[1] ) { warn "didn't match\n"; if ( $options{'verbose'} ) { - warn - "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-nomatch"; @@ -1109,8 +1045,7 @@ sub get_strip { if ( $page =~ /^ERROR/ ) { if ( $options{'verbose'} ) { - warn - "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-server"; @@ -1127,8 +1062,7 @@ sub get_strip { unless ( $regexmatch[2] ) { warn "didn't match\n"; if ( $options{'verbose'} ) { - warn - "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-nomatch"; @@ -1174,8 +1108,7 @@ sub get_strip { $page = &http_get( $defs{$strip}{'searchpage'} ); if ( $page =~ /^ERROR/ ) { if ( $options{'verbose'} ) { - warn - "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: could not download searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-server"; } @@ -1188,8 +1121,7 @@ sub get_strip { unless ( $regexmatch[1] ) { warn "didn't match\n"; if ( $options{'verbose'} ) { - warn - "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; + warn "Error: $strip: searchpattern $defs{$strip}{'searchpattern'} did not match anything in searchpage $defs{$strip}{'searchpage'}\n"; } $addr = "unavail-nomatch"; } @@ -1203,8 +1135,7 @@ sub get_strip { } else { # find the information in the second page - my ($searchkey) = - $page =~ /$defs{$strip}{'searchpattern2'}/; + my ($searchkey) = $page =~ /$defs{$strip}{'searchpattern2'}/; if ( $searchkey eq '' ) { warn "Error: $strip: unable to locate searchkey2\n"; } @@ -1224,9 +1155,7 @@ sub get_strip { unless ( $addr =~ /^(http:\/\/|unavail)/io ) { $addr = "http://" . $addr } - push( @strips, - "$strip;$defs{$strip}{'name'};$defs{$strip}{'homepage'};$addr;$defs{$strip}{'referer'};$defs{$strip}{'prefetch'};$defs{$strip}{'artist'}" - ); + push( @strips, "$strip;$defs{$strip}{'name'};$defs{$strip}{'homepage'};$addr;$defs{$strip}{'referer'};$defs{$strip}{'prefetch'};$defs{$strip}{'artist'}" ); } sub get_defs { @@ -1293,12 +1222,8 @@ sub get_defs { my $using_class = $defs{$strip}{'useclass'}; # import vars from class - for ( - qw(homepage searchpage searchpattern baseurl imageurl urlsuffix referer prefetch artist drmurl searchpattern2) - ) - { - if ( $classes{$using_class}{$_} and !$defs{$strip}{$_} ) - { + for (qw(homepage searchpage searchpattern baseurl imageurl urlsuffix referer prefetch artist drmurl searchpattern2)) { + if ( $classes{$using_class}{$_} and !$defs{$strip}{$_} ) { my $classvar = $classes{$using_class}{$_}; $classvar =~ s/(\$[0-9])/$defs{$strip}{$1}/g; $classvar =~ s/\$strip/$strip/g; @@ -1307,8 +1232,7 @@ sub get_defs { } for (qw(type matchpart provides)) { - if ( $classes{$using_class}{$_} and !$defs{$strip}{$_} ) - { + if ( $classes{$using_class}{$_} and !$defs{$strip}{$_} ) { $defs{$strip}{$_} = $classes{$using_class}{$_}; } } @@ -1328,35 +1252,23 @@ sub get_defs { } #other vars in definition - for ( - qw(homepage searchpage searchpattern imageurl baseurl urlsuffix referer prefetch) - ) - { + for (qw(homepage searchpage searchpattern imageurl baseurl urlsuffix referer prefetch)) { if ( $defs{$strip}{$_} ) { - $defs{$strip}{$_} =~ - s/\$(name|homepage|searchpage|searchpattern|imageurl|baseurl|referer|prefetch)/$defs{$strip}{$1}/g; + $defs{$strip}{$_} =~ s/\$(name|homepage|searchpage|searchpattern|imageurl|baseurl|referer|prefetch)/$defs{$strip}{$1}/g; } } #dates - for ( - qw(homepage searchpage searchpattern imageurl baseurl urlsuffix referer prefetch) - ) - { + for (qw(homepage searchpage searchpattern imageurl baseurl urlsuffix referer prefetch)) { if ( $defs{$strip}{$_} ) { - $defs{$strip}{$_} =~ - s/(\%(-?)[a-zA-Z])/strftime("$1", @localtime_today)/ge; + $defs{$strip}{$_} =~ s/(\%(-?)[a-zA-Z])/strftime("$1", @localtime_today)/ge; } } # stuff - for ( - qw(homepage searchpage searchpattern imageurl baseurl urlsuffix referer) - ) - { + for (qw(homepage searchpage searchpattern imageurl baseurl urlsuffix referer)) { if ( $defs{$strip}{$_} ) { - $defs{$strip}{$_} =~ - s//&my_eval($1)/ge; + $defs{$strip}{$_} =~ s//&my_eval($1)/ge; } } @@ -1377,13 +1289,11 @@ sub get_defs { if ( $defs{$strip}{'type'} eq "search" ) { unless ( $defs{$strip}{'searchpattern'} ) { - die - "Error: strip $strip has no 'searchpattern' value in $defs_file\n"; + die "Error: strip $strip has no 'searchpattern' value in $defs_file\n"; } unless ( $defs{$strip}{'searchpattern'} =~ /\(.+\)/ ) { - die - "Error: strip $strip has no parentheses in searchpattern\n"; + die "Error: strip $strip has no parentheses in searchpattern\n"; } unless ( $defs{$strip}{'matchpart'} ) { @@ -1398,20 +1308,17 @@ sub get_defs { or $defs{$strip}{'urlsuffix'} ) ) { - die - "Error: strip $strip: cannot use both 'imageurl' at the same time as 'baseurl'\nor 'urlsuffix'\n"; + die "Error: strip $strip: cannot use both 'imageurl' at the same time as 'baseurl'\nor 'urlsuffix'\n"; } } elsif ( $defs{$strip}{'type'} eq "generate" ) { unless ( $defs{$strip}{'imageurl'} ) { - die - "Error: strip $strip has no 'imageurl' value in $defs_file\n"; + die "Error: strip $strip has no 'imageurl' value in $defs_file\n"; } } unless ( $defs{$strip}{'provides'} ) { - die - "Error: strip $strip has no 'provides' value in $defs_file\n"; + die "Error: strip $strip has no 'provides' value in $defs_file\n"; } #debugger @@ -1498,8 +1405,7 @@ sub get_defs { } elsif (/^useclass\s+(.+)$/i) { unless ( defined $classes{$1} ) { - die - "Error: strip $strip references invalid class $1 at $defs_file line $line\n"; + die "Error: strip $strip references invalid class $1 at $defs_file line $line\n"; } $defs{$strip}{'useclass'} = $1; @@ -1508,9 +1414,7 @@ sub get_defs { $defs{$strip}{'homepage'} = $1; } elsif (/^type\s+(.+)$/i) { - unless ( $1 =~ - /^(search|generate|flashdrm|search2pages|doublesearch)$/i ) - { + unless ( $1 =~ /^(search|generate|flashdrm|search2pages|doublesearch)$/i ) { die "Error: invalid 'type' at $defs_file line $line\n"; } @@ -1561,8 +1465,7 @@ sub get_defs { $defs{$strip}{'artist'} = $1; } elsif (/^(.+)\s+?/) { - die - "Error: Unknown keyword '$1' at $defs_file line $line, in strip $strip\n"; + die "Error: Unknown keyword '$1' at $defs_file line $line, in strip $strip\n"; } } elsif ( $sectype eq "group" ) { @@ -1578,8 +1481,7 @@ sub get_defs { join( ';', split( /\s+/, $1 ) ) . ";"; } elsif (/^(.+)\s+?/) { - die - "Error: Unknown keyword '$1' at $defs_file line $line, in group $group\n"; + die "Error: Unknown keyword '$1' at $defs_file line $line, in group $group\n"; } } } @@ -1655,10 +1557,7 @@ sub make_avantgo_table { foreach my $row ( 0 .. ( $rows - 1 ) ) { $table .= "
    "; foreach my $col ( 0 .. ( $cols - 1 ) ) { - $table .= - ""; + $table .= ""; } $table .= ""; diff --git a/bin/dailystrips-clean b/bin/dailystrips-clean index 94999b319..d0c79077a 100755 --- a/bin/dailystrips-clean +++ b/bin/dailystrips-clean @@ -27,10 +27,7 @@ $version = "1.0.1"; $time_today = time; # Get options -GetOptions( - \%options, 'quiet|q', 'verbose|v', 'test|t', - 'dir=s', 'archive|a', 'version|v', 'help|h' -) or exit 1; +GetOptions( \%options, 'quiet|q', 'verbose|v', 'test|t', 'dir=s', 'archive|a', 'version|v', 'help|h' ) or exit 1; # Help and version override anything else if ( $options{'help'} ) { @@ -133,8 +130,7 @@ for (@files) { if ( $options{'archive'} ) { if ( open( ARCHIVE, "<$options{'dir'}archive.html" ) ) { - my $oldest = strftime( "\%Y.\%m.\%d", - localtime( $time_today - ( 86400 * ( $options{'days'} - 1 ) ) ) ); + my $oldest = strftime( "\%Y.\%m.\%d", localtime( $time_today - ( 86400 * ( $options{'days'} - 1 ) ) ) ); my $out; while () { @@ -151,8 +147,7 @@ if ( $options{'archive'} ) { print ARCHIVE $out; } else { - warn - "Error: cannot update archive.html - could not write file: $!\n"; + warn "Error: cannot update archive.html - could not write file: $!\n"; } } else { diff --git a/bin/display b/bin/display index 6ba7408db..0d2b294e9 100755 --- a/bin/display +++ b/bin/display @@ -7,7 +7,7 @@ my ( $Pgm_Path, $Pgm_Name ); BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /(.*)[\\\/](.*)\.?/; ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; - eval "use lib '$Pgm_Path/../lib'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib'"; # Use BEGIN eval to keep perl2exe happy } use Getopt::Long; @@ -36,8 +36,7 @@ End_of_Help # Find the path to the mh/bin directory. BEGIN { ( $Pgm_Path, $Pgm_Name ) = $0 =~ /^(.*?)[\\\/]?([^\\\/]+).bat/; - use Cwd - ; # perl2exe leaves $0=mh.bat, so assume/require current directory is mh/bin dir + use Cwd; # perl2exe leaves $0=mh.bat, so assume/require current directory is mh/bin dir $Pgm_Path = cwd() unless ($Pgm_Path); } diff --git a/bin/display_callers b/bin/display_callers index 9154790fd..5c28babf8 100755 --- a/bin/display_callers +++ b/bin/display_callers @@ -17,20 +17,18 @@ # #--------------------------------------------------------------------------- -package display_callers - ; # So we can do the faster 'do' from mh, and not mess it up. +package display_callers; # So we can do the faster 'do' from mh, and not mess it up. my ( $Pgm_Path, $Pgm_Name ); -use vars '$Pgm_Root'; # So we can see it in eval var subs in read_parms +use vars '$Pgm_Root'; # So we can see it in eval var subs in read_parms BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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; $Pgm_Root = "$Pgm_Path/.."; - eval "use lib '$Pgm_Path/../lib'"; # Use BEGIN eval to keep perl2exe happy + eval "use lib '$Pgm_Path/../lib'"; # Use BEGIN eval to keep perl2exe happy } use Getopt::Long; @@ -69,10 +67,10 @@ else { &display; } -my ( $file_qual, $dir, $MW ); -my ( @members1, @members2, @members_picked1, @members_picked2, %info ); +my ( $file_qual, $dir, $MW ); +my ( @members1, @members2, @members_picked1, @members_picked2, %info ); my ( %callerid_name_by_number, %dbm_name_by_number, %calls ); -my ( @pl1, @pl2, @pl3 ); +my ( @pl1, @pl2, @pl3 ); my ($loop); my %config_parms; @@ -90,8 +88,7 @@ sub setup { # use DB_File; # neede for ocmpiled version?? if ( $parms{cgi} ) { - eval "use CGI ':all';" - ; # Use eval so we don't fail if CGI not available (e.g. mh.exe) + eval "use CGI ':all';"; # Use eval so we don't fail if CGI not available (e.g. mh.exe) print &header( -target => 'New Window' ); print &start_html("Phone Logs"); print "

    Phone Logs

    \n"; @@ -137,9 +134,8 @@ sub read_member_list { opendir( DIR, "$dir/logs" ) or die "Could not open directory $dir/logs: $!\n"; my @members = readdir(DIR); - @members1 = reverse sort grep( /callerid.*$file_qual.*log$/, @members ) - ; # ... should sort by -M instead of name ?? - @members2 = reverse sort grep( /phone.*$file_qual.*log$/, @members ); + @members1 = reverse sort grep( /callerid.*$file_qual.*log$/, @members ); # ... should sort by -M instead of name ?? + @members2 = reverse sort grep( /phone.*$file_qual.*log$/, @members ); # Default to just the latest member unless (@members_picked1) { @@ -156,8 +152,7 @@ sub read_member_list { sub read_callerid_list { print "Reading override phone list\n"; open( CALLERID, $config_parms{caller_id_file} ) - or print - "Error, could not open mh.ini caller_id_file=$config_parms{caller_id_file}: $!\n"; + or print "Error, could not open mh.ini caller_id_file=$config_parms{caller_id_file}: $!\n"; my $callerid_cnt = 0; while () { @@ -201,15 +196,12 @@ sub read_dbm_file { # print "1 number=$number data=$data\n" if $data =~ /SPENSER/i or $number =~ /8619/ or $number =~ /5307/; # next if $data =~ /[^\x9\xa\xd\x20-\x7e]/; # Ignore messed up records ... should filter these out of source file. next - if $data =~ /[^\x20-\x7e]/ - ; # Ignore messed up records ... should filter these out of source file. + if $data =~ /[^\x20-\x7e]/; # Ignore messed up records ... should filter these out of source file. next - if $number =~ /[^\x20-\x7e]/ - ; # Ignore messed up records ... should filter these out of source file. + if $number =~ /[^\x20-\x7e]/; # Ignore messed up records ... should filter these out of source file. #2 3:53 PM Sat, Dec 27 1997 name=WINTER BRUCE LA - my ( $calls, $time, $date, $name ) = - $data =~ /^(\d+) +(.+), (.+) name=(.+)/; + my ( $calls, $time, $date, $name ) = $data =~ /^(\d+) +(.+), (.+) name=(.+)/; next unless $name; $dbm_name_by_number{$number} = $name; @@ -218,8 +210,7 @@ sub read_dbm_file { $calls{dbm}{$count}{date} = sprintf( "last=%010d calls=%4d", parsedate($date), $calls ); - $calls{dbm}{$count}{'# calls'} = sprintf( "%04d", $calls ) - ; # Since we do not have a sortable date here, sort on number of calls. + $calls{dbm}{$count}{'# calls'} = sprintf( "%04d", $calls ); # Since we do not have a sortable date here, sort on number of calls. $calls{dbm}{$count}{time_date} = sprintf( "last=%s calls=%4d", $date, $calls ); $calls{dbm}{$count}{number} = $number; @@ -251,16 +242,15 @@ sub read_in_call_log { while () { # next if /[^\x9\xa\xd\x20-\x7e]/; # Ignore messed up records - tr/\x20-\x7e//cd; # Translate bad characters or else TK will mess up + tr/\x20-\x7e//cd; # Translate bad characters or else TK will mess up #Tue, Nov 2 6:46 PM 507-252-8619 SPENCER DARYL #Mon 04/14/97 14:28:00 #Sat 12/15/01 17:37:51 507-281-3888 name=TACINELLI JOHN data=###DATE12151737...NMBR5072813888...NAMETACINELLI JOHN +++ line=W - my ( $time_date, $number, $name ) = - $_ =~ /(.+?)(\d\d\d\-?\d\d\d\-?\d\d\d\d) (.+)$/; - my ($line) = $_ =~ /line=(.+?)/; # Optional ... which incoming line + my ( $time_date, $number, $name ) = $_ =~ /(.+?)(\d\d\d\-?\d\d\d\-?\d\d\d\d) (.+)$/; + my ($line) = $_ =~ /line=(.+?)/; # Optional ... which incoming line $name =~ s/^name=//; # Deal with "private, 'out of area', and bad data" calls @@ -323,7 +313,7 @@ sub read_out_call_log { while () { # next if /[^\x9\xa\xd\x20-\x7e]/; # Ignore messed up records - tr/\x20-\x7e//cd; # Translate bad characters or else TK will mess up + tr/\x20-\x7e//cd; # Translate bad characters or else TK will mess up #Tue, Nov 2 9:28 AM O2537009 #Fri 11/26/99 10:14:06 O2537009 @@ -392,25 +382,18 @@ sub display { $fbr->Label( -text => 'List of log files' )->pack; my $fbr2 = $fbr->Frame->pack(qw/-expand no -fill x/); - $fbr2->Label( -text => 'Incoming files' ) - ->pack(qw/-expand yes -fill x -side left/); - $fbr2->Label( -text => 'Outgoing files' ) - ->pack(qw/-expand yes -fill x -side left/); + $fbr2->Label( -text => 'Incoming files' )->pack(qw/-expand yes -fill x -side left/); + $fbr2->Label( -text => 'Outgoing files' )->pack(qw/-expand yes -fill x -side left/); my ( $files1, $files2 ); - $files1 = $fbr->Scrolled( - qw/Listbox -selectmode extended -width -1 -height 10 -setgrid 1 -scrollbars e -bg cyan/ - ); + $files1 = $fbr->Scrolled(qw/Listbox -selectmode extended -width -1 -height 10 -setgrid 1 -scrollbars e -bg cyan/); $files1->pack(@pl2)->insert( 0, @members1 ); - $files2 = $fbr->Scrolled( - qw/Listbox -selectmode extended -width -1 -height 10 -setgrid 1 -scrollbars e -bg cyan/ - ); + $files2 = $fbr->Scrolled(qw/Listbox -selectmode extended -width -1 -height 10 -setgrid 1 -scrollbars e -bg cyan/); $files2->pack(@pl2)->insert( 0, @members2 ); # $files1->activate(0); # $files2->activate(0); - $files1->selection( 'set', 0 ) - ; # Only one listbox widget can have a row slected at a time :( + $files1->selection( 'set', 0 ); # Only one listbox widget can have a row slected at a time :( # $files2->selection('set', 0); @@ -478,19 +461,14 @@ sub phone_list { if ( $class eq 'dbm' ) { my $search_frame = $ptr->Frame->pack; - my $search_label = - $search_frame->Label( -text => "Search:" )->pack( -side => 'left' ); + my $search_label = $search_frame->Label( -text => "Search:" )->pack( -side => 'left' ); - my $search_entry = - $search_frame->Entry( -textvariable => \$search_var, -width => 10 ) - ->pack; + my $search_entry = $search_frame->Entry( -textvariable => \$search_var, -width => 10 )->pack; $search_entry->bind( '', sub { &search_data($search_var) } ); } my $frame = $ptr->Frame->pack; - my $log = $ptr->Scrolled( - qw/Text -state normal -width 60 -height 15 -scrollbars e -bg cyan/) - ->pack(@pl2); + my $log = $ptr->Scrolled(qw/Text -state normal -width 60 -height 15 -scrollbars e -bg cyan/)->pack(@pl2); $info{$class}{window} = $log; for my $button (qw/name number date calls/) { @@ -509,16 +487,11 @@ sub phone_list { $log->bind( '' => sub { - my $text = - $log->get( 'current linestart +20 chars', 'current lineend' ); + my $text = $log->get( 'current linestart +20 chars', 'current lineend' ); $text =~ s/^.*? (\d\d\d-)/$1/; # Delete up to the number my $popup = $MW->Toplevel; - my $label = - $popup->Label( -text => - "Edit name to be more prounancable,\nthen hit enter (or Escape to exit)" - )->pack( -side => 'top' ); - my $entry = $popup->Entry( -width => 40, -textvariable => \$text ) - ->pack( -side => 'bottom' ); + my $label = $popup->Label( -text => "Edit name to be more prounancable,\nthen hit enter (or Escape to exit)" )->pack( -side => 'top' ); + my $entry = $popup->Entry( -width => 40, -textvariable => \$text )->pack( -side => 'bottom' ); $entry->tkwait( 'visibility', $entry ); $entry->focus; @@ -551,20 +524,16 @@ sub search_data { print "Searching for $search_var\n"; # Search data logged from incoming caller id data. - my ( $count1, $count2, %results ) = - &main::search_dbm( "$config_parms{data_dir}/phone/callerid.dbm", - $search_var ); + my ( $count1, $count2, %results ) = &main::search_dbm( "$config_parms{data_dir}/phone/callerid.dbm", $search_var ); # Also search in array created from mh.ini caller_id_file data while ( my ( $key, $value ) = each %callerid_name_by_number ) { if ( $key =~ /$search_var/i or $value =~ /$search_var/i ) { - $value = - &main::read_dbm( "$config_parms{data_dir}/phone/callerid.dbm", - $key ); # Use dbm data for consistency + $value = &main::read_dbm( "$config_parms{data_dir}/phone/callerid.dbm", $key ); # Use dbm data for consistency $results{$key} = $value; } } - $count2 = keys %results; # Reset count, in case Caller_ID search found any + $count2 = keys %results; # Reset count, in case Caller_ID search found any my $list = $info{dbm}{window}; @@ -573,25 +542,21 @@ sub search_data { my $results; if ($count2) { for ( sort keys %results ) { - my ( $cid_number, $cid_date, $cid_name ) = - $results{$_} =~ /(\S+) (.+) name=(.+)/; + my ( $cid_number, $cid_date, $cid_name ) = $results{$_} =~ /(\S+) (.+) name=(.+)/; $cid_name = $callerid_name_by_number{$_} if $callerid_name_by_number{$_}; $cid_date = ( split( ',', $cid_date ) )[1]; # Drop leading time field # $results .= sprintf("%15s: %-15s calls=%3s last=%s\n %s\n", $_, $cid_name, $cid_number, $cid_date, $caller); - $results .= sprintf( "last=%12s calls=%4d %s %s\n", - $cid_date, $cid_number, $_, $cid_name ); + $results .= sprintf( "last=%12s calls=%4d %s %s\n", $cid_date, $cid_number, $_, $cid_name ); } # map {$results .= " $_: $results{$_}\n\n"} sort keys %results; - $results = - "Results: $count2 out of $count1 records matched\n\n" . $results; + $results = "Results: $count2 out of $count1 records matched\n\n" . $results; } else { - $results = - "\nResults: No match found out of $count1 records searched\n"; + $results = "\nResults: No match found out of $count1 records searched\n"; } $list->delete( '0.0', 'end' ); @@ -615,11 +580,8 @@ sub sort_data { ) { $count++; - $data .= sprintf( - "%s %s %-12s %s\n", - $calls{$class}{$rec}{time_date}, $calls{$class}{$rec}{line}, - $calls{$class}{$rec}{number}, $calls{$class}{$rec}{name2} - ); + $data .= + sprintf( "%s %s %-12s %s\n", $calls{$class}{$rec}{time_date}, $calls{$class}{$rec}{line}, $calls{$class}{$rec}{number}, $calls{$class}{$rec}{name2} ); } if ( $parms{cgi} ) { return $data; @@ -689,8 +651,7 @@ sub post_cgi_form { sub phone_list2 { my ($list) = @_; - print "\n
    TimeStateSet By
    "+new Date(json.data.data[i][0]).toUTCString()+""+new Date(json.data.data[i][0]).toString().replace(/GMT-\d\d\d\d/,"")+""+String(json.data.data[i][1])+""+String(json.data.data[i][2])+"
    "+ config_name + "_config.json
    "+ i + "
    "+ j + "
    "+k+" = "+config_data[i][j][k]+"
    "+j+" = "+config_data[i][j]+"
    "+ String(json_store.ia7_config[i]) + "
    \n|; $html .= qq|\n|; @@ -1596,9 +1613,10 @@ sub html_form_input_set_func { sub html_form_input_set_var { my ( $var, $resp, $default ) = @_; $default = HTML::Entities::encode($default); - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); + my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); my $id = ""; - #$id = "id='mhresponse'" if ($mode eq 'ia7'); + + #$id = "id='mhresponse'" if ($mode eq 'ia7'); my $html .= qq|
    \n|; $html .= qq|\n|; $html .= qq|\n|; @@ -1626,10 +1644,11 @@ sub html_form_select { sub html_form_select_set_func { my ( $func, $resp, $var1, $default, @values ) = @_; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); + my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); my $id = ""; + #$id = "id='mhresponse'" if ($mode eq 'ia7'); -# my $form .= qq|\n|; + # my $form .= qq|\n|; my $form .= qq|\n|; $form .= qq|\n|; $form .= qq|\n|; @@ -1643,15 +1662,17 @@ sub html_form_select_set_func { $form .= qq|\n|; } $form .= "
    \n"; $html .= qq|\n|; $html .= qq|\n|; From 600c68f3fce34421977b8ef7a9183c5fb2cc5c36 Mon Sep 17 00:00:00 2001 From: Jeff Siddall Date: Thu, 1 Dec 2016 15:38:51 -0500 Subject: [PATCH 065/209] Updates to add MYS_MULTIMETER --- lib/read_table_A.pl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index b4cc1070c..a3e04e19a 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1544,6 +1544,13 @@ sub read_table_A { $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Humidity($address, '$long_name', $parent, $other)"; } + elsif ( $type eq "MYS_MULTIMETER" ) { + require 'MySensors.pm'; + my ( $parent, $long_name ); + ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "MySensors::Multimeter($address, '$long_name', $parent, $other)"; + } #-------------- AD2 Objects ----------------- elsif ( $type eq "AD2_INTERFACE" ) { From f59b47705e7bad7f6e31bff15f56ba29d2c749c4 Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Sat, 3 Dec 2016 21:18:58 +0100 Subject: [PATCH 066/209] PCLBUS: replace experimental smartmach operator with grep --- lib/PLCBUS.pm | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/PLCBUS.pm b/lib/PLCBUS.pm index 065513082..4966f86c7 100644 --- a/lib/PLCBUS.pm +++ b/lib/PLCBUS.pm @@ -1,7 +1,6 @@ use strict; use warnings; use Time::HiRes; -use experimental 'smartmatch'; use Group; use Process_Item; use IO::Socket::INET; @@ -536,7 +535,7 @@ sub _handle_incoming_commands { } if ( $self->{current_cmd}->{expected_response} && !$dec->{REPRQ} - && $cmd ~~ ( $self->{current_cmd}->{expected_response} ) ) + && grep { $_ eq $cmd } @{ $self->{current_cmd}->{expected_response} } ) { $self->{current_cmd}->{expected_response_seen} = 1; @@ -1830,8 +1829,8 @@ sub preset_dim { $self->command( 'presetdim', $bright_percent, $fade_rate_secs ); } -my @light_cmds = [ "on", "off", "bright", "dim" ]; -my @plc_cmds = [ +my @light_cmds = ( "on", "off", "bright", "dim" ); +my @plc_cmds = ( "status req", "blink", "status on", @@ -1839,7 +1838,7 @@ my @plc_cmds = [ "get signal strength", "get noise strength", "all scenes addrs erase", -]; +); sub set { my ( $self, $new_state, $setby, $respond ) = @_; @@ -1849,13 +1848,13 @@ sub set { $l .= "respond $respond " if $respond; $self->_logd($l); - if ( $new_state ~~ @light_cmds ) { + if ( grep { $new_state eq $_ } @light_cmds ) { if ($self->{state} && $new_state eq $self->{state} ) { $self->_logd("Already in state $new_state, sending command anyway"); } $self->command( $new_state, undef, undef, $setby, $respond ); } - elsif ( $new_state ~~ @plc_cmds ) { + elsif ( grep { $_ eq $new_state } @plc_cmds ) { $new_state =~ s/ /_/g; $self->command( $new_state, undef, undef, $setby, $respond ); } From d48dab7a284ba9cc27fa2b4004d4adea3c5bc868 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Thu, 8 Dec 2016 21:21:21 +0100 Subject: [PATCH 067/209] Don't run git when we're not in a repository --- bin/mh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/mh b/bin/mh index 0e85956c4..3dc23b6da 100755 --- a/bin/mh +++ b/bin/mh @@ -81,7 +81,8 @@ BEGIN { $Version .= " (compiler: $Info{Perl_compiled})" if $Info{Perl_compiled}; #Create a build number if this is unstable version - if ( lc $Version eq 'unstable' ) { + # but only do this if we're on a .git version, not when we started from a zip file archive + if ( lc $Version eq 'unstable' && -e '../.git' ) { my $build = lc `git describe --long`; $build =~ /(\S+)-(\d+)-g([0-9a-f]+)/; $Version = "$1 Build $2 ($3)" if ( $1 && defined($2) && $3 ); @@ -291,7 +292,7 @@ BEGIN { chomp $autover; close(VERSION); } - if ( lc $autover eq 'unstable' ) { + if ( lc $autover eq 'unstable' && -e '../.git') { my $build_date = `git show -s --format="%ci"`; $Version_date = $build_date if ( $build_date =~ /^\d\d\d\d-\d\d-\d\d/ ); From 0a29f3ca0ef5b4dc8c2c55f8a8ef0429c7b49421 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 9 Dec 2016 10:31:44 -0700 Subject: [PATCH 068/209] Replaced smartmatch operators to remove experimental dependancy for 4.2 --- lib/Owfs_Item.pm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Owfs_Item.pm b/lib/Owfs_Item.pm index 647f998d3..1e5e06563 100644 --- a/lib/Owfs_Item.pm +++ b/lib/Owfs_Item.pm @@ -1127,12 +1127,12 @@ sub convert_value { my $location = $self->{location}; my $channel = $self->{channel}; my $value = $state; - $value = 1 if ( $state ~~ $ON ); - $value = 0 if ( $state ~~ $OFF ); - $value = 1 if ( $state ~~ main::ON ); - $value = 0 if ( $state ~~ main::OFF ); - $value = 1 if ( $state ~~ 'yes' ); - $value = 0 if ( $state ~~ 'no' ); + $value = 1 if ( lc $state eq lc $ON ); #( $state ~~ $ON ); + $value = 0 if ( lc $state eq lc $OFF ); #( $state ~~ $OFF ); + $value = 1 if ( lc $state eq main::ON ); #( $state ~~ main::ON ); + $value = 0 if ( lc $state eq main::OFF ); #( $state ~~ main::OFF ); + $value = 1 if (lc $state eq 'yes' ); #( $state ~~ 'yes' ); + $value = 0 if (lc $state eq 'no' ); #( $state ~~ 'no' ); if ( ( $value ne 1 ) && ( $value ne 0 ) ) { my $debug = $self->{debug} || $main::Debug{owfs}; &main::print_log( From bb9325f7bbcec0b56f55379db86392f395e3b315 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 9 Dec 2016 12:51:02 -0700 Subject: [PATCH 069/209] Added in missing package definitions --- lib/Owfs_Item.pm | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/Owfs_Item.pm b/lib/Owfs_Item.pm index 1e5e06563..f73f2f15e 100644 --- a/lib/Owfs_Item.pm +++ b/lib/Owfs_Item.pm @@ -505,11 +505,11 @@ sub _dump { &main::print_log("id: \t\t$$self{id}") if $main::Debug{owfs}; &main::print_log("type: \t\t$$self{type}") if $main::Debug{owfs}; for my $key ( sort keys %$self ) { - next if ( $key eq "root" ); - next if ( $key eq "path" ); - next if ( $key eq "family" ); - next if ( $key eq "id" ); - next if ( $key eq "type" ); + next if ( $key eq "root" ); + next if ( $key eq "path" ); + next if ( $key eq "family" ); + next if ( $key eq "id" ); + next if ( $key eq "type" ); &main::print_log("$key:\t\t$$self{$key}") if $main::Debug{owfs}; } &main::print_log("\n") if $main::Debug{owfs}; @@ -1127,12 +1127,12 @@ sub convert_value { my $location = $self->{location}; my $channel = $self->{channel}; my $value = $state; - $value = 1 if ( lc $state eq lc $ON ); #( $state ~~ $ON ); - $value = 0 if ( lc $state eq lc $OFF ); #( $state ~~ $OFF ); - $value = 1 if ( lc $state eq main::ON ); #( $state ~~ main::ON ); - $value = 0 if ( lc $state eq main::OFF ); #( $state ~~ main::OFF ); - $value = 1 if (lc $state eq 'yes' ); #( $state ~~ 'yes' ); - $value = 0 if (lc $state eq 'no' ); #( $state ~~ 'no' ); + $value = 1 if ( lc $state eq lc $ON ); #( $state ~~ $ON ); + $value = 0 if ( lc $state eq lc $OFF ); #( $state ~~ $OFF ); + $value = 1 if ( lc $state eq main::ON ); #( $state ~~ main::ON ); + $value = 0 if ( lc $state eq main::OFF ); #( $state ~~ main::OFF ); + $value = 1 if ( lc $state eq 'yes' ); #( $state ~~ 'yes' ); + $value = 0 if ( lc $state eq 'no' ); #( $state ~~ 'no' ); if ( ( $value ne 1 ) && ( $value ne 0 ) ) { my $debug = $self->{debug} || $main::Debug{owfs}; &main::print_log( @@ -1533,6 +1533,15 @@ sub new { return $self; } +package Owfs_DS2405_pio; +use strict; + +our $ON = 'on'; +our $OFF = 'off'; +our $PIO = 0; +our $SENSE = 1; +our $LATCH = 2; + @Owfs_DS2405_pio::ISA = ('Owfs_Switch'); sub new { @@ -1542,6 +1551,15 @@ sub new { return $self; } +package Owfs_DS2405_sense; +use strict; + +our $ON = 'on'; +our $OFF = 'off'; +our $PIO = 0; +our $SENSE = 1; +our $LATCH = 2; + @Owfs_DS2405_sense::ISA = ('Owfs_Switch'); sub new { From 47a1f1048e6317fae378f29fe91251d2e3980a23 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 9 Dec 2016 15:08:40 -0700 Subject: [PATCH 070/209] removed the 'use experimental' line --- lib/Owfs_Item.pm | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Owfs_Item.pm b/lib/Owfs_Item.pm index f73f2f15e..4a155a63d 100644 --- a/lib/Owfs_Item.pm +++ b/lib/Owfs_Item.pm @@ -88,7 +88,6 @@ use Socket_Item; package Owfs_Item; use strict; -use experimental 'smartmatch'; @Owfs_Item::ISA = ('Generic_Item'); From 9912b03b47768def908cd2b9ef040e6b4825aa95 Mon Sep 17 00:00:00 2001 From: hplato Date: Fri, 9 Dec 2016 15:21:07 -0700 Subject: [PATCH 071/209] Modified the long_poll trigger time to provide a second cushion, as per Jeff Huettner --- lib/json_server.pl | 359 +++++++++++++++++++++++++-------------------- 1 file changed, 196 insertions(+), 163 deletions(-) diff --git a/lib/json_server.pl b/lib/json_server.pl index b4cf561a1..89bdd14a9 100644 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -174,8 +174,10 @@ sub json_get { eval { my $json_collections = file_read($collection_file); $json_collections =~ s/\$config_parms\{(.+?)\}/$config_parms{$1}/gs; - $json_collections =~ s/\$Authorized/$Authorized/gs; # needed for including current "Authorize" status - $json_data{'collections'} = decode_json($json_collections); #HP, wrap this in eval to prevent MH crashes + $json_collections =~ s/\$Authorized/$Authorized/gs + ; # needed for including current "Authorize" status + $json_data{'collections'} = decode_json($json_collections) + ; #HP, wrap this in eval to prevent MH crashes }; if ($@) { print_log @@ -206,7 +208,7 @@ sub json_get { $json_data{'ia7_config'} = decode_json('{ "prefs" : { "status" : "error" } }') ; #write a blank collection - config_checker($prefs_file); + config_checker($prefs_file); } # Look at the client ip overrides, and replace any pref key with the client_ip specific item @@ -217,8 +219,11 @@ sub json_get { print_log "Json_Server.pl: Client override section for $Http{Client_address} found"; for my $key ( - keys %{$json_data{'ia7_config'}->{clients} - ->{ $Http{Client_address} }} ) + keys %{ + $json_data{'ia7_config'}->{clients} + ->{ $Http{Client_address} } + } + ) { print_log "Json_Server.pl: Client key=$key, value = $json_data{'ia7_config'}->{clients}->{$Http{Client_address}}->{$key}"; @@ -415,85 +420,96 @@ sub json_get { } # List object history - if ( $path[0] eq 'history') { + if ( $path[0] eq 'history' ) { if ( $args{items} && $args{items}[0] ne "" ) { - my %statemaps = ('on' => 100, - 'open' => 100, - 'opened' => 100, - 'motion' => 100, - 'enable' => 100, - 'enabled' => 100, - 'online' => 100, - 'off' => -100, - 'close' => -100, - 'closed' => -100, - 'still' => -100, - 'disable' => -100, - 'disabled' => -100, - 'offline' => -100, - 'dim' => 50, - ); - my $unknown_value = 40; - my @dataset = (); - my %states; + my %statemaps = ( + 'on' => 100, + 'open' => 100, + 'opened' => 100, + 'motion' => 100, + 'enable' => 100, + 'enabled' => 100, + 'online' => 100, + 'off' => -100, + 'close' => -100, + 'closed' => -100, + 'still' => -100, + 'disable' => -100, + 'disabled' => -100, + 'offline' => -100, + 'dim' => 50, + ); + my $unknown_value = 40; + my @dataset = (); + my %states; my %data; - my $index = 0; - my $start = time; - $start = $args{start}[0] if (defined $args{start}[0]); - my $graph = 0; - $graph = $args{graph}[0] if (defined $args{graph}[0]); - my $days = 0; - $days = $args{days}[0] if (defined $args{days}[0]); + my $index = 0; + my $start = time; + $start = $args{start}[0] if ( defined $args{start}[0] ); + my $graph = 0; + $graph = $args{graph}[0] if ( defined $args{graph}[0] ); + my $days = 0; + $days = $args{days}[0] if ( defined $args{days}[0] ); + foreach my $name ( @{ $args{items} } ) { - my $o = &get_object_by_name($name); - next unless (defined $o); - next unless $o->get_logger_status(); - my $label = $o->set_label(); - $label = $name unless (defined $label); - my $logger_data = $o->get_logger_data($start,$days); - #alert if any anomalous states are detected - my @lines = split /\n/, $logger_data; - foreach my $line (@lines) { - my $value; - my ($time1,$time2,$obj,$state,$setby,$target) = split ",",$line; - if ($graph) { - if (defined $statemaps{$state}) { - $value = $statemaps{$state}; - } else { - if ($state =~ /(\d+)\%/) { - $value = $_; - } else { - &main::print_log("json_server.pl: WARNING. object history state $state not found in mapping"); - $value = $unknown_value++; - } - } - $states{$value} = $state; - push @{$dataset[$index]->{data}}, [ int($time2), int($value) ]; - } else { - push @dataset, [int($time2), $state, $setby]; - } - } - push @{$dataset[$index]->{label}}, $label if ($graph); - $index++; - } - if ($graph) { - #flot expects to see an array - my @yaxticks = (); - for my $j (sort keys %states) { - push @yaxticks, [ $j, $states{$j} ]; - } - $data{'options'}->{'yaxis'}->{'ticks'} = \@yaxticks; #\%states; - $data{'options'}->{'legend'}->{'show'} = "true"; - $data{'options'}->{'xaxis'}->{'mode'} = "time"; - $data{'options'}->{'points'}->{'show'} = "true"; - $data{'options'}->{'xaxis'}->{'timezone'} = "browser"; - $data{'options'}->{'grid'}->{'hoverable'} = "true"; - } - $data{'data'} = \@dataset; - $json_data{'history'} = \%data; - } - } + my $o = &get_object_by_name($name); + next unless ( defined $o ); + next unless $o->get_logger_status(); + my $label = $o->set_label(); + $label = $name unless ( defined $label ); + my $logger_data = $o->get_logger_data( $start, $days ); + + #alert if any anomalous states are detected + my @lines = split /\n/, $logger_data; + foreach my $line (@lines) { + my $value; + my ( $time1, $time2, $obj, $state, $setby, $target ) = + split ",", $line; + if ($graph) { + if ( defined $statemaps{$state} ) { + $value = $statemaps{$state}; + } + else { + if ( $state =~ /(\d+)\%/ ) { + $value = $_; + } + else { + &main::print_log( + "json_server.pl: WARNING. object history state $state not found in mapping" + ); + $value = $unknown_value++; + } + } + $states{$value} = $state; + push @{ $dataset[$index]->{data} }, + [ int($time2), int($value) ]; + } + else { + push @dataset, [ int($time2), $state, $setby ]; + } + } + push @{ $dataset[$index]->{label} }, $label if ($graph); + $index++; + } + if ($graph) { + + #flot expects to see an array + my @yaxticks = (); + for my $j ( sort keys %states ) { + push @yaxticks, [ $j, $states{$j} ]; + } + $data{'options'}->{'yaxis'}->{'ticks'} = \@yaxticks; #\%states; + $data{'options'}->{'legend'}->{'show'} = "true"; + $data{'options'}->{'xaxis'}->{'mode'} = "time"; + $data{'options'}->{'points'}->{'show'} = "true"; + $data{'options'}->{'xaxis'}->{'timezone'} = "browser"; + $data{'options'}->{'grid'}->{'hoverable'} = "true"; + } + $data{'data'} = \@dataset; + $json_data{'history'} = \%data; + } + } # List objects if ( $path[0] eq 'objects' || $path[0] eq '' ) { @@ -533,7 +549,8 @@ sub json_get { push @objects, &list_objects_by_type($object_type); } } -# foreach my $o ( map { &get_object_by_name($_) } sort @objects ) { + + # foreach my $o ( map { &get_object_by_name($_) } sort @objects ) { foreach my $o ( map { &get_object_by_name($_) } @objects ) { next unless $o; my $name = $o; @@ -710,12 +727,12 @@ sub json_get { } } - if ( $path[0] eq 'fp_icon_sets' ){ - my $p = "../web/ia7/graphics/*default_".$args{px}[0].".png"; + if ( $path[0] eq 'fp_icon_sets' ) { + my $p = "../web/ia7/graphics/*default_" . $args{px}[0] . ".png"; my @icons = glob($p); s/^..\/web// for @icons; $json_data{'icon_sets'} = []; - push( @{ $json_data{'fp_icon_sets'} }, @icons); + push( @{ $json_data{'fp_icon_sets'} }, @icons ); } # List speak phrases @@ -954,9 +971,11 @@ sub json_object_detail { #Items that have NEVER been set to a state have a null idle time return; } - elsif ( $request_time >= ( $current_time - $object->get_idle_time ) ) { + elsif ( + ( $request_time - 1 ) > ( $current_time - $object->get_idle_time ) ) + { - #Should get_tickcount be replaced with output_time?? + #To avoid missed changes, since they can happen at the millisecond level, give a second's cushion #Object has not changed since time, so return undefined return; } @@ -1368,87 +1387,101 @@ sub json_notification { push @json_notifications, $data; } -sub config_checker { - my ($file) = @_; - - my (%collections, $key, $output, $temp); - my @data = file_read($file); - - - foreach my $row (@data) { - $key = $1 if $row =~ /\"(\d+?)\" \:/; - $row =~ /\"(.+?)\" \: \"(.+?)\"/ ; - $collections{$key}{$1} = $2 if $1; - } - - foreach my $row (@data) { - $key = $1 if $row =~ /\"(\d+?)\" \:/; - if ($row =~ /(\d+?)(\,|\n)/) { - my $sub_key = $1; - my $comma = $2; - $collections{$key}{children} .= "\t$1: " ; - $collections{$key}{children} .= "$collections{$sub_key}{name}"; - $collections{$key}{children} .= "\n"; - } - } - - - #foreach $key (sort check_numerically keys %collections) { - # $output .= "$key: $collections{$key}{name}:$collections{$key}{link}$collections{$key}{external}$collections{$key}{iframe}:$collections{$key}{comment}:$collections{$key}{mode}:$collections{$key}{children}"; - #} - - my ($row, $show_row, $bracket_errors, $comma_errors1, $comma_errors2, %brackets, $curly, $square); - $curly = 'closed'; - $square = 'closed'; - - while ($row < @data) { - $show_row = $row + 1; - $data[$row] =~ s/\s+$//; # remove trailing spaces - if ($data[$row] =~ /\{/ and $data[$row] !~ /iframe|external/) { - $brackets{open_curly} ++; - $bracket_errors .= "Repeated open curly bracket in line $show_row: '$data[$row]'\n" if $curly and $curly eq 'open'; - $curly = 'open'; - } - if ($data[$row] =~ /\}/ and $data[$row] !~ /iframe|external/) { - $brackets{close_curly} ++; - $bracket_errors .= "Repeated close curly bracket in line $show_row: '$data[$row]'\n" if $curly eq 'closed'; - $curly = 'closed'; - } - if ($data[$row] =~ /\[/) { - $brackets{open_square} ++; - $bracket_errors .= "Repeated open square bracket in line $show_row: '$data[$row]'\n" if $square eq 'open'; - $square = 'open'; - } - if ($data[$row] =~ /\]/) { - $brackets{close_square} ++; - $bracket_errors .= "Repeated close square bracket in line $show_row: '$data[$row]'\n" if $square eq 'closed'; - $square = 'closed'; - } - - $comma_errors1 .= $row + 1 . ": '$data[$row]'\n" if $data[$row] !~ /\, *$/ - and $data[$row + 1] !~ /(\}|\])/ - and $data[$row] !~ /\: (\{|\[)/ - and $data[$row] !~ /^\{/ - and $data[$row] !~ /\}$/; - - $comma_errors2 .= $row + 1 . ": '$data[$row]'\n" if $data[$row] =~ /\,/ and $data[$row + 1] =~ /(\}|\])\,/; - - $row ++; - } - - $output .= "Possible bracket errors:\n$bracket_errors\n" if $bracket_errors; - $output .= "The following lines should possibly have a comma at the end:\n$comma_errors1\n" if $comma_errors1; - $output .= "The following lines should possibly not have a comma at the end:\n$comma_errors2\n" if $comma_errors2; - - $output .= "There are $brackets{open_square} '[' and $brackets{close_square} ']'.\n"; - $output .= "There are $brackets{open_curly} '{' and $brackets{close_curly} '}'.\n"; - - print_log $output; +sub config_checker { + my ($file) = @_; + + my ( %collections, $key, $output, $temp ); + my @data = file_read($file); + + foreach my $row (@data) { + $key = $1 if $row =~ /\"(\d+?)\" \:/; + $row =~ /\"(.+?)\" \: \"(.+?)\"/; + $collections{$key}{$1} = $2 if $1; + } + + foreach my $row (@data) { + $key = $1 if $row =~ /\"(\d+?)\" \:/; + if ( $row =~ /(\d+?)(\,|\n)/ ) { + my $sub_key = $1; + my $comma = $2; + $collections{$key}{children} .= "\t$1: "; + $collections{$key}{children} .= "$collections{$sub_key}{name}"; + $collections{$key}{children} .= "\n"; + } + } + + #foreach $key (sort check_numerically keys %collections) { + # $output .= "$key: $collections{$key}{name}:$collections{$key}{link}$collections{$key}{external}$collections{$key}{iframe}:$collections{$key}{comment}:$collections{$key}{mode}:$collections{$key}{children}"; + #} + + my ( $row, $show_row, $bracket_errors, $comma_errors1, $comma_errors2, + %brackets, $curly, $square ); + $curly = 'closed'; + $square = 'closed'; + + while ( $row < @data ) { + $show_row = $row + 1; + $data[$row] =~ s/\s+$//; # remove trailing spaces + if ( $data[$row] =~ /\{/ and $data[$row] !~ /iframe|external/ ) { + $brackets{open_curly}++; + $bracket_errors .= + "Repeated open curly bracket in line $show_row: '$data[$row]'\n" + if $curly and $curly eq 'open'; + $curly = 'open'; + } + if ( $data[$row] =~ /\}/ and $data[$row] !~ /iframe|external/ ) { + $brackets{close_curly}++; + $bracket_errors .= + "Repeated close curly bracket in line $show_row: '$data[$row]'\n" + if $curly eq 'closed'; + $curly = 'closed'; + } + if ( $data[$row] =~ /\[/ ) { + $brackets{open_square}++; + $bracket_errors .= + "Repeated open square bracket in line $show_row: '$data[$row]'\n" + if $square eq 'open'; + $square = 'open'; + } + if ( $data[$row] =~ /\]/ ) { + $brackets{close_square}++; + $bracket_errors .= + "Repeated close square bracket in line $show_row: '$data[$row]'\n" + if $square eq 'closed'; + $square = 'closed'; + } + + $comma_errors1 .= $row + 1 . ": '$data[$row]'\n" + if $data[$row] !~ /\, *$/ + and $data[ $row + 1 ] !~ /(\}|\])/ + and $data[$row] !~ /\: (\{|\[)/ + and $data[$row] !~ /^\{/ + and $data[$row] !~ /\}$/; + + $comma_errors2 .= $row + 1 . ": '$data[$row]'\n" + if $data[$row] =~ /\,/ and $data[ $row + 1 ] =~ /(\}|\])\,/; + + $row++; + } + + $output .= "Possible bracket errors:\n$bracket_errors\n" if $bracket_errors; + $output .= + "The following lines should possibly have a comma at the end:\n$comma_errors1\n" + if $comma_errors1; + $output .= + "The following lines should possibly not have a comma at the end:\n$comma_errors2\n" + if $comma_errors2; + + $output .= + "There are $brackets{open_square} '[' and $brackets{close_square} ']'.\n"; + $output .= + "There are $brackets{open_curly} '{' and $brackets{close_curly} '}'.\n"; + + print_log $output; } sub check_numerically { $a <=> $b } - return 1; # Make require happy =back From 5267e3df4b58e7e4206dc4812945b8082d2eaa9c Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 9 Dec 2016 16:50:01 -0700 Subject: [PATCH 072/209] Added a notification in the startup setup section if object logging is enabled or disabled --- bin/mh | 74 ++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/bin/mh b/bin/mh index 0e85956c4..2abbacceb 100755 --- a/bin/mh +++ b/bin/mh @@ -45,7 +45,7 @@ BEGIN { if $PAR::VERSION; # If built with PAR # $0=path to mh/bin, $^X=path to perl, unless perl2exe compiled mh.exe - ( $Pgm_Path, $Pgm_Name ) = $0 =~ /^(.*)[\\\/]([^.]+)/; + ( $Pgm_Path, $Pgm_Name ) = $0 =~ /^(.*)[\\\/]([^.]+)/; ( $Pgm_Path, $Pgm_Name ) = $^X =~ /^(.*)[\\\/]([^.]+)/ if $Info{Perl_compiled}; @@ -479,7 +479,7 @@ EOF # Allow for multiple debugs like serial;x10 for my $debug ( split /[,|;]/, lc $config_parms{debug} ) { $debug =~ s/^\s+|\s+$//g; #Trim whitespace - # Allow for debug level like: x10:4 + # Allow for debug level like: x10:4 next unless $debug; if ( $debug =~ /\s*(\S+)\s*:\s*(\d+)\s*/ ) { $Debug{$1} = $2; @@ -758,14 +758,14 @@ sub setup { # otherwise we use a simple but fast exact word match distance function. { local $SIG{__WARN__} = - sub { }; # Disable 5.8 "Can't locate auto/..." message + sub { }; # Disable 5.8 "Can't locate auto/..." message eval 'use Text::LevenshteinXS qw(distance)'; eval 'print " - using simple Text distance function\n"; sub distance { return $_[0] ne $_[1] }' if $@; } - use Time::Local; # For timelocal - use Time::DaysInMonth; # For days_in (and is_leap?) + use Time::Local; # For timelocal + use Time::DaysInMonth; # For days_in (and is_leap?) # Date::Manip needs it in ISO 8601 form: +-HHMM # Hmmm, this messes up str2time from Date::Parse @@ -849,7 +849,7 @@ sub setup { # require 'console_utils.pl'; require 'http_server.pl'; - require 'ia7_utilities.pl'; + require 'ia7_utilities.pl'; require 'xml_server.pl'; require 'menu_code.pl'; require 'trigger_code.pl'; @@ -915,7 +915,8 @@ sub setup { mkdir( "$config_parms{html_dir}/tv/clicktv", 0777 ) unless -d "$config_parms{html_dir}/tv/clicktv"; - $Time_Date = &time_date_stamp( $config_parms{time_format_log}, $Time ) + $Time_Date = + &time_date_stamp( $config_parms{time_format_log}, $Time ) ; # Needed by print_log &open_logs; @@ -961,7 +962,7 @@ sub setup { # eval qq[\$break = "$break"]; my $pretty_port_name = $port_name; - $pretty_port_name =~ s/_/\x20/g; # a slight improvement + $pretty_port_name =~ s/_/\x20/g; # a slight improvement printf " - creating %-15s on %3s %s %5s %s\n", $pretty_port_name, $proto, $address, $port, $datatype; @@ -1221,6 +1222,13 @@ sub setup { &serial_port_create( 'cm17', $config_parms{cm17_port} ); } + if ( $config_parms{object_logger_enable} ) { + print " - object logging enabled\n"; + } + else { + print " - object logging disabled\n"; + } + # Store boot time in seconds since epoc if ($OS_win) { $Time_Boot_time = @@ -1268,7 +1276,8 @@ sub setup { # NOTE: on Windows, default is best left empty for tk_font (inherits font from OS display scheme) $config_parms{tk_font} = 'Times 10' - unless $config_parms{tk_font} or $OS_win; + unless $config_parms{tk_font} + or $OS_win; $config_parms{tk_font_fixed} = 'Courier 10' unless $config_parms{tk_font_fixed}; @@ -2361,7 +2370,8 @@ sub check_for_generic_serial_data { $Serial_Ports{$port_name}{data} =~ /(.+?)$break(.*)/s ) { print "Data from $port_name: $record. remainder=$remainder.\n" - if $Debug{serial} or $Debug{$port_name}; + if $Debug{serial} + or $Debug{$port_name}; $Serial_Ports{$port_name}{data_record} = $record; $Serial_Ports{$port_name}{data} = $remainder; if ( $Serial_Ports{$port_name}{process_data} ) { @@ -2585,7 +2595,7 @@ sub check_for_proxy_data { # Drop only dynamic proxies, like those in common/proxy_client_server.pl. # Leave static ones, so we can keep testing it so we can reconnect when proxy comes back &drop_proxy($address) if $config_parms{mh_proxyreg_port}; - $address =~ s/\:\d+$//; # Shorten up name for speaking + $address =~ s/\:\d+$//; # Shorten up name for speaking $address =~ s/.+\.(\d+)$/$1/; &speak("proxy $address is dead") if &new_minute(2); next; @@ -2782,7 +2792,7 @@ sub check_for_socket_data { # - could probably use a smarter select check here, rather than loop for each port for my $port_name ( keys %Socket_Ports ) { next if $port_name eq 'http'; # Deal with this elsewhere - # Need to use _flag var so active/inactive_this_pass is valid for 1 full pass. + # Need to use _flag var so active/inactive_this_pass is valid for 1 full pass. $Socket_Ports{$port_name}{active_this_pass} = 0; $Socket_Ports{$port_name}{active_this_pass} = 1 if $Socket_Ports{$port_name}{active_this_pass_flag}; @@ -2847,8 +2857,8 @@ sub check_for_socket_data { push @{ $Socket_Ports{$port_name}{clients} }, [ $new_sock, $client_ip_address, $client_port, undef ]; - delete $Socket_Ports{$port_name}{data} - ; # Delete data from previous session + delete $Socket_Ports{$port_name} + {data}; # Delete data from previous session $Socket_Ports{$port_name}{client_number} = @{ $Socket_Ports{$port_name}{clients} } - 1; @@ -3080,8 +3090,8 @@ sub check_for_tied_events { &print_log($log_msg) unless $log_msg eq '1'; my $state = $state1; # So eval can substitute $state my $object = $object1; - $Set_By = $object1->{set_by} - ; # Checked in Generic_Item set method (not usually at this time) + $Set_By = $object1 + ->{set_by}; # Checked in Generic_Item set method (not usually at this time) print "Event link: state=$state set_by=$Set_By object=$object->{object_name} eval event=$event\n" if $Debug{events}; @@ -3528,7 +3538,8 @@ sub eval_user_code_load { . &eval_user_code_error( $@, $temp_code ); print $error; &display( $error, 60 ) - unless $Startup or !$config_parms{tk}; + unless $Startup + or !$config_parms{tk}; undef $old_error; last; } @@ -4481,7 +4492,8 @@ sub phrase_match { # for my $phrase2 (('when will the sun set', 'new moon')) { # Do a fast less accurate search on all phrases for my $phrase2 ( &Voice_Cmd::voice_items( 'mh', 'no_category' ) ) { - my $d = pdistance( $phrase, $phrase2, $set1, \&distance, + my $d = + pdistance( $phrase, $phrase2, $set1, \&distance, { -cost => [ 1, 0, 3 ], -mode => 'set' } ); print " - d1=$d phrase=$phrase2.\n" if $Debug{phrase}; push @{ $list1{$d} }, $phrase2 if $d <= $d_min1; @@ -4495,7 +4507,8 @@ sub phrase_match { my $d_min2 = 999; my $set2 = 'abcdefghijklmnopqrstuvwxyz0123456789+-%'; for my $phrase2 ( @{ $list1{$d_min1} } ) { - my $d = pdistance( $phrase, $phrase2, $set2, \&distance, + my $d = + pdistance( $phrase, $phrase2, $set2, \&distance, { -cost => [ 1, 0, 3 ], -mode => 'set' } ); print " - d2=$d phrase=$phrase2.\n" if $Debug{phrase}; push @{ $list2{$d} }, $phrase2 if $d <= $d_min2; @@ -4774,7 +4787,7 @@ sub play { } - &Play_post_hooks(%parms); # Created by &add_hooks + &Play_post_hooks(%parms); # Created by &add_hooks } @@ -5945,7 +5958,8 @@ sub setup_DBI { unless ( &my_use('DBI') ) { # So we don't fail if DBI is not installed return - if $DBI = DBI->connect( $db, $config_parms{dbi_user}, + if $DBI = + DBI->connect( $db, $config_parms{dbi_user}, $config_parms{dbi_password} ); } @@ -6149,7 +6163,8 @@ sub read_user_code { # Check for the end of a statment ... allow for end of line comments $noloop_statement_flag = 0 - if $record =~ /\;\s*$/ or $record =~ /\;\s*#/; + if $record =~ /\;\s*$/ + or $record =~ /\;\s*#/; $noloop_flag = 0 if $record =~ /#\s*noloop=stop/i; @@ -6281,7 +6296,7 @@ sub read_user_code_loopcode { # Much quicker than a read_code call. # Like the 'do' function, except we add sub member_name {} around the code sub do_user_file { - my ($file) = @_; + my ($file) = @_; my ($member_name) = $file =~ /([^\\\/]+)\.(pl|mhp)$/i; $member_name .= '_table' if $file =~ /mhp$/; my ( $sub_name, $code ) = @@ -6708,7 +6723,8 @@ sub set_global_vars { } $New_Second = $New_Minute = $New_Hour = $New_Day = $New_Week = $New_Month = $New_Year = 0 - if $Startup or $Reload; + if $Startup + or $Reload; # More $New_Second stuff ... $Info{cpu_used} = 0; @@ -7304,7 +7320,7 @@ sub respond { if $target !~ /\S/ or $target =~ /unknown/i or $target =~ /UserCode/i - or $target =~ /time/i; # includes tie_time + or $target =~ /time/i; # includes tie_time print "respond target=$target lr=$Last_Response RT=$Respond_Target lso=$leave_socket_open_passes lsa=leave_socket_open_action a=@_\n" @@ -7737,7 +7753,8 @@ sub route_display_rooms { my $func = "display_$targets{$target_room}{device}"; foreach my $key ( keys %{ $targets{$target_room} } ) { $parms{$key} = $targets{$target_room}{$key} - unless $key eq 'device' or $key eq 'text'; + unless $key eq 'device' + or $key eq 'text'; } if ( $main::{$func} ) { no strict 'refs'; @@ -7877,7 +7894,7 @@ sub time_now { # - if we add a 'catchup mode', we can go back to checking on the exact second my $time_now = &my_str2time($time_date); unless ($time_now) { - my @caller = caller; # This is not useful in user_code eval :( + my @caller = caller; # This is not useful in user_code eval :( print "Bad time_now format: $time_date caller=@caller\n"; } @@ -8189,7 +8206,8 @@ sub x10_dim_level_decode { my ($code) = @_; # Convert bit string to decimal - my $level_b = $table_hcodes{ substr( $code, 0, 1 ) } + my $level_b = + $table_hcodes{ substr( $code, 0, 1 ) } . $table_dcodes{ substr( $code, 1, 1 ) }; my $level_d = unpack( 'C', pack( 'B8', $level_b ) ); From efea5a0534c3ec52cc23434b9fc136610c70ed6e Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 11 Dec 2016 10:54:36 -0700 Subject: [PATCH 073/209] Fixed staticpage with direct_control --- web/ia7/house/main.shtml | 4 ++-- web/ia7/include/javascript.js | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index d66a35d99..da784c621 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,10 +82,10 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.3.600 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.3.601 Font Awesome by Dave Gandy - http://fontawesome.io

    \ No newline at end of file + diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 8e5d4fabb..1340b8b1a 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,4 @@ -// v1.3.600 +// v1.3.601 var entity_store = {}; //global storage of entities var json_store = {}; @@ -972,7 +972,9 @@ var updateStaticPage = function(link,time) { $('button[entity="'+entity+'"]').removeClass("btn-danger"); $('button[entity="'+entity+'"]').removeClass("btn-info"); $('button[entity="'+entity+'"]').addClass("btn-"+color); - if (json_store.ia7_config.objects[entity].direct_control !== undefined && json_store.ia7_config.objects[entity].direct_control == "yes") $('button[entity="'+entity+'"]').addClass("btn-direct"); + if (json_store.ia7_config.objects[entity] !== undefined && + json_store.ia7_config.objects[entity].direct_control !== undefined && + json_store.ia7_config.objects[entity].direct_control == "yes") $('button[entity="'+entity+'"]').addClass("btn-direct"); //don't run this if stategrp0 exists if (states_loaded == 0) { @@ -3016,4 +3018,4 @@ $(document).ready(function() { // // 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. -// \ No newline at end of file +// From df1185dd135549fe4f3aa8f14f4966a2afa028b0 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 17 Dec 2016 09:45:35 +0100 Subject: [PATCH 074/209] Fix incorrect newsflash animation (by david-mark) --- lib/Display_Alpha.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Display_Alpha.pm b/lib/Display_Alpha.pm index 753555916..601d5a09f 100644 --- a/lib/Display_Alpha.pm +++ b/lib/Display_Alpha.pm @@ -172,7 +172,7 @@ my %special_modes = ( starburst => "\x37", welcome => "\x38", slotmachine => "\x39", - newsflash => "\x3A", + newsflash => "\x41", trumpet => "\x3B", cyclecolors => "\x43", thankyou => "\x53", From dafb9281cc49b795a676c89d6e3cc631732068a9 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 17 Dec 2016 13:57:39 -0700 Subject: [PATCH 075/209] Homebridge v4 update, a few fixes --- code/common/homebridge.pl | 640 +++++++++++++++++++++++--------------- 1 file changed, 385 insertions(+), 255 deletions(-) diff --git a/code/common/homebridge.pl b/code/common/homebridge.pl index c24c7e8f0..d2ea69579 100644 --- a/code/common/homebridge.pl +++ b/code/common/homebridge.pl @@ -3,11 +3,10 @@ #@ This module generates a config.json to be used by the homebridge system #@ To use several groups need to be set up: #@ HB__ where type is LIGHT, LOCK, FAN, GARAGEDOOR, BLINDS, SWITCH, THERMOSTAT -#@ Thermostat control only tested with a few models. - +#@ Thermostat control only tested with a few models. my $hb_debug = 1; -my $port = $config_parms{homebridge_port}; +my $port = $config_parms{homebridge_port}; $port = 51826 unless ($port); my $name = $config_parms{homebridge_name}; $name = "Homebridge" unless ($name); @@ -15,136 +14,231 @@ $pin = "031-45-154" unless ($pin); my $username = $config_parms{homebridge_username}; $username = "CC:22:3D:E3:CE:30" unless ($username); -my $version = "3"; +my $version = "4"; my $filepath = $config_parms{data_dir} . "/homebridge_config.json"; -$filepath = $config_parms{homebridge_config_dir} . "/config.json" if (defined $config_parms{homebridge_config_dir}); +$filepath = $config_parms{homebridge_config_dir} . "/config.json" + if ( defined $config_parms{homebridge_config_dir} ); my $acc_count; -$v_generate_hb_config = new Voice_Cmd("Generate new Homebridge config.json file"); +$v_generate_hb_config = + new Voice_Cmd("Generate new Homebridge config.json file"); $v_restart_hb_server = new Voice_Cmd("[start,stop,restart] Homebridge Server"); my $units = "C"; -$units = $config_parms{homebridge_temp_units} if (defined $config_parms{homebridge_temp_units}); +$units = $config_parms{homebridge_temp_units} + if ( defined $config_parms{homebridge_temp_units} ); -if (my $action = said $v_restart_hb_server) { - if (defined $config_parms{homebridge_service_path}) { - print_log "[Homebridge]: " . $action . "ing the Homebridge Server..."; - my $cmd = $config_parms{homebridge_service_path} . " " . $action; - my $r = system ($cmd); - if ($r != 0) { - print_log "[Homebridge]: Warning, couldn't control homebridge service: $r" - } - } else { - print_log "[Homebridge]: Error, homebridge service path not defined" - } +if ( my $action = said $v_restart_hb_server) { + if ( defined $config_parms{homebridge_service_path} ) { + print_log "[Homebridge]: " . $action . "ing the Homebridge Server..."; + my $cmd = $config_parms{homebridge_service_path} . " " . $action; + my $r = system($cmd); + if ( $r != 0 ) { + print_log + "[Homebridge]: Warning, couldn't control homebridge service: $r"; + } + } + else { + print_log "[Homebridge]: Error, homebridge service path not defined"; + } } -if (said $v_generate_hb_config) { - my $config_json = "{\n\t\"bridge\": {\n"; - $config_json .= "\t\t\"name\": \"" . $name . "\",\n"; - $config_json .= "\t\t\"username\": \"" . $username . "\",\n"; - $config_json .= "\t\t\"port\": " . $port . ",\n"; - $config_json .= "\t\t\"pin\": \"" . $pin . "\"\n\t},\n"; - $config_json .= "\t\"description\": \"MH Generated HomeKit Configuration v" . $version . " " . &time_date_stamp(17) . "\",\n"; - - $config_json .= "\n\t\"accessories\": [\n"; - $acc_count = 0; - $config_json .= add_group("fan"); - $config_json .= add_group("switch"); - $config_json .= add_group("light"); - $config_json .= add_group("lock"); - $config_json .= add_group("garagedoor"); - $config_json .= add_group("blinds"); - $config_json .= add_group("thermostat"); +if ( said $v_generate_hb_config) { + my $config_json = "{\n\t\"bridge\": {\n"; + $config_json .= "\t\t\"name\": \"" . $name . "\",\n"; + $config_json .= "\t\t\"username\": \"" . $username . "\",\n"; + $config_json .= "\t\t\"port\": " . $port . ",\n"; + $config_json .= "\t\t\"pin\": \"" . $pin . "\"\n\t},\n"; + $config_json .= + "\t\"description\": \"MH Generated HomeKit Configuration v" + . $version . " " + . &time_date_stamp(17) . "\",\n"; - $config_json .= "\t\t}\n\t]\n}\n"; - print_log "[Homebridge]: Writing configuration to $filepath..."; - #print_log $config_json; - file_write($filepath, $config_json); -} + $config_json .= "\n\t\"accessories\": [\n"; + $acc_count = 0; + $config_json .= add_group("fan"); + $config_json .= add_group("switch"); + $config_json .= add_group("light"); + $config_json .= add_group("lock"); + $config_json .= add_group("garagedoor"); + $config_json .= add_group("blinds"); + $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 $config_json; + file_write( $filepath, $config_json ); +} sub add_group { - my ($type) = @_; - my %url_types; - $url_types{lock}{on} = "lock"; - $url_types{lock}{off} = "unlock"; - $url_types{blinds}{on} = "up"; - $url_types{blinds}{off} = "down"; - $url_types{garagedoor}{on} = "up"; - $url_types{garagedoor}{off} = "down"; - my $groupname = "HB__" . (uc $type); - my $group = &get_object_by_name($groupname); - print_log "gn=$groupname"; - return unless ($group); - my $text = ""; - for my $member (list $group) { - $text .= "\t\t},\n" if ($acc_count > 0 ); - $acc_count++; - $text .= "\t\t{\n"; - $text .= "\t\t\"accessory\": \"HttpMulti\",\n"; - my $name = $member->{object_name}; - $name =~ s/_/ /g; - $name =~ s/\$//g; - $name = $member->{label} if (defined $member->{label}); - $text .= "\t\t\"name\": \"" . $name . "\",\n"; - if ($type eq "thermostat") { - my $name2 = $member->{object_name}; - $name2 =~ s/\$//g; - $text .= "\t\t\"setpoint_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/sub?hb_thermo_setpoint(%27" . $name2 . "%27,%VALUE%)\",\n"; - } else { - my $on = "on"; - $on = $url_types{$type}{on} if (defined $url_types{$type}{on}); - my $off = "off"; - $off = $url_types{$type}{off} if (defined $url_types{$type}{off}); - $text .= "\t\t\"" . $on . "_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=" .$on . "\",\n"; - $text .= "\t\t\"" . $off . "_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=" .$off . "\",\n"; - $text .= "\t\t\"brightness_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=%VALUE%\",\n" if ($type eq "light"); - $text .= "\t\t\"speed_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SET;none?select_item=" . $member->{object_name} . "&select_state=%VALUE%\",\n" if ($type eq "fan"); - if ($type eq "thermostat") { - $text .= "\t\t\"mode_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_set_state%28" . $member->{object_name} .",%VALUE%%29\",\n"; - $text .= "\t\t\"status_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_get_state%28" . $member->{object_name} ."," . $type . "%29\",\n"; - $text .= "\t\t\"setpoint_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_set_setpoint%28" . $member->{object_name} . "%29\",\n"; - $text .= "\t\t\"gettemp_url\": \"http://" . $Info{IPAddress_Local} . ":" . $config_parms{http_port} . "/SUB?hb_thermo_get_setpoint%28" . $member->{object_name} . "%29\",\n"; - $text .= "\t\t\"unit_type\": \"" . $units . "\",\n"; - } else { - my $obj_name = $member->{object_name}; - $obj_name =~ s/^\$//; #remove $ since the web sub system doesn't seem to like it. - $text .= "\t\t\"status_url\": \"http://" . $Info{IPAddress_local} . ":" . $config_parms{http_port} . "/SUB?hb_status%28" . $obj_name ."%29\",\n"; - } + my ($type) = @_; + my %url_types; + $url_types{lock}{on} = "lock"; + $url_types{lock}{off} = "unlock"; + $url_types{blinds}{on} = "up"; + $url_types{blinds}{off} = "down"; + $url_types{garagedoor}{on} = "up"; + $url_types{garagedoor}{off} = "down"; + my $groupname = "HB__" . ( uc $type ); + my $group = &get_object_by_name($groupname); + print_log "gn=$groupname"; + return unless ($group); + my $text = ""; + + for my $member ( list $group) { + $text .= "\t\t},\n" if ( $acc_count > 0 ); + $acc_count++; + $text .= "\t\t{\n"; + $text .= "\t\t\"accessory\": \"HttpMulti\",\n"; + my $name = $member->{object_name}; + $name =~ s/_/ /g; + $name =~ s/\$//g; + $name = $member->{label} if ( defined $member->{label} ); + my $obj_name = $member->{object_name}; + $obj_name =~ + s/^\$//; #remove $ since the web sub system doesn't seem to like it. - } - $text .= "\t\t\"deviceType\": \"" . $type . "\"\n"; - } - return $text; + $text .= "\t\t\"name\": \"" . $name . "\",\n"; + if ( $type eq "thermostat" ) { + $text .= + "\t\t\"mode_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SUB?hb_thermo_set_state%28" + . $obj_name + . ",%VALUE%%29\",\n"; + $text .= + "\t\t\"status_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SUB?hb_thermo_get_state%28" + . $obj_name . "," + . $type + . "%29\",\n"; + $text .= + "\t\t\"setpoint_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SUB?hb_thermo_set_setpoint%28" + . $obj_name + . ",%VALUE%%29\",\n"; + $text .= + "\t\t\"gettemp_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SUB?hb_thermo_get_setpoint%28" + . $obj_name + . "%29\",\n"; + $text .= "\t\t\"unit_type\": \"" . $units . "\",\n"; + } + else { + my $on = "on"; + $on = $url_types{$type}{on} if ( defined $url_types{$type}{on} ); + my $off = "off"; + $off = $url_types{$type}{off} if ( defined $url_types{$type}{off} ); + $text .= + "\t\t\"" + . $on + . "_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SET;none?select_item=" + . $member->{object_name} + . "&select_state=" + . $on . "\",\n"; + $text .= + "\t\t\"" + . $off + . "_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SET;none?select_item=" + . $member->{object_name} + . "&select_state=" + . $off . "\",\n"; + $text .= + "\t\t\"brightness_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SET;none?select_item=" + . $member->{object_name} + . "&select_state=%VALUE%\",\n" + if ( $type eq "light" ); + $text .= + "\t\t\"speed_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SET;none?select_item=" + . $member->{object_name} + . "&select_state=%VALUE%\",\n" + if ( $type eq "fan" ); + $text .= + "\t\t\"status_url\": \"http://" + . $Info{IPAddress_local} . ":" + . $config_parms{http_port} + . "/SUB?hb_status%28" + . $obj_name . "," + . $type + . "%29\",\n"; + } + $text .= "\t\t\"deviceType\": \"" . $type . "\"\n"; + } + return $text; } -#curl "http://127.0.0.1:8080/sub?hb_status%28test_light%29" #() +#curl "http://127.0.0.1:8080/sub?hb_status%28test_light%29" #() sub hb_status { - my ($item,$type) = @_; - my $object = &get_object_by_name($item); - my $data = ""; - unless (defined $object) { - print_log "[Homebridge: hb_status]: Error, unknown object $item"; - } else { - my $state = lc $object->state; - if (($state eq "on") or ($state =~ /^lock/i) or ($state =~ /^close/)) { - $data = "1"; - } elsif (($state eq "off") or ($state =~ /^unlock/i) or ($state =~ /^open/) or ($state =~ /^down/)) { - $data = "0"; - } elsif ($state =~ /^low/) { - $data = "20"; - } elsif ($state =~ /^med/) { - $data = "50"; - } elsif ($state =~ /^high/) { - $data = "70"; - } elsif ($state =~ /^up/) { - $data = "100"; - } else { - ($data) = $state =~ /(\d+)/; - } - print_log "[Homebridge]: Warning, no state data to return!" if ($data eq ""); - print_log "[Homebridge]: Status request: item=$item state=$state status=[$data]\n" if ($hb_debug); - } - return <state; + if ( ( $state =~ /^lock/i ) or ( $state =~ /^close/ ) ) { + $data = "1"; + } + elsif (( lc $state eq "off" ) + or ( $state =~ /^unlock/i ) + or ( $state =~ /^open/i ) + or ( $state =~ /^down/i ) ) + { + $data = "0"; + } + elsif ( $state =~ /^low/i ) { + $data = "20"; + } + elsif ( $state =~ /^med/i ) { + $data = "50"; + } + elsif ( $state =~ /^high/i ) { + $data = "70"; + } + elsif ( $state =~ /^up/i ) { + $data = "100"; + } + elsif ( lc $state eq "on" ) { + $data = "100"; + if ( lc $type eq "light" ) { + if ( $object->can('level') ) { + $data = $object->level; + } + print_log "[Homebridge]: Light Level $data" if ($hb_debug); + } + } + else { + ($data) = $state =~ /(\d+)/; + } + print_log "[Homebridge]: Warning, no state data to return!" + if ( $data eq "" ); + print_log + "[Homebridge]: Status request: item=$item state=$state status=[$data] type=$type" + if ($hb_debug); + } + return <get_mode(); - print_log "[Homebridge]: Thermostat Venstar Colortouch with mode " . $mode . " found"; - if ($mode =~ /^auto/i) { - $data = "3"; - } elsif ($mode =~ /^cool/i) { - $data = "2"; - } elsif ($mode =~ /^heat/i) { - $data = "1"; - } else { - $data = "0"; - } - } - print_log "[Homebridge]: Thermostat State request: item=$item mode=$mode status=[$data]\n" if ($hb_debug); - } - return <get_mode(); + print_log "[Homebridge]: Thermostat Venstar Colortouch with mode " + . $mode + . " found"; + if ( $mode =~ /^auto/i ) { + $data = "3"; + } + elsif ( $mode =~ /^cool/i ) { + $data = "2"; + } + elsif ( $mode =~ /^heat/i ) { + $data = "1"; + } + else { + $data = "0"; + } + } + print_log + "[Homebridge]: Thermostat State request: item=$item mode=$mode status=[$data]\n" + if ($hb_debug); + } + return <get_sched() eq "on") { - print_log "[Homebridge]: Thermostat on a schedule, turning off schedule for override"; - $object -> set_schedule("off"); - $sp_delay = 5; - } - my $mode = "off"; - if ($value == 1) { - $mode = "heat"; - } elsif ($value == 2) { - $mode = "cool"; - } elsif ($value == 3) { - $mode = "auto"; - } - print_log "[Homebridge]: Setting thermostat to $mode"; - if ($sp_delay) { - eval_with_timer '$' . $item . '->set_mode(' . $mode . ');', $sp_delay; - } else { - $object -> set_mode($mode); - } + my ( $item, $value ) = @_; + print_log "[Homebridge]: Mode change request for $item to $value"; + my $object = &get_object_by_name($item); - } elsif (UNIVERSAL::isa($object,'Nest_Thermostat')) { - print_log "[Homebridge]: Nest Thermostat found"; + if ( UNIVERSAL::isa( $object, 'Venstar_Colortouch' ) ) { + print_log "[Homebridge]: Thermostat Venstar Colortouch found"; + my $sp_delay = 0; + if ( $object->get_sched() eq "on" ) { + print_log + "[Homebridge]: Thermostat on a schedule, turning off schedule for override"; + $object->set_schedule("off"); + $sp_delay = 5; + } + my $mode = "off"; + if ( $value == 1 ) { + $mode = "heat"; + } + elsif ( $value == 2 ) { + $mode = "cool"; + } + elsif ( $value == 3 ) { + $mode = "auto"; + } + print_log "[Homebridge]: Setting thermostat to $mode"; + if ($sp_delay) { + eval_with_timer '$' . $item . '->set_mode(' . $mode . ');', + $sp_delay; + } + else { + $object->set_mode($mode); + } - } elsif (UNIVERSAL::isa($object,'Insteon::Thermostat')) { - print_log "[Homebridge]: Insteon Thermostat found"; + } + elsif ( UNIVERSAL::isa( $object, 'Nest_Thermostat' ) ) { + print_log "[Homebridge]: Nest Thermostat found"; - } else { - print_log "Unsupported Thermostat type"; - } + } + elsif ( UNIVERSAL::isa( $object, 'Insteon::Thermostat' ) ) { + print_log "[Homebridge]: Insteon Thermostat found"; + + } + else { + print_log "Unsupported Thermostat type"; + } } sub hb_thermo_get_setpoint { - my ($item) = @_; - print_log "[Homebridge]: Get Thermostat Current setpoint"; - my $data = "0"; - my $mode = "off"; - my $object = &get_object_by_name($item); - unless (defined $object) { - print_log "[Homebridge: hb_thermo_state]: Error, unknown object $object"; - } else { - if (UNIVERSAL::isa($object,'Venstar_Colortouch')) { - $object->get_mode(); - print_log "[Homebridge]: Thermostat Venstar Colortouch with mode " . $mode . " found"; - if ($mode =~ /^cool/) { - $data = $object -> get_cool_sp(); - } else { - $data = $object -> get_heat_sp(); - } - } - } - print_log "[Homebridge]: Thermostat State request: item=$item mode=$mode status=[$data]\n" if ($hb_debug); - return <get_mode(); + print_log "[Homebridge]: Thermostat Venstar Colortouch with mode " + . $mode + . " found"; + if ( $mode =~ /^cool/ ) { + $data = $object->get_sp_cool; + } + else { + $data = $object->get_sp_heat; + } + } + } + print_log + "[Homebridge]: Thermostat Get Setpoint request: item=$item mode=$mode status=[$data]\n" + if ($hb_debug); + return <get_sched() eq "on") { - print_log "[Homebridge]: Thermostat on a schedule, turning off schedule for override"; - $object -> set_schedule("off"); - $sp_delay = 5; - } - my $auto_mode = ""; - $auto_mode = &calc_auto_mode($value,$object->get_temp()) if ($object->get_mode() eq "auto"); - print_log "[Homebridge]: Thermostat calc mode is $auto_mode" if ($auto_mode); - if (($object->get_mode() eq "cooling") or ($auto_mode eq "cool")) { - if ($sp_delay) { - eval_with_timer '$' . $item . '->set_cool_sp(' . $value . ');', $sp_delay; - } else { - $object -> set_cool_sp($value); - } - } else { - if ($sp_delay) { - eval_with_timer '$' . $item . '->set_heat_sp(' . $value . ');', $sp_delay; - } else { - $object -> set_heat_sp($value); - } - } + my ( $item, $value ) = @_; + print_log "[Homebridge]: Temperature change request for $item to $value"; + my $object = &get_object_by_name($item); + + if ( UNIVERSAL::isa( $object, 'Venstar_Colortouch' ) ) { + print_log "[Homebridge]: Thermostat Venstar Colortouch found"; + my $sp_delay = 0; + if ( $object->get_sched() eq "on" ) { + print_log + "[Homebridge]: Thermostat on a schedule, turning off schedule for override"; + $object->set_schedule("off"); + $sp_delay = 5; + } + my $auto_mode = ""; + $auto_mode = &calc_auto_mode( $value, $object->get_temp() ) + if ( $object->get_mode() eq "auto" ); + print_log "[Homebridge]: Thermostat calc mode is $auto_mode" + if ($auto_mode); + if ( ( $object->get_mode() eq "cooling" ) or ( $auto_mode eq "cool" ) ) + { + if ($sp_delay) { + eval_with_timer '$' . $item . '->set_cool_sp(' . $value . ');', + $sp_delay; + } + else { + $object->set_cool_sp($value); + } + } + else { + if ($sp_delay) { + eval_with_timer '$' . $item . '->set_heat_sp(' . $value . ');', + $sp_delay; + } + else { + $object->set_heat_sp($value); + } + } - } elsif (UNIVERSAL::isa($object,'Nest_Thermostat')) { - print_log "[Homebridge]: Nest Thermostat found"; - $object -> set_target_temp($value); + } + elsif ( UNIVERSAL::isa( $object, 'Nest_Thermostat' ) ) { + print_log "[Homebridge]: Nest Thermostat found"; + $object->set_target_temp($value); - } elsif (UNIVERSAL::isa($object,'Insteon::Thermostat')) { - print_log "[Homebridge]: Insteon Thermostat found"; - my $auto_mode = ""; - $auto_mode = &calc_auto_mode($value) if ($object->get_mode() eq "auto"); - print_log "[Homebridge]: Thermostat calc mode is $auto_mode" if ($auto_mode); - if (($object->get_mode() eq "cool") or ($auto_mode eq "cool")) { - $object -> cool_setpoint($value); - } else { - $object -> heat_setpoint($value); - } - } else { - print_log "Unsupported Thermostat type"; - } + } + elsif ( UNIVERSAL::isa( $object, 'Insteon::Thermostat' ) ) { + print_log "[Homebridge]: Insteon Thermostat found"; + my $auto_mode = ""; + $auto_mode = &calc_auto_mode($value) + if ( $object->get_mode() eq "auto" ); + print_log "[Homebridge]: Thermostat calc mode is $auto_mode" + if ($auto_mode); + if ( ( $object->get_mode() eq "cool" ) or ( $auto_mode eq "cool" ) ) { + $object->cool_setpoint($value); + } + else { + $object->heat_setpoint($value); + } + } + else { + print_log "Unsupported Thermostat type"; + } } sub calc_auto_mode { - my ($value,$intemp,$outtemp) = @_; - - my $mode = "heat"; - my $cool_threshold = 8; #set to cool if outside less - my $outside = ""; - $outside = $Weather{Outdoor} if (defined $Weather{TempOutdoor}); - $outside = $outtemp if (defined $outtemp); - my $inside = ""; - $inside = $Weather{Inside} if (defined $Weather{TempInside}); - $inside = $intemp if (defined $intemp); - $mode = "cool" if ($value < $inside); - $mode = "heat" if (($value - $cool_threshold) > $outside); - - return $mode; + my ( $value, $intemp, $outtemp ) = @_; + + my $mode = "heat"; + my $cool_threshold = 8; #set to cool if outside less + my $outside = ""; + $outside = $Weather{Outdoor} if ( defined $Weather{TempOutdoor} ); + $outside = $outtemp if ( defined $outtemp ); + my $inside = ""; + $inside = $Weather{Inside} if ( defined $Weather{TempInside} ); + $inside = $intemp if ( defined $intemp ); + $mode = "cool" if ( $value < $inside ); + $mode = "heat" if ( ( $value - $cool_threshold ) > $outside ); + + return $mode; } - - From a4e6af9ab88594d9b4526a4dc3ae7f888ec92978 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Sat, 17 Dec 2016 13:40:48 -0800 Subject: [PATCH 076/209] Add Ecobee.pl user code --- code/common/Ecobee.pl | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 code/common/Ecobee.pl diff --git a/code/common/Ecobee.pl b/code/common/Ecobee.pl new file mode 100644 index 000000000..96aabd5a1 --- /dev/null +++ b/code/common/Ecobee.pl @@ -0,0 +1,68 @@ +# Category=HVAC + +# MisterHouse Ecobee3 thermostat controller +# Brian Rudy (brudyNO@SPAMpraecogito.com) +# +# For additional functionality and configuration requirements, see Ecobee.pm + +# Home/away +if (my $state = state_now $home_occupants) { + if ($state eq "home") { + print_log "Setting thermostat to home mode (occupants home)"; + $ecobee_thermo->set_hold("climate_nextTransition_home"); + } else { + print_log "Setting thermostat to away mode (occupants away)"; + $ecobee_thermo->set_hold("climate_nextTransition_away"); + } +} + +# New stuff that interacts with the Ecobee MH module + +$v_query_ecobee_temp = new Voice_Cmd 'Get current thermostat temperature'; + + +if (my $state = said $v_query_ecobee_temp) { + print_log "Getting current thermostat temperature"; + my $actualTemp = $ecobee_thermo->get_temp(); + print_log "The thermostat temperature is $actualTemp"; +} + +if (my $state = state_now $ecobee_thermo) { + print_log "Ecobee temperature is now " . ($state * 0.1) . " degrees F"; +} + +if (my $state = state_now $thermo_humid) { + print_log "Ecobee humidity is now $state\%"; +} + +if (my $state = state_now $thermo_hvac_status) { + print_log "Ecobee status is now $state"; +} + +if (my $state = state_now $thermo_mode) { + print_log "Ecobee mode is now $state"; +} + +if (my $state = state_now $thermo_climate) { + print_log "Ecobee climate is now $state"; +} + +$v_clear_ecobee_hold = new Voice_Cmd 'Clear thermostat hold'; +if (my $state = said $v_clear_ecobee_hold) { + print_log "Clearing the current thermostat hold"; + $ecobee_thermo->clear_hold(); +} + +$v_set_temp_hold = new Voice_Cmd 'Set thermostat temperature hold'; +if (my $state = said $v_set_temp_hold) { + print_log "Setting a thermostat temperature hold"; + $ecobee_thermo->set_hold("temperature_nextTransition_740_760"); +} + +$v_set_climate_hold = new Voice_Cmd 'Set thermostat climate hold to [home,away,sleep]'; +if (my $state = said $v_set_climate_hold) { + print_log "Setting a thermostat climate hold to $state"; + $ecobee_thermo->set_hold("climate_nextTransition_$state"); +} + + From 412cd177e81165ee04ff3190fadcb43d3b904a6e Mon Sep 17 00:00:00 2001 From: rudybrian Date: Sat, 17 Dec 2016 14:01:46 -0800 Subject: [PATCH 077/209] Perltidy source --- code/common/Ecobee.pl | 70 +- lib/Ecobee.pm | 1996 +++++++++++++++++++++++++---------------- 2 files changed, 1261 insertions(+), 805 deletions(-) diff --git a/code/common/Ecobee.pl b/code/common/Ecobee.pl index 96aabd5a1..4057adeae 100644 --- a/code/common/Ecobee.pl +++ b/code/common/Ecobee.pl @@ -1,68 +1,68 @@ # Category=HVAC -# MisterHouse Ecobee3 thermostat controller +# MisterHouse Ecobee3 thermostat controller # Brian Rudy (brudyNO@SPAMpraecogito.com) # # For additional functionality and configuration requirements, see Ecobee.pm # Home/away -if (my $state = state_now $home_occupants) { - if ($state eq "home") { - print_log "Setting thermostat to home mode (occupants home)"; - $ecobee_thermo->set_hold("climate_nextTransition_home"); - } else { - print_log "Setting thermostat to away mode (occupants away)"; - $ecobee_thermo->set_hold("climate_nextTransition_away"); - } +if ( my $state = state_now $home_occupants) { + if ( $state eq "home" ) { + print_log "Setting thermostat to home mode (occupants home)"; + $ecobee_thermo->set_hold("climate_nextTransition_home"); + } + else { + print_log "Setting thermostat to away mode (occupants away)"; + $ecobee_thermo->set_hold("climate_nextTransition_away"); + } } # New stuff that interacts with the Ecobee MH module $v_query_ecobee_temp = new Voice_Cmd 'Get current thermostat temperature'; - -if (my $state = said $v_query_ecobee_temp) { - print_log "Getting current thermostat temperature"; - my $actualTemp = $ecobee_thermo->get_temp(); - print_log "The thermostat temperature is $actualTemp"; +if ( my $state = said $v_query_ecobee_temp) { + print_log "Getting current thermostat temperature"; + my $actualTemp = $ecobee_thermo->get_temp(); + print_log "The thermostat temperature is $actualTemp"; } -if (my $state = state_now $ecobee_thermo) { - print_log "Ecobee temperature is now " . ($state * 0.1) . " degrees F"; +if ( my $state = state_now $ecobee_thermo) { + print_log "Ecobee temperature is now " . ( $state * 0.1 ) . " degrees F"; } -if (my $state = state_now $thermo_humid) { - print_log "Ecobee humidity is now $state\%"; +if ( my $state = state_now $thermo_humid) { + print_log "Ecobee humidity is now $state\%"; } -if (my $state = state_now $thermo_hvac_status) { - print_log "Ecobee status is now $state"; +if ( my $state = state_now $thermo_hvac_status) { + print_log "Ecobee status is now $state"; } -if (my $state = state_now $thermo_mode) { - print_log "Ecobee mode is now $state"; +if ( my $state = state_now $thermo_mode) { + print_log "Ecobee mode is now $state"; } -if (my $state = state_now $thermo_climate) { - print_log "Ecobee climate is now $state"; +if ( my $state = state_now $thermo_climate) { + print_log "Ecobee climate is now $state"; } $v_clear_ecobee_hold = new Voice_Cmd 'Clear thermostat hold'; -if (my $state = said $v_clear_ecobee_hold) { - print_log "Clearing the current thermostat hold"; - $ecobee_thermo->clear_hold(); +if ( my $state = said $v_clear_ecobee_hold) { + print_log "Clearing the current thermostat hold"; + $ecobee_thermo->clear_hold(); } $v_set_temp_hold = new Voice_Cmd 'Set thermostat temperature hold'; -if (my $state = said $v_set_temp_hold) { - print_log "Setting a thermostat temperature hold"; - $ecobee_thermo->set_hold("temperature_nextTransition_740_760"); +if ( my $state = said $v_set_temp_hold) { + print_log "Setting a thermostat temperature hold"; + $ecobee_thermo->set_hold("temperature_nextTransition_740_760"); } -$v_set_climate_hold = new Voice_Cmd 'Set thermostat climate hold to [home,away,sleep]'; -if (my $state = said $v_set_climate_hold) { - print_log "Setting a thermostat climate hold to $state"; - $ecobee_thermo->set_hold("climate_nextTransition_$state"); +$v_set_climate_hold = + new Voice_Cmd 'Set thermostat climate hold to [home,away,sleep]'; +if ( my $state = said $v_set_climate_hold) { + print_log "Setting a thermostat climate hold to $state"; + $ecobee_thermo->set_hold("climate_nextTransition_$state"); } - diff --git a/lib/Ecobee.pm b/lib/Ecobee.pm index 5e224276b..1bf9ee7a5 100644 --- a/lib/Ecobee.pm +++ b/lib/Ecobee.pm @@ -90,18 +90,17 @@ changing certain parameters on the thermostat. =cut - # Notes: # -# As of 1/31/2016 some API examples using cURL incorrectly show the authorize and token -# endpoints as https://api.ecobee.com/1/authorize and https://api.ecobee.com/1/token. +# As of 1/31/2016 some API examples using cURL incorrectly show the authorize and token +# endpoints as https://api.ecobee.com/1/authorize and https://api.ecobee.com/1/token. # However, these are actually https://api.ecobee.com/authorize and https://api.ecobee.com/token. # Other endpoints are versioned and appear to work correctly. #todo # -# -Child items that are registered for state changes are currently only updated after the -# state changes post-initialization, and are are left at a default value on startup until +# -Child items that are registered for state changes are currently only updated after the +# state changes post-initialization, and are are left at a default value on startup until # this happens # # -Add support for creating and cancelling vacations @@ -137,7 +136,7 @@ sub debug { package Ecobee_Interface; -@Ecobee_Interface::ISA = ('Generic_Item', 'Ecobee'); +@Ecobee_Interface::ISA = ( 'Generic_Item', 'Ecobee' ); use strict; use warnings; @@ -163,10 +162,10 @@ $rest{group} = "/1/group"; sub new { my ( $class, $api_key, $poll_interval, $port_name, $url ) = @_; my $self = {}; - $port_name = 'Ecobee' if !$port_name; - $url = "https://api.ecobee.com" if !$url; - $poll_interval = 60 if !$poll_interval; - $api_key = $::config_parms{ $port_name . "_api_key" } if !$api_key; + $port_name = 'Ecobee' if !$port_name; + $url = "https://api.ecobee.com" if !$url; + $poll_interval = 60 if !$poll_interval; + $api_key = $::config_parms{ $port_name . "_api_key" } if !$api_key; $$self{port_name} = $port_name; $$self{url} = $url; $$self{poll_interval} = $poll_interval; @@ -181,8 +180,8 @@ sub new { $$self{auth_check_timer} = new Timer; $$self{token_check_timer} = new Timer; bless $self, $class; - - $self->restore_data('access_token','refresh_token'); + + $self->restore_data( 'access_token', 'refresh_token' ); $self->_init(); return $self; } @@ -192,131 +191,187 @@ sub _init { # Start a timer to check if we are authenticated my $action = sub { $self->_check_auth() }; - $$self{auth_check_timer}->set(1, $action); + $$self{auth_check_timer}->set( 1, $action ); } # We need to do this asynchronously so we don't block execution that blocks the saved tokens from being restored sub _check_auth { - my ($self) = @_; - #$$self{access_token} = undef; - #$$self{refresh_token} = undef; - # Check if we have cached access and refresh tokens - if ((defined $$self{access_token}) && (defined $$self{refresh_token})) { - if (($$self{access_token} eq '') || ($$self{refresh_token} eq '')) { - # The access_token or refresh_token are missing. Tell the user and wait - $self->debug("Error: Missing tokens. Please reauthenticate the API key with the Ecobee portal"); - $self->_request_pin_auth(); - } else { - # Ok, we have tokens. Make sure they are current, then go get the initial state of the device, then start the time to look for updates - $self->debug("We have tokens, lets proceed"); - $self->_thermostat_summary(); # This populates the revision numbers and operating state - $self->_list_thermostats(); # This gets the full initial state of each thermostat - $self->_get_groups(); # This gets the groups, their settings and the associated thermostats in each group - - #### - # Testing functions here. These will be removed eventually - ## - #$self->print_devices(); - #main::print_log( "[Ecobee] actualTemperature is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "actualTemperature")/10) . " degrees F" ); - #main::print_log( "[Ecobee] desiredHeat is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Heat")/10) . " degrees F" ); - #main::print_log( "[Ecobee] desiredCool is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Cool")/10) . " degrees F" ); - #main::print_log( "[Ecobee] Office temp is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "Office")/10) . " degrees F" ); - #main::print_log( "[Ecobee] actualHumidity is " . $self->get_humidity("Monet Thermostat", "actualHumidity") . "%"); - #main::print_log( "[Ecobee] desiredHumidity is " . $self->get_desired_comfort("Monet Thermostat", "Humidity") . "%"); - #main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); - #main::print_log( "[Ecobee] hvacMode is " . $self->get_setting("Monet Thermostat", "hvacMode") ); - #my $alerts = $self->get_alert("Monet Thermostat"); - #if ($alerts) { - # foreach my $key (keys %{$alerts}) { - # main::print_log( "[Ecobee] Alert " . $key . ": (\"" . $alerts->{$key}{text} . "\")" ); - # } - #} - #my $events = $self->get_event("Monet Thermostat"); - #if ($events) { - # foreach my $key (keys %{$events}) { - # main::print_log( "[Ecobee] Event: $key" ); - # } - #} - #### - - # The basic details should be populated now so we can start to poll - $self->populate_monitor_hash( $$self{monitor} ); - $$self{ready} = 1; - my $action = sub { $self->_poll() }; - $$self{polling_timer}->set($$self{poll_interval}, $action); - } - } else { - # If we don't have tokens, we need to get a PIN and wait until they user registers it - # The access_token or refresh_token are undefined. This is probably the first run. Tell the user and wait - $self->debug("Error: Token variables undefined. Please authenticate the PIN with the Ecobee portal. A request for a new PIN will follow this message."); - $self->_request_pin_auth(); + my ($self) = @_; + + #$$self{access_token} = undef; + #$$self{refresh_token} = undef; + # Check if we have cached access and refresh tokens + if ( ( defined $$self{access_token} ) && ( defined $$self{refresh_token} ) ) + { + if ( ( $$self{access_token} eq '' ) || ( $$self{refresh_token} eq '' ) ) + { + # The access_token or refresh_token are missing. Tell the user and wait + $self->debug( + "Error: Missing tokens. Please reauthenticate the API key with the Ecobee portal" + ); + $self->_request_pin_auth(); + } + else { + # Ok, we have tokens. Make sure they are current, then go get the initial state of the device, then start the time to look for updates + $self->debug("We have tokens, lets proceed"); + $self->_thermostat_summary() + ; # This populates the revision numbers and operating state + $self->_list_thermostats() + ; # This gets the full initial state of each thermostat + $self->_get_groups() + ; # This gets the groups, their settings and the associated thermostats in each group + + #### + # Testing functions here. These will be removed eventually + ## + #$self->print_devices(); + #main::print_log( "[Ecobee] actualTemperature is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "actualTemperature")/10) . " degrees F" ); + #main::print_log( "[Ecobee] desiredHeat is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Heat")/10) . " degrees F" ); + #main::print_log( "[Ecobee] desiredCool is " . sprintf("%.1f", $self->get_desired_comfort("Monet Thermostat", "Cool")/10) . " degrees F" ); + #main::print_log( "[Ecobee] Office temp is " . sprintf("%.1f", $self->get_temp("Monet Thermostat", "Office")/10) . " degrees F" ); + #main::print_log( "[Ecobee] actualHumidity is " . $self->get_humidity("Monet Thermostat", "actualHumidity") . "%"); + #main::print_log( "[Ecobee] desiredHumidity is " . $self->get_desired_comfort("Monet Thermostat", "Humidity") . "%"); + #main::print_log( "[Ecobee] Humidity is " . $self->get_humidity("Monet Thermostat", "Monet Thermostat") . "%"); + #main::print_log( "[Ecobee] hvacMode is " . $self->get_setting("Monet Thermostat", "hvacMode") ); + #my $alerts = $self->get_alert("Monet Thermostat"); + #if ($alerts) { + # foreach my $key (keys %{$alerts}) { + # main::print_log( "[Ecobee] Alert " . $key . ": (\"" . $alerts->{$key}{text} . "\")" ); + # } + #} + #my $events = $self->get_event("Monet Thermostat"); + #if ($events) { + # foreach my $key (keys %{$events}) { + # main::print_log( "[Ecobee] Event: $key" ); + # } + #} + #### + + # The basic details should be populated now so we can start to poll + $self->populate_monitor_hash( $$self{monitor} ); + $$self{ready} = 1; + my $action = sub { $self->_poll() }; + $$self{polling_timer}->set( $$self{poll_interval}, $action ); + } + } + else { + # If we don't have tokens, we need to get a PIN and wait until they user registers it + # The access_token or refresh_token are undefined. This is probably the first run. Tell the user and wait + $self->debug( + "Error: Token variables undefined. Please authenticate the PIN with the Ecobee portal. A request for a new PIN will follow this message." + ); + $self->_request_pin_auth(); } + # If we have tokens, go get the initial state of the device, then start the time to look for updates } - # We don't have a valid set of tokens, so request a new PIN sub _request_pin_auth { my ($self) = @_; - my ($isSuccessResponse1, $keyparams) = $self->_get_JSON_data("GET", "authorize", "?response_type=ecobeePin&client_id=" . $$self{api_key} . "&scope=smartWrite"); - if (!$isSuccessResponse1) { - # something has gone wrong, we should probably exit - main::print_log( "[Ecobee]: Error, failed to get PIN! Have you entered a valid API key?"); - } else { - # print the PIN to the log so the user knows what to enter - main::print_log( "[Ecobee]: New PIN generated ->" . $keyparams->{ecobeePin} . "<-. You have " . $keyparams->{expires_in} . " minutes to add a new application with this PIN on the Ecobee website!"); - # loop until the user enters the PIN or the code expires - my $action = sub { $self->_wait_for_tokens($keyparams->{ecobeePin},$keyparams->{code}, $keyparams->{interval}) }; - $$self{token_check_timer}->set($keyparams->{interval}, $action); + my ( $isSuccessResponse1, $keyparams ) = $self->_get_JSON_data( + "GET", + "authorize", + "?response_type=ecobeePin&client_id=" + . $$self{api_key} + . "&scope=smartWrite" + ); + if ( !$isSuccessResponse1 ) { + + # something has gone wrong, we should probably exit + main::print_log( + "[Ecobee]: Error, failed to get PIN! Have you entered a valid API key?" + ); } -} + else { + # print the PIN to the log so the user knows what to enter + main::print_log( "[Ecobee]: New PIN generated ->" + . $keyparams->{ecobeePin} + . "<-. You have " + . $keyparams->{expires_in} + . " minutes to add a new application with this PIN on the Ecobee website!" + ); + # loop until the user enters the PIN or the code expires + my $action = sub { + $self->_wait_for_tokens( $keyparams->{ecobeePin}, + $keyparams->{code}, $keyparams->{interval} ); + }; + $$self{token_check_timer}->set( $keyparams->{interval}, $action ); + } +} # Poll for tokens sub _wait_for_tokens { - my ($self, $pin, $code, $interval) = @_; - # check for token - my ($isSuccessResponse1, $tokenparams) = $self->_get_JSON_data("POST", "token", "?grant_type=ecobeePin&code=" . $code . "&client_id=" . $$self{api_key}); - if ($isSuccessResponse1 && defined $tokenparams->{access_token} && defined $tokenparams->{refresh_token}) { - # save tokens - $$self{access_token} = $tokenparams->{access_token}; - $$self{refresh_token} = $tokenparams->{refresh_token}; - $self->_check_auth(); + my ( $self, $pin, $code, $interval ) = @_; + + # check for token + my ( $isSuccessResponse1, $tokenparams ) = $self->_get_JSON_data( + "POST", + "token", + "?grant_type=ecobeePin&code=" . $code . "&client_id=" . $$self{api_key} + ); + if ( $isSuccessResponse1 + && defined $tokenparams->{access_token} + && defined $tokenparams->{refresh_token} ) + { + # save tokens + $$self{access_token} = $tokenparams->{access_token}; + $$self{refresh_token} = $tokenparams->{refresh_token}; + $self->_check_auth(); } + # If expired, get a new PIN and start over - if (defined $tokenparams->{error}) { - if ($tokenparams->{error} eq "authorization_expired") { - main::print_log( "[Ecobee]: Warning, PIN has expired, requesting new PIN." ); - $self->_request_pin_auth(); - } elsif ($tokenparams->{error} eq "authorization_pending") { - main::print_log( "[Ecobee]: Authorization is still pending. Please add a new application with PIN ->" . $pin . "<- on Ecobee website before it expires." ); - my $action = sub { $self->_wait_for_tokens($pin, $code, $interval) }; - $$self{token_check_timer}->set($interval, $action); - } - } + if ( defined $tokenparams->{error} ) { + if ( $tokenparams->{error} eq "authorization_expired" ) { + main::print_log( + "[Ecobee]: Warning, PIN has expired, requesting new PIN."); + $self->_request_pin_auth(); + } + elsif ( $tokenparams->{error} eq "authorization_pending" ) { + main::print_log( + "[Ecobee]: Authorization is still pending. Please add a new application with PIN ->" + . $pin + . "<- on Ecobee website before it expires." ); + my $action = + sub { $self->_wait_for_tokens( $pin, $code, $interval ) }; + $$self{token_check_timer}->set( $interval, $action ); + } + } } # We need to periodically refresh the tokens when they expire sub _refresh_tokens { my ($self) = @_; $self->debug("Refreshing tokens"); - my ($isSuccessResponse1, $tokenparams) = $self->_get_JSON_data("POST", "token", "?grant_type=refresh_token&refresh_token=" . $$self{refresh_token} . "&client_id=" . $$self{api_key}); + my ( $isSuccessResponse1, $tokenparams ) = $self->_get_JSON_data( + "POST", + "token", + "?grant_type=refresh_token&refresh_token=" + . $$self{refresh_token} + . "&client_id=" + . $$self{api_key} + ); if ($isSuccessResponse1) { - $self->debug("Refresh token response looks good"); - $$self{access_token} = $tokenparams->{access_token}; - $$self{refresh_token} = $tokenparams->{refresh_token}; - } else { - # We need to handle the case where the refresh token has expired and start a new PIN authorization request. - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the refresh token request. Flushing tokens and requesting a new PIN" ); - # It looks like the tokens are FUBAR. We need to re-authenticate - $$self{access_token} = undef; - $$self{refresh_token} = undef; - $$self{ready} = 0; # This disables polling until the issue can be corrected - $self->_request_pin_auth(); + $self->debug("Refresh token response looks good"); + $$self{access_token} = $tokenparams->{access_token}; + $$self{refresh_token} = $tokenparams->{refresh_token}; } -} + else { + # We need to handle the case where the refresh token has expired and start a new PIN authorization request. + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the refresh token request. Flushing tokens and requesting a new PIN" + ); + # It looks like the tokens are FUBAR. We need to re-authenticate + $$self{access_token} = undef; + $$self{refresh_token} = undef; + $$self{ready} = + 0; # This disables polling until the issue can be corrected + $self->_request_pin_auth(); + } +} =item C<_list_thermostats()> @@ -328,68 +383,103 @@ sub _list_thermostats { my ($self) = @_; $self->debug("Listing thermostats..."); my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} - ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true","includeSettings":"true","includeEvents":"true","includeRuntime":"true","includeSensors":"true","includeProgram":"true"}}'; - my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", - '?format=json&body=' . uri_escape($json_body), $headers); + ); + my $json_body = + '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true","includeSettings":"true","includeEvents":"true","includeRuntime":"true","includeSensors":"true","includeProgram":"true"}}'; + my ( $isSuccessResponse1, $thermoparams ) = + $self->_get_JSON_data( "GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers ); if ($isSuccessResponse1) { - $self->debug("Thermostat response looks good."); - foreach my $device (@{$thermoparams->{thermostatList}}) { - # we need to inspect the runtime and remoteSensors - foreach my $key (keys %{$device->{runtime}}) { - $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; - } - - foreach my $index (@{$device->{remoteSensors}}) { - # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} = $index->{id}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} = $index->{name}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{type} = $index->{type}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} = $index->{inUse}; - foreach my $capability (@{$index->{capability}}) { - # capabilities can vary by sensor type - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{id} = $capability->{id}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{type} = $capability->{type}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; - } - } - # We need to do this for the settings as well - foreach my $key (keys %{$device->{settings}}) { - $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; - } - # Get the Alerts (provided as a JSON array) - foreach my $index (@{$device->{alerts}}) { - foreach my $key (keys %{$index}) { - $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}}{$key} = $index->{$key}; - } - } - # We also need to get the Events (provided as a JSON array, but does not contain a unique ID so we have to create our own to make this a hash) - foreach my $index (@{$device->{events}}) { - if (exists $index->{holdClimateRef}) { - $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; - } else { - $$self{data}{devices}{$device->{identifier}}{eventsHash}{$index->{type} . "-" . $index->{name}} = $index; - } - } - # set to empty so we don't crash on dclone - if (scalar @{$device->{events}} == 0) { - my $non_event = {'none' => {'holdClimateRef' => 'none'}}; - $$self{data}{devices}{$device->{identifier}}{eventsHash} = $non_event; - } - # Save the program information as well. Note: the schedule and climate info are stored in arrays. Since we need to use this same format to - # modify a schedule or climate, they will be retained in this format - $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; - - $self->debug($$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); - } - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" ); + $self->debug("Thermostat response looks good."); + foreach my $device ( @{ $thermoparams->{thermostatList} } ) { + + # we need to inspect the runtime and remoteSensors + foreach my $key ( keys %{ $device->{runtime} } ) { + $$self{data}{devices}{ $device->{identifier} }{runtime}{$key} = + $device->{runtime}{$key}; + } + + foreach my $index ( @{ $device->{remoteSensors} } ) { + + # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{id} = $index->{id}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{name} = $index->{name}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{type} = $index->{type}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{inUse} = $index->{inUse}; + foreach my $capability ( @{ $index->{capability} } ) { + + # capabilities can vary by sensor type + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{id} = $capability->{id}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{type} = $capability->{type}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{value} = $capability->{value}; + } + } + + # We need to do this for the settings as well + foreach my $key ( keys %{ $device->{settings} } ) { + $$self{data}{devices}{ $device->{identifier} }{settings}{$key} + = $device->{settings}{$key}; + } + + # Get the Alerts (provided as a JSON array) + foreach my $index ( @{ $device->{alerts} } ) { + foreach my $key ( keys %{$index} ) { + $$self{data}{devices}{ $device->{identifier} }{alertsHash} + { $index->{acknowledgeRef} }{$key} = $index->{$key}; + } + } + + # We also need to get the Events (provided as a JSON array, but does not contain a unique ID so we have to create our own to make this a hash) + foreach my $index ( @{ $device->{events} } ) { + if ( exists $index->{holdClimateRef} ) { + $$self{data}{devices}{ $device->{identifier} }{eventsHash} + { $index->{type} . "-" + . $index->{name} . "-" + . $index->{holdClimateRef} } = $index; + } + else { + $$self{data}{devices}{ $device->{identifier} }{eventsHash} + { $index->{type} . "-" . $index->{name} } = $index; + } + } + + # set to empty so we don't crash on dclone + if ( scalar @{ $device->{events} } == 0 ) { + my $non_event = { 'none' => { 'holdClimateRef' => 'none' } }; + $$self{data}{devices}{ $device->{identifier} }{eventsHash} = + $non_event; + } + + # Save the program information as well. Note: the schedule and climate info are stored in arrays. Since we need to use this same format to + # modify a schedule or climate, they will be retained in this format + $$self{data}{devices}{ $device->{identifier} }{program} = + $device->{program}; + + $self->debug( $$self{data}{devices}{ $device->{identifier} }{name} + . " ID is " + . $$self{data}{devices}{ $device->{identifier} }{identifier} + ); + } + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" + ); } } - =item C<_get_settings()> Gets the current settings, events and program @@ -400,86 +490,147 @@ sub _get_settings { my ($self) = @_; $self->debug("Getting settings, events and programs..."); my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} - ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true","includeEvents":"true","includeProgram":"true"}}'; - my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", - '?format=json&body=' . uri_escape($json_body), $headers); + ); + my $json_body = + '{"selection":{"selectionType":"registered","selectionMatch":"","includeSettings":"true","includeEvents":"true","includeProgram":"true"}}'; + my ( $isSuccessResponse1, $thermoparams ) = + $self->_get_JSON_data( "GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers ); if ($isSuccessResponse1) { - $self->debug("Settings response looks good."); - # We just asked for the settings this time - foreach my $device (@{$thermoparams->{thermostatList}}) { - # Save the previous settings - $$self{prev_data}{devices}{$device->{identifier}}{settings} = dclone $$self{data}{devices}{$device->{identifier}}{settings}; - - foreach my $key (keys %{$device->{settings}}) { - if ($device->{settings}{$key} ne $$self{data}{devices}{$device->{identifier}}{settings}{$key}) { - $self->debug("Settings parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{settings}{$key} . " to " . $device->{settings}{$key}); - } - $$self{data}{devices}{$device->{identifier}}{settings}{$key} = $device->{settings}{$key}; - } - # Compare the old with the new - $self->compare_data( $$self{data}{devices}{$device->{identifier}}{settings}, $$self{prev_data}{devices}{$device->{identifier}}{settings}, $$self{monitor}{$device->{identifier}}{settings} ); - - # Save the previous events - $$self{prev_data}{devices}{$device->{identifier}}{eventsHash} = dclone $$self{data}{devices}{$device->{identifier}}{eventsHash}; - - # create a temporary hash to compare current and previous events - my $temp_events; - foreach my $index (@{$device->{events}}) { - if (exists $index->{holdClimateRef}) { - $temp_events->{$index->{type} . "-" . $index->{name} . "-" . $index->{holdClimateRef}} = $index; - } else { - $temp_events->{$index->{type} . "-" . $index->{name}} = $index; - } - } - # Look for new events - foreach my $key (keys %{$temp_events}) { - unless (defined $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}) { - $self->debug("New event added: $key"); - $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key} = $temp_events->{$key}; - if (exists $$self{data}{devices}{$device->{identifier}}{eventsHash}{'none'}) { - # delete the none event if there is a real one - delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{'none'}; - } - } - } - # Look for deleted events - foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{eventsHash}}) { - unless (defined $temp_events->{$key} || ($key eq 'none')) { - $self->debug("Event deleted: $key"); - delete $$self{data}{devices}{$device->{identifier}}{eventsHash}{$key}; - } - } - # set to null so we don't crash on dclone - if (scalar @{$device->{events}} == 0) { - my $non_event = {'none' => {'holdClimateRef' => 'none'}}; - $$self{data}{devices}{$device->{identifier}}{eventsHash} = $non_event; - } - - # Compare the old with the new - $self->compare_data( $$self{data}{devices}{$device->{identifier}}{eventsHash}, $$self{prev_data}{devices}{$device->{identifier}}{eventsHash}, $$self{monitor}{$device->{identifier}}{eventsHash} ); - - # Save the previous program data - $$self{prev_data}{devices}{$device->{identifier}}{program} = dclone $$self{data}{devices}{$device->{identifier}}{program}; - - # Save the new program data - $$self{data}{devices}{$device->{identifier}}{program} = $device->{program}; - - if ($$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ne $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef}) { - $self->debug("currentClimateRef has changed from " . $$self{prev_data}{devices}{$device->{identifier}}{program}{currentClimateRef} . " to " . $$self{data}{devices}{$device->{identifier}}{program}{currentClimateRef} ); - } - - # Compare the old with the new - $self->compare_data( $$self{data}{devices}{$device->{identifier}}{program}, $$self{prev_data}{devices}{$device->{identifier}}{program}, $$self{monitor}{$device->{identifier}}{program} ); - } - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the settings request" ); + $self->debug("Settings response looks good."); + + # We just asked for the settings this time + foreach my $device ( @{ $thermoparams->{thermostatList} } ) { + + # Save the previous settings + $$self{prev_data}{devices}{ $device->{identifier} }{settings} = + dclone $$self{data}{devices}{ $device->{identifier} }{settings}; + + foreach my $key ( keys %{ $device->{settings} } ) { + if ( $device->{settings}{$key} ne + $$self{data}{devices}{ $device->{identifier} }{settings} + {$key} ) + { + $self->debug( "Settings parameter " + . $key + . " has changed from " + . $$self{data}{devices}{ $device->{identifier} } + {settings}{$key} . " to " + . $device->{settings}{$key} ); + } + $$self{data}{devices}{ $device->{identifier} }{settings}{$key} + = $device->{settings}{$key}; + } + + # Compare the old with the new + $self->compare_data( + $$self{data}{devices}{ $device->{identifier} }{settings}, + $$self{prev_data}{devices}{ $device->{identifier} }{settings}, + $$self{monitor}{ $device->{identifier} }{settings} + ); + + # Save the previous events + $$self{prev_data}{devices}{ $device->{identifier} }{eventsHash} = + dclone $$self{data}{devices}{ $device->{identifier} }{eventsHash}; + + # create a temporary hash to compare current and previous events + my $temp_events; + foreach my $index ( @{ $device->{events} } ) { + if ( exists $index->{holdClimateRef} ) { + $temp_events->{ $index->{type} . "-" + . $index->{name} . "-" + . $index->{holdClimateRef} } = $index; + } + else { + $temp_events->{ $index->{type} . "-" . $index->{name} } = + $index; + } + } + + # Look for new events + foreach my $key ( keys %{$temp_events} ) { + unless ( + defined $$self{data}{devices}{ $device->{identifier} } + {eventsHash}{$key} ) + { + $self->debug("New event added: $key"); + $$self{data}{devices}{ $device->{identifier} }{eventsHash} + {$key} = $temp_events->{$key}; + if ( + exists $$self{data}{devices}{ $device->{identifier} } + {eventsHash}{'none'} ) + { + # delete the none event if there is a real one + delete $$self{data}{devices}{ $device->{identifier} } + {eventsHash}{'none'}; + } + } + } + + # Look for deleted events + foreach my $key ( + keys + %{ $$self{data}{devices}{ $device->{identifier} }{eventsHash} } + ) + { + unless ( defined $temp_events->{$key} || ( $key eq 'none' ) ) { + $self->debug("Event deleted: $key"); + delete $$self{data}{devices}{ $device->{identifier} } + {eventsHash}{$key}; + } + } + + # set to null so we don't crash on dclone + if ( scalar @{ $device->{events} } == 0 ) { + my $non_event = { 'none' => { 'holdClimateRef' => 'none' } }; + $$self{data}{devices}{ $device->{identifier} }{eventsHash} = + $non_event; + } + + # Compare the old with the new + $self->compare_data( + $$self{data}{devices}{ $device->{identifier} }{eventsHash}, + $$self{prev_data}{devices}{ $device->{identifier} }{eventsHash}, + $$self{monitor}{ $device->{identifier} }{eventsHash} + ); + + # Save the previous program data + $$self{prev_data}{devices}{ $device->{identifier} }{program} = + dclone $$self{data}{devices}{ $device->{identifier} }{program}; + + # Save the new program data + $$self{data}{devices}{ $device->{identifier} }{program} = + $device->{program}; + + if ( $$self{data}{devices}{ $device->{identifier} }{program} + {currentClimateRef} ne + $$self{prev_data}{devices}{ $device->{identifier} }{program} + {currentClimateRef} ) + { + $self->debug( "currentClimateRef has changed from " + . $$self{prev_data}{devices}{ $device->{identifier} } + {program}{currentClimateRef} . " to " + . $$self{data}{devices}{ $device->{identifier} }{program} + {currentClimateRef} ); + } + + # Compare the old with the new + $self->compare_data( + $$self{data}{devices}{ $device->{identifier} }{program}, + $$self{prev_data}{devices}{ $device->{identifier} }{program}, + $$self{monitor}{ $device->{identifier} }{program} + ); + } + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the settings request" + ); } } - =item C<_get_alerts()> Gets the current alerts @@ -490,50 +641,75 @@ sub _get_alerts { my ($self) = @_; $self->debug("Getting alerts..."); my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} - ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true"}}'; - my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", - '?format=json&body=' . uri_escape($json_body), $headers); + ); + my $json_body = + '{"selection":{"selectionType":"registered","selectionMatch":"","includeAlerts":"true"}}'; + my ( $isSuccessResponse1, $thermoparams ) = + $self->_get_JSON_data( "GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers ); if ($isSuccessResponse1) { - $self->debug("Alerts response looks good."); - # We just asked for the settings this time - foreach my $device (@{$thermoparams->{thermostatList}}) { - # Save the previous alerts - $$self{prev_data}{devices}{$device->{identifier}}{alertsHash} = dclone $$self{data}{devices}{$device->{identifier}}{alertsHash}; - # Look for events that have been acked and are no longer in the array - foreach my $key (keys %{$$self{data}{devices}{$device->{identifier}}{alertsHash}}) { - my $matched = 0; - foreach my $index (@{$device->{alerts}}) { - if ($key eq $index->{acknowledgeRef}) { - $matched = 1; - } - } - if (!$matched) { - # Alert has been acked - $self->debug("Alert $key: (\"" . $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}{text} . "\") has been acked."); - delete $$self{data}{devices}{$device->{identifier}}{alertsHash}{$key}; - } - } - foreach my $index (@{$device->{alerts}}) { - if (defined $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}}) { - # Do we need to see if something has changed? All of the alert properties should be static and the alert dissappears from the JSON array once acked. - } else { - # This is a new alert - $self->debug("A new alert " . $index->{acknowledgeRef} . ": (\"" . $index->{text} . "\") has been generated."); - $$self{data}{devices}{$device->{identifier}}{alertsHash}{$index->{acknowledgeRef}} = $index; - } - } - # Compare the old with the new - #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{alertsHash}, $$self{prev_data}{devices}{$device->{identifier}}{alertsHash}, $$self{monitor}{alertsHash} ); - } - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the alerts request" ); + $self->debug("Alerts response looks good."); + + # We just asked for the settings this time + foreach my $device ( @{ $thermoparams->{thermostatList} } ) { + + # Save the previous alerts + $$self{prev_data}{devices}{ $device->{identifier} }{alertsHash} = + dclone $$self{data}{devices}{ $device->{identifier} }{alertsHash}; + + # Look for events that have been acked and are no longer in the array + foreach my $key ( + keys + %{ $$self{data}{devices}{ $device->{identifier} }{alertsHash} } + ) + { + my $matched = 0; + foreach my $index ( @{ $device->{alerts} } ) { + if ( $key eq $index->{acknowledgeRef} ) { + $matched = 1; + } + } + if ( !$matched ) { + + # Alert has been acked + $self->debug( "Alert $key: (\"" + . $$self{data}{devices}{ $device->{identifier} } + {alertsHash}{$key}{text} + . "\") has been acked." ); + delete $$self{data}{devices}{ $device->{identifier} } + {alertsHash}{$key}; + } + } + foreach my $index ( @{ $device->{alerts} } ) { + if ( + defined $$self{data}{devices}{ $device->{identifier} } + {alertsHash}{ $index->{acknowledgeRef} } ) + { + # Do we need to see if something has changed? All of the alert properties should be static and the alert dissappears from the JSON array once acked. + } + else { + # This is a new alert + $self->debug( "A new alert " + . $index->{acknowledgeRef} . ": (\"" + . $index->{text} + . "\") has been generated." ); + $$self{data}{devices}{ $device->{identifier} }{alertsHash} + { $index->{acknowledgeRef} } = $index; + } + } + + # Compare the old with the new + #$self->compare_data( $$self{data}{devices}{$device->{identifier}}{alertsHash}, $$self{prev_data}{devices}{$device->{identifier}}{alertsHash}, $$self{monitor}{alertsHash} ); + } + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the alerts request"); } } - =item C<_get_groups()> Gets the group and grouping data for thermostats @@ -544,24 +720,27 @@ sub _get_groups { my ($self) = @_; $self->debug("Getting groups..."); my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} - ); + ); my $json_body = '{"selection":{"selectionType":"registered"}}'; - my ($isSuccessResponse1, $groupparams) = $self->_get_JSON_data("GET", "group", - '?format=json&body=' . uri_escape($json_body), $headers); + my ( $isSuccessResponse1, $groupparams ) = + $self->_get_JSON_data( "GET", "group", + '?format=json&body=' . uri_escape($json_body), $headers ); if ($isSuccessResponse1) { - $self->debug("Groups response looks good."); - foreach my $group (@{$groupparams->{groups}}) { - # groupRef is the unique ID - $$self{data}{groups}{$group->{groupRef}} = $group; - } - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the groups request" ); + $self->debug("Groups response looks good."); + foreach my $group ( @{ $groupparams->{groups} } ) { + + # groupRef is the unique ID + $$self{data}{groups}{ $group->{groupRef} } = $group; + } + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the groups request"); } } - =item C<_get_runtime_with_sensors()> Gets the runtime and sensor data @@ -572,67 +751,151 @@ sub _get_runtime_with_sensors { my ($self) = @_; $self->debug("Getting runtime and sensor data..."); my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} - ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":"true","includeSensors":"true"}}'; - my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostat", - '?format=json&body=' . uri_escape($json_body), $headers); + ); + my $json_body = + '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":"true","includeSensors":"true"}}'; + my ( $isSuccessResponse1, $thermoparams ) = + $self->_get_JSON_data( "GET", "thermostat", + '?format=json&body=' . uri_escape($json_body), $headers ); if ($isSuccessResponse1) { - $self->debug("Runtime response looks good."); - - foreach my $device (@{$thermoparams->{thermostatList}}) { - # Save the previous runtime - $$self{prev_data}{devices}{$device->{identifier}}{runtime} = dclone $$self{data}{devices}{$device->{identifier}}{runtime}; - # we need to inspect the runtime and remoteSensors - foreach my $key (keys %{$device->{runtime}}) { - if ($device->{runtime}{$key} ne $$self{data}{devices}{$device->{identifier}}{runtime}{$key}) { - main::print_log( "[Ecobee]: runtime parameter " . $key . " has changed from " . $$self{data}{devices}{$device->{identifier}}{runtime}{$key} . " to " . $device->{runtime}{$key}); - } - $$self{data}{devices}{$device->{identifier}}{runtime}{$key} = $device->{runtime}{$key}; - } - # Compare the old with the new - #main::print_log( "[Ecobee]: runtime monitor -pre-"); - #print "*** Object *** \n"; - #print Data::Dumper::Dumper( \$self->{monitor}); - #print "*** Object *** \n"; - $self->compare_data( $$self{data}{devices}{$device->{identifier}}{runtime}, $$self{prev_data}{devices}{$device->{identifier}}{runtime}, $$self{monitor}{$device->{identifier}}{runtime} ); - - # Save the previous remoteSensorsHash - $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash} = dclone $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}; - foreach my $index (@{$device->{remoteSensors}}) { - # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} = $index->{id}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} = $index->{name}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{type} = $index->{type}; - if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse}) { - if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} ne $index->{inUse}) { - $self->debug($$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} . " to " . $index->{inUse} ); - } - } - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{inUse} = $index->{inUse}; - foreach my $capability (@{$index->{capability}}) { - # capabilities can vary by sensor type - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{id} = $capability->{id}; - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{type} = $capability->{type}; - if (defined $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value}) { - if ($$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} ne $capability->{value}) { - $self->debug($$self{data}{devices}{$device->{identifier}}{name} . ", sensor " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{name} . ", capability " . $capability->{type} . " (id " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{id} . ":" . $capability->{id} . ") has changed from " . $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} . " to " . $capability->{value} ); + $self->debug("Runtime response looks good."); + + foreach my $device ( @{ $thermoparams->{thermostatList} } ) { + + # Save the previous runtime + $$self{prev_data}{devices}{ $device->{identifier} }{runtime} = + dclone $$self{data}{devices}{ $device->{identifier} }{runtime}; + + # we need to inspect the runtime and remoteSensors + foreach my $key ( keys %{ $device->{runtime} } ) { + if ( $device->{runtime}{$key} ne + $$self{data}{devices}{ $device->{identifier} }{runtime} + {$key} ) + { + main::print_log( "[Ecobee]: runtime parameter " + . $key + . " has changed from " + . $$self{data}{devices}{ $device->{identifier} } + {runtime}{$key} . " to " + . $device->{runtime}{$key} ); + } + $$self{data}{devices}{ $device->{identifier} }{runtime}{$key} = + $device->{runtime}{$key}; + } + + # Compare the old with the new + #main::print_log( "[Ecobee]: runtime monitor -pre-"); + #print "*** Object *** \n"; + #print Data::Dumper::Dumper( \$self->{monitor}); + #print "*** Object *** \n"; + $self->compare_data( + $$self{data}{devices}{ $device->{identifier} }{runtime}, + $$self{prev_data}{devices}{ $device->{identifier} }{runtime}, + $$self{monitor}{ $device->{identifier} }{runtime} + ); + + # Save the previous remoteSensorsHash + $$self{prev_data}{devices}{ $device->{identifier} } + {remoteSensorsHash} = + dclone $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}; + foreach my $index ( @{ $device->{remoteSensors} } ) { + + # Since this is an array, the sensor order can vary. We need to save these into hashes so they can be indexed by ID + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{id} = $index->{id}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{name} = $index->{name}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{type} = $index->{type}; + if ( + defined $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{inUse} ) + { + if ( $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{inUse} ne + $index->{inUse} ) + { + $self->debug( + $$self{data}{devices}{ $device->{identifier} }{name} + . ", sensor " + . $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{name} + . " (id " + . $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{id} + . ") has changed from " + . $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{inUse} + . " to " + . $index->{inUse} ); + } + } + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{inUse} = $index->{inUse}; + foreach my $capability ( @{ $index->{capability} } ) { + + # capabilities can vary by sensor type + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{id} = $capability->{id}; + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{type} = $capability->{type}; + if ( + defined $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{value} ) + { + if ( $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{value} ne + $capability->{value} ) + { + $self->debug( + $$self{data}{devices}{ $device->{identifier} } + {name} + . ", sensor " + . $$self{data}{devices} + { $device->{identifier} }{remoteSensorsHash} + { $index->{id} }{name} + . ", capability " + . $capability->{type} . " (id " + . $$self{data}{devices} + { $device->{identifier} }{remoteSensorsHash} + { $index->{id} }{id} . ":" + . $capability->{id} + . ") has changed from " + . $$self{data}{devices} + { $device->{identifier} }{remoteSensorsHash} + { $index->{id} }{capability} + { $capability->{id} }{value} . " to " + . $capability->{value} ); + } } - } - $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}{capability}{$capability->{id}}{value} = $capability->{value}; - } - # Compare the old with the new, but we need to handle this differently since it is nested (a child of a child) - # $self->compare_data( $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{monitor}{remoteSensorsHash} ); - } - $self->debug($$self{data}{devices}{$device->{identifier}}{name} . " ID is " . $$self{data}{devices}{$device->{identifier}}{identifier} ); - } - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the runtime and sensor request" ); + $$self{data}{devices}{ $device->{identifier} } + {remoteSensorsHash}{ $index->{id} }{capability} + { $capability->{id} }{value} = $capability->{value}; + } + + # Compare the old with the new, but we need to handle this differently since it is nested (a child of a child) + # $self->compare_data( $$self{data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{prev_data}{devices}{$device->{identifier}}{remoteSensorsHash}{$index->{id}}, $$self{monitor}{remoteSensorsHash} ); + } + $self->debug( $$self{data}{devices}{ $device->{identifier} }{name} + . " ID is " + . $$self{data}{devices}{ $device->{identifier} }{identifier} + ); + } + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the runtime and sensor request" + ); } } - =item C<_thermostat_summary()> Collects the revision numbers from each device on the Ecobee account. Changes to the revision number tell us that something @@ -644,209 +907,284 @@ sub _thermostat_summary { my ($self) = @_; $self->debug("Getting thermostat summary..."); my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{access_token} - ); - my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":"","includeEquipmentStatus":true}}'; - my ($isSuccessResponse1, $thermoparams) = $self->_get_JSON_data("GET", "thermostatSummary", - '?json=' . uri_escape($json_body), $headers); + ); + my $json_body = + '{"selection":{"selectionType":"registered","selectionMatch":"","includeEquipmentStatus":true}}'; + my ( $isSuccessResponse1, $thermoparams ) = + $self->_get_JSON_data( "GET", "thermostatSummary", + '?json=' . uri_escape($json_body), $headers ); if ($isSuccessResponse1) { - $self->debug("Thermostat response looks good. Found " . $thermoparams->{thermostatCount} . " thermostats"); - foreach my $device (@{$thermoparams->{revisionList}}) { - # format: - # identifier:name:connected:thermoRev:alertsRev:runtimeRev:intervalRev - # Example line: - # 123456789101:MyStat:true:071223012334:080102000000:080102000000 - my @values = split(':',$device); - $$self{data}{devices}{$values[0]}{identifier} = $values[0]; - $$self{data}{devices}{$values[0]}{name} = $values[1]; - - if (defined $$self{data}{devices}{$values[0]}{connected}) { - if ($$self{data}{devices}{$values[0]}{connected} ne $values[2]) { - # This tells us if we are connected to the Ecobee servers - $self->debug("connected has changed from " . $$self{data}{devices}{$values[0]}{connected} . " to " . $values[2] ); - } - } - $$self{data}{devices}{$values[0]}{connected} = $values[2]; - - if (defined $$self{data}{devices}{$values[0]}{thermoRev}) { - if ($$self{data}{devices}{$values[0]}{thermoRev} != $values[3]) { - # This tells us that the thermostat program, hvac mode, settings or configuration has changed - $self->debug("thermoRev has changed from " . $$self{data}{devices}{$values[0]}{thermoRev} . " to " . $values[3] ); - $self->_get_settings(); - } - } - $$self{data}{devices}{$values[0]}{thermoRev} = $values[3]; - - if (defined $$self{data}{devices}{$values[0]}{alertsRev}) { - if ($$self{data}{devices}{$values[0]}{alertsRev} != $values[4]) { - # This tells us of a new alert is issued or an alert is modified (acked) - $self->debug("alertsRev has changed from " . $$self{data}{devices}{$values[0]}{alertsRev} . " to " . $values[4] ); - $self->_get_alerts(); - } - } - $$self{data}{devices}{$values[0]}{alertsRev} = $values[4]; - - if (defined $$self{data}{devices}{$values[0]}{runtimeRev}) { - if ($$self{data}{devices}{$values[0]}{runtimeRev} != $values[5]) { - # This tells us when the thermostat has sent a new status message, or the equipment state or remote sensor readings have changed - $self->debug("runtimeRev has changed from " . $$self{data}{devices}{$values[0]}{runtimeRev} . " to " . $values[5] ); - $self->_get_runtime_with_sensors(); - } - } - $$self{data}{devices}{$values[0]}{runtimeRev} = $values[5]; - - if (defined $$self{data}{devices}{$values[0]}{intervalRev}) { - if ($$self{data}{devices}{$values[0]}{intervalRev} != $values[6]) { - # This tells us that the thermostat has sent a new status message (every 15 minutes) - $self->debug("intervalRev has changed from " . $$self{data}{devices}{$values[0]}{intervalRev} . " to " . $values[6] ); - } - } - $$self{data}{devices}{$values[0]}{intervalRev} = $values[6]; - - #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " ID is " . $$self{data}{devices}{$values[0]}{identifier} . - # " connected is " . $$self{data}{devices}{$values[0]}{connected} ); - } - # update the status for each device - foreach my $device (@{$thermoparams->{statusList}}) { - my %default_status = ( - 'heatPump' => 0, - 'heatPump2' => 0, - 'heatPump3' => 0, - 'compCool1' => 0, - 'compCool2' => 0, - 'auxHeat1' => 0, - 'auxHeat2' => 0, - 'auxHeat3' => 0, - 'fan' => 0, - 'humidifier' => 0, - 'dehumidifier' => 0, - 'ventilator' => 0, - 'economizer' => 0, - 'compHotWater' => 0, - 'auxHotWater' => 0 - ); - my $status_bit_LUT = { - 'heatPump' => 0x0001, - 'heatPump2' => 0x0002, - 'heatPump3' => 0x0004, - 'compCool1' => 0x0008, - 'compCool2' => 0x0010, - 'auxHeat1' => 0x0020, - 'auxHeat2' => 0x0040, - 'auxHeat3' => 0x0080, - 'fan' => 0x0100, - 'humidifier' => 0x0200, - 'dehumidifier' => 0x0400, - 'ventilator' => 0x0800, - 'economizer' => 0x1000, - 'compHotWater' => 0x2000, - 'auxHotWater' => 0x4000 - }; - my @values = split(':',$device); - my @states; - - # populate the status vector - my $statusvec = 0x0000; - if (scalar @values > 1) { - @states = split(',',$values[1]); - foreach my $index (0..$#states) { - $statusvec |= $status_bit_LUT->{$states[$index]}; - $self->debug("Statusvec=$statusvec, key=" . $states[$index] . ", LUT=" . $status_bit_LUT->{$states[$index]}); - } - } - if (exists $$self{data}{devices}{$values[0]}{statusvec}{status}) { - $$self{prev_data}{devices}{$values[0]}{statusvec} = dclone $$self{data}{devices}{$values[0]}{statusvec}; - # Since state_now doesn't fire for values of 0, we need to create this artificial "all off" state - if ($statusvec == 0) { - $$self{data}{devices}{$values[0]}{statusvec}{status} = 0x8000; - } else { - $$self{data}{devices}{$values[0]}{statusvec}{status} = $statusvec; - } - $self->compare_data( $$self{data}{devices}{$values[0]}{statusvec}, $$self{prev_data}{devices}{$values[0]}{statusvec}, $$self{monitor}{$values[0]}{statusvec} ); - } else { - # Since state_now doesn't fire for values of 0, we need to create this artificial "all off" state - if ($statusvec == 0) { - $$self{data}{devices}{$values[0]}{statusvec}{status} = 0x8000; - } else { - $$self{data}{devices}{$values[0]}{statusvec}{status} = $statusvec; - } - } - - # we probably need to notify something if one of these changes - if (defined $$self{data}{devices}{$values[0]}{status}) { - # Save the previous status - $$self{prev_data}{devices}{$values[0]}{status} = dclone $$self{data}{devices}{$values[0]}{status}; - # This isn't our first run, so see if something has changed - my $matched; - for my $stat (keys %default_status) { - if (scalar @values > 1) { - $matched = 0; - foreach my $index (0..$#states) { - if ($states[$index] eq $stat) { - $matched = 1; - if ($$self{data}{devices}{$values[0]}{status}{$states[$index]} == 0) { - $$self{data}{devices}{$values[0]}{status}{$states[$index]} = 1; - $self->debug("Status $stat has changed from off to on"); - } - } - } - if ((!$matched) && ($$self{data}{devices}{$values[0]}{status}{$stat} == 1)) { - $self->debug("C1 Status $stat has changed from on to off"); - } - } else { - if ($$self{data}{devices}{$values[0]}{status}{$stat} != 0) { - $$self{data}{devices}{$values[0]}{status}{$stat} = 0; - $self->debug("C2 Status $stat has changed from on to off"); - } + $self->debug( "Thermostat response looks good. Found " + . $thermoparams->{thermostatCount} + . " thermostats" ); + foreach my $device ( @{ $thermoparams->{revisionList} } ) { + + # format: + # identifier:name:connected:thermoRev:alertsRev:runtimeRev:intervalRev + # Example line: + # 123456789101:MyStat:true:071223012334:080102000000:080102000000 + my @values = split( ':', $device ); + $$self{data}{devices}{ $values[0] }{identifier} = $values[0]; + $$self{data}{devices}{ $values[0] }{name} = $values[1]; + + if ( defined $$self{data}{devices}{ $values[0] }{connected} ) { + if ( $$self{data}{devices}{ $values[0] }{connected} ne + $values[2] ) + { + # This tells us if we are connected to the Ecobee servers + $self->debug( "connected has changed from " + . $$self{data}{devices}{ $values[0] }{connected} + . " to " + . $values[2] ); + } + } + $$self{data}{devices}{ $values[0] }{connected} = $values[2]; + + if ( defined $$self{data}{devices}{ $values[0] }{thermoRev} ) { + if ( $$self{data}{devices}{ $values[0] }{thermoRev} != + $values[3] ) + { + # This tells us that the thermostat program, hvac mode, settings or configuration has changed + $self->debug( "thermoRev has changed from " + . $$self{data}{devices}{ $values[0] }{thermoRev} + . " to " + . $values[3] ); + $self->_get_settings(); + } + } + $$self{data}{devices}{ $values[0] }{thermoRev} = $values[3]; + + if ( defined $$self{data}{devices}{ $values[0] }{alertsRev} ) { + if ( $$self{data}{devices}{ $values[0] }{alertsRev} != + $values[4] ) + { + # This tells us of a new alert is issued or an alert is modified (acked) + $self->debug( "alertsRev has changed from " + . $$self{data}{devices}{ $values[0] }{alertsRev} + . " to " + . $values[4] ); + $self->_get_alerts(); + } + } + $$self{data}{devices}{ $values[0] }{alertsRev} = $values[4]; + + if ( defined $$self{data}{devices}{ $values[0] }{runtimeRev} ) { + if ( $$self{data}{devices}{ $values[0] }{runtimeRev} != + $values[5] ) + { + # This tells us when the thermostat has sent a new status message, or the equipment state or remote sensor readings have changed + $self->debug( "runtimeRev has changed from " + . $$self{data}{devices}{ $values[0] }{runtimeRev} + . " to " + . $values[5] ); + $self->_get_runtime_with_sensors(); + } + } + $$self{data}{devices}{ $values[0] }{runtimeRev} = $values[5]; + + if ( defined $$self{data}{devices}{ $values[0] }{intervalRev} ) { + if ( $$self{data}{devices}{ $values[0] }{intervalRev} != + $values[6] ) + { + # This tells us that the thermostat has sent a new status message (every 15 minutes) + $self->debug( "intervalRev has changed from " + . $$self{data}{devices}{ $values[0] }{intervalRev} + . " to " + . $values[6] ); + } + } + $$self{data}{devices}{ $values[0] }{intervalRev} = $values[6]; + + #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " ID is " . $$self{data}{devices}{$values[0]}{identifier} . + # " connected is " . $$self{data}{devices}{$values[0]}{connected} ); + } + + # update the status for each device + foreach my $device ( @{ $thermoparams->{statusList} } ) { + my %default_status = ( + 'heatPump' => 0, + 'heatPump2' => 0, + 'heatPump3' => 0, + 'compCool1' => 0, + 'compCool2' => 0, + 'auxHeat1' => 0, + 'auxHeat2' => 0, + 'auxHeat3' => 0, + 'fan' => 0, + 'humidifier' => 0, + 'dehumidifier' => 0, + 'ventilator' => 0, + 'economizer' => 0, + 'compHotWater' => 0, + 'auxHotWater' => 0 + ); + my $status_bit_LUT = { + 'heatPump' => 0x0001, + 'heatPump2' => 0x0002, + 'heatPump3' => 0x0004, + 'compCool1' => 0x0008, + 'compCool2' => 0x0010, + 'auxHeat1' => 0x0020, + 'auxHeat2' => 0x0040, + 'auxHeat3' => 0x0080, + 'fan' => 0x0100, + 'humidifier' => 0x0200, + 'dehumidifier' => 0x0400, + 'ventilator' => 0x0800, + 'economizer' => 0x1000, + 'compHotWater' => 0x2000, + 'auxHotWater' => 0x4000 + }; + my @values = split( ':', $device ); + my @states; + + # populate the status vector + my $statusvec = 0x0000; + if ( scalar @values > 1 ) { + @states = split( ',', $values[1] ); + foreach my $index ( 0 .. $#states ) { + $statusvec |= $status_bit_LUT->{ $states[$index] }; + $self->debug( "Statusvec=$statusvec, key=" + . $states[$index] + . ", LUT=" + . $status_bit_LUT->{ $states[$index] } ); + } + } + if ( exists $$self{data}{devices}{ $values[0] }{statusvec}{status} ) + { + $$self{prev_data}{devices}{ $values[0] }{statusvec} = + dclone $$self{data}{devices}{ $values[0] }{statusvec}; + + # Since state_now doesn't fire for values of 0, we need to create this artificial "all off" state + if ( $statusvec == 0 ) { + $$self{data}{devices}{ $values[0] }{statusvec}{status} = + 0x8000; + } + else { + $$self{data}{devices}{ $values[0] }{statusvec}{status} = + $statusvec; + } + $self->compare_data( + $$self{data}{devices}{ $values[0] }{statusvec}, + $$self{prev_data}{devices}{ $values[0] }{statusvec}, + $$self{monitor}{ $values[0] }{statusvec} + ); + } + else { + # Since state_now doesn't fire for values of 0, we need to create this artificial "all off" state + if ( $statusvec == 0 ) { + $$self{data}{devices}{ $values[0] }{statusvec}{status} = + 0x8000; + } + else { + $$self{data}{devices}{ $values[0] }{statusvec}{status} = + $statusvec; + } + } + + # we probably need to notify something if one of these changes + if ( defined $$self{data}{devices}{ $values[0] }{status} ) { + + # Save the previous status + $$self{prev_data}{devices}{ $values[0] }{status} = + dclone $$self{data}{devices}{ $values[0] }{status}; + + # This isn't our first run, so see if something has changed + my $matched; + for my $stat ( keys %default_status ) { + if ( scalar @values > 1 ) { + $matched = 0; + foreach my $index ( 0 .. $#states ) { + if ( $states[$index] eq $stat ) { + $matched = 1; + if ( $$self{data}{devices}{ $values[0] }{status} + { $states[$index] } == 0 ) + { + $$self{data}{devices}{ $values[0] }{status} + { $states[$index] } = 1; + $self->debug( + "Status $stat has changed from off to on" + ); + } + } + } + if ( + ( !$matched ) + && ( $$self{data}{devices}{ $values[0] }{status} + {$stat} == 1 ) + ) + { + $self->debug( + "C1 Status $stat has changed from on to off"); + } + } + else { + if ( $$self{data}{devices}{ $values[0] }{status}{$stat} + != 0 ) + { + $$self{data}{devices}{ $values[0] }{status}{$stat} + = 0; + $self->debug( + "C2 Status $stat has changed from on to off"); + } + } + } + + # Compare the old with the new. This may not work as-is. We also need to consider the initial value. + #$self->compare_data( $$self{data}{devices}{$values[0]}{status}, $$self{prev_data}{devices}{$values[0]}{status}, $$self{monitor}{status} ); + } + else { + $$self{data}{devices}{ $values[0] }{status} = \%default_status; + if ( scalar @values > 1 ) { + foreach my $index ( 0 .. $#states ) { + $$self{data}{devices}{ $values[0] }{status} + { $states[$index] } = 1; + } } - } - # Compare the old with the new. This may not work as-is. We also need to consider the initial value. - #$self->compare_data( $$self{data}{devices}{$values[0]}{status}, $$self{prev_data}{devices}{$values[0]}{status}, $$self{monitor}{status} ); - } else { - $$self{data}{devices}{$values[0]}{status} = \%default_status; - if (scalar @values > 1) { - foreach my $index (0..$#states) { - $$self{data}{devices}{$values[0]}{status}{$states[$index]} = 1; - } - } - } - #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " fan is " . $$self{data}{devices}{$values[0]}{status}{fan} ); - } - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" ); + } + + #main::print_log( "[Ecobee]: " . $$self{data}{devices}{$values[0]}{name} . " fan is " . $$self{data}{devices}{$values[0]}{status}{fan} ); + } + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the thermostat list request" + ); } } - sub _poll { my ($self) = @_; - if ($$self{ready}) { + if ( $$self{ready} ) { $self->_thermostat_summary(); } + # reset the timer my $action = sub { $self->_poll() }; - $$self{polling_timer}->set($$self{poll_interval}, $action); + $$self{polling_timer}->set( $$self{poll_interval}, $action ); } #------------------------------------------------------------------------------------ sub _get_JSON_data { - my ( $self, $type, $endpoint, $args, $headers, $content) = @_; + my ( $self, $type, $endpoint, $args, $headers, $content ) = @_; my $ua = new LWP::UserAgent(); $ua->timeout( $$self{timeout} ); my $url = $$self{url}; - my $request = HTTP::Request->new( $type, $url . $rest{$endpoint} . $args, $headers ); + my $request = + HTTP::Request->new( $type, $url . $rest{$endpoint} . $args, $headers ); $request->content($content) if defined $content; - $self->debug("Full request ->" . $request->as_string . "<-"); + $self->debug( "Full request ->" . $request->as_string . "<-" ); my $responseObj = $ua->request($request); - $self->debug($responseObj->content . "\n--------------------"); + $self->debug( $responseObj->content . "\n--------------------" ); my $responseCode = $responseObj->code; - $self->debug("Response code: " . $responseCode); + $self->debug( "Response code: " . $responseCode ); my $isSuccessResponse = $responseCode < 400; my $response; @@ -859,31 +1197,44 @@ sub _get_JSON_data { } else { if ( !$isSuccessResponse ) { - if (($endpoint eq "token") && ($responseCode == 401)) { - # Don't bother printing an error as this is expected, because the user hasn't entered the PIN yet - } elsif (($responseCode == 500) && (defined $response->{status}->{code})) { - if ($response->{status}->{code} == 14) { + if ( ( $endpoint eq "token" ) && ( $responseCode == 401 ) ) { + + # Don't bother printing an error as this is expected, because the user hasn't entered the PIN yet + } + elsif (( $responseCode == 500 ) + && ( defined $response->{status}->{code} ) ) + { + if ( $response->{status}->{code} == 14 ) { + # Our tokens have expired, we must refresh them and try again $self->_refresh_tokens(); + # Update the token in the header first - $headers->header('Authorization' => 'Bearer ' . $$self{access_token}); - return $self->_get_JSON_data($type, $endpoint, $args, $headers); - } else { - main::print_log( "[Ecobee]: Warning, failed to get data. Response code $responseCode, error code " . $response->{status}->{code}); + $headers->header( + 'Authorization' => 'Bearer ' . $$self{access_token} ); + return $self->_get_JSON_data( $type, $endpoint, $args, + $headers ); } - } else { - main::print_log( "[Ecobee]: Warning, failed to get data. Response code $responseCode"); - } + else { + main::print_log( + "[Ecobee]: Warning, failed to get data. Response code $responseCode, error code " + . $response->{status}->{code} ); + } + } + else { + main::print_log( + "[Ecobee]: Warning, failed to get data. Response code $responseCode" + ); + } } else { - $self->debug("Got a valid response."); + $self->debug("Got a valid response."); } return ( $isSuccessResponse, $response ); } } - =item C Used to register actions to be run if a specific value changes. @@ -898,36 +1249,39 @@ Used to register actions to be run if a specific value changes. sub register { my ( $self, $parent, $value ) = @_; + #main::print_log( "[Ecobee]: interface registering value $value"); push( @{ $$self{register} }, [ $parent, $value ] ); } - # Walk through the data hash and looks for changes from previous hash if a # change is found, looks for children to notify and notifies them. # This needs to be enhanced a bit from the original Nest version as we are also dealing -# with remote sensors that have the same set of properties that are associated with -# the same thermostat and the parameter names are not globally unique. As such, the -# monitor hash structure will be provided as a nested hash that is traversed with the -# data hash. Unlike the Nest, different data comes in at different times and would false -# trigger if run at the top level of the data hash each time as it would compare previously +# with remote sensors that have the same set of properties that are associated with +# the same thermostat and the parameter names are not globally unique. As such, the +# monitor hash structure will be provided as a nested hash that is traversed with the +# data hash. Unlike the Nest, different data comes in at different times and would false +# trigger if run at the top level of the data hash each time as it would compare previously # evaluated data each run sub compare_data { my ( $self, $data, $prev_data, $monitor_hash ) = @_; $self->debug("Starting execution within compare_data()"); while ( my ( $key, $value ) = each %{$data} ) { + # Use empty hash reference is it doesn't exist my $prev_value = {}; $prev_value = $$prev_data{$key} if exists $$prev_data{$key}; my $monitor_value = {}; $monitor_value = $$monitor_hash{$key} if exists $$monitor_hash{$key}; + #main::print_log( "[Ecobee]: key is $key, value is $value, prev_value is $prev_value, monitor_value is $monitor_value"); - if ( ref $value eq 'HASH') { + if ( ref $value eq 'HASH' ) { $self->compare_data( $value, $prev_value, $monitor_value ); } - elsif ( ($value ne $prev_value) && (ref $monitor_value eq 'ARRAY') ) { + elsif ( ( $value ne $prev_value ) && ( ref $monitor_value eq 'ARRAY' ) ) + { for my $action ( @{$monitor_value} ) { $self->debug("I am running action for key $key, value $value"); &$action( $key, $value ); @@ -936,44 +1290,45 @@ sub compare_data { } } -# Populate the monitor_hash with device IDs and monitor values in the register for each device +# Populate the monitor_hash with device IDs and monitor values in the register for each device sub populate_monitor_hash { my ($self) = @_; for my $array_ref ( @{ $$self{register} } ) { my ( $parent, $value ) = @{$array_ref}; my $device_id = $parent->device_id(); - if ( $$parent{type} eq 'sensor') { + if ( $$parent{type} eq 'sensor' ) { my $sensor_id = $parent->sensor_id(); - $$self{monitor}{$device_id}{$sensor_id} = $self->_merge($value, $$self{monitor}{$device_id}{$sensor_id}); + $$self{monitor}{$device_id}{$sensor_id} = + $self->_merge( $value, $$self{monitor}{$device_id}{$sensor_id} ); } else { - $$self{monitor}{$device_id} = $self->_merge($value, $$self{monitor}{$device_id}); + $$self{monitor}{$device_id} = + $self->_merge( $value, $$self{monitor}{$device_id} ); } } delete $$self{register}; } - # Merge the source into the dest. This is used to populate the monitor hash. sub _merge { - my ($self,$source,$dest) = @_; - for my $key (keys %{$source}) { - if ('ARRAY' eq ref $dest->{$key}) { - push @{$dest->{$key}}, $source->{$key}; - } elsif ('HASH' eq ref $dest->{$key}) { - $self->_merge($source->{$key},$dest->{$key}); - } else { + my ( $self, $source, $dest ) = @_; + for my $key ( keys %{$source} ) { + if ( 'ARRAY' eq ref $dest->{$key} ) { + push @{ $dest->{$key} }, $source->{$key}; + } + elsif ( 'HASH' eq ref $dest->{$key} ) { + $self->_merge( $source->{$key}, $dest->{$key} ); + } + else { $dest->{$key} = $source->{$key}; } } return $dest; } - #------------ # User access methods - =item C Prints the name and id of all devices found in the Ecobee account. @@ -983,13 +1338,15 @@ Prints the name and id of all devices found in the Ecobee account. sub print_devices { my ($self) = @_; my $output = "The list of devices reported by Ecobee is:\n"; - foreach my $key (keys %{ $$self{data}{devices} } ) { - $output .= " Name:" . $$self{data}{devices}{$key}{name} . " ID: " . $$self{data}{devices}{$key}{identifier} . "\n"; + foreach my $key ( keys %{ $$self{data}{devices} } ) { + $output .= + " Name:" + . $$self{data}{devices}{$key}{name} . " ID: " + . $$self{data}{devices}{$key}{identifier} . "\n"; } $self->debug($output); } - =item C Returns the temperature of the named temperature sensor registered with the given device (thermostat) @@ -997,33 +1354,41 @@ Returns the temperature of the named temperature sensor registered with the give =cut sub get_temp { - my ($self,$device,$name) = @_; + my ( $self, $device, $name ) = @_; + # Get the id of the given device my $d_id; - foreach my $key (keys %{$$self{data}{devices}}) { - if ($$self{data}{devices}{$key}{name} eq $device) { - $d_id = $key; - last; - } + foreach my $key ( keys %{ $$self{data}{devices} } ) { + if ( $$self{data}{devices}{$key}{name} eq $device ) { + $d_id = $key; + last; + } } if ($d_id) { - if ($name eq "actualTemperature") { - # This is a special case where we return the runtime actualTemperature property instead of a sensor value - return $$self{data}{devices}{$d_id}{runtime}{actualTemperature}; - } else { - foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { - if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { - return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{1}{value}; - } - } - return 0; - } - } else { - return 0; + if ( $name eq "actualTemperature" ) { + + # This is a special case where we return the runtime actualTemperature property instead of a sensor value + return $$self{data}{devices}{$d_id}{runtime}{actualTemperature}; + } + else { + foreach my $key ( + keys %{ $$self{data}{devices}{$d_id}{remoteSensorsHash} } ) + { + if ( $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} + eq $name ) + { + return $$self{data}{devices}{$d_id}{remoteSensorsHash} + {$key}{capability}{1}{value}; + } + } + return 0; + } + } + else { + return 0; } } - =item C Returns the humidity of the named sensor registered with the given device (thermostat). @@ -1032,38 +1397,49 @@ Currently only the main thermostat device, not the remote sensors have humidity =cut sub get_humidity { - my ($self,$device,$name) = @_; + my ( $self, $device, $name ) = @_; + # Get the id of the given device my $d_id; - foreach my $key (keys %{$$self{data}{devices}}) { - if ($$self{data}{devices}{$key}{name} eq $device) { - $d_id = $key; - last; - } + foreach my $key ( keys %{ $$self{data}{devices} } ) { + if ( $$self{data}{devices}{$key}{name} eq $device ) { + $d_id = $key; + last; + } } if ($d_id) { - if ($name eq "actualHumidity") { - # This is a special case where we return the runtime actualHumidity property instead of a sensor value - return $$self{data}{devices}{$d_id}{runtime}{actualHumidity}; - } else { - foreach my $key (keys %{$$self{data}{devices}{$d_id}{remoteSensorsHash}}) { - if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} eq $name) { - if ($$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{type} eq "humidity") { - return $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{capability}{2}{value}; - } else { - # This sensor type doesn't have a humidity sensor. Only the main thermostat unit does - return 0; + if ( $name eq "actualHumidity" ) { + + # This is a special case where we return the runtime actualHumidity property instead of a sensor value + return $$self{data}{devices}{$d_id}{runtime}{actualHumidity}; + } + else { + foreach my $key ( + keys %{ $$self{data}{devices}{$d_id}{remoteSensorsHash} } ) + { + if ( $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key}{name} + eq $name ) + { + if ( $$self{data}{devices}{$d_id}{remoteSensorsHash}{$key} + {capability}{2}{type} eq "humidity" ) + { + return $$self{data}{devices}{$d_id}{remoteSensorsHash} + {$key}{capability}{2}{value}; + } + else { + # This sensor type doesn't have a humidity sensor. Only the main thermostat unit does + return 0; + } } - } - } - return 0; - } - } else { - return 0; + } + return 0; + } + } + else { + return 0; } } - =item C Returns the given runtime property for desiredX comfort setting. Known properties are: @@ -1077,27 +1453,31 @@ desiredFanMode => FanMode =cut sub get_desired_comfort { - my ($self,$device,$name) = @_; + my ( $self, $device, $name ) = @_; + # Get the id of the given device my $d_id; - foreach my $key (keys %{$$self{data}{devices}}) { - if ($$self{data}{devices}{$key}{name} eq $device) { - $d_id = $key; - last; - } + foreach my $key ( keys %{ $$self{data}{devices} } ) { + if ( $$self{data}{devices}{$key}{name} eq $device ) { + $d_id = $key; + last; + } } if ($d_id) { - if (defined $$self{data}{devices}{$d_id}{runtime}{"desired" . $name}) { - return $$self{data}{devices}{$d_id}{runtime}{"desired" . $name}; - } else { - return 0; - } - } else { - return 0; + if ( + defined $$self{data}{devices}{$d_id}{runtime}{ "desired" . $name } ) + { + return $$self{data}{devices}{$d_id}{runtime}{ "desired" . $name }; + } + else { + return 0; + } + } + else { + return 0; } } - =item C Returns the given setting property. @@ -1105,27 +1485,29 @@ Returns the given setting property. =cut sub get_setting { - my ($self,$device,$setting) = @_; + my ( $self, $device, $setting ) = @_; + # Get the id of the given device my $d_id; - foreach my $key (keys %{$$self{data}{devices}}) { - if ($$self{data}{devices}{$key}{name} eq $device) { - $d_id = $key; - last; - } + foreach my $key ( keys %{ $$self{data}{devices} } ) { + if ( $$self{data}{devices}{$key}{name} eq $device ) { + $d_id = $key; + last; + } } if ($d_id) { - if (defined $$self{data}{devices}{$d_id}{settings}{$setting}) { - return $$self{data}{devices}{$d_id}{settings}{$setting}; - } else { - return 0; - } - } else { - return 0; + if ( defined $$self{data}{devices}{$d_id}{settings}{$setting} ) { + return $$self{data}{devices}{$d_id}{settings}{$setting}; + } + else { + return 0; + } + } + else { + return 0; } } - =item C Returns either the given alert by the given $id, or all of them if $id is undefined @@ -1133,33 +1515,36 @@ Returns either the given alert by the given $id, or all of them if $id is undefi =cut sub get_alert { - my ($self,$device,$id) = @_; + my ( $self, $device, $id ) = @_; + # Get the id of the given device my $d_id; - foreach my $key (keys %{$$self{data}{devices}}) { - if ($$self{data}{devices}{$key}{name} eq $device) { - $d_id = $key; - last; - } + foreach my $key ( keys %{ $$self{data}{devices} } ) { + if ( $$self{data}{devices}{$key}{name} eq $device ) { + $d_id = $key; + last; + } } if ($d_id) { - if (defined $id) { - if (defined $$self{data}{devices}{$d_id}{alertsHash}{$id}) { - return $$self{data}{devices}{$d_id}{alertsHash}{$id}; - } else { - # Normal return is a hashref, so this is probably unwise - return 0; - } - } else { - return $$self{data}{devices}{$d_id}{alertsHash}; - } - } else { - # Normal return is a hashref, so this is probably unwise - return 0; + if ( defined $id ) { + if ( defined $$self{data}{devices}{$d_id}{alertsHash}{$id} ) { + return $$self{data}{devices}{$d_id}{alertsHash}{$id}; + } + else { + # Normal return is a hashref, so this is probably unwise + return 0; + } + } + else { + return $$self{data}{devices}{$d_id}{alertsHash}; + } + } + else { + # Normal return is a hashref, so this is probably unwise + return 0; } } - =item C Returns either the given event by the given $id, or all of them if $id is undefined @@ -1167,38 +1552,39 @@ Returns either the given event by the given $id, or all of them if $id is undefi =cut sub get_event { - my ($self,$device,$id) = @_; + my ( $self, $device, $id ) = @_; + # Get the id of the given device my $d_id; - foreach my $key (keys %{$$self{data}{devices}}) { - if ($$self{data}{devices}{$key}{name} eq $device) { - $d_id = $key; - last; - } + foreach my $key ( keys %{ $$self{data}{devices} } ) { + if ( $$self{data}{devices}{$key}{name} eq $device ) { + $d_id = $key; + last; + } } if ($d_id) { - if (defined $id) { - if (defined $$self{data}{devices}{$d_id}{eventsHash}{$id}) { - return $$self{data}{devices}{$d_id}{eventsHash}{$id}; - } else { - # Normal return is a hashref, so this is probably unwise - return 0; - } - } else { - return $$self{data}{devices}{$d_id}{eventsHash}; - } - } else { - # Normal return is a hashref, so this is probably unwise - return 0; + if ( defined $id ) { + if ( defined $$self{data}{devices}{$d_id}{eventsHash}{$id} ) { + return $$self{data}{devices}{$d_id}{eventsHash}{$id}; + } + else { + # Normal return is a hashref, so this is probably unwise + return 0; + } + } + else { + return $$self{data}{devices}{$d_id}{eventsHash}; + } + } + else { + # Normal return is a hashref, so this is probably unwise + return 0; } } - - #------------ # User control methods - package Ecobee_Generic; =back @@ -1249,11 +1635,10 @@ sub new { $$self{parent} = $self if ( $$self{parent} eq '' ); my $monitor_value = $self->_delve($monitor_hash); - $$self{interface}->register( $$self{parent}, $monitor_value); + $$self{interface}->register( $$self{parent}, $monitor_value ); return $self; } - =item C<_delve()> Internal function to help populate the monitor_hash with action functions @@ -1261,13 +1646,15 @@ Internal function to help populate the monitor_hash with action functions =cut sub _delve { - my ( $self, $datahash) = @_; - while (my ($key, $value) = each %{$datahash}) { - if (ref $value eq 'HASH') { + my ( $self, $datahash ) = @_; + while ( my ( $key, $value ) = each %{$datahash} ) { + if ( ref $value eq 'HASH' ) { + # We need to go deeper - $self->_delve($datahash->{$key}); - } else { - $value = [sub { $self->data_changed(@_); }] if $value eq ''; + $self->_delve( $datahash->{$key} ); + } + else { + $value = [ sub { $self->data_changed(@_); } ] if $value eq ''; $datahash->{$key} = $value; } } @@ -1285,7 +1672,7 @@ More sophisticated children can hijack this method to do more complex tasks. sub data_changed { my ( $self, $value_name, $new_value ) = @_; my ( $setby, $response ); - $self->debug( "Data changed called $value_name, $new_value"); + $self->debug("Data changed called $value_name, $new_value"); if ( defined $$self{parent}{state_pending}{$value_name} ) { ( $setby, $response ) = @{ $$self{parent}{state_pending}{$value_name} }; delete $$self{parent}{state_pending}{$value_name}; @@ -1296,7 +1683,6 @@ sub data_changed { $self->set_receive( $new_value, $setby, $response ); } - =item C Handles setting the state of the object inside MisterHouse @@ -1308,7 +1694,6 @@ sub set_receive { $self->SUPER::set( $p_state, $p_setby, $p_response ); } - =item C Returns the device_id of an object. @@ -1319,16 +1704,18 @@ sub device_id { my ($self) = @_; my $type_hash; my $parent = $$self{parent}; - for my $device_id ( keys %{$$self{interface}{data}{devices}} ) { - if ( $$parent{name} eq $$self{interface}{data}{devices}{$device_id}{name} ) { + for my $device_id ( keys %{ $$self{interface}{data}{devices} } ) { + if ( $$parent{name} eq + $$self{interface}{data}{devices}{$device_id}{name} ) + { return $device_id; } } - $self->debug("ERROR, no device by the name " . $$parent{name} . " was found." ); + $self->debug( + "ERROR, no device by the name " . $$parent{name} . " was found." ); return 0; } - =item C Returns the sensor_id of an object. @@ -1339,20 +1726,34 @@ sub sensor_id { my ($self) = @_; my $type_hash; my $parent = $$self{parent}; - for my $device_id ( keys %{$$self{interface}{data}{devices}} ) { - if ( $$parent{name} eq $$self{interface}{data}{devices}{$device_id}{name} ) { - foreach my $sensor_id ( keys %{$$self{interface}{data}{devices}{$device_id}{remoteSensorsHash}} ) { - if ($$parent{sensor_name} eq $$self{interface}{data}{devices}{$device_id}{remoteSensorsHash}{$sensor_id}{name}) { + for my $device_id ( keys %{ $$self{interface}{data}{devices} } ) { + if ( $$parent{name} eq + $$self{interface}{data}{devices}{$device_id}{name} ) + { + foreach my $sensor_id ( + keys %{ + $$self{interface}{data}{devices}{$device_id} + {remoteSensorsHash} + } + ) + { + if ( $$parent{sensor_name} eq + $$self{interface}{data}{devices}{$device_id} + {remoteSensorsHash}{$sensor_id}{name} ) + { return $sensor_id; } } } } - $self->debug("ERROR, no sensor by the name " . $$parent{sensor_name} . " was found on device " . $$parent{name} . "." ); + $self->debug( "ERROR, no sensor by the name " + . $$parent{sensor_name} + . " was found on device " + . $$parent{name} + . "." ); return 0; } - =item C Returns the data contained in value for this device. @@ -1362,15 +1763,16 @@ Returns the data contained in value for this device. sub get_value { my ( $self, $value ) = @_; my $device_id = $self->device_id; - if (defined $$self{interface}{data}{devices}{$device_id}{$value}) { + if ( defined $$self{interface}{data}{devices}{$device_id}{$value} ) { return $$self{interface}{data}{devices}{$device_id}{$value}; - } else { - $self->debug("ERROR, no value for $value on device $device_id was found." ); + } + else { + $self->debug( + "ERROR, no value for $value on device $device_id was found."); return 0; } } - package Ecobee_Thermostat; =back @@ -1434,13 +1836,12 @@ sub new { $monitor_value->{runtime}{actualTemperature} = ''; my $self = new Ecobee_Generic( $interface, '', $monitor_value ); bless $self, $class; - $$self{class} = 'devices', - $$self{type} = 'thermostats', - $$self{name} = $name; + $$self{class} = 'devices', + $$self{type} = 'thermostats', + $$self{name} = $name; return $self; } - =item C Returns the current ambient temperature. @@ -1449,12 +1850,12 @@ Returns the current ambient temperature. sub get_temp { my ($self) = @_; - my $runtime = $self->get_value( "runtime"); # This returns a hashref with all the runtime properties - #$self->debug("The actualTemperature is " . $runtime->{actualTemperature} ); + my $runtime = $self->get_value("runtime") + ; # This returns a hashref with all the runtime properties + #$self->debug("The actualTemperature is " . $runtime->{actualTemperature} ); return $runtime->{actualTemperature}; } - =item C Returns the current events. @@ -1463,10 +1864,12 @@ Returns the current events. sub get_events { my ($self) = @_; - my $eventshash = $self->get_value( "eventsHash"); # This returns a hashref with all the events (including holds) - if (scalar keys %{$eventshash} > 0) { + my $eventshash = $self->get_value("eventsHash") + ; # This returns a hashref with all the events (including holds) + if ( scalar keys %{$eventshash} > 0 ) { return $eventshash; - } else { + } + else { return; } } @@ -1479,12 +1882,11 @@ Returns the current programs. sub get_programs { my ($self) = @_; - my $programs = $self->get_value("program"); # This returns a hashref with all the programs + my $programs = $self->get_value("program") + ; # This returns a hashref with all the programs return $programs; } - - =item C Sets the mode to $state, must be [heat,auxHeatOnly,cool,auto,off] @@ -1493,37 +1895,52 @@ Sets the mode to $state, must be [heat,auxHeatOnly,cool,auto,off] sub set_hvac_mode { my ( $self, $state, $p_setby, $p_response ) = @_; - main::print_log( "[Ecobee]: Attempting to set the thermostat mode to $state" ); + main::print_log( + "[Ecobee]: Attempting to set the thermostat mode to $state"); $$self{interface}{polling_timer}->pause; $state = lc($state); if ( $state ne 'heat' && $state ne 'auxHeatOnly' && $state ne 'cool' && $state ne 'auto' - && $state ne 'off' ) { - $self->debug("set_hvac_mode must be one of: heat, auxHeatOnly, cool, auto, or off. Not $state."); + && $state ne 'off' ) + { + $self->debug( + "set_hvac_mode must be one of: heat, auxHeatOnly, cool, auto, or off. Not $state." + ); return; } $$self{state_pending}{hvacMode} = [ $p_setby, $p_response ]; + # Send the new mode to the API my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{interface}{access_token} - ); + ); + # Note: this will change all thermostats on the account to this mode. The selection needs to be more specific to control just one. - #my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":""},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; + #my $json_body = '{"selection":{"selectionType":"registered","selectionMatch":""},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; # Note: This will only change the specific device (or devices if more than one is given in CSV format) - my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"thermostat":{"settings":{"hvacMode":"' . $state . '"}}}'; - my ($isSuccessResponse1, $modeparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); + my $json_body = + '{"selection":{"selectionType":"thermostats","selectionMatch":"' + . $self->device_id + . '"},"thermostat":{"settings":{"hvacMode":"' + . $state . '"}}}'; + my ( $isSuccessResponse1, $modeparams ) = + $$self{interface} + ->_get_JSON_data( "POST", "thermostat", "?format=json", $headers, + $json_body ); if ($isSuccessResponse1) { - $self->debug("Mode change response looks good" ); - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the mode change request" ); + $self->debug("Mode change response looks good"); + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the mode change request" + ); } $$self{interface}{polling_timer}->resume; } - =item C Sets a hold for the properties defined in $state. $state format can be either a temperature hold or a @@ -1534,16 +1951,34 @@ climate holds are in the format climate__ sub set_hold { my ( $self, $state, $p_setby, $p_response ) = @_; - $self->debug("Attempting to set a thermostat hold to $state" ); - my @s_params = split('_', $state); + $self->debug("Attempting to set a thermostat hold to $state"); + my @s_params = split( '_', $state ); my $json_body; - if (($s_params[0] eq 'climate') && (scalar @s_params == 3)) { + if ( ( $s_params[0] eq 'climate' ) && ( scalar @s_params == 3 ) ) { + # climate hold - $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"setHold","params":{"holdType":"' . $s_params[1] . '","holdClimateRef":' . $s_params[2] . '}}]}'; - } elsif (($s_params[0] eq 'temperature') && (scalar @s_params == 4)) { + $json_body = + '{"selection":{"selectionType":"thermostats","selectionMatch":"' + . $self->device_id + . '"},"functions": [{"type":"setHold","params":{"holdType":"' + . $s_params[1] + . '","holdClimateRef":' + . $s_params[2] . '}}]}'; + } + elsif ( ( $s_params[0] eq 'temperature' ) && ( scalar @s_params == 4 ) ) { + # temperature hold - $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"setHold","params":{"holdType":"' . $s_params[1] . '","heatHoldTemp":' . $s_params[2] . ',"coolHoldTemp":' . $s_params[3] . '}}]}'; - } else { + $json_body = + '{"selection":{"selectionType":"thermostats","selectionMatch":"' + . $self->device_id + . '"},"functions": [{"type":"setHold","params":{"holdType":"' + . $s_params[1] + . '","heatHoldTemp":' + . $s_params[2] + . ',"coolHoldTemp":' + . $s_params[3] . '}}]}'; + } + else { # format is wrong $self->debug("set_hold state \"$state\" is invalid"); return; @@ -1552,19 +1987,24 @@ sub set_hold { $$self{state_pending}{hold} = [ $p_setby, $p_response ]; $$self{interface}{polling_timer}->pause; my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{interface}{access_token} - ); - my ($isSuccessResponse1, $holdparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); + ); + my ( $isSuccessResponse1, $holdparams ) = + $$self{interface} + ->_get_JSON_data( "POST", "thermostat", "?format=json", $headers, + $json_body ); if ($isSuccessResponse1) { - $self->debug("Set hold response looks good" ); - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the set hold request" ); + $self->debug("Set hold response looks good"); + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the set hold request" + ); } $$self{interface}{polling_timer}->resume; } - =item C Clears the current thermostat hold. @@ -1576,20 +2016,28 @@ sub clear_hold { $self->debug("Attempting to clear thermostat hold"); $$self{interface}{polling_timer}->pause; my $headers = HTTP::Headers->new( - 'Content-Type' => 'text/json', + 'Content-Type' => 'text/json', 'Authorization' => 'Bearer ' . $$self{interface}{access_token} - ); - my $json_body = '{"selection":{"selectionType":"thermostats","selectionMatch":"' . $self->device_id . '"},"functions": [{"type":"resumeProgram","params":{"resumeAll":false}}]}'; - my ($isSuccessResponse1, $holdparams) = $$self{interface}->_get_JSON_data("POST", "thermostat", "?format=json", $headers, $json_body); + ); + my $json_body = + '{"selection":{"selectionType":"thermostats","selectionMatch":"' + . $self->device_id + . '"},"functions": [{"type":"resumeProgram","params":{"resumeAll":false}}]}'; + my ( $isSuccessResponse1, $holdparams ) = + $$self{interface} + ->_get_JSON_data( "POST", "thermostat", "?format=json", $headers, + $json_body ); if ($isSuccessResponse1) { $self->debug("Clear hold response looks good"); - } else { - main::print_log( "[Ecobee]: Uh, oh... Something went wrong with the clear hold request" ); + } + else { + main::print_log( + "[Ecobee]: Uh, oh... Something went wrong with the clear hold request" + ); } $$self{interface}{polling_timer}->resume; } - package Ecobee_Thermo_Humidity; =head1 B @@ -1615,7 +2063,6 @@ C =cut - use strict; @Ecobee_Thermo_Humidity::ISA = ('Ecobee_Generic'); @@ -1624,12 +2071,12 @@ sub new { my ( $class, $parent ) = @_; my $monitor_value; $monitor_value->{runtime}{actualHumidity} = ''; - my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + my $self = + new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); bless $self, $class; return $self; } - package Ecobee_Thermo_HVAC_Status; =head1 B @@ -1664,12 +2111,12 @@ sub new { my ( $class, $parent ) = @_; my $monitor_value; $monitor_value->{statusvec}{status} = ''; - my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + my $self = + new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); bless $self, $class; return $self; } - package Ecobee_Thermo_Mode; =head1 B @@ -1704,7 +2151,8 @@ sub new { my ( $class, $parent ) = @_; my $monitor_value; $monitor_value->{settings}{hvacMode} = ''; - my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + my $self = + new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); $$self{states} = [ 'heat', 'auxHeatOnly', 'cool', 'auto', 'off' ]; bless $self, $class; return $self; @@ -1716,7 +2164,6 @@ sub set { $$self{parent}->set_hvac_mode( $p_state, $p_setby, $p_response ); } - package Ecobee_Thermo_Climate; =head1 B @@ -1750,46 +2197,55 @@ use strict; sub new { my ( $class, $parent ) = @_; my $monitor_value; - $monitor_value->{program}{currentClimateRef} = ''; + $monitor_value->{program}{currentClimateRef} = ''; $monitor_value->{eventsHash}{'hold-auto-home'}{holdClimateRef} = ''; $monitor_value->{eventsHash}{'hold-auto-away'}{holdClimateRef} = ''; - $monitor_value->{eventsHash}{'none'}{holdClimateRef} = ''; - my $self = new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); + $monitor_value->{eventsHash}{'none'}{holdClimateRef} = ''; + my $self = + new Ecobee_Generic( $$parent{interface}, $parent, $monitor_value ); bless $self, $class; return $self; } -# Holds with a holdClimateRef override the value in currentClimateRef +# Holds with a holdClimateRef override the value in currentClimateRef sub data_changed { my ( $self, $value_name, $new_value ) = @_; - $self->debug( "Data changed called $value_name, $new_value"); + $self->debug("Data changed called $value_name, $new_value"); my $state = ''; - if ($value_name eq 'holdClimateRef') { - if ($new_value eq 'none') { + if ( $value_name eq 'holdClimateRef' ) { + if ( $new_value eq 'none' ) { + # A hold ws cleared, so we need to set the value back to the currentClimateRef my $programs = $$self{parent}->get_programs; $state = $programs->{currentClimateRef}; - } else { + } + else { $state = $new_value; } - } else { + } + else { # We need to check if there are any active holds with a holdClimateRef before changing the state my $events = $$self{parent}->get_events; - if (!exists $events->{'none'}) { - main::print_log( "[Ecobee]: Not setting the state to $new_value because there is still an active hold" ); + if ( !exists $events->{'none'} ) { + main::print_log( + "[Ecobee]: Not setting the state to $new_value because there is still an active hold" + ); return; - } else { + } + else { $state = $new_value; } } - if ($self->{state} ne $state) { - $self->set_receive($state, $$self{parent}{interface}); - } else { - $self->debug( "Not setting the state to $state because that is already the current value" ); + if ( $self->{state} ne $state ) { + $self->set_receive( $state, $$self{parent}{interface} ); + } + else { + $self->debug( + "Not setting the state to $state because that is already the current value" + ); } } - =back =head1 AUTHOR From 1f41417bc8afea7b4005d8f3badc6137a5751899 Mon Sep 17 00:00:00 2001 From: rudybrian Date: Thu, 29 Dec 2016 11:24:30 -0800 Subject: [PATCH 078/209] added support for lower case content-length (as used by Postman) This was causing the POST body to be ignored. --- lib/http_server.pl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index a52c5bace..ce03d8df2 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -58,12 +58,12 @@ 'wmlsc' => 'application/vnd.wap.wmlscriptc', 'wrl' => 'x-world/x-vrml', 'json' => 'application/json', - 'svg' => 'image/svg+xml', - 'otf' => 'application/font-sfnt', - 'ttf' => 'application/font-sfnt', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - 'eot' => 'application/vnd.ms-fontobject', + 'svg' => 'image/svg+xml', + 'otf' => 'application/font-sfnt', + 'ttf' => 'application/font-sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + 'eot' => 'application/vnd.ms-fontobject', ); my ( %http_dirs, %html_icons, $html_info_overlib, %password_protect_dirs, @@ -332,8 +332,10 @@ sub http_process_request { . "A=$Authorized format=$Http{format} ua=$Http{'User-Agent'} h=$header" if $main::Debug{http}; if ( $req_typ eq "POST" || $req_typ eq "PUT" ) { - my $cl = $Http{'Content-Length'} - || $Http{'Content-length'}; # Netscape uses lower case l + my $cl = + $Http{'Content-Length'} + || $Http{'Content-length'} + || $Http{'content-length'}; # Netscape uses lower case l print "http POST query has $cl bytes of args\n"; # if $main::Debug{http}; my $buf; From 817222fdf57ae5d1c98e35c4ef8a1f305553a7b5 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sun, 1 Jan 2017 15:15:23 -0600 Subject: [PATCH 079/209] Alexa Echo and Google Home bridge emulating the Hue api --- bin/mh | 3 + lib/AlexaBridge.pm | 582 +++++++++++++++++++++++++++++++++++++++++++++ lib/http_server.pl | 6 + 3 files changed, 591 insertions(+) create mode 100644 lib/AlexaBridge.pm diff --git a/bin/mh b/bin/mh index 0e85956c4..e5b6f209c 100755 --- a/bin/mh +++ b/bin/mh @@ -807,6 +807,7 @@ sub setup { use EIB_Items; use EIB_Device; use ajax; + use AlexaBridge; eval "use BSC"; # Base_Items @@ -976,6 +977,7 @@ sub setup { &socket_open($port_name); } + &AlexaBridge::startup; # Start the AlexaBridge sockets, in lib/AlexaBridge.pm &xAP::startup; # Start the xAP sockets, in lib/xAP_Items.pm &xPL::startup; # Start the xPL sockets, in lib/xPL_Items.pm &EIB_Device::startup; # Start the EIB device, in lib/EIB_Device.pm @@ -2921,6 +2923,7 @@ sub check_for_socket_data { { ( my $from_port, my $from_ip ) = sockaddr_in($from_saddr) if $from_saddr; + $Socket_Ports{$port_name}{from_ipport} = $from_saddr; $Socket_Ports{$port_name}{from_port} = $from_port; $Socket_Ports{$port_name}{from_ip} = $from_ip; } diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm new file mode 100644 index 000000000..bfb9670b9 --- /dev/null +++ b/lib/AlexaBridge.pm @@ -0,0 +1,582 @@ +package AlexaBridge; + +@AlexaBridge::ISA = ('Generic_Item'); + +use Carp; +use IO::Socket::INET; +use Socket; +use IO::Socket::Multicast; + + + +use constant SSDP_IP => "239.255.255.250"; +use constant SSDP_PORT => 1900; +use constant CRLF => "\015\012"; + +use constant DEFAULT_HTTP_PORT => 8085; +use constant DEFAULT_LEASE_TIME => 1800; +use constant DEFAULT_NOTIFICATION_PORT => 50000; +use constant DEFAULT_PORT_COUNT => 0; + +my ($ssdpNotificationName, $ssdpListenName, $AlexaGlobal); + +sub startup { + unless ($::config_parms{'alexa_enable'}) { return } + &open_port(); + &::MainLoop_pre_add_hook( \&AlexaBridge::check_for_data, 1 ); +} + +sub open_port { + + my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || DEFAULT_PORT_COUNT; + for my $count (0..$AlexaHttpPortCount) { + my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || DEFAULT_HTTP_PORT; + $AlexaHttpPort = ($AlexaHttpPort + $count); + my $AlexaHttpName = 'alexaServer'.$count; + &http_ports($AlexaHttpName, $AlexaHttpPort); + $AlexaGlobal->{http_sockets}->{$AlexaHttpName} = new Socket_Item( undef, undef, $AlexaHttpName ); + &main::print_log ("Alexa open_port: p=$AlexaHttpPort pn=$AlexaHttpName s=$$AlexaHttpName\n") + if $main::Debug{alexa}; + } + + + $AlexaGlobal->{http_sender}->{'alexa_http_sender'} = new Socket_Item('alexa_http_sender', undef, $::config_parms{'http_server'}.':'.$::config_parms{'http_port'}, 'alexa_http_sender', 'tcp', 'raw'); + + + my $notificationPort = $::config_parms{'alexa_notification_port'} || DEFAULT_NOTIFICATION_PORT; + + + $ssdpNotificationName = 'alexaSsdpNotification'; + $ssdpNotificationSocket = new IO::Socket::INET->new( + Proto => 'udp', + LocalPort => $notificationPort) + || &main::print_log( "\nError: Could not start a udp alexa multicast notification sender on $notificationPort: $@\n\n" ) && return; + + setsockopt($ssdpNotificationSocket, + getprotobyname('ip'), + IP_MULTICAST_TTL, + pack 'I', 4); + $::Socket_Ports{$ssdpNotificationName}{protocol} = 'udp'; + $::Socket_Ports{$ssdpNotificationName}{datatype} = 'raw'; + $::Socket_Ports{$ssdpNotificationName}{port} = $notificationPort; + $::Socket_Ports{$ssdpNotificationName}{sock} = $ssdpNotificationSocket; + $::Socket_Ports{$ssdpNotificationName}{socka} = $ssdpNotificationSocket; # UDP ports are always "active" + $alexa_ssdp_send = new Socket_Item( undef, undef, $ssdpNotificationName ); + + printf " - creating %-15s on %3s %5s %s\n", $ssdpNotificationName, 'udp', $notificationPort; + &main::print_log ("Alexa open_port: p=$notificationPort pn=$ssdpNotificationName s=$alexa_ssdp_send\n") + if $main::Debug{alexa}; + + + $ssdpListenName = 'alexaSsdpListen'; + $ssdpListenSocket = new IO::Socket::Multicast->new( + LocalPort => SSDP_PORT, + Proto => 'udp', + Reuse => 1) + || &main::print_log( "\nError: Could not start a udp alexa multicast listen server on ". SSDP_PORT .$@ ."\n\n" ) && return; + $ssdpListenSocket->mcast_add(SSDP_IP); + $::Socket_Ports{$ssdpListenName}{protocol} = 'udp'; + $::Socket_Ports{$ssdpListenName}{datatype} = 'raw'; + $::Socket_Ports{$ssdpListenName}{port} = SSDP_PORT; + $::Socket_Ports{$ssdpListenName}{sock} = $ssdpListenSocket; + $::Socket_Ports{$ssdpListenName}{socka} = $ssdpListenSocket; # UDP ports are always "active" + $alexa_ssdp_listen = new Socket_Item( undef, undef, $ssdpListenName ); + + printf " - creating %-15s on %3s %5s %s\n", $ssdpListenName, 'udp', SSDP_PORT; + &main::print_log ("Alexa open_port: p=$ssdpPort pn=$ssdpListenName s=$alexa_ssdp_listen\n") + if $main::Debug{alexa}; + + return 1; +} + + +sub http_ports { + my ( $AlexaHttpName, $AlexaHttpPort ) = @_; + my $AlexaHttpSocket = new IO::Socket::INET->new( + Proto => 'tcp', + LocalPort => $AlexaHttpPort, + Reuse => 1, + Listen => 10) + || &main::print_log( "\nError: Could not start a tcp $AlexaHttpName on $AlexaHttpPort: $@\n\n" ) && return; + + $::Socket_Ports{$AlexaHttpName}{protocol} = 'tcp'; + $::Socket_Ports{$AlexaHttpName}{datatype} = 'raw'; + $::Socket_Ports{$AlexaHttpName}{port} = $AlexaHttpPort; + $::Socket_Ports{$AlexaHttpName}{sock} = $AlexaHttpSocket; + $::Socket_Ports{$AlexaHttpName}{socka} = $AlexaHttpSocket; + printf " - creating %-15s on %3s %5s %s\n", $AlexaHttpName, 'tcp', $AlexaHttpPort; +} + +sub check_for_data { + my $alexa_http_sender = $AlexaGlobal->{http_sender}->{'alexa_http_sender'}; + #foreach my $socketName ( keys %{$AlexaGlobal->{http_sockets}} ) { + my $socketName = 'alexaServer0'; + my $alexa_listen = $AlexaGlobal->{http_sockets}{$socketName}; + if ( $alexa_listen && ( my $alexa_data = said $alexa_listen ) ) { + #&main::print_log( "[Alexa] Info: Data - $alexa_data" ); + $alexa_http_sender->start unless $alexa_http_sender->active; + $alexa_http_sender->set($alexa_data); + + } + + if ( $alexa_http_sender && ( my $alexa_sender_data = said $alexa_http_sender ) ) { + $alexa_listen->set($alexa_sender_data); + # $alexa_http_sender->stop; + } + # } + + + if ( $alexa_ssdp_listen && ( my $ssdp_data = said $alexa_ssdp_listen) ) { + my $peer = $::Socket_Ports{$ssdpListenName}{from_ipport}; + &_receiveSSDPEvent($ssdp_data, $peer); + } +} + +sub _receiveSSDPEvent { + my ( $buf, $peer ) = @_; + + + if ($buf !~ /\015?\012\015?\012/) { + return; + } + + $buf =~ s/^(?:\015?\012)+//; # ignore leading blank lines + if (!($buf =~ s/^(\S+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\012]*\012//)) { + # Bad header + return; + } + + my $method = $1; + if ($method ne 'M-SEARCH') { + # We only care about searches + return; + } + + my $target; + if ( $buf =~ /ST: urn:Belkin:device:\*\*.*/ ) { &_sendSearchResponse($peer) } + elsif ( $buf =~ /ST: urn:schemas-upnp-org:device:basic:1.*/ ) { &_sendSearchResponse($peer) } +} + + + +sub _sendSearchResponse { + my $peer = shift; + my $count = 0; + my $selfname = (&main::list_objects_by_type('AlexaBridge'))[0]; + my $self = ::get_object_by_name($selfname); + + foreach my $port ( (sort keys %{$self->{child}->{'ports'}}) ) { + next unless ( $self->{child}->{$port} ); + my $output = "HTTP/1.1 200 OK\r\n"; + $output .= 'Location: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; + $output .= 'OPT: '."\"http://schemas.upnp.org/upnp/1/0/\"\; ns\=01"."\r\n"; + $output .= '01-NLS: D1710C33-328D-4152-A5FA-5382541A92FF'."\r\n"; + $output .= 'USN: uuid:Socket-1_0-221438K0100073::urn:Belkin:device:**'."\r\n"; + $output .= 'Cache-control: max-age=86400'."\r\n"; + $output .= 'ST: urn:schemas-upnp-org:device:basic:1'."\r\n"; + $output .= 'EXT: '."\r\n"; + $output .= "\r\n"; + my $socket = handle $alexa_ssdp_send; + + send($socket, $output, 0, $peer); + $count++; + } +} + +sub process_http { + + unless ($::config_parms{'alexa_enable'}) { return 0 } + my ( $uri, $request_type, $host, $body, $socket ) = @_; + + unless ( ($uri =~ /^\/upnp\//) || ($uri =~ /^\/api\//) ) { return 0 } # Added for performance + + my $selfname = (&main::list_objects_by_type('AlexaBridge'))[0]; + my $self = ::get_object_by_name($selfname); + unless ($self) { &main::print_log( "[Alexa] Error: No AlexaBridge parent object found" ); return 0 } + + use HTTP::Date qw(time2str); + + #get the port from the host header + my @uris = split(/\//, $uri); + my $port; + if ( $host =~ /(.*):(\d+)/ ) { + $host = $1; + $port = $2; + } + + +my $xmlmessage = qq[ + + +1 +0 + +http://$::config_parms{'alexaHttpIp'}:$port/ + +urn:schemas-upnp-org:device:basic:1 +Amazon-Echo-MH-Bridge (192.168.195.37) +Royal Philips Electronics +http://misterhouse.sourceforge.net/ +Hue Emulator for Amazon Echo bridge +Philips hue bridge 2012 +929000226503 +https://github.com/hollie/misterhouse +amazon-mh-bridge0 +uuid:amazon-mh-bridge0 + + +(null) +(null) +(null) +(null) +(null) + + +index.html + + +image/png +48 +48 +24 +hue_logo_0.png + + +image/png +120 +120 +24 +hue_logo_3.png + + + +]; + + +my $AlexaObjects; + if ( $self->{child}->{$port} ) { + # use Data::Dumper; + $AlexaObjects = $self->{child}->{$port}; + #&main::print_log( Data::Dumper->Dumper($AlexaObjects) ); + } + else { + &main::print_log( "[Alexa] Error: No Matching object for port ( $port )" ); + $output = "HTTP/1.1 404 Not Found\r\n"; + return $output; + } + +&main::print_log ("[Alexa] Debug: Port: ( $port ) URI: ( $uri ) Body: ( $body ) Type: ( $request_type ) \n") if $main::Debug{'alexa'}; + + if ( ($uri =~ /^\/upnp\/.*\/setup.xml$/) && (lc($request_type) eq "get") ) { + my $output = "HTTP/1.1 200 OK\r\n"; + $output .= "Server: MisterHouse\r\n"; + $output .= 'Access-Control-Allow-Origin: *'."\r\n"; + $output .= 'Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT'."\r\n"; + $output .= 'Access-Control-Max-Age: 3600'."\r\n"; + $output .= 'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'."\r\n"; + $output .= 'X-Application-Context: application'."\r\n"; + $output .= 'Content-Type: application/xml;charset=UTF-8'."\r\n"; + $output .= "Content-Length: ". (length $xmlmessage) ."\r\n"; + $output .= "Date: ". time2str(time)."\r\n"; + $output .= "\r\n"; + $output .= $xmlmessage; + return $output; + } + elsif ( ($uri =~ /^\/api\/$/) && (lc($request_type) eq "post") ) { + my $content = qq[\[{"success":{"username":"lights"}}\]]; + my $output = "HTTP/1.1 200 OK\r\n"; + $output .= "Server: MisterHouse\r\n"; + $output .= 'Access-Control-Allow-Origin: *'."\r\n"; + $output .= 'Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT'."\r\n"; + $output .= 'Access-Control-Max-Age: 3600'."\r\n"; + $output .= 'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'."\r\n"; + $output .= 'X-Application-Context: application'."\r\n"; + $output .= 'Content-Type: application/json;charset=UTF-8'."\r\n"; + $output .= "Content-Length: ". (length $content) ."\r\n"; + $output .= "Date: ". time2str(time)."\r\n"; + $output .= "\r\n"; + $output .= $content; + return $output; + } + elsif ( ($uri =~ /^\/api\/.*\/lights\/(.*)\/state$/) && (lc($request_type) eq "put") ) { + my $output; + my $deviceID = $1; + my $state = undef; + if ( $body =~ /\"(on)\": (true)/ ) { $state = 'on' } + elsif ( $body =~ /\"(on)\": (false)/ ) { $state = 'off' } + elsif ( $body =~ /\"(off)\": (true)/ ) { $state = 'off' } + elsif ( $body =~ /\"(off)\": (false)/ ) { $state = 'on' } + if ( $body =~ /\"(bri)\": (\d+)/ ) { $state = $2 } + elsif ( $body =~ /\"(bri)\":(\d+)/ ) { $state = $2 } + my $content = qq[\[{"success":{"/lights/$deviceID/state/$1":$2}}\]]; + + if ( ($AlexaObjects->{'uuid'}->{$deviceID}) && (defined($state)) ) { + &get_set_state($self, $AlexaObjects, $deviceID, 'set', $state); + + $output = "HTTP/1.1 200 OK\r\n"; + $output .= "Server: MisterHouse\r\n"; + $output .= 'Access-Control-Allow-Origin: *'."\r\n"; + $output .= 'Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT'."\r\n"; + $output .= 'Access-Control-Max-Age: 3600'."\r\n"; + $output .= 'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'."\r\n"; + $output .= 'X-Application-Context: application'."\r\n"; + $output .= 'Content-Type: text/plain;charset=UTF-8'."\r\n"; + $output .= "Content-Length: ". (length $content) ."\r\n"; + $output .= "Date: ". time2str(time)."\r\n"; + $output .= "\r\n"; + $output .= $content; + } else { + $output = "HTTP/1.1 404 Not Found\r\n"; + return $output; + } + print $socket $output; # print direct to the socket so it does not close. + &main::http_process_request($socket); # we know there will be another request so get it in the same tcp session. + return ' '; + #return $output; + } + elsif ( ($uri =~ /^\/api\/.*/) && (lc($request_type) eq "get") ) { + my $count = 0; + my $content; my $name; my $statep1; my $statep2; my $statep3; my $statep4; my $delm; my $output; + my $end = ''; + if (defined $uris[4]) { + 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"}}]; + $content = $statep1.$name.$statep2; + $count = 1; + } + elsif ( ($uris[3] eq 'groups') && ($AlexaObjects->{'groups'}->{$uris[4]}) ) { + $name = $AlexaObjects->{'groups'}->{$uris[4]}->{'name'}; + $content = qq[{"action": {"on": true,"hue": 0,"effect": "none","bri": 100,"sat": 100,"ct": 500,"xy": \[0.5, 0.5\]},"lights": \["1","2"\],"state":{"any_on":true,"all_on":true}"type":"Room","class":"Other","name":"$name"}]; + $count = 1; + } + + } + elsif (defined $uris[3]) { + if ( $uris[3] eq 'lights' ) { + $statep1 = qq[{"]; + $statep2 = qq[":"]; + $end = qq["}]; + $delm = qq[","]; + foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { + $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; + next unless $name; + if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name } + else { $content = $statep1.$uuid.$statep2.$name } + $count++; + } + } + elsif ( $uris[3] eq 'groups' ) { + $statep1 = qq[{"]; + $statep2 = qq[":"]; + $end = qq["}]; + $delm = qq[","]; + foreach my $id ( keys %{$AlexaObjects->{'groups'}} ) { + $name = $AlexaObjects->{'groups'}->{$id}->{'name'}; + next unless $name; + $statep1 = qq[{"$id": {"name": "$name","lights": \["1","2"\],"type": "LightGroup","action": {"on": true,"bri": 254,"hue": 10000,"sat": 254,"effect": "none","xy": \[0.5,0.5\],"ct": 250,"alert": "select","colormode": "ct"}}]; + $delim = qq[,]; + $statep2 = qq["$id": {"name": "$name","lights": \["3","4"\],"type": "LightGroup","action": {"on": true,"bri": 153,"hue": 4345,"sat": 254,"effect": "none","xy": \[0.5,0.5\],"ct": 250,"alert": "select","colormode": "ct"}}]; + $end = qq[}]; + if ($count >= 1) { $content = $content.$delim.$statep2 } + else { $content = $statep1 } + $count++; + } + } + } + elsif (defined $uris[2]) { + $statep1 = qq[{"lights":{"]; + #$statep2 = qq[":{"state":{"on":false,"bri":254,"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[":{"state":{"on":false,"bri":254,"reachable":true},"type":"Extended color light","name":"]; # dis + #$statep2 = qq[":{"state":{"on":false,"bri":254,"hue":15823,"sat":88,"effect":"none","ct":313,"alert":"none","colormode":"ct","reachable":true},"type":"Extended color light","name":"]; + #$statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","uniqueid":"]; + $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; # + #$statep4 = qq[","swversion":"65003148","pointsymbol":{"1":"none","2":"none","3":"none","4":"none","5":"none","6":"none","7":"none","8":"none"}}]; + $end = qq[}}]; + $delm = qq[,"]; + foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { + $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; + next unless $name; + #if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3.$uuid.$statep4 } + #else { $content = $statep1.$uuid.$statep2.$name.$statep3.$uuid.$statep4 } + if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3 } + else { $content = $statep1.$uuid.$statep2.$name.$statep3 } + $count++; + } + } + if ($count >= 1) { + $content = $content.$end; + $output = "HTTP/1.1 200 OK\r\n"; + $output .= "Server: MisterHouse\r\n"; + $output .= 'Access-Control-Allow-Origin: *'."\r\n"; + $output .= 'Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT'."\r\n"; + $output .= 'Access-Control-Max-Age: 3600'."\r\n"; + $output .= 'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'."\r\n"; + $output .= 'X-Application-Context: application'."\r\n"; + $output .= 'Content-Type: application/json;charset=UTF-8'."\r\n"; + $output .= "Content-Length: ". (length $content) ."\r\n"; + $output .= "Date: ". time2str(time)."\r\n"; + $output .= "\r\n"; + $output .= $content; + } else { + my $output = "HTTP/1.1 404 Not Found\r\n"; + } + return $output; + } + else { return 0 } +} + +sub get_set_state { + my ( $self, $AlexaObjects, $uuid, $action, $state ) = @_; + my $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; + my $realname = $AlexaObjects->{'uuid'}->{$uuid}->{'realname'}; + my $sub = $AlexaObjects->{'uuid'}->{$uuid}->{'sub'}; + my $statesub = $AlexaObjects->{'uuid'}->{$uuid}->{'statesub'}; + $state = $AlexaObjects->{'uuid'}->{$uuid}->{$state} if $AlexaObjects->{'uuid'}->{$uuid}->{$state}; + if ( $state =~ /\d+/ ) { $state = &roundoff($state / 2.52) } + &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 ( $realname =~ /^\$/ ) { + my $object = ::get_object_by_name( $realname ); + if ( $action eq 'get' ) { + my $cstate = $object->$statesub; + $cstate =~ s/\%//; + if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":252] } + elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":252] } + elsif ( $cstate =~ /\d+/ ) { return qq["on":true,"bri":].&roundoff($cstate * 2.52) } + else { return qq["on":false,"bri":252] } + } + elsif ( $action eq 'set' ) { + + &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state )\n") if $main::Debug{'alexa'}; + $object->$sub($state); + return; + } + } + elsif ( $sub =~ /^run_voice_cmd$/ ) { + if ( $action eq 'set' ) { + $realname =~ s/#/$state/; + &main::print_log ("[Alexa] Debug: running voice command: ( $realname )\n") if $main::Debug{'alexa'}; + &main::run_voice_cmd("$realname"); + } + elsif ( $action eq 'get' ) { + return qq["on":false,"bri":252]; + } + + } + elsif ( ref($sub) eq 'CODE' ) { + &main::print_log ("[Alexa] Debug: running sub: $sub( $state ) \n") if $main::Debug{'alexa'}; + &{$sub}($state) if ($action eq 'set'); + return qq["on":false,"bri":252] if ($action eq 'get'); + } +} + +sub roundoff +{ + my $num = shift; + my $roundto = shift || 1; + + return int($num/$roundto+0.5)*$roundto; +} + +sub new { + my ($class) = @_; + my $self = new Generic_Item(); + bless $self, $class; + return $self; +} + +sub register { + my ( $self, $child ) = @_; + $self->{child} = $child; +} + +package AlexaBridge_Item; + +@AlexaBridge_Item::ISA = ('Generic_Item'); + +sub new { + my ($class, $parent) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $parent->register($self); + my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || DEFAULT_PORT_COUNT; + for my $count (0..$AlexaHttpPortCount) { + my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || DEFAULT_HTTP_PORT; + $AlexaHttpPort = ($AlexaHttpPort + $count); + $self->{'ports'}->{$AlexaHttpPort} = 0; + } + $self->{'ports'}->{$::config_parms{'http_port'}} = 0; + return $self; +} + +sub add { + my ($self, $realname, $name, $sub, $on, $off, $statesub) = @_; + + return unless defined $realname; + my $fullname; + my $cleanname = $realname; + $cleanname =~ s/\$//; + $cleanname =~ s/ //; + $cleanname =~ s/#//; + $cleanname =~ s/\\//; + $cleanname =~ s/&//; + + if ( defined($name) ) { + $fullname = $cleanname.'.'.$name; + } + else { + $fullname = $cleanname.'.'.$cleanname; + } + #use Data::Dumper; + my $uuid = $self->uuid($fullname); + + foreach my $port ( (sort keys %{$self->{'ports'}}) ) { + my $size = keys %{$self->{$port}->{'uuid'}}; + next if ($size eq 60); + $self->{$port}->{'uuid'}->{$uuid}->{'realname'}=$realname; + $self->{$port}->{'uuid'}->{$uuid}->{'name'}=$name || $cleanname; + $self->{$port}->{'uuid'}->{$uuid}->{'sub'}=$sub || 'set'; + $self->{$port}->{'uuid'}->{$uuid}->{'on'}=$on || 'on'; + $self->{$port}->{'uuid'}->{$uuid}->{'off'}=$off || 'off'; + $self->{$port}->{'uuid'}->{$uuid}->{'statesub'}=$statesub || 'state'; + last; + } + +# Testing groups, saw the Echo hit /api/odtQdwTaiTjPgURo4ZyEtGfIqRgfSeCm1fl2AMG2/groups/0 +#$self->{'groups'}->{0}->{'name'}='group0'; +#$self->{'groups'}->{0}->{'realname'}='$light0'; +#$self->{'groups'}->{0}->{'sub'}='set'; +#$self->{'groups'}->{0}->{'on'}='on'; +#$self->{'groups'}->{0}->{'off'}='off'; +#$self->{'groups'}->{1}->{'name'}='group1'; +#$self->{'groups'}->{1}->{'realname'}='$light1'; +#$self->{'groups'}->{1}->{'sub'}='set'; +#$self->{'groups'}->{1}->{'on'}='on'; +#$self->{'groups'}->{1}->{'off'}='off'; +#$self->{'groups'}->{2}->{'name'}='group2'; +#$self->{'groups'}->{2}->{'realname'}='$light2'; +#$self->{'groups'}->{2}->{'sub'}='set'; +#$self->{'groups'}->{2}->{'on'}='on'; +#$self->{'groups'}->{2}->{'off'}='off'; + #&main::print_log( Data::Dumper->Dumper($self->{'uuid'}) ); +} + +sub get_objects { + my ($self) = @_; + return $self->{'uuid'}; +} + +sub uuid { + my ($self, $name) = @_; + use Data::UUID; + $ug = Data::UUID->new; + $uuid = $ug->to_string( ( $ug->create_from_name(NameSpace_DNS, $name) ) ); + return lc($uuid); +} + +1; + diff --git a/lib/http_server.pl b/lib/http_server.pl index cf0f58624..04ee1f60c 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -7,7 +7,9 @@ use strict; use Text::ParseWords; +use AlexaBridge; require 'http_utils.pl'; +#require 'alexa_server.pl'; #use Data::Dumper; #$main::Debug{http} = 4; @@ -537,6 +539,10 @@ sub http_process_request { return; } + if ( my $alexa_response = &AlexaBridge::process_http($get_req, $req_typ, $Http{'Host'}, $HTTP_BODY, $socket) ) { + print $socket $alexa_response unless $alexa_response eq ' '; + return; + } # See if the request was for a file if ( &test_for_file( $socket, $get_req, $get_arg ) ) { } From 2824d2026f5e0ee7970dec1d6e89e64029357abe Mon Sep 17 00:00:00 2001 From: waynieack Date: Fri, 6 Jan 2017 15:06:08 -0600 Subject: [PATCH 080/209] General Code cleanup, fixed some Google Home issues, added new ini option for mac addres --- lib/AlexaBridge.pm | 209 +++++++++++++++++++++++++++++++-------------- lib/http_server.pl | 2 +- 2 files changed, 145 insertions(+), 66 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index bfb9670b9..8b8015c36 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -18,7 +18,7 @@ use constant DEFAULT_LEASE_TIME => 1800; use constant DEFAULT_NOTIFICATION_PORT => 50000; use constant DEFAULT_PORT_COUNT => 0; -my ($ssdpNotificationName, $ssdpListenName, $AlexaGlobal); +my ($AlexaGlobal); sub startup { unless ($::config_parms{'alexa_enable'}) { return } @@ -28,25 +28,27 @@ sub startup { sub open_port { - my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || DEFAULT_PORT_COUNT; - for my $count (0..$AlexaHttpPortCount) { - my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || DEFAULT_HTTP_PORT; - $AlexaHttpPort = ($AlexaHttpPort + $count); - my $AlexaHttpName = 'alexaServer'.$count; - &http_ports($AlexaHttpName, $AlexaHttpPort); - $AlexaGlobal->{http_sockets}->{$AlexaHttpName} = new Socket_Item( undef, undef, $AlexaHttpName ); - &main::print_log ("Alexa open_port: p=$AlexaHttpPort pn=$AlexaHttpName s=$$AlexaHttpName\n") + my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || DEFAULT_PORT_COUNT; + if ($AlexaHttpPortCount) { + $AlexaHttpPortCount = ($AlexaHttpPortCount - 1); + for my $count (0..$AlexaHttpPortCount) { + my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || DEFAULT_HTTP_PORT; + $AlexaHttpPort = ($AlexaHttpPort + $count); + my $AlexaHttpName = 'alexaServer'.$count; + &http_ports($AlexaHttpName, $AlexaHttpPort); + $AlexaGlobal->{http_sockets}->{$AlexaHttpName} = new Socket_Item( undef, undef, $AlexaHttpName ); + $AlexaGlobal->{http_sockets}->{$AlexaHttpName}->{port} = $AlexaHttpPort; + &main::print_log ("Alexa open_port: p=$AlexaHttpPort pn=$AlexaHttpName s=$AlexaHttpName\n") if $main::Debug{alexa}; - } - - - $AlexaGlobal->{http_sender}->{'alexa_http_sender'} = new Socket_Item('alexa_http_sender', undef, $::config_parms{'http_server'}.':'.$::config_parms{'http_port'}, 'alexa_http_sender', 'tcp', 'raw'); + } + $AlexaGlobal->{http_sender}->{'alexa_http_sender'} = new Socket_Item('alexa_http_sender', undef, $::config_parms{'http_server'}.':'.$::config_parms{'http_port'}, 'alexa_http_sender', 'tcp', 'raw'); + } my $notificationPort = $::config_parms{'alexa_notification_port'} || DEFAULT_NOTIFICATION_PORT; - $ssdpNotificationName = 'alexaSsdpNotification'; + my $ssdpNotificationName = 'alexaSsdpNotification'; $ssdpNotificationSocket = new IO::Socket::INET->new( Proto => 'udp', LocalPort => $notificationPort) @@ -61,15 +63,15 @@ sub open_port { $::Socket_Ports{$ssdpNotificationName}{port} = $notificationPort; $::Socket_Ports{$ssdpNotificationName}{sock} = $ssdpNotificationSocket; $::Socket_Ports{$ssdpNotificationName}{socka} = $ssdpNotificationSocket; # UDP ports are always "active" - $alexa_ssdp_send = new Socket_Item( undef, undef, $ssdpNotificationName ); + $AlexaGlobal->{'ssdp_send'} = new Socket_Item( undef, undef, $ssdpNotificationName ); printf " - creating %-15s on %3s %5s %s\n", $ssdpNotificationName, 'udp', $notificationPort; - &main::print_log ("Alexa open_port: p=$notificationPort pn=$ssdpNotificationName s=$alexa_ssdp_send\n") + &main::print_log ("Alexa open_port: p=$notificationPort pn=$ssdpNotificationName s=".$AlexaGlobal->{'ssdp_send'} ."\n") if $main::Debug{alexa}; - $ssdpListenName = 'alexaSsdpListen'; - $ssdpListenSocket = new IO::Socket::Multicast->new( + my $ssdpListenName = 'alexaSsdpListen'; + my $ssdpListenSocket = new IO::Socket::Multicast->new( LocalPort => SSDP_PORT, Proto => 'udp', Reuse => 1) @@ -80,10 +82,10 @@ sub open_port { $::Socket_Ports{$ssdpListenName}{port} = SSDP_PORT; $::Socket_Ports{$ssdpListenName}{sock} = $ssdpListenSocket; $::Socket_Ports{$ssdpListenName}{socka} = $ssdpListenSocket; # UDP ports are always "active" - $alexa_ssdp_listen = new Socket_Item( undef, undef, $ssdpListenName ); + $AlexaGlobal->{'ssdp_listen'} = new Socket_Item( undef, undef, $ssdpListenName ); printf " - creating %-15s on %3s %5s %s\n", $ssdpListenName, 'udp', SSDP_PORT; - &main::print_log ("Alexa open_port: p=$ssdpPort pn=$ssdpListenName s=$alexa_ssdp_listen\n") + &main::print_log ("Alexa open_port: p=$ssdpPort pn=$ssdpListenName s=" .$AlexaGlobal->{'ssdp_listen'} ."\n") if $main::Debug{alexa}; return 1; @@ -109,28 +111,36 @@ sub http_ports { sub check_for_data { my $alexa_http_sender = $AlexaGlobal->{http_sender}->{'alexa_http_sender'}; - #foreach my $socketName ( keys %{$AlexaGlobal->{http_sockets}} ) { - my $socketName = 'alexaServer0'; - my $alexa_listen = $AlexaGlobal->{http_sockets}{$socketName}; + my $alexa_ssdp_listen = $AlexaGlobal->{ssdp_listen}; + #foreach my $AlexaHttpName ( keys %{$AlexaGlobal->{http_sockets}} ) { + my $AlexaHttpName = 'alexaServer0'; + my $alexa_listen = $AlexaGlobal->{http_sockets}{$AlexaHttpName}; if ( $alexa_listen && ( my $alexa_data = said $alexa_listen ) ) { #&main::print_log( "[Alexa] Info: Data - $alexa_data" ); $alexa_http_sender->start unless $alexa_http_sender->active; $alexa_http_sender->set($alexa_data); } + &_sendHttpData($alexa_listen, $alexa_http_sender); - if ( $alexa_http_sender && ( my $alexa_sender_data = said $alexa_http_sender ) ) { - $alexa_listen->set($alexa_sender_data); - # $alexa_http_sender->stop; - } # } + + my $alexa_ssdp_listen = $AlexaGlobal->{ssdp_listen}; if ( $alexa_ssdp_listen && ( my $ssdp_data = said $alexa_ssdp_listen) ) { - my $peer = $::Socket_Ports{$ssdpListenName}{from_ipport}; + my $peer = $::Socket_Ports{'alexaSsdpListen'}{from_ipport}; &_receiveSSDPEvent($ssdp_data, $peer); } } + + +sub _sendHttpData { + my ($alexa_listen, $alexa_http_sender) = @_; + if ( $alexa_http_sender && ( my $alexa_sender_data = said $alexa_http_sender ) ) { + $alexa_listen->set($alexa_sender_data); + } +} sub _receiveSSDPEvent { my ( $buf, $peer ) = @_; @@ -155,30 +165,65 @@ sub _receiveSSDPEvent { my $target; if ( $buf =~ /ST: urn:Belkin:device:\*\*.*/ ) { &_sendSearchResponse($peer) } elsif ( $buf =~ /ST: urn:schemas-upnp-org:device:basic:1.*/ ) { &_sendSearchResponse($peer) } + elsif ( $buf =~ /ST: ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } } sub _sendSearchResponse { - my $peer = shift; + my ($peer,$type) = @_; my $count = 0; my $selfname = (&main::list_objects_by_type('AlexaBridge'))[0]; my $self = ::get_object_by_name($selfname); + my $alexa_ssdp_send = $AlexaGlobal->{'ssdp_send'}; + my $mac = $::config_parms{'alexaMac'} || '9aa645cc40aa'; + foreach my $port ( (sort keys %{$self->{child}->{'ports'}}) ) { - next unless ( $self->{child}->{$port} ); - my $output = "HTTP/1.1 200 OK\r\n"; - $output .= 'Location: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; - $output .= 'OPT: '."\"http://schemas.upnp.org/upnp/1/0/\"\; ns\=01"."\r\n"; - $output .= '01-NLS: D1710C33-328D-4152-A5FA-5382541A92FF'."\r\n"; - $output .= 'USN: uuid:Socket-1_0-221438K0100073::urn:Belkin:device:**'."\r\n"; - $output .= 'Cache-control: max-age=86400'."\r\n"; - $output .= 'ST: urn:schemas-upnp-org:device:basic:1'."\r\n"; + next unless ( $self->{child}->{$port} ); + my $socket = handle $alexa_ssdp_send; + my $output; + if ($type eq 'all') { + $output = "HTTP/1.1 200 OK\r\n"; + $output .= 'HOST: 239.255.255.250:1900'."\r\n"; + $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; - $output .= "\r\n"; - my $socket = handle $alexa_ssdp_send; - + $output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/description.xml' ."\r\n"; + #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; + $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; + $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; + $output .= 'ST: upnp:rootdevice' ."\r\n"; + $output .= 'USN: uuid:'.$mac.'::upnp:rootdevice' ."\r\n"; + $output .= "\r\n"; + send($socket, $output, 0, $peer); + + $output = "HTTP/1.1 200 OK\r\n"; + $output .= 'HOST: 239.255.255.250:1900'."\r\n"; + $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; + $output .= 'EXT: '."\r\n"; + $output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/description.xml' ."\r\n"; + #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; + $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; + $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; + $output .= 'ST: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; + $output .= 'USN: uuid:2f402f80-da50-11e1-9b23-001e06'.$mac."\r\n"; + $output .= "\r\n"; + send($socket, $output, 0, $peer); + } + + $output = "HTTP/1.1 200 OK\r\n"; + $output .= 'HOST: 239.255.255.250:1900'."\r\n"; + $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; + $output .= 'EXT: '."\r\n"; + $output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/description.xml' ."\r\n"; + #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; + $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; + $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; + $output .= 'ST: urn:schemas-upnp-org:device:basic:1'."\r\n"; + $output .= 'USN: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; + $output .= "\r\n"; send($socket, $output, 0, $peer); + $count++; } } @@ -186,25 +231,34 @@ sub _sendSearchResponse { sub process_http { unless ($::config_parms{'alexa_enable'}) { return 0 } - my ( $uri, $request_type, $host, $body, $socket ) = @_; + my ( $uri, $request_type, $body, $socket, %Http ) = @_; - unless ( ($uri =~ /^\/upnp\//) || ($uri =~ /^\/api\//) ) { return 0 } # Added for performance + unless ( ($uri =~ /^\/upnp\//) || ($uri =~ /^\/api/ ) || ($uri =~ /^\/description.xml$/) ) { return 0 } # Added for performance my $selfname = (&main::list_objects_by_type('AlexaBridge'))[0]; my $self = ::get_object_by_name($selfname); unless ($self) { &main::print_log( "[Alexa] Error: No AlexaBridge parent object found" ); return 0 } use HTTP::Date qw(time2str); + use IO::Compress::Gzip qw(gzip); #get the port from the host header my @uris = split(/\//, $uri); + my $host = $Http{'Host'}; my $port; if ( $host =~ /(.*):(\d+)/ ) { $host = $1; $port = $2; + } + elsif ( $host =~ /(\d+)/ ) { + $host = $1; + $port = '80'; } - - + elsif ( $host =~ /(\w+)/ ) { + $host = $1; + $port = '80'; + } + my $xmlmessage = qq[ @@ -261,13 +315,13 @@ my $AlexaObjects; } else { &main::print_log( "[Alexa] Error: No Matching object for port ( $port )" ); - $output = "HTTP/1.1 404 Not Found\r\n"; + $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; return $output; } &main::print_log ("[Alexa] Debug: Port: ( $port ) URI: ( $uri ) Body: ( $body ) Type: ( $request_type ) \n") if $main::Debug{'alexa'}; - if ( ($uri =~ /^\/upnp\/.*\/setup.xml$/) && (lc($request_type) eq "get") ) { + if ( ( ($uri =~ /^\/upnp\/.*\/setup.xml$/) || ($uri =~ /^\/description.xml$/) ) && (lc($request_type) eq "get") ) { my $output = "HTTP/1.1 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; $output .= 'Access-Control-Allow-Origin: *'."\r\n"; @@ -280,9 +334,10 @@ my $AlexaObjects; $output .= "Date: ". time2str(time)."\r\n"; $output .= "\r\n"; $output .= $xmlmessage; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; return $output; } - elsif ( ($uri =~ /^\/api\/$/) && (lc($request_type) eq "post") ) { + elsif ( ($uri =~ /^\/api/) && (lc($request_type) eq "post") ) { my $content = qq[\[{"success":{"username":"lights"}}\]]; my $output = "HTTP/1.1 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; @@ -296,6 +351,7 @@ my $AlexaObjects; $output .= "Date: ". time2str(time)."\r\n"; $output .= "\r\n"; $output .= $content; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; return $output; } elsif ( ($uri =~ /^\/api\/.*\/lights\/(.*)\/state$/) && (lc($request_type) eq "put") ) { @@ -326,13 +382,15 @@ my $AlexaObjects; $output .= "\r\n"; $output .= $content; } else { - $output = "HTTP/1.1 404 Not Found\r\n"; + $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; return $output; } - print $socket $output; # print direct to the socket so it does not close. - &main::http_process_request($socket); # we know there will be another request so get it in the same tcp session. - return ' '; - #return $output; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; + return $output; + #print $socket $output; # print direct to the socket so it does not close. + #&main::http_process_request($socket); # we know there will be another request so get it in the same tcp session. + #return ' '; } elsif ( ($uri =~ /^\/api\/.*/) && (lc($request_type) eq "get") ) { my $count = 0; @@ -349,12 +407,17 @@ my $AlexaObjects; $content = $statep1.$name.$statep2; $count = 1; } + elsif ( $uris[3] eq 'lights' ) { + &main::print_log("[Alexa] Error: No Matching object for UUID ( $uris[4] )"); + $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; + return $output; + } elsif ( ($uris[3] eq 'groups') && ($AlexaObjects->{'groups'}->{$uris[4]}) ) { $name = $AlexaObjects->{'groups'}->{$uris[4]}->{'name'}; $content = qq[{"action": {"on": true,"hue": 0,"effect": "none","bri": 100,"sat": 100,"ct": 500,"xy": \[0.5, 0.5\]},"lights": \["1","2"\],"state":{"any_on":true,"all_on":true}"type":"Room","class":"Other","name":"$name"}]; $count = 1; } - } elsif (defined $uris[3]) { if ( $uris[3] eq 'lights' ) { @@ -410,6 +473,7 @@ my $AlexaObjects; } if ($count >= 1) { $content = $content.$end; + $content = &_Gzip($content,$Http{'Accept-Encoding'}); $output = "HTTP/1.1 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; $output .= 'Access-Control-Allow-Origin: *'."\r\n"; @@ -418,18 +482,30 @@ my $AlexaObjects; $output .= 'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'."\r\n"; $output .= 'X-Application-Context: application'."\r\n"; $output .= 'Content-Type: application/json;charset=UTF-8'."\r\n"; + $output .= "Content-Encoding: gzip\r\n" if ($Http{'Accept-Encoding'} =~ m/gzip/); $output .= "Content-Length: ". (length $content) ."\r\n"; $output .= "Date: ". time2str(time)."\r\n"; $output .= "\r\n"; $output .= $content; } else { - my $output = "HTTP/1.1 404 Not Found\r\n"; + my $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; } + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; return $output; } else { return 0 } } +sub _Gzip { + my ($content_raw, $Encoding) = @_; + my $content; + if ( $Encoding =~ m/gzip/ && ((length $content_raw) >= 1) ) { + gzip \$content_raw => \$content; + } + else { $content = $content_raw; } + return $content; +} + sub get_set_state { my ( $self, $AlexaObjects, $uuid, $action, $state ) = @_; my $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; @@ -450,7 +526,6 @@ sub get_set_state { else { return qq["on":false,"bri":252] } } elsif ( $action eq 'set' ) { - &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state )\n") if $main::Debug{'alexa'}; $object->$sub($state); return; @@ -461,16 +536,22 @@ sub get_set_state { $realname =~ s/#/$state/; &main::print_log ("[Alexa] Debug: running voice command: ( $realname )\n") if $main::Debug{'alexa'}; &main::run_voice_cmd("$realname"); + return; } elsif ( $action eq 'get' ) { return qq["on":false,"bri":252]; - } + } } elsif ( ref($sub) eq 'CODE' ) { - &main::print_log ("[Alexa] Debug: running sub: $sub( $state ) \n") if $main::Debug{'alexa'}; - &{$sub}($state) if ($action eq 'set'); - return qq["on":false,"bri":252] if ($action eq 'get'); + if ( $action eq 'set' ) { + &main::print_log ("[Alexa] Debug: running sub: $sub( $state ) \n") if $main::Debug{'alexa'}; + &{$sub}($state); + return; + } + elsif ( $action eq 'get' ) { + return qq["on":false,"bri":252]; + } } } @@ -503,12 +584,10 @@ sub new { my $self = new Generic_Item(); bless $self, $class; $parent->register($self); - my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || DEFAULT_PORT_COUNT; - for my $count (0..$AlexaHttpPortCount) { - my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || DEFAULT_HTTP_PORT; - $AlexaHttpPort = ($AlexaHttpPort + $count); - $self->{'ports'}->{$AlexaHttpPort} = 0; - } + foreach my $AlexaHttpName ( keys %{$AlexaGlobal->{http_sockets}} ) { + my $AlexaHttpPort = $AlexaGlobal->{http_sockets}->{$AlexaHttpName}->{port}; + $self->{'ports'}->{$AlexaHttpPort} = 0; + } $self->{'ports'}->{$::config_parms{'http_port'}} = 0; return $self; } diff --git a/lib/http_server.pl b/lib/http_server.pl index 04ee1f60c..abdf878ec 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -539,7 +539,7 @@ sub http_process_request { return; } - if ( my $alexa_response = &AlexaBridge::process_http($get_req, $req_typ, $Http{'Host'}, $HTTP_BODY, $socket) ) { + if ( my $alexa_response = &AlexaBridge::process_http($get_req, $req_typ, $HTTP_BODY, $socket, %Http) ) { print $socket $alexa_response unless $alexa_response eq ' '; return; } From 73fc39dffb702f77fdc88bd63369959d716cf208 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 6 Jan 2017 14:18:06 -0700 Subject: [PATCH 081/209] Turned on object logging by default --- lib/Generic_Item.pm | 488 +++++++++++++++++++++++-------------- lib/Insteon/BaseInsteon.pm | 25 +- 2 files changed, 320 insertions(+), 193 deletions(-) diff --git a/lib/Generic_Item.pm b/lib/Generic_Item.pm index 9ba7d753f..5cda0668c 100644 --- a/lib/Generic_Item.pm +++ b/lib/Generic_Item.pm @@ -132,13 +132,20 @@ sub new { $$self{state_now} = undef; $$self{state_changed} = undef; $self->restore_data('sort_order'); - $self->{logger_enable} = $main::config_parms{object_logger_enable} if (defined $main::config_parms{object_logger_enable}); - $self->{logger_mintime} = 1; + $self->{logger_enable} = 1; #default to on unless object_logger_enable = 0 + $self->{logger_enable} = $main::config_parms{object_logger_enable} + if ( defined $main::config_parms{object_logger_enable} ); + $self->{logger_mintime} = 1; $self->{logger_updatetime} = 0; - $self->restore_data('active_state', 'schedule_count'); - for my $index (1..20) { - $self->restore_data('schedule_'.$index, 'schedule_label_'.$index, 'schedule_once_'.$index); - } + $self->restore_data( 'active_state', 'schedule_count' ); + + for my $index ( 1 .. 20 ) { + $self->restore_data( + 'schedule_' . $index, + 'schedule_label_' . $index, + 'schedule_once_' . $index + ); + } $self->_initialize_schedule; return $self; @@ -486,8 +493,9 @@ sub hidden { $self->{hidden} = $flag; } else { # Return it, but this currently only will work on $Reload. - return $self->{hidden} - ; # HP - really, no reason why this can't be a read-only method any time? + return + $self + ->{hidden}; # HP - really, no reason why this can't be a read-only method any time? } } @@ -1139,9 +1147,10 @@ sub set_state_log { $state = '' unless defined $state; $set_by_name = '' unless defined $set_by_name; $target = '' unless defined $target; -# - $self->logger($state,$set_by_name,$target) if ($self->{logger_enable}); - + # + $self->logger( $state, $set_by_name, $target ) + if ( $self->{logger_enable} ); + unshift( @{ $$self{state_log} }, "$main::Time_Date $state set_by=$set_by_name" @@ -1161,25 +1170,57 @@ sub set_state_log { TODO =cut + sub logger { - my ($self,$state,$set_by_name,$target) = @_; - my $object_name = $self->{object_name}; - $object_name =~ s/^\$//; - return if ($object_name eq ""); - return if ($state eq ""); - my $tickcount = int(&::get_tickcount()); #log in milliseconds - return if ($tickcount < ($self->{logger_updatetime} + $self->{logger_mintime})); - - #create directory structure if it doesn't exist - mkdir ($::config_parms{data_dir} . "/object_logs") unless (-d $::config_parms{data_dir} . "/object_logs"); - mkdir ($::config_parms{data_dir} . "/object_logs/" . $object_name) unless (-d $::config_parms{data_dir} . "/object_logs/" . $object_name); - mkdir ($::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . $::Year) unless (-d $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . $::Year); - mkdir ($::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . $::Year . "/" . $::Month) unless (-d $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . $::Year . "/" . $::Month); - #write the data to the log; time, ticks (milliseconds), object, state, set_by, target - &::logit ($::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . $::Year . "/" . $::Month . "/" . $::Mday . ".log", "$main::Time_Date,$tickcount,$object_name,$state,$set_by_name," . ( ($target) ? "$target" : '') ."\n",0); - $self->{logger_updatetime} = $tickcount; -} - + my ( $self, $state, $set_by_name, $target ) = @_; + my $object_name = $self->{object_name}; + $object_name =~ s/^\$//; + return if ( $object_name eq "" ); + return if ( $state eq "" ); + my $tickcount = int( &::get_tickcount() ); #log in milliseconds + return + if ( + $tickcount < ( $self->{logger_updatetime} + $self->{logger_mintime} ) ); + + #create directory structure if it doesn't exist + mkdir( $::config_parms{data_dir} . "/object_logs" ) + unless ( -d $::config_parms{data_dir} . "/object_logs" ); + mkdir( $::config_parms{data_dir} . "/object_logs/" . $object_name ) + unless ( -d $::config_parms{data_dir} . "/object_logs/" . $object_name ); + mkdir( $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . $::Year ) + unless ( -d $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . $::Year ); + mkdir( $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . $::Year . "/" + . $::Month ) + unless ( -d $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . $::Year . "/" + . $::Month ); + + #write the data to the log; time, ticks (milliseconds), object, state, set_by, target + &::logit( + $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . $::Year . "/" + . $::Month . "/" + . $::Mday . ".log", + "$main::Time_Date,$tickcount,$object_name,$state,$set_by_name," + . ( ($target) ? "$target" : '' ) . "\n", + 0 + ); + $self->{logger_updatetime} = $tickcount; +} + =item C TODO @@ -1196,8 +1237,8 @@ sub reset_states2 { $ref->{set_by} = $set_by; $ref->{target} = $target; $ref->{legacy_target} = &main::set_by_to_target($set_by) - unless $ref->{target} - ; # just for old code and will be phased out along with old respond calls (done for speed in said and state_now methods) + unless $ref + ->{target}; # just for old code and will be phased out along with old respond calls (done for speed in said and state_now methods) if ( ( defined $state and !defined $ref->{state_prev} ) @@ -1288,10 +1329,13 @@ Will start logging state changes to a historical log file sub logger_enable { my ( $self, $enable ) = @_; - if ($self->isa('Group') and (defined $main::config_parms{object_logger_group})) { - $self->{logger_enable} = $main::config_parms{object_logger_group}; - } else { - $self->{logger_enable} = 1; + if ( $self->isa('Group') + and ( defined $main::config_parms{object_logger_group} ) ) + { + $self->{logger_enable} = $main::config_parms{object_logger_group}; + } + else { + $self->{logger_enable} = 1; } } @@ -1325,7 +1369,7 @@ Returns 1 if logger is enabled on the object. Otherwise 0. sub get_logger_status { my ( $self, $enable ) = @_; - return ($self->{logger_enable} ? 1 : 0); + return ( $self->{logger_enable} ? 1 : 0 ); } =item C @@ -1336,26 +1380,62 @@ Date format is epoch sub get_logger_data { my ( $self, $epoch, $days ) = @_; - $days = 0 unless (defined $days); - my $object_name = $self->{object_name}; - $object_name =~ s/^\$//; - my $data = ""; - $epoch = $epoch - ($days * 60 * 60 * 24); - for (my $i = 0; $i <= $days; $i++) { - print "db i=$i, days=$days, epoch=$epoch\n"; - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($epoch + ($i * 60 * 60 * 24)); - print "Epoch: $epoch is " . $mday . "/" . ($mon + 1) . "/" . ($year+1900) . "\n"; - print "Checking " . $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . ($year + 1900) . "/" . ($mon + 1) . "/" . $mday . "\n"; - print "Reading " . $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . ($year + 1900) . "/" . ($mon + 1) . "/" . $mday . "\n" if ( -e $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . ($year + 1900) . "/" . ($mon + 1) . "/" . $mday . ".log"); - $data .= ::file_read($::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . ($year + 1900) . "/" . ($mon + 1) . "/" . $mday . ".log") if ( -e $::config_parms{data_dir} . "/object_logs/" . $object_name . "/" . ($year + 1900) . "/" . ($mon + 1) . "/" . $mday . ".log"); -# $epoch = $epoch + (60*60*24); - } - - return $data; + $days = 0 unless ( defined $days ); + my $object_name = $self->{object_name}; + $object_name =~ s/^\$//; + my $data = ""; + $epoch = $epoch - ( $days * 60 * 60 * 24 ); + for ( my $i = 0; $i <= $days; $i++ ) { + print "db i=$i, days=$days, epoch=$epoch\n"; + my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = + localtime( $epoch + ( $i * 60 * 60 * 24 ) ); + print "Epoch: $epoch is " + . $mday . "/" + . ( $mon + 1 ) . "/" + . ( $year + 1900 ) . "\n"; + print "Checking " + . $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . ( $year + 1900 ) . "/" + . ( $mon + 1 ) . "/" + . $mday . "\n"; + print "Reading " + . $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . ( $year + 1900 ) . "/" + . ( $mon + 1 ) . "/" + . $mday . "\n" + if (-e $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . ( $year + 1900 ) . "/" + . ( $mon + 1 ) . "/" + . $mday + . ".log" ); + $data .= + ::file_read( $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . ( $year + 1900 ) . "/" + . ( $mon + 1 ) . "/" + . $mday + . ".log" ) + if (-e $::config_parms{data_dir} + . "/object_logs/" + . $object_name . "/" + . ( $year + 1900 ) . "/" + . ( $mon + 1 ) . "/" + . $mday + . ".log" ); + + # $epoch = $epoch + (60*60*24); + } + + return $data; } - - =item C If the state of the generic_item changes, then code will trigger, with the lexical variables $state and $object getting set. The code is a string that will be eval'd and the variables are available to it, but not to any subroutines called by it unless you pass them. You can also set the state variable explicitly since you usually know the item. The code is a string that will be eval'd. @@ -1820,177 +1900,223 @@ sub android_xml_tag { return $xml_objects; } - -sub _initialize_schedule { - my ($self) = @_; - $self->{'initialize_timer'} = ::Timer::new(); - $self->{'initialize_timer'}->set(5, sub { - if (defined($self->{schedule_object}) && ($self->{schedule_object})) { return } - if ($self->{'schedule_count'} > 1) { - ::print_log("[SCHEDULE] Initialize schedule for ". $self->get_object_name." Schedule count is ".$self->{'schedule_count'}); - $self->{'check_date_handler'} = sub { Generic_Item::check_date($self); }; - ::MainLoop_post_add_hook( $self->{'check_date_handler'}); - } -}); +sub _initialize_schedule { + my ($self) = @_; + $self->{'initialize_timer'} = ::Timer::new(); + $self->{'initialize_timer'}->set( + 5, + sub { + if ( defined( $self->{schedule_object} ) + && ( $self->{schedule_object} ) ) + { + return; + } + if ( $self->{'schedule_count'} > 1 ) { + ::print_log( "[SCHEDULE] Initialize schedule for " + . $self->get_object_name + . " Schedule count is " + . $self->{'schedule_count'} ); + $self->{'check_date_handler'} = + sub { Generic_Item::check_date($self); }; + ::MainLoop_post_add_hook( $self->{'check_date_handler'} ); + } + } + ); } - - sub _set_schedule_active_state { - my ($self, $state) = @_; - $self->{'active_state'} = $state if defined($state); + my ( $self, $state ) = @_; + $self->{'active_state'} = $state if defined($state); } - - sub set_schedule { - my ($self,$index,$entry,$label) = @_; - unless ( defined($self->{'check_date_handler'}) ) { - $self->{'check_date_handler'} = sub { Generic_Item::check_date($self); }; - #::MainLoop_post_add_hook( $self->{'check_date_handler'}, 'persistent'); - ::MainLoop_post_add_hook( $self->{'check_date_handler'}); + my ( $self, $index, $entry, $label ) = @_; + unless ( defined( $self->{'check_date_handler'} ) ) { + $self->{'check_date_handler'} = + sub { Generic_Item::check_date($self); }; + + #::MainLoop_post_add_hook( $self->{'check_date_handler'}, 'persistent'); + ::MainLoop_post_add_hook( $self->{'check_date_handler'} ); } - if ($index > $self->{'schedule_count'}) { $self->{'schedule_count'} = $index; } - $self->{'schedule_'.$index} = $entry if (defined($entry)); - $self->{'schedule_label_'.$index} = $label if (defined($label)); - if (defined($self->{'schedule_'.$index})) { # the UI deletes all entries and adds them back which sets this flag to 2. - $self->{'schedule_once_'.$index} = 1 if ($self->{'schedule_once_'.$index} eq 2); # We only want real deleted entries set to 2, so set to 1. + if ( $index > $self->{'schedule_count'} ) { + $self->{'schedule_count'} = $index; + } + $self->{ 'schedule_' . $index } = $entry if ( defined($entry) ); + $self->{ 'schedule_label_' . $index } = $label if ( defined($label) ); + if ( defined( $self->{ 'schedule_' . $index } ) ) + { # the UI deletes all entries and adds them back which sets this flag to 2. + $self->{ 'schedule_once_' . $index } = 1 + if ( $self->{ 'schedule_once_' . $index } eq 2 ) + ; # We only want real deleted entries set to 2, so set to 1. } unless ($entry) { - undef $self->{'schedule_label_'.$index}; - undef $self->{'schedule_'.$index}; - $self->{'schedule_once_'.$index} = 2 if ($self->{'schedule_once_'.$index}); + undef $self->{ 'schedule_label_' . $index }; + undef $self->{ 'schedule_' . $index }; + $self->{ 'schedule_once_' . $index } = 2 + if ( $self->{ 'schedule_once_' . $index } ); } $self->{set_time} = $::Time; } sub set_schedule_once { - my ($self,$index,$entry,$label) = @_; - unless ($self->{'schedule_once_'.$index} eq 1) { - if ((defined($self->{'set_timer_'.$index})) && ($self->{'set_timer_'.$index}->expired)) { - $self->{'schedule_once_'.$index} = 1; - $self->set_schedule($index,$entry,$label); - } else { - $self->{'set_timer_'.$index} = ::Timer::new(); - $self->{'set_timer_'.$index}->set(10, sub { - $self->set_schedule_once($index,$entry,$label); - }); + my ( $self, $index, $entry, $label ) = @_; + unless ( $self->{ 'schedule_once_' . $index } eq 1 ) { + if ( ( defined( $self->{ 'set_timer_' . $index } ) ) + && ( $self->{ 'set_timer_' . $index }->expired ) ) + { + $self->{ 'schedule_once_' . $index } = 1; + $self->set_schedule( $index, $entry, $label ); + } + else { + $self->{ 'set_timer_' . $index } = ::Timer::new(); + $self->{ 'set_timer_' . $index }->set( + 10, + sub { + $self->set_schedule_once( $index, $entry, $label ); + } + ); } } } - sub delete_schedule { - my ($self,$index) = @_; + my ( $self, $index ) = @_; $self->set_schedule($index); } - sub reset_schedule { my ($self) = @_; my $count = $self->{'schedule_count'}; - for my $index (1..$count) { - $self->set_schedule($index); - } - $self->{'schedule_count'} = 0; - $self->{set_time} = $::Time; + for my $index ( 1 .. $count ) { + $self->set_schedule($index); + } + $self->{'schedule_count'} = 0; + $self->{set_time} = $::Time; } - sub get_schedule { - my ($self) = @_; - if ( ( defined($self->{'initialize_timer'}) ) && ( $self->{'initialize_timer'}->active ) ) { return } - my @schedule; - my $count = $self->{'schedule_count'}; - my @states = &get_states($self); - - - $schedule[0][0] = 0; #Index - $schedule[0][1] = '0 0 5 1 1'; #schedule - $schedule[0][2] = 0; #Label - $schedule[0][3] = \@states; - my $nullcount = 0; - for my $index (1..$count) { - unless(defined($self->{'schedule_'.$index})) { - if ($self->{'schedule_once_'.$index}) { - $self->{'schedule_once_'.$index} = 2; - $self->{'schedule_label_'.$index} = undef; - } else { + my ($self) = @_; + if ( ( defined( $self->{'initialize_timer'} ) ) + && ( $self->{'initialize_timer'}->active ) ) + { + return; + } + my @schedule; + my $count = $self->{'schedule_count'}; + my @states = &get_states($self); + + $schedule[0][0] = 0; #Index + $schedule[0][1] = '0 0 5 1 1'; #schedule + $schedule[0][2] = 0; #Label + $schedule[0][3] = \@states; + my $nullcount = 0; + for my $index ( 1 .. $count ) { + unless ( defined( $self->{ 'schedule_' . $index } ) ) { + if ( $self->{ 'schedule_once_' . $index } ) { + $self->{ 'schedule_once_' . $index } = 2; + $self->{ 'schedule_label_' . $index } = undef; + } + else { $nullcount++; - $self->{'schedule_label_'.$index} = undef; - $self->{'schedule_once_'.$index} = undef; + $self->{ 'schedule_label_' . $index } = undef; + $self->{ 'schedule_once_' . $index } = undef; next; - } - } - - if (defined($self->{'schedule_'.$index})) { # the UI deletes all entries and adds them back which sets this flag to 2. - $self->{'schedule_once_'.$index} = 1 if ($self->{'schedule_once_'.$index} eq 2); # We only want real deleted entries set to 2, so set to 1. - } - - if ((defined($self->{'schedule_'.$index})) || ($self->{'schedule_once_'.$index} eq 2)) { - $self->{'schedule_'.($index-$nullcount)} = $self->{'schedule_'.$index}; - $self->{'schedule_label_'.($index-$nullcount)} = $self->{'schedule_label_'.$index}; - $self->{'schedule_once_'.($index-$nullcount)} = $self->{'schedule_once_'.$index}; - $schedule[($index-$nullcount)][0] = ($index-$nullcount); - if ($self->{'schedule_once_'.$index} eq 2) { $schedule[($index-$nullcount)][1] = undef } - else { $schedule[($index-$nullcount)][1] = $self->{'schedule_'.$index} } - - if (defined($self->{'schedule_label_'.$index}) ) { $schedule[($index-$nullcount)][2] = $self->{'schedule_label_'.$index} } - else { $schedule[($index-$nullcount)][2] = ($index-$nullcount) } - - unless (($index-$nullcount) eq $index) { - $self->{'schedule_'.$index} = undef; - $self->{'schedule_label_'.$index} = undef; - $self->{'schedule_once_'.$index} = undef; - } - } - } - $self->{'schedule_count'} = scalar @schedule; - return \@schedule; -} + } + } -sub check_date { - my ($self) = @_; - if ($::New_Minute) { - - unless ($self->{'schedule_count'} > 1) { - if ( $self->{'schedule_delete_count'} eq 2 ) { - ::print_log("[SCHEDULE] Dropping schedule for ". $self->get_object_name ." check count ". $self->{'schedule_delete_count'}); - ::MainLoop_post_drop_hook( $self->{'check_date_handler'} ); - undef $self->{'check_date_handler'}; - undef $self->{'schedule_delete_count'}; + if ( defined( $self->{ 'schedule_' . $index } ) ) + { # the UI deletes all entries and adds them back which sets this flag to 2. + $self->{ 'schedule_once_' . $index } = 1 + if ( $self->{ 'schedule_once_' . $index } eq 2 ) + ; # We only want real deleted entries set to 2, so set to 1. } - $self->{'schedule_delete_count'}++; - } + if ( ( defined( $self->{ 'schedule_' . $index } ) ) + || ( $self->{ 'schedule_once_' . $index } eq 2 ) ) + { + $self->{ 'schedule_' . ( $index - $nullcount ) } = + $self->{ 'schedule_' . $index }; + $self->{ 'schedule_label_' . ( $index - $nullcount ) } = + $self->{ 'schedule_label_' . $index }; + $self->{ 'schedule_once_' . ( $index - $nullcount ) } = + $self->{ 'schedule_once_' . $index }; + $schedule[ ( $index - $nullcount ) ][0] = ( $index - $nullcount ); + if ( $self->{ 'schedule_once_' . $index } eq 2 ) { + $schedule[ ( $index - $nullcount ) ][1] = undef; + } + else { + $schedule[ ( $index - $nullcount ) ][1] = + $self->{ 'schedule_' . $index }; + } + + if ( defined( $self->{ 'schedule_label_' . $index } ) ) { + $schedule[ ( $index - $nullcount ) ][2] = + $self->{ 'schedule_label_' . $index }; + } + else { + $schedule[ ( $index - $nullcount ) ][2] = + ( $index - $nullcount ); + } - for my $index (1..$self->{'schedule_count'}) { - if (defined($self->{'schedule_'.$index})) { - ::print_log("[SCHEDULE] Checking time for ". $self->get_object_name. " schedule is " . $self->{'schedule_'.$index} ." time_cron return ". &main::time_cron($self->{'schedule_'.$index})); - if (&main::time_cron($self->{'schedule_'.$index})) { $self->set_action($self->{'schedule_label_'.$index}) } - } - } - } + unless ( ( $index - $nullcount ) eq $index ) { + $self->{ 'schedule_' . $index } = undef; + $self->{ 'schedule_label_' . $index } = undef; + $self->{ 'schedule_once_' . $index } = undef; + } + } + } + $self->{'schedule_count'} = scalar @schedule; + return \@schedule; } +sub check_date { + my ($self) = @_; + if ($::New_Minute) { + + unless ( $self->{'schedule_count'} > 1 ) { + if ( $self->{'schedule_delete_count'} eq 2 ) { + ::print_log( "[SCHEDULE] Dropping schedule for " + . $self->get_object_name + . " check count " + . $self->{'schedule_delete_count'} ); + ::MainLoop_post_drop_hook( $self->{'check_date_handler'} ); + undef $self->{'check_date_handler'}; + undef $self->{'schedule_delete_count'}; + } + $self->{'schedule_delete_count'}++; + } -sub set_action { - my ($self,$state) = @_; - return if &main::check_for_tied_filters( $self, $state ); - $self->_set_schedule_active_state($state); - my $sub = 'set'; - $sub = $self->{sub} if defined($self->{sub}); - $self->$sub($state,'schedule',1); + for my $index ( 1 .. $self->{'schedule_count'} ) { + if ( defined( $self->{ 'schedule_' . $index } ) ) { + ::print_log( "[SCHEDULE] Checking time for " + . $self->get_object_name + . " schedule is " + . $self->{ 'schedule_' . $index } + . " time_cron return " + . &main::time_cron( $self->{ 'schedule_' . $index } ) ); + if ( &main::time_cron( $self->{ 'schedule_' . $index } ) ) { + $self->set_action( $self->{ 'schedule_label_' . $index } ); + } + } + } + } } +sub set_action { + my ( $self, $state ) = @_; + return if &main::check_for_tied_filters( $self, $state ); + $self->_set_schedule_active_state($state); + my $sub = 'set'; + $sub = $self->{sub} if defined( $self->{sub} ); + $self->$sub( $state, 'schedule', 1 ); +} sub set_sub { - my ($self, $sub) = @_; - $self->{sub} = $sub; + my ( $self, $sub ) = @_; + $self->{sub} = $sub; } - =back =head2 INI PARAMETERS diff --git a/lib/Insteon/BaseInsteon.pm b/lib/Insteon/BaseInsteon.pm index 9e1190259..ce6407a4a 100644 --- a/lib/Insteon/BaseInsteon.pm +++ b/lib/Insteon/BaseInsteon.pm @@ -60,10 +60,10 @@ sub derive_link_state { if ( $p_state =~ /^([+-])(\d+)/ ) { my $rel_state = $1 . $2; my $curr_state = '100'; - $curr_state = '0' if ( $self->state eq 'off' ); - $curr_state = $1 if $self->state =~ /(\d{1,3})/; + $curr_state = '0' if ( $self->state eq 'off' ); + $curr_state = $1 if $self->state =~ /(\d{1,3})/; $p_state = $curr_state + $rel_state; - $p_state = 'on' if ( $p_state > 0 ); + $p_state = 'on' if ( $p_state > 0 ); $p_state = 'off' if ( $p_state <= 0 ); } @@ -122,16 +122,18 @@ sub new { $$self{is_acknowledged} = 0; $$self{max_queue_time} = $::config_parms{'Insteon_PLM_max_queue_time'}; $$self{max_queue_time} = 10 - unless $$self{max_queue_time} - ; # 10 seconds is max time allowed in command stack + unless + $$self{max_queue_time}; # 10 seconds is max time allowed in command stack @{ $$self{command_stack} } = (); $$self{_onlevel} = undef; $$self{is_responder} = 1; $$self{default_hop_count} = 0; $$self{timeout_factor} = 1.0; $$self{is_deaf} = 0; - $$self{logger_enable} = $main::config_parms{object_logger_enable} if (defined $main::config_parms{object_logger_enable}); - $$self{logger_mintime} = 1; + $$self{logger_enable} = 1; #default to on unless object_logger_enable = 0 + $$self{logger_enable} = $main::config_parms{object_logger_enable} + if ( defined $main::config_parms{object_logger_enable} ); + $$self{logger_mintime} = 1; $$self{logger_updatetime} = 0; &Insteon::add($self); return $self; @@ -1517,8 +1519,8 @@ sub new { $$self{is_acknowledged} = 0; $$self{max_queue_time} = $::config_parms{'Insteon_PLM_max_queue_time'}; $$self{max_queue_time} = 10 - unless $$self{max_queue_time} - ; # 10 seconds is max time allowed in command stack + unless + $$self{max_queue_time}; # 10 seconds is max time allowed in command stack @{ $$self{command_stack} } = (); $$self{_onlevel} = undef; $$self{retry_count_log} = 0; @@ -3409,8 +3411,7 @@ sub sync_links { # 2. Does a responder link exist on the PLM if ( ( - !$insteon_object->isa('Insteon_PLM') - && !$self->interface->has_link( + !$insteon_object->isa('Insteon_PLM') && !$self->interface->has_link( $insteon_object, $self->group, 0, '00' ) ) @@ -3451,7 +3452,7 @@ sub sync_links { $tgt_on_level = '100' unless defined $tgt_on_level; my $tgt_ramp_rate = $$self{members}{$member_ref}{ramp_rate}; $tgt_ramp_rate = '0' unless defined $tgt_ramp_rate; - $tgt_on_level =~ s/(\d+)%?/$1/; + $tgt_on_level =~ s/(\d+)%?/$1/; $tgt_ramp_rate =~ s/(\d)s?/$1/; my $resp_aldbkey = $member_root->_aldb->get_linkkey( $insteon_object->device_id, From a86d679d47914fce8462895cb17f8ee503293793 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 6 Jan 2017 14:19:46 -0700 Subject: [PATCH 082/209] Add in config_parm to disable command parsing of message text --- code/common/internet_mail.pl | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/code/common/internet_mail.pl b/code/common/internet_mail.pl index c41c37f3f..cb99bb6f0 100644 --- a/code/common/internet_mail.pl +++ b/code/common/internet_mail.pl @@ -17,6 +17,9 @@ #@

    The config param net_mail_scan_timeout_cycles prevents the process item #@ being killed if it didn't complete within a scan interval. +#@

    The config param net_mail_command_disable globally disables scanning message +#@ text looking for commands to execute + #@

    Check here after you have enabled and configured #@ this script to see your email messages. @@ -268,8 +271,11 @@ sub scan_subjects { for my $line ( file_read $file) { my ( $from, $to, $subject_body ) = $line =~ /From: *(.+) To: *(.+) Subject: *(.*)/; - if ( my ( $command, $code ) = - $subject_body =~ /command:(.+?)\s+code:(\S+)/i ) + if ( + !( defined $config_parms{net_mail_command_disable} ) + and ( my ( $command, $code ) = + $subject_body =~ /command:(.+?)\s+code:(\S+)/i ) + ) { my $results; if ( $config_parms{net_mail_command_code} @@ -362,7 +368,8 @@ sub open_email_message_window { unless ( $w_window->{activated} ) { $w_window->{MW}{top_frame}->Label( -text => 'Re:' ) ->pack(qw/-side left/); - $w_window->{re} = $w_window->{MW}{top_frame}->Entry() + $w_window->{re} = + $w_window->{MW}{top_frame}->Entry() ->pack(qw/-expand yes -fill both -side left/); $w_window->activate(); $w_window->{re}->focus(); From 939f1ed68730c3e32ca0fa2110dde6db1ad704b0 Mon Sep 17 00:00:00 2001 From: waynieack Date: Fri, 6 Jan 2017 17:30:22 -0600 Subject: [PATCH 083/209] More Google Home fixes --- lib/AlexaBridge.pm | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 8b8015c36..28216747f 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -163,9 +163,11 @@ sub _receiveSSDPEvent { } my $target; + &main::print_log ("[Alexa] Debug: SSDP IN - $buf \n") if $main::Debug{'alexa'}; if ( $buf =~ /ST: urn:Belkin:device:\*\*.*/ ) { &_sendSearchResponse($peer) } elsif ( $buf =~ /ST: urn:schemas-upnp-org:device:basic:1.*/ ) { &_sendSearchResponse($peer) } elsif ( $buf =~ /ST: ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } + elsif ( $buf =~ /ST:ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } } @@ -195,6 +197,7 @@ sub _sendSearchResponse { $output .= 'ST: upnp:rootdevice' ."\r\n"; $output .= 'USN: uuid:'.$mac.'::upnp:rootdevice' ."\r\n"; $output .= "\r\n"; + &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'}; send($socket, $output, 0, $peer); $output = "HTTP/1.1 200 OK\r\n"; @@ -208,6 +211,7 @@ sub _sendSearchResponse { $output .= 'ST: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; $output .= 'USN: uuid:2f402f80-da50-11e1-9b23-001e06'.$mac."\r\n"; $output .= "\r\n"; + &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'}; send($socket, $output, 0, $peer); } @@ -221,7 +225,8 @@ sub _sendSearchResponse { $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; $output .= 'ST: urn:schemas-upnp-org:device:basic:1'."\r\n"; $output .= 'USN: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; - $output .= "\r\n"; + $output .= "\r\n"; + &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'}; send($socket, $output, 0, $peer); $count++; @@ -334,7 +339,7 @@ my $AlexaObjects; $output .= "Date: ". time2str(time)."\r\n"; $output .= "\r\n"; $output .= $xmlmessage; - &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: MH Response $xmlmessage \n") if $main::Debug{'alexa'}; return $output; } elsif ( ($uri =~ /^\/api/) && (lc($request_type) eq "post") ) { @@ -421,15 +426,22 @@ my $AlexaObjects; } elsif (defined $uris[3]) { if ( $uris[3] eq 'lights' ) { - $statep1 = qq[{"]; - $statep2 = qq[":"]; - $end = qq["}]; - $delm = qq[","]; + #$statep1 = qq[{"]; + #$statep2 = qq[":"]; + #$end = qq["}]; + #$delm = qq[","]; + $statep1 = qq[{"lights":{"]; + $statep2 = qq[":{"state":{"on":false,"bri":254,"reachable":true},"type":"Extended color light","name":"]; + $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; + $end = qq[}}]; + $delm = qq[,"]; foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; next unless $name; - if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name } - else { $content = $statep1.$uuid.$statep2.$name } + #if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name } + #else { $content = $statep1.$uuid.$statep2.$name } + if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3 } + else { $content = $statep1.$uuid.$statep2.$name.$statep3 } $count++; } } From 0282788dabec67a2920a2a1d55bdb97f90dd545c Mon Sep 17 00:00:00 2001 From: waynieack Date: Fri, 6 Jan 2017 22:53:53 -0600 Subject: [PATCH 084/209] Changed UUIDs to numeric only for Google Home --- lib/AlexaBridge.pm | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 28216747f..9ff4f24ea 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -426,22 +426,26 @@ my $AlexaObjects; } elsif (defined $uris[3]) { if ( $uris[3] eq 'lights' ) { - #$statep1 = qq[{"]; + $statep1 = qq[{"]; #$statep2 = qq[":"]; #$end = qq["}]; #$delm = qq[","]; - $statep1 = qq[{"lights":{"]; + #### 1 $statep2 = qq[":{"state":{"on":false,"bri":254,"reachable":true},"type":"Extended color light","name":"]; $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; - $end = qq[}}]; + $end = qq[}]; $delm = qq[,"]; + #### 2 + foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; next unless $name; #if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name } #else { $content = $statep1.$uuid.$statep2.$name } + #### 1 if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3 } else { $content = $statep1.$uuid.$statep2.$name.$statep3 } + #### 2 $count++; } } @@ -610,11 +614,11 @@ sub add { return unless defined $realname; my $fullname; my $cleanname = $realname; - $cleanname =~ s/\$//; - $cleanname =~ s/ //; - $cleanname =~ s/#//; - $cleanname =~ s/\\//; - $cleanname =~ s/&//; + $cleanname =~ s/\$//g; + $cleanname =~ s/ //g; + $cleanname =~ s/#//g; + $cleanname =~ s/\\//g; + $cleanname =~ s/&//g; if ( defined($name) ) { $fullname = $cleanname.'.'.$name; @@ -666,6 +670,9 @@ sub uuid { 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, 18); return lc($uuid); } From 3aa074537f2f3f80d4e18f5e90f84b320c55167c Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 01:14:07 -0600 Subject: [PATCH 085/209] Added IP and Mac discovery --- lib/AlexaBridge.pm | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 9ff4f24ea..243f213a6 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -18,7 +18,11 @@ use constant DEFAULT_LEASE_TIME => 1800; use constant DEFAULT_NOTIFICATION_PORT => 50000; use constant DEFAULT_PORT_COUNT => 0; -my ($AlexaGlobal); +my ($LOCAL_IP, $LOCAL_MAC) = &DiscoverAddy unless ( (defined($::config_parms{'alexaMac'})) && (defined($::config_parms{'alexaHttpIp'})) ); +$LOCAL_IP = $::config_parms{'alexaHttpIp'} if defined($::config_parms{'alexaHttpIp'}); +$LOCAL_MAC = $::config_parms{'alexaMac'} if defined($::config_parms{'alexaMac'}); + +my $AlexaGlobal; sub startup { unless ($::config_parms{'alexa_enable'}) { return } @@ -178,7 +182,7 @@ sub _sendSearchResponse { my $selfname = (&main::list_objects_by_type('AlexaBridge'))[0]; my $self = ::get_object_by_name($selfname); my $alexa_ssdp_send = $AlexaGlobal->{'ssdp_send'}; - my $mac = $::config_parms{'alexaMac'} || '9aa645cc40aa'; + my $mac = $LOCAL_MAC; foreach my $port ( (sort keys %{$self->{child}->{'ports'}}) ) { @@ -190,7 +194,7 @@ sub _sendSearchResponse { $output .= 'HOST: 239.255.255.250:1900'."\r\n"; $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; - $output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/description.xml' ."\r\n"; + $output .= 'LOCATION: http://'.$LOCAL_IP.':'.$port.'/description.xml' ."\r\n"; #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; @@ -204,7 +208,7 @@ sub _sendSearchResponse { $output .= 'HOST: 239.255.255.250:1900'."\r\n"; $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; - $output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/description.xml' ."\r\n"; + $output .= 'LOCATION: http://'.$LOCAL_IP.':'.$port.'/description.xml' ."\r\n"; #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; @@ -219,7 +223,7 @@ sub _sendSearchResponse { $output .= 'HOST: 239.255.255.250:1900'."\r\n"; $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; - $output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/description.xml' ."\r\n"; + $output .= 'LOCATION: http://'.$LOCAL_IP.':'.$port.'/description.xml' ."\r\n"; #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; @@ -270,10 +274,10 @@ my $xmlmessage = qq[ 1 0 -http://$::config_parms{'alexaHttpIp'}:$port/ +http://$LOCAL_IP:$port/ urn:schemas-upnp-org:device:basic:1 -Amazon-Echo-MH-Bridge (192.168.195.37) +Amazon-Echo-MH-Bridge ($LOCAL_IP) Royal Philips Electronics http://misterhouse.sourceforge.net/ Hue Emulator for Amazon Echo bridge @@ -522,6 +526,22 @@ sub _Gzip { return $content; } +sub DiscoverAddy { + use Net::Address::Ethernet qw( :all ); + my @a = get_addresses(@_); + foreach my $adapter (@a) { + # print $adapter->{sIP}."\n"; + # print $adapter->{sEthernet}."\n"; + # print "____________________\n"; + next unless ($adapter->{iActive} eq 1); + next if ($adapter->{sEthernet} eq ''); + next if ($adapter->{sIP} =~ /127\.0\.0\.1/); + my $Mac = $adapter->{sEthernet}; + $Mac =~ s/://g; + return ($adapter->{sIP},$Mac); + } +} + sub get_set_state { my ( $self, $AlexaObjects, $uuid, $action, $state ) = @_; my $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; From 75991869bad3f8c854e37b2a79668a2992630e82 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 08:47:09 -0600 Subject: [PATCH 086/209] Added more debugs, changed http debug to level 2 and SSDP debug to level 3 --- lib/AlexaBridge.pm | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 243f213a6..6c1c5ce61 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -167,7 +167,7 @@ sub _receiveSSDPEvent { } my $target; - &main::print_log ("[Alexa] Debug: SSDP IN - $buf \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: SSDP IN - $buf \n") if $main::Debug{'alexa'} >= 3; if ( $buf =~ /ST: urn:Belkin:device:\*\*.*/ ) { &_sendSearchResponse($peer) } elsif ( $buf =~ /ST: urn:schemas-upnp-org:device:basic:1.*/ ) { &_sendSearchResponse($peer) } elsif ( $buf =~ /ST: ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } @@ -201,7 +201,7 @@ sub _sendSearchResponse { $output .= 'ST: upnp:rootdevice' ."\r\n"; $output .= 'USN: uuid:'.$mac.'::upnp:rootdevice' ."\r\n"; $output .= "\r\n"; - &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'} >= 3; send($socket, $output, 0, $peer); $output = "HTTP/1.1 200 OK\r\n"; @@ -215,7 +215,7 @@ sub _sendSearchResponse { $output .= 'ST: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; $output .= 'USN: uuid:2f402f80-da50-11e1-9b23-001e06'.$mac."\r\n"; $output .= "\r\n"; - &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'} >= 3; send($socket, $output, 0, $peer); } @@ -230,7 +230,7 @@ sub _sendSearchResponse { $output .= 'ST: urn:schemas-upnp-org:device:basic:1'."\r\n"; $output .= 'USN: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; $output .= "\r\n"; - &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: SSDP OUT - $output \n") if $main::Debug{'alexa'} >= 3; send($socket, $output, 0, $peer); $count++; @@ -343,7 +343,7 @@ my $AlexaObjects; $output .= "Date: ". time2str(time)."\r\n"; $output .= "\r\n"; $output .= $xmlmessage; - &main::print_log ("[Alexa] Debug: MH Response $xmlmessage \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: MH Response $xmlmessage \n") if $main::Debug{'alexa'} >= 2; return $output; } elsif ( ($uri =~ /^\/api/) && (lc($request_type) eq "post") ) { @@ -360,7 +360,7 @@ my $AlexaObjects; $output .= "Date: ". time2str(time)."\r\n"; $output .= "\r\n"; $output .= $content; - &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'} >= 2; return $output; } elsif ( ($uri =~ /^\/api\/.*\/lights\/(.*)\/state$/) && (lc($request_type) eq "put") ) { @@ -374,6 +374,7 @@ my $AlexaObjects; if ( $body =~ /\"(bri)\": (\d+)/ ) { $state = $2 } elsif ( $body =~ /\"(bri)\":(\d+)/ ) { $state = $2 } my $content = qq[\[{"success":{"/lights/$deviceID/state/$1":$2}}\]]; + &main::print_log ("[Alexa] Debug: MH Got request ( $1 - $2 ) to Set device ( $deviceID ) to ( $state )\n") if $main::Debug{'alexa'}; if ( ($AlexaObjects->{'uuid'}->{$deviceID}) && (defined($state)) ) { &get_set_state($self, $AlexaObjects, $deviceID, 'set', $state); @@ -392,10 +393,11 @@ my $AlexaObjects; $output .= $content; } else { $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + &main::print_log("[Alexa] Error: No Matching object for UUID ( $deviceID )"); &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; return $output; } - &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'} >= 2; return $output; #print $socket $output; # print direct to the socket so it does not close. #&main::http_process_request($socket); # we know there will be another request so get it in the same tcp session. @@ -493,6 +495,7 @@ my $AlexaObjects; } if ($count >= 1) { $content = $content.$end; + $debugcontent = $content if $main::Debug{'alexa'} >= 2; $content = &_Gzip($content,$Http{'Accept-Encoding'}); $output = "HTTP/1.1 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; @@ -510,7 +513,7 @@ my $AlexaObjects; } else { my $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; } - &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; + &main::print_log ("[Alexa] Debug: MH Response $debugcontent \n") if $main::Debug{'alexa'} >= 2; return $output; } else { return 0 } From 713de7aa972a9f7913d45cc5304644404ef25caf Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 09:56:29 -0600 Subject: [PATCH 087/209] Fixed space issue in state message for Google Home --- lib/AlexaBridge.pm | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 6c1c5ce61..dde2001f6 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -324,7 +324,7 @@ my $AlexaObjects; } else { &main::print_log( "[Alexa] Error: No Matching object for port ( $port )" ); - $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n\r\n"; return $output; } @@ -367,6 +367,7 @@ my $AlexaObjects; my $output; my $deviceID = $1; my $state = undef; + if ($body =~ /:\w/ ) { $body =~ s/:/: /g } if ( $body =~ /\"(on)\": (true)/ ) { $state = 'on' } elsif ( $body =~ /\"(on)\": (false)/ ) { $state = 'off' } elsif ( $body =~ /\"(off)\": (true)/ ) { $state = 'off' } @@ -552,17 +553,17 @@ sub get_set_state { my $sub = $AlexaObjects->{'uuid'}->{$uuid}->{'sub'}; my $statesub = $AlexaObjects->{'uuid'}->{$uuid}->{'statesub'}; $state = $AlexaObjects->{'uuid'}->{$uuid}->{$state} if $AlexaObjects->{'uuid'}->{$uuid}->{$state}; - if ( $state =~ /\d+/ ) { $state = &roundoff($state / 2.52) } + 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 ( $realname =~ /^\$/ ) { my $object = ::get_object_by_name( $realname ); if ( $action eq 'get' ) { my $cstate = $object->$statesub; $cstate =~ s/\%//; - if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":252] } - elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":252] } - elsif ( $cstate =~ /\d+/ ) { return qq["on":true,"bri":].&roundoff($cstate * 2.52) } - else { return qq["on":false,"bri":252] } + if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":254] } + elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":254] } + elsif ( $cstate =~ /\d+/ ) { return qq["on":true,"bri":].&roundoff($cstate * 2.54) } + else { return qq["on":false,"bri":254] } } elsif ( $action eq 'set' ) { &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state )\n") if $main::Debug{'alexa'}; @@ -578,7 +579,7 @@ sub get_set_state { return; } elsif ( $action eq 'get' ) { - return qq["on":false,"bri":252]; + return qq["on":false,"bri":254]; } } @@ -589,7 +590,7 @@ sub get_set_state { return; } elsif ( $action eq 'get' ) { - return qq["on":false,"bri":252]; + return qq["on":false,"bri":254]; } } } From a0e1847496e29bb4899b82066cca30a8af14a7d4 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 10:23:17 -0600 Subject: [PATCH 088/209] Fixed space issue in state message for Google Home - Again --- lib/AlexaBridge.pm | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index dde2001f6..43d9258f6 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -367,13 +367,12 @@ my $AlexaObjects; my $output; my $deviceID = $1; my $state = undef; - if ($body =~ /:\w/ ) { $body =~ s/:/: /g } - if ( $body =~ /\"(on)\": (true)/ ) { $state = 'on' } - elsif ( $body =~ /\"(on)\": (false)/ ) { $state = 'off' } - elsif ( $body =~ /\"(off)\": (true)/ ) { $state = 'off' } - elsif ( $body =~ /\"(off)\": (false)/ ) { $state = 'on' } - if ( $body =~ /\"(bri)\": (\d+)/ ) { $state = $2 } - elsif ( $body =~ /\"(bri)\":(\d+)/ ) { $state = $2 } + $body =~ s/: /:/g; + if ( $body =~ /\"(on)\":(true)/ ) { $state = 'on' } + elsif ( $body =~ /\"(on)\":(false)/ ) { $state = 'off' } + elsif ( $body =~ /\"(off)\":(true)/ ) { $state = 'off' } + elsif ( $body =~ /\"(off)\":(false)/ ) { $state = 'on' } + if ( $body =~ /\"(bri)\":(\d+)/ ) { $state = $2 } my $content = qq[\[{"success":{"/lights/$deviceID/state/$1":$2}}\]]; &main::print_log ("[Alexa] Debug: MH Got request ( $1 - $2 ) to Set device ( $deviceID ) to ( $state )\n") if $main::Debug{'alexa'}; @@ -394,7 +393,8 @@ my $AlexaObjects; $output .= $content; } else { $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; - &main::print_log("[Alexa] Error: No Matching object for UUID ( $deviceID )"); + &main::print_log("[Alexa] Error: No Matching object for UUID ( $deviceID )") unless ($AlexaObjects->{'uuid'}->{$deviceID}); + &main::print_log("[Alexa] Error: Missing State from PUT for object with UUID ( $deviceID )") unless (defined($state)); &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; return $output; } From b8d491b04593465316c6e0771168bf4bc5866477 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 10:37:46 -0600 Subject: [PATCH 089/209] Fixed space issue in state message for Google Home - Again --- lib/AlexaBridge.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 43d9258f6..f862db8b2 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -566,8 +566,10 @@ sub get_set_state { else { return qq["on":false,"bri":254] } } elsif ( $action eq 'set' ) { - &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state )\n") if $main::Debug{'alexa'}; - $object->$sub($state); + my $end; + if ($object->$statesub =~ /%/) { $end = '%' } + &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state$end )\n") if $main::Debug{'alexa'}; + $object->$sub($state.$end); return; } } From 90d10c1c90a59efe5cb0a10a88d7b21ba5107c0b Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Fri, 6 Jan 2017 22:00:14 +0100 Subject: [PATCH 090/209] ia7: prevent access of undefined property Enables adding objects at the root level of the collection. --- web/ia7/include/javascript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 1340b8b1a..e6b6ce26d 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1064,7 +1064,7 @@ var loadCollection = function(collection_keys) { var item = json_store.collections[collection].item; if (item !== undefined) { - if (json_store.objects[item] === undefined) { + if (json_store.objects === undefined || json_store.objects[item] === undefined) { var path_str = "/objects"; var arg_str = "fields=state,states,label,state_log,schedule,logger_status,&items="+item; $.ajax({ From 3c82128a544565eccf10b0c9e8505dd9eb94723f Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Sat, 7 Jan 2017 19:18:55 +0100 Subject: [PATCH 091/209] ia7: enable direct_control and longclick for items in collection.json --- web/ia7/include/javascript.js | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index e6b6ce26d..f51ad7dac 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1077,13 +1077,21 @@ var loadCollection = function(collection_keys) { } }); } else { + var btn_direct = ""; + if (json_store.ia7_config.objects !== undefined + && json_store.ia7_config.objects[item] !== undefined + && json_store.ia7_config.objects[item].direct_control !== undefined + && json_store.ia7_config.objects[item].direct_control == "yes") { + btn_direct = "btn-direct"; + } + var name = item; var color = getButtonColor(json_store.objects[item].state); if (json_store.objects[item].label !== undefined) name = json_store.objects[item].label; var dbl_btn = ""; if (name.length < 30) dbl_btn = "
    "; var button_html = "

    "; entity_arr.push(button_html); items += item+","; @@ -1143,10 +1151,35 @@ var loadCollection = function(collection_keys) { // if any items present, then create modals and activate updateItem... if (items !== "") { items = items.slice(0,-1); //remove last comma - $('.btn-state-cmd').click( function () { - var entity = $(this).attr("entity"); - create_state_modal(entity); - }); + $('.btn-state-cmd').click( function () { + var entity = $(this).attr("entity"); + if (json_store.ia7_config.objects !== undefined && json_store.ia7_config.objects[entity] !== undefined) { + if (json_store.ia7_config.objects[entity].direct_control !== undefined && json_store.ia7_config.objects[entity].direct_control == "yes") { + var new_state = ""; + var possible_states = 0; + for (var i = 0; i < json_store.objects[entity].states.length; i++){ + if (filterSubstate(json_store.objects[entity].states[i]) == 1) continue; + possible_states++; + if (json_store.objects[entity].states[i] !== json_store.objects[entity].state) new_state = json_store.objects[entity].states[i]; + + } + if ((possible_states > 2) || (new_state == "")) alert("Check configuration of "+entity+". "+possible_states+" states detected for direct control object. State is "+new_state); + url= '/SET;none?select_item='+entity+'&select_state='+new_state; + $.get( url); + } else { + create_state_modal(entity); + } + } + else { + create_state_modal(entity); + } + } + ); + $(".btn-state-cmd").mayTriggerLongClicks().on( 'longClick', function() { + var entity = $(this).attr("entity"); + create_state_modal(entity); + }); + $('.btn-resp-modal').click( function () { var url = $(this).attr('href'); alert("resp-model. opening url="+url); From 2b0989d758fb9588204bc26b87cb489c90aba8c6 Mon Sep 17 00:00:00 2001 From: Tobias Sachs Date: Sat, 7 Jan 2017 19:21:11 +0100 Subject: [PATCH 092/209] raZberry: allow custom device id for binary sensors Fibaro FGK-10x transmits its on/off state with the id -0-113-6-Door-A this change allows us to use such custom Ids for devices that are like binary sensors but do not use the default id (-0-48-1) --- lib/raZberry.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index cf7eeb6ee..be606b082 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1288,7 +1288,7 @@ sub new { #push( @{ $$self{states} }, 'on', 'off'); I'm not sure we should set the states here, since it's not a controlable item? $$self{master_object} = $object; - $devid = $devid . "-0-48-1"; + $devid = $devid . "-0-48-1" unless ( $devid =~ m/-\d+-\d+/ ); $$self{type} = "Binary Sensor"; $$self{devid} = $devid; $object->register( $self, $devid, $options ); From 1b0b5bc201fcd0b2878598ffe5631d332bb32eb8 Mon Sep 17 00:00:00 2001 From: klier Date: Sat, 7 Jan 2017 13:12:57 -0600 Subject: [PATCH 093/209] Update Weather_davisvantageproii.pm Comment out some lines that were causing problems with the latest Perl version. --- lib/Weather_davisvantageproii.pm | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/Weather_davisvantageproii.pm b/lib/Weather_davisvantageproii.pm index a90bc13e1..f71a40cc7 100644 --- a/lib/Weather_davisvantageproii.pm +++ b/lib/Weather_davisvantageproii.pm @@ -321,24 +321,24 @@ sub process { $$wptr{RainTotal} = $rain_storm; $$wptr{RainRate} = $rain_rate; - if ( $::Debug{weather} ) { - foreach my $key qw( - TempIndoor - TempOutdoor - DewIndoor - DewOutdoor - WindAvgSpeed - WindAvgDir - Barom - BaromSea - HumidIndoor - HumidOutdoor - RainTotal - RainRate - ) { - &::print_log( "davisvantageproii: $key " . $$wptr{$key} ); - }; - } + #if ( $::Debug{weather} ) { + # foreach my $key qw( + # TempIndoor + # TempOutdoor + # DewIndoor + # DewOutdoor + # WindAvgSpeed + # WindAvgDir + # Barom + # BaromSea + # HumidIndoor + # HumidOutdoor + # RainTotal + # RainRate + # ) { + # &::print_log( "davisvantageproii: $key " . $$wptr{$key} ); + # }; + #} if ( $::Debug{weather} ) { &::print_log("uv: $uv"); @@ -461,21 +461,21 @@ value should be between 20 inches and 32.5 inches in Vantage Pro and between 20 inches and 32.5 inches in both Vantatge Pro Vantage Pro2. Values outside these ranges will not be logged. Inside Temperature 9 2 The value is sent as 10th of a degree in F. For example, 795 is -returned for 79.5�F. +returned for 79.5°F. Inside Humidity 11 1 This is the relative humidity in %, such as 50 is returned for 50%. Outside Temperature 12 2 The value is sent as 10th of a degree in F. For example, 795 is -returned for 79.5�F. +returned for 79.5°F. Wind Speed 14 1 It is a byte unsigned value in mph. If the wind speed is dashed because it lost synchronization with the radio or due to some other reason, the wind speed is forced to be 0. 10 Min Avg Wind Speed 15 1 It is a byte unsigned value in mph. -Wind Direction 16 2 It is a two byte unsigned value from 1 to 360 degrees. (0� is no -wind data, 90� is East, 180� is South, 270� is West and 360� is +Wind Direction 16 2 It is a two byte unsigned value from 1 to 360 degrees. (0° is no +wind data, 90° is East, 180° is South, 270° is West and 360° is north) Extra Temperatures 18 7 This field supports seven extra temperature stations. Each byte is one extra temperature value in whole degrees F with -an offset of 90 degrees. For example, a value of 0 = -90�F ; a -value of 100 = 10�F ; and a value of 169 = 79�F. +an offset of 90 degrees. For example, a value of 0 = -90°F ; a +value of 100 = 10°F ; and a value of 169 = 79°F. Soil Temperatures 25 4 This field supports four soil temperature sensors, in the same format as the Extra Temperature field above Leaf Temperatures 29 4 This field supports four leaf temperature sensors, in the same @@ -579,8 +579,8 @@ AND the daily UV dose has been manually cleared. Outside Humidity Alarms 74 1 Currently active outside humidity alarms. Low Humidity alarm 2 High Humidity alarm 3 -Extra Temp/Hum Alarms 75 - 81 7 Each byte contains four alarm bits (0 � 3) for a single extra -Temp/Hum station. Bits (4 � 7) are not used and reserved for +Extra Temp/Hum Alarms 75 - 81 7 Each byte contains four alarm bits (0 – 3) for a single extra +Temp/Hum station. Bits (4 – 7) are not used and reserved for future use. Use the temperature and humidity sensor numbers, as described in Section XIII.4 to locate which byte contains the From 981a32e242bf42e3d768bfc1621ff6bdb37f6cc5 Mon Sep 17 00:00:00 2001 From: klier Date: Sat, 7 Jan 2017 13:28:35 -0600 Subject: [PATCH 094/209] runit.pl: Run Voice Commands sent by URL Do fuzzy logic matches with voice commands send by URL and run them. Originally by Bruce Winter. I added commands to strip out some articles and verbs that made the fuzzy logic less accurate. --- web/bin/runit.pl | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 web/bin/runit.pl diff --git a/web/bin/runit.pl b/web/bin/runit.pl new file mode 100644 index 000000000..f4883b063 --- /dev/null +++ b/web/bin/runit.pl @@ -0,0 +1,38 @@ +return &runit(@ARGV); +sub runit { + my ($cmd) = "@_"; + #print_log "-------------------- Original Command: $cmd"; + $cmd =~ s/_/ /g; + #print_log "-------------------- Manipulated Command: $cmd"; + # Look for exact command matches + if (&process_external_command($cmd, 1, 'android', 'speak')) { + print_log "-------------------- Exact Command Match $cmd"; + return &html_page('', 'done'); + } + + # added by Brian: STRIP out articles and then check for exact command match + $cmd =~ s/the //g; + $cmd =~ s/to //g; + $cmd =~ s/turn //g; + + $cmd =~ s/an //g; + $cmd =~ s/make //g; + $cmd =~ s/switch //g; + + if (&process_external_command($cmd, 1, 'android', 'speak')) { + print_log "-------------------- Exact Command Match removing articles $cmd"; + return &html_page('', 'done'); + } + + # Look for nearest fuzzy match + my $cmd1 = &phrase_match1($cmd); + print_log "-------------------- Fuzzy Command Match $cmd1"; + &process_external_command($cmd1, 1, 'android', 'speak'); + return &html_page('', 'done'); + +# # Added by Brian: Give up +# print_log "-------------------- No command found $cmd"; +# play('file' => 'c:\mh\sounds\log.wav'); +# return &html_page('', 'No command found'); + +} \ No newline at end of file From cf3029a4fec4eca5ecb94047882068d4947de669 Mon Sep 17 00:00:00 2001 From: klier Date: Sat, 7 Jan 2017 13:31:39 -0600 Subject: [PATCH 095/209] vr_match.pl: Subroutine for fuzzy logic using Levenshtein distance vr_match.pl: Subroutine for fuzzy logic using Levenshtein distance. By: Bruce Winter, used by /web/bin/runit.pl --- code/common/vr_match.pl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 code/common/vr_match.pl diff --git a/code/common/vr_match.pl b/code/common/vr_match.pl new file mode 100644 index 000000000..bf2b7df4c --- /dev/null +++ b/code/common/vr_match.pl @@ -0,0 +1,16 @@ +sub phrase_match1 { + my ($phrase) = @_; + my (%list1); + my $d_min1 = 999; + my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); + for my $phrase2 (sort @phrases) { + my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); +# my $brianlendist = abs(length($phrase)-length($phrase2)); +# $d = $brianlendist + $d; +# print_log "---------------- $phrase --- $phrase2 --- $d"; + push @{$list1{$d}}, $phrase2 if $d <= $d_min1; + $d_min1 = $d if $d < $d_min1; + } + return ${$list1{$d_min1}}[0]; +} \ No newline at end of file From 4ab8f10c4b78e023249bcef278e95d8fbc410de8 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 17:44:59 -0600 Subject: [PATCH 096/209] Fixed space issue in state message for Google Home - Again --- lib/AlexaBridge.pm | 51 ++++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index f862db8b2..09b82be06 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -413,7 +413,6 @@ my $AlexaObjects; $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"}}]; $content = $statep1.$name.$statep2; @@ -433,26 +432,17 @@ my $AlexaObjects; } elsif (defined $uris[3]) { if ( $uris[3] eq 'lights' ) { - $statep1 = qq[{"]; - #$statep2 = qq[":"]; - #$end = qq["}]; - #$delm = qq[","]; - #### 1 - $statep2 = qq[":{"state":{"on":false,"bri":254,"reachable":true},"type":"Extended color light","name":"]; - $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; - $end = qq[}]; - $delm = qq[,"]; - #### 2 - foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; - next unless $name; - #if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name } - #else { $content = $statep1.$uuid.$statep2.$name } - #### 1 + 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"}]; + $end = qq[}]; + $delm = qq[,"]; if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3 } else { $content = $statep1.$uuid.$statep2.$name.$statep3 } - #### 2 $count++; } } @@ -475,20 +465,15 @@ my $AlexaObjects; } } elsif (defined $uris[2]) { - $statep1 = qq[{"lights":{"]; - #$statep2 = qq[":{"state":{"on":false,"bri":254,"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[":{"state":{"on":false,"bri":254,"reachable":true},"type":"Extended color light","name":"]; # dis - #$statep2 = qq[":{"state":{"on":false,"bri":254,"hue":15823,"sat":88,"effect":"none","ct":313,"alert":"none","colormode":"ct","reachable":true},"type":"Extended color light","name":"]; - #$statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","uniqueid":"]; - $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; # - #$statep4 = qq[","swversion":"65003148","pointsymbol":{"1":"none","2":"none","3":"none","4":"none","5":"none","6":"none","7":"none","8":"none"}}]; - $end = qq[}}]; - $delm = qq[,"]; foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { - $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; + $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; next unless $name; - #if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3.$uuid.$statep4 } - #else { $content = $statep1.$uuid.$statep2.$name.$statep3.$uuid.$statep4 } + my $state = &get_set_state($self, $AlexaObjects, $uuid,'get'); + $statep1 = qq[{"lights":{"]; + $statep2 = qq[":{"state":{$state,"reachable":true},"type":"Extended color light","name":"]; # dis + $statep3 = qq[","modelid":"LCT001","manufacturername":"Philips","swversion":"65003148"}]; # + $end = qq[}}]; + $delm = qq[,"]; if ($count >= 1) { $content = $content.$delm.$uuid.$statep2.$name.$statep3 } else { $content = $statep1.$uuid.$statep2.$name.$statep3 } $count++; @@ -563,11 +548,11 @@ sub get_set_state { if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":254] } elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":254] } elsif ( $cstate =~ /\d+/ ) { return qq["on":true,"bri":].&roundoff($cstate * 2.54) } - else { return qq["on":false,"bri":254] } + else { return qq["on":true,"bri":254] } } elsif ( $action eq 'set' ) { my $end; - if ($object->$statesub =~ /%/) { $end = '%' } + if ($object->$statesub =~ /\%/) { $end = '%' } &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state$end )\n") if $main::Debug{'alexa'}; $object->$sub($state.$end); return; @@ -581,7 +566,7 @@ sub get_set_state { return; } elsif ( $action eq 'get' ) { - return qq["on":false,"bri":254]; + return qq["on":true,"bri":254]; } } @@ -592,7 +577,7 @@ sub get_set_state { return; } elsif ( $action eq 'get' ) { - return qq["on":false,"bri":254]; + return qq["on":true,"bri":254]; } } } From 073b396da28b16b1032efe10f4e4a56bb0b4ad34 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 7 Jan 2017 17:49:33 -0600 Subject: [PATCH 097/209] Added correct states in all responses as is seems that Google Home actually uses them --- lib/AlexaBridge.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 09b82be06..416610b2a 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -683,7 +683,7 @@ sub uuid { $uuid = $ug->to_string( ( $ug->create_from_name(NameSpace_DNS, $name) ) ); $uuid =~ s/\D//g; $uuid =~ s/-//g; - #$uuid = (substr $uuid, 0, 18); + $uuid = (substr $uuid, 0, 9); return lc($uuid); } From f646325502a8b73b693fd5d111c2367d46203afe Mon Sep 17 00:00:00 2001 From: waynieack Date: Sun, 8 Jan 2017 02:26:58 -0600 Subject: [PATCH 098/209] Added correct states in all responses as is seems that Google Home actually uses them --- lib/AlexaBridge.pm | 73 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 416610b2a..7518097ce 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -545,14 +545,17 @@ sub get_set_state { if ( $action eq 'get' ) { my $cstate = $object->$statesub; $cstate =~ s/\%//; - if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":254] } - elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":254] } + my $level = '254'; + if ( $object->can('state_level') ) { $level = ( &roundoff(($object->level) * 2.54) ) } + if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":$level] } + elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":$level] } elsif ( $cstate =~ /\d+/ ) { return qq["on":true,"bri":].&roundoff($cstate * 2.54) } - else { return qq["on":true,"bri":254] } + else { return qq["on":true,"bri":$level] } } elsif ( $action eq 'set' ) { my $end; - if ($object->$statesub =~ /\%/) { $end = '%' } + #if ($object->$statesub =~ /\%/) { $end = '%' } + if ( $object->can('state_level') ) { $end = '%'} &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state$end )\n") if $main::Debug{'alexa'}; $object->$sub($state.$end); return; @@ -605,17 +608,23 @@ sub register { package AlexaBridge_Item; @AlexaBridge_Item::ISA = ('Generic_Item'); +use Storable; sub new { my ($class, $parent) = @_; my $self = new Generic_Item(); + my $file = $::config_parms{'data_dir'}.'/alexa_temp.saved_id'; bless $self, $class; $parent->register($self); foreach my $AlexaHttpName ( keys %{$AlexaGlobal->{http_sockets}} ) { my $AlexaHttpPort = $AlexaGlobal->{http_sockets}->{$AlexaHttpName}->{port}; $self->{'ports'}->{$AlexaHttpPort} = 0; } - $self->{'ports'}->{$::config_parms{'http_port'}} = 0; + $self->{'ports'}->{$::config_parms{'http_port'}} = 0; + if (-e $file) { + my $restoredhash = retrieve($file); + $self->{idmap} = $restoredhash->{idmap}; + } else { $self->{idmap} } return $self; } @@ -652,6 +661,13 @@ sub add { last; } + # $self->{8080}->{'uuid'}->{3}->{'realname'}=$realname; + # $self->{8080}->{'uuid'}->{3}->{'name'}=$name || $cleanname; + # $self->{8080}->{'uuid'}->{3}->{'sub'}=$sub || 'set'; + # $self->{8080}->{'uuid'}->{3}->{'on'}=$on || 'on'; + # $self->{8080}->{'uuid'}->{3}->{'off'}=$off || 'off'; + # $self->{8080}->{'uuid'}->{3}->{'statesub'}=$statesub || 'state'; + # Testing groups, saw the Echo hit /api/odtQdwTaiTjPgURo4ZyEtGfIqRgfSeCm1fl2AMG2/groups/0 #$self->{'groups'}->{0}->{'name'}='group0'; #$self->{'groups'}->{0}->{'realname'}='$light0'; @@ -678,13 +694,46 @@ sub get_objects { sub uuid { my ($self, $name) = @_; - 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); + my $file = $::config_parms{'data_dir'}.'/alexa_temp.saved_id'; + return $self->{'idmap'}->{objects}->{$name} if ($self->{'idmap'}->{objects}->{$name}); + + my $highid; + my $missing; + my $count = 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'}; +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); +} + +sub isDeleted { + my ($self, $uuid) = @_; + my $count; + foreach my $port ( (sort keys %{$self->{'ports'}}) ) { + $count++ if ( $self->{$port}->{'uuid'}->{$uuid} ); + } + return 1 unless $count; + return 0; } 1; From 8d11e3e5c9bfe57792e9426056f307af7e4d0e9d Mon Sep 17 00:00:00 2001 From: waynieack Date: Sun, 8 Jan 2017 19:00:19 -0600 Subject: [PATCH 099/209] FIxed % with on/off command --- lib/AlexaBridge.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 7518097ce..b608741fa 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -555,7 +555,7 @@ sub get_set_state { elsif ( $action eq 'set' ) { my $end; #if ($object->$statesub =~ /\%/) { $end = '%' } - if ( $object->can('state_level') ) { $end = '%'} + if ( $object->can('state_level') && $state =~ /\d+/ ) { $end = '%'} &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state$end )\n") if $main::Debug{'alexa'}; $object->$sub($state.$end); return; From 272566c9bb593845583abc8c7322294a13f0277b Mon Sep 17 00:00:00 2001 From: waynieack Date: Sun, 8 Jan 2017 19:17:18 -0600 Subject: [PATCH 100/209] Fixed crash with undefined object --- lib/AlexaBridge.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index b608741fa..418e76466 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -542,6 +542,7 @@ sub get_set_state { &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 ( $realname =~ /^\$/ ) { my $object = ::get_object_by_name( $realname ); + return qq["on":true,"bri":254] unless defined $object; if ( $action eq 'get' ) { my $cstate = $object->$statesub; $cstate =~ s/\%//; From ee07f2a2e40729385d1a30efab537aae7079b5b8 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 9 Jan 2017 14:50:31 -0600 Subject: [PATCH 101/209] Added more debug messages to get state --- lib/AlexaBridge.pm | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 418e76466..de822eda2 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -537,7 +537,7 @@ sub get_set_state { my $realname = $AlexaObjects->{'uuid'}->{$uuid}->{'realname'}; my $sub = $AlexaObjects->{'uuid'}->{$uuid}->{'sub'}; my $statesub = $AlexaObjects->{'uuid'}->{$uuid}->{'statesub'}; - $state = $AlexaObjects->{'uuid'}->{$uuid}->{$state} if $AlexaObjects->{'uuid'}->{$uuid}->{$state}; + $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 ( $realname =~ /^\$/ ) { @@ -547,11 +547,15 @@ sub get_set_state { my $cstate = $object->$statesub; $cstate =~ s/\%//; my $level = '254'; - if ( $object->can('state_level') ) { $level = ( &roundoff(($object->level) * 2.54) ) } - if ( $AlexaObjects->{'uuid'}->{$uuid}->{'on'} eq $cstate ) { return qq["on":true,"bri":$level] } - elsif ( $AlexaObjects->{'uuid'}->{$uuid}->{'off'} eq $cstate ) { return qq["on":false,"bri":$level] } - elsif ( $cstate =~ /\d+/ ) { return qq["on":true,"bri":].&roundoff($cstate * 2.54) } - else { return qq["on":true,"bri":$level] } + my $debug = "[Alexa] Debug: get_state (actual object state: $cstate) - "; + my $return; + if ( $object->can('state_level') ) { $level = ( &roundoff(($object->level) * 2.54) ); $debug .= "(level: $level) - "; } + if ( lc($AlexaObjects->{'uuid'}->{$uuid}->{'on'}) eq lc($cstate) ) { $return = qq["on":true,"bri":$level] } + elsif ( lc($AlexaObjects->{'uuid'}->{$uuid}->{'off'}) eq lc($cstate) ) { $return = qq["on":false,"bri":$level] } + elsif ( $cstate =~ /\d+/ ) { $return = qq["on":true,"bri":].&roundoff($cstate * 2.54) } + else { $return = qq["on":true,"bri":$level] } + &main::print_log ( "$debug returning - $return\n" ) if $main::Debug{'alexa'}; + return $return; } elsif ( $action eq 'set' ) { my $end; @@ -656,8 +660,8 @@ sub add { $self->{$port}->{'uuid'}->{$uuid}->{'realname'}=$realname; $self->{$port}->{'uuid'}->{$uuid}->{'name'}=$name || $cleanname; $self->{$port}->{'uuid'}->{$uuid}->{'sub'}=$sub || 'set'; - $self->{$port}->{'uuid'}->{$uuid}->{'on'}=$on || 'on'; - $self->{$port}->{'uuid'}->{$uuid}->{'off'}=$off || 'off'; + $self->{$port}->{'uuid'}->{$uuid}->{'on'}=lc($on) || 'on'; + $self->{$port}->{'uuid'}->{$uuid}->{'off'}=lc($off) || 'off'; $self->{$port}->{'uuid'}->{$uuid}->{'statesub'}=$statesub || 'state'; last; } From 5fb412421506656f4350fdca9b9d59582c947565 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 9 Jan 2017 16:28:33 -0600 Subject: [PATCH 102/209] Fixed x10 object so dim/brighten/-%/+% states are on --- lib/AlexaBridge.pm | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index de822eda2..476b459a0 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -547,9 +547,21 @@ sub get_set_state { my $cstate = $object->$statesub; $cstate =~ s/\%//; my $level = '254'; - my $debug = "[Alexa] Debug: get_state (actual object state: $cstate) - "; + my $type = $object->get_type(); + my $debug = "[Alexa] Debug: get_state (actual object state: $cstate) - (object type: $type) - "; my $return; - if ( $object->can('state_level') ) { $level = ( &roundoff(($object->level) * 2.54) ); $debug .= "(level: $level) - "; } + if ( $object->can('state_level') ) { + my $l = $object->level; + $l =~ s/\%//; + if ( $l =~ /\d+/ ) { + $level = ( &roundoff(($l) * 2.54) ); + $debug .= "(level: $level) - "; + } + } + if ( lc($type) =~ /x10/ ) { + if ( ($cstate =~ /\d+/) || ($cstate =~ /dim/) || ($cstate =~ /bright/) ) { $cstate = 'on' } + $debug .= "(determined state: $cstate) - "; + } if ( lc($AlexaObjects->{'uuid'}->{$uuid}->{'on'}) eq lc($cstate) ) { $return = qq["on":true,"bri":$level] } elsif ( lc($AlexaObjects->{'uuid'}->{$uuid}->{'off'}) eq lc($cstate) ) { $return = qq["on":false,"bri":$level] } elsif ( $cstate =~ /\d+/ ) { $return = qq["on":true,"bri":].&roundoff($cstate * 2.54) } @@ -590,6 +602,20 @@ sub get_set_state { } } +sub get_state { +my ( $self, $object, $statesub ) = @_; + my $cstate = $object->$statesub; + $cstate =~ s/\%//; + my $type = $object->get_type(); + my $debug = "[Alexa] Debug: get_state (actual object state: $cstate) - (object type: $type) - "; + if ( lc($type) =~ /x10/ ) { + if ( ($state =~ /\d+/) || ($state =~ /dim/) || ($state =~ /bright/) ) { $cstate = 'on' } + } + $debug .= "(determined state: $cstate) - "; + return $cstate; +} + + sub roundoff { my $num = shift; From 30f256b32d3937c1a2db49b343d35f0002031d20 Mon Sep 17 00:00:00 2001 From: waynieack Date: Tue, 10 Jan 2017 09:49:53 -0600 Subject: [PATCH 103/209] Added option for an external proxy like Apache or IIS to listen on a single port. --- lib/AlexaBridge.pm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 476b459a0..10703a3eb 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -651,7 +651,10 @@ sub new { my $AlexaHttpPort = $AlexaGlobal->{http_sockets}->{$AlexaHttpName}->{port}; $self->{'ports'}->{$AlexaHttpPort} = 0; } - $self->{'ports'}->{$::config_parms{'http_port'}} = 0; + if ( ($::config_parms{'alexaHttpPortCount'} eq 0) && ($::config_parms{'alexaHttpPort'}) ) { + $self->{'ports'}->{$::config_parms{'alexaHttpPort'}} = 0; # This is to disable all MH proxy ports and use an external proxy port via Apache + } + else { $self->{'ports'}->{$::config_parms{'http_port'}} = 0; } if (-e $file) { my $restoredhash = retrieve($file); $self->{idmap} = $restoredhash->{idmap}; @@ -730,7 +733,7 @@ sub uuid { my $highid; my $missing; - my $count = 1; + my $count = $::config_parms{'alexaUuidStart'} || 1; foreach my $object (keys %{$self->{idmap}->{objects}}) { my $currentid = $self->{idmap}->{objects}->{$object}; $highid = $currentid if ( $currentid > $highid ); From 6865b62251e00d08dad07380ffecddd0768bb172 Mon Sep 17 00:00:00 2001 From: waynieack Date: Fri, 13 Jan 2017 21:23:57 -0600 Subject: [PATCH 104/209] Added Chunked mode for the Echo so only 1 port is needed to support 300 MH objects --- lib/AlexaBridge.pm | 156 +++++++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 49 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 10703a3eb..44c349ab9 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -9,14 +9,14 @@ use IO::Socket::Multicast; -use constant SSDP_IP => "239.255.255.250"; -use constant SSDP_PORT => 1900; -use constant CRLF => "\015\012"; +#use constant SSDP_IP => "239.255.255.250"; +#use constant SSDP_PORT => 1900; +#use constant CRLF => "\015\012"; -use constant DEFAULT_HTTP_PORT => 8085; -use constant DEFAULT_LEASE_TIME => 1800; -use constant DEFAULT_NOTIFICATION_PORT => 50000; -use constant DEFAULT_PORT_COUNT => 0; +#use constant DEFAULT_HTTP_PORT => 80; +#use constant DEFAULT_LEASE_TIME => 1800; +#use constant DEFAULT_NOTIFICATION_PORT => 50000; +#use constant DEFAULT_PORT_COUNT => 0; my ($LOCAL_IP, $LOCAL_MAC) = &DiscoverAddy unless ( (defined($::config_parms{'alexaMac'})) && (defined($::config_parms{'alexaHttpIp'})) ); $LOCAL_IP = $::config_parms{'alexaHttpIp'} if defined($::config_parms{'alexaHttpIp'}); @@ -32,11 +32,12 @@ sub startup { sub open_port { - my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || DEFAULT_PORT_COUNT; + my $SSDP_PORT = '1900'; + my $AlexaHttpPortCount = $::config_parms{'alexaHttpPortCount'} || '0'; if ($AlexaHttpPortCount) { $AlexaHttpPortCount = ($AlexaHttpPortCount - 1); for my $count (0..$AlexaHttpPortCount) { - my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || DEFAULT_HTTP_PORT; + my $AlexaHttpPort = $::config_parms{'alexaHttpPort'} || '80'; $AlexaHttpPort = ($AlexaHttpPort + $count); my $AlexaHttpName = 'alexaServer'.$count; &http_ports($AlexaHttpName, $AlexaHttpPort); @@ -49,7 +50,7 @@ sub open_port { $AlexaGlobal->{http_sender}->{'alexa_http_sender'} = new Socket_Item('alexa_http_sender', undef, $::config_parms{'http_server'}.':'.$::config_parms{'http_port'}, 'alexa_http_sender', 'tcp', 'raw'); } - my $notificationPort = $::config_parms{'alexa_notification_port'} || DEFAULT_NOTIFICATION_PORT; + my $notificationPort = $::config_parms{'alexa_notification_port'} || '50000'; my $ssdpNotificationName = 'alexaSsdpNotification'; @@ -76,19 +77,19 @@ sub open_port { my $ssdpListenName = 'alexaSsdpListen'; my $ssdpListenSocket = new IO::Socket::Multicast->new( - LocalPort => SSDP_PORT, + LocalPort => $SSDP_PORT, Proto => 'udp', Reuse => 1) - || &main::print_log( "\nError: Could not start a udp alexa multicast listen server on ". SSDP_PORT .$@ ."\n\n" ) && return; - $ssdpListenSocket->mcast_add(SSDP_IP); + || &main::print_log( "\nError: Could not start a udp alexa multicast listen server on ". $SSDP_PORT .$@ ."\n\n" ) && return; + $ssdpListenSocket->mcast_add('239.255.255.250'); $::Socket_Ports{$ssdpListenName}{protocol} = 'udp'; $::Socket_Ports{$ssdpListenName}{datatype} = 'raw'; - $::Socket_Ports{$ssdpListenName}{port} = SSDP_PORT; + $::Socket_Ports{$ssdpListenName}{port} = $SSDP_PORT; $::Socket_Ports{$ssdpListenName}{sock} = $ssdpListenSocket; $::Socket_Ports{$ssdpListenName}{socka} = $ssdpListenSocket; # UDP ports are always "active" $AlexaGlobal->{'ssdp_listen'} = new Socket_Item( undef, undef, $ssdpListenName ); - printf " - creating %-15s on %3s %5s %s\n", $ssdpListenName, 'udp', SSDP_PORT; + printf " - creating %-15s on %3s %5s %s\n", $ssdpListenName, 'udp', $SSDP_PORT; &main::print_log ("Alexa open_port: p=$ssdpPort pn=$ssdpListenName s=" .$AlexaGlobal->{'ssdp_listen'} ."\n") if $main::Debug{alexa}; @@ -119,8 +120,10 @@ sub check_for_data { #foreach my $AlexaHttpName ( keys %{$AlexaGlobal->{http_sockets}} ) { my $AlexaHttpName = 'alexaServer0'; my $alexa_listen = $AlexaGlobal->{http_sockets}{$AlexaHttpName}; + if ( $alexa_listen && ( my $alexa_data = said $alexa_listen ) ) { - #&main::print_log( "[Alexa] Info: Data - $alexa_data" ); + my $peerip = $alexa_listen->peer; + &main::print_log( "[Alexa] Debug: Peer: $peerip Data IN - $alexa_data" ) if $main::Debug{'alexa'} >= 5; $alexa_http_sender->start unless $alexa_http_sender->active; $alexa_http_sender->set($alexa_data); @@ -142,6 +145,8 @@ sub check_for_data { sub _sendHttpData { my ($alexa_listen, $alexa_http_sender) = @_; if ( $alexa_http_sender && ( my $alexa_sender_data = said $alexa_http_sender ) ) { + my $peerip = $alexa_listen->peer; + &main::print_log( "[Alexa] Debug: Peer: $peerip Data OUT - $alexa_sender_data" ) if $main::Debug{'alexa'} >= 5; $alexa_listen->set($alexa_sender_data); } } @@ -167,11 +172,12 @@ sub _receiveSSDPEvent { } my $target; - &main::print_log ("[Alexa] Debug: SSDP IN - $buf \n") if $main::Debug{'alexa'} >= 3; - if ( $buf =~ /ST: urn:Belkin:device:\*\*.*/ ) { &_sendSearchResponse($peer) } - elsif ( $buf =~ /ST: urn:schemas-upnp-org:device:basic:1.*/ ) { &_sendSearchResponse($peer) } - elsif ( $buf =~ /ST: ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } - elsif ( $buf =~ /ST:ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } + $buf =~ s/ST: /ST:/g; + &main::print_log ("[Alexa] Debug: SSDP IN - $buf \n") if $main::Debug{'alexa'} >= 3; + if ( $buf =~ /ST:urn:Belkin:device:\*\*.*/ ) { &_sendSearchResponse($peer) } + elsif ( $buf =~ /ST:urn:schemas-upnp-org:device:basic:1.*/ ) { &_sendSearchResponse($peer) } + elsif ( $buf =~ /ST:ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } + #elsif ( $buf =~ /ST:ssdp:all.*/ ) { &_sendSearchResponse($peer,'all') } } @@ -186,7 +192,7 @@ sub _sendSearchResponse { foreach my $port ( (sort keys %{$self->{child}->{'ports'}}) ) { - next unless ( $self->{child}->{$port} ); + #next unless ( $self->{child}->{$port} ); my $socket = handle $alexa_ssdp_send; my $output; if ($type eq 'all') { @@ -195,7 +201,6 @@ sub _sendSearchResponse { $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; $output .= 'LOCATION: http://'.$LOCAL_IP.':'.$port.'/description.xml' ."\r\n"; - #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; $output .= 'ST: upnp:rootdevice' ."\r\n"; @@ -209,7 +214,6 @@ sub _sendSearchResponse { $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; $output .= 'LOCATION: http://'.$LOCAL_IP.':'.$port.'/description.xml' ."\r\n"; - #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; $output .= 'ST: uuid:2f402f80-da50-11e1-9b23-'.lc($mac)."\r\n"; @@ -224,7 +228,6 @@ sub _sendSearchResponse { $output .= 'CACHE-CONTROL: max-age=100'."\r\n"; $output .= 'EXT: '."\r\n"; $output .= 'LOCATION: http://'.$LOCAL_IP.':'.$port.'/description.xml' ."\r\n"; - #$output .= 'LOCATION: http://'.$::config_parms{'alexaHttpIp'}.':'.$port.'/upnp/alexa-mh-bridge'.$count.'/setup.xml' ."\r\n"; $output .= 'SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0' ."\r\n"; $output .= 'hue-bridgeid: B827EBFFFE'.uc((substr $mac, -6))."\r\n"; $output .= 'ST: urn:schemas-upnp-org:device:basic:1'."\r\n"; @@ -242,7 +245,7 @@ sub process_http { unless ($::config_parms{'alexa_enable'}) { return 0 } my ( $uri, $request_type, $body, $socket, %Http ) = @_; - unless ( ($uri =~ /^\/upnp\//) || ($uri =~ /^\/api/ ) || ($uri =~ /^\/description.xml$/) ) { return 0 } # Added for performance + unless ( ($uri =~ /^\/api/ ) || ($uri =~ /^\/description.xml$/) ) { return 0 } # Added for performance my $selfname = (&main::list_objects_by_type('AlexaBridge'))[0]; my $self = ::get_object_by_name($selfname); @@ -316,21 +319,25 @@ my $xmlmessage = qq[
    ]; -my $AlexaObjects; - if ( $self->{child}->{$port} ) { +my ($AlexaObjects,$AlexaObjChunk); + if ( $::config_parms{'alexaEnableChunked'} ) { + $AlexaObjects = $self->{child}->{fulllist}; + } + elsif ( $self->{child}->{$port} ) { # use Data::Dumper; $AlexaObjects = $self->{child}->{$port}; + $AlexaObjChunk = $self->{child}->{$port}; #&main::print_log( Data::Dumper->Dumper($AlexaObjects) ); } else { &main::print_log( "[Alexa] Error: No Matching object for port ( $port )" ); - $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n\r\n"; + $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.."; return $output; } &main::print_log ("[Alexa] Debug: Port: ( $port ) URI: ( $uri ) Body: ( $body ) Type: ( $request_type ) \n") if $main::Debug{'alexa'}; - if ( ( ($uri =~ /^\/upnp\/.*\/setup.xml$/) || ($uri =~ /^\/description.xml$/) ) && (lc($request_type) eq "get") ) { + if ( ($uri =~ /^\/description.xml$/) && (lc($request_type) eq "get") ) { my $output = "HTTP/1.1 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; $output .= 'Access-Control-Allow-Origin: *'."\r\n"; @@ -392,7 +399,7 @@ my $AlexaObjects; $output .= "\r\n"; $output .= $content; } else { - $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + $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] Error: No Matching object for UUID ( $deviceID )") unless ($AlexaObjects->{'uuid'}->{$deviceID}); &main::print_log("[Alexa] Error: Missing State from PUT for object with UUID ( $deviceID )") unless (defined($state)); &main::print_log ("[Alexa] Debug: MH Response $output \n") if $main::Debug{'alexa'}; @@ -420,7 +427,7 @@ my $AlexaObjects; } elsif ( $uris[3] eq 'lights' ) { &main::print_log("[Alexa] Error: No Matching object for UUID ( $uris[4] )"); - $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + $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 $output \n") if $main::Debug{'alexa'}; return $output; } @@ -432,8 +439,9 @@ my $AlexaObjects; } elsif (defined $uris[3]) { if ( $uris[3] eq 'lights' ) { - foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { - $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; + $AlexaObjChunk = $self->_GetChunk($uris[3]) if ( $::config_parms{'alexaEnableChunked'} ); + foreach my $uuid ( keys %{$AlexaObjChunk->{'uuid'}} ) { + $name = $AlexaObjChunk->{'uuid'}->{$uuid}->{'name'}; next unless $name; my $state = &get_set_state($self, $AlexaObjects, $uuid,'get'); $statep1 = qq[{"]; @@ -451,8 +459,9 @@ my $AlexaObjects; $statep2 = qq[":"]; $end = qq["}]; $delm = qq[","]; - foreach my $id ( keys %{$AlexaObjects->{'groups'}} ) { - $name = $AlexaObjects->{'groups'}->{$id}->{'name'}; + $AlexaObjChunk = $self->_GetChunk($uris[3]) if ( $::config_parms{'alexaEnableChunked'} ); + foreach my $id ( keys %{$AlexaObjChunk->{'groups'}} ) { + $name = $AlexaObjChunk->{'groups'}->{$id}->{'name'}; next unless $name; $statep1 = qq[{"$id": {"name": "$name","lights": \["1","2"\],"type": "LightGroup","action": {"on": true,"bri": 254,"hue": 10000,"sat": 254,"effect": "none","xy": \[0.5,0.5\],"ct": 250,"alert": "select","colormode": "ct"}}]; $delim = qq[,]; @@ -465,8 +474,9 @@ my $AlexaObjects; } } elsif (defined $uris[2]) { - foreach my $uuid ( keys %{$AlexaObjects->{'uuid'}} ) { - $name = $AlexaObjects->{'uuid'}->{$uuid}->{'name'}; + $AlexaObjChunk = $self->_GetChunk('all') if ( $::config_parms{'alexaEnableChunked'} ); + foreach my $uuid ( keys %{$AlexaObjChunk->{'uuid'}} ) { + $name = $AlexaObjChunk->{'uuid'}->{$uuid}->{'name'}; next unless $name; my $state = &get_set_state($self, $AlexaObjects, $uuid,'get'); $statep1 = qq[{"lights":{"]; @@ -497,9 +507,9 @@ my $AlexaObjects; $output .= "\r\n"; $output .= $content; } else { - my $output = "HTTP/1.0 404 Not Found\r\nServer: MisterHouse\r\nCache-Control: no-cache\r\n"; + $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; + &main::print_log ("[Alexa] Debug: MH Response $output.$debugcontent \n") if $main::Debug{'alexa'} >= 2; return $output; } else { return 0 } @@ -515,13 +525,30 @@ sub _Gzip { return $content; } + +sub _GetChunk { + my ( $self,$uri ) = @_; + use Time::HiRes qw(clock_gettime); + my $realtime = clock_gettime(CLOCK_REALTIME); + $self->{'conn'}->{$uri}->{time} = clock_gettime(CLOCK_REALTIME) unless $self->{'conn'}->{$uri}->{time}; + $self->{'conn'}->{$uri}->{count} = 0 unless defined($self->{'conn'}->{$uri}->{count}); + + if ( ($realtime - $self->{'conn'}->{$uri}->{time}) <= .7 ) { + my $size = $self->{child}->{ChkCnt}; + if ( $self->{'conn'}->{$uri}->{count} eq $size ) { $ChkCnt = $size; $self->{'conn'}->{$uri}->{count} = 0 } + elsif ( defined($self->{'conn'}->{$uri}->{count}) ) { $ChkCnt = $self->{'conn'}->{$uri}->{count}; $self->{'conn'}->{$uri}->{count}++ } + &main::print_log ("[Alexa] Debug: GetChunk - Time ( $realtime ) ChunkSize: ( $size ) Count: ( $ChkCnt ) CountHash: ( $self->{'conn'}->{$uri}->{count} )\n") if $main::Debug{'alexa'}; + } + else { undef $self->{'conn'}->{$uri}->{time}; undef $self->{'conn'}->{$uri}->{count} } + my $AlexaObjChunk = $self->{child}->{$ChkCnt}; + return $AlexaObjChunk; +} + + sub DiscoverAddy { use Net::Address::Ethernet qw( :all ); my @a = get_addresses(@_); foreach my $adapter (@a) { - # print $adapter->{sIP}."\n"; - # print $adapter->{sEthernet}."\n"; - # print "____________________\n"; next unless ($adapter->{iActive} eq 1); next if ($adapter->{sEthernet} eq ''); next if ($adapter->{sIP} =~ /127\.0\.0\.1/); @@ -571,7 +598,6 @@ sub get_set_state { } elsif ( $action eq 'set' ) { my $end; - #if ($object->$statesub =~ /\%/) { $end = '%' } if ( $object->can('state_level') && $state =~ /\d+/ ) { $end = '%'} &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state$end )\n") if $main::Debug{'alexa'}; $object->$sub($state.$end); @@ -650,11 +676,20 @@ sub new { foreach my $AlexaHttpName ( keys %{$AlexaGlobal->{http_sockets}} ) { my $AlexaHttpPort = $AlexaGlobal->{http_sockets}->{$AlexaHttpName}->{port}; $self->{'ports'}->{$AlexaHttpPort} = 0; + &main::print_log ("[Alexa] Debug: Configured for port $AlexaHttpPort\n") if $main::Debug{'alexa'}; } if ( ($::config_parms{'alexaHttpPortCount'} eq 0) && ($::config_parms{'alexaHttpPort'}) ) { $self->{'ports'}->{$::config_parms{'alexaHttpPort'}} = 0; # This is to disable all MH proxy ports and use an external proxy port via Apache + &main::print_log ("[Alexa] Debug: Configured for a EXTERNAL proxy on port $::config_parms{'alexaHttpPort'}\n") if $main::Debug{'alexa'}; + } + elsif ( ($::config_parms{'alexaNoDefaultHttp'}) && ($::config_parms{'alexaHttpPort'}) ) { + #this is to disable the default MH web port and only use a proxy port + &main::print_log ("[Alexa] Debug: Configured to disable port $::config_parms{'http_port'} and proxy port $::config_parms{'alexaHttpPort'}\n") if $main::Debug{'alexa'}; + } + else { + $self->{'ports'}->{$::config_parms{'http_port'}} = 0; + &main::print_log ("[Alexa] Debug: Configured for port $::config_parms{'http_port'}\n") if $main::Debug{'alexa'}; } - else { $self->{'ports'}->{$::config_parms{'http_port'}} = 0; } if (-e $file) { my $restoredhash = retrieve($file); $self->{idmap} = $restoredhash->{idmap}; @@ -681,11 +716,34 @@ sub add { $fullname = $cleanname.'.'.$cleanname; } #use Data::Dumper; - my $uuid = $self->uuid($fullname); - + my $uuid = $self->uuid($fullname); + my $alexaObjectsPerGet = $::config_parms{'alexaObjectsPerGet'} || '60'; + + if ( $::config_parms{'alexaEnableChunked'} ) { + $self->{fulllist}->{'uuid'}->{$uuid}->{'realname'}=$realname; + $self->{fulllist}->{'uuid'}->{$uuid}->{'name'}=$name || $cleanname; + $self->{fulllist}->{'uuid'}->{$uuid}->{'sub'}=$sub || 'set'; + $self->{fulllist}->{'uuid'}->{$uuid}->{'on'}=lc($on) || 'on'; + $self->{fulllist}->{'uuid'}->{$uuid}->{'off'}=lc($off) || 'off'; + $self->{fulllist}->{'uuid'}->{$uuid}->{'statesub'}=$statesub || 'state'; + for my $count (0..5) { + my $size = keys %{$self->{$count}->{'uuid'}}; + next if ($size eq $alexaObjectsPerGet); + $self->{$count}->{'uuid'}->{$uuid}->{'realname'}=$realname; + $self->{$count}->{'uuid'}->{$uuid}->{'name'}=$name || $cleanname; + $self->{$count}->{'uuid'}->{$uuid}->{'sub'}=$sub || 'set'; + $self->{$count}->{'uuid'}->{$uuid}->{'on'}=lc($on) || 'on'; + $self->{$count}->{'uuid'}->{$uuid}->{'off'}=lc($off) || 'off'; + $self->{$count}->{'uuid'}->{$uuid}->{'statesub'}=$statesub || 'state'; + $self->{ChkCnt} = $count; + &main::print_log ("[Alexa] Debug: UUID:( $uuid ) Count: ( $count ) \n") if $main::Debug{'alexa'}; + last; + } + } + else { foreach my $port ( (sort keys %{$self->{'ports'}}) ) { my $size = keys %{$self->{$port}->{'uuid'}}; - next if ($size eq 60); + next if ($size eq $alexaObjectsPerGet); $self->{$port}->{'uuid'}->{$uuid}->{'realname'}=$realname; $self->{$port}->{'uuid'}->{$uuid}->{'name'}=$name || $cleanname; $self->{$port}->{'uuid'}->{$uuid}->{'sub'}=$sub || 'set'; @@ -694,7 +752,7 @@ sub add { $self->{$port}->{'uuid'}->{$uuid}->{'statesub'}=$statesub || 'state'; last; } - +} # $self->{8080}->{'uuid'}->{3}->{'realname'}=$realname; # $self->{8080}->{'uuid'}->{3}->{'name'}=$name || $cleanname; # $self->{8080}->{'uuid'}->{3}->{'sub'}=$sub || 'set'; From c90c97ad8b623216dbd670e8ff5cc2bff538f18f Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 14 Jan 2017 19:16:10 -0700 Subject: [PATCH 105/209] merged master --- code/common/vr_match.pl | 35 +++++++++--------- web/bin/runit.pl | 80 +++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/code/common/vr_match.pl b/code/common/vr_match.pl index bf2b7df4c..1675ed1af 100644 --- a/code/common/vr_match.pl +++ b/code/common/vr_match.pl @@ -1,16 +1,19 @@ -sub phrase_match1 { - my ($phrase) = @_; - my (%list1); - my $d_min1 = 999; - my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; - my @phrases = &Voice_Cmd::voice_items('mh', 'no_category'); - for my $phrase2 (sort @phrases) { - my $d = pdistance($phrase, $phrase2, $set1, \&distance, {-cost => [0.5,0,4], -mode => 'set'}); -# my $brianlendist = abs(length($phrase)-length($phrase2)); -# $d = $brianlendist + $d; -# print_log "---------------- $phrase --- $phrase2 --- $d"; - push @{$list1{$d}}, $phrase2 if $d <= $d_min1; - $d_min1 = $d if $d < $d_min1; - } - return ${$list1{$d_min1}}[0]; -} \ No newline at end of file +sub phrase_match1 { + my ($phrase) = @_; + my (%list1); + my $d_min1 = 999; + my $set1 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + my @phrases = &Voice_Cmd::voice_items( 'mh', 'no_category' ); + for my $phrase2 ( sort @phrases ) { + my $d = + pdistance( $phrase, $phrase2, $set1, \&distance, + { -cost => [ 0.5, 0, 4 ], -mode => 'set' } ); + + # my $brianlendist = abs(length($phrase)-length($phrase2)); + # $d = $brianlendist + $d; + # print_log "---------------- $phrase --- $phrase2 --- $d"; + push @{ $list1{$d} }, $phrase2 if $d <= $d_min1; + $d_min1 = $d if $d < $d_min1; + } + return ${ $list1{$d_min1} }[0]; +} diff --git a/web/bin/runit.pl b/web/bin/runit.pl index f4883b063..801133b20 100644 --- a/web/bin/runit.pl +++ b/web/bin/runit.pl @@ -1,38 +1,42 @@ -return &runit(@ARGV); -sub runit { - my ($cmd) = "@_"; - #print_log "-------------------- Original Command: $cmd"; - $cmd =~ s/_/ /g; - #print_log "-------------------- Manipulated Command: $cmd"; - # Look for exact command matches - if (&process_external_command($cmd, 1, 'android', 'speak')) { - print_log "-------------------- Exact Command Match $cmd"; - return &html_page('', 'done'); - } - - # added by Brian: STRIP out articles and then check for exact command match - $cmd =~ s/the //g; - $cmd =~ s/to //g; - $cmd =~ s/turn //g; - - $cmd =~ s/an //g; - $cmd =~ s/make //g; - $cmd =~ s/switch //g; - - if (&process_external_command($cmd, 1, 'android', 'speak')) { - print_log "-------------------- Exact Command Match removing articles $cmd"; - return &html_page('', 'done'); - } - - # Look for nearest fuzzy match - my $cmd1 = &phrase_match1($cmd); - print_log "-------------------- Fuzzy Command Match $cmd1"; - &process_external_command($cmd1, 1, 'android', 'speak'); - return &html_page('', 'done'); - -# # Added by Brian: Give up -# print_log "-------------------- No command found $cmd"; -# play('file' => 'c:\mh\sounds\log.wav'); -# return &html_page('', 'No command found'); - -} \ No newline at end of file +return &runit(@ARGV); + +sub runit { + my ($cmd) = "@_"; + + #print_log "-------------------- Original Command: $cmd"; + $cmd =~ s/_/ /g; + + #print_log "-------------------- Manipulated Command: $cmd"; + # Look for exact command matches + if ( &process_external_command( $cmd, 1, 'android', 'speak' ) ) { + print_log "-------------------- Exact Command Match $cmd"; + return &html_page( '', 'done' ); + } + + # added by Brian: STRIP out articles and then check for exact command match + $cmd =~ s/the //g; + $cmd =~ s/to //g; + $cmd =~ s/turn //g; + + $cmd =~ s/an //g; + $cmd =~ s/make //g; + $cmd =~ s/switch //g; + + if ( &process_external_command( $cmd, 1, 'android', 'speak' ) ) { + print_log + "-------------------- Exact Command Match removing articles $cmd"; + return &html_page( '', 'done' ); + } + + # Look for nearest fuzzy match + my $cmd1 = &phrase_match1($cmd); + print_log "-------------------- Fuzzy Command Match $cmd1"; + &process_external_command( $cmd1, 1, 'android', 'speak' ); + return &html_page( '', 'done' ); + + # # Added by Brian: Give up + # print_log "-------------------- No command found $cmd"; + # play('file' => 'c:\mh\sounds\log.wav'); + # return &html_page('', 'No command found'); + +} From f6bf50066776662a917ddae466b4e793fdc8477b Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 14 Jan 2017 19:19:04 -0700 Subject: [PATCH 106/209] IA7 collections updater, called it directly from the mh setup sub and added in dependancies to the lib --- bin/mh | 5 +- lib/ia7_utilities.pl | 140 +++++++++++++++++++++++++++---------------- 2 files changed, 91 insertions(+), 54 deletions(-) diff --git a/bin/mh b/bin/mh index 6dd31b4de..fc38d6565 100755 --- a/bin/mh +++ b/bin/mh @@ -292,7 +292,7 @@ BEGIN { chomp $autover; close(VERSION); } - if ( lc $autover eq 'unstable' && -e '../.git') { + if ( lc $autover eq 'unstable' && -e '../.git' ) { my $build_date = `git show -s --format="%ci"`; $Version_date = $build_date if ( $build_date =~ /^\d\d\d\d-\d\d-\d\d/ ); @@ -1368,6 +1368,9 @@ sub setup { print "Done with setup\n\n"; + &ia7_utilities::ia7_update_collection + ; #Check if the collections need updating. + } # Called from generic item *** and undo sub diff --git a/lib/ia7_utilities.pl b/lib/ia7_utilities.pl index 8b29a1a87..df87f77fc 100644 --- a/lib/ia7_utilities.pl +++ b/lib/ia7_utilities.pl @@ -1,68 +1,102 @@ package ia7_utilities; use strict; -use Data::Dumper; +use JSON qw(decode_json to_json); sub main::ia7_update_schedule { - my ($object,@schedules) = @_; - - &main::print_log("Updating Schedule for object $object, schedule size is " . scalar(@schedules)); - - my $obj = &main::get_object_by_name($object); - my $s=0; + my ( $object, @schedules ) = @_; + + &main::print_log( "Updating Schedule for object $object, schedule size is " + . scalar(@schedules) ); + + my $obj = &main::get_object_by_name($object); + my $s = 0; my $index; my @curr_schedule = $obj->get_schedule; $obj->reset_schedule(); - for (my $i = 1; $i <= (scalar(@schedules)/3); $i++) { - &main::print_log("Adding Schedule (id=" . $schedules[$i*3 - 3] . " cron=" . $schedules[$i*3 - 2] . " label=" . $schedules[$i*3 -1] . ")"); - $obj->set_schedule($schedules[$i*3 - 3],$schedules[$i*3 - 2],$schedules[$i*3 -1]); - } -} - + for ( my $i = 1; $i <= ( scalar(@schedules) / 3 ); $i++ ) { + my $jqCron = $schedules[ $i * 3 - 2 ]; + #jqCron uses 1-7 for Sat - Sunday, MH uses 0-6, so shift all the numbers + &main::print_log( "Adding Schedule (id=" + . $schedules[ $i * 3 - 3 ] + . " cron=" + . $schedules[ $i * 3 - 2 ] + . " label=" + . $schedules[ $i * 3 - 1 ] + . ")" ); + $obj->set_schedule( + $schedules[ $i * 3 - 3 ], + $schedules[ $i * 3 - 2 ], + $schedules[ $i * 3 - 1 ] + ); + } +} -if ($main::Startup) { - - my $ia7_coll_current_ver = 1.2; +sub ia7_update_collection { + &main::print_log("[IA7_Collection_Updater] : Starting"); + my $ia7_coll_current_ver = 1.2; - my @collection_files = ("$main::Pgm_Root/data/web/collections.json", - "$main::config_parms{data_dir}/web/collections.json", - "$main::config_parms{ia7_data_dir}/collections.json"); + my @collection_files = ( + "$main::Pgm_Root/data/web/collections.json", + "$main::config_parms{data_dir}/web/collections.json", + "$main::config_parms{ia7_data_dir}/collections.json" + ); for my $file (@collection_files) { - if (-e $file) { - &main::print_log("[IA7_Collection_Updater] : Checking $file to current version $ia7_coll_current_ver"); - my $json_data; - my $file_data; - eval { - $file_data = &main::file_read($file); - $json_data = decode_json($file_data); #HP, wrap this in eval to prevent MH crashes - }; - + if ( -e $file ) { + &main::print_log( + "[IA7_Collection_Updater] : Reviewing $file to current version $ia7_coll_current_ver" + ); + my $json_data; + my $file_data; + eval { + $file_data = &main::file_read($file); + $json_data = decode_json($file_data) + ; #HP, wrap this in eval to prevent MH crashes + }; + if ($@) { - &main::print_log("[IA7_Collection_Updater] : WARNING: decode_json failed for $file. Please check this file!"); - } else { - my $updated = 0; - - if ((! defined $json_data->{meta}->{version}) or ($json_data->{meta}->{version} < 1.2)) { #IA7 v1.2 required change - $json_data->{700}->{user} = '$Authorized' unless (defined $json_data->{700}->{user}); - my $found = 0; - foreach my $i (@{$json_data->{500}->{children}}) { - $found = 1 if ($i == 700); - } - push (@{$json_data->{500}->{children}},700) unless ($found); - $json_data->{meta}->{version} = "1.2"; - &main::print_log("[IA7_Collection_Updater] : Updating $file to version 1.2"); - $updated = 1; - } - if ($updated) { - my $json_newdata = to_json($json_data, {utf8 => 1, pretty => 1}); - my $backup_file = $file . ".t" . int( ::get_tickcount() / 1000 ) . ".backup"; - &main::file_write($backup_file,$file_data); - &main::print_log("[IA7_Collection_Updater] : Saved backup " . $file . ".t" . int( ::get_tickcount() / 1000 ) . ".backup"); - &main::file_write($file,$json_newdata); - } - } + &main::print_log( + "[IA7_Collection_Updater] : WARNING: decode_json failed for $file. Please check this file!" + ); + } + else { + my $updated = 0; + + if ( ( !defined $json_data->{meta}->{version} ) + or ( $json_data->{meta}->{version} < 1.2 ) ) + { #IA7 v1.2 required change + $json_data->{700}->{user} = '$Authorized' + unless ( defined $json_data->{700}->{user} ); + my $found = 0; + foreach my $i ( @{ $json_data->{500}->{children} } ) { + $found = 1 if ( $i == 700 ); + } + push( @{ $json_data->{500}->{children} }, 700 ) + unless ($found); + $json_data->{meta}->{version} = "1.2"; + &main::print_log( + "[IA7_Collection_Updater] : Updating $file to version 1.2" + ); + $updated = 1; + } + if ($updated) { + my $json_newdata = + to_json( $json_data, { utf8 => 1, pretty => 1 } ); + my $backup_file = + $file . ".t" + . int( ::get_tickcount() / 1000 ) + . ".backup"; + &main::file_write( $backup_file, $file_data ); + &main::print_log( "[IA7_Collection_Updater] : Saved backup " + . $file . ".t" + . int( ::get_tickcount() / 1000 ) + . ".backup" ); + &main::file_write( $file, $json_newdata ); + } + } } - } + } + &main::print_log("[IA7_Collection_Updater] : Finished"); } -1; \ No newline at end of file +1; From a24665b4cc86f01ff4251804ca51ddaac4eb956d Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 16 Jan 2017 18:04:08 -0700 Subject: [PATCH 107/209] raZberry v1.6.1 - added in ability to specify full device ID for new zwave devices --- lib/raZberry.pm | 512 ++++++++++++++++++++++++++++++------------------ 1 file changed, 323 insertions(+), 189 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index be606b082..020b24da4 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v1.6 +=head1 B v1.6.1 =head2 SYNOPSIS @@ -119,6 +119,7 @@ use warnings; use LWP::UserAgent; use HTTP::Request::Common qw(POST); use JSON qw(decode_json); + #use Data::Dumper; @raZberry::ISA = ('Generic_Item'); @@ -160,9 +161,11 @@ sub new { $self->{data} = undef; $self->{child_object} = undef; $self->{config}->{poll_seconds} = 5; - $self->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} if ( defined $main::config_parms{raZberry_poll_seconds} ); + $self->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} + if ( defined $main::config_parms{raZberry_poll_seconds} ); $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{config}->{poll_seconds} = 1 + if ( $self->{config}->{poll_seconds} < 1 ); $self->{updating} = 0; $self->{data}->{retry} = 0; my ( $host, $port ) = ( split /:/, $addr )[ 0, 1 ]; @@ -200,48 +203,96 @@ sub poll { } for my $dev ( keys %{ $self->{data}->{ping} } ) { - &main::print_log("[raZberry] Keep_alive: Pinging device $dev..."); # if ($self->{debug}); - &main::print_log("[raZberry] ping_dev $dev"); # if ($self->{debug}); - #$self->ping_dev($dev); + &main::print_log("[raZberry] Keep_alive: Pinging device $dev...") + ; # if ($self->{debug}); + &main::print_log("[raZberry] ping_dev $dev"); # if ($self->{debug}); + #$self->ping_dev($dev); } - my ( $isSuccessResponse1, $devices ) = _get_JSON_data( $self, 'devices', $cmd ); -# print Dumper $devices if ( $self->{debug} > 1 ); + my ( $isSuccessResponse1, $devices ) = + _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] Found:" . $item->{id} . " with level " . $item->{metrics}->{level} . " and updated " . $item->{updateTime} . "." ) if ( $self->{debug} ); - my ($id) = ( split /_/, $item->{id} )[2]; - - print "id=$id\n" if ($self->{debug} > 1); + &main::print_log( "[raZberry] 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 ); my $battery_dev = 0; - $battery_dev = 1 if ($id =~ m/-0-128$/); - if ($battery_dev) { #for a battery, set a different object - $self->{data}->{devices}->{$id}->{battery_level} = $item->{metrics}->{level}; - } else { - $self->{data}->{devices}->{$id}->{level} = $item->{metrics}->{level}; + $battery_dev = 1 if ( $id =~ m/-0-128$/ ); + if ($battery_dev) { #for a battery, set a different object + $self->{data}->{devices}->{$id}->{battery_level} = + $item->{metrics}->{level}; + } + 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}->{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->{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] Child object detected: Battery Level:[" . $item->{metrics}->{level} . "] Child Level:[" . $self->{child_object}->{$id}->battery_level() . "]" ) if ( $self->{debug} > 1 ); - $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] 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 ($battery_dev) { + &main::print_log( + "[raZberry] Child object detected: Battery Level:[" + . $item->{metrics}->{level} + . "] Child Level:[" + . $self->{child_object}->{$id}->battery_level() + . "]" ) + if ( $self->{debug} > 1 ); + $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] 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 + } } } } @@ -275,7 +326,7 @@ sub set_dev { return ('0'); } -# print Dumper $status if ( $self->{debug} > 1 ); + # print Dumper $status if ( $self->{debug} > 1 ); } } @@ -362,14 +413,19 @@ sub _get_JSON_data { my $params = ""; $params = $cmd if ($cmd); my $method = "ZAutomation/api/v1"; - $method = "ZWaveAPI/Run" if ( ( $mode eq "force_update" ) + $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" ) ); - &main::print_log("[raZberry] contacting http://$host:$port/$method/$rest{$mode}$params") if ( $self->{debug} ); + &main::print_log( + "[raZberry] contacting http://$host:$port/$method/$rest{$mode}$params" + ) if ( $self->{debug} ); - my $request = HTTP::Request->new( GET => "http://$host:$port/$method/$rest{$mode}$params" ); + my $request = + HTTP::Request->new( + GET => "http://$host:$port/$method/$rest{$mode}$params" ); $request->content_type("application/x-www-form-urlencoded"); my $responseObj = $ua->request($request); @@ -415,14 +471,17 @@ sub _get_JSON_data { 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 + + # 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]: WARNING: decode_json failed for returned data"); - return ("0",""); + &main::print_log( + "[raZberry]: WARNING: decode_json failed for returned data"); + return ( "0", "" ); } return ( $isSuccessResponse, $response ) @@ -490,20 +549,25 @@ 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] Registering " . $type . " Device ID $dev to controller"); + #TODO + my $type = $object->{type}; + $type = "Digital " . $type + if ( ( defined $options ) and ( $options =~ m/digital/ ) ); + &main::print_log( "[raZberry] Registering " + . $type + . " Device ID $dev to controller" ); $self->{child_object}->{$dev} = $object; if ( defined $options ) { if ( $options =~ m/force_update/ ) { $self->{data}->{force_update}->{$dev} = 1; - &main::print_log("[raZberry] Forcing Controller to contact Device $dev at each poll" + &main::print_log( + "[raZberry] Forcing Controller to contact Device $dev at each poll" ); } if ( $options =~ m/keep_alive/ ) { $self->{data}->{ping}->{$dev} = 1; - &main::print_log("[raZberry] Forcing Controller to ping Device $dev at each poll" + &main::print_log( + "[raZberry] Forcing Controller to ping Device $dev at each poll" ); } } @@ -526,9 +590,9 @@ sub new { ); $$self{master_object} = $object; - $devid = $devid . $zway_suffix unless ( $devid =~ m/-\d+-\d+$/ ); + $devid = $devid . $zway_suffix if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; - $$self{type} = "Dimmer"; + $$self{type} = "Dimmer"; $object->register( $self, $devid, $options ); #$self->set($object->get_dev_status,$devid,'poll'); @@ -612,7 +676,7 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; + my ( $self, $data ) = @_; } package raZberry_switch; @@ -624,12 +688,12 @@ sub new { my $self = {}; bless $self, $class; - push(@{ $$self{states} }, 'off', 'on', ); + push( @{ $$self{states} }, 'off', 'on', ); - $$self{master_object} = $object; - $devid = $devid . "-0-37" unless ( $devid =~ m/-\d+-\d+$/ ); + $$self{master_object} = $object; + $devid = $devid . "-0-37" if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; - $$self{type} = "Switch"; + $$self{type} = "Switch"; $object->register( $self, $devid, $options ); #$self->set($object->get_dev_status,$devid,'poll'); @@ -643,14 +707,17 @@ sub set { my ( $self, $p_state, $p_setby ) = @_; if ( $p_setby eq 'poll' ) { - if (lc $p_state eq "on" ) { + if ( lc $p_state eq "on" ) { $self->{level} = 100; } - elsif (lc $p_state eq "off" ) { + elsif ( lc $p_state eq "off" ) { $self->{level} = 0; } - - main::print_log("[raZberry_switch] Setting value to $p_state. Level is " . $self->{level} ) if ( $self->{debug} ); + + main::print_log( + "[raZberry_switch] Setting value to $p_state. Level is " + . $self->{level} ) + if ( $self->{debug} ); $self->SUPER::set($p_state); } else { @@ -683,7 +750,7 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; + my ( $self, $data ) = @_; } package raZberry_blind; @@ -714,34 +781,42 @@ sub new { $$self{master_object} = $object; my $devid_battery = $devid . "-0-128"; - $devid = $devid . $zway_suffix unless ( $devid =~ m/-\d+-\d+$/ ); + $devid = $devid . $zway_suffix if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; - $$self{type} = "Blind"; + $$self{type} = "Blind"; $object->register( $self, $devid, $options ); + #$self->set($object->get_dev_status,$devid,'poll'); - $self->{level} = ""; - $self->{debug} = $object->{debug}; - $self->{digital} = 0; - $self->{digital} = 1 if ((defined $options) and ($options =~ m/digital/i)); - if ($self->{digital} ) { - push( @{ $$self{states} }, 'down', '10%','20%','30%','40%','50%','60%','70%','80%','90%','up' ); - } else { - push( @{ $$self{states} }, 'down', 'stop', 'up' ); - } - $self->{battery} = 1 if ((defined $options) and ($options =~ m/battery/i)); - if ($self->{battery} ) { - $$self{battery_level} = ""; - $$self{devid_battery} = $devid_battery; - $$self{type} = "Blind.Battery"; - - $object->register( $self, $devid_battery, $options ); + $self->{level} = ""; + $self->{debug} = $object->{debug}; + $self->{digital} = 0; + $self->{digital} = 1 + if ( ( defined $options ) and ( $options =~ m/digital/i ) ); + if ( $self->{digital} ) { + push( + @{ $$self{states} }, + 'down', '10%', '20%', '30%', '40%', '50%', + '60%', '70%', '80%', '90%', 'up' + ); + } + else { + push( @{ $$self{states} }, 'down', 'stop', 'up' ); + } + $self->{battery} = 1 + if ( ( defined $options ) and ( $options =~ m/battery/i ) ); + if ( $self->{battery} ) { + $$self{battery_level} = ""; + $$self{devid_battery} = $devid_battery; + $$self{type} = "Blind.Battery"; + + $object->register( $self, $devid_battery, $options ); $self->{battery_alert} = 0; - $self->{battery_poll_seconds} = 12 * 60 * 60; - $self->{battery_timer} = new Timer; - $self->_battery_timer; - } - + $self->{battery_poll_seconds} = 12 * 60 * 60; + $self->{battery_timer} = new Timer; + $self->_battery_timer; + } + return $self; } @@ -754,43 +829,54 @@ sub set { my $n_state; if ( $p_state == 0 ) { $n_state = "down"; - } else { - if ($self->{digital}) { - if ($p_state >= 99) { - $n_state = "up"; - } else { - $n_state = "$p_state%"; - } - } else { - $n_state = "up"; - } - } + } + else { + if ( $self->{digital} ) { + if ( $p_state >= 99 ) { + $n_state = "up"; + } + else { + $n_state = "$p_state%"; + } + } + else { + $n_state = "up"; + } + } # stop level? - main::print_log( "[raZberry_blind] Setting value to $n_state. Level is " . $self->{level} )if ( $self->{debug} ); - $self->SUPER::set($n_state); + main::print_log( "[raZberry_blind] Setting value to $n_state. Level is " + . $self->{level} ) + if ( $self->{debug} ); + $self->SUPER::set($n_state); } else { - if ($self->{digital}) { - if ( lc $p_state eq "down" ) { - $$self{master_object}->set_dev( $$self{devid}, $p_state ); - } - elsif ( lc $p_state eq "up" ) { - $$self{master_object}->set_dev( $$self{devid}, "level=100" ); - } - elsif (($p_state eq "100%") or ($p_state =~ m/^\d{1,2}\%$/)) { - my ($n_state) = ($p_state =~ /(\d+)%/); - $$self{master_object}->set_dev($$self{devid},"level=$n_state"); + if ( $self->{digital} ) { + if ( lc $p_state eq "down" ) { + $$self{master_object}->set_dev( $$self{devid}, $p_state ); + } + elsif ( lc $p_state eq "up" ) { + $$self{master_object}->set_dev( $$self{devid}, "level=100" ); } - else { - main::print_log("[raZberry_blind] Error. Unknown set state $p_state"); - } - } - elsif (( lc $p_state eq "up" ) or ( lc $p_state eq "down" ) or ( lc $p_state eq "stop" )) { + elsif ( ( $p_state eq "100%" ) or ( $p_state =~ m/^\d{1,2}\%$/ ) ) { + my ($n_state) = ( $p_state =~ /(\d+)%/ ); + $$self{master_object} + ->set_dev( $$self{devid}, "level=$n_state" ); + } + else { + main::print_log( + "[raZberry_blind] Error. Unknown set state $p_state"); + } + } + elsif (( lc $p_state eq "up" ) + or ( lc $p_state eq "down" ) + or ( lc $p_state eq "stop" ) ) + { $$self{master_object}->set_dev( $$self{devid}, $p_state ); } else { - main::print_log("[raZberry_blind] Error. Unknown set state $p_state"); + main::print_log( + "[raZberry_blind] Error. Unknown set state $p_state"); } } } @@ -814,25 +900,33 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; - if (defined $data->{battery_level}) { - &main::print_log( "[raZberry_blind] Setting battery value to " . $data->{battery_level} . ".") if ( $self->{debug} ); - $self->{battery_level} = $data->{battery_level}; - } + my ( $self, $data ) = @_; + if ( defined $data->{battery_level} ) { + &main::print_log( "[raZberry_blind] Setting battery value to " + . $data->{battery_level} + . "." ) + if ( $self->{debug} ); + $self->{battery_level} = $data->{battery_level}; + } } sub battery_check { my ($self) = @_; - unless ($self->{battery}) { - main::print_log("[raZberry_blind] ERROR, battery option not defined on this object"); + unless ( $self->{battery} ) { + main::print_log( + "[raZberry_blind] ERROR, battery option not defined on this object" + ); return; - } - + } + if ( $self->{battery_level} eq "" ) { - main::print_log("[raZberry_blind] INFO Battery level currently undefined"); + main::print_log( + "[raZberry_blind] INFO Battery level currently undefined"); return; } - main::print_log("[raZberry_blind] INFO Battery currently at " . $self->{battery_level} . "%" ); + 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"); @@ -860,6 +954,7 @@ package raZberry_lock; #only tested with Kwikset 914 @raZberry_lock::ISA = ('Generic_Item'); + #use Data::Dumper; sub new { @@ -871,17 +966,17 @@ sub new { $$self{master_object} = $object; my $devid_battery = $devid . "-0-128"; - $devid = $devid . "-0-98"; + $devid = $devid . "-0-98" if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; $$self{devid_battery} = $devid_battery; - $$self{type} = "Lock.Battery"; + $$self{type} = "Lock.Battery"; $object->register( $self, $devid, $options ); $object->register( $self, $devid_battery, $options ); #$self->set($object->get_dev_status,$devid,'poll'); $self->{level} = ""; - $self->{battery_level} = ""; + $self->{battery_level} = ""; $self->{user_data_delay} = 10; $self->{battery_alert} = 0; $self->{battery_poll_seconds} = 12 * 60 * 60; @@ -897,8 +992,8 @@ sub set { # if level is open/closed its the state. if level is a number its the battery # object states are locked and unlocked, but zwave sees close and open my %map_states; - $p_state = "locked" if (lc $p_state eq "lock"); - $p_state = "unlocked" if (lc $p_state eq "unlock"); + $p_state = "locked" if ( lc $p_state eq "lock" ); + $p_state = "unlocked" if ( lc $p_state eq "unlock" ); $map_states{close} = "locked"; $map_states{open} = "unlocked"; $map_states{locked} = "close"; @@ -959,20 +1054,26 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; - if (defined $data->{battery_level}) { - &main::print_log( "[raZberry_lock] Setting battery value to " . $data->{battery_level} . ".") if ( $self->{debug} ); - $self->{battery_level} = $data->{battery_level}; - } + my ( $self, $data ) = @_; + if ( defined $data->{battery_level} ) { + &main::print_log( "[raZberry_lock] Setting battery value to " + . $data->{battery_level} + . "." ) + if ( $self->{debug} ); + $self->{battery_level} = $data->{battery_level}; + } } sub battery_check { my ($self) = @_; if ( $self->{battery_level} eq "" ) { - &main::print_log("[raZberry_lock] INFO Battery level currently undefined"); + &main::print_log( + "[raZberry_lock] INFO Battery level currently undefined"); return; } - &main::print_log("[raZberry_lock] INFO Battery currently at " . $self->{battery_level} . "%" ); + &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"); @@ -1092,7 +1193,8 @@ sub _update_users { $self->{data}->{retry}++; return ('0'); } -# print Dumper $response if ( $self->{debug} > 1 ); + + # print Dumper $response if ( $self->{debug} > 1 ); foreach my $key ( keys %{$response} ) { if ( $key =~ m/^[0-9]*$/ ) { #a number, so a user code $self->{users}->{"$key"}->{status} = @@ -1130,7 +1232,7 @@ sub set { } sub update_data { - my ($self,$data) = @_; + my ( $self, $data ) = @_; } package raZberry_thermostat; @@ -1142,23 +1244,32 @@ sub new { my $self = {}; bless $self, $class; - if ((defined $deg) and (lc $deg eq "f")) { - push(@{ $$self{states} },60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80); - $self->{units} = "F"; - $self->{min_temp} = 58; - $self->{max_temp} = 80; - - } else { - push(@{ $$self{states} },12,13,14,15,16,17,18,19,20,21,22,23,24,25,16,27,28,29,30); - $self->{units} = "C"; - $self->{min_temp} = 10; - $self->{max_temp} = 30; - } + if ( ( defined $deg ) and ( lc $deg eq "f" ) ) { + push( + @{ $$self{states} }, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 + ); + $self->{units} = "F"; + $self->{min_temp} = 58; + $self->{max_temp} = 80; + + } + else { + push( + @{ $$self{states} }, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 16, 27, 28, 29, 30 + ); + $self->{units} = "C"; + $self->{min_temp} = 10; + $self->{max_temp} = 30; + } $$self{master_object} = $object; - $devid = $devid . "-0-67" unless ( $devid =~ m/-\d+-\d+$/ ); + $devid = $devid . "-0-67" if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; - $$self{type} = "Thermostat"; + $$self{type} = "Thermostat"; $object->register( $self, $devid, $options ); @@ -1176,11 +1287,17 @@ sub set { $self->SUPER::set($p_state); } else { - if (($p_state < $self->{min_temp}) or ($p_state > $self->{max_temp})) { - main::pring_log("[raZberry]: WARNING not setting level to $p_state since out of bounds " . $self->{min_temp} . ":" . $self->{max_temp}); - } else { - $$self{master_object}->set_dev( $$self{devid}, "level=$p_state" ); - } + if ( ( $p_state < $self->{min_temp} ) + or ( $p_state > $self->{max_temp} ) ) + { + main::pring_log( + "[raZberry]: WARNING not setting level to $p_state since out of bounds " + . $self->{min_temp} . ":" + . $self->{max_temp} ); + } + else { + $$self{master_object}->set_dev( $$self{devid}, "level=$p_state" ); + } } } @@ -1209,18 +1326,26 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; - #if units is F then rescale states - - if ($data->{units} =~ m/F/) { - @{ $$self{states} } = ( - 58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80 - ); - } - $self->{min_temp} = $data->{temp_min}; - $self->{max_temp} = $data->{temp_max}; - main::print_log("In set, units = " . $data->{units} . " max = " . $data->{temp_max} . " min = " . $data->{temp_min}) if ($self->{debug}); - + my ( $self, $data ) = @_; + + #if units is F then rescale states + + if ( $data->{units} =~ m/F/ ) { + @{ $$self{states} } = ( + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 + ); + } + $self->{min_temp} = $data->{temp_min}; + $self->{max_temp} = $data->{temp_max}; + main::print_log( "In set, units = " + . $data->{units} + . " max = " + . $data->{temp_max} + . " min = " + . $data->{temp_min} ) + if ( $self->{debug} ); + } package raZberry_temp_sensor; @@ -1234,9 +1359,9 @@ sub new { bless $self, $class; $$self{master_object} = $object; - $devid = $devid . "-0-49-1" unless ( $devid =~ m/-\d+-\d+$/ ); + $devid = $devid . "-0-49-1" if ( $devid =~ m/^\d+$/ ); $$self{devid} = $devid; - $$self{type} = "Thermostat Sensor"; + $$self{type} = "Thermostat Sensor"; $object->register( $self, $devid, $options ); @@ -1266,7 +1391,6 @@ sub ping { $$self{master_object}->ping_dev( $$self{devid} ); } - sub isfailed { my ($self) = @_; @@ -1274,7 +1398,7 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; + my ( $self, $data ) = @_; } package raZberry_binary_sensor; @@ -1285,11 +1409,12 @@ sub new { my $self = {}; bless $self, $class; + #push( @{ $$self{states} }, 'on', 'off'); I'm not sure we should set the states here, since it's not a controlable item? $$self{master_object} = $object; - $devid = $devid . "-0-48-1" unless ( $devid =~ m/-\d+-\d+/ ); - $$self{type} = "Binary Sensor"; + $devid = $devid . "-0-48-1" if ( $devid =~ m/^\d+$/ ); + $$self{type} = "Binary Sensor"; $$self{devid} = $devid; $object->register( $self, $devid, $options ); @@ -1319,7 +1444,7 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; + my ( $self, $data ) = @_; } package raZberry_openclose; @@ -1328,7 +1453,8 @@ package raZberry_openclose; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = $class->SUPER::new($object, $devid, $options); + my $self = $class->SUPER::new( $object, $devid, $options ); + #$$self{states} = (); #push( @{ $$self{states} }, 'open', 'closed'); return $self; @@ -1346,11 +1472,15 @@ 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 { - main::print_log("[raZberry] ERROR Can not set state $p_state for openclose"); + main::print_log( + "[raZberry] ERROR Can not set state $p_state for openclose"); } } @@ -1365,12 +1495,12 @@ sub new { bless $self, $class; push( @{ $$self{states} }, 'locked', 'unlocked' ); - $$self{master_object} = $object; - $devid = $devid . "-0-128"; - $$self{devid} = $devid; - $$self{type} = "Battery"; + $$self{master_object} = $object; + $devid = $devid . "-0-128" if ( $devid =~ m/^\d+$/ ); + $$self{devid} = $devid; + $$self{type} = "Battery"; - $object->register( $self, $devid, $options ); + $object->register( $self, $devid, $options ); #$self->set($object->get_dev_status,$devid,'poll'); $self->{battery_level} = ""; @@ -1378,7 +1508,8 @@ sub new { $self->{battery_poll_seconds} = 12 * 60 * 60; $self->{battery_timer} = new Timer; $self->{debug} = $object->{debug}; -# $self->_battery_timer; + + # $self->_battery_timer; return $self; } @@ -1401,20 +1532,23 @@ sub isfailed { } sub update_data { - my ($self,$data) = @_; - - $self->{battery_level} = $data->{battery_level}; - $self->SUPER::set($self->{battery_level}); - + my ( $self, $data ) = @_; + + $self->{battery_level} = $data->{battery_level}; + $self->SUPER::set( $self->{battery_level} ); + } sub battery_check { my ($self) = @_; if ( $self->{battery_level} eq "" ) { - main::print_log("[raZberry_battery] INFO Battery level currently undefined"); + main::print_log( + "[raZberry_battery] INFO Battery level currently undefined"); return; } - main::print_log("[raZberry_battery] INFO Battery currently at " . $self->{battery_level} . "%" ); + 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"); From 0f5598228be206537e5be42feceead8c5011d9e7 Mon Sep 17 00:00:00 2001 From: H Plato Date: Tue, 17 Jan 2017 17:33:53 -0700 Subject: [PATCH 108/209] v2.0.16 - cleaned up some error processing, and added in option to allow get_url to fetch via https --- lib/Venstar_Colortouch.pm | 113 +++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 45 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 35d5a1d9a..010c7e777 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,7 +1,8 @@ package Venstar_Colortouch; -# v2.0.14 -# background > 2 for polling +# v2.0.16 + +#added in https support and don't retry commands that have a valid error reason code. Only retry if the device doesn't respond. (ie error 500) #check linesl829 l477 l493 l203 #TODO - check that data is current before issuing command. within 2 poll periods. @@ -17,7 +18,7 @@ use JSON::XS; use Data::Dumper; # Venstar::Colortouch Objects -# $stat_upper = new Venstar_Colortouch('192.168.0.100'); +# $stat_upper = new Venstar_Colortouch('192.168.0.100',,"ssl,debug=1"); # # $stat_upper_mode = new Venstar_Colortouch_Mode($stat_upper); # $stat_upper_temp = new Venstar_Colortouch_Temp($stat_upper); @@ -74,7 +75,7 @@ $rest{control} = "control"; $rest{settings} = "settings"; sub new { - my ( $class, $host, $poll, $debug ) = @_; + my ( $class, $host, $poll, $options ) = @_; my $self = {}; bless $self, $class; $self->{data} = undef; @@ -92,12 +93,15 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{host} = $host; + $self->{method} = "http"; + $self->{method} = "https" if ( $options =~ m/ssl/i ); $self->{debug} = 0; - $self->{debug} = $debug if ($debug); - $self->{debug} = 0 if ( $self->{debug} < 0 ); - $self->{loglevel} = 1; - $self->{status} = ""; - $self->{timeout} = 15; #for http direct mode; + ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) + if ( $options =~ m/debug\=/i ); + $self->{debug} = 0 if ( $self->{debug} < 0 ); + $self->{loglevel} = 1; + $self->{status} = ""; + $self->{timeout} = 15; #for http direct mode; $self->{background} = 2; #0 for direct, 1 for set commands, 2 for poll and set commands $self->{poll_data_timestamp} = 0; @@ -307,9 +311,9 @@ sub process_check { # catch crashes: if ($@) { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR! JSON file parser crashed! $@\n"; + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] ERROR! JSON file parser crashed! $@\n" ); $com_status = "offline"; } else { @@ -328,9 +332,10 @@ sub process_check { $self->process_data(); } else { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR! Returned data not structured! Not processing..."; + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] ERROR! Returned data not structured! Not processing..." + ); $com_status = "offline"; } } @@ -388,9 +393,9 @@ sub process_check { # catch crashes: if ($@) { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR! JSON file parser crashed! $@\n"; + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] ERROR! JSON file parser crashed! $@\n" ); $com_status = "offline"; } @@ -403,30 +408,41 @@ sub process_check { $self->poll; } else { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] WARNING Issued command was unsuccessful (success=" - . $data->{success} - . ") , retrying..."; - if ( $self->{cmd_process_retry} > - $self->{cmd_process_retry_limit} ) - { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR Issued command max retries reached. Abandoning command attempt..."; + if ( defined $data->{reason} ) { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] WARNING Issued command was unsuccessful (reason=" + . $data->{reason} + . ")" ); shift @{ $self->{cmd_queue} }; - $self->{cmd_process_retry} = 0; - $com_status = "offline"; } else { - $self->{cmd_process_retry}++; + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] WARNING Issued command was unsuccessful with no returned reason , retrying..." + ); + if ( $self->{cmd_process_retry} > + $self->{cmd_process_retry_limit} ) + { + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] ERROR Issued command max retries reached. Abandoning command attempt..." + ); + shift @{ $self->{cmd_queue} }; + $self->{cmd_process_retry} = 0; + $com_status = "offline"; + } + else { + $self->{cmd_process_retry}++; + } } } } else { - print "[Venstar Colortouch:" - . $self->{data}->{name} - . "] ERROR! Returned data not structured! Not processing..."; + print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] ERROR! Returned data not structured! Not processing..." + ); $com_status = "offline"; } } @@ -441,7 +457,7 @@ sub process_check { . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) - if ( $self->{debug} ); + if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); } if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} ne $com_status ) { @@ -465,7 +481,11 @@ sub _get_JSON_data { if ( ( $self->{background} > 1 ) and ( lc $method ne "direct" ) ) { - my $cmd = 'get_url "http://' . $self->{host} . "/$rest{$mode}" . '"'; + my $cmd = + 'get_url "' + . $self->{method} . '://' + . $self->{host} + . "/$rest{$mode}" . '"'; if ( $self->{poll_process}->done() ) { $self->{poll_process}->set($cmd); $self->{poll_process}->start(); @@ -524,7 +544,8 @@ sub _get_JSON_data { my $host = $self->{host}; my $request = - HTTP::Request->new( POST => "http://$host/$rest{$mode}" ); + HTTP::Request->new( + POST => $self->{method} . "://$host/$rest{$mode}" ); $request->content_type("application/x-www-form-urlencoded"); my $responseObj = $ua->request($request); @@ -1002,8 +1023,8 @@ sub _push_JSON_data { $response = "success"; my $cmd = 'get_url -post "' - . $cmd - . '" "http://' + . $cmd . '" "' + . $self->{method} . '://' . $self->{host} . "/$rest{$type}" . '"'; push @{ $self->{cmd_queue} }, "$cmd"; @@ -1045,16 +1066,18 @@ sub _push_JSON_data { my $host = $self->{host}; - my $request = HTTP::Request->new( POST => "http://$host/$rest{$type}" ); + my $request = + HTTP::Request->new( + POST => $self->{method} . "://$host/$rest{$type}" ); $request->content_type("application/x-www-form-urlencoded"); $request->content($cmd) if $cmd; my $responseObj = $ua->request($request); print $responseObj->content . "\n--------------------\n" - if $self->{debug}; + if ( $self->{debug} ); my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" if $self->{debug}; + print 'Response code: ' . $responseCode . "\n" if ( $self->{debug} ); $isSuccessResponse = $responseCode < 400; if ( !$isSuccessResponse ) { main::print_log( "[Venstar Colortouch:" @@ -2439,7 +2462,7 @@ sub set { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] DB super::set, in master set, p_state=$p_state, p_setby=$p_setby" - ); + ) if ( $self->{debug} ); $self->SUPER::set($p_state); $self->start_timer if ( $self->{background} == 2 ); @@ -2449,7 +2472,7 @@ sub set { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] DB set_mode, in master set, p_state=$p_state, p_setby=$p_setby" - ); + ) if ( $self->{debug} ); $self->set_mode($p_state); } From 52224e57c4aad340250dee96592f6892de31bd03 Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 21 Jan 2017 12:14:16 -0600 Subject: [PATCH 109/209] Fixed issue with Google Home stuck connections. Added 'set' and 'state' when calling a configured sub so a real state can be returned by the sub --- lib/AlexaBridge.pm | 106 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 44c349ab9..cc99cc6a0 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -1,3 +1,38 @@ +#For Google Home and a reverse proxy (Apache/IIS/etc): +#alexa_enable = 1 +#alexaHttpPortCount = 0 # disables all proxy ports +#alexaHttpPort = 80 # tells the module to send port 80 in the SSDP response and look for port 80 in the HTTP host header +#alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response +# +#For Google Home using the builtin proxy port: +#alexa_enable = 1 +#alexaHttpPortCount = 1 # Open 1 proxy port on port 80 (We default to port 80 so no need to define it) +#alexaNoDefaultHttp = 1 # Disable responding on the default MH web port because Google Home will not use it any way. +#alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response +# +# +#For Echo (Chunked method): +#alexa_enable = 1 +#alexaEnableChunked = 1 +# +# +#For Echo (Multi-port method): +## This method should not be needed unless for some reason your Echo does not work with the Chunked method. +#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. +#alexaHttpPort=8085 # The proxy port will be on port 8085, this port should be higher than the MH web port so it is used first. +# +# +# +#alexa_enable # Enable the module +#alexaEnableChunked # Enable chunked return method (For the Echo) +#alexaHttpPortCount # Amount of proxy ports to open +#alexaNoDefaultHttp # Disable responding on the default MH web port +#alexaObjectsPerGet # Amount of MH objects we return per GET from the Echo/GH +#alexaHttpPort # First proxy port number +#alexaMac # This is used in the SSDP response, We discover it so it does not need to be defined uless something goes wrong +#alexaHttpIp # This is the IP of the local MH server, We discover it so it does not need to be defined uless something goes wrong + package AlexaBridge; @AlexaBridge::ISA = ('Generic_Item'); @@ -129,6 +164,7 @@ sub check_for_data { } &_sendHttpData($alexa_listen, $alexa_http_sender); + &close_stuck_sockets($alexa_listen, $AlexaHttpName); #This closes the oldest connection from a source IP if a second one is made. Fix for GH stuck connections # } @@ -240,6 +276,31 @@ sub _sendSearchResponse { } } + +sub close_stuck_sockets { +my ($alexa_listen, $AlexaHttpName) = @_; + my $current_client_ip = $alexa_listen->peer; + $current_client_ip =~ s/:.*//; + my $current_client_port = $alexa_listen->peer; + $current_client_port =~ s/.*\://; + if ( (scalar @{ $::Socket_Ports{$AlexaHttpName}{clients} }) > 1 ) { + for my $ptr ( @{ $::Socket_Ports{$AlexaHttpName}{clients} } ) { + my ( $socka, $client_ip_address, $client_port, $data ) = @{$ptr}; + next if ( ($client_ip_address eq $current_client_ip) && ($client_port eq $current_client_port)); + if ($client_ip_address eq $current_client_ip) { + $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time} = time unless $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time}; + if ( (time - $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time}) ge 60 ) { + close $socka if $socka; + delete $AlexaGlobal->{http_client}->{$client_ip_address}; + &main::print_log( "[Alexa] Debug: Client count: ".(scalar @{ $::Socket_Ports{$AlexaHttpName}{clients} }) ." closing $client_ip_address : $client_port") if $main::Debug{'alexa'} >= 2; + } + } + } + + } +} + + sub process_http { unless ($::config_parms{'alexa_enable'}) { return 0 } @@ -505,11 +566,12 @@ my ($AlexaObjects,$AlexaObjChunk); $output .= "Content-Length: ". (length $content) ."\r\n"; $output .= "Date: ". time2str(time)."\r\n"; $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.."; } - &main::print_log ("[Alexa] Debug: MH Response $output.$debugcontent \n") if $main::Debug{'alexa'} >= 2; + &main::print_log ("[Alexa] Debug: MH Response $debugcontent \n") if $main::Debug{'alexa'} >= 2; return $output; } else { return 0 } @@ -528,9 +590,12 @@ sub _Gzip { sub _GetChunk { my ( $self,$uri ) = @_; - use Time::HiRes qw(clock_gettime); - my $realtime = clock_gettime(CLOCK_REALTIME); - $self->{'conn'}->{$uri}->{time} = clock_gettime(CLOCK_REALTIME) unless $self->{'conn'}->{$uri}->{time}; + #use Time::HiRes qw(clock_gettime); + use Time::HiRes qw(time); + #my $realtime = clock_gettime(CLOCK_REALTIME); + my $realtime = time; + #$self->{'conn'}->{$uri}->{time} = clock_gettime(CLOCK_REALTIME) unless $self->{'conn'}->{$uri}->{time}; + $self->{'conn'}->{$uri}->{time} = time unless $self->{'conn'}->{$uri}->{time}; $self->{'conn'}->{$uri}->{count} = 0 unless defined($self->{'conn'}->{$uri}->{count}); if ( ($realtime - $self->{'conn'}->{$uri}->{time}) <= .7 ) { @@ -597,10 +662,10 @@ sub get_set_state { return $return; } elsif ( $action eq 'set' ) { - my $end; - if ( $object->can('state_level') && $state =~ /\d+/ ) { $end = '%'} - &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state$end )\n") if $main::Debug{'alexa'}; - $object->$sub($state.$end); + if ( $object->can('state_level') && $state =~ /\d+/ ) { $state = $state.'%'} + &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state )\n") if $main::Debug{'alexa'}; + if ( lc($type) =~ /clipsal_cbus/ ) { $object->$sub($state,'Alexa') } + else { $object->$sub($state) } return; } } @@ -617,15 +682,24 @@ sub get_set_state { } elsif ( ref($sub) eq 'CODE' ) { - if ( $action eq 'set' ) { - &main::print_log ("[Alexa] Debug: running sub: $sub( $state ) \n") if $main::Debug{'alexa'}; - &{$sub}($state); - return; - } - elsif ( $action eq 'get' ) { - return qq["on":true,"bri":254]; - } + if ( $action eq 'set' ) { + &main::print_log ("[Alexa] Debug: running sub: $sub( set, $state ) \n") if $main::Debug{'alexa'}; + &{$sub}('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; + } + return qq["on":true,"bri":254]; + } } + } sub get_state { From b3ce83db24dae431873f9ef7a46837448e684ce3 Mon Sep 17 00:00:00 2001 From: CityDweller Date: Mon, 23 Jan 2017 21:28:13 -0500 Subject: [PATCH 110/209] update to contain name of object when there is an error --- lib/UPBPIM.pm | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/UPBPIM.pm b/lib/UPBPIM.pm index 62be7e956..fa4719416 100644 --- a/lib/UPBPIM.pm +++ b/lib/UPBPIM.pm @@ -334,9 +334,28 @@ sub _parse_data { #UPB No Acknowledgement elsif ( uc( substr( $data, 1, 1 ) ) eq 'N' ) { $$self{xmit_in_progress} = 0; - &::print_log( - "$self->object_name: Reports device does not respond"); - pop( @{ $$self{command_stack} } ); + + + my $pop = pop( @{ $$self{command_stack} } ); + my $destination = unpack( "C", pack( "H*", substr( $pop, 6, 2 ) ) ); + my $msgid = unpack( "C", pack( "H*", substr( $pop, 10, 2 ) ) ); + my $command; + for my $key ( keys %UPB_Device::message_types ) + { + if ($UPB_Device::message_types{$key} == $msgid) + { + $command = $key; + last; + } + } + for my $obj ( @{ $$self{objects} } ) + { + if ($obj->device_id() == $destination ) + { + #&::print_log("$self->object_name: Reports device does not respond; LastCommand: $pop"); + &::print_log("UPBPIM reports that device: " . $obj->get_object_name . " does not respond to command: $command"); + } + } select( undef, undef, undef, .15 ); $self->process_command_stack(); } From 71e768c9977d8fe2debac709e37977455d926eba Mon Sep 17 00:00:00 2001 From: hplato Date: Fri, 27 Jan 2017 13:10:54 -0700 Subject: [PATCH 111/209] v2.0 pre - design to allow razberry controller to push updates rather than MH polling --- lib/raZberry.pm | 185 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 133 insertions(+), 52 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 020b24da4..190af438c 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v1.6.1 +=head1 B v2.0 =head2 SYNOPSIS @@ -111,6 +111,7 @@ raZberry_poll_seconds =cut use strict; +our $push_obj; package raZberry; @@ -120,7 +121,7 @@ use LWP::UserAgent; use HTTP::Request::Common qw(POST); use JSON qw(decode_json); -#use Data::Dumper; +use Data::Dumper; @raZberry::ISA = ('Generic_Item'); @@ -153,9 +154,10 @@ $rest{ping} = "devices"; $rest{isfailed} = "devices"; $rest{usercode_data} = "devices"; $rest{usercode} = "devices"; +$rest{controller} = "Data/*"; sub new { - my ( $class, $addr, $poll ) = @_; + my ( $class, $addr, $poll, $method ) = @_; my $self = {}; bless $self, $class; $self->{data} = undef; @@ -172,40 +174,90 @@ sub new { $self->{host} = $host; $self->{port} = 8083; $self->{port} = $port if ($port); - $self->{debug} = 0; - $self->{debug} = $main::Debug{razberry} - if ( defined $main::Debug{razberry} ); + $self->{debug} = 1; + $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->{status} = ""; + $self->{controller_data} = (); + $self->{push} = 0; + $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); + if ( ( $push_obj eq "" ) and ( $self->{push} ) ) { + &main::print_log("[raZberry]: Push method selected"); + &main::print_log( + "[raZberry]: The HTTPGet Automation module needs to be installed for push to work" + ); + &main::print_log( + '[raZberry]: URL is http://mh:port/SUB?razberry_push(%DEVICE%,%LEVEL%).' + ); + $push_obj = \%{$self}; + } + else { + &main::print_log( + "[raZberry]: Push method already in use on other object") + if ($push_obj); + &main::print_log("[raZberry]: Poll method selected"); + } + + $self->get_controllerdata; $self->{timer} = new Timer; - $self->start_timer; - &main::print_log("[raZberry] Controller initialized."); + unless ( $self->{push} ) { + $self->start_timer; + } + else { + $self->poll; #get initial data + } + &main::print_log("[raZberry]: Object initialized."); return $self; } +sub get_controllerdata { + my ($self) = @_; + my ( $isSuccessResponse1, $controller_data ) = + _get_JSON_data( $self, 'controller' ); + if ($isSuccessResponse1) { + + #print Dumper $controller_data; + $self->{controller_data} = $controller_data->{controller}->{data}; + &main::print_log("[raZberry]: Controller found"); + &main::print_log( "[raZberry]: Chip version:\t\t" + . $self->{controller_data}->{ZWaveChip}->{value} ); + &main::print_log( "[raZberry]: Software version:\t" + . $self->{controller_data}->{softwareRevisionVersion}->{value} ); + &main::print_log( "[raZberry]: API version:\t\t" + . $self->{controller_data}->{APIVersion}->{value} ); + &main::print_log( "[raZberry]: SDK version:\t\t" + . $self->{controller_data}->{SDK}->{value} ); + } + else { + &main::print_log( + "[raZberry]: Problem connecting to controller " . $self->{host} ); + } +} + sub poll { my ($self) = @_; - &main::print_log("[raZberry] Polling initiated") if ( $self->{debug} ); + &main::print_log("[raZberry]: Polling initiated") if ( $self->{debug} ); my $cmd = ""; $cmd = "?since=" . $self->{lastupdate} if ( defined $self->{lastupdate} ); - &main::print_log("[raZberry] cmd=$cmd") if ( $self->{debug} > 1 ); + &main::print_log("[raZberry]: cmd=$cmd") if ( $self->{debug} > 1 ); for my $dev ( keys %{ $self->{data}->{force_update} } ) { &main::print_log( - "[raZberry] Forcing update to device $dev to account for local changes" + "[raZberry]: Forcing update to device $dev to account for local changes" ) if ( $self->{debug} ); $self->update_dev($dev); } for my $dev ( keys %{ $self->{data}->{ping} } ) { - &main::print_log("[raZberry] Keep_alive: Pinging device $dev...") + &main::print_log("[raZberry]: Keep_alive: Pinging device $dev...") ; # if ($self->{debug}); - &main::print_log("[raZberry] ping_dev $dev"); # if ($self->{debug}); + &main::print_log("[raZberry]: ping_dev $dev"); # if ($self->{debug}); #$self->ping_dev($dev); } @@ -216,7 +268,7 @@ sub poll { if ($isSuccessResponse1) { $self->{lastupdate} = $devices->{data}->{updateTime}; foreach my $item ( @{ $devices->{data}->{devices} } ) { - &main::print_log( "[raZberry] Found:" + &main::print_log( "[raZberry]: Found:" . $item->{id} . " with level " . $item->{metrics}->{level} @@ -262,7 +314,7 @@ sub poll { if ( defined $self->{child_object}->{$id} ) { if ($battery_dev) { &main::print_log( - "[raZberry] Child object detected: Battery Level:[" + "[raZberry]: Child object detected: Battery Level:[" . $item->{metrics}->{level} . "] Child Level:[" . $self->{child_object}->{$id}->battery_level() @@ -274,7 +326,7 @@ sub poll { } else { &main::print_log( - "[raZberry] Child object detected: Controller Level:[" + "[raZberry]: Child object detected: Controller Level:[" . $item->{metrics}->{level} . "] Child Level:[" . $self->{child_object}->{$id}->level() @@ -298,7 +350,7 @@ sub poll { } else { &main::print_log( - "[raZberry] Problem retrieving data from " . $self->{host} ); + "[raZberry]: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -308,7 +360,7 @@ sub poll { sub set_dev { my ( $self, $device, $mode ) = @_; - &main::print_log("[raZberry] Setting $device to $mode") + &main::print_log("[raZberry]: Setting $device to $mode") if ( $self->{debug} ); my $cmd; @@ -316,13 +368,13 @@ sub set_dev { if ( defined $rest{$action} ) { $cmd = "/$zway_vdev" . "_" . $device . "/$rest{$action}"; $cmd .= "$lvl" if $lvl; - &main::print_log("[raZberry] sending command $cmd") + &main::print_log("[raZberry]: 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} ); + "[raZberry]: Problem retrieving data from " . $self->{host} ); return ('0'); } @@ -336,15 +388,15 @@ sub ping_dev { #curl --globoff "http://mhip:8083/ZWaveAPI/Run/devices[x].SendNoOperation()" my ( $devid, $instance, $class ) = ( split /-/, $device )[ 0, 1, 2 ]; - &main::print_log("[raZberry] Pinging device $device ($devid)...") + &main::print_log("[raZberry]: Pinging device $device ($devid)...") if ( $self->{debug} ); my $cmd; $cmd = "%5B" . $devid . "%5D.SendNoOperation()"; &main::print_log("ping cmd=$cmd"); # if ($self->{debug} > 1); my ( $isSuccessResponse0, $status ) = _get_JSON_data( $self, 'ping', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( - "[raZberry] Error: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " + . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -356,7 +408,7 @@ sub isfailed_dev { #"http://mhip:8083/ZWaveAPI/Run/devices[x].data.isFailed.value" my ( $self, $device ) = @_; my ( $devid, $instance, $class ) = ( split /-/, $device )[ 0, 1, 2 ]; - &main::print_log("[raZberry] Checking $device ($devid)...") + &main::print_log("[raZberry]: Checking $device ($devid)...") if ( $self->{debug} ); my $cmd; $cmd = "%5B" . $devid . "%5D.data.isFailed.value"; @@ -365,8 +417,8 @@ sub isfailed_dev { _get_JSON_data( $self, 'isfailed', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( - "[raZberry] Error: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " + . $self->{host} ); $self->{data}->{retry}++; return ('error'); } @@ -384,14 +436,14 @@ sub update_dev { . "%5D.commandClasses%5B" . $class . "%5D.Get()"; - &main::print_log("[raZberry] Getting local state from $device ($devid)...") + &main::print_log("[raZberry]: Getting local state from $device ($devid)...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); my ( $isSuccessResponse0, $status ) = _get_JSON_data( $self, 'force_update', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( - "[raZberry] Error: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " + . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -419,8 +471,9 @@ sub _get_JSON_data { or ( $mode eq "isfailed" ) or ( $mode eq "usercode" ) or ( $mode eq "usercode_data" ) ); + $method = "ZWaveAPI" if ( $mode eq "controller" ); &main::print_log( - "[raZberry] contacting http://$host:$port/$method/$rest{$mode}$params" + "[raZberry]: contacting http://$host:$port/$method/$rest{$mode}$params" ) if ( $self->{debug} ); my $request = @@ -439,13 +492,13 @@ sub _get_JSON_data { $self->{updating} = 0; if ( !$isSuccessResponse ) { &main::print_log( - "[raZberry] Warning, failed to get data. Response code $responseCode" + "[raZberry]: 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 " + "[raZberry]: Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); @@ -458,7 +511,7 @@ sub _get_JSON_data { if ( $self->{status} eq "offline" ) { $self->{status} = "online"; main::print_log - "[raZberry] Communication Tracking object found. Updating from " + "[raZberry]: Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to online..." if ( $self->{loglevel} ); @@ -488,7 +541,7 @@ sub _get_JSON_data { } else { &main::print_log( - "[raZberry] Warning, not fetching data due to operation in progress" + "[raZberry]: Warning, not fetching data due to operation in progress" ); return ('0'); } @@ -535,7 +588,7 @@ sub get_dev_status { else { &main::print_log( - "[raZberry] Warning, unable to get status of device $id"); + "[raZberry]: Warning, unable to get status of device $id"); return 0; } @@ -545,7 +598,7 @@ sub register { my ( $self, $object, $dev, $options ) = @_; if ( lc $dev eq 'comm' ) { &main::print_log( - "[raZberry] Registering Communication object to controller"); + "[raZberry]: Registering Communication object to controller"); $self->{child_object}->{'comm'} = $object; } else { @@ -553,7 +606,7 @@ sub register { my $type = $object->{type}; $type = "Digital " . $type if ( ( defined $options ) and ( $options =~ m/digital/ ) ); - &main::print_log( "[raZberry] Registering " + &main::print_log( "[raZberry]: Registering " . $type . " Device ID $dev to controller" ); $self->{child_object}->{$dev} = $object; @@ -561,19 +614,48 @@ sub register { if ( $options =~ m/force_update/ ) { $self->{data}->{force_update}->{$dev} = 1; &main::print_log( - "[raZberry] Forcing Controller to contact Device $dev at each poll" + "[raZberry]: Forcing Controller to contact Device $dev at each poll" ); } if ( $options =~ m/keep_alive/ ) { $self->{data}->{ping}->{$dev} = 1; &main::print_log( - "[raZberry] Forcing Controller to ping Device $dev at each poll" + "[raZberry]: Forcing Controller to ping Device $dev at each poll" ); } } } } +sub main::razberry_push { + my ( $id, $level ) = @_; + + &main::print_log( + "[raZberry]: HTTP Push update called for device id: $id and level $level" + ); + + #my $obj = &main::get_object_by_name($object); + unless ( defined $push_obj ) { + &main::print_log("[raZberry]: Error, Push controller not defined!"); + } + else { + + if ( defined $push_obj->{child_object}->{$id} ) { + &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' + . $id + . '}->set( ' + . $level + . ", 'poll' );" ); + + #$push_obj->{child_object}->{$id}->set( $level, 'poll' ); + } + else { + &main::print_log( + "[raZberry]: WARNING, $id child object not found!"); + } + } +} + package raZberry_dimmer; @raZberry_dimmer::ISA = ('Generic_Item'); @@ -1149,14 +1231,14 @@ sub _control_user { . $userid . "," . $code . "," . $control . ")"; - &main::print_log("[raZberry] Enabling usercodes $userid ($devid)...") + &main::print_log("[raZberry]: Enabling usercodes $userid ($devid)...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); my ( $isSuccessResponse0, $status ) = &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( - "[raZberry] Error: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " + . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -1169,27 +1251,27 @@ sub _update_users { my $cmd; my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; $cmd = "%5B" . $devid . "%5D.UserCode.Get()"; - &main::print_log("[raZberry] Getting local usercodes ($devid)...") + &main::print_log("[raZberry]: Getting local usercodes ($devid)...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); my ( $isSuccessResponse0, $status ) = &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( - "[raZberry] Error: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " + . $self->{host} ); $self->{data}->{retry}++; return ('0'); } $cmd = "%5B" . $devid . "%5D.UserCode.data"; - &main::print_log("[raZberry] Downloading local usercodes from $devid...") + &main::print_log("[raZberry]: Downloading local usercodes from $devid...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); my ( $isSuccessResponse1, $response ) = &raZberry::_get_JSON_data( $self->{master_object}, 'usercode_data', $cmd ); unless ($isSuccessResponse1) { - &main::print_log( - "[raZberry] Error: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " + . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -1473,14 +1555,14 @@ sub set { $n_state = "closed"; } main::print_log( - "[raZberry] Setting openclose value to $n_state. Level is " + "[raZberry]: Setting openclose 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 openclose"); + "[raZberry]: ERROR Can not set state $p_state for openclose"); } } @@ -1558,4 +1640,3 @@ sub battery_check { } } -1; From 6432392e940b99db741f6410c4dd95769f6487e1 Mon Sep 17 00:00:00 2001 From: waynieack Date: Tue, 31 Jan 2017 19:00:55 -0600 Subject: [PATCH 112/209] Fixed issue with Google Home stuck connections...Again. Added objects to read_table_A.pl so users can define them the the mht file. --- lib/AlexaBridge.pm | 160 ++++++++++++++++++++++++++++++++++++++++++-- lib/read_table_A.pl | 28 ++++++++ 2 files changed, 181 insertions(+), 7 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index cc99cc6a0..3670d683e 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -33,6 +33,110 @@ #alexaMac # This is used in the SSDP response, We discover it so it does not need to be defined uless something goes wrong #alexaHttpIp # This is the IP of the local MH server, We discover it so it does not need to be defined uless something goes wrong + + +# mht example + +# ALEXABRIDGE_ADD, , , , +# , , +# +# ALEX_BRIDGE, Alexa +# ALEXABRIDGE_ITEM, AlexaItems, Alexa + +# ALEXABRIDGE_ADD, AlexaItems, light1 light1, set, on, off, state # these are the defaults +# ALEXABRIDGE_ADD, AlexaItems, light1 # same as the line above +# ALEXABRIDGE_ADD, AlexaItems, light3, Test_Light_3 # if you want to change the name you say +# ALEXABRIDGE_ADD, AlexaItems, 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") +# ALEXABRIDGE_ADD, AlexaItems, test_voice_!, Test_Voice, run_voice_cmd + + + + + +# user code example: +# $Alexa = new AlexaBridge(); # parent object +# $AlexaItems = new AlexaBridge_Item($Alexa); # child object +# +# $AlexaItems->add('$light1','light1','set','on','off','state'); +# +# +# +# In order to allow the user to map pretty much anything in MH to a Echo/GH +# command I created a mapping. +# +# $AlexaItems->add('','','','','',''); +# +# +# +# $AlexaItems->add('$light1','light1','set','on','off','state'); # This +# is the same as $AlexaItems->add('$light1') +# +# +# # To change the name of an object to a more natural name that you would +# say to the Echo/GH: +# +# $AlexaItems->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: +# +# $AlexaItems->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. +# # (on/off/dim%) are passed to the sub as an argument when its run. +# $AlexaItems->add('testsub','Test_Sub',\&testsub); # say "Alexa, Turn on +# Test Sub", &testsub("on") is run in MH. +# +# +# # I have an Insteon thermostat and I configured it like: +# $AlexaItems->add('$thermostat','Heat','heat_setpoint',undef,undef,'get_heat_sp'); +# # say "Alexa, Set Heat to 73%", $thermostat->heat_setpoint("73") is run +# in MH. +# $AlexaItems->add('$thermostat','Cool','cool_setpoint',undef,undef,'get_cool_sp'); + + +# 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 MH user code: +# $AlexaItems->add('set tv mode #','tv','run_voice_cmd'); # "Alexa, Turn on +# tv" / "Alexa, Turn off tv" # turns all my AV stuff on or off +# $AlexaItems->add('set tv mode #','DirectTv','run_voice_cmd','directtv','directtv'); +# #"Alexa, Turn on Direct Tv" # turns all my AV stuff on and set the input to +# direct tv +# $AlexaItems->add('set tv mode #','Hbo','run_voice_cmd','hbo','hbo'); +# #"Alexa, Turn on hbo" # turns all my AV stuff on and set the input to Roku +# and launches the HBO Go app +# $AlexaItems->add('set tv mode #','Netflix','run_voice_cmd','netflix','netflix'); +# #"Alexa, Turn on Netflix" # turns all my AV stuff on and set the input to +# Roku and launches the Netflix app +# $AlexaItems->add('set tv mode #','Roku','run_voice_cmd','roku','roku'); +# $AlexaItems->add('set tv mode #','xbmc','run_voice_cmd','xbmc','xbmc'); +# $AlexaItems->add('set tv mode #','wii','run_voice_cmd','wii','wii'); + + + package AlexaBridge; @AlexaBridge::ISA = ('Generic_Item'); @@ -157,14 +261,19 @@ sub check_for_data { my $alexa_listen = $AlexaGlobal->{http_sockets}{$AlexaHttpName}; if ( $alexa_listen && ( my $alexa_data = said $alexa_listen ) ) { - my $peerip = $alexa_listen->peer; - &main::print_log( "[Alexa] Debug: Peer: $peerip Data IN - $alexa_data" ) if $main::Debug{'alexa'} >= 5; + my $client_ip_address = $alexa_listen->peer; + &main::print_log( "[Alexa] Debug: Peer: $client_ip_address Sent Data" ) if $main::Debug{'alexa'} >= 2; + &main::print_log( "[Alexa] Debug: Peer: $client_ip_address Data IN - $alexa_data" ) if $main::Debug{'alexa'} >= 5; + $client_ip_address =~ s/:.*//; + my $client_port = $alexa_listen->peer; + $client_port =~ s/.*\://; + $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time} = time; $alexa_http_sender->start unless $alexa_http_sender->active; $alexa_http_sender->set($alexa_data); } &_sendHttpData($alexa_listen, $alexa_http_sender); - &close_stuck_sockets($alexa_listen, $AlexaHttpName); #This closes the oldest connection from a source IP if a second one is made. Fix for GH stuck connections + &close_stuck_sockets($alexa_listen, $AlexaHttpName) if ($alexa_listen); #This closes the oldest connection from a source IP if a second one is made. Fix for GH stuck connections # } @@ -181,8 +290,13 @@ sub check_for_data { sub _sendHttpData { my ($alexa_listen, $alexa_http_sender) = @_; if ( $alexa_http_sender && ( my $alexa_sender_data = said $alexa_http_sender ) ) { - my $peerip = $alexa_listen->peer; - &main::print_log( "[Alexa] Debug: Peer: $peerip Data OUT - $alexa_sender_data" ) if $main::Debug{'alexa'} >= 5; + my $client_ip_address = $alexa_listen->peer; + &main::print_log( "[Alexa] Debug: Peer: $client_ip_address Data OUT - $alexa_sender_data" ) if $main::Debug{'alexa'} >= 5; + $client_ip_address =~ s/:.*//; + my $client_port = $alexa_listen->peer; + $client_port =~ s/.*\://; + + delete $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port} if $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}; $alexa_listen->set($alexa_sender_data); } } @@ -277,8 +391,9 @@ sub _sendSearchResponse { } -sub close_stuck_sockets { +sub close_stuck_sockets_old { my ($alexa_listen, $AlexaHttpName) = @_; + return unless $alexa_listen; my $current_client_ip = $alexa_listen->peer; $current_client_ip =~ s/:.*//; my $current_client_port = $alexa_listen->peer; @@ -301,6 +416,29 @@ my ($alexa_listen, $AlexaHttpName) = @_; } +sub close_stuck_sockets { +my ($alexa_listen, $AlexaHttpName) = @_; + return unless $alexa_listen; + my $current_client_ip = $alexa_listen->peer; + $current_client_ip =~ s/:.*//; + my $current_client_port = $alexa_listen->peer; + $current_client_port =~ s/.*\://; + for my $ptr ( @{ $::Socket_Ports{$AlexaHttpName}{clients} } ) { + my ( $socka, $client_ip_address, $client_port, $data ) = @{$ptr}; + next if ( ($client_ip_address eq $current_client_ip) && ($client_port eq $current_client_port)); + next unless $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time}; + my $timediff = (time - $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time}); + if ( $timediff >= 20 ) { + $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.."; + print $socka $output; + delete $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}; + &main::print_log( "[Alexa] Debug: Sending 404 to $client_ip_address:$client_port socket has been open for $timediff with no response") if $main::Debug{'alexa'} >= 2; + } + } + +} + + sub process_http { unless ($::config_parms{'alexa_enable'}) { return 0 } @@ -312,6 +450,13 @@ sub process_http { my $self = ::get_object_by_name($selfname); unless ($self) { &main::print_log( "[Alexa] Error: No AlexaBridge parent object found" ); return 0 } + #my $client_ip_address = $::Socket_Ports{http}{client_ip_address}; + #$client_ip_address =~ s/:.*//; + #my $client_port = $::Socket_Ports{http}{client_port}; + #$client_port =~ s/.*\://; + +#&main::print_log( "[Alexa] Debug: Process_http - Client: $client_ip_address:$client_port has sent data") if $main::Debug{'alexa'} >= 2; + use HTTP::Date qw(time2str); use IO::Compress::Gzip qw(gzip); @@ -665,13 +810,14 @@ sub get_set_state { if ( $object->can('state_level') && $state =~ /\d+/ ) { $state = $state.'%'} &main::print_log ("[Alexa] Debug: setting object ( $realname ) to state ( $state )\n") if $main::Debug{'alexa'}; if ( lc($type) =~ /clipsal_cbus/ ) { $object->$sub($state,'Alexa') } - else { $object->$sub($state) } + else { $object->$sub($state,'Alexa') } return; } } elsif ( $sub =~ /^run_voice_cmd$/ ) { if ( $action eq 'set' ) { $realname =~ s/#/$state/; + $realname =~ s/!/$state/; &main::print_log ("[Alexa] Debug: running voice command: ( $realname )\n") if $main::Debug{'alexa'}; &main::run_voice_cmd("$realname"); return; diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index b4cc1070c..863abb322 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1609,6 +1609,34 @@ sub read_table_A { } #-------------- End AD2 Objects ------------- + #-------------- Alexa Objects ----------------- + elsif ( $type eq "ALEX_BRIDGE" ) { + require 'AlexaBridge.pm'; + ( $name ) = @item_info; + $object = "AlexaBridge('$other')"; + } + elsif ( $type eq "ALEXABRIDGE_ITEM" ) { + require 'AlexaBridge.pm'; + my ( $parent ); + ( $name, $parent ) = @item_info; + $object = "AlexaBridge_Item(\$$parent)"; + } + elsif ( $type eq "ALEXABRIDGE_ADD" ) { + my ( $parent, $realname, $name, $sub, $on, $off, $statesub, @other ) = @item_info; + if ($sub =~ /^&/) { $sub =~ s/&/\\&/ } + if ($sub =~ /^\\\\&/) { $sub =~ s/\\// } + if ($sub =~ /run_voice_cmd/) { $realname =~ s/_/ /g } + unless ( ($sub =~ /run_voice_cmd/) || ($sub =~ /&/) ) { $realname = "\$$realname" } + unless ( $sub =~ /&/ ) { $sub = "'".$sub."'" } + my $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if ( !$packages{AlexaBridge}++ ) { # first time for this object type? + $code .= "use AlexaBridge;\n"; + } + $code .= sprintf "\$%-35s -> add('$realname','$name',$sub,'$on','$off','$statesub',$other);\n", $parent; + $object = ''; + } + #-------------- End Alexa Objects ---------------- + elsif ( $type =~ /PLCBUS_.*/ ) { require PLCBUS; ( $address, $name, $grouplist, @other ) = @item_info; From ba64fb8c21c811eab87c5db6c5a4190fd440ae7d Mon Sep 17 00:00:00 2001 From: waynieack Date: Sat, 4 Feb 2017 18:37:22 -0600 Subject: [PATCH 113/209] Updated POD doc --- lib/AlexaBridge.pm | 481 +++++++++++++++++++++++++++++++-------------- 1 file changed, 329 insertions(+), 152 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 3670d683e..468501321 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -1,141 +1,253 @@ -#For Google Home and a reverse proxy (Apache/IIS/etc): -#alexa_enable = 1 -#alexaHttpPortCount = 0 # disables all proxy ports -#alexaHttpPort = 80 # tells the module to send port 80 in the SSDP response and look for port 80 in the HTTP host header -#alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response -# -#For Google Home using the builtin proxy port: -#alexa_enable = 1 -#alexaHttpPortCount = 1 # Open 1 proxy port on port 80 (We default to port 80 so no need to define it) -#alexaNoDefaultHttp = 1 # Disable responding on the default MH web port because Google Home will not use it any way. -#alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response -# -# -#For Echo (Chunked method): -#alexa_enable = 1 -#alexaEnableChunked = 1 -# -# -#For Echo (Multi-port method): -## This method should not be needed unless for some reason your Echo does not work with the Chunked method. -#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. -#alexaHttpPort=8085 # The proxy port will be on port 8085, this port should be higher than the MH web port so it is used first. -# -# -# -#alexa_enable # Enable the module -#alexaEnableChunked # Enable chunked return method (For the Echo) -#alexaHttpPortCount # Amount of proxy ports to open -#alexaNoDefaultHttp # Disable responding on the default MH web port -#alexaObjectsPerGet # Amount of MH objects we return per GET from the Echo/GH -#alexaHttpPort # First proxy port number -#alexaMac # This is used in the SSDP response, We discover it so it does not need to be defined uless something goes wrong -#alexaHttpIp # This is the IP of the local MH server, We discover it so it does not need to be defined uless something goes wrong - - - -# mht example - -# ALEXABRIDGE_ADD, , , , -# , , -# -# ALEX_BRIDGE, Alexa -# ALEXABRIDGE_ITEM, AlexaItems, Alexa - -# ALEXABRIDGE_ADD, AlexaItems, light1 light1, set, on, off, state # these are the defaults -# ALEXABRIDGE_ADD, AlexaItems, light1 # same as the line above -# ALEXABRIDGE_ADD, AlexaItems, light3, Test_Light_3 # if you want to change the name you say -# ALEXABRIDGE_ADD, AlexaItems, 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") -# ALEXABRIDGE_ADD, AlexaItems, test_voice_!, Test_Voice, run_voice_cmd - - - - - -# user code example: -# $Alexa = new AlexaBridge(); # parent object -# $AlexaItems = new AlexaBridge_Item($Alexa); # child object -# -# $AlexaItems->add('$light1','light1','set','on','off','state'); -# -# -# -# In order to allow the user to map pretty much anything in MH to a Echo/GH -# command I created a mapping. -# -# $AlexaItems->add('','','','','',''); -# -# -# -# $AlexaItems->add('$light1','light1','set','on','off','state'); # This -# is the same as $AlexaItems->add('$light1') -# -# -# # To change the name of an object to a more natural name that you would -# say to the Echo/GH: -# -# $AlexaItems->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: -# -# $AlexaItems->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. -# # (on/off/dim%) are passed to the sub as an argument when its run. -# $AlexaItems->add('testsub','Test_Sub',\&testsub); # say "Alexa, Turn on -# Test Sub", &testsub("on") is run in MH. -# -# -# # I have an Insteon thermostat and I configured it like: -# $AlexaItems->add('$thermostat','Heat','heat_setpoint',undef,undef,'get_heat_sp'); -# # say "Alexa, Set Heat to 73%", $thermostat->heat_setpoint("73") is run -# in MH. -# $AlexaItems->add('$thermostat','Cool','cool_setpoint',undef,undef,'get_cool_sp'); - - -# 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 MH user code: -# $AlexaItems->add('set tv mode #','tv','run_voice_cmd'); # "Alexa, Turn on -# tv" / "Alexa, Turn off tv" # turns all my AV stuff on or off -# $AlexaItems->add('set tv mode #','DirectTv','run_voice_cmd','directtv','directtv'); -# #"Alexa, Turn on Direct Tv" # turns all my AV stuff on and set the input to -# direct tv -# $AlexaItems->add('set tv mode #','Hbo','run_voice_cmd','hbo','hbo'); -# #"Alexa, Turn on hbo" # turns all my AV stuff on and set the input to Roku -# and launches the HBO Go app -# $AlexaItems->add('set tv mode #','Netflix','run_voice_cmd','netflix','netflix'); -# #"Alexa, Turn on Netflix" # turns all my AV stuff on and set the input to -# Roku and launches the Netflix app -# $AlexaItems->add('set tv mode #','Roku','run_voice_cmd','roku','roku'); -# $AlexaItems->add('set tv mode #','xbmc','run_voice_cmd','xbmc','xbmc'); -# $AlexaItems->add('set tv mode #','wii','run_voice_cmd','wii','wii'); +=head1 B +=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. + +=head2 CONFIGURATION + + +The AlexaBridge_Item object holds the configured Misterhouse objects that are presented to the Amazon Echo or Google Home. +See + +=head2 mh.private.ini Configuration + +Note: +You must use port 80 for Google Home, it is locked down to port 80. +The user running MH must be root to run on port 80 or you have to give the MH user rights to use the port. + +For Google Home and a reverse proxy (Apache/IIS/etc): + + alexa_enable = 1 + alexaHttpPortCount = 0 # disables all proxy ports + alexaHttpPort = 80 # tells the module to send port 80 in the SSDP response and look for port 80 in the HTTP host header + alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response + +For Google Home using the builtin proxy port: + + alexa_enable = 1 + alexaHttpPortCount = 1 # Open 1 proxy port on port 80 (We default to port 80 so no need to define it) + alexaNoDefaultHttp = 1 # Disable responding on the default MH web port because Google Home will not use it any way. + alexaObjectsPerGet = 300 # Google Home can handle us returning all objects in a single response + + +For Echo (Chunked method): + + alexa_enable = 1 + alexaEnableChunked = 1 + + +For Echo (Multi-port method): +This method should not be needed unless for some reason your Echo does not work with the Chunked method. + + 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. + alexaHttpPort=8085 # The proxy port will be on port 8085, this port should be higher than the MH web port so it is used first. + + +# All options + + alexa_enable # Enable the module + alexaEnableChunked # Enable chunked return method (For the Echo) + alexaHttpPortCount # Amount of proxy ports to open + alexaNoDefaultHttp # Disable responding on the default MH web port + alexaObjectsPerGet # Amount of MH objects we return per GET from the Echo/GH + alexaHttpPort # First proxy port number + alexaMac # This is used in the SSDP response, We discover it so it does not need to be defined unless something goes wrong + alexaHttpIp # This is the IP of the local MH server, We discover it so it does not need to be defined unless something goes wrong + +=head2 Defining the Primary Object + +The object can be defined in the user code or in a .mht file. + +In mht: + + ALEX_BRIDGE, Alexa + + +Or in user code: + + $Alexa = new AlexaBridge(); # parent object + + +=head2 NOTES + +The most important part of the configuration is mapping the objects/code you want to present to the module (Echo/Google Home/Etc.). +This allows the user to map pretty much anything in MH to a Echo/GH command. + + ALEXABRIDGE_ADD, , , , + , , + + - This is the only required parameter. If you are +good with the defaults, you can add an object like: +# In MHT + + ALEXABRIDGE_ADD, AlexaItems, light1 + +# or in user code + + $AlexaItems->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: + + ALEX_BRIDGE, Alexa + ALEXABRIDGE_ITEM, AlexaItems, Alexa + ALEXABRIDGE_ADD, AlexaItems, light1 light1, set, on, off, state # these are the defaults + ALEXABRIDGE_ADD, AlexaItems, light1 # same as the line above + ALEXABRIDGE_ADD, AlexaItems, light3, Test_Light_3 # if you want to change the name you say + ALEXABRIDGE_ADD, AlexaItems, 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") + ALEXABRIDGE_ADD, AlexaItems, test_voice_!, Test_Voice, run_voice_cmd + + +User code examples: + + $Alexa = new AlexaBridge(); # parent object + $AlexaItems = new AlexaBridge_Item($Alexa); # child object + + $AlexaItems->add('$light1','light1','set','on','off','state'); # This is the same as $AlexaItems->add('$light1') + + + +To change the name of an object to a more natural name that you would say to the Echo/GH: + + $AlexaItems->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: + + $AlexaItems->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 + + ALEXABRIDGE_ADD, AlexaItems, testsub, Test_Sub, &testsub + +# User Code + + $AlexaItems->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: + + ALEXABRIDGE_ADD, AlexaItems, thermostat, Heat, heat_setpoint, on, off, get_heat_sp + +# say "Alexa, Set Heat to 73", $thermostat->heat_setpoint("73") is run in MH. + + ALEXABRIDGE_ADD, AlexaItems, 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 + + ALEXABRIDGE_ADD, AlexaItems, 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: + + ALEXABRIDGE_ADD, AlexaItems, set_tv_mode_!, DirectTv, run_voice_cmd, directtv, directtv + ALEXABRIDGE_ADD, AlexaItems, set_tv_mode_!, Roku, run_voice_cmd, roku, roku + ALEXABRIDGE_ADD, AlexaItems, set_tv_mode_!, xbmc, run_voice_cmd, xbmc, xbmc + ALEXABRIDGE_ADD, AlexaItems, set_tv_mode_!, wii, run_voice_cmd, wii, wii + ALEXABRIDGE_ADD, AlexaItems, set_tv_mode_!, Hbo, run_voice_cmd, hbo, hbo + ALEXABRIDGE_ADD, AlexaItems, set_tv_mode_!, Netflix, run_voice_cmd, netflix, netflix + + + +=head2 INHERITS + +L + +HTTP::Date +IO::Compress::Gzip +Time::HiRes +Net::Address::Ethernet +Storable +Socket +IO::Socket::INET +IO::Socket::Multicast + +=over + +=cut package AlexaBridge; @@ -147,16 +259,6 @@ use Socket; use IO::Socket::Multicast; - -#use constant SSDP_IP => "239.255.255.250"; -#use constant SSDP_PORT => 1900; -#use constant CRLF => "\015\012"; - -#use constant DEFAULT_HTTP_PORT => 80; -#use constant DEFAULT_LEASE_TIME => 1800; -#use constant DEFAULT_NOTIFICATION_PORT => 50000; -#use constant DEFAULT_PORT_COUNT => 0; - my ($LOCAL_IP, $LOCAL_MAC) = &DiscoverAddy unless ( (defined($::config_parms{'alexaMac'})) && (defined($::config_parms{'alexaHttpIp'})) ); $LOCAL_IP = $::config_parms{'alexaHttpIp'} if defined($::config_parms{'alexaHttpIp'}); $LOCAL_MAC = $::config_parms{'alexaMac'} if defined($::config_parms{'alexaMac'}); @@ -450,12 +552,6 @@ sub process_http { my $self = ::get_object_by_name($selfname); unless ($self) { &main::print_log( "[Alexa] Error: No AlexaBridge parent object found" ); return 0 } - #my $client_ip_address = $::Socket_Ports{http}{client_ip_address}; - #$client_ip_address =~ s/:.*//; - #my $client_port = $::Socket_Ports{http}{client_port}; - #$client_port =~ s/.*\://; - -#&main::print_log( "[Alexa] Debug: Process_http - Client: $client_ip_address:$client_port has sent data") if $main::Debug{'alexa'} >= 2; use HTTP::Date qw(time2str); use IO::Compress::Gzip qw(gzip); @@ -882,6 +978,55 @@ sub register { $self->{child} = $child; } +=back + +=head1 B + +=head2 DESCRIPTION + +The AlexaBridge_Item object holds the configured Misterhouse objects that are presented to the Amazon Echo or Google Home + +=head2 mh.private.ini Configuration + +See L + +=head2 Defining the Child object + +The object can be defined in the user code or in a .mht file. + +In mht: + +ALEXABRIDGE_ITEM, , + +ie: + + ALEXABRIDGE_ITEM, AlexaItems, Alexa + + +Or in user code: + + = new AlexaBridge_Item(); + +ie: + + $AlexaItems = new AlexaBridge_Item($Alexa); + + +=head2 NOTES + +See L for complete examples + +=head2 INHERITS + +L + +=head2 METHODS + +=over + +=cut + + package AlexaBridge_Item; @AlexaBridge_Item::ISA = ('Generic_Item'); @@ -917,6 +1062,17 @@ sub new { return $self; } +=item C + +Presents misterhouse objects, subs, or voice coommands to the Echo, Google Home, or any thing that supports +the HUE bridge. + +add('','', +'','', +'',''); + +=cut + sub add { my ($self, $realname, $name, $sub, $on, $off, $statesub) = @_; @@ -1050,3 +1206,24 @@ sub isDeleted { 1; +=back + +=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 + + From 0871609d6cae1cdb3303a841393caf1765303e90 Mon Sep 17 00:00:00 2001 From: waynieack Date: Fri, 10 Feb 2017 19:00:16 -0600 Subject: [PATCH 114/209] Added lib/site/IO/Socket/Multicast.pm --- lib/site/IO/Socket/Multicast.pm | 426 ++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 lib/site/IO/Socket/Multicast.pm diff --git a/lib/site/IO/Socket/Multicast.pm b/lib/site/IO/Socket/Multicast.pm new file mode 100644 index 000000000..d69edb17a --- /dev/null +++ b/lib/site/IO/Socket/Multicast.pm @@ -0,0 +1,426 @@ +package IO::Socket::Multicast; + +use 5.005; +use strict; +use Carp 'croak'; +use Exporter (); +use DynaLoader (); +use IO::Socket; +BEGIN { + eval "use IO::Interface 0.94 'IFF_MULTICAST';"; +} +use vars qw(@ISA @EXPORT_OK @EXPORT %EXPORT_TAGS $VERSION); +BEGIN { + my @functions = qw( + mcast_add + mcast_drop + mcast_if + mcast_loopback + mcast_ttl + mcast_dest + mcast_send + ); + $VERSION = '1.12'; + @ISA = qw( + Exporter + DynaLoader + IO::Socket::INET + ); + @EXPORT = ( ); + %EXPORT_TAGS = ( + 'all' => \@functions, + 'functions' => \@functions, + ); + @EXPORT_OK = @{ $EXPORT_TAGS{'all'} }; +} + +my $IP = '\d+\.\d+\.\d+\.\d+'; + +sub import { + Socket->export_to_level(1,@_); + IO::Socket::Multicast->export_to_level(1,@_); +} + +sub new { + my $class = shift; + unshift @_,(Proto => 'udp') unless @_; + $class->SUPER::new(@_); +} + +sub configure { + my($self,$arg) = @_; + $arg->{Proto} ||= 'udp'; + $self->SUPER::configure($arg); +} + +sub mcast_add { + my $sock = shift; + my $group = shift || croak 'usage: $sock->mcast_add($mcast_addr [,$interface])'; + $group = inet_ntoa($group) unless $group =~ /^$IP$/o; + my $interface = get_if_addr($sock,shift); + return $sock->_mcast_add($group,$interface); +} + +sub mcast_drop { + my $sock = shift; + my $group = shift || croak 'usage: $sock->mcast_add($mcast_addr [,$interface])'; + $group = inet_ntoa($group) unless $group =~ /^$IP$/o; + my $interface = get_if_addr($sock,shift); + return $sock->_mcast_drop($group,$interface); +} + +sub mcast_if { + my $sock = shift; + + my $previous = $sock->_mcast_if; + $previous = $sock->addr_to_interface($previous) + if $sock->can('addr_to_interface'); + return $previous unless @_; + + my $interface = get_if_addr($sock,shift); + return $sock->_mcast_if($interface) ? $previous : undef; +} + +sub get_if_addr { + my $sock = shift; + return '0.0.0.0' unless defined (my $interface = shift); + return $interface if $interface =~ /^$IP$/; + return $interface if length $interface == 16; + croak "IO::Interface module not available; use IP addr for interface" + unless $sock->can('if_addr'); + croak "unknown or unconfigured interace $interface" + unless my $addr = $sock->if_addr($interface); + croak "interface is not multicast capable" + unless $interface eq 'any' or ($sock->if_flags($interface) & IFF_MULTICAST()); + return $addr; +} + +sub mcast_dest { + my $sock = shift; + my $prev = ${*$sock}{'io_socket_mcast_dest'}; + if (my $dest = shift) { + $dest = sockaddr_in($2,inet_aton($1)) if $dest =~ /^($IP):(\d+)$/; + croak "invalid destination address" unless length($dest) == 16; + ${*$sock}{'io_socket_mcast_dest'} = $dest; + } + return $prev; +} + +sub mcast_send { + my $sock = shift; + my $data = shift || croak 'usage: $sock->mcast_send($data [,$address])'; + $sock->mcast_dest(shift) if @_; + my $dest = $sock->mcast_dest || croak "no destination specified with mcast_send() or mcast_dest()"; + return send($sock,$data,0,$dest); +} + +bootstrap IO::Socket::Multicast $VERSION; + +1; + +__END__ + +=pod + +=head1 NAME + +IO::Socket::Multicast - Send and receive multicast messages + +=head1 SYNOPSIS + + use IO::Socket::Multicast; + + # create a new UDP socket ready to read datagrams on port 1100 + my $s = IO::Socket::Multicast->new(LocalPort=>1100); + + # Add a multicast group + $s->mcast_add('225.0.1.1'); + + # Add a multicast group to eth0 device + $s->mcast_add('225.0.0.2','eth0'); + + # now receive some multicast data + $s->recv($data,1024); + + # Drop a multicast group + $s->mcast_drop('225.0.0.1'); + + # Set outgoing interface to eth0 + $s->mcast_if('eth0'); + + # Set time to live on outgoing multicast packets + $s->mcast_ttl(10); + + # Turn off loopbacking + $s->mcast_loopback(0); + + # Multicast a message to group 225.0.0.1 + $s->mcast_send('hello world!','225.0.0.1:1200'); + $s->mcast_set('225.0.0.2:1200'); + $s->mcast_send('hello again!'); + +=head1 DESCRIPTION + +The IO::Socket::Multicast module subclasses IO::Socket::INET to enable +you to manipulate multicast groups. With this module (and an +operating system that supports multicasting), you will be able to +receive incoming multicast transmissions and generate your own +outgoing multicast packets. + +This module requires IO::Interface version 0.94 or higher. + +=head2 INTRODUCTION + +Multicasting is designed for streaming multimedia applications and for +conferencing systems in which one transmitting machines needs to +distribute data to a large number of clients. + +IP addresses in the range 224.0.0.0 and 239.255.255.255 are reserved +for multicasting. These addresses do not correspond to individual +machines, but to multicast groups. Messages sent to these addresses +will be delivered to a potentially large number of machines that have +registered their interest in receiving transmissions on these groups. +They work like TV channels. A program tunes in to a multicast group +to receive transmissions to it, and tunes out when it no longer +wishes to receive the transmissions. + +To receive transmissions B a multicast group, you will use +IO::Socket::Multicast->new() to create a UDP socket and bind it to a local +network port. You will then subscribe one or more multicast groups +using the mcast_add() method. Subsequent calls to the standard recv() +method will now receive messages incoming messages transmitted to the +subscribed groups using the selected port number. + +To send transmissions B a multicast group, you can use the +standard send() method to send messages to the multicast group and +port of your choice. The mcast_set() and mcast_send() methods are +provided as convenience functions. Mcast_set() will set a default +multicast destination for messages which you then send with +mcast_send(). + +To set the number of hops (routers) that outgoing multicast messages +will cross, call mcast_ttl(). To activate or deactivate the looping +back of multicast messages (in which a copy of the transmitted +messages is received by the local machine), call mcast_loopback(). + +=head2 CONSTRUCTORS + +=over 4 + +=item $socket = IO::Socket::Multicast->new([LocalPort=>$port,...]) + +The new() method is the constructor for the IO::Socket::Multicast +class. It takes the same arguments as IO::Socket::INET, except that +the B argument, rather than defaulting to "tcp", will default +to "udp", which is more appropriate for multicasting. + +To create a UDP socket suitable for sending outgoing multicast +messages, call new() without arguments (or with +C'udp'>). To create a UDP socket that can also receive +incoming multicast transmissions on a specific port, call new() with +the B argument. + +If you plan to run the client and server on the same machine, you may +wish to set the IO::Socket B argument to a true value. +This allows multiple multicast sockets to bind to the same address. + +=back + +=head2 METHODS + +=over 4 + +=item $success = $socket->mcast_add($multicast_address [,$interface]) + +The mcast_add() method will add the provided multicast address to the +list of subscribed multicast groups. The address may be provided +either as a dotted-quad decimal, or as a packed IP address (such as +produced by the inet_aton() function). On success, the method will +return a true value. + +The optional $interface argument can be used to specify on which +network interface to listen for incoming multicast messages. If the +IO::Interface module is installed, you may use the device name for the +interface (e.g. "tu0"). Otherwise, you must use the IP address of the +desired network interface. Either dotted quad form or packed IP +address is acceptable. If no interface is specified, then the +multicast group is joined on INADDR_ANY, meaning that multicast +transmissions received on B of the host's network interfaces will +be forwarded to the socket. + +Note that mcast_add() operates on the underlying interface(s) and not +on the socket. If you have multiple sockets listening on a port, and +you mcast_add() a group to one of those sockets, subsequently B +the sockets will receive mcast messages on this group. To filter +messages that can be received by a socket so that only those sent to a +particular multicast address are received, pass the B +option to the socket at the time you create it: + + my $socket = IO::Socket::Multicast->new(LocalPort=>2000, + LocalAddr=>226.1.1.2', + ReuseAddr=>1); + $socket->mcast_add('226.1.1.2'); + +By combining this technique with IO::Select, you can write +applications that listen to multiple multicast groups and distinguish +which group a message was addressed to by identifying which socket it +was received on. + +=item $success = $socket->mcast_drop($multicast_address) + +This reverses the action of mcast_add(), removing the indicated +multicast address from the list of subscribed groups. + +=item $loopback = $socket->mcast_loopback + +=item $previous = $socket->mcast_loopback($new) + +The mcast_loopback() method controls whether the socket will receive +its own multicast transmissions (default yes). Called without +arguments, the method returns the current state of the loopback +flag. Called with a boolean argument, the method will set the loopback +flag, and return its previous value. + +=item $ttl = $socket->mcast_ttl + +=item $previous = $socket->mcast_ttl($new) + +The mcast_ttl() method examines or sets the time to live (TTL) for +outgoing multicast messages. The TTL controls the numbers of routers +the packet can cross before being expired. The default TTL is 1, +meaning that the message is confined to the local area network. +Values between 0 and 255 are valid. + +Called without arguments, this method returns the socket's current +TTL. Called with a value, this method sets the TTL and returns its +previous value. + +=item $interface = $socket->mcast_if + +=item $previous = $socket->mcast_if($new) + +By default, the OS will pick the network interface to use for outgoing +multicasts automatically. You can control this process by using the +mcast_if() method to set the outgoing network interface explicitly. +Called without arguments, returns the current interface. Called with +the name of an interface, sets the outgoing interface and returns its +previous value. + +You can use the device name for the interface (e.g. "tu0") if the +IO::Interface module is present. Otherwise, you must use the +interface's dotted IP address. + +B: To set the interface used for B multicasts, use the +mcast_add() method. + +=item $dest = $socket->mcast_dest + +=item $previous = $socket->mcast_dest($new) + +The mcast_dest() method is a convenience function that allows you to +set the default destination group for outgoing multicasts. Called +without arguments, returns the current destination as a packed binary +sockaddr_in data structure. Called with a new destination address, +the method sets the default destination and returns the previous one, +if any. + +Destination addresses may be provided as packed sockaddr_in +structures, or in the form "XX.XX.XX.XX:YY" where the first part is +the IP address, and the second the port number. + +=item $bytes = $socket->mcast_send($data [,$dest]) + +Mcast_send() is a convenience function that simplifies the sending of +multicast messages. C<$data> is the message contents, and C<$dest> is +an optional destination group. You can use either the dotted IP form +of the destination address and its port number, or a packed +sockaddr_in structure. If the destination is not supplied, it will +default to the most recent value set in mcast_dest() or a previous +call to mcast_send(). + +The method returns the number of bytes successfully queued for +delivery. + +As a side-effect, the method will call mcast_dest() to remember the +destination address. + +Example: + + $socket->mcast_send('Hi there group members!','225.0.1.1:1900') || die; + $socket->mcast_send("How's the weather?") || die; + +Note that you may still call IO::Socket::Multicast->new() with a +B, and IO::Socket::INET will perform a connect(), creating a +default destination for calls to send(). + +=back + +=head1 EXAMPLE + +The following is an example of a multicast server. Every 10 seconds +it transmits the current time and the list of logged-in users to the +local network using multicast group 226.1.1.2, port 2000 (these are +chosen arbitrarily). + + #!/usr/bin/perl + # server + use strict; + use IO::Socket::Multicast; + + use constant DESTINATION => '226.1.1.2:2000'; + my $sock = IO::Socket::Multicast->new(Proto=>'udp',PeerAddr=>DESTINATION); + + while (1) { + my $message = localtime; + $message .= "\n" . `who`; + $sock->send($message) || die "Couldn't send: $!"; + } continue { + sleep 10; + } + +This is the corresponding client. It listens for transmissions on +group 226.1.1.2, port 2000, and echoes the messages to standard +output. + + #!/usr/bin/perl + # client + + use strict; + use IO::Socket::Multicast; + + use constant GROUP => '226.1.1.2'; + use constant PORT => '2000'; + + my $sock = IO::Socket::Multicast->new(Proto=>'udp',LocalPort=>PORT); + $sock->mcast_add(GROUP) || die "Couldn't set group: $!\n"; + + while (1) { + my $data; + next unless $sock->recv($data,1024); + print $data; + } + +=head2 EXPORT + +None by default. However, if you wish to call mcast_add(), +mcast_drop(), mcast_if(), mcast_loopback(), mcast_ttl, mcast_dest() +and mcast_send() as functions you may import them explicitly on the +B line or by importing the tag ":functions". + +=head2 BUGS + +The mcast_if(), mcast_ttl() and mcast_loopback() methods will cause a +crash on versions of Linux earlier than 2.2.0 because of a kernel bug +in the implementation of the multicast socket options. + +=head1 AUTHOR + +Lincoln Stein, lstein@cshl.org. + +This module is distributed under the same terms as Perl itself. + +=head1 SEE ALSO + +perl(1), IO::Socket(3), IO::Socket::INET(3). + +=cut From 256b1eb17c4de63a3aa795a7ac836348e9e3dfea Mon Sep 17 00:00:00 2001 From: waynieack Date: Fri, 10 Feb 2017 23:21:23 -0600 Subject: [PATCH 115/209] Added lib/site/IO/Interface --- lib/site/IO/Interface.pm | 303 ++++++++++++++++++++++++++++++++ lib/site/IO/Interface/Simple.pm | 287 ++++++++++++++++++++++++++++++ 2 files changed, 590 insertions(+) create mode 100644 lib/site/IO/Interface.pm create mode 100644 lib/site/IO/Interface/Simple.pm diff --git a/lib/site/IO/Interface.pm b/lib/site/IO/Interface.pm new file mode 100644 index 000000000..419aa004b --- /dev/null +++ b/lib/site/IO/Interface.pm @@ -0,0 +1,303 @@ +package IO::Interface; + +require 5.005; +use strict; +use Carp; +use vars qw(@EXPORT @EXPORT_OK @ISA %EXPORT_TAGS $VERSION $AUTOLOAD); + +use IO::Socket; + +require Exporter; +require DynaLoader; + +my @functions = qw(if_addr if_broadcast if_netmask if_dstaddr if_hwaddr if_flags if_list if_mtu if_metric + addr_to_interface if_index if_indextoname ); +my @flags = qw(IFF_ALLMULTI IFF_AUTOMEDIA IFF_BROADCAST + IFF_DEBUG IFF_LOOPBACK IFF_MASTER + IFF_MULTICAST IFF_NOARP IFF_NOTRAILERS + IFF_POINTOPOINT IFF_PORTSEL IFF_PROMISC + IFF_RUNNING IFF_SLAVE IFF_UP); +%EXPORT_TAGS = ( 'all' => [@functions,@flags], + 'functions' => \@functions, + 'flags' => \@flags, + ); + +@EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); + +@EXPORT = qw( ); + +@ISA = qw(Exporter DynaLoader); +$VERSION = '1.09'; + +sub AUTOLOAD { + # This AUTOLOAD is used to 'autoload' constants from the constant() + # XS function. If a constant is not found then control is passed + # to the AUTOLOAD in AutoLoader. + + my $constname; + ($constname = $AUTOLOAD) =~ s/.*:://; + croak "&constant not defined" if $constname eq 'constant'; + my $val = constant($constname, @_ ? $_[0] : 0); + if ($! != 0) { + if ($! =~ /Invalid/ || $!{EINVAL}) { + $AutoLoader::AUTOLOAD = $AUTOLOAD; + goto &AutoLoader::AUTOLOAD; + } + else { + croak "Your vendor has not defined IO::Interface macro $constname"; + } + } + { + no strict 'refs'; + *$AUTOLOAD = sub { $val }; # *$AUTOLOAD = sub() { $val }; + } + goto &$AUTOLOAD; +} + +bootstrap IO::Interface $VERSION; + +# copy routines into IO::Socket +{ + no strict 'refs'; + *{"IO\:\:Socket\:\:$_"} = \&$_ foreach @functions; +} + +# Preloaded methods go here. + +sub if_list { + my %hash = map {$_=>undef} &_if_list; + sort keys %hash; +} + +sub addr_to_interface { + my ($sock,$addr) = @_; + return "any" if $addr eq '0.0.0.0'; + my @interfaces = $sock->if_list; + foreach (@interfaces) { + my $if_addr = $sock->if_addr($_) or next; + return $_ if $if_addr eq $addr; + } + return; # couldn't find it +} + +# Autoload methods go after =cut, and are processed by the autosplit program. +1; +__END__ + +=head1 NAME + +IO::Interface - Perl extension for access to network card configuration information + +=head1 SYNOPSIS + + # ====================== + # the new, preferred API + # ====================== + + use IO::Interface::Simple; + + my $if1 = IO::Interface::Simple->new('eth0'); + my $if2 = IO::Interface::Simple->new_from_address('127.0.0.1'); + my $if3 = IO::Interface::Simple->new_from_index(1); + + my @interfaces = IO::Interface::Simple->interfaces; + + for my $if (@interfaces) { + print "interface = $if\n"; + print "addr = ",$if->address,"\n", + "broadcast = ",$if->broadcast,"\n", + "netmask = ",$if->netmask,"\n", + "dstaddr = ",$if->dstaddr,"\n", + "hwaddr = ",$if->hwaddr,"\n", + "mtu = ",$if->mtu,"\n", + "metric = ",$if->metric,"\n", + "index = ",$if->index,"\n"; + + print "is running\n" if $if->is_running; + print "is broadcast\n" if $if->is_broadcast; + print "is p-to-p\n" if $if->is_pt2pt; + print "is loopback\n" if $if->is_loopback; + print "is promiscuous\n" if $if->is_promiscuous; + print "is multicast\n" if $if->is_multicast; + print "is notrailers\n" if $if->is_notrailers; + print "is noarp\n" if $if->is_noarp; + } + + + # =========== + # the old API + # =========== + + use IO::Socket; + use IO::Interface qw(:flags); + + my $s = IO::Socket::INET->new(Proto => 'udp'); + my @interfaces = $s->if_list; + + for my $if (@interfaces) { + print "interface = $if\n"; + my $flags = $s->if_flags($if); + print "addr = ",$s->if_addr($if),"\n", + "broadcast = ",$s->if_broadcast($if),"\n", + "netmask = ",$s->if_netmask($if),"\n", + "dstaddr = ",$s->if_dstaddr($if),"\n", + "hwaddr = ",$s->if_hwaddr($if),"\n"; + + print "is running\n" if $flags & IFF_RUNNING; + print "is broadcast\n" if $flags & IFF_BROADCAST; + print "is p-to-p\n" if $flags & IFF_POINTOPOINT; + print "is loopback\n" if $flags & IFF_LOOPBACK; + print "is promiscuous\n" if $flags & IFF_PROMISC; + print "is multicast\n" if $flags & IFF_MULTICAST; + print "is notrailers\n" if $flags & IFF_NOTRAILERS; + print "is noarp\n" if $flags & IFF_NOARP; + } + + my $interface = $s->addr_to_interface('127.0.0.1'); + + +=head1 DESCRIPTION + +IO::Interface adds methods to IO::Socket objects that allows them to +be used to retrieve and change information about the network +interfaces on your system. In addition to the object-oriented access +methods, you can use a function-oriented style. + +THIS API IS DEPRECATED. Please see L for the +preferred way to get and set interface configuration information. + +=head2 Creating a Socket to Access Interface Information + +You must create a socket before you can access interface +information. The socket does not have to be connected to a remote +site, or even used for communication. The simplest procedure is to +create a UDP protocol socket: + + my $s = IO::Socket::INET->new(Proto => 'udp'); + +The various IO::Interface functions will now be available as methods +on this socket. + +=head2 Methods + +=over 4 + +=item @iflist = $s->if_list + +The if_list() method will return a list of active interface names, for +example "eth0" or "tu0". If no interfaces are configured and running, +returns an empty list. + +=item $addr = $s->if_addr($ifname [,$newaddr]) + +if_addr() gets or sets the interface address. Call with the interface +name to retrieve the address (in dotted decimal format). Call with a +new address to set the interface. In the latter case, the routine +will return a true value if the operation was successful. + + my $oldaddr = $s->if_addr('eth0'); + $s->if_addr('eth0','192.168.8.10') || die "couldn't set address: $!"; + +Special case: the address of the pseudo-device "any" will return the +IP address "0.0.0.0", which corresponds to the INADDR_ANY constant. + +=item $broadcast = $s->if_broadcast($ifname [,$newbroadcast] + +Get or set the interface broadcast address. If the interface does not +have a broadcast address, returns undef. + +=item $mask = $s->if_netmask($ifname [,$newmask]) + +Get or set the interface netmask. + +=item $dstaddr = $s->if_dstaddr($ifname [,$newdest]) + +Get or set the destination address for point-to-point interfaces. + +=item $hwaddr = $s->if_hwaddr($ifname [,$newhwaddr]) + +Get or set the hardware address for the interface. Currently only +ethernet addresses in the form "00:60:2D:2D:51:70" are accepted. + +=item $flags = $s->if_flags($ifname [,$newflags]) + +Get or set the flags for the interface. The flags are a bitmask +formed from a series of constants. See L below. + +=item $ifname = $s->addr_to_interface($ifaddr) + +Given an interface address in dotted form, returns the name of the +interface associated with it. Special case: the INADDR_ANY address, +0.0.0.0 will return a pseudo-interface name of "any". + +=back + +=head2 EXPORT + +IO::Interface exports nothing by default. However, you can import the +following symbol groups into your namespace: + + :functions Function-oriented interface (see below) + :flags Flag constants (see below) + :all All of the above + +=head2 Function-Oriented Interface + +By importing the ":functions" set, you can access IO::Interface in a +function-oriented manner. This imports all the methods described +above into your namespace. Example: + + use IO::Socket; + use IO::Interface ':functions'; + + my $sock = IO::Socket::INET->new(Proto=>'udp'); + my @interfaces = if_list($sock); + print "address = ",if_addr($sock,$interfaces[0]); + +=head2 Exportable constants + +The ":flags" constant imports the following constants for use with the +flags returned by if_flags(): + + IFF_ALLMULTI + IFF_AUTOMEDIA + IFF_BROADCAST + IFF_DEBUG + IFF_LOOPBACK + IFF_MASTER + IFF_MULTICAST + IFF_NOARP + IFF_NOTRAILERS + IFF_POINTOPOINT + IFF_PORTSEL + IFF_PROMISC + IFF_RUNNING + IFF_SLAVE + IFF_UP + +This example determines whether interface 'tu0' supports multicasting: + + use IO::Socket; + use IO::Interface ':flags'; + my $sock = IO::Socket::INET->new(Proto=>'udp'); + print "can multicast!\n" if $sock->if_flags & IFF_MULTICAST. + +=head1 AUTHOR + +Lincoln D. Stein +Copyright 2001-2014, Lincoln D. Stein. + +This library is distributed under the Perl Artistic License +2.0. Please see LICENSE for more information. + +=head1 SUPPORT + +For feature requests, bug reports and code contributions, please use +the GitHub repository at +https://github.com/lstein/LibIO-Interface-Perl + +=head1 SEE ALSO + +perl(1), IO::Socket(3), IO::Multicast(3), L + +=cut diff --git a/lib/site/IO/Interface/Simple.pm b/lib/site/IO/Interface/Simple.pm new file mode 100644 index 000000000..def0b1ebf --- /dev/null +++ b/lib/site/IO/Interface/Simple.pm @@ -0,0 +1,287 @@ +package IO::Interface::Simple; +use strict; +use IO::Socket; +use IO::Interface; + +use overload '""' => \&as_string, + eq => '_eq_', + fallback => 1; + +# class variable +my $socket; + +# class methods +sub interfaces { + my $class = shift; + my $s = $class->sock; + return sort {($a->index||0) <=> ($b->index||0) } map {$class->new($_)} $s->if_list; +} + +sub new { + my $class = shift; + my $if_name = shift; + my $s = $class->sock; + return unless defined $s->if_mtu($if_name); + return bless {s => $s, + name => $if_name},ref $class || $class; +} + +sub new_from_address { + my $class = shift; + my $addr = shift; + my $s = $class->sock; + my $name = $s->addr_to_interface($addr) or return; + return $class->new($name); +} + +sub new_from_index { + my $class = shift; + my $index = shift; + my $s = $class->sock; + my $name = $s->if_indextoname($index) or return; + return $class->new($name); +} + +sub sock { + my $self = shift; + if (ref $self) { + return $self->{s} ||= $socket; + } else { + return $socket ||= IO::Socket::INET->new(Proto=>'udp'); + } +} + +sub _eq_ { + return shift->name eq shift; +} + +sub as_string { + shift->name; +} + +sub name { + shift->{name}; +} + +sub address { + my $self = shift; + $self->sock->if_addr($self->name,@_); +} + +sub broadcast { + my $self = shift; + $self->sock->if_broadcast($self->name,@_); +} + +sub netmask { + my $self = shift; + $self->sock->if_netmask($self->name,@_); +} + +sub dstaddr { + my $self = shift; + $self->sock->if_dstaddr($self->name,@_); +} + +sub hwaddr { + my $self = shift; + $self->sock->if_hwaddr($self->name,@_); +} + +sub flags { + my $self = shift; + $self->sock->if_flags($self->name,@_); +} + +sub mtu { + my $self = shift; + $self->sock->if_mtu($self->name,@_); +} + +sub metric { + my $self = shift; + $self->sock->if_metric($self->name,@_); +} + +sub index { + my $self = shift; + return $self->sock->if_index($self->name); +} + +sub is_running { shift->_gettestflag(IO::Interface::IFF_RUNNING(),@_) } +sub is_broadcast { shift->_gettestflag(IO::Interface::IFF_BROADCAST(),@_) } +sub is_pt2pt { shift->_gettestflag(IO::Interface::IFF_POINTOPOINT(),@_) } +sub is_loopback { shift->_gettestflag(IO::Interface::IFF_LOOPBACK(),@_) } +sub is_promiscuous { shift->_gettestflag(IO::Interface::IFF_PROMISC(),@_) } +sub is_multicast { shift->_gettestflag(IO::Interface::IFF_MULTICAST(),@_) } +sub is_notrailers { shift->_gettestflag(IO::Interface::IFF_NOTRAILERS(),@_) } +sub is_noarp { shift->_gettestflag(IO::Interface::IFF_NOARP(),@_) } + +sub _gettestflag { + my $self = shift; + my $bitmask = shift; + my $flags = $self->flags; + if (@_) { + $flags |= $bitmask; + $self->flags($flags); + } else { + return ($flags & $bitmask) != 0; + } +} + +1; + +=head1 NAME + +IO::Interface::Simple - Perl extension for access to network card configuration information + +=head1 SYNOPSIS + + use IO::Interface::Simple; + + my $if1 = IO::Interface::Simple->new('eth0'); + my $if2 = IO::Interface::Simple->new_from_address('127.0.0.1'); + my $if3 = IO::Interface::Simple->new_from_index(1); + + my @interfaces = IO::Interface::Simple->interfaces; + + for my $if (@interfaces) { + print "interface = $if\n"; + print "addr = ",$if->address,"\n", + "broadcast = ",$if->broadcast,"\n", + "netmask = ",$if->netmask,"\n", + "dstaddr = ",$if->dstaddr,"\n", + "hwaddr = ",$if->hwaddr,"\n", + "mtu = ",$if->mtu,"\n", + "metric = ",$if->metric,"\n", + "index = ",$if->index,"\n"; + + print "is running\n" if $if->is_running; + print "is broadcast\n" if $if->is_broadcast; + print "is p-to-p\n" if $if->is_pt2pt; + print "is loopback\n" if $if->is_loopback; + print "is promiscuous\n" if $if->is_promiscuous; + print "is multicast\n" if $if->is_multicast; + print "is notrailers\n" if $if->is_notrailers; + print "is noarp\n" if $if->is_noarp; + } + + +=head1 DESCRIPTION + +IO::Interface::Simple allows you to interrogate and change network +interfaces. It has overlapping functionality with Net::Interface, but +might compile and run on more platforms. + +=head2 Class Methods + +=over 4 + +=item $interface = IO::Interface::Simple->new('eth0') + +Given an interface name, new() creates an interface object. + +=item @iflist = IO::Interface::Simple->interfaces; + +Returns a list of active interface objects. + +=item $interface = IO::Interface::Simple->new_from_address('192.168.0.1') + +Returns the interface object corresponding to the given address. + +=item $interface = IO::Interface::Simple->new_from_index(2) + +Returns the interface object corresponding to the given numeric +index. This is only supported on BSD-ish platforms. + +=back + +=head2 Object Methods + +=over 4 + +=item $name = $interface->name + +Get the name of the interface. The interface object is also overloaded +so that if you use it in a string context it is the same as calling +name(). + +=item $index = $interface->index + +Get the index of the interface. This is only supported on BSD-like +platforms. + +=item $addr = $interface->address([$newaddr]) + +Get or set the interface's address. + + +=item $addr = $interface->broadcast([$newaddr]) + +Get or set the interface's broadcast address. + +=item $addr = $interface->netmask([$newmask]) + +Get or set the interface's netmask. + +=item $addr = $interface->hwaddr([$newaddr]) + +Get or set the interface's hardware address. + +=item $addr = $interface->mtu([$newmtu]) + +Get or set the interface's MTU. + +=item $addr = $interface->metric([$newmetric]) + +Get or set the interface's metric. + +=item $flags = $interface->flags([$newflags]) + +Get or set the interface's flags. These can be ANDed with the IFF +constants exported by IO::Interface or Net::Interface in order to +interrogate the state and capabilities of the interface. However, it +is probably more convenient to use the broken-out methods listed +below. + +=item $flag = $interface->is_running([$newflag]) + +=item $flag = $interface->is_broadcast([$newflag]) + +=item $flag = $interface->is_pt2pt([$newflag]) + +=item $flag = $interface->is_loopback([$newflag]) + +=item $flag = $interface->is_promiscuous([$newflag]) + +=item $flag = $interface->is_multicast([$newflag]) + +=item $flag = $interface->is_notrailers([$newflag]) + +=item $flag = $interface->is_noarp([$newflag]) + +Get or set the corresponding configuration parameters. Note that the +operating system may not let you set some of these. + +=back + +=head1 AUTHOR + +Lincoln D. Stein +Copyright 2001-2014, Lincoln D. Stein. + +This library is distributed under the Perl Artistic License +2.0. Please see LICENSE for more information. + +=head1 SUPPORT + +For feature requests, bug reports and code contributions, please use +the GitHub repository at +https://github.com/lstein/LibIO-Interface-Perl + +=head1 SEE ALSO + +L, L, L), L, L + +=cut + From ee037f6dd9abe5c2110931960f32847bcc6382cf Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 16 Feb 2017 15:41:05 -0700 Subject: [PATCH 116/209] v2.0b1 - structure to support controller push commands --- lib/raZberry.pm | 164 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 38 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 190af438c..81c8fcb55 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -6,7 +6,7 @@ In user code: use raZberry; - $razberry_controller = new raZberry('10.0.1.1'); + $razberry_controller = new raZberry('10.0.1.1',10); $razberry_comm = new raZberry_comm($razberry_controller); $room_fan = new raZberry_dimmer($razberry_controller,'2','force_update'); $room_blind = new raZberry_blind($razberry_controller,'3','digital'); @@ -54,16 +54,56 @@ the razberry is polled every 5 seconds. Update for local control use the 'niffler' plug in. This saves forcing a local device status every poll. -=head3 SENSOR STATE CHILD OBJECT +=head3 CHILD OBJECTS Each device class will need a child object, as the controller object is just a gateway -to the hardware. Currently the only working device is a razberry_dimmer, and has only -been tested with the leviton fan +to the hardware. There is also a communication object to allow for alerting and monitoring of the razberry controller. -=head2 NOTES +=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: + +1: Enable anonymous authentication: +- Create a room named devices, and assign all ZWay devices to that room +- Create a user named anonymous with role anonymous +- Edit user anonymous and allow access to room devices + +2: Create a new user and give it the admin role. Then in the controller definition, provide the username and password: +$razberry_controller = new raZberry('10.0.1.1',10,"method=poll,username=user,password=pwd"); + + +=head2 v2 PUSH or POLL +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%) + +If the razberry or mh get out of sync, and $controller->poll can be issued to get the latest states. + +Only 1 razberry controller can be the push source, due to only a single controller can be linked to the web service. + +=head2 MH.INI CONFIG PARAMS + +raZberry_timeout +raZberry_poll_seconds + + +=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.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) @@ -82,29 +122,6 @@ v1.2 - added a check to see if the device is 'dead'. If dead it will attempt a ping for X attempts a Y seconds apart. -OTHER - -Works and tested with v2.0.0. It _should_ also work with v1.7.4. -For later versions, Z_Way has introduced authentication. raZberry will support that at a later time -To get a 2.0+ version to work, anonymous authentication has to be enabled: -- Create a room named devices, and assign all ZWay devices to that room -- Create a user named anonymous with role anonymous -- Edit user anonymous and allow access to room devices - - -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. - -config parmas - -raZberry_timeout -raZberry_poll_seconds - -=head2 BUGS - - - -=head2 METHODS =over @@ -119,9 +136,10 @@ use warnings; use LWP::UserAgent; use HTTP::Request::Common qw(POST); +use HTTP::Cookies; use JSON qw(decode_json); -use Data::Dumper; +#use Data::Dumper; @raZberry::ISA = ('Generic_Item'); @@ -136,6 +154,7 @@ $zway_system{id}{2} = "2"; my $zway_vdev = "ZWayVDev_zway"; my $zway_suffix = "-0-38"; +our $push_obj = ""; our %rest; $rest{api} = ""; @@ -157,7 +176,7 @@ $rest{usercode} = "devices"; $rest{controller} = "Data/*"; sub new { - my ( $class, $addr, $poll, $method ) = @_; + my ( $class, $addr, $poll, $options ) = @_; my $self = {}; bless $self, $class; $self->{data} = undef; @@ -174,7 +193,9 @@ sub new { $self->{host} = $host; $self->{port} = 8083; $self->{port} = $port if ($port); - $self->{debug} = 1; + $self->{debug} = 0; + ( $self->{debug} ) = ( $options =~ /debug=(\s+)/i ) + if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); $self->{debug} = $main::Debug{raZberry} if ( defined $main::Debug{raZberry} ); $self->{lastupdate} = undef; @@ -183,8 +204,18 @@ sub new { if ( defined $main::config_parms{raZberry_timeout} ); $self->{status} = ""; $self->{controller_data} = (); - $self->{push} = 0; + &main::print_log("[raZberry]: options are $options") + if ( ( $self->{debug} ) and ( defined $options ) ); + + my $method = "poll"; + ($method) = ( $options =~ /method=(\s+)/ ) if ( defined $options ); + $self->{push} = 0; $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); + $self->{username} = ""; + ( $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]: Push method selected"); @@ -192,7 +223,7 @@ sub new { "[raZberry]: The HTTPGet Automation module needs to be installed for push to work" ); &main::print_log( - '[raZberry]: URL is http://mh:port/SUB?razberry_push(%DEVICE%,%LEVEL%).' + '[raZberry]: URL is http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%)' ); $push_obj = \%{$self}; } @@ -201,8 +232,12 @@ sub new { "[raZberry]: Push method already in use on other object") if ($push_obj); &main::print_log("[raZberry]: Poll method selected"); + $self->{push} = 0; + } + if ( $self->{username} ) { + $self->{cookie_jar} = HTTP::Cookies->new( {} ); + $self->login; } - $self->get_controllerdata; $self->{timer} = new Timer; unless ( $self->{push} ) { @@ -215,6 +250,56 @@ sub new { return $self; } +sub login { + my ($self) = @_; + + my $ua = new LWP::UserAgent( keep_alive => 1 ); + $ua->timeout( $self->{timeout} ); + $ua->cookie_jar( $self->{cookie_jar} ); + $ua->default_header( 'Accept' => "application/json" ); + $ua->default_header( 'Content-Type' => "application/json" ); + + my $host = $self->{host}; + my $port = $self->{port}; + &main::print_log("[raZberry]: Attempting to authenticate to host $host"); + &main::print_log( "[raZberry]: with user:" + . $self->{username} + . " password: " + . $self->{password} ) + if ( $self->{debug} ); + + my $request = + HTTP::Request->new( + POST => "http://$host:$port/ZAutomation/api/v1/login" ); + my $json = + '{"form": true, "login": "' + . $self->{username} + . '", "password": "' + . $self->{password} + . '", "keepme": false, "default_ui": 1}'; + $request->content($json); + 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]: Error attempting to authenticate to $host"); + &main::print_log( "[raZberry]: Code is " + . $responseObj->code + . " and content is " + . $responseObj->content ); + } + else { + &main::print_log("[raZberry]: Successful authentication."); + $self->{login_success} = 1; + } +} + sub get_controllerdata { my ($self) = @_; my ( $isSuccessResponse1, $controller_data ) = @@ -459,6 +544,7 @@ sub _get_JSON_data { $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}; @@ -628,15 +714,17 @@ sub register { } sub main::razberry_push { - my ( $id, $level ) = @_; + my ( $dev, $level ) = @_; + + my ($id) = ( split /_/, $dev )[-1]; #always just get the last element &main::print_log( - "[raZberry]: HTTP Push update called for device id: $id and level $level" + "[raZberry]: HTTP Push update received for device: $dev, id: $id and level: $level" ); #my $obj = &main::get_object_by_name($object); unless ( defined $push_obj ) { - &main::print_log("[raZberry]: Error, Push controller not defined!"); + &main::print_log("[raZberry]: ERROR, Push controller not defined!"); } else { @@ -651,7 +739,7 @@ sub main::razberry_push { } else { &main::print_log( - "[raZberry]: WARNING, $id child object not found!"); + "[raZberry]: ERROR, child object id $id not found!"); } } } From 9effb09f2fd1bb2cb237c134fa7567e328994b26 Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 16 Feb 2017 16:15:37 -0700 Subject: [PATCH 117/209] Added in Olaf's fix to support newer versions of RRDTool --- lib/json_server.pl | 109 +++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/lib/json_server.pl b/lib/json_server.pl index 795fd0924..9829aecdf 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -1,3 +1,4 @@ + =head1 B =head2 SYNOPSIS @@ -229,8 +230,8 @@ sub json_get { print_log "Json_Server.pl: Master value = $json_data{'ia7_config'}->{prefs}->{$key}"; $json_data{'ia7_config'}->{prefs}->{$key} = - $json_data{'ia7_config'}->{clients} - ->{ $Http{Client_address} }->{$key}; + $json_data{'ia7_config'}->{clients}->{ $Http{Client_address} } + ->{$key}; } delete $json_data{'ia7_config'}->{clients}; } @@ -258,16 +259,17 @@ sub json_get { # RRD data routines if ( $path[0] eq 'rrd' || $path[0] eq '' ) { my $path = "$config_parms{data_dir}/rrd"; - $path = "$config_parms{rrd_dir}" - if ( defined $config_parms{rrd_dir} ); + $path = "$config_parms{rrd_dir}" + if ( ( defined $config_parms{rrd_dir} ) + and ( $config_parms{rrd_dir} ) ); $path = $json_data{'rrd_config'}->{'prefs'}->{'path'} if ( defined $json_data{'rrd_config'}->{'prefs'}->{'path'} ); my $rrd_file = "weather_data.rrd"; - $rrd_file = $config_parms{weather_data_rrd} - if ( defined $config_parms{weather_data_rrd} ); - if ( $rrd_file =~ m/.*\/(.*\.rrd)/ ) { - $rrd_file = $1; - } + $rrd_file = $config_parms{weather_data_rrd} + if ( defined $config_parms{weather_data_rrd} ); + if ( $rrd_file =~ m/.*\/(.*\.rrd)/ ) { + $rrd_file = $1; + } $rrd_file = $json_data{'rrd_config'}->{'prefs'}->{'default_rrd'} if ( defined $json_data{'rrd_config'}->{'prefs'}->{'default_rrd'} ); my $default_cf = "AVERAGE"; @@ -287,12 +289,16 @@ sub json_get { my @round = (); my @type = (); my $celsius = 0; + my $kph = 0; my $arg_time = 0; my $xml_info; $arg_time = int( $args{time}[0] ) if ( defined int( $args{time}[0] ) ); $celsius = 1 if ( $config_parms{weather_uom_temp} eq 'C' ); $celsius = 1 if ( lc $json_data{'rrd_config'}->{'prefs'}->{'uom'} eq "celsius" ); + + $kph = 1 if ( $config_parms{weather_uom_wind} eq 'kph' ); + my %data; my $end = "now"; @@ -311,8 +317,8 @@ sub json_get { ->{'group'} ) { foreach my $group ( split /,/, - $json_data{'rrd_config'}->{'ds'}->{$dsg}->{'group'} - ) + $json_data{'rrd_config'}->{'ds'}->{$dsg} + ->{'group'} ) { push @{ $args{ds} }, $dsg if ( lc $group ) eq ( lc $args{group}[0] ); @@ -334,13 +340,13 @@ sub json_get { $dataset[$index]->{'label'} = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'label'} if ( - defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'label'} - ); + defined $json_data{'rrd_config'}->{'ds'}->{$ds} + ->{'label'} ); $dataset[$index]->{'color'} = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'color'} if ( - defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'color'} - ); + defined $json_data{'rrd_config'}->{'ds'}->{$ds} + ->{'color'} ); if ( lc $json_data{'rrd_config'}->{'ds'}->{$ds}->{'type'} eq "bar" ) @@ -363,8 +369,8 @@ sub json_get { $round[$index] = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'round'} if ( - defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'round'} - ); + defined $json_data{'rrd_config'}->{'ds'}->{$ds} + ->{'round'} ); $type[$index] = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'type'} if ( @@ -379,40 +385,45 @@ sub json_get { $xml_info = $rrd->info if ( $default_timestamp eq "true" ); my @lines = split /\n/, $xml; - foreach my $line (@lines) { + my ($step) = $xml =~ + /\(\d+)\<\/step\>/; #this is the width for any bar charts - #print "line=$line\n"; - my ($step) = $line =~ /\(\d+)\<\/step\>/ - ; #this is the width for any bar charts - if ($step) { - for my $i ( 0 .. $#dataset ) { - $dataset[$i]->{'bars'}->{'barWidth'} = - int($step) * 1000 - if ( defined $dataset[$i]->{'bars'} ); - } + if ($step) { + for my $i ( 0 .. $#dataset ) { + $dataset[$i]->{'bars'}->{'barWidth'} = int($step) * 1000 + if ( defined $dataset[$i]->{'bars'} ); } - my ($time) = $line =~ /\\(\d*)\<\/t\>/; - $time = $time * 1000; #javascript is in milliseconds - next if ( $arg_time > int($time) ); #only return new items + } + + my $time_index = 0; + my ($db_start) = $xml =~ /\(\d+)\<\/start\>/; + + foreach my $line (@lines) { my (@values) = $line =~ /\(-?[e.+-\d]*|NaN)\<\/v\>/g; - if ($time) { - my $index = 0; - foreach my $value (@values) { - my $value1 = sprintf( "%.10g", $value ); - $value1 = ( $value1 - 32 ) * ( 5 / 9 ) - if ( ($celsius) - and ( lc $type[$index] eq "temperature" ) ); - $value1 = - sprintf( "%." . $round[$index] . "f", $value1 ) - if ( defined $round[$index] ); - $value1 =~ s/\.0*$// - unless ( $value1 == 0 ) - ; #remove unneccessary trailing decimals - $value1 = "null" if ( lc $value1 eq "nan" ); - push @{ $dataset[$index]->{data} }, [ $time, $value1 ]; - $index++; - } + + next if ( !@values ); #skip header lines + + my $index = 0; + foreach my $value (@values) { + my $value1 = sprintf( "%.10g", $value ); + $value1 = ( $value1 - 32 ) * ( 5 / 9 ) + if ( ($celsius) + and ( lc $type[$index] eq "temperature" ) ); + $value1 = sprintf( "%.2f", $value1 * 1.60934 ) + if ( ($kph) and ( lc $type[$index] eq "speed" ) ); + $value1 = sprintf( "%." . $round[$index] . "f", $value1 ) + if ( defined $round[$index] ); + $value1 =~ s/\.0*$// + unless ( $value1 == 0 ) + ; #remove unneccessary trailing decimals + $value1 = "null" if ( lc $value1 eq "nan" ); + push @{ $dataset[$index]->{data} }, + [ + ( $db_start + ( $time_index * $step ) ) * 1000, $value1 + ]; + $index++; } + $time_index++; } } $data{'data'} = \@dataset; @@ -652,7 +663,8 @@ sub json_get { for my $i ( 0 .. $#json_notifications ) { my $n_time = int( $json_notifications[$i]{time} ); - my $x = $args{time}[0] + my $x = + $args{time}[0] ; #Weird, does nothing, but notifications doesn't work if removed... if ( ($n_time) and @@ -1513,4 +1525,3 @@ =head2 LICENSE You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. =cut - From 1d3d63a062954a69b35c6d1429b22279960164bf Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 17 Feb 2017 12:51:07 -0700 Subject: [PATCH 118/209] v2.0b2: added push object into poll for testing --- lib/raZberry.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 81c8fcb55..0c7b60653 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0 +=head1 B v2.0b2 =head2 SYNOPSIS @@ -232,6 +232,7 @@ sub new { "[raZberry]: Push method already in use on other object") if ($push_obj); &main::print_log("[raZberry]: Poll method selected"); + $push_obj = \%{$self}; $self->{push} = 0; } if ( $self->{username} ) { From 07a0e0ea29945d1916b3bb0633ec11b18a315db7 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 18 Feb 2017 10:07:36 -0700 Subject: [PATCH 119/209] v2.0b3 - added in read table A, fixed a bug in b2, and increased perltidy length --- bin/.perltidyrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/.perltidyrc b/bin/.perltidyrc index c689e069a..c63adb806 100644 --- a/bin/.perltidyrc +++ b/bin/.perltidyrc @@ -51,7 +51,7 @@ --look-for-selfloader --maximum-consecutive-blank-lines=1 --maximum-fields-per-table=0 ---maximum-line-length=80 +--maximum-line-length=160 --minimum-space-to-comment=4 --noopening-anonymous-sub-brace-on-new-line --noopening-brace-on-new-line From 49bf8a2dcf185d8170e1412230c625ab4e23fe01 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 18 Feb 2017 14:37:40 -0700 Subject: [PATCH 120/209] v2.0b3 - actually added the files this time --- lib/raZberry.pm | 451 ++++++++------------------ lib/read_table_A.pl | 752 +++++++++++++++++--------------------------- 2 files changed, 419 insertions(+), 784 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 0c7b60653..405b1496e 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b2 +=head1 B v2.0b3 =head2 SYNOPSIS @@ -24,7 +24,7 @@ raZberry_(,,) In items.mht: -RAZBERRY_CONTROLLER, ip_address, controller_name, group, options +RAZBERRY_CONTROLLER, ip_address, controller_name, group, poll time, options RAZBERRY_DIMMER, device_id, name, group, controller_name, options RAZBERRY_SWITCH, device_id, name, group, controller_name, options RAZBERRY_BLIND, device_id, name, group, controller_name, options @@ -38,6 +38,10 @@ for example: RAZBERRY_CONTROLLER, 10.0.1.1, razberry_controller, zwave RAZBERRY_BLIND, 4, main_blinds, HVAC|zwave, razberry_controller, battery +for specifying controller options; + +RAZBERRY_CONTROLLER, 10.0.1.1, razberry_controller, zwave, ,'username=admin,password=bob' + =head2 DESCRIPTION @@ -76,13 +80,13 @@ For later versions, Z_Way has introduced authentication. raZberry v2.0 supports $razberry_controller = new raZberry('10.0.1.1',10,"method=poll,username=user,password=pwd"); -=head2 v2 PUSH or POLL +=head2 v2 PUSH or POLL. Only supported in version raZberry 2.1.0+ 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%) -If the razberry or mh get out of sync, and $controller->poll can be issued to get the latest states. +If the razberry or mh get out of sync, $controller->poll can be issued to get the latest states. -Only 1 razberry controller can be the push source, due to only a single controller can be linked to the web service. +Only 1 razberry controller can be the push source, due to only a single controller object that can be linked to the web service. =head2 MH.INI CONFIG PARAMS @@ -185,55 +189,41 @@ sub new { $self->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} if ( defined $main::config_parms{raZberry_poll_seconds} ); $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 - if ( $self->{config}->{poll_seconds} < 1 ); - $self->{updating} = 0; - $self->{data}->{retry} = 0; + $self->{config}->{poll_seconds} = 1 if ( $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=(\s+)/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->{debug} ) = ( $options =~ /debug=(\s+)/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} = (); - &main::print_log("[raZberry]: options are $options") - if ( ( $self->{debug} ) and ( defined $options ) ); + &main::print_log("[raZberry]: options are $options") if ( ( $self->{debug} ) and ( defined $options ) ); my $method = "poll"; ($method) = ( $options =~ /method=(\s+)/ ) if ( defined $options ); - $self->{push} = 0; - $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); + $self->{push} = 0; + $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); $self->{username} = ""; - ( $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 ) ); - + ( $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]: Push method selected"); - &main::print_log( - "[raZberry]: The HTTPGet Automation module needs to be installed for push to work" - ); - &main::print_log( - '[raZberry]: URL is http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%)' - ); + &main::print_log("[raZberry]: The HTTPGet Automation module needs to be installed for push to work"); + &main::print_log('[raZberry]: URL is http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%)'); $push_obj = \%{$self}; } else { - &main::print_log( - "[raZberry]: Push method already in use on other object") - if ($push_obj); + &main::print_log("[raZberry]: Push method already in use on other object") if ($push_obj); &main::print_log("[raZberry]: Poll method selected"); - $push_obj = \%{$self}; - $self->{push} = 0; + $push_obj = \%{$self}; #TODO: remove when out of beta + $self->{push} = 1; } if ( $self->{username} ) { $self->{cookie_jar} = HTTP::Cookies->new( {} ); @@ -263,21 +253,10 @@ sub login { my $host = $self->{host}; my $port = $self->{port}; &main::print_log("[raZberry]: Attempting to authenticate to host $host"); - &main::print_log( "[raZberry]: with user:" - . $self->{username} - . " password: " - . $self->{password} ) - if ( $self->{debug} ); + &main::print_log( "[raZberry]: with user:" . $self->{username} . " password: " . $self->{password} ) if ( $self->{debug} ); - my $request = - HTTP::Request->new( - POST => "http://$host:$port/ZAutomation/api/v1/login" ); - my $json = - '{"form": true, "login": "' - . $self->{username} - . '", "password": "' - . $self->{password} - . '", "keepme": false, "default_ui": 1}'; + my $request = HTTP::Request->new( POST => "http://$host:$port/ZAutomation/api/v1/login" ); + my $json = '{"form": true, "login": "' . $self->{username} . '", "password": "' . $self->{password} . '", "keepme": false, "default_ui": 1}'; $request->content($json); my $responseObj = $ua->request($request); $self->{cookie_jar}->extract_cookies($responseObj); @@ -288,12 +267,8 @@ sub login { #print $responseObj->content . "\n--------------------\n"; if ( $responseObj->code > 400 ) { $self->{login_success} = 0; - &main::print_log( - "[raZberry]: Error attempting to authenticate to $host"); - &main::print_log( "[raZberry]: Code is " - . $responseObj->code - . " and content is " - . $responseObj->content ); + &main::print_log("[raZberry]: Error attempting to authenticate to $host"); + &main::print_log( "[raZberry]: Code is " . $responseObj->code . " and content is " . $responseObj->content ); } else { &main::print_log("[raZberry]: Successful authentication."); @@ -303,25 +278,19 @@ sub login { sub get_controllerdata { my ($self) = @_; - my ( $isSuccessResponse1, $controller_data ) = - _get_JSON_data( $self, 'controller' ); + my ( $isSuccessResponse1, $controller_data ) = _get_JSON_data( $self, 'controller' ); if ($isSuccessResponse1) { #print Dumper $controller_data; $self->{controller_data} = $controller_data->{controller}->{data}; &main::print_log("[raZberry]: Controller found"); - &main::print_log( "[raZberry]: Chip version:\t\t" - . $self->{controller_data}->{ZWaveChip}->{value} ); - &main::print_log( "[raZberry]: Software version:\t" - . $self->{controller_data}->{softwareRevisionVersion}->{value} ); - &main::print_log( "[raZberry]: API version:\t\t" - . $self->{controller_data}->{APIVersion}->{value} ); - &main::print_log( "[raZberry]: SDK version:\t\t" - . $self->{controller_data}->{SDK}->{value} ); + &main::print_log( "[raZberry]: Chip version:\t\t" . $self->{controller_data}->{ZWaveChip}->{value} ); + &main::print_log( "[raZberry]: Software version:\t" . $self->{controller_data}->{softwareRevisionVersion}->{value} ); + &main::print_log( "[raZberry]: API version:\t\t" . $self->{controller_data}->{APIVersion}->{value} ); + &main::print_log( "[raZberry]: SDK version:\t\t" . $self->{controller_data}->{SDK}->{value} ); } else { - &main::print_log( - "[raZberry]: Problem connecting to controller " . $self->{host} ); + &main::print_log( "[raZberry]: Problem connecting to controller " . $self->{host} ); } } @@ -334,109 +303,79 @@ sub poll { &main::print_log("[raZberry]: cmd=$cmd") if ( $self->{debug} > 1 ); for my $dev ( keys %{ $self->{data}->{force_update} } ) { - &main::print_log( - "[raZberry]: Forcing update to device $dev to account for local changes" - ) if ( $self->{debug} ); + &main::print_log( "[raZberry]: Forcing update to device $dev to account for local changes" ) if ( $self->{debug} ); $self->update_dev($dev); } for my $dev ( keys %{ $self->{data}->{ping} } ) { - &main::print_log("[raZberry]: Keep_alive: Pinging device $dev...") - ; # if ($self->{debug}); - &main::print_log("[raZberry]: ping_dev $dev"); # if ($self->{debug}); - #$self->ping_dev($dev); + &main::print_log("[raZberry]: Keep_alive: Pinging device $dev..."); # if ($self->{debug}); + &main::print_log("[raZberry]: ping_dev $dev"); # if ($self->{debug}); + #$self->ping_dev($dev); } - my ( $isSuccessResponse1, $devices ) = - _get_JSON_data( $self, 'devices', $cmd ); + my ( $isSuccessResponse1, $devices ) = _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]: Found:" - . $item->{id} - . " with level " - . $item->{metrics}->{level} - . " and updated " - . $item->{updateTime} - . "." ) + &main::print_log( "[raZberry]: 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 + 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$/ ); - if ($battery_dev) { #for a battery, set a different object - $self->{data}->{devices}->{$id}->{battery_level} = - $item->{metrics}->{level}; + if ($battery_dev) { #for a battery, set a different object + $self->{data}->{devices}->{$id}->{battery_level} = $item->{metrics}->{level}; } else { - $self->{data}->{devices}->{$id}->{level} = - $item->{metrics}->{level}; + $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}; + $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} + $self->{data}->{devices}->{$id}->{units} = $item->{metrics}->{scaleTitle} if ( defined $item->{metrics}->{scaleTitle} ); - $self->{data}->{devices}->{$id}->{temp_min} = - $item->{metrics}->{min} + $self->{data}->{devices}->{$id}->{temp_min} = $item->{metrics}->{min} if ( defined $item->{metrics}->{min} ); - $self->{data}->{devices}->{$id}->{temp_max} = - $item->{metrics}->{max} + $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]: Child object detected: Battery Level:[" + &main::print_log( "[raZberry]: Child object detected: Battery Level:[" . $item->{metrics}->{level} . "] Child Level:[" . $self->{child_object}->{$id}->battery_level() . "]" ) if ( $self->{debug} > 1 ); - $self->{child_object}->{$id} - ->update_data( $self->{data}->{devices}->{$id} ) - ; #be able to push other data to objects for actions + $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]: Child object detected: Controller Level:[" + &main::print_log( "[raZberry]: 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 + $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]: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -456,11 +395,9 @@ sub set_dev { $cmd .= "$lvl" if $lvl; &main::print_log("[raZberry]: sending command $cmd") if ( $self->{debug} > 1 ); - my ( $isSuccessResponse1, $status ) = - _get_JSON_data( $self, 'devices', $cmd ); + my ( $isSuccessResponse1, $status ) = _get_JSON_data( $self, 'devices', $cmd ); unless ($isSuccessResponse1) { - &main::print_log( - "[raZberry]: Problem retrieving data from " . $self->{host} ); + &main::print_log( "[raZberry]: Problem retrieving data from " . $self->{host} ); return ('0'); } @@ -481,8 +418,7 @@ sub ping_dev { &main::print_log("ping cmd=$cmd"); # if ($self->{debug} > 1); my ( $isSuccessResponse0, $status ) = _get_JSON_data( $self, 'ping', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( "[raZberry]: Error: Problem retrieving data from " - . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -499,12 +435,10 @@ sub isfailed_dev { my $cmd; $cmd = "%5B" . $devid . "%5D.data.isFailed.value"; &main::print_log("isFailed cmd=$cmd"); # if ($self->{debug} > 1); - my ( $isSuccessResponse0, $status ) = - _get_JSON_data( $self, 'isfailed', $cmd ); + my ( $isSuccessResponse0, $status ) = _get_JSON_data( $self, 'isfailed', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( "[raZberry]: Error: Problem retrieving data from " - . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('error'); } @@ -515,21 +449,13 @@ sub update_dev { my ( $self, $device ) = @_; my $cmd; my ( $devid, $instance, $class ) = ( split /-/, $device )[ 0, 1, 2 ]; - $cmd = "%5B" - . $devid - . "%5D.instances%5B" - . $instance - . "%5D.commandClasses%5B" - . $class - . "%5D.Get()"; + $cmd = "%5B" . $devid . "%5D.instances%5B" . $instance . "%5D.commandClasses%5B" . $class . "%5D.Get()"; &main::print_log("[raZberry]: Getting local state from $device ($devid)...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); - my ( $isSuccessResponse0, $status ) = - _get_JSON_data( $self, 'force_update', $cmd ); + my ( $isSuccessResponse0, $status ) = _get_JSON_data( $self, 'force_update', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( "[raZberry]: Error: Problem retrieving data from " - . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -559,13 +485,9 @@ sub _get_JSON_data { or ( $mode eq "usercode" ) or ( $mode eq "usercode_data" ) ); $method = "ZWaveAPI" if ( $mode eq "controller" ); - &main::print_log( - "[raZberry]: contacting http://$host:$port/$method/$rest{$mode}$params" - ) if ( $self->{debug} ); + &main::print_log( "[raZberry]: contacting http://$host:$port/$method/$rest{$mode}$params" ) if ( $self->{debug} ); - my $request = - HTTP::Request->new( - GET => "http://$host:$port/$method/$rest{$mode}$params" ); + my $request = HTTP::Request->new( GET => "http://$host:$port/$method/$rest{$mode}$params" ); $request->content_type("application/x-www-form-urlencoded"); my $responseObj = $ua->request($request); @@ -578,14 +500,11 @@ sub _get_JSON_data { my $isSuccessResponse = $responseCode < 400; $self->{updating} = 0; if ( !$isSuccessResponse ) { - &main::print_log( - "[raZberry]: Warning, failed to get data. Response code $responseCode" - ); + &main::print_log( "[raZberry]: 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 " + main::print_log "[raZberry]: Communication Tracking object found. Updating from " . $self->{child_object}->{comm}->state() . " to offline..." if ( $self->{loglevel} ); @@ -597,10 +516,7 @@ sub _get_JSON_data { 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..." + 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' ); } @@ -608,28 +524,23 @@ sub _get_JSON_data { 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. + 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 + $response = decode_json( $responseObj->content ); #HP, wrap this in eval to prevent MH crashes }; if ($@) { - &main::print_log( - "[raZberry]: WARNING: decode_json failed for returned data"); + &main::print_log("[raZberry]: WARNING: decode_json failed for returned data"); return ( "0", "" ); } return ( $isSuccessResponse, $response ) } else { - &main::print_log( - "[raZberry]: Warning, not fetching data due to operation in progress" - ); + &main::print_log( "[raZberry]: Warning, not fetching data due to operation in progress" ); return ('0'); } } @@ -643,8 +554,7 @@ sub stop_timer { sub start_timer { my ($self) = @_; - $self->{timer}->set( $self->{config}->{poll_seconds}, - sub { &raZberry::poll($self) }, -1 ); + $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &raZberry::poll($self) }, -1 ); } sub display_all_devices { @@ -654,10 +564,8 @@ sub display_all_devices { print "RaZberry Device $id\n"; print "\t level:\t\t $self->{data}->{devices}->{$id}->{level}\n"; - print "\t updateTime:\t " - . localtime( $self->{data}->{devices}->{$id}->{updateTime} ) . "\n"; - print - "\t deviceType:\t $self->{data}->{devices}->{$id}->{devicetype}\n"; + print "\t updateTime:\t " . localtime( $self->{data}->{devices}->{$id}->{updateTime} ) . "\n"; + print "\t deviceType:\t $self->{data}->{devices}->{$id}->{devicetype}\n"; print "\t location:\t $self->{data}->{devices}->{$id}->{location}\n"; print "\t title:\t\t $self->{data}->{devices}->{$id}->{title}\n"; print "\t icon:\t\t $self->{data}->{devices}->{$id}->{icon}\n\n"; @@ -674,8 +582,7 @@ sub get_dev_status { } else { - &main::print_log( - "[raZberry]: Warning, unable to get status of device $id"); + &main::print_log("[raZberry]: Warning, unable to get status of device $id"); return 0; } @@ -684,8 +591,7 @@ sub get_dev_status { sub register { my ( $self, $object, $dev, $options ) = @_; if ( lc $dev eq 'comm' ) { - &main::print_log( - "[raZberry]: Registering Communication object to controller"); + &main::print_log("[raZberry]: Registering Communication object to controller"); $self->{child_object}->{'comm'} = $object; } else { @@ -693,22 +599,16 @@ sub register { my $type = $object->{type}; $type = "Digital " . $type if ( ( defined $options ) and ( $options =~ m/digital/ ) ); - &main::print_log( "[raZberry]: Registering " - . $type - . " Device ID $dev to controller" ); + &main::print_log( "[raZberry]: Registering " . $type . " Device ID $dev to controller" ); $self->{child_object}->{$dev} = $object; if ( defined $options ) { if ( $options =~ m/force_update/ ) { $self->{data}->{force_update}->{$dev} = 1; - &main::print_log( - "[raZberry]: Forcing Controller to contact Device $dev at each poll" - ); + &main::print_log( "[raZberry]: Forcing Controller to contact Device $dev at each poll" ); } if ( $options =~ m/keep_alive/ ) { $self->{data}->{ping}->{$dev} = 1; - &main::print_log( - "[raZberry]: Forcing Controller to ping Device $dev at each poll" - ); + &main::print_log( "[raZberry]: Forcing Controller to ping Device $dev at each poll" ); } } } @@ -719,28 +619,21 @@ sub main::razberry_push { my ($id) = ( split /_/, $dev )[-1]; #always just get the last element - &main::print_log( - "[raZberry]: HTTP Push update received for device: $dev, id: $id and level: $level" - ); + &main::print_log("[raZberry]: HTTP Push update received for device: $dev, id: $id and level: $level"); #my $obj = &main::get_object_by_name($object); - unless ( defined $push_obj ) { - &main::print_log("[raZberry]: ERROR, Push controller not defined!"); + if ( $push_obj eq "" ) { + &main::print_log("[raZberry]: ERROR, Push control not enabled on this controller."); } else { if ( defined $push_obj->{child_object}->{$id} ) { - &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' - . $id - . '}->set( ' - . $level - . ", 'poll' );" ); + &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set( ' . $level . ", 'poll' );" ); #$push_obj->{child_object}->{$id}->set( $level, 'poll' ); } else { - &main::print_log( - "[raZberry]: ERROR, child object id $id not found!"); + &main::print_log("[raZberry]: ERROR, child object id $id not found!"); } } } @@ -754,11 +647,7 @@ sub new { my $self = {}; bless $self, $class; - push( - @{ $$self{states} }, - 'off', 'low', 'med', 'high', 'on', '10%', '20%', - '30%', '40%', '50%', '60%', '70%', '80%', '90%' - ); + push( @{ $$self{states} }, 'off', 'low', 'med', 'high', 'on', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%' ); $$self{master_object} = $object; $devid = $devid . $zway_suffix if ( $devid =~ m/^\d+$/ ); @@ -797,9 +686,7 @@ sub set { else { $n_state .= "$p_state%"; } - main::print_log( - "[raZberry_dimmer] Setting value to $n_state. Level is " - . $self->{level} ) + main::print_log( "[raZberry_dimmer] Setting value to $n_state. Level is " . $self->{level} ) if ( $self->{debug} ); $self->SUPER::set($n_state); @@ -822,8 +709,7 @@ sub set { $$self{master_object}->set_dev( $$self{devid}, "level=$n_state" ); } else { - main::print_log( - "[raZberry_dimmer] Error. Unknown set state $p_state"); + main::print_log("[raZberry_dimmer] Error. Unknown set state $p_state"); } } } @@ -885,9 +771,7 @@ sub set { $self->{level} = 0; } - main::print_log( - "[raZberry_switch] Setting value to $p_state. Level is " - . $self->{level} ) + main::print_log( "[raZberry_switch] Setting value to $p_state. Level is " . $self->{level} ) if ( $self->{debug} ); $self->SUPER::set($p_state); } @@ -896,8 +780,7 @@ sub set { $$self{master_object}->set_dev( $$self{devid}, $p_state ); } else { - main::print_log( - "[raZberry_switch] Error. Unknown set state $p_state"); + main::print_log("[raZberry_switch] Error. Unknown set state $p_state"); } } } @@ -964,11 +847,7 @@ sub new { $self->{digital} = 1 if ( ( defined $options ) and ( $options =~ m/digital/i ) ); if ( $self->{digital} ) { - push( - @{ $$self{states} }, - 'down', '10%', '20%', '30%', '40%', '50%', - '60%', '70%', '80%', '90%', 'up' - ); + push( @{ $$self{states} }, 'down', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'up' ); } else { push( @{ $$self{states} }, 'down', 'stop', 'up' ); @@ -1016,8 +895,7 @@ sub set { } # stop level? - main::print_log( "[raZberry_blind] Setting value to $n_state. Level is " - . $self->{level} ) + main::print_log( "[raZberry_blind] Setting value to $n_state. Level is " . $self->{level} ) if ( $self->{debug} ); $self->SUPER::set($n_state); } @@ -1031,12 +909,10 @@ sub set { } elsif ( ( $p_state eq "100%" ) or ( $p_state =~ m/^\d{1,2}\%$/ ) ) { my ($n_state) = ( $p_state =~ /(\d+)%/ ); - $$self{master_object} - ->set_dev( $$self{devid}, "level=$n_state" ); + $$self{master_object}->set_dev( $$self{devid}, "level=$n_state" ); } else { - main::print_log( - "[raZberry_blind] Error. Unknown set state $p_state"); + main::print_log("[raZberry_blind] Error. Unknown set state $p_state"); } } elsif (( lc $p_state eq "up" ) @@ -1046,8 +922,7 @@ sub set { $$self{master_object}->set_dev( $$self{devid}, $p_state ); } else { - main::print_log( - "[raZberry_blind] Error. Unknown set state $p_state"); + main::print_log("[raZberry_blind] Error. Unknown set state $p_state"); } } } @@ -1073,9 +948,7 @@ sub isfailed { sub update_data { my ( $self, $data ) = @_; if ( defined $data->{battery_level} ) { - &main::print_log( "[raZberry_blind] Setting battery value to " - . $data->{battery_level} - . "." ) + &main::print_log( "[raZberry_blind] Setting battery value to " . $data->{battery_level} . "." ) if ( $self->{debug} ); $self->{battery_level} = $data->{battery_level}; } @@ -1084,20 +957,15 @@ sub update_data { sub battery_check { my ($self) = @_; unless ( $self->{battery} ) { - main::print_log( - "[raZberry_blind] ERROR, battery option not defined on this object" - ); + main::print_log( "[raZberry_blind] ERROR, battery option not defined on this object" ); return; } if ( $self->{battery_level} eq "" ) { - main::print_log( - "[raZberry_blind] INFO Battery level currently undefined"); + main::print_log("[raZberry_blind] INFO Battery level currently undefined"); return; } - main::print_log( "[raZberry_blind] INFO Battery currently at " - . $self->{battery_level} - . "%" ); + 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"); @@ -1110,8 +978,7 @@ sub battery_check { sub _battery_timer { my ($self) = @_; - $self->{battery_timer}->set( $self->{battery_poll_seconds}, - sub { &raZberry_blind::battery_check($self) }, -1 ); + $self->{battery_timer}->set( $self->{battery_poll_seconds}, sub { &raZberry_blind::battery_check($self) }, -1 ); } sub battery_level { @@ -1171,10 +1038,7 @@ sub set { $map_states{unlocked} = "open"; if ( $p_setby eq 'poll' ) { - main::print_log( "[raZberry_lock] Setting value to $p_state: " - . $map_states{$p_state} - . ". Battery Level is " - . $self->{battery_level} ) + main::print_log( "[raZberry_lock] Setting value to $p_state: " . $map_states{$p_state} . ". Battery Level is " . $self->{battery_level} ) if ( $self->{debug} ); if ( ( $p_state eq "open" ) or ( $p_state eq "close" ) ) { $self->SUPER::set( $map_states{$p_state} ); @@ -1183,19 +1047,16 @@ sub set { $self->{level} = $p_state; } else { - main::print_log( - "[raZberry_lock] Unknown value $p_state in poll set"); + main::print_log("[raZberry_lock] Unknown value $p_state in poll set"); } } else { if ( ( lc $p_state eq "locked" ) or ( lc $p_state eq "unlocked" ) ) { - $$self{master_object} - ->set_dev( $$self{devid}, $map_states{$p_state} ); + $$self{master_object}->set_dev( $$self{devid}, $map_states{$p_state} ); } else { - main::print_log( "[raZberry_lock] Error. Unknown set state " - . $map_states{$p_state} ); + main::print_log( "[raZberry_lock] Error. Unknown set state " . $map_states{$p_state} ); } } } @@ -1227,9 +1088,7 @@ sub isfailed { sub update_data { my ( $self, $data ) = @_; if ( defined $data->{battery_level} ) { - &main::print_log( "[raZberry_lock] Setting battery value to " - . $data->{battery_level} - . "." ) + &main::print_log( "[raZberry_lock] Setting battery value to " . $data->{battery_level} . "." ) if ( $self->{debug} ); $self->{battery_level} = $data->{battery_level}; } @@ -1238,13 +1097,10 @@ sub update_data { sub battery_check { my ($self) = @_; if ( $self->{battery_level} eq "" ) { - &main::print_log( - "[raZberry_lock] INFO Battery level currently undefined"); + &main::print_log("[raZberry_lock] INFO Battery level currently undefined"); return; } - &main::print_log( "[raZberry_lock] INFO Battery currently at " - . $self->{battery_level} - . "%" ); + &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"); @@ -1261,8 +1117,7 @@ sub enable_user { $status = $self->_control_user( $userid, $code, "1" ); #delay for the lock to process the code and then read in the users - main::eval_with_timer( sub { &raZberry_lock::_update_users($self) }, - $self->{user_data_delay} ); + main::eval_with_timer( sub { &raZberry_lock::_update_users($self) }, $self->{user_data_delay} ); return ($status); } @@ -1275,8 +1130,7 @@ sub disable_user { $status = $self->_control_user( $userid, $code, "0" ); #delay for the lock to process the code and then read in the users - main::eval_with_timer( sub { &raZberry_lock::_update_users($self) }, - $self->{user_data_delay} ); + main::eval_with_timer( sub { &raZberry_lock::_update_users($self) }, $self->{user_data_delay} ); return ($status); } @@ -1303,8 +1157,7 @@ sub print_users { sub _battery_timer { my ($self) = @_; - $self->{battery_timer}->set( $self->{battery_poll_seconds}, - sub { &raZberry_lock::battery_check($self) }, -1 ); + $self->{battery_timer}->set( $self->{battery_poll_seconds}, sub { &raZberry_lock::battery_check($self) }, -1 ); } sub _control_user { @@ -1314,20 +1167,13 @@ sub _control_user { my $cmd; my ( $devid, $instance, $class ) = ( split /-/, $self->{devid} )[ 0, 1, 2 ]; - $cmd = "%5B" - . $devid - . "%5D.UserCode.Set(" - . $userid . "," - . $code . "," - . $control . ")"; + $cmd = "%5B" . $devid . "%5D.UserCode.Set(" . $userid . "," . $code . "," . $control . ")"; &main::print_log("[raZberry]: Enabling usercodes $userid ($devid)...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); - my ( $isSuccessResponse0, $status ) = - &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); + my ( $isSuccessResponse0, $status ) = &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( "[raZberry]: Error: Problem retrieving data from " - . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -1343,11 +1189,9 @@ sub _update_users { &main::print_log("[raZberry]: Getting local usercodes ($devid)...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); - my ( $isSuccessResponse0, $status ) = - &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); + my ( $isSuccessResponse0, $status ) = &raZberry::_get_JSON_data( $self->{master_object}, 'usercode', $cmd ); unless ($isSuccessResponse0) { - &main::print_log( "[raZberry]: Error: Problem retrieving data from " - . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -1355,12 +1199,9 @@ sub _update_users { &main::print_log("[raZberry]: Downloading local usercodes from $devid...") if ( $self->{debug} ); &main::print_log("cmd=$cmd") if ( $self->{debug} > 1 ); - my ( $isSuccessResponse1, $response ) = - &raZberry::_get_JSON_data( $self->{master_object}, 'usercode_data', - $cmd ); + my ( $isSuccessResponse1, $response ) = &raZberry::_get_JSON_data( $self->{master_object}, 'usercode_data', $cmd ); unless ($isSuccessResponse1) { - &main::print_log( "[raZberry]: Error: Problem retrieving data from " - . $self->{host} ); + &main::print_log( "[raZberry]: Error: Problem retrieving data from " . $self->{host} ); $self->{data}->{retry}++; return ('0'); } @@ -1368,8 +1209,7 @@ sub _update_users { # print Dumper $response if ( $self->{debug} > 1 ); foreach my $key ( keys %{$response} ) { if ( $key =~ m/^[0-9]*$/ ) { #a number, so a user code - $self->{users}->{"$key"}->{status} = - $response->{"$key"}->{status}->{value}; + $self->{users}->{"$key"}->{status} = $response->{"$key"}->{status}->{value}; } } @@ -1416,22 +1256,14 @@ sub new { my $self = {}; bless $self, $class; if ( ( defined $deg ) and ( lc $deg eq "f" ) ) { - push( - @{ $$self{states} }, - 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, - 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 - ); + push( @{ $$self{states} }, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 ); $self->{units} = "F"; $self->{min_temp} = 58; $self->{max_temp} = 80; } 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, 16, 27, 28, 29, 30 ); $self->{units} = "C"; $self->{min_temp} = 10; $self->{max_temp} = 30; @@ -1461,10 +1293,7 @@ sub set { if ( ( $p_state < $self->{min_temp} ) or ( $p_state > $self->{max_temp} ) ) { - main::pring_log( - "[raZberry]: WARNING not setting level to $p_state since out of bounds " - . $self->{min_temp} . ":" - . $self->{max_temp} ); + main::pring_log( "[raZberry]: WARNING not setting level to $p_state since out of bounds " . $self->{min_temp} . ":" . $self->{max_temp} ); } else { $$self{master_object}->set_dev( $$self{devid}, "level=$p_state" ); @@ -1502,19 +1331,11 @@ sub update_data { #if units is F then rescale states if ( $data->{units} =~ m/F/ ) { - @{ $$self{states} } = ( - 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, - 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 - ); + @{ $$self{states} } = ( 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 ); } $self->{min_temp} = $data->{temp_min}; $self->{max_temp} = $data->{temp_max}; - main::print_log( "In set, units = " - . $data->{units} - . " max = " - . $data->{temp_max} - . " min = " - . $data->{temp_min} ) + main::print_log( "In set, units = " . $data->{units} . " max = " . $data->{temp_max} . " min = " . $data->{temp_min} ) if ( $self->{debug} ); } @@ -1643,15 +1464,12 @@ sub set { else { $n_state = "closed"; } - main::print_log( - "[raZberry]: Setting openclose value to $n_state. Level is " - . $self->{level} ) + main::print_log( "[raZberry]: Setting openclose 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 openclose"); + main::print_log("[raZberry]: ERROR Can not set state $p_state for openclose"); } } @@ -1713,13 +1531,10 @@ sub update_data { sub battery_check { my ($self) = @_; if ( $self->{battery_level} eq "" ) { - main::print_log( - "[raZberry_battery] INFO Battery level currently undefined"); + main::print_log("[raZberry_battery] INFO Battery level currently undefined"); return; } - main::print_log( "[raZberry_battery] INFO Battery currently at " - . $self->{battery_level} - . "%" ); + 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"); diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index a3e04e19a..523b9d3f8 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -15,8 +15,7 @@ #print_log "Using read_table_A.pl"; -my ( %groups, %objects, %packages, %addresses, %scene_build_controllers, - %scene_build_responders ); +my ( %groups, %objects, %packages, %addresses, %scene_build_controllers, %scene_build_responders ); sub read_table_init_A { @@ -40,10 +39,8 @@ sub read_table_A { $record =~ s/\s*#.*$//; my ( - $code, $address, $name, $object, - $grouplist, $comparison, $limit, @other, - $other, $vcommand, $occupancy, $network, - $password, $interface, $additional_code + $code, $address, $name, $object, $grouplist, $comparison, $limit, @other, + $other, $vcommand, $occupancy, $network, $password, $interface, $additional_code ); my (@item_info) = split( ',\s*', $record ); my $type = uc shift @item_info; @@ -69,58 +66,57 @@ sub read_table_A { require Clipsal_CBus; require Clipsal_CBus::CGate; ( $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Clipsal_CBus::CGate('Clipsal_CBus_Cgate',$other)"; } elsif ( $type eq "CBUS_GROUP" ) { require Clipsal_CBus::Group; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Clipsal_CBus::Group('$address','$name',$other)"; } elsif ( $type eq "CBUS_TRIGGER" ) { require Clipsal_CBus::TriggerGroup; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Clipsal_CBus::TriggerGroup('$address','$name',$other)"; } elsif ( $type eq "CBUS_UNIT" ) { require Clipsal_CBus::Unit; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Clipsal_CBus::Unit('$address','$name',$other)"; } # -[ UPB ]---------------------------------------------------------- elsif ( $type eq "UPBPIM" ) { require 'UPBPIM.pm'; - ( $name, $network, $password, $address, $grouplist, @other ) = - @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + ( $name, $network, $password, $address, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "UPBPIM('UPBPIM', $network,$password,$address)"; } elsif ( $type eq "UPBD" ) { require 'UPB_Device.pm'; ( $name, $object, $network, $address, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "UPB_Device(\$$object, $network,$address)"; } elsif ( $type eq "UPBL" ) { require 'UPB_Link.pm'; ( $name, $object, $network, $address, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "UPB_Link(\$$object, $network,$address)"; } elsif ( $type eq "UPBRAIN8" ) { require 'UPB_Rain8.pm'; ( $name, $object, $network, $address, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "UPB_Rain8(\$$object, $network,$address)"; } elsif ( $type eq "UPBT" ) { require 'UPB_Thermostat.pm'; ( $name, $object, $network, $address, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "UPB_Thermostat(\$$object, $network,$address)"; } @@ -128,237 +124,165 @@ sub read_table_A { elsif ( $type eq "INSTEON_PLM" ) { require Insteon_PLM; ( $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon_PLM('Insteon_PLM',$other)"; } elsif ( $type eq "INSTEON_LAMPLINC" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, [ 'insteon_address', 'name' ], \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'insteon_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::LampLinc(\'$address\',$other)"; } } elsif ( $type eq "INSTEON_BULBLINC" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, [ 'insteon_address', 'name' ], \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'insteon_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::BulbLinc(\'$address\',$other)"; } } elsif ( $type eq "INSTEON_APPLIANCELINC" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, [ 'insteon_address', 'name' ], \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'insteon_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::ApplianceLinc(\'$address\',$other)"; } } elsif ( $type eq "INSTEON_SWITCHLINC" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, [ 'insteon_address', 'name' ], \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'insteon_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::SwitchLinc(\'$address\',$other)"; } } elsif ( $type eq "INSTEON_SWITCHLINCRELAY" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, [ 'insteon_address', 'name' ], \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'insteon_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::SwitchLincRelay(\'$address\',$other)"; } } elsif ( $type eq "INSTEON_KEYPADLINC" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, - [ - qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, - 'name' - ], - \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::KeyPadLinc(\'$address\', $other)"; } } elsif ( $type eq "INSTEON_KEYPADLINCRELAY" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, - [ - qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, - 'name' - ], - \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::KeyPadLincRelay(\'$address\', $other)"; } } elsif ( $type eq "INSTEON_REMOTELINC" ) { require Insteon::Controller; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::RemoteLinc(\'$address\', $other)"; } elsif ( $type eq "INSTEON_MOTIONSENSOR" ) { require Insteon::Security; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::MotionSensor(\'$address\', $other)"; } elsif ( $type eq "INSTEON_TRIGGERLINC" ) { require Insteon::Security; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::TriggerLinc(\'$address\', $other)"; } elsif ( $type eq "INSTEON_IOLINC" ) { require Insteon::IOLinc; - if ( - validate_def( - $type, 2, [ 'insteon_address', 'name' ], \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'insteon_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::IOLinc(\'$address\', $other)"; } } - elsif($type eq "INSTEON_EZIO8SA") { + elsif ( $type eq "INSTEON_EZIO8SA" ) { require Insteon::EZIO8SA; - if ( - validate_def( - $type, 2, - [ - qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, - 'name' - ], - \@item_info - ) - ) - { - ($address, $name, $grouplist, @other) = @item_info; - $other = join ', ', (map {"'$_'"} @other); # Quote data + if ( validate_def( $type, 2, [ qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, 'name' ], \@item_info ) ) { + ( $address, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::EZIO8SA(\'$address\', $other)"; } } - elsif($type eq "INSTEON_EZIO8SA_RELAY") { + elsif ( $type eq "INSTEON_EZIO8SA_RELAY" ) { require Insteon::EZIO8SA; - ($address, $object, $name, $grouplist, @other) = @item_info; - $other = join ', ', (map {"'$_'"} @other); # Quote data + ( $address, $object, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::EZIO8SA_relay($address, $object, $other)"; } - elsif($type eq "INSTEON_EZIO8SA_INPUT") { + elsif ( $type eq "INSTEON_EZIO8SA_INPUT" ) { require Insteon::Lighting; - if ( - validate_def( - $type, 2, - [ - qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, - 'name' - ], - \@item_info - ) - ) - { - ($address, $name, $grouplist, @other) = @item_info; - $other = join ', ', (map {"'$_'"} @other); # Quote data + if ( validate_def( $type, 2, [ qr/^[[:xdigit:]]{2}\.[[:xdigit:]]{2}\.[[:xdigit:]]{2}:[[:xdigit:]]{2}$/, 'name' ], \@item_info ) ) { + ( $address, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::SwitchLincRelay(\'$address\',$other)"; } } elsif ( $type eq "INSTEON_FANLINC" ) { require Insteon::Lighting; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::FanLinc(\'$address\', $other)"; } elsif ( $type eq "INSTEON_ICONTROLLER" ) { require Insteon::BaseInsteon; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data my ( $deviceid, $groupid ) = $address =~ /(\S+):(\S+)/; if ($groupid) { - $object = - "Insteon::InterfaceController(\'00.00.00:$groupid\', $other)"; + $object = "Insteon::InterfaceController(\'00.00.00:$groupid\', $other)"; } else { - $object = - "Insteon::InterfaceController(\'00.00.00:$address\', $other)"; + $object = "Insteon::InterfaceController(\'00.00.00:$address\', $other)"; } } elsif ( $type eq 'IPLT' or $type eq 'INSTEON_THERMOSTAT' ) { require Insteon::Thermostat; ( $address, $name, $grouplist, $object, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::Thermostat(\'$address\', $other)"; } elsif ( $type eq "INSTEON_IRRIGATION" ) { require Insteon::Irrigation; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::Irrigation(\'$address\', $other)"; } elsif ( $type eq "INSTEON_SYNCHROLINC" ) { require Insteon::Energy; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::SynchroLinc(\'$address\', $other)"; } elsif ( $type eq "INSTEON_IMETER" ) { require Insteon::Energy; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::iMeter(\'$address\', $other)"; } elsif ( $type eq "INSTEON_MICROSWITCH" ) { require Insteon::Lighting; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::MicroSwitch(\'$address\', $other)"; } elsif ( $type eq "INSTEON_MICROSWITCHRELAY" ) { require Insteon::Lighting; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "Insteon::MicroSwitchRelay(\'$address\', $other)"; } @@ -366,46 +290,42 @@ sub read_table_A { elsif ( $type eq 'FROG' ) { require 'FroggyRita.pm'; ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "FroggyRita('$address', $other)"; } elsif ( $type eq "X10A" ) { - if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) - { + if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "X10_Appliance('$address', $other)"; } } elsif ( $type eq "X10I" ) { - if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) - { + if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "X10_Item('$address', $other)"; } } elsif ( $type eq "X10TR" ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "X10_Transmitter('$address', $other)"; } elsif ( $type eq "X10O" ) { ( $address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "X10_Ote('$address', $other)"; } elsif ( $type eq "X10SL" ) { - if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) - { + if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); $object = "X10_Switchlinc('$address', $other)"; } } elsif ( $type eq "X10AL" ) { - if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) - { + if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); $object = "X10_Appliancelinc('$address', $other)"; @@ -417,8 +337,7 @@ sub read_table_A { $object = "X10_Keypadlinc('$address', $other)"; } elsif ( $type eq "X10LL" ) { - if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) - { + if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); $object = "X10_Lamplinc('$address', $other)"; @@ -450,8 +369,7 @@ sub read_table_A { $object = "RCS_Item('$address', $other)"; } elsif ( $type eq "X10MS" ) { - if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) - { + if ( validate_def( $type, 2, [ 'x10_address', 'name' ], \@item_info ) ) { ( $address, $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "X10_Sensor('$address', '$name', $other)"; @@ -464,8 +382,7 @@ sub read_table_A { } elsif ( $type eq "COMPOOL" ) { ( $address, $name, $grouplist ) = @item_info; - ( $address, $comparison, $limit ) = - $address =~ /\s*(\w+)\s*(\<|\>|\=)*\s*(\d*)/; + ( $address, $comparison, $limit ) = $address =~ /\s*(\w+)\s*(\<|\>|\=)*\s*(\d*)/; $object = "Compool_Item('$address', '$comparison', '$limit')" if $comparison ne undef; $object = "Compool_Item('$address')" if $comparison eq undef; @@ -536,18 +453,19 @@ sub read_table_A { ( $name, $grouplist, @other ) = @item_info; $object = "Occupancy_Monitor( $other)"; } - elsif($type eq "DSC") { + elsif ( $type eq "DSC" ) { require 'dsc.pm'; - ($name, $grouplist, @other) = @item_info; + ( $name, $grouplist, @other ) = @item_info; + # $grouplist translates to $type in the new object definition call $object = "DSC('$name', '$grouplist')"; } - elsif($type eq "DSC_PARTITION") { - ($name, $object, $address, $other, $grouplist, @other) = @item_info; + elsif ( $type eq "DSC_PARTITION" ) { + ( $name, $object, $address, $other, $grouplist, @other ) = @item_info; $object = "DSC::Partition(\$$object, '$address')"; } - elsif($type eq "DSC_ZONE") { - ($name, $object, $address, $other, $grouplist, @other) = @item_info; + elsif ( $type eq "DSC_ZONE" ) { + ( $name, $object, $address, $other, $grouplist, @other ) = @item_info; $object = "DSC::Zone(\$$object, '$address', '$other')"; } elsif ( $type eq "MUSICA" ) { @@ -680,12 +598,11 @@ sub read_table_A { if ( !( $vcommand =~ /.*\[.*/ ) ) { $vcommand .= " [ON,OFF]"; } - $code .= sprintf "\nmy \$v_%s_state;\n", $name; - $code .= sprintf "\$v_%s = new Voice_Cmd(\"%s\");\n", $name, $vcommand; - $code .= sprintf "if (\$v_%s_state = said \$v_%s) {\n", $name, $name; - $code .= sprintf " set \$%s \$v_%s_state;\n", $name, $name; - $code .= sprintf " respond \"Turning %s \$v_%s_state\";\n", - $fixedname, $name; + $code .= sprintf "\nmy \$v_%s_state;\n", $name; + $code .= sprintf "\$v_%s = new Voice_Cmd(\"%s\");\n", $name, $vcommand; + $code .= sprintf "if (\$v_%s_state = said \$v_%s) {\n", $name, $name; + $code .= sprintf " set \$%s \$v_%s_state;\n", $name, $name; + $code .= sprintf " respond \"Turning %s \$v_%s_state\";\n", $fixedname, $name; $code .= sprintf "}\n"; return $code; } @@ -729,7 +646,7 @@ sub read_table_A { else { $object = "Sensor_Zone('$address')"; } - if ( !$packages{caddx}++ ) { # first time for this object type? + if ( !$packages{caddx}++ ) { # first time for this object type? $code .= "use caddx;\n"; } } @@ -751,14 +668,11 @@ sub read_table_A { ( $address, $name, $grouplist, $serial, $pa_type, @other ) = @item_info; $pa_type = 'wdio' unless $pa_type; - if ( !$packages{PAobj}++ ) { # first time for this object type? - $code .= - "my (%pa_weeder_max_port,%pa_zone_types,%pa_zone_type_by_zone);\n"; + if ( !$packages{PAobj}++ ) { # first time for this object type? + $code .= "my (%pa_weeder_max_port,%pa_zone_types,%pa_zone_type_by_zone);\n"; } - $code .= - sprintf "\n\$%-35s = new PAobj_zone('%s','%s','%s','%s','%s');\n", - "pa_$name", $address, $name, $grouplist, $serial, $pa_type; + $code .= sprintf "\n\$%-35s = new PAobj_zone('%s','%s','%s','%s','%s');\n", "pa_$name", $address, $name, $grouplist, $serial, $pa_type; $name = "pa_$name"; $grouplist = "|$grouplist|allspeakers"; @@ -772,64 +686,47 @@ sub read_table_A { # AHB / ALB or DBH / DBL $address =~ s/^(\S)(\S)$/$1H$2/; # if $pa_type eq 'wdio'; $address = "D$address" if $pa_type eq 'wdio_old'; - $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n", - $name . '_obj', $address, $serial; + $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n", $name . '_obj', $address, $serial; $address =~ s/^(\S{1,2})H(\S)$/$1L$2/; - $code .= sprintf "\$%-35s -> add ('%s','off');\n", $name . '_obj', - $address; + $code .= sprintf "\$%-35s -> add ('%s','off');\n", $name . '_obj', $address; $object = ''; } elsif ( lc $pa_type eq 'object' ) { if ( $name =~ /^pa_pa_/i ) { - print - "\nObject name \"$name\" starts with \"pa_\". This will cause conflicts. Ignoring entry"; + print "\nObject name \"$name\" starts with \"pa_\". This will cause conflicts. Ignoring entry"; } else { - $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n", - $name, $address; - $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", - $name, $address; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n", $name, $address; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, $address; } } elsif ( lc $pa_type eq 'audrey' ) { require 'Audrey_Play.pm'; - $code .= sprintf "\$%-35s = new Audrey_Play('%s');\n", - $name . '_obj', $address; + $code .= sprintf "\$%-35s = new Audrey_Play('%s');\n", $name . '_obj', $address; $code .= sprintf "\$%-35s -> hidden(1);\n", $name . '_obj'; } elsif ( lc $pa_type eq 'x10' ) { $other = join ', ', ( map { "'$_'" } @other ); # Quote data - $code .= sprintf "\$%-35s = new X10_Appliance('%s','%s');\n", - $name . '_obj', $address, $serial; + $code .= sprintf "\$%-35s = new X10_Appliance('%s','%s');\n", $name . '_obj', $address, $serial; $code .= sprintf "\$%-35s -> hidden(1);\n", $name . '_obj'; - $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n", - $name, $name . '_obj'; - $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, - $name . '_obj'; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'off','off');\n", $name, $name . '_obj'; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, $name . '_obj'; } elsif ( lc $pa_type eq 'xap' ) { - $code .= sprintf "\$%-35s = new xAP_Item('%s');\n", $name . '_obj', - $address; - $code .= sprintf "\$%-35s -> hidden(1);\n", $name . '_obj'; - $code .= sprintf "\$%-35s -> target_address('%s');\n", - $name . '_obj', $address; - $code .= sprintf "\$%-35s -> class_name('%s');\n", $name . '_obj', - $serial; - $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, - $name . '_obj'; + $code .= sprintf "\$%-35s = new xAP_Item('%s');\n", $name . '_obj', $address; + $code .= sprintf "\$%-35s -> hidden(1);\n", $name . '_obj'; + $code .= sprintf "\$%-35s -> target_address('%s');\n", $name . '_obj', $address; + $code .= sprintf "\$%-35s -> class_name('%s');\n", $name . '_obj', $serial; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, $name . '_obj'; } elsif ( lc $pa_type eq 'xpl' ) { - $code .= sprintf "\$%-35s = new xPL_Item('%s');\n", $name . '_obj', - $address; - $code .= sprintf "\$%-35s -> hidden(1);\n", $name . '_obj'; - $code .= sprintf "\$%-35s -> target_address('%s');\n", - $name . '_obj', $address; - $code .= sprintf "\$%-35s -> class_name('%s');\n", $name . '_obj', - $serial; - $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, - $name . '_obj'; + $code .= sprintf "\$%-35s = new xPL_Item('%s');\n", $name . '_obj', $address; + $code .= sprintf "\$%-35s -> hidden(1);\n", $name . '_obj'; + $code .= sprintf "\$%-35s -> target_address('%s');\n", $name . '_obj', $address; + $code .= sprintf "\$%-35s -> class_name('%s');\n", $name . '_obj', $serial; + $code .= sprintf "\$%-35s -> tie_items(\$%s,'on','on');\n", $name, $name . '_obj'; } elsif ( lc $pa_type eq 'aviosys' ) { my $aviosysref = { @@ -854,10 +751,8 @@ sub read_table_A { '8' => ']' } }; - $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n", - $name . '_obj', $aviosysref->{'on'}{$address}, $serial; - $code .= sprintf "\$%-35s -> add ('%s','off');\n", $name . '_obj', - $aviosysref->{'off'}{$address}; + $code .= sprintf "\$%-35s = new Serial_Item('%s','on','%s');\n", $name . '_obj', $aviosysref->{'on'}{$address}, $serial; + $code .= sprintf "\$%-35s -> add ('%s','off');\n", $name . '_obj', $aviosysref->{'off'}{$address}; } elsif ( lc $pa_type eq 'amixer' ) { @@ -878,11 +773,10 @@ sub read_table_A { my $monitor_name; ( $address, $name, $monitor_name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - if ( !$packages{ZoneMinder_xAP}++ ) { # first time for this object type? + if ( !$packages{ZoneMinder_xAP}++ ) { # first time for this object type? $code .= "use ZoneMinder_xAP;\n"; } - $code .= sprintf "\n\$%-35s = new ZM_ZoneItem('%s');\n", $name, - $address; + $code .= sprintf "\n\$%-35s = new ZM_ZoneItem('%s');\n", $name, $address; if ( $objects{$monitor_name} ) { $code .= sprintf "\$%-35s -> add(\$%s);\n", $monitor_name, $name; } @@ -897,17 +791,16 @@ sub read_table_A { else { $object = "ZM_MonitorItem('$address')"; } - if ( !$packages{ZoneMinder_xAP}++ ) { # first time for this object type? + if ( !$packages{ZoneMinder_xAP}++ ) { # first time for this object type? $code .= "use ZoneMinder_xAP;\n"; } } elsif ( $type eq "ANALOG_SENSOR" ) { - my $xc_name; #xap conduit + my $xc_name; #xap conduit my $sensor_type; #ANALOG_SENSOR, xap source, object name, xap conduit name, groups, xap sensor type, tokens... - if ( !$packages{AnalogSensor_Item}++ ) - { # first time for this object type? + if ( !$packages{AnalogSensor_Item}++ ) { # first time for this object type? $code .= "use AnalogSensor_Item;\n"; } $address = shift @item_info; @@ -917,19 +810,16 @@ sub read_table_A { $xc_name = $address; ( $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - $code .= sprintf "\n\$%-35s = new AnalogSensor_Item(\$%s, %s);\n", - $name, $xc_name, $other; + $code .= sprintf "\n\$%-35s = new AnalogSensor_Item(\$%s, %s);\n", $name, $xc_name, $other; } else { ( $name, $xc_name, $grouplist, $sensor_type, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ( lc $name eq 'auto' ) { #new $name = $xc_name . "_" . $sensor_type . "_" . $address; - $name =~ s/\./_/g; #strip out all the periods from xap names + $name =~ s/\./_/g; #strip out all the periods from xap names } - $code .= - sprintf "\n\$%-35s = new AnalogSensor_Item('%s', '%s', %s);\n", - $name, $address, $sensor_type, $other; + $code .= sprintf "\n\$%-35s = new AnalogSensor_Item('%s', '%s', %s);\n", $name, $address, $sensor_type, $other; if ( $objects{$xc_name} ) { $code .= sprintf "\$%-35s -> add(\$%s);\n", $xc_name, $name; } @@ -937,12 +827,11 @@ sub read_table_A { $object = ''; } elsif ( $type eq "ANALOG_SENSOR_R" ) { - my $xc_name; #xap conduit + my $xc_name; #xap conduit my $sensor_type; #ANALOG_SENSOR_R, xap source, object name, xap conduit name, groups, xap sensor type, tokens... - if ( !$packages{AnalogSensor_Item}++ ) - { # first time for this object type? + if ( !$packages{AnalogSensor_Item}++ ) { # first time for this object type? $code .= "use AnalogSensor_Item;\n"; } $address = shift @item_info; @@ -952,21 +841,16 @@ sub read_table_A { $xc_name = $address; ( $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - $code .= - sprintf "\n\$%-35s = new AnalogRangeSensor_Item(\$%s, %s);\n", - $name, $xc_name, $other; + $code .= sprintf "\n\$%-35s = new AnalogRangeSensor_Item(\$%s, %s);\n", $name, $xc_name, $other; } else { ( $name, $xc_name, $grouplist, $sensor_type, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ( lc $name eq 'auto' ) { #new $name = $xc_name . "_" . $sensor_type . "_" . $address; - $name =~ s/\./_/g; #strip out all the periods from xap names + $name =~ s/\./_/g; #strip out all the periods from xap names } - $code .= - sprintf - "\n\$%-35s = new AnalogRangeSensor_Item('%s', '%s', %s);\n", - $name, $address, $sensor_type, $other; + $code .= sprintf "\n\$%-35s = new AnalogRangeSensor_Item('%s', '%s', %s);\n", $name, $address, $sensor_type, $other; if ( $objects{$xc_name} ) { $code .= sprintf "\$%-35s -> add(\$%s);\n", $xc_name, $name; } @@ -979,8 +863,7 @@ sub read_table_A { #ANALOG_SENSOR, xap source, object name, xap conduit name, groups, xap sensor type, tokens... ( $sensor_name, $name, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - if ( !$packages{AnalogSensor_Item}++ ) - { # first time for this object type? + if ( !$packages{AnalogSensor_Item}++ ) { # first time for this object type? $code .= "use AnalogSensor_Item;\n"; } @@ -1003,7 +886,7 @@ sub read_table_A { else { $object = "OneWire_xAP('$address')"; } - if ( !$packages{OneWire_xAP}++ ) { # first time for this object type? + if ( !$packages{OneWire_xAP}++ ) { # first time for this object type? $code .= "use OneWire_xAP;\n"; } } @@ -1013,7 +896,7 @@ sub read_table_A { #SDX, xap instance, object name, psixc server name ( $address, $name, $server, $grouplist, @other ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - if ( !$packages{SysDiag_xAP}++ ) { # first time for this object type? + if ( !$packages{SysDiag_xAP}++ ) { # first time for this object type? $code .= "use SysDiag_xAP;\n"; } if ($other) { @@ -1032,7 +915,7 @@ sub read_table_A { else { $object = "BSC_Item('$address')"; } - if ( !$packages{BSC}++ ) { # first time for this object type? + if ( !$packages{BSC}++ ) { # first time for this object type? $code .= "use BSC;\n"; } } @@ -1045,7 +928,7 @@ sub read_table_A { else { $object = "xPL_Sensor('$address')"; } - if ( !$packages{xPL_Items}++ ) { # first time for this object type? + if ( !$packages{xPL_Items}++ ) { # first time for this object type? $code .= "use xPL_Items;\n"; } } @@ -1058,7 +941,7 @@ sub read_table_A { else { $object = "xPL_UPS('$address')"; } - if ( !$packages{xPL_Items}++ ) { # first time for this object type? + if ( !$packages{xPL_Items}++ ) { # first time for this object type? $code .= "use xPL_Items;\n"; } } @@ -1071,7 +954,7 @@ sub read_table_A { else { $object = "xPL_X10Security('$address')"; } - if ( !$packages{xPL_Items}++ ) { # first time for this object type? + if ( !$packages{xPL_Items}++ ) { # first time for this object type? $code .= "use xPL_Items;\n"; } } @@ -1084,7 +967,7 @@ sub read_table_A { else { $object = "xPL_X10Basic('$address')"; } - if ( !$packages{xPL_X10Basic}++ ) { # first time for this objecttype? + if ( !$packages{xPL_X10Basic}++ ) { # first time for this objecttype? $code .= "use xPL_X10Basic;\n"; } } @@ -1097,7 +980,7 @@ sub read_table_A { else { $object = "xPL_IrrigationGateway('$address')"; } - if ( !$packages{xPL_Irrigation}++ ) { # first time for this object type? + if ( !$packages{xPL_Irrigation}++ ) { # first time for this object type? $code .= "use xPL_Irrigation;\n"; } } @@ -1110,7 +993,7 @@ sub read_table_A { else { $object = "xPL_IrrigationValve('$address',\$$object)"; } - if ( !$packages{xPL_Irrigation}++ ) { # first time for this object type? + if ( !$packages{xPL_Irrigation}++ ) { # first time for this object type? $code .= "use xPL_Irrigation;\n"; } } @@ -1123,7 +1006,7 @@ sub read_table_A { else { $object = "xPL_IrrigationQueue('$address',\$$object)"; } - if ( !$packages{xPL_Irrigation}++ ) { # first time for this object type? + if ( !$packages{xPL_Irrigation}++ ) { # first time for this object type? $code .= "use xPL_Irrigation;\n"; } } @@ -1136,7 +1019,7 @@ sub read_table_A { else { $object = "xPL_LightGateway('$address')"; } - if ( !$packages{xPL_Lighting}++ ) { # first time for this object type? + if ( !$packages{xPL_Lighting}++ ) { # first time for this object type? $code .= "use xPL_Lighting;\n"; } } @@ -1149,7 +1032,7 @@ sub read_table_A { else { $object = "xPL_Light('$address',\$$object)"; } - if ( !$packages{xPL_Lighting}++ ) { # first time for this object type? + if ( !$packages{xPL_Lighting}++ ) { # first time for this object type? $code .= "use xPL_Lighting;\n"; } } @@ -1162,7 +1045,7 @@ sub read_table_A { else { $object = "xPL_PlugwiseGateway('$address')"; } - if ( !$packages{xPL_Plugwise}++ ) { # first time for this object type? + if ( !$packages{xPL_Plugwise}++ ) { # first time for this object type? $code .= "use xPL_Plugwise;\n"; } } @@ -1175,7 +1058,7 @@ sub read_table_A { else { $object = "xPL_Plugwise('$address',\$$object)"; } - if ( !$packages{xPL_Plugwise}++ ) { # first time for this object type? + if ( !$packages{xPL_Plugwise}++ ) { # first time for this object type? $code .= "use xPL_Plugwise;\n"; } } @@ -1188,7 +1071,7 @@ sub read_table_A { else { $object = "xPL_Squeezebox('$address')"; } - if ( !$packages{xPL_Squeezebox}++ ) { # first time for this object type? + if ( !$packages{xPL_Squeezebox}++ ) { # first time for this object type? $code .= "use xPL_Squeezebox;\n"; } } @@ -1201,7 +1084,7 @@ sub read_table_A { else { $object = "xPL_SecurityGateway('$address')"; } - if ( !$packages{xPL_Security}++ ) { # first time for this object type? + if ( !$packages{xPL_Security}++ ) { # first time for this object type? $code .= "use xPL_Security;\n"; } } @@ -1214,7 +1097,7 @@ sub read_table_A { else { $object = "xPL_Zone('$address',\$$object)"; } - if ( !$packages{xPL_Security}++ ) { # first time for this object type? + if ( !$packages{xPL_Security}++ ) { # first time for this object type? $code .= "use xPL_Security;\n"; } } @@ -1227,7 +1110,7 @@ sub read_table_A { else { $object = "xPL_Area('$address',\$$object)"; } - if ( !$packages{xPL_Security}++ ) { # first time for this object type? + if ( !$packages{xPL_Security}++ ) { # first time for this object type? $code .= "use xPL_Security;\n"; } } @@ -1240,7 +1123,7 @@ sub read_table_A { else { $object = "X10SL_Scene('$address')"; } - if ( !$packages{X10_Scene}++ ) { # first time for this object type? + if ( !$packages{X10_Scene}++ ) { # first time for this object type? $code .= "use X10_Scene;\n"; } } @@ -1253,7 +1136,7 @@ sub read_table_A { else { $object = "Scene()"; } - if ( !$packages{Scene}++ ) { # first time for this object type? + if ( !$packages{Scene}++ ) { # first time for this object type? $code .= "use Scene;\n"; } } @@ -1261,18 +1144,16 @@ sub read_table_A { my ( $scene_name, $on_level, $ramp_rate ); ( $name, $scene_name, $on_level, $ramp_rate ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - if ( !$packages{X10_Scene}++ ) { # first time for this object type? + if ( !$packages{X10_Scene}++ ) { # first time for this object type? $code .= "use X10_Scene;\n"; } if ( ( $objects{$scene_name} ) and ( $objects{$name} ) ) { if ($on_level) { if ($ramp_rate) { - $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", - $scene_name, $name, $on_level, $ramp_rate; + $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", $scene_name, $name, $on_level, $ramp_rate; } else { - $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", - $scene_name, $name, $on_level; + $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", $scene_name, $name, $on_level; } } else { @@ -1283,42 +1164,30 @@ sub read_table_A { } elsif ( $type eq "SCENE_MEMBER" ) { my ( $scene_name, $on_level, $ramp_rate ); - if ( - validate_def( - $type, 2, - [ 'name', 'name', 'insteon_on_level', 'insteon_ramp_rate' ], - \@item_info - ) - ) - { + if ( validate_def( $type, 2, [ 'name', 'name', 'insteon_on_level', 'insteon_ramp_rate' ], \@item_info ) ) { ( $name, $scene_name, $on_level, $ramp_rate ) = @item_info; $other = join ', ', ( map { "'$_'" } @other ); # Quote data - if ( !$packages{Scene}++ ) { # first time for this object type? + if ( !$packages{Scene}++ ) { # first time for this object type? $code .= "use Scene;\n"; } if ( ( $objects{$scene_name} ) and ( $objects{$name} ) ) { if ($on_level) { if ($ramp_rate) { - $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", - $scene_name, $name, $on_level, $ramp_rate; + $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", $scene_name, $name, $on_level, $ramp_rate; } else { - $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", - $scene_name, $name, $on_level; + $code .= sprintf "\$%-35s -> add(\$%s,'%s');\n", $scene_name, $name, $on_level; } } else { - $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_name, - $name; + $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_name, $name; } } else { - print - "\nThere is no object called $scene_name defined. Ignoring SCENE_MEMBER entry.\n" + print "\nThere is no object called $scene_name defined. Ignoring SCENE_MEMBER entry.\n" unless $objects{$scene_name}; - print - "\nThere is no object called $name defined. Ignoring SCENE_MEMBER entry.\n" + print "\nThere is no object called $name defined. Ignoring SCENE_MEMBER entry.\n" unless $objects{$name}; } $object = ''; @@ -1327,25 +1196,10 @@ sub read_table_A { elsif ( $type eq "SCENE_BUILD" ) { #SCENE_BUILD, scene_name, scene_member, is_controller?, is_responder?, onlevel, ramprate - my ( $scene_member, $is_scene_controller, $is_scene_responder, - $on_level, $ramp_rate ); - if ( - validate_def( - $type, 2, - [ - 'name', 'name', - 'boolean', 'boolean', - 'insteon_on_level', 'insteon_ramp_rate' - ], - \@item_info - ) - ) - { + my ( $scene_member, $is_scene_controller, $is_scene_responder, $on_level, $ramp_rate ); + if ( validate_def( $type, 2, [ 'name', 'name', 'boolean', 'boolean', 'insteon_on_level', 'insteon_ramp_rate' ], \@item_info ) ) { - ( - $name, $scene_member, $is_scene_controller, - $is_scene_responder, $on_level, $ramp_rate - ) = @item_info; + ( $name, $scene_member, $is_scene_controller, $is_scene_responder, $on_level, $ramp_rate ) = @item_info; if ( !$packages{Scene}++ ) { # first time for this object type? $code .= "use Scene;\n"; @@ -1354,8 +1208,7 @@ sub read_table_A { $scene_build_controllers{$name}{$scene_member} = "1"; } if ($is_scene_responder) { - $scene_build_responders{$name}{$scene_member} = - "$on_level,$ramp_rate"; + $scene_build_responders{$name}{$scene_member} = "$on_level,$ramp_rate"; } $object = ''; } @@ -1369,7 +1222,7 @@ sub read_table_A { else { $object = "Philips_Hue('$address')"; } - if ( !$packages{Philips_Hue}++ ) { # first time for this object type? + if ( !$packages{Philips_Hue}++ ) { # first time for this object type? $code .= "use Philips_Hue;\n"; } } @@ -1382,116 +1235,123 @@ sub read_table_A { else { $object = "Philips_Lux('$address')"; } - if ( !$packages{Philips_Hue}++ ) { # first time for this object type? + if ( !$packages{Philips_Hue}++ ) { # first time for this object type? $code .= "use Philips_Hue;\n"; } } + #-------------- RaZberry Objects ----------------- elsif ( $type eq "RAZBERRY_CONTROLLER" ) { - ($address, $name, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my $poll; + ( $address, $name, $grouplist, $poll, @other ) = @item_info; + $other = join ',', ( map { "$_" } @other ); # Quote data if ($other) { - $object = "raZberry('$address','$other')"; + $object = "raZberry('$address','$poll',$other)"; + } + elsif ($poll) { + $object = "raZberry('$address',$poll)"; } else { - $object = "raZberry('$address')"; - } + $object = "raZberry('$address')"; + } $code .= "use raZberry;\n"; - } + } elsif ( $type eq "RAZBERRY_COMM" ) { - my ($controller); - ($name, $controller, $grouplist ) = @item_info; - $object = "raZberry_comm(\$" . $controller . ")"; + my ($controller); + ( $name, $controller, $grouplist ) = @item_info; + $object = "raZberry_comm(\$" . $controller . ")"; - } + } elsif ( $type eq "RAZBERRY_DIMMER" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_dimmer(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_dimmer(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_dimmer(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_dimmer(\$" . $controller . ",'$devid')"; + } + } elsif ( $type eq "RAZBERRY_SWITCH" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_switch(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_switch(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_switch(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_switch(\$" . $controller . ",'$devid')"; + } + } elsif ( $type eq "RAZBERRY_BLIND" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, $other ) = @item_info; - #$other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, $other ) = @item_info; + + #$other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_blind(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_blind(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_blind(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_blind(\$" . $controller . ",'$devid')"; + } + } elsif ( $type eq "RAZBERRY_LOCK" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_lock(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_lock(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_lock(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_lock(\$" . $controller . ",'$devid')"; + } + } elsif ( $type eq "RAZBERRY_THERMOSTAT" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_thermostat(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_thermostat(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_thermostat(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_thermostat(\$" . $controller . ",'$devid')"; + } + } elsif ( $type eq "RAZBERRY_TEMP_SENSOR" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_temp_sensor(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_temp_sensor(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_temp_sensor(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_temp_sensor(\$" . $controller . ",'$devid')"; + } + } elsif ( $type eq "RAZBERRY_BINARY_SENSOR" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if ($other) { + $object = "raZberry_binary_sensor(\$" . $controller . ",'$devid','$other')"; + } + else { + $object = "raZberry_binary_sensor(\$" . $controller . ",'$devid')"; + } + } + elsif ( $type eq "RAZBERRY_BATTERY" ) { + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data if ($other) { - $object = "raZberry_binary_sensor(\$" . $controller . ",'$devid','$other')"; + $object = "raZberry_battery(\$" . $controller . ",'$devid','$other')"; } else { - $object = "raZberry_binary_sensor(\$" . $controller . ",'$devid')"; - } - } - elsif ( $type eq "RAZBERRY_BATTERY" ) { - my ($devid, $controller); - ($devid, $name, $grouplist, $controller, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data - if ($other) { - $object = "raZberry_battery(\$" . $controller . ",'$devid','$other')"; - } - else { - $object = "raZberry_battery(\$" . $controller . ",'$devid')"; - } - } + $object = "raZberry_battery(\$" . $controller . ",'$devid')"; + } + } + #-------------- End of RaZberry Objects ----------------- # -[ MySensors ]------------------------------------------------------ @@ -1499,119 +1359,112 @@ sub read_table_A { require 'MySensors.pm'; my ( $gw_type, $long_name, $port ); ( $name, $long_name, $gw_type, $port, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Interface('$gw_type', '$port', '$long_name', $other)"; } elsif ( $type eq "MYS_NODE" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Node($address, '$long_name', $parent, $other)"; } elsif ( $type eq "MYS_BINARY" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Binary($address, '$long_name', $parent, $other)"; } elsif ( $type eq "MYS_DOOR" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Door($address, '$long_name', $parent, $other)"; } elsif ( $type eq "MYS_MOTION" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Motion($address, '$long_name', $parent, $other)"; } elsif ( $type eq "MYS_TEMPERATURE" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Temperature($address, '$long_name', $parent, $other)"; } elsif ( $type eq "MYS_HUMIDITY" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data - $object = "MySensors::Humidity($address, '$long_name', $parent, $other)"; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "MySensors::Humidity($address, '$long_name', $parent, $other)"; } elsif ( $type eq "MYS_MULTIMETER" ) { require 'MySensors.pm'; my ( $parent, $long_name ); ( $address, $name, $long_name, $parent, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "MySensors::Multimeter($address, '$long_name', $parent, $other)"; } - + #-------------- AD2 Objects ----------------- elsif ( $type eq "AD2_INTERFACE" ) { require AD2; my ($instance); ( $name, $instance, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "AD2('$instance','$other')"; } elsif ( $type eq "AD2_DOOR_ITEM" ) { require AD2; my ( $instance, $expander, $relay, $wireless, $zone, $partition ); - ( $name, $instance, $zone, $partition, $address, $grouplist, @other ) = - @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + ( $name, $instance, $zone, $partition, $address, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data my ( $map, $address ) = split( '=', $address ); $expander = $address if ( uc($map) eq "EXP" ); $relay = $address if ( uc($map) eq "REL" ); $wireless = $address if ( uc($map) eq "RFX" ); - $object = - "AD2_Item('door','$instance','$zone','$partition','$expander','$relay','$wireless','$other')"; + $object = "AD2_Item('door','$instance','$zone','$partition','$expander','$relay','$wireless','$other')"; } elsif ( $type eq "AD2_MOTION_ITEM" ) { require AD2; my ( $instance, $expander, $relay, $wireless, $zone, $partition ); - ( $name, $instance, $zone, $partition, $address, $grouplist, @other ) = - @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + ( $name, $instance, $zone, $partition, $address, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data my ( $map, $address ) = split( '=', $address ); $expander = $address if ( uc($map) eq "EXP" ); $relay = $address if ( uc($map) eq "REL" ); $wireless = $address if ( uc($map) eq "RFX" ); - $object = - "AD2_Item('motion','$instance','$zone','$partition','$expander','$relay','$wireless','$other')"; + $object = "AD2_Item('motion','$instance','$zone','$partition','$expander','$relay','$wireless','$other')"; } elsif ( $type eq "AD2_GENERIC_ITEM" ) { require AD2; my ( $instance, $expander, $relay, $wireless, $zone, $partition ); - ( $name, $instance, $zone, $partition, $address, $grouplist, @other ) = - @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + ( $name, $instance, $zone, $partition, $address, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data my ( $map, $address ) = split( '=', $address ); $expander = $address if ( uc($map) eq "EXP" ); $relay = $address if ( uc($map) eq "REL" ); $wireless = $address if ( uc($map) eq "RFX" ); - $object = - "AD2_Item('','$instance','$zone','$partition','$expander','$relay','$wireless','$other')"; + $object = "AD2_Item('','$instance','$zone','$partition','$expander','$relay','$wireless','$other')"; } elsif ( $type eq "AD2_PARTITION" ) { require AD2; my ( $instance, $number ); - ( $name, $instance, $number, $address, $grouplist, @other ) = - @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + ( $name, $instance, $number, $address, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "AD2_Partition('$instance','$number','$address','$other')"; } elsif ( $type eq "AD2_OUTPUT" ) { require AD2; my ( $instance, $output ); ( $name, $instance, $output, $grouplist, @other ) = @item_info; - $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $other = join ', ', ( map { "'$_'" } @other ); # Quote data $object = "AD2_Output('$instance','$output','$other')"; } @@ -1619,18 +1472,17 @@ sub read_table_A { elsif ( $type =~ /PLCBUS_.*/ ) { require PLCBUS; ( $address, $name, $grouplist, @other ) = @item_info; - ( $object, $grouplist, $additional_code ) = - PLCBUS->generate_code( $type, @item_info ); - } - elsif ($type eq "WINK"){ - ($address, $name, $grouplist, @other) = @item_info; - $other = join ', ', (map {"'$_'"} @other); # Quote data - $object = "Wink('$address',$other)"; - if( ! $packages{Wink}++ ) { # first time for this object type? - $code .= "use Wink;\n"; - &::MainLoop_pre_add_hook( \&Wink::GetDevicesAndStatus, 1 ); - } - } + ( $object, $grouplist, $additional_code ) = PLCBUS->generate_code( $type, @item_info ); + } + elsif ( $type eq "WINK" ) { + ( $address, $name, $grouplist, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + $object = "Wink('$address',$other)"; + if ( !$packages{Wink}++ ) { # first time for this object type? + $code .= "use Wink;\n"; + &::MainLoop_pre_add_hook( \&Wink::GetDevicesAndStatus, 1 ); + } + } else { print "\nUnrecognized .mht entry: $record\n"; return; @@ -1653,8 +1505,7 @@ sub read_table_A { } if ( $name eq $group ) { - &::print_log( - "mht object and group name are the same: $name Bad idea!"); + &::print_log("mht object and group name are the same: $name Bad idea!"); } else { # Allow for floorplan data: Bedroom(5,15)|Lights @@ -1697,15 +1548,11 @@ sub read_table_finish_A { #Loop through the controller hash if ( exists $scene_build_controllers{$scene} ) { - foreach my $scene_controller ( - keys %{ $scene_build_controllers{$scene} } ) - { + foreach my $scene_controller ( keys %{ $scene_build_controllers{$scene} } ) { if ( $objects{$scene_controller} ) { #Make a link to each responder in the responder hash - while ( my ( $scene_responder, $responder_data ) = - each( %{ $scene_build_responders{$scene} } ) ) - { + while ( my ( $scene_responder, $responder_data ) = each( %{ $scene_build_responders{$scene} } ) ) { my ( $on_level, $ramp_rate ) = split( ',', $responder_data ); @@ -1714,11 +1561,7 @@ sub read_table_finish_A { { if ($on_level) { if ($ramp_rate) { - $code .= - sprintf - "\$%-35s -> add(\$%s,'%s','%s');\n", - $scene_controller, $scene_responder, - $on_level, $ramp_rate; + $code .= sprintf "\$%-35s -> add(\$%s,'%s','%s');\n", $scene_controller, $scene_responder, $on_level, $ramp_rate; } else { $code .= @@ -1728,23 +1571,19 @@ sub read_table_finish_A { } } else { - $code .= sprintf "\$%-35s -> add(\$%s);\n", - $scene_controller, $scene_responder; + $code .= sprintf "\$%-35s -> add(\$%s);\n", $scene_controller, $scene_responder; } } } } else { - ::print_log( "[Read_Table_A] ERROR: There is no object " - . "called $scene_controller defined. Ignoring SCENE_BUILD entry." - ); + ::print_log( "[Read_Table_A] ERROR: There is no object " . "called $scene_controller defined. Ignoring SCENE_BUILD entry." ); } } } else { - ::print_log( "[Read_Table_A] ERROR: There is no controller " - . "defined for $scene. Ignoring SCENE_BUILD entry." ); + ::print_log( "[Read_Table_A] ERROR: There is no controller " . "defined for $scene. Ignoring SCENE_BUILD entry." ); } } return $code; @@ -1762,9 +1601,8 @@ sub read_table_finish_A { # #Global validation routine: sub validate_def { my ( $type, $req_count, $req_array, $passed_values ) = @_; - my $paramNum = 0; - my $paramCount = - scalar @{$passed_values}; # Number of parameters passed on the item + my $paramNum = 0; + my $paramCount = scalar @{$passed_values}; # Number of parameters passed on the item foreach my $param_type ( @{$req_array} ) { if ( defined $$passed_values[$paramNum] @@ -1775,14 +1613,12 @@ sub validate_def { ::print_log( "[Read_Table_A] ERROR: $_[0]: $$passed_values[0], failed to match $param_type: Found \"$$passed_values[$paramNum]\", Definition skipped." ); - ::print_log( "[Read_table-A] $_[0], " - . join( ', ', @$passed_values ) ); + ::print_log( "[Read_table-A] $_[0], " . join( ', ', @$passed_values ) ); return 0; } } elsif ( $param_type eq 'boolean' ) { - ::print_log( - "Error item $paramNum in definition, should be 0 or 1") + ::print_log("Error item $paramNum in definition, should be 0 or 1") unless ( $$passed_values[$paramNum] =~ /^(0|1)$/ ); } elsif ( $param_type eq 'name' ) { @@ -1790,65 +1626,49 @@ sub validate_def { ::print_log( "[Read_Table_A] ERROR: $_[0]: $$passed_values[0], can only use characters A-z and _ Found \"$$passed_values[$paramNum]\", Definition skipped." ); - ::print_log( "[Read_table-A] $_[0], " - . join( ', ', @$passed_values ) ); + ::print_log( "[Read_table-A] $_[0], " . join( ', ', @$passed_values ) ); return 0; } } elsif ( $param_type eq 'insteon_on_level' ) { - ::print_log( - "[Read_Table_A] WARNING: $_[0]: $$passed_values[0] On level should be 0-100%, got \"$$passed_values[$paramNum]\" " - ) + ::print_log( "[Read_Table_A] WARNING: $_[0]: $$passed_values[0] On level should be 0-100%, got \"$$passed_values[$paramNum]\" " ) unless ( $$passed_values[$paramNum] =~ m/^(\d+)%?$/ && $1 <= 100 && $1 >= 0 ); } elsif ( $param_type eq 'insteon_ramp_rate' ) { - ::print_log( - "[Read_Table_A] WARNING: $_[0]: $$passed_values[0] Ramp rate should be 0-540 seconds, got \"$$passed_values[$paramNum]\" " - ) + ::print_log( "[Read_Table_A] WARNING: $_[0]: $$passed_values[0] Ramp rate should be 0-540 seconds, got \"$$passed_values[$paramNum]\" " ) unless ( $$passed_values[$paramNum] =~ m/^([.0-9]+)s?$/ && $1 <= 540 && $1 >= 0 ); } elsif ( $param_type eq 'insteon_address' ) { - my ( $x1, $x2, $x3 ) = $$passed_values[$paramNum] =~ - m/^([A-F0-9]{2})\.([A-F0-9]{2})\.([A-F0-9]{2})$/i; + my ( $x1, $x2, $x3 ) = $$passed_values[$paramNum] =~ m/^([A-F0-9]{2})\.([A-F0-9]{2})\.([A-F0-9]{2})$/i; unless ( $x1 && $x2 && $x3 ) { - ::print_log( - "[Read_Table_A] ERROR: $_[0]: $$passed_values[0] Insteon Address should be xx.xx.xx, got \"$$passed_values[$paramNum]\" " - ); - ::print_log( "[Read_table-A] $_[0], " - . join( ', ', @$passed_values ) ); + ::print_log( "[Read_Table_A] ERROR: $_[0]: $$passed_values[0] Insteon Address should be xx.xx.xx, got \"$$passed_values[$paramNum]\" " ); + ::print_log( "[Read_table-A] $_[0], " . join( ', ', @$passed_values ) ); return 0; } } elsif ( $param_type eq 'x10_address' ) { - my ( $ha, $da ) = - $$passed_values[$paramNum] =~ m/^([A-P])([0-9]{1,2})$/i; + my ( $ha, $da ) = $$passed_values[$paramNum] =~ m/^([A-P])([0-9]{1,2})$/i; unless ( $ha && $da && $da < 17 ) { ::print_log( "[Read_Table_A] ERROR: $_[0]: $$passed_values[0] X10 Address should be 2-3 characters, House code A-P, Device 1-16, got \"$$passed_values[$paramNum]\", Definition skipped." ); - ::print_log( "[Read_table-A] $_[0], " - . join( ', ', @$passed_values ) ); + ::print_log( "[Read_table-A] $_[0], " . join( ', ', @$passed_values ) ); return 0; } } else { - ::print_log( - "[Read_Table_A] WARNING: Unknown validation type: $param_type" - ); + ::print_log( "[Read_Table_A] WARNING: Unknown validation type: $param_type" ); } } else { if ( $paramNum < $req_count ) { my $pc = scalar @$passed_values; - ::print_log( - "[Read_table-A] ERROR: $_[0] $req_count parameters are required in the definition, $pc parameters found: definition skipped." - ); - ::print_log( "[Read_table-A] $_[0], " - . join( ', ', @$passed_values ) ); + ::print_log( "[Read_table-A] ERROR: $_[0] $req_count parameters are required in the definition, $pc parameters found: definition skipped." ); + ::print_log( "[Read_table-A] $_[0], " . join( ', ', @$passed_values ) ); return 0; } } From db89a90cc9ff1044a7aa7ae022bba0eba0fc6c59 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 20 Feb 2017 11:18:28 -0600 Subject: [PATCH 121/209] Removed requirement for IO::Socket::Multicast --- lib/AlexaBridge.pm | 92 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 468501321..5a4893aac 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -62,7 +62,7 @@ The object can be defined in the user code or in a .mht file. In mht: - ALEX_BRIDGE, Alexa + ALEXA_BRIDGE, Alexa Or in user code: @@ -123,7 +123,7 @@ The module supports 300 devices which is the max supported by the Echo MHT examples: - ALEX_BRIDGE, Alexa + ALEXA_BRIDGE, Alexa ALEXABRIDGE_ITEM, AlexaItems, Alexa ALEXABRIDGE_ADD, AlexaItems, light1 light1, set, on, off, state # these are the defaults ALEXABRIDGE_ADD, AlexaItems, light1 # same as the line above @@ -241,9 +241,7 @@ IO::Compress::Gzip Time::HiRes Net::Address::Ethernet Storable -Socket IO::Socket::INET -IO::Socket::Multicast =over @@ -253,11 +251,7 @@ package AlexaBridge; @AlexaBridge::ISA = ('Generic_Item'); -use Carp; use IO::Socket::INET; -use Socket; -use IO::Socket::Multicast; - my ($LOCAL_IP, $LOCAL_MAC) = &DiscoverAddy unless ( (defined($::config_parms{'alexaMac'})) && (defined($::config_parms{'alexaHttpIp'})) ); $LOCAL_IP = $::config_parms{'alexaHttpIp'} if defined($::config_parms{'alexaHttpIp'}); @@ -301,8 +295,8 @@ sub open_port { || &main::print_log( "\nError: Could not start a udp alexa multicast notification sender on $notificationPort: $@\n\n" ) && return; setsockopt($ssdpNotificationSocket, - getprotobyname('ip'), - IP_MULTICAST_TTL, + getprotobyname('ip') || 0, + _constant('IP_MULTICAST_TTL'), pack 'I', 4); $::Socket_Ports{$ssdpNotificationName}{protocol} = 'udp'; $::Socket_Ports{$ssdpNotificationName}{datatype} = 'raw'; @@ -317,12 +311,18 @@ sub open_port { my $ssdpListenName = 'alexaSsdpListen'; - my $ssdpListenSocket = new IO::Socket::Multicast->new( - LocalPort => $SSDP_PORT, - Proto => 'udp', - Reuse => 1) - || &main::print_log( "\nError: Could not start a udp alexa multicast listen server on ". $SSDP_PORT .$@ ."\n\n" ) && return; - $ssdpListenSocket->mcast_add('239.255.255.250'); + my $ssdpListenSocket = new IO::Socket::INET->new( + LocalPort => $SSDP_PORT, + Proto => 'udp', + Reuse => 1) + || &main::print_log( "\nError: Could not start a udp alexa multicast listen server on ". $SSDP_PORT .$@ ."\n\n" ) && return; + + _mcast_add( $ssdpListenSocket, '239.255.255.250' ); + setsockopt($ssdpListenSocket, + getprotobyname('ip') || 0, + _constant('IP_MULTICAST_TTL'), + pack 'I', 4); + $::Socket_Ports{$ssdpListenName}{protocol} = 'udp'; $::Socket_Ports{$ssdpListenName}{datatype} = 'raw'; $::Socket_Ports{$ssdpListenName}{port} = $SSDP_PORT; @@ -355,6 +355,39 @@ sub http_ports { printf " - creating %-15s on %3s %5s %s\n", $AlexaHttpName, 'tcp', $AlexaHttpPort; } +sub _constant { + my $name = shift; + my %names = ( + IP_MULTICAST_TTL => 0, + IP_ADD_MEMBERSHIP => 1, + IP_MULTICAST_LOOP => 0, + ); + + my %constants = ( + MSWin32 => [10,12], + cygwin => [3,5], + darwin => [10,12], + linux => [33,35], + default => [33,35], + ); + + my $index = $names{$name}; + my $ref = $constants{ $^O } || $constants{default}; + return $ref->[ $index ]; +} + +sub _mcast_add { + my ( $sock, $addr ) = @_; + my $ip_mreq = inet_aton( $addr ) . INADDR_ANY; + + setsockopt( + $sock, + getprotobyname('ip') || 0, + _constant('IP_ADD_MEMBERSHIP'), + $ip_mreq + ) || warn "Unable to add IGMP membership: $!\n"; +} + sub check_for_data { my $alexa_http_sender = $AlexaGlobal->{http_sender}->{'alexa_http_sender'}; my $alexa_ssdp_listen = $AlexaGlobal->{ssdp_listen}; @@ -370,8 +403,9 @@ sub check_for_data { my $client_port = $alexa_listen->peer; $client_port =~ s/.*\://; $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}->{time} = time; + #push (@{ $AlexaGlobal->{http_client_queue} }, $client_ip_address.":".$client_port); # Put the request in queue so the response is sent in order $alexa_http_sender->start unless $alexa_http_sender->active; - $alexa_http_sender->set($alexa_data); + $alexa_http_sender->set($alexa_data); # Send data from client on our proxy port to MH http server } &_sendHttpData($alexa_listen, $alexa_http_sender); @@ -397,11 +431,31 @@ sub _sendHttpData { $client_ip_address =~ s/:.*//; my $client_port = $alexa_listen->peer; $client_port =~ s/.*\://; - delete $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port} if $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}; - $alexa_listen->set($alexa_sender_data); + $alexa_listen->set($alexa_sender_data); # Send data from the MH http server to the client on the proxy port } } + + +sub _sendHttpData_test { +my ($alexa_listen, $alexa_http_sender, $AlexaHttpName) = @_; + if ( $alexa_http_sender && ( my $alexa_sender_data = said $alexa_http_sender ) ) { + my $current_client_ip = @{ $AlexaGlobal->{http_client_queue} }[0]; + $current_client_ip =~ s/:.*//; + my $current_client_port = @{ $AlexaGlobal->{http_client_queue} }[0]; + $current_client_port =~ s/.*\://; + for my $ptr ( @{ $::Socket_Ports{$AlexaHttpName}{clients} } ) { + my ( $socka, $client_ip_address, $client_port, $data ) = @{$ptr}; + if ( ($client_ip_address eq $current_client_ip) && ($client_port eq $current_client_port)) { + &main::print_log( "[Alexa] Debug: Peer: $client_ip_address:$client_port Data OUT - $alexa_sender_data" ) if $main::Debug{'alexa'} >= 5; + delete $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port} if $AlexaGlobal->{http_client}->{$client_ip_address}->{$client_port}; + print $socka $alexa_sender_data; + splice(@{ $AlexaGlobal->{http_client_queue} }, 0, 1); # Delete served queue item (first item in the array) + } + } + } + +} sub _receiveSSDPEvent { my ( $buf, $peer ) = @_; From d55412ae3ef41e01b911e10f0e0598527b49129f Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 20 Feb 2017 11:51:59 -0600 Subject: [PATCH 122/209] Added lib/site/Net/Address/Ethernet.pm --- lib/site/Net/Address/Ethernet.pm | 428 +++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 lib/site/Net/Address/Ethernet.pm diff --git a/lib/site/Net/Address/Ethernet.pm b/lib/site/Net/Address/Ethernet.pm new file mode 100644 index 000000000..d39d8b5e1 --- /dev/null +++ b/lib/site/Net/Address/Ethernet.pm @@ -0,0 +1,428 @@ + +package Net::Address::Ethernet; + +use warnings; +use strict; + +=head1 NAME + +Net::Address::Ethernet - find hardware ethernet address + +=head1 SYNOPSIS + + use Net::Address::Ethernet qw( get_address ); + my $sAddress = get_address; + +=head1 FUNCTIONS + +The following functions will be exported to your namespace if you request :all like so: + + use Net::Address::Ethernet qw( :all ); + +=over + +=cut + +use Carp; +use Data::Dumper; # for debugging only +use Exporter; +use Net::Domain; +use Net::Ifconfig::Wrapper qw( Ifconfig ); +use Regexp::Common; +use Sys::Hostname; + +use constant DEBUG_MATCH => 0; + +use vars qw( $DEBUG $VERSION @EXPORT_OK %EXPORT_TAGS ); +use base 'Exporter'; + +$VERSION = 1.124; + +$DEBUG = 0 || $ENV{N_A_E_DEBUG}; + +%EXPORT_TAGS = ( 'all' => [ qw( get_address get_addresses canonical is_address ), ], ); +@EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); + +my @ahInfo; + +=item get_address + +Returns the 6-byte ethernet address in canonical form. +For example, '1A:2B:3C:4D:5E:6F'. + +When called in array context, returns a 6-element list representing +the 6 bytes of the address in decimal. For example, +(26,43,60,77,94,111). + +If any non-zero argument is given, +debugging information will be printed to STDERR. + +=cut + +sub get_address + { + if (0) + { + # If you know the name of the adapter, you can use this code to + # get its IP address: + use Socket qw/PF_INET SOCK_DGRAM inet_ntoa sockaddr_in/; + if (! socket(SOCKET, PF_INET, SOCK_DGRAM, getprotobyname('ip'))) + { + warn " WWW socket() failed\n"; + goto IFCONFIG_VERSION; + } # if + # use ioctl() interface with SIOCGIFADDR. + my $ifreq = pack('a32', 'enp1s0'); + if (! ioctl(SOCKET, 0x8915, $ifreq)) + { + warn " WWW ioctl failed\n"; + goto IFCONFIG_VERSION; + } # if + # Format the IP address from the output of ioctl(). + my $s = inet_ntoa((sockaddr_in((unpack('a16 a16', $ifreq))[1]))[1]); + if (! $s) + { + warn " WWW inet_ntoa failed\n"; + goto IFCONFIG_VERSION; + } # if + use Data::Dumper; + warn Dumper($s); exit 88; # for debugging + } # if 0 + IFCONFIG_VERSION: + my @a = get_addresses(@_); + _debug(" DDD in get_address, a is ", Dumper(\@a)); + # Even if none are active, we'll return the first one: + my $sAddr = $a[0]->{sEthernet}; + # Look through the list, returning the first active one that has a + # non-loopback IP address assigned to it: + TRY_ADDR: + foreach my $rh (@a) + { + my $sName = $rh->{sAdapter}; + _debug(" DDD inspecting interface $sName...\n"); + if (! $rh->{iActive}) + { + _debug(" DDD but it is not active.\n"); + next TRY_ADDR; + } # if + _debug(" DDD it is active...\n"); + if (! exists $rh->{sIP}) + { + _debug(" DDD but it has no IP address.\n"); + next TRY_ADDR; + } # if + if (! defined $rh->{sIP}) + { + _debug(" DDD but its IP address is undefined.\n"); + next TRY_ADDR; + } # if + if ($rh->{sIP} eq '') + { + _debug(" DDD but its IP address is empty.\n"); + next TRY_ADDR; + } # if + if ($rh->{sIP} =~ /127\.0\.0\.1/) + { + _debug(" DDD but it's the loopback.\n"); + next TRY_ADDR; + } # if + if (! exists $rh->{sEthernet}) + { + _debug(" DDD but it has no ethernet address.\n"); + next TRY_ADDR; + } # if + if (! defined $rh->{sEthernet}) + { + _debug(" DDD but its ethernet address is undefined.\n"); + next TRY_ADDR; + } # if + if ($rh->{sEthernet} eq q{}) + { + _debug(" DDD but its ethernet address is empty.\n"); + next TRY_ADDR; + } # if + $sAddr = $rh->{sEthernet}; + _debug(" DDD and its ethernet address is $sAddr.\n"); + last TRY_ADDR; + } # foreach TRY_ADDR + return wantarray ? map { hex } split(/[-:]/, $sAddr) : $sAddr; + } # get_address + + +=item get_addresses + +Returns an array of hashrefs. +Each hashref describes one Ethernet adapter found in the current hardware configuration, +with the following entries filled in to the best of our ability to determine: + +=over + +=item sEthernet -- The MAC address in canonical form. + +=item rasIP -- A reference to an array of all the IP addresses on this adapter. + +=item sIP -- The "first" IP address on this adapter. + +=item sAdapter -- The name of this adapter. + +=item iActive -- Whether this adapter is active. + +=back + +For example: + + { + 'sAdapter' => 'Ethernet adapter Local Area Connection', + 'sEthernet' => '12:34:56:78:9A:BC', + 'rasIP' => ['111.222.33.44',], + 'sIP' => '111.222.33.44', + 'iActive' => 1, + }, + +If any non-zero argument is given, +debugging information will be printed to STDERR. + +=cut + +sub get_addresses + { + $DEBUG ||= shift; + # Short-circuit if this function has already been called: + if (! $DEBUG && @ahInfo) + { + goto ALL_DONE; + } # if + my $sAddr = undef; + my $rh = Ifconfig('list', '', '', ''); + if (! defined $rh || (! scalar keys %$rh)) + { + warn " EEE Ifconfig failed: $@"; + } # if + _debug(" DDD raw output from Ifconfig is ", Dumper($rh)); + # Convert their hashref to our array format: + foreach my $key (keys %$rh) + { + my %hash; + _debug(" DDD working on key $key...\n"); + my $sAdapter = $key; + if ($key =~ m!\A\{.+}\z!) + { + $sAdapter = $rh->{$key}->{descr}; + } # if + $hash{sAdapter} = $sAdapter; + my @asIP = keys %{$rh->{$key}->{inet}}; + # Thanks to Sergey Kotenko for the array idea: + $hash{rasIP} = \@asIP; + $hash{sIP} = $asIP[0]; + my $sEther = $rh->{$key}->{ether} || ''; + if ($sEther eq '') + { + $sEther = _find_mac($sAdapter, $hash{sIP}); + } # if + $hash{sEthernet} = canonical($sEther); + $hash{iActive} = 0; + if (defined $rh->{$key}->{status} && ($rh->{$key}->{status} =~ m!\A(1|UP)\z!i)) + { + $hash{iActive} = 1; + } # if + push @ahInfo, \%hash; + } # foreach + ALL_DONE: + return @ahInfo; + } # get_addresses + + +# Attempt other ways of finding the MAC Address: +sub _find_mac + { + my $sAdapter = shift || return; + my $sIP = shift || ''; + # No hope on some OSes: + return if ($^O eq 'MSWIn32'); + my @asARP = qw( /usr/sbin/arp /sbin/arp /bin/arp /usr/bin/arp ); + my $sHostname = hostname || Net::Domain::hostname || ''; + my $sHostfqdn = Net::Domain::hostfqdn || ''; + my @asHost = ($sHostname, $sHostfqdn, ''); + ARP: + foreach my $sARP (@asARP) + { + next ARP if ! -x $sARP; + HOSTNAME: + foreach my $sHost (@asHost) + { + $sHost ||= q{}; + next HOSTNAME if ($sHost eq q{}); + my $sCmd = qq{$sARP $sHost}; + # print STDERR " DDD trying ==$sCmd==\n"; + my @as = qx{$sCmd}; + LINE_OF_CMD: + while (@as) + { + my $sLine = shift @as; + DEBUG_MATCH && print STDERR " DDD output line of cmd ==$sLine==\n"; + if ($sLine =~ m!\(($RE{net}{IPv4})\)\s+AT\s+($RE{net}{MAC})\b!i) + { + # Looks like arp on Solaris. + my ($sIPFound, $sEtherFound) = ($1, $2); + # print STDERR " DDD found IP =$sIPFound=, found ether =$sEtherFound=\n"; + return $sEtherFound if ($sIPFound eq $sIP); + # print STDERR " DDD does NOT match the one I wanted =$sIP=\n"; + } # if + if ($sLine =~ m!($RE{net}{IPv4})\s+ETHER\s+($RE{net}{MAC})\b!i) + { + # Looks like arp on Solaris. + return $2 if ($1 eq $sIP); + } # if + } # while LINE_OF_CMD + } # foreach HOSTNAME + } # foreach ARP + } # _find_mac + +=item is_address + +Returns a true value if its argument looks like an ethernet address. + +=cut + +sub is_address + { + my $s = uc(shift || ''); + # Convert all non-hex digits to colon: + $s =~ s![^0-9A-F]+!:!g; + return ($s =~ m!\A$RE{net}{MAC}\Z!i); + } # is_address + + +=item canonical + +Given a 6-byte ethernet address, converts it to canonical form. +Canonical form is 2-digit uppercase hexadecimal numbers with colon +between the bytes. The address to be converted can have any kind of +punctuation between the bytes, the bytes can be 1-digit, and the bytes +can be lowercase; but the bytes must already be hex. + +=cut + +sub canonical + { + my $s = shift; + return '' if ! is_address($s); + # Convert all non-hex digits to colon: + $s =~ s![^0-9a-fA-F]+!:!g; + my @as = split(':', $s); + # Cobble together 2-digit hex bytes: + $s = ''; + map { $s .= length() < 2 ? "0$_" : $_; $s .= ':' } @as; + chop $s; + return uc $s; + } # canonical + +sub _debug + { + return if ! $DEBUG; + print STDERR @_; + } # _debug + +=back + +=head1 NOTES + +=head1 SEE ALSO + +arp, ifconfig, ipconfig + +=head1 BUGS + +Please tell the author if you find any! And please show me the output +of `arp ` +or `ifconfig` +or `ifconfig -a` +from your system. + +=head1 AUTHOR + +Martin 'Kingpin' Thurn, C, L. + +=head1 LICENSE + +This software is released under the same license as Perl itself. + +=cut + +1; + +__END__ + +=pod + +#### This is an example of @ahInfo on MSWin32: +( + { + 'sAdapter' => 'Ethernet adapter Local Area Connection', + 'sEthernet' => '00-0C-F1-EE-F0-39', + 'sIP' => '16.25.10.14', + 'iActive' => 1, + }, + { + 'sAdapter' => 'Ethernet adapter Wireless Network Connection', + 'sEthernet' => '00-33-BD-F3-33-E3', + 'sIP' => '19.16.20.12', + 'iActive' => 1, + }, + { + 'sAdapter' => '{gobbledy-gook}', + 'sDesc' => 'PPP adapter Verizon Online', + 'sEthernet' => '00-53-45-00-00-00', + 'sIP' => '71.24.23.85', + 'iActive' => 1, + }, +) + +#### This is Solaris 8: + +> /usr/sbin/arp myhost +myhost (14.81.16.10) at 03:33:ba:46:f2:ef permanent published + +#### This is Solaris 8: + +> /usr/sbin/ifconfig -a +lo0: flags=1000849 mtu 8232 index 1 + inet 127.0.0.1 netmask ff000000 +bge0: flags=1000843 mtu 1500 index 2 + inet 14.81.16.10 netmask ffffff00 broadcast 14.81.16.255 + +#### This is Fedora Core 6: + +$ /sbin/arp +Address HWtype HWaddress Flags Mask Iface +19.16.11.11 ether 03:53:53:e3:43:93 C eth0 + +#### This is amd64-freebsd: + +$ ifconfig +fwe0: flags=108802 mtu 1500 + options=8 + ether 02:31:38:31:35:35 + ch 1 dma -1 +vr0: flags=8843 mtu 1500 + inet6 fe8d::2500:bafd:fecd:cdcd%vr0 prefixlen 64 scopeid 0x2 + inet 19.16.12.52 netmask 0xffffff00 broadcast 19.16.12.255 + ether 00:53:b3:c3:3d:39 + media: Ethernet autoselect (100baseTX ) + status: active +nfe0: flags=8843 mtu 1500 + options=8 + inet6 fe8e::21e:31ef:fee1:26eb%nfe0 prefixlen 64 scopeid 0x3 + ether 00:13:33:53:23:13 + media: Ethernet autoselect (100baseTX ) + status: active +plip0: flags=108810 mtu 1500 +lo0: flags=8049 mtu 16384 + inet6 ::1 prefixlen 128 + inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5 + inet 127.0.0.1 netmask 0xff000000 + inet 127.0.0.2 netmask 0xffffffff + inet 127.0.0.3 netmask 0xffffffff +tun0: flags=8051 mtu 1492 + inet 83.173.73.3 --> 233.131.83.3 netmask 0xffffffff + Opened by PID 268 From 837b68cfded63a2e1b58c9cf4b1f0d3a93b910c0 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 20 Feb 2017 12:16:25 -0600 Subject: [PATCH 123/209] Eval check for Net::Address::Ethernet as its only used for discovering the local IP and can be manually defined in the ini. --- lib/AlexaBridge.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 5a4893aac..2e0012b25 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -906,7 +906,11 @@ sub _GetChunk { sub DiscoverAddy { - use Net::Address::Ethernet qw( :all ); + eval "use Net::Address::Ethernet qw( :all )"; + if ($@) { + print "\n [Alexa] Error: Net::Address::Ethernet is not installed... Please install it or define the local IP and mac in alexaHttpIp/alexaMac\n\n"; + return ('127.0.0.1','9aa645cc40aa'); # return localhost as we dont know the real address + } my @a = get_addresses(@_); foreach my $adapter (@a) { next unless ($adapter->{iActive} eq 1); From 5529f2b110f1541edde2b03d32d755a125d7d1f5 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 20 Feb 2017 12:17:46 -0600 Subject: [PATCH 124/209] Removed lib/site/Net/Address/Ethernet.pm --- lib/site/Net/Address/Ethernet.pm | 428 ------------------------------- 1 file changed, 428 deletions(-) delete mode 100644 lib/site/Net/Address/Ethernet.pm diff --git a/lib/site/Net/Address/Ethernet.pm b/lib/site/Net/Address/Ethernet.pm deleted file mode 100644 index d39d8b5e1..000000000 --- a/lib/site/Net/Address/Ethernet.pm +++ /dev/null @@ -1,428 +0,0 @@ - -package Net::Address::Ethernet; - -use warnings; -use strict; - -=head1 NAME - -Net::Address::Ethernet - find hardware ethernet address - -=head1 SYNOPSIS - - use Net::Address::Ethernet qw( get_address ); - my $sAddress = get_address; - -=head1 FUNCTIONS - -The following functions will be exported to your namespace if you request :all like so: - - use Net::Address::Ethernet qw( :all ); - -=over - -=cut - -use Carp; -use Data::Dumper; # for debugging only -use Exporter; -use Net::Domain; -use Net::Ifconfig::Wrapper qw( Ifconfig ); -use Regexp::Common; -use Sys::Hostname; - -use constant DEBUG_MATCH => 0; - -use vars qw( $DEBUG $VERSION @EXPORT_OK %EXPORT_TAGS ); -use base 'Exporter'; - -$VERSION = 1.124; - -$DEBUG = 0 || $ENV{N_A_E_DEBUG}; - -%EXPORT_TAGS = ( 'all' => [ qw( get_address get_addresses canonical is_address ), ], ); -@EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); - -my @ahInfo; - -=item get_address - -Returns the 6-byte ethernet address in canonical form. -For example, '1A:2B:3C:4D:5E:6F'. - -When called in array context, returns a 6-element list representing -the 6 bytes of the address in decimal. For example, -(26,43,60,77,94,111). - -If any non-zero argument is given, -debugging information will be printed to STDERR. - -=cut - -sub get_address - { - if (0) - { - # If you know the name of the adapter, you can use this code to - # get its IP address: - use Socket qw/PF_INET SOCK_DGRAM inet_ntoa sockaddr_in/; - if (! socket(SOCKET, PF_INET, SOCK_DGRAM, getprotobyname('ip'))) - { - warn " WWW socket() failed\n"; - goto IFCONFIG_VERSION; - } # if - # use ioctl() interface with SIOCGIFADDR. - my $ifreq = pack('a32', 'enp1s0'); - if (! ioctl(SOCKET, 0x8915, $ifreq)) - { - warn " WWW ioctl failed\n"; - goto IFCONFIG_VERSION; - } # if - # Format the IP address from the output of ioctl(). - my $s = inet_ntoa((sockaddr_in((unpack('a16 a16', $ifreq))[1]))[1]); - if (! $s) - { - warn " WWW inet_ntoa failed\n"; - goto IFCONFIG_VERSION; - } # if - use Data::Dumper; - warn Dumper($s); exit 88; # for debugging - } # if 0 - IFCONFIG_VERSION: - my @a = get_addresses(@_); - _debug(" DDD in get_address, a is ", Dumper(\@a)); - # Even if none are active, we'll return the first one: - my $sAddr = $a[0]->{sEthernet}; - # Look through the list, returning the first active one that has a - # non-loopback IP address assigned to it: - TRY_ADDR: - foreach my $rh (@a) - { - my $sName = $rh->{sAdapter}; - _debug(" DDD inspecting interface $sName...\n"); - if (! $rh->{iActive}) - { - _debug(" DDD but it is not active.\n"); - next TRY_ADDR; - } # if - _debug(" DDD it is active...\n"); - if (! exists $rh->{sIP}) - { - _debug(" DDD but it has no IP address.\n"); - next TRY_ADDR; - } # if - if (! defined $rh->{sIP}) - { - _debug(" DDD but its IP address is undefined.\n"); - next TRY_ADDR; - } # if - if ($rh->{sIP} eq '') - { - _debug(" DDD but its IP address is empty.\n"); - next TRY_ADDR; - } # if - if ($rh->{sIP} =~ /127\.0\.0\.1/) - { - _debug(" DDD but it's the loopback.\n"); - next TRY_ADDR; - } # if - if (! exists $rh->{sEthernet}) - { - _debug(" DDD but it has no ethernet address.\n"); - next TRY_ADDR; - } # if - if (! defined $rh->{sEthernet}) - { - _debug(" DDD but its ethernet address is undefined.\n"); - next TRY_ADDR; - } # if - if ($rh->{sEthernet} eq q{}) - { - _debug(" DDD but its ethernet address is empty.\n"); - next TRY_ADDR; - } # if - $sAddr = $rh->{sEthernet}; - _debug(" DDD and its ethernet address is $sAddr.\n"); - last TRY_ADDR; - } # foreach TRY_ADDR - return wantarray ? map { hex } split(/[-:]/, $sAddr) : $sAddr; - } # get_address - - -=item get_addresses - -Returns an array of hashrefs. -Each hashref describes one Ethernet adapter found in the current hardware configuration, -with the following entries filled in to the best of our ability to determine: - -=over - -=item sEthernet -- The MAC address in canonical form. - -=item rasIP -- A reference to an array of all the IP addresses on this adapter. - -=item sIP -- The "first" IP address on this adapter. - -=item sAdapter -- The name of this adapter. - -=item iActive -- Whether this adapter is active. - -=back - -For example: - - { - 'sAdapter' => 'Ethernet adapter Local Area Connection', - 'sEthernet' => '12:34:56:78:9A:BC', - 'rasIP' => ['111.222.33.44',], - 'sIP' => '111.222.33.44', - 'iActive' => 1, - }, - -If any non-zero argument is given, -debugging information will be printed to STDERR. - -=cut - -sub get_addresses - { - $DEBUG ||= shift; - # Short-circuit if this function has already been called: - if (! $DEBUG && @ahInfo) - { - goto ALL_DONE; - } # if - my $sAddr = undef; - my $rh = Ifconfig('list', '', '', ''); - if (! defined $rh || (! scalar keys %$rh)) - { - warn " EEE Ifconfig failed: $@"; - } # if - _debug(" DDD raw output from Ifconfig is ", Dumper($rh)); - # Convert their hashref to our array format: - foreach my $key (keys %$rh) - { - my %hash; - _debug(" DDD working on key $key...\n"); - my $sAdapter = $key; - if ($key =~ m!\A\{.+}\z!) - { - $sAdapter = $rh->{$key}->{descr}; - } # if - $hash{sAdapter} = $sAdapter; - my @asIP = keys %{$rh->{$key}->{inet}}; - # Thanks to Sergey Kotenko for the array idea: - $hash{rasIP} = \@asIP; - $hash{sIP} = $asIP[0]; - my $sEther = $rh->{$key}->{ether} || ''; - if ($sEther eq '') - { - $sEther = _find_mac($sAdapter, $hash{sIP}); - } # if - $hash{sEthernet} = canonical($sEther); - $hash{iActive} = 0; - if (defined $rh->{$key}->{status} && ($rh->{$key}->{status} =~ m!\A(1|UP)\z!i)) - { - $hash{iActive} = 1; - } # if - push @ahInfo, \%hash; - } # foreach - ALL_DONE: - return @ahInfo; - } # get_addresses - - -# Attempt other ways of finding the MAC Address: -sub _find_mac - { - my $sAdapter = shift || return; - my $sIP = shift || ''; - # No hope on some OSes: - return if ($^O eq 'MSWIn32'); - my @asARP = qw( /usr/sbin/arp /sbin/arp /bin/arp /usr/bin/arp ); - my $sHostname = hostname || Net::Domain::hostname || ''; - my $sHostfqdn = Net::Domain::hostfqdn || ''; - my @asHost = ($sHostname, $sHostfqdn, ''); - ARP: - foreach my $sARP (@asARP) - { - next ARP if ! -x $sARP; - HOSTNAME: - foreach my $sHost (@asHost) - { - $sHost ||= q{}; - next HOSTNAME if ($sHost eq q{}); - my $sCmd = qq{$sARP $sHost}; - # print STDERR " DDD trying ==$sCmd==\n"; - my @as = qx{$sCmd}; - LINE_OF_CMD: - while (@as) - { - my $sLine = shift @as; - DEBUG_MATCH && print STDERR " DDD output line of cmd ==$sLine==\n"; - if ($sLine =~ m!\(($RE{net}{IPv4})\)\s+AT\s+($RE{net}{MAC})\b!i) - { - # Looks like arp on Solaris. - my ($sIPFound, $sEtherFound) = ($1, $2); - # print STDERR " DDD found IP =$sIPFound=, found ether =$sEtherFound=\n"; - return $sEtherFound if ($sIPFound eq $sIP); - # print STDERR " DDD does NOT match the one I wanted =$sIP=\n"; - } # if - if ($sLine =~ m!($RE{net}{IPv4})\s+ETHER\s+($RE{net}{MAC})\b!i) - { - # Looks like arp on Solaris. - return $2 if ($1 eq $sIP); - } # if - } # while LINE_OF_CMD - } # foreach HOSTNAME - } # foreach ARP - } # _find_mac - -=item is_address - -Returns a true value if its argument looks like an ethernet address. - -=cut - -sub is_address - { - my $s = uc(shift || ''); - # Convert all non-hex digits to colon: - $s =~ s![^0-9A-F]+!:!g; - return ($s =~ m!\A$RE{net}{MAC}\Z!i); - } # is_address - - -=item canonical - -Given a 6-byte ethernet address, converts it to canonical form. -Canonical form is 2-digit uppercase hexadecimal numbers with colon -between the bytes. The address to be converted can have any kind of -punctuation between the bytes, the bytes can be 1-digit, and the bytes -can be lowercase; but the bytes must already be hex. - -=cut - -sub canonical - { - my $s = shift; - return '' if ! is_address($s); - # Convert all non-hex digits to colon: - $s =~ s![^0-9a-fA-F]+!:!g; - my @as = split(':', $s); - # Cobble together 2-digit hex bytes: - $s = ''; - map { $s .= length() < 2 ? "0$_" : $_; $s .= ':' } @as; - chop $s; - return uc $s; - } # canonical - -sub _debug - { - return if ! $DEBUG; - print STDERR @_; - } # _debug - -=back - -=head1 NOTES - -=head1 SEE ALSO - -arp, ifconfig, ipconfig - -=head1 BUGS - -Please tell the author if you find any! And please show me the output -of `arp ` -or `ifconfig` -or `ifconfig -a` -from your system. - -=head1 AUTHOR - -Martin 'Kingpin' Thurn, C, L. - -=head1 LICENSE - -This software is released under the same license as Perl itself. - -=cut - -1; - -__END__ - -=pod - -#### This is an example of @ahInfo on MSWin32: -( - { - 'sAdapter' => 'Ethernet adapter Local Area Connection', - 'sEthernet' => '00-0C-F1-EE-F0-39', - 'sIP' => '16.25.10.14', - 'iActive' => 1, - }, - { - 'sAdapter' => 'Ethernet adapter Wireless Network Connection', - 'sEthernet' => '00-33-BD-F3-33-E3', - 'sIP' => '19.16.20.12', - 'iActive' => 1, - }, - { - 'sAdapter' => '{gobbledy-gook}', - 'sDesc' => 'PPP adapter Verizon Online', - 'sEthernet' => '00-53-45-00-00-00', - 'sIP' => '71.24.23.85', - 'iActive' => 1, - }, -) - -#### This is Solaris 8: - -> /usr/sbin/arp myhost -myhost (14.81.16.10) at 03:33:ba:46:f2:ef permanent published - -#### This is Solaris 8: - -> /usr/sbin/ifconfig -a -lo0: flags=1000849 mtu 8232 index 1 - inet 127.0.0.1 netmask ff000000 -bge0: flags=1000843 mtu 1500 index 2 - inet 14.81.16.10 netmask ffffff00 broadcast 14.81.16.255 - -#### This is Fedora Core 6: - -$ /sbin/arp -Address HWtype HWaddress Flags Mask Iface -19.16.11.11 ether 03:53:53:e3:43:93 C eth0 - -#### This is amd64-freebsd: - -$ ifconfig -fwe0: flags=108802 mtu 1500 - options=8 - ether 02:31:38:31:35:35 - ch 1 dma -1 -vr0: flags=8843 mtu 1500 - inet6 fe8d::2500:bafd:fecd:cdcd%vr0 prefixlen 64 scopeid 0x2 - inet 19.16.12.52 netmask 0xffffff00 broadcast 19.16.12.255 - ether 00:53:b3:c3:3d:39 - media: Ethernet autoselect (100baseTX ) - status: active -nfe0: flags=8843 mtu 1500 - options=8 - inet6 fe8e::21e:31ef:fee1:26eb%nfe0 prefixlen 64 scopeid 0x3 - ether 00:13:33:53:23:13 - media: Ethernet autoselect (100baseTX ) - status: active -plip0: flags=108810 mtu 1500 -lo0: flags=8049 mtu 16384 - inet6 ::1 prefixlen 128 - inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5 - inet 127.0.0.1 netmask 0xff000000 - inet 127.0.0.2 netmask 0xffffffff - inet 127.0.0.3 netmask 0xffffffff -tun0: flags=8051 mtu 1492 - inet 83.173.73.3 --> 233.131.83.3 netmask 0xffffffff - Opened by PID 268 From 2c5bedfdf86a50a8f67cdd162112fcc0f2180c03 Mon Sep 17 00:00:00 2001 From: waynieack Date: Mon, 20 Feb 2017 12:41:09 -0600 Subject: [PATCH 125/209] Made the Net::Address::Ethernet error message log only when the alexa module is enabled. --- lib/AlexaBridge.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index 2e0012b25..ebf44ab69 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -908,7 +908,8 @@ sub _GetChunk { sub DiscoverAddy { eval "use Net::Address::Ethernet qw( :all )"; if ($@) { - print "\n [Alexa] Error: Net::Address::Ethernet is not installed... Please install it or define the local IP and mac in alexaHttpIp/alexaMac\n\n"; + print "\n [Alexa] Error: Net::Address::Ethernet is not installed... Please install it or define the local IP and mac in alexaHttpIp/alexaMac\n\n" + if ($::config_parms{'alexa_enable'}); return ('127.0.0.1','9aa645cc40aa'); # return localhost as we dont know the real address } my @a = get_addresses(@_); From 6bf7776ed0d46105d4e86dac99ed879dc7647a0f Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 20 Feb 2017 15:37:57 -0700 Subject: [PATCH 126/209] v2.0b5 - added push setby method, fixed user example --- lib/raZberry.pm | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 405b1496e..4317e49c3 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b3 +=head1 B v2.0b5 =head2 SYNOPSIS @@ -40,7 +40,7 @@ RAZBERRY_BLIND, 4, main_blinds, HVAC|zwave, razberry_controller, bat for specifying controller options; -RAZBERRY_CONTROLLER, 10.0.1.1, razberry_controller, zwave, ,'username=admin,password=bob' +RAZBERRY_CONTROLLER, 10.0.1.1, razberry_controller, zwave, ,'user=admin,password=bob' =head2 DESCRIPTION @@ -77,10 +77,10 @@ For later versions, Z_Way has introduced authentication. raZberry v2.0 supports - Edit user anonymous and allow access to room devices 2: Create a new user and give it the admin role. Then in the controller definition, provide the username and password: -$razberry_controller = new raZberry('10.0.1.1',10,"method=poll,username=user,password=pwd"); +$razberry_controller = new raZberry('10.0.1.1',10,"method=poll,user=user,password=pwd"); -=head2 v2 PUSH or POLL. Only supported in version raZberry 2.1.0+ +=head2 v2 PUSH or POLL. Only tested in version raZberry 2.3.0 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%) @@ -211,8 +211,9 @@ sub new { $self->{push} = 0; $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); $self->{username} = ""; - ( $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 ) ); + $options =~ s/username\=/user\=/i; + ( $self->{username} ) = ( $options =~ /user\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/user\=/i ) ); + ( $self->{password} ) = ( $options =~ /pass\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/password\=/i ) ); if ( ( $push_obj eq "" ) and ( $self->{push} ) ) { &main::print_log("[raZberry]: Push method selected"); &main::print_log("[raZberry]: The HTTPGet Automation module needs to be installed for push to work"); @@ -624,18 +625,22 @@ sub main::razberry_push { #my $obj = &main::get_object_by_name($object); if ( $push_obj eq "" ) { &main::print_log("[raZberry]: ERROR, Push control not enabled on this controller."); - } - else { + } + elsif ( $dev =~ m/^ZWayVDev_zway_/ ) { if ( defined $push_obj->{child_object}->{$id} ) { - &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set( ' . $level . ", 'poll' );" ); - - #$push_obj->{child_object}->{$id}->set( $level, 'poll' ); + &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set( ' . $level . ", 'push' );" ); + $push_obj->{child_object}->{$id}->set( $level, 'push' ); } else { &main::print_log("[raZberry]: ERROR, child object id $id not found!"); } + + } + else { + &main::print_log("[raZberry]: ERROR, only ZWayVDev devices supported for push"); } + } package raZberry_dimmer; @@ -665,7 +670,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { $self->{level} = $p_state; my $n_state; if ( $p_state == 100 ) { @@ -763,7 +768,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { if ( lc $p_state eq "on" ) { $self->{level} = 100; } @@ -874,7 +879,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( defined $p_setby && $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { $self->{level} = $p_state; my $n_state; if ( $p_state == 0 ) { @@ -1037,7 +1042,7 @@ sub set { $map_states{locked} = "close"; $map_states{unlocked} = "open"; - if ( $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { main::print_log( "[raZberry_lock] Setting value to $p_state: " . $map_states{$p_state} . ". Battery Level is " . $self->{battery_level} ) if ( $self->{debug} ); if ( ( $p_state eq "open" ) or ( $p_state eq "close" ) ) { @@ -1237,7 +1242,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { $self->SUPER::set($p_state); } } @@ -1285,7 +1290,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { $self->{level} = $p_state; $self->SUPER::set($p_state); } @@ -1364,7 +1369,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( $p_setby eq 'poll' ) { + if ( defined $p_setby && ( ( $p_setby eq 'poll' ) or ( $p_setby eq 'push' ) ) ) { $self->{level} = $p_state; $self->SUPER::set($p_state); @@ -1455,7 +1460,7 @@ sub new { sub set { my ( $self, $p_state, $p_setby ) = @_; - if ( $p_setby eq 'poll' ) { + 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" ) { From cfc09f243471296e7c3201f30cd2a953a30e7cd7 Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 20 Feb 2017 19:51:45 -0700 Subject: [PATCH 127/209] v2.0b6 - fixed authentication password issue --- lib/raZberry.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 4317e49c3..c6d7e3cf6 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b5 +=head1 B v2.0b6 =head2 SYNOPSIS @@ -211,9 +211,9 @@ sub new { $self->{push} = 0; $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); $self->{username} = ""; - $options =~ s/username\=/user\=/i; - ( $self->{username} ) = ( $options =~ /user\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/user\=/i ) ); - ( $self->{password} ) = ( $options =~ /pass\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/password\=/i ) ); + $options =~ s/username\=/user\=/i if ( defined $options ); + ( $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]: Push method selected"); &main::print_log("[raZberry]: The HTTPGet Automation module needs to be installed for push to work"); From 94c4862abbde9a321b47574f3bc0475968d13029 Mon Sep 17 00:00:00 2001 From: H Plato Date: Tue, 21 Feb 2017 18:22:34 -0700 Subject: [PATCH 128/209] v2.0b7 - fixed polling, roughed in voltage and generic child objects --- lib/raZberry.pm | 171 ++++++++++++++++++++++++++++++++++++++------ lib/read_table_A.pl | 22 ++++++ 2 files changed, 172 insertions(+), 21 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index c6d7e3cf6..34d3fc871 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b6 +=head1 B v2.0b7 =head2 SYNOPSIS @@ -33,6 +33,11 @@ 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_GENERIC, device_id, name, group, controller_name, options + * Note GENERIC requires the full device ID, ie 2-0-48-1 +RAZBERRY_VOLTAGE, device_id, name, group, controller_name, options + * Note VOLTAGE is a multiattribute device, so device_id can only be the major number + for example: RAZBERRY_CONTROLLER, 10.0.1.1, razberry_controller, zwave @@ -208,8 +213,10 @@ sub new { my $method = "poll"; ($method) = ( $options =~ /method=(\s+)/ ) if ( defined $options ); - $self->{push} = 0; - $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); + $self->{push} = 0; + $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); + $self->{config}->{poll_seconds} = 1800 if ( $self->{push} ); #poll the raZberry every 30 minutes if we are using the push method + $self->{username} = ""; $options =~ s/username\=/user\=/i if ( defined $options ); ( $self->{username} ) = ( $options =~ /user\=([a-zA-Z0-9]+)/i ) if ( ( defined $options ) and ( $options =~ m/user\=/i ) ); @@ -223,8 +230,6 @@ sub new { else { &main::print_log("[raZberry]: Push method already in use on other object") if ($push_obj); &main::print_log("[raZberry]: Poll method selected"); - $push_obj = \%{$self}; #TODO: remove when out of beta - $self->{push} = 1; } if ( $self->{username} ) { $self->{cookie_jar} = HTTP::Cookies->new( {} ); @@ -232,13 +237,9 @@ sub new { } $self->get_controllerdata; $self->{timer} = new Timer; - unless ( $self->{push} ) { - $self->start_timer; - } - else { - $self->poll; #get initial data - } - &main::print_log("[raZberry]: Object initialized."); + $self->poll; + $self->start_timer; + &main::print_log("[raZberry]: Controller Initialization Complete"); return $self; } @@ -304,7 +305,7 @@ sub poll { &main::print_log("[raZberry]: cmd=$cmd") if ( $self->{debug} > 1 ); for my $dev ( keys %{ $self->{data}->{force_update} } ) { - &main::print_log( "[raZberry]: Forcing update to device $dev to account for local changes" ) if ( $self->{debug} ); + &main::print_log("[raZberry]: Forcing update to device $dev to account for local changes") if ( $self->{debug} ); $self->update_dev($dev); } @@ -328,9 +329,14 @@ sub poll { 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]: Voltage Device found"); + } else { $self->{data}->{devices}->{$id}->{level} = $item->{metrics}->{level}; } @@ -486,7 +492,7 @@ sub _get_JSON_data { or ( $mode eq "usercode" ) or ( $mode eq "usercode_data" ) ); $method = "ZWaveAPI" if ( $mode eq "controller" ); - &main::print_log( "[raZberry]: contacting http://$host:$port/$method/$rest{$mode}$params" ) if ( $self->{debug} ); + &main::print_log("[raZberry]: 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"); @@ -501,7 +507,7 @@ sub _get_JSON_data { my $isSuccessResponse = $responseCode < 400; $self->{updating} = 0; if ( !$isSuccessResponse ) { - &main::print_log( "[raZberry]: Warning, failed to get data. Response code $responseCode" ); + &main::print_log("[raZberry]: Warning, failed to get data. Response code $responseCode"); if ( defined $self->{child_object}->{comm} ) { if ( $self->{status} eq "online" ) { $self->{status} = "offline"; @@ -541,7 +547,7 @@ sub _get_JSON_data { } else { - &main::print_log( "[raZberry]: Warning, not fetching data due to operation in progress" ); + &main::print_log("[raZberry]: Warning, not fetching data due to operation in progress"); return ('0'); } } @@ -605,11 +611,11 @@ sub register { if ( defined $options ) { if ( $options =~ m/force_update/ ) { $self->{data}->{force_update}->{$dev} = 1; - &main::print_log( "[raZberry]: Forcing Controller to contact Device $dev at each poll" ); + &main::print_log("[raZberry]: Forcing Controller to contact Device $dev at each poll"); } if ( $options =~ m/keep_alive/ ) { $self->{data}->{ping}->{$dev} = 1; - &main::print_log( "[raZberry]: Forcing Controller to ping Device $dev at each poll" ); + &main::print_log("[raZberry]: Forcing Controller to ping Device $dev at each poll"); } } } @@ -629,8 +635,14 @@ sub main::razberry_push { } elsif ( $dev =~ m/^ZWayVDev_zway_/ ) { if ( defined $push_obj->{child_object}->{$id} ) { - &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set( ' . $level . ", 'push' );" ); - $push_obj->{child_object}->{$id}->set( $level, 'push' ); + 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 );" ); + } + else { + &main::print_log( '[raZberry]: Calling $push_obj->{child_object}->{' . $id . '}->set( ' . $level . ", 'push' );" ); + $push_obj->{child_object}->{$id}->set( $level, 'push' ); + } } else { &main::print_log("[raZberry]: ERROR, child object id $id not found!"); @@ -962,7 +974,7 @@ sub update_data { sub battery_check { my ($self) = @_; unless ( $self->{battery} ) { - main::print_log( "[raZberry_blind] ERROR, battery option not defined on this object" ); + main::print_log("[raZberry_blind] ERROR, battery option not defined on this object"); return; } @@ -1549,3 +1561,120 @@ sub battery_check { } } +package raZberry_voltage; +@raZberry_voltage::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $devid, $options ) = @_; + + my $self = {}; + bless $self, $class; + + #ZWayVDev_zway_x-0-50-0 - Power Meter kWh + #ZWayVDev_zway_x-0-50-1 - RGB setting of the switch LED + #ZWayVDev_zway_x-0-50-2 - Power Sensor W + #ZWayVDev_zway_x-0-50-4 - Voltage Sensor V + #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+$/ ) { + $$self{master_object} = $object; + $$self{type} = "Multilevel Voltage"; + $$self{devid} = $devid; + $object->register( $self, $devid . "-0-50-0", $options ); + $object->register( $self, $devid . "-0-50-1", $options ); + $object->register( $self, $devid . "-0-50-2", $options ); + $object->register( $self, $devid . "-0-50-4", $options ); + $object->register( $self, $devid . "-0-50-5", $options ); + + #$self->set($object->get_dev_status,$devid,'poll'); + $self->{level}->{0} = ""; + $self->{debug} = $object->{debug}; + } + else { + main::print_log("[raZberry_voltage] ERROR, Voltage can only be a major dev id"); + + } + return $self; + +} + +sub level { + my ( $self, $attr ) = @_; + + $attr = 0 unless ($attr); + if ( defined $self->{level}->{$attr} ) { + return ( $self->{level} ); + } + else { + main::print_log("[raZberry_voltage] ERROR, unknown attribute $attr"); + return (0); + } +} + +sub set_level { + my ( $self, $value, $attr ) = @_; + + $attr = 0 unless ($attr); + $self->{level}->{$attr} = $value; + +} + +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 ) = @_; +} + +package raZberry_generic; +@raZberry_generic::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $devid, $options ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + $$self{type} = "Generic"; + $$self{devid} = $devid; + $object->register( $self, $devid, $options ); + + #$self->set($object->get_dev_status,$devid,'poll'); + $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 ) = @_; +} diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 523b9d3f8..1d6002b36 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1351,6 +1351,28 @@ sub read_table_A { $object = "raZberry_battery(\$" . $controller . ",'$devid')"; } } + elsif ( $type eq "RAZBERRY_GENERIC" ) { + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if ($other) { + $object = "raZberry_generic(\$" . $controller . ",'$devid','$other')"; + } + else { + $object = "raZberry_generic(\$" . $controller . ",'$devid')"; + } + } + elsif ( $type eq "RAZBERRY_VOLTAGE" ) { + my ( $devid, $controller ); + ( $devid, $name, $grouplist, $controller, @other ) = @item_info; + $other = join ', ', ( map { "'$_'" } @other ); # Quote data + if ($other) { + $object = "raZberry_voltage(\$" . $controller . ",'$devid','$other')"; + } + else { + $object = "raZberry_voltage(\$" . $controller . ",'$devid')"; + } + } #-------------- End of RaZberry Objects ----------------- From 15e5a436ac47e0433267ba51fee85b6ac7d7ef5f Mon Sep 17 00:00:00 2001 From: H Plato Date: Wed, 22 Feb 2017 12:45:48 -0700 Subject: [PATCH 129/209] v2.0b8 - added mh.ini parameters and make it simpler to specify a push method --- lib/raZberry.pm | 42 +++++++++++++++++++++++++----------------- lib/read_table_A.pl | 2 +- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 34d3fc871..3d6db04c4 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b7 +=head1 B v2.0b8 =head2 SYNOPSIS @@ -18,13 +18,13 @@ In user code: $remote_1 = new raZberry_battery($razberry_controller,12); -raZberry(,); +raZberry(,|'push'); raZberry_(,,) In items.mht: -RAZBERRY_CONTROLLER, ip_address, controller_name, group, poll time, options +RAZBERRY_CONTROLLER, ip_address, controller_name, group, poll time/'push', options RAZBERRY_DIMMER, device_id, name, group, controller_name, options RAZBERRY_SWITCH, device_id, name, group, controller_name, options RAZBERRY_BLIND, device_id, name, group, controller_name, options @@ -81,8 +81,11 @@ For later versions, Z_Way has introduced authentication. raZberry v2.0 supports - Create a user named anonymous with role anonymous - Edit user anonymous and allow access to room devices -2: Create a new user and give it the admin role. Then in the controller definition, provide the username and password: -$razberry_controller = new raZberry('10.0.1.1',10,"method=poll,user=user,password=pwd"); +2: Create a new user and give it the admin role. Credentials can be stored in MH either in the mh.private.ini, +or on a per controller basis. + +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 @@ -91,13 +94,14 @@ URL for updating: http://mh:port/SUB;razberry_push(%DEVICE%,%VALUE%) If the razberry or mh get out of sync, $controller->poll can be issued to get the latest states. -Only 1 razberry controller can be the push source, due to only a single controller object that can be linked to the web service. +Only one razberry controller can be the push source, due to only a single controller object that can be linked to the web service. =head2 MH.INI CONFIG PARAMS raZberry_timeout raZberry_poll_seconds - +raZberry_user +raZberry_password =head2 BUGS @@ -193,10 +197,18 @@ sub new { $self->{config}->{poll_seconds} = 5; $self->{config}->{poll_seconds} = $main::config_parms{raZberry_poll_seconds} if ( defined $main::config_parms{raZberry_poll_seconds} ); - $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); - $self->{updating} = 0; - $self->{data}->{retry} = 0; + $self->{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 + } + else { + $self->{config}->{poll_seconds} = $poll if ( defined $poll ); + $self->{config}->{poll_seconds} = 1 if ( $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; @@ -211,14 +223,10 @@ sub new { $self->{controller_data} = (); &main::print_log("[raZberry]: options are $options") if ( ( $self->{debug} ) and ( defined $options ) ); - my $method = "poll"; - ($method) = ( $options =~ /method=(\s+)/ ) if ( defined $options ); - $self->{push} = 0; - $self->{push} = 1 if ( ( defined $method ) and ( lc $method eq 'push' ) ); - $self->{config}->{poll_seconds} = 1800 if ( $self->{push} ); #poll the raZberry every 30 minutes if we are using the push method - $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} ) ) { diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 1d6002b36..7cee72356 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1249,7 +1249,7 @@ sub read_table_A { $object = "raZberry('$address','$poll',$other)"; } elsif ($poll) { - $object = "raZberry('$address',$poll)"; + $object = "raZberry('$address','$poll')"; } else { $object = "raZberry('$address')"; From 7e8a38b363872d2dc900116bf5215a08f8d1b577 Mon Sep 17 00:00:00 2001 From: H Plato Date: Wed, 22 Feb 2017 18:37:13 -0700 Subject: [PATCH 130/209] v2.0b9 - added in reauthentication and filter some builtin razberry objects from showing up in the push log --- lib/raZberry.pm | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 3d6db04c4..6c11575e1 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b8 +=head1 B v2.0b9 =head2 SYNOPSIS @@ -487,7 +487,6 @@ sub _get_JSON_data { 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 = ""; @@ -505,13 +504,25 @@ sub _get_JSON_data { my $request = HTTP::Request->new( GET => "http://$host:$port/$method/$rest{$mode}$params" ); $request->content_type("application/x-www-form-urlencoded"); - my $responseObj = $ua->request($request); - print $responseObj->content . "\n--------------------\n" - if ( $self->{debug} > 1 ); + #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]: ReAuthenticating..."); + $self->login; + $connect_req = 1; + } + else { + $connect_req = 2; + } + } until ( $connect_req == 2 ); - my $responseCode = $responseObj->code; - print 'Response code: ' . $responseCode . "\n" - if ( $self->{debug} > 1 ); my $isSuccessResponse = $responseCode < 400; $self->{updating} = 0; if ( !$isSuccessResponse ) { @@ -634,6 +645,10 @@ sub main::razberry_push { 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_/ ) ); + &main::print_log("[raZberry]: HTTP Push update received for device: $dev, id: $id and level: $level"); #my $obj = &main::get_object_by_name($object); From 6fd39d79c8f4a36316fdc4275f45818e44cc944a Mon Sep 17 00:00:00 2001 From: waynieack Date: Thu, 23 Feb 2017 21:31:55 -0600 Subject: [PATCH 131/209] Added option to manually define the RWR zone with nws_rwr_zone option in the mh.private.ini --- bin/get_weather | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/get_weather b/bin/get_weather index 83fda2bb1..6549707af 100755 --- a/bin/get_weather +++ b/bin/get_weather @@ -39,7 +39,7 @@ my %parms; if ( !&GetOptions( \%parms, "h", "help", "v", "city:s", "zone:s", - "state:s", "data=s", "no_log" + "state:s", "data=s", "no_log", "nws_rwr_zone:s" ) or @ARGV or ( $parms{h} or $parms{help} ) @@ -102,7 +102,7 @@ $Geo::WeatherNOAA::proxy_from_env = 1; if ( $data{conditions} ) { print "\nGetting the current weather for $parms{city}, $parms{state}\n"; $conditions = - print_current( $parms{city}, $parms{state}, undef, undef, undef, 1 ); + print_current( $parms{city}, $parms{state}, undef, undef, undef, $parms{nws_rwr_zone} ); $conditions =~ s/°F/ degrees /; $conditions =~ s/ in\./ inches. /g; } From 8566037cdb69cd1d4903cc29cdf4ab85605d5acc Mon Sep 17 00:00:00 2001 From: waynieack Date: Thu, 23 Feb 2017 21:39:25 -0600 Subject: [PATCH 132/209] Added option to manually define the RWR zone with nws_rwr_zone option in the mh.private.ini --- code/common/internet_weather.pl | 4 ++-- lib/site/Geo/WeatherNOAA.pm | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/code/common/internet_weather.pl b/code/common/internet_weather.pl index 3c8afde7b..858303352 100644 --- a/code/common/internet_weather.pl +++ b/code/common/internet_weather.pl @@ -80,7 +80,7 @@ sub normalize_conditions { if ( said $v_get_internet_weather_data) { if (&net_connect_check) { set $p_weather_data - qq|get_weather -state $config_parms{state} -city "$config_parms{city}"|; + qq|get_weather -state $config_parms{state} -city "$config_parms{city}" -nws_rwr_zone $config_parms{nws_rwr_zone}|; start $p_weather_data; $v_get_internet_weather_data->respond( "app=weather Weather data requested for $config_parms{city}, $config_parms{state}" @@ -97,7 +97,7 @@ sub normalize_conditions { if ( said $v_get_internet_weather_conditions) { if (&net_connect_check) { set $p_weather_conditions - qq|get_weather -state $config_parms{state} -city "$config_parms{city}" -data conditions|; + qq|get_weather -state $config_parms{state} -city "$config_parms{city}" -data conditions -nws_rwr_zone $config_parms{nws_rwr_zone}|; start $p_weather_conditions; $v_get_internet_weather_conditions->respond( "app=weather Weather conditions requested for $config_parms{city}, $config_parms{state}" diff --git a/lib/site/Geo/WeatherNOAA.pm b/lib/site/Geo/WeatherNOAA.pm index 57d29170f..caf4f9fb6 100644 --- a/lib/site/Geo/WeatherNOAA.pm +++ b/lib/site/Geo/WeatherNOAA.pm @@ -480,8 +480,7 @@ sub ucfirst_words { ######################################################################### sub get_city_hourly { - my ( $city, $state, $filename, $fileopt, $UA ) = @_; - + my ( $city, $state, $filename, $fileopt, $UA, $rwrzone ) = @_; # City and state in all caps please # $city = uc $city; @@ -493,11 +492,12 @@ sub get_city_hourly { # Get data # my $zone = &get_zone( $ZONE_SEARCH_URL, "$city, $state" ); + $rwrzone = $zone unless length($rwrzone); my $URL = $URL_BASE . $zone . '&issuedby=' - . $zone + . $rwrzone . '&product=RWR&format=txt&version=1&glossary=0'; #print STDERR "Getting data from $URL\n"; @@ -514,7 +514,14 @@ sub get_city_hourly { $data = get_data( $URL, $filename, $fileopt, $UA ); $datalength = length($data); } - + if ( $data =~ /None issued/ ) { + print " + NWS is not returing any information, please configure the 2 or 3 letter zone + in the mh.private.ini with the nws_rwr_zone option. IE: nws_rwr_zone=LIX + The zone can be found at the following site: + http://forecast.weather.gov/product_sites.php?site=$zone&product=RWR\n + "; + } #print STDERR "Got data ($datalength)\n"; # Return error if there's an error @@ -598,14 +605,14 @@ sub get_city_hourly { } # get_city_hourly() sub print_current { - my ( $city, $state, $filename, $fileopt, $UA ) = @_; - my $in = process_city_hourly( $city, $state, $filename, $fileopt, $UA ); + my ( $city, $state, $filename, $fileopt, $UA, $rwrzone ) = @_; + my $in = process_city_hourly( $city, $state, $filename, $fileopt, $UA, $rwrzone ); return wrap( '', ' ', $in ); } sub process_city_hourly { - my ( $city, $state, $filename, $fileopt, $UA ) = @_; - my $in = get_city_hourly( $city, $state, $filename, $fileopt, $UA ); + my ( $city, $state, $filename, $fileopt, $UA, $rwrzone ) = @_; + my $in = get_city_hourly( $city, $state, $filename, $fileopt, $UA, $rwrzone ); $state = uc($state); From b9c1351d141577e25939e5a22dcff9a8840a82af Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 24 Feb 2017 14:56:34 -0700 Subject: [PATCH 133/209] v2.0 - Voltage and Generic still roughed in, but sufficient for 4.2 release --- lib/raZberry.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 6c11575e1..723c8976c 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0b9 +=head1 B v2.0 =head2 SYNOPSIS From 419ed2ea7bd06a6715548d9195254a98283faca8 Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 2 Mar 2017 17:57:17 -0700 Subject: [PATCH 134/209] IA7 v1.3.610 - Object History now works with safari --- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 40 ++++++++++------------------------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index da784c621..f06676d43 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.3.601 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.3.610 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 1340b8b1a..26e039abb 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,4 @@ -// v1.3.601 +// v1.3.610 var entity_store = {}; //global storage of entities var json_store = {}; @@ -266,10 +266,10 @@ function changePage (){ nav_link = '#path=/objects&parents='+nav_name; if (collection_keys_arr.length > 2 && collection_keys_arr[collection_keys_arr.length-2].substring(0,1) == "$") nav_link = '#path=/objects&type='+nav_name; if (nav_name == "Group") nav_link = '#path=objects&type=Group'; //Hardcode this use case - if (json_store.objects[nav_name].label !== undefined) nav_name = (json_store.objects[nav_name].label); + if (json_store.objects[nav_name] !== undefined && json_store.objects[nav_name].label !== undefined) nav_name = (json_store.objects[nav_name].label); - } - else { + } else { + if (json_store.collections[collection_keys_arr[i]] == undefined) continue; //last breadcrumb duplicated so we don't need it. nav_link = json_store.collections[collection_keys_arr[i]].link; nav_name = json_store.collections[collection_keys_arr[i]].name; } @@ -278,8 +278,7 @@ function changePage (){ if (i == (collection_keys_arr.length-1)){ $('#nav').append('
  • ' + nav_name + '
  • '); $('title').html("MisterHouse - " + nav_name); - } - else { + } else { $('#nav').append('
  • ' + nav_name + '
  • '); } } @@ -309,7 +308,6 @@ function loadPrefs (config_name){ //show ia7 prefs, args ia7_prefs, ia7_rrd_pref html += "
    "+ config_name + "_config.json
    "+ i + "
    ", - "$info{$list}{label} Calls ($info{$list}{count} entries)
    \n"; + print "\n
    ", "$info{$list}{label} Calls ($info{$list}{count} entries)
    \n"; print &radio_group( -name => "sort_$list", -values => [ 'name', 'number', 'date' ], diff --git a/bin/find_files b/bin/find_files index 4b6d8daa0..10197e861 100755 --- a/bin/find_files +++ b/bin/find_files @@ -19,8 +19,7 @@ use strict; my ( $Pgm_Path, $Pgm_Name, $Version ); BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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; } @@ -65,19 +64,18 @@ $search = shift; @skip = split ',', $parms{skip} if $parms{skip}; @boxes = split ',', $parms{boxes} if $parms{boxes}; @shares = split ',', $parms{dirs}; -@skip = qw(^/program ^/win) unless @skip; # Skip program and windows dirs +@skip = qw(^/program ^/win) unless @skip; # Skip program and windows dirs @boxes = &find_boxes unless @boxes; @shares = &find_shares(@boxes) unless @shares; -$| = 1; # Turn on command buffering (e.g. flush on every print) +$| = 1; # Turn on command buffering (e.g. flush on every print) print "\nSearching for $search in @shares\n\n"; for my $share (@shares) { print "Traversing share $share\n"; &read_dir($share); } -print - "\nRead $counts{dir} directories and $counts{file} files.\n - Found $counts{found} files that match $search\n"; +print "\nRead $counts{dir} directories and $counts{file} files.\n - Found $counts{found} files that match $search\n"; print "\nFound files:\n ", join "\n ", @found if @found; print "\nRun took ", scalar(time) - $time_start, " seconds\n"; @@ -115,19 +113,12 @@ sub find_shares_wmi { print "Finding shares on @boxes\n"; use Win32::OLE; for my $box (@boxes) { - if ( - my $WMI = Win32::OLE->GetObject( - "WinMgmts:{impersonationLevel=impersonate}!//$box") - ) - { - for my $share ( Win32::OLE::in( $WMI->InstancesOf('Win32_Share') ) ) - { + if ( my $WMI = Win32::OLE->GetObject("WinMgmts:{impersonationLevel=impersonate}!//$box") ) { + for my $share ( Win32::OLE::in( $WMI->InstancesOf('Win32_Share') ) ) { next unless $share->{Type} == 0; # Look at shares only next if $share->{Name} =~ /\$$/; # Ignore hidden shares push @shares, "//$box/$share->{Name}"; - printf "%-8s Name: %s, Type: %s, Status: %s Path: %s\n", - $box, $share->{Name}, $share->{Type}, $share->{Status}, - $share->{Path}; + printf "%-8s Name: %s, Type: %s, Status: %s Path: %s\n", $box, $share->{Name}, $share->{Type}, $share->{Status}, $share->{Path}; } } } diff --git a/bin/find_programs b/bin/find_programs index 41cc5b013..9cfff1445 100755 --- a/bin/find_programs +++ b/bin/find_programs @@ -46,8 +46,7 @@ use strict; my ( $Pgm_Path, $Pgm_Name, $Version ); BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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; } @@ -109,20 +108,13 @@ sub wmi_processes { defwatch rtvscan stisvc vptray OSA msgsys mgabg pdesk); my $count = 0; - if ( - my $WMI = Win32::OLE->GetObject( - "WinMgmts:{impersonationLevel=impersonate}!//$box") - ) - { - for my $process ( Win32::OLE::in( $WMI->InstancesOf('Win32_Process') ) ) - { + if ( my $WMI = Win32::OLE->GetObject("WinMgmts:{impersonationLevel=impersonate}!//$box") ) { + for my $process ( Win32::OLE::in( $WMI->InstancesOf('Win32_Process') ) ) { my $name = $process->{Name}; $count++; next if !$parms{all} and grep $name =~ /$_/i, @ignore_list; next if $find and $name !~ /$find/i; - $list .= - sprintf - " PID:%5d,%5d Pgm:%-15s Threads:%3s Mem:%6.2f,%6.2f Date: %s\n", + $list .= sprintf " PID:%5d,%5d Pgm:%-15s Threads:%3s Mem:%6.2f,%6.2f Date: %s\n", $process->{ProcessID}, $process->{ParentProcessID}, $process->{Name}, $process->{ThreadCount}, $process->{WorkingSetSize} / 10**6, @@ -130,8 +122,7 @@ sub wmi_processes { } } else { - print "WMI unable to connect to \\$box:" - . Win32::OLE->LastError() . "\n"; + print "WMI unable to connect to \\$box:" . Win32::OLE->LastError() . "\n"; } return ( $count, $list ); } diff --git a/bin/get_earthquakes b/bin/get_earthquakes index 501a92008..dc382c264 100755 --- a/bin/get_earthquakes +++ b/bin/get_earthquakes @@ -29,8 +29,7 @@ BEGIN { ($Pgm_Name) = $0 =~ /([^.]+)/, $Pgm_Path = '.' unless $Pgm_Name; } -my ($Version) = - q$Revision: 398 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs +my ($Version) = q$Revision: 398 $ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs use Getopt::Long; our %parms; @@ -97,7 +96,7 @@ unless ( tie %DBM, 'DB_File', $parms{dbm}, O_RDWR | O_CREAT, 0666 ) { } if ( $parms{d} ) { - print map( {"$_ => $DBM{$_}\n"} keys(%DBM) ); + print map( { "$_ => $DBM{$_}\n" } keys(%DBM) ); untie %DBM; die "Dumpped the DBM file. Exiting!\n"; } @@ -105,8 +104,7 @@ if ( $parms{d} ) { unlink $f_cnss_merged_txt; my $getURLcmd = 'net_ftp -passive 1 -command get -server hazards.cr.usgs.gov '; $getURLcmd .= ' -user anonymous -password anonymous'; -system( $getURLcmd - . " -file $f_cnss_merged_txt -file_remote cnss/cnss_14.fing " ); +system( $getURLcmd . " -file $f_cnss_merged_txt -file_remote cnss/cnss_14.fing " ); unless ( open CNSS, $f_cnss_merged_txt ) { die "$Pgm_Name: Failed to retrieve file from USGS\n"; @@ -134,9 +132,7 @@ foreach $line (@txtFile) { if ( !exists( $DBM{$key} ) ) { #Entry is new so go ahead and calculate the distance; expensive - $event->[7] = sprintf "%d", - calc_distance( $latitude, $longitude, - $event->[1], $event->[2], $Earthquake_Units ) + .5; + $event->[7] = sprintf "%d", calc_distance( $latitude, $longitude, $event->[1], $event->[2], $Earthquake_Units ) + .5; $event->[9] = 0; } else { @@ -154,11 +150,7 @@ foreach $line (@txtFile) { if ( $event->[7] <= $distance and $event->[4] >= $Magnitude_thresholds{$distance} ) { - print( "$Pgm_Name: found magnitude " - . $event->[4] - . " quake " - . $event->[7] - . " $Earthquake_Units away\n" ) + print( "$Pgm_Name: found magnitude " . $event->[4] . " quake " . $event->[7] . " $Earthquake_Units away\n" ) if $parms{v}; print($line) if $parms{v}; $event->[8] = 1; @@ -206,13 +198,9 @@ if ( $keyNewest ne "" ) { ? ( abs( 5 * round( $dbmEvent[1] / 5 ) ) > 65 ? 20 : 10 ) : 5; my $image = - 'http://earthquake.usgs.gov/recenteqsww/Maps/10/' - . $long_reso * - round( - ( $dbmEvent[2] < 0 ? 360 + $dbmEvent[2] : $dbmEvent[2] ) / $long_reso ) - . '_' - . 5 * - round( $dbmEvent[1] / 5 ) . '.gif'; + 'http://earthquake.usgs.gov/recenteqsww/Maps/10/' + . $long_reso * round( ( $dbmEvent[2] < 0 ? 360 + $dbmEvent[2] : $dbmEvent[2] ) / $long_reso ) . '_' + . 5 * round( $dbmEvent[1] / 5 ) . '.gif'; unlink $f_earthquakes_gif; $getURLcmd = "get_url " . ( $parms{v} ? "" : "-quiet" ); system( $getURLcmd . " $image $f_earthquakes_gif" ); @@ -244,12 +232,10 @@ else { my $units = $Earthquake_Units eq 'miles' ? "miles" : "km "; print( TXT "List is filtered using the follwing:\n" ); print( TXT " Distance <= Magnitude >=\n" ); - foreach my $distThresh ( sort( { $b <=> $a } keys(%Magnitude_thresholds) ) ) - { + foreach my $distThresh ( sort( { $b <=> $a } keys(%Magnitude_thresholds) ) ) { my $magThresh = $Magnitude_thresholds{$distThresh}; - $distThresh = - $distThresh > 20038 ? " Any" : sprintf( "%5.0d", $distThresh ); - $magThresh = $magThresh == 0 ? " Any" : sprintf( "%4.1f", $magThresh ); + $distThresh = $distThresh > 20038 ? " Any" : sprintf( "%5.0d", $distThresh ); + $magThresh = $magThresh == 0 ? " Any" : sprintf( "%4.1f", $magThresh ); print( TXT " $distThresh $units $magThresh\n" ); } print( TXT "\n" ); @@ -273,20 +259,13 @@ foreach my $key ( reverse( sort(@keysSpeak) ) ) { ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = gmtime( $dbmEvent[0] ); - printf( TXT "%04d/%02d/%02d %02d:%02d:%02d ", - $year + 1900, - $mon + 1, $mday, $hour, $min, $sec - ); + printf( TXT "%04d/%02d/%02d %02d:%02d:%02d ", $year + 1900, $mon + 1, $mday, $hour, $min, $sec ); $qnoso = $dbmEvent[1] < 0 ? "S" : "N"; $qeawe = $dbmEvent[2] < 0 ? "W" : "E"; $qmag = $dbmEvent[4] > 0 ? sprintf( "%3.1fM", $dbmEvent[4] ) : " "; $qspeek = $dbmEvent[8] ? "S" : " "; - printf( TXT "%6.2f%s %6.2f%s %5.1f %s %s %s\n", - abs( $dbmEvent[1] ), - $qnoso, abs( $dbmEvent[2] ), - $qeawe, $dbmEvent[3], $qmag, $qspeek, $dbmEvent[6] - ); + printf( TXT "%6.2f%s %6.2f%s %5.1f %s %s %s\n", abs( $dbmEvent[1] ), $qnoso, abs( $dbmEvent[2] ), $qeawe, $dbmEvent[3], $qmag, $qspeek, $dbmEvent[6] ); } close TXT; @@ -303,23 +282,15 @@ sub parse_quake { # 0 1 2 3 4 5 6 7 8 9 #[gmt,lat,lon,depth,magnitude,source,location,distance,speak,spoken] - if ( - my ( - $qdate, $qtime, $qlatd, $qnoso, $qlong, - $qeawe, $qdept, $qmagn, $qsrc, $qloca - ) - = $line =~ - m!^(.{10})\s(.{8})\s(.{6})([NS])\s(.{6})([EW])\s(.{5})\s(.{3})\s(.{7}):\s(.+)! - ) + if ( my ( $qdate, $qtime, $qlatd, $qnoso, $qlong, $qeawe, $qdept, $qmagn, $qsrc, $qloca ) = + $line =~ m!^(.{10})\s(.{8})\s(.{6})([NS])\s(.{6})([EW])\s(.{5})\s(.{3})\s(.{7}):\s(.+)! ) { #convert timestamp to gmt my ( $qyear, $qmnth, $qday ) = $qdate =~ m!(\S+)/(\S+)/(\S+)!; my ( $qhour, $qminu, $qseco ) = $qtime =~ m!(\S+):(\S+):(\S+)!; my $qgmt; - eval { - $qgmt = timegm( $qseco, $qminu, $qhour, $qday, $qmnth - 1, $qyear ); - }; + eval { $qgmt = timegm( $qseco, $qminu, $qhour, $qday, $qmnth - 1, $qyear ); }; if ($@) { print("$Pgm_Name: timegm() failed to parse date and time\n") if $parms{v}; @@ -361,12 +332,7 @@ sub calc_distance { $lon1 /= $c; $lon2 /= $c; - $d = 2 * Math::Trig::asin( - sqrt( - ( sin( ( $lat1 - $lat2 ) / 2 ) )**2 + - cos($lat1) * cos($lat2) * ( sin( ( $lon1 - $lon2 ) / 2 ) )**2 - ) - ); + $d = 2 * Math::Trig::asin( sqrt( ( sin( ( $lat1 - $lat2 ) / 2 ) )**2 + cos($lat1) * cos($lat2) * ( sin( ( $lon1 - $lon2 ) / 2 ) )**2 ) ); if ( $units ne 'miles' ) { return $d * 6378; # convert to kilometers and return diff --git a/bin/get_email b/bin/get_email index 703ccdf68..de87dc217 100755 --- a/bin/get_email +++ b/bin/get_email @@ -27,24 +27,19 @@ my ( $Pgm_Path, $Pgm_Name, $Version ); use vars '$Pgm_Root'; # So we can see it in eval var subs in read_parms BEGIN { - ($Version) = - q$Revision$ =~ /: (\S+)/; # Note: revision number is auto-updated by cvs + ($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; $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 } use Getopt::Long; use vars qw(%config_parms %config_parms_startup); -if ( - !&GetOptions( \%config_parms_startup, 'quiet', 'debug', 'h', 'help', - 'net_mail_scan_age=s' ) +if ( !&GetOptions( \%config_parms_startup, 'quiet', 'debug', 'h', 'help', 'net_mail_scan_age=s' ) or @ARGV or $config_parms_startup{h} - or $config_parms_startup{help} - ) + or $config_parms_startup{help} ) { print <Latest emails
    ]; + $summary = qq[$time    Latest emails
    ]; my $wday = (localtime)[6]; my $day = ( 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' )[$wday]; @@ -219,7 +210,7 @@ sub check_accounts { my $message_s = "s"; $message_s = "" if ( $msgcnt == 1 ); $summary .= - qq[   $msgcnt message$message_s ($msg_unread unread) $msgsize in ] + qq[   $msgcnt message$message_s ($msg_unread unread) $msgsize in ] . qq[$account: $config_parms{"net_mail_${account}_address"}, $config_parms{"net_mail_${account}_user"}
    \n]; if ( $$msgptr{from_name} ) { @@ -248,8 +239,7 @@ sub check_accounts { # print "dbx get_email body=$body.\n"; - $body =~ - s/Content-Disposition: attachment.+?filename=(.+?)^.+/Attachment deleted: $1/gsm; + $body =~ s/Content-Disposition: attachment.+?filename=(.+?)^.+/Attachment deleted: $1/gsm; # while ($body =~ m/Content-Disposition: attachment.+?filename=(.+?\").+?NextPart.+?\n/s) { # $body =~ s/Content-Disposition: attachment.+?filename=(.+?\").+?NextPart.+?\n/Attachment removed: $1\n\n/sm; @@ -268,14 +258,11 @@ sub check_accounts { # Scan/summarize email # - .scan is deleted in code_dir/internet_mail.pl after scanning for commands # - .html will be deleted (in get_email) only after email has been read - logit( "$config_parms{data_dir}/get_email.scan", - "Msg: $number From:$$msgptr{from}[$cnt] To:$to Subject:$subject Body:$body" - ); + logit( "$config_parms{data_dir}/get_email.scan", "Msg: $number From:$$msgptr{from}[$cnt] To:$to Subject:$subject Body:$body" ); # print "dbx get_email s=$subject body=$body\n"; - use HTML::Entities - ; # So we can encode stuff like + use HTML::Entities; # So we can encode stuff like $to = encode_entities $to; $replyto = encode_entities $replyto; $from = encode_entities $from; @@ -290,11 +277,11 @@ sub check_accounts { $body = "
    $body
    "; } - my $href = time . $cnt; # An arbitrary index + my $href = time . $cnt; # An arbitrary index my $href_prev = $href - 1 unless $cnt == 0; my $href_next = $href + 1 unless $cnt == $#list; $href_prev = 'top' - unless $href_prev; # No easy way to track prev href + unless $href_prev; # No easy way to track prev href my $time_date = &time_date_stamp( 14, time ); my $html; @@ -303,8 +290,7 @@ sub check_accounts { # $html = 'Date: ' . &time_date_stamp(14, time) . "
    \n"; # Log format same as .scan logit $html = "Previous , "; $html .= "Next\n" if $href_next; - $html .= "
    Date: $time_date
    \n" - ; # Log format same as .scan logit + $html .= "
    Date: $time_date
    \n"; # Log format same as .scan logit $html .= "To:$to
    \nFrom: $name
    \nReply to: $replyto
    \nSubject:$subject
    \n"; $html .= "
    $body


    \n"; @@ -314,24 +300,17 @@ sub check_accounts { $msg_latest .= $html; # Log by account and day of week - my $log = - "$config_parms{data_dir}/email/${account}_${day}.shtml"; - my $logi = - "$config_parms{data_dir}/email/${account}_${day}_index.html"; + my $log = "$config_parms{data_dir}/email/${account}_${day}.shtml"; + my $logi = "$config_parms{data_dir}/email/${account}_${day}_index.html"; unlink $log - if time - ( stat $log )[9] > - 3600 * 24; # Reset if from last week + if time - ( stat $log )[9] > 3600 * 24; # Reset if from last week unless ( -e $log ) { unlink $logi; - my $html2 = - "\n"; - $html2 .= - "\n"; + my $html2 = "
    MsgReceivedFromSubject
    \n"; + $html2 .= "\n"; logit( $logi, $html2, 0 ); - $html2 = - qq[\n]; - $html2 .= - qq[\n
    MsgReceivedFromSubject
    \n
    \n]; + $html2 = qq[\n]; + $html2 .= qq[\n
    \n
    \n]; logit( $log, $html2, 0 ); } @@ -345,14 +324,11 @@ sub check_accounts { unless ( defined $msgcnt_prev ); $msgcnt_prev = 0 unless ( defined $msgcnt_prev ); my $msgcnt2 = sprintf( "%02d", $msgcnt_prev + $cnt + 1 ); - $html = - "($msgcnt2) Back to Index , " - . $html; + $html = "($msgcnt2) Back to Index , " . $html; logit( $log, $html, 0 ); - my $index = - "
  • $msgcnt2$time_date$name"; + my $index = "
    $msgcnt2$time_date$name"; $index .= "$subject
    |; + my $channel_number = ''; + my $channel_name = ''; + my $pgm_name = ''; + my $channelRowsSaved = 0; + my $current_time_bar = ''; + my $rowOfLastTimeBar = 0; + my $tableStartText = qq|
    |; my $rowsInCurrentOutTable = 0; my $script_flag = 0; @@ -920,10 +873,7 @@ eof #------------------------------------------------------------ # remove Shockwave ads #------------------------------------------------------------ - if ( $record =~ - m!\s*
    ! - ) - { + if ( $record =~ m!
    \s*
    ! ) { next; } @@ -994,11 +944,9 @@ eof # (Don't do this if this is the last line of the table.) #------------------------------------------------------------ if ( $rowsInCurrentOutTable % $parms{timebars} == 0 - and $rowsInCurrentOutTable != - $parms{tableChannels} ) + and $rowsInCurrentOutTable != $parms{tableChannels} ) { - my $tempResult = - $rowsInCurrentOutTable % $parms{timebars}; + my $tempResult = $rowsInCurrentOutTable % $parms{timebars}; print OUT $current_time_bar; } @@ -1058,17 +1006,12 @@ eof # can show them on the local output webpage. Also, alter url ref # so that it points to our logos directory. #----------------------------------------------------------------- - if ( $record =~ - s!/tms_network_logos/([^/]*\.(jpg|gif))!/$parms{db}/logos/$1! - ) - { + if ( $record =~ s!/tms_network_logos/([^/]*\.(jpg|gif))!/$parms{db}/logos/$1! ) { unless ( -f "$parms{outdir}/logos/$1" ) { #print "Retrieving logo: $1\n" if ( $parms{debug} ); print "Retrieving logo: $1\n"; - my $logoRequest = - HTTP::Request->new( - GET => $url . "tms_network_logos/$1" ); + my $logoRequest = HTTP::Request->new( GET => $url . "tms_network_logos/$1" ); $ua->request( $logoRequest, "$parms{outdir}/logos/$1" ); @@ -1080,15 +1023,12 @@ eof # can show them on the local output webpage. Also, alter url ref # so that it points to our logos directory. #----------------------------------------------------------------- - if ( $record =~ - s!images/([^/]*\.(jpg|gif))!/$parms{db}/logos/$1! ) - { + if ( $record =~ s!images/([^/]*\.(jpg|gif))!/$parms{db}/logos/$1! ) { unless ( -f "$parms{outdir}/logos/$1" ) { #print "Retrieving logo: $1\n" if ( $parms{debug} ); print "Retrieving logo: $1\n"; - my $logoRequest = - HTTP::Request->new( GET => $url . "images/$1" ); + my $logoRequest = HTTP::Request->new( GET => $url . "images/$1" ); $ua->request( $logoRequest, "$parms{outdir}/logos/$1" ); } @@ -1102,8 +1042,7 @@ eof # #
    @@ -215,18 +207,11 @@ sub edit { } $data .= "$_\n"; $rowcount++; @@ -289,13 +274,8 @@ sub edit_list { } $data .= "$_\n"; + $data .= map_chars( $cparms{$_} ) . '">\n"; $rowcount++; } @@ -342,8 +322,7 @@ sub make_doc { next unless defined $dparms{$_}; $data .= '

    ' . $_ . '

    ' . "\n"; $data .= 'Insert description here.

    ' . "\n"; - $data .= - 'Default value: ' . map_chars( $dparms{$_} ) . '

    ' . "\n"; + $data .= 'Default value: ' . map_chars( $dparms{$_} ) . '

    ' . "\n"; } return $head . $data . $tail; @@ -359,8 +338,7 @@ sub map_chars { # This is similar to handy_Utils read_opts ... maybe should merge or share code? sub my_read_opts { - my ( $ref_parms, $ref_order, $ref_cats, $ref_catorder, $config_file, - $debug ) = @_; + my ( $ref_parms, $ref_order, $ref_cats, $ref_catorder, $config_file, $debug ) = @_; my ( $key, $value, $value_continued, $help, $help_flag ); # If debug == 0 (instead of undef) this is disabled @@ -428,8 +406,7 @@ sub my_read_opts { $$ref_cats{$key} = $category; } } - print - "parm key=$key value=$$ref_parms{$key} category=$$ref_cats{$key}\n" + print "parm key=$key value=$$ref_parms{$key} category=$$ref_cats{$key}\n" if $debug; } close CONFIG; @@ -445,11 +422,9 @@ sub localize { closedir(DIR); foreach my $file ( sort @files ) { next - if $file =~ /location_specimen|example/ - ; # Ignore mh.location_specimen.ini and mh.example.ini - next if $file =~ /^\./; # Ignore ./ and ../ - $file =~ s/(mh\.|\.ini)//i - ; # problem with this line (next line should not be needed) + if $file =~ /location_specimen|example/; # Ignore mh.location_specimen.ini and mh.example.ini + next if $file =~ /^\./; # Ignore ./ and ../ + $file =~ s/(mh\.|\.ini)//i; # problem with this line (next line should not be needed) $file =~ s/\.ini//i; # $file = ucfirst($file); diff --git a/web/bin/items.pl b/web/bin/items.pl index 630288ba6..70da032a0 100644 --- a/web/bin/items.pl +++ b/web/bin/items.pl @@ -16,8 +16,7 @@ return &web_items_list(); -use vars '$web_item_file_name' - ; # Avoid my, so we can keep the same name between web calls +use vars '$web_item_file_name'; # Avoid my, so we can keep the same name between web calls my (@file_data); sub web_items_list { @@ -27,8 +26,7 @@ sub web_items_list { $html = qq| Items Menu\n$html Use this page to review or update your .mht file.|; - $html .= - qq|
    Read-Only: Login as admin to edit| + $html .= qq|
    Read-Only: Login as admin to edit| unless $Authorized eq 'admin'; $html .= qq|A backup is made and comments and record order are preserved. To update existing items, enter/change the field and hit Enter.| @@ -63,37 +61,23 @@ sub web_items_list { if $ARGV[0] =~ /^file=(.+)$/; # User selected another mht file # Create a form to pick which file - $html .= - "

    #------------------------------------------------------------ - $record =~ - s|\"([_a-z]+\.asp\??)|\"http://tvlistings2.zap2it.com/$1|g; + $record =~ s|\"([_a-z]+\.asp\??)|\"http://tvlistings2.zap2it.com/$1|g; # last if $record =~ / END GRID TABLE /; @@ -1158,8 +1097,7 @@ eof $pgm_name_html =~ s/&/and/g; $pgm_name_html =~ s/\'/'/g; $pgm_name_html =~ s/\?/?/g; - $pgm_name =~ - s/\<.+?\>//g; # Drop extra HTML directives (e.g. font) + $pgm_name =~ s/\<.+?\>//g; # Drop extra HTML directives (e.g. font) # Set program times/dates my $time_start = &min_to_hour($min_start); @@ -1172,16 +1110,14 @@ eof $time_start = &min_to_hour( $min_start - 24 * 60 ); $time_end = &min_to_hour( $min_end - 24 * 60 ); } - print - "found program start $time_start pgm_date $pgm_date\n"; + print "found program start $time_start pgm_date $pgm_date\n"; - print - "db $pgm_name, $min_pgm, $min_start, $min_end, $time_start, $time_end, $pgm_date\n" + print "db $pgm_name, $min_pgm, $min_start, $min_end, $time_start, $time_end, $pgm_date\n" if $parms{debug}; # Insert the mh VCR link my $vcr_ref = - "$parms{label}"; $pgm_data = "$vcr_ref for $pgm_data" unless lc $parms{label} eq 'none'; @@ -1194,21 +1130,17 @@ eof $pgm_data .= $record; ($pgm_desc) = $record =~ /> *(.+)/; - $record = - $pgm_data; # This will get saved to $channel_data; + $record = $pgm_data; # This will get saved to $channel_data; # Clean up the program description - $pgm_desc =~ - s/\<.+?\>//g; # Drop extra HTML directives (e.g. font) - $pgm_desc =~ s/ / /g; # Drop extra spaces + $pgm_desc =~ s/\<.+?\>//g; # Drop extra HTML directives (e.g. font) + $pgm_desc =~ s/ / /g; # Drop extra spaces #------------------------------------------------------------ # Store the data in the DBM #------------------------------------------------------------ - my $db_key = - join( $;, $channel_number, $pgm_date, $time_start ); - my $db_data = - join( $;, $time_end, $pgm_name, $pgm_desc ); + my $db_key = join( $;, $channel_number, $pgm_date, $time_start ); + my $db_data = join( $;, $time_end, $pgm_name, $pgm_desc ); $DBM{$db_key} = $db_data; $DBM2{$channel_number} = $channel_name; print "db key=$db_key\n data=$db_data.\n" @@ -1236,8 +1168,7 @@ eof } } # Main while-loop - print "$count1 records with $count2 grid records were read. " - . "$count3 programs stored for $channelRowsSaved channels.\n"; + print "$count1 records with $count2 grid records were read. " . "$count3 programs stored for $channelRowsSaved channels.\n"; close IN; $provnum++; } @@ -1252,11 +1183,9 @@ eof #====================================================================== sub make_index { my ( $hour, $down, $day, $day_time ) = @_; - print OUT "
    \n\n\n\n); + print qq( \n); } else { $width = $maxwidth[$_] ? $maxwidth[$_] + 1 : 20; @@ -678,29 +671,25 @@ EOF &HTMLescape( &slashescapetextarea( $field[$_] ) ); if ( $flags[$_]{'r'} ) { $safefieldta =~ s/\n/
    \n/g; - print - qq( \n); + print qq( \n); } else { $width = $maxwidth[$_] > 20 ? $maxwidth[$_] : 20; $width = $width < 60 ? $width : 60; $height = $maxheight[$_] > 1 ? $maxheight[$_] + 1 : 2; $height = $height < 10 ? $height : 10; - print - qq( \n); + print qq( \n); } # ordinary text fields } else { if ( $flags[$_]{'r'} ) { - print - qq( \n); + print qq( \n); } else { $width = $maxwidth[$_] ? $maxwidth[$_] + 1 : 20; - print - qq( \n); + print qq( \n); } } @@ -868,7 +857,7 @@ sub getcgivars { ( $name, $value ) = split( '=', $_, 2 ); $name =~ s/%(..)/sprintf("%c",hex($1))/ge; $value =~ s/%(..)/sprintf("%c",hex($1))/ge; - $in{$name} .= "\0" if defined( $in{$name} ); # concatenate multiple vars + $in{$name} .= "\0" if defined( $in{$name} ); # concatenate multiple vars $in{$name} .= $value; } @@ -909,9 +898,10 @@ sub urlencode { # create URL-encoded QUERY_STRING for an associative array sub urlencodelist { local (%a) = @_; - return join( '&', - map { &urlencode($_) . '=' . &urlencode( $a{$_} ) } - grep( defined( $a{$_} ), keys %a ) ); + return join( + '&', map { &urlencode($_) . '=' . &urlencode( $a{$_} ) } + grep( defined( $a{$_} ), keys %a ) + ); } # returns a subset of associative array @@ -939,11 +929,7 @@ sub hiddenvars { foreach ( keys %a ) { if ( defined( $a{$_} ) ) { - $ret .= - '\n"; + $ret .= '\n"; } } return $ret; diff --git a/web/bin/display_map.pl b/web/bin/display_map.pl index d31705cd3..b59f8cd67 100644 --- a/web/bin/display_map.pl +++ b/web/bin/display_map.pl @@ -25,8 +25,7 @@ #u2 = qq|&FAM=myblast&W=600&H=350|; $u2 = qq|&FAM=myblast&W=300&H=300|; -$u1 = - qq|http://maps.aprsworld.net/mapserver/map.php?lat=$y&lon=$x&label=$text2|; +$u1 = qq|http://maps.aprsworld.net/mapserver/map.php?lat=$y&lon=$x&label=$text2|; my $html = "

    $text

    \n"; diff --git a/web/bin/flight_status.pl b/web/bin/flight_status.pl index 8b6f41bb7..b0f09ce45 100644 --- a/web/bin/flight_status.pl +++ b/web/bin/flight_status.pl @@ -31,8 +31,7 @@ } if ( $statusURL ne '' ) { - $html .= - qq[

    Airline: $HTTP_ARGV{airline}. Flight Number: $HTTP_ARGV{flight}.

    + $html .= qq[

    Airline: $HTTP_ARGV{airline}. Flight Number: $HTTP_ARGV{flight}.

    Request the status of another flight

    Refresh this flight status page

    ]; @@ -150,8 +149,7 @@
    ]; - $html .= &insert_keyboard( - { form => 'main', target => 'flight', numeric_keypad => 'yes' } ); + $html .= &insert_keyboard( { form => 'main', target => 'flight', numeric_keypad => 'yes' } ); } return $html; diff --git a/web/bin/floorplan.pl b/web/bin/floorplan.pl index 343d1756a..d23b20479 100644 --- a/web/bin/floorplan.pl +++ b/web/bin/floorplan.pl @@ -44,8 +44,7 @@ my $object_name = shift || '$Property'; my $object = &get_object_by_name($object_name); -return &html_page( 'FloorPlan', - "No $object_name Group found to generate a floorplan from" ) +return &html_page( 'FloorPlan', "No $object_name Group found to generate a floorplan from" ) unless $object; my $html = ""; @@ -87,13 +86,9 @@ sub web_fp #render table representation of objects and their co-ordinates for my $obj (@l_objs) { ( $l_x, $l_y, $l_w, $l_h ) = $obj->get_fp_location(); if ( $l_x ne "" ) { #Only do items with co-ordinates - for ( my $h = $l_y; - $h < $l_y + $l_h; - $h++ ) # Create Virtual Frame Buffer of object blocks + for ( my $h = $l_y; $h < $l_y + $l_h; $h++ ) # Create Virtual Frame Buffer of object blocks { - for ( my $w = $l_x; - $w < $l_x + $l_w; - $w++ ) # Create Virtual Frame buffer of object blocks + for ( my $w = $l_x; $w < $l_x + $l_w; $w++ ) # Create Virtual Frame buffer of object blocks { $l_fp[$w][$h] = $obj; $l_xmax = $w if $l_xmax < $w; @@ -104,61 +99,40 @@ sub web_fp #render table representation of objects and their co-ordinates } $l_html .= web_fp_item($p_obj) . "
    "; if ( @l_objs > 0 ) { - $l_html .= - "
    \n" - ; ### DW: remove bgcolors + print OUT "
    \n\n\n\n]; } - $html .= - qq[ \n]; + $html .= qq[ \n]; $html .= qq[ \n]; # pda status - $html .= - qq[ \n] + $html .= qq[ \n] if ( $Config{'DisplayMode'} eq 'pda' ); $html .= qq[
    \n"; ### DW: remove bgcolors - print OUT - " TV Listings for $parms{name} "; + print OUT " TV Listings for $parms{name} "; print OUT ""; #---------------------------------------------------------------------- @@ -1270,8 +1199,7 @@ sub make_index { # be the friendliest way to do this (in my own humble opinion.) # Feel free to change this if you like. #---------------------------------------------------------------------- - print OUT - "\n"; #my $dow_start = -$down - 7; my $dow_start = -$parms{purge}; @@ -1279,8 +1207,7 @@ sub make_index { # my $dow_stop = $parms{days} -1; for my $count ( $dow_start .. $dow_stop ) { - my ( $dow2, $dow2n, $day2, $month2, $year2 ) = - &days_from_now( $day_time, $count ); + my ( $dow2, $dow2n, $day2, $month2, $year2 ) = &days_from_now( $day_time, $count ); print OUT "

    $Status

    $Status

    $Status

    \n]; @@ -899,13 +864,10 @@ sub DisplayHomePage { # we display all category my $num = 0; - $html .= - qq[ \n]; - $html .= - qq[ \n]; + $html .= qq[ \n]; + $html .= qq[ \n]; foreach my $cat ( sort @Category ) { - $html .= - qq[  \n]; + $html .= qq[  \n]; $num++; } @@ -945,18 +907,14 @@ sub DisplayManageCategory { $html .= qq[\n]; $html .= qq[\n]; - $html .= - qq[ \n]; - $html .= - qq[ \n]; + $html .= qq[ \n]; + $html .= qq[ \n]; $html .= qq[
    \n]; $html .= qq[\n]; - $html .= - qq[\n]; + $html .= qq[\n]; $html .= qq[
    \n]; - $html .= - qq[\n]; + $html .= qq[
    \n]; # display delete header my $i = 0; @@ -973,10 +931,8 @@ sub DisplayManageCategory { foreach my $Category ( keys %CurrentItem ) { next if $Category eq "Web_Functions"; $html .= qq[ \n] if $num % $DisplayColumns == 0; - $html .= - qq[ \n]; + $html .= qq[ \n]; $num++; $html .= qq[ \n] if $num % $DisplayColumns == 0; } @@ -1019,12 +975,10 @@ sub DisplayManageItem { ]; $html .= qq[\n]; - $html .= - qq[\n]; + $html .= qq[\n]; $html .= qq[
    \n]; - $html .= - qq[ \n]; + $html .= qq[
    \n]; - $html .= - qq[ \n]; + $html .= qq[ \n]; # normal mode status $Status = '' if !$Config{'DisplayStatus'}; @@ -1032,30 +986,25 @@ sub DisplayManageItem { $html .= qq[ \n]; } - $html .= - qq[ \n]; + $html .= qq[ \n]; $html .= qq[ \n]; # pda status - $html .= - qq[ \n] + $html .= qq[ \n] if ( $Config{'DisplayMode'} eq 'pda' ); $html .= qq[

    $Status

    $Status

    $Status

    \n]; $html .= qq[\n]; - $html .= - qq[\n]; + $html .= qq[\n]; $html .= qq[
    \n]; if ( $Category eq '' ) { - $html .= - qq[

    Please select a category

    \n]; + $html .= qq[

    Please select a category

    \n]; } else { $html .= qq[

    $Category

    \n]; } - $html .= - qq[\n]; + $html .= qq[
    \n]; # display delete/rename header my $i = 0; @@ -1072,10 +1021,8 @@ sub DisplayManageItem { foreach my $Item ( keys %{ $CurrentItem{$Category} } ) { next if ( $Item eq 'DummyItem' ); $html .= qq[ \n] if $num % $DisplayColumns == 0; - $html .= - qq[ \n]; + $html .= qq[ \n]; $num++; $html .= qq[ \n] if $num % $DisplayColumns == 0; } @@ -1087,8 +1034,7 @@ sub DisplayManageItem { # we display all category foreach my $cat ( sort keys %CurrentItem ) { next if $cat eq ''; - $html .= - qq[ \n]; + $html .= qq[ \n]; } 1; @@ -1116,22 +1062,19 @@ sub DisplayManageList { #}}} $html .= ( $Config{'DisplayMode'} eq 'normal' ) ? "
    \n" : "\n"; - $html .= - qq[

    Select from the following list, or enter a new list name

    \n]; + $html .= qq[

    Select from the following list, or enter a new list name

    \n]; $html .= qq[
    \n]; - $html .= - qq[ \n]; + $html .= qq[
    \n]; my @Lists = <$config_parms{'data_dir'}/ListManager/*.lst>; foreach my $l (@Lists) { my ($PrettyName) = $l =~ /.*\/(.*).lst$/; $html .= qq[\n\n] if $num % $cols == 0; - $html .= - qq[\n]; + $html .= qq[\n]; $num++; } $html .= qq[
    \n]; - $html .= - qq[

    New List:

    \n]; + $html .= qq[

    New List:

    \n]; # display keyboard InsertKeyboard('DisplayManageList_NewList') if $DisplayKeyboard; @@ -1144,8 +1087,7 @@ sub DisplayManageList {

    ]; - $html .= - ( $Config{'DisplayMode'} eq 'normal' ) ? "
    \n" : "\n"; + $html .= ( $Config{'DisplayMode'} eq 'normal' ) ? "\n" : "\n"; 1; } @@ -1385,8 +1327,7 @@ =head1 Credits and contact information # ======================== POD END ================================== ]; - open DOC, - "echo \"$POD\" | pod2html --cachedir=$config_parms{html_alias_cache} --flush 2>/dev/null |"; + open DOC, "echo \"$POD\" | pod2html --cachedir=$config_parms{html_alias_cache} --flush 2>/dev/null |"; my $content = 0; while () { $content = 1 if /\n]; $html .= qq[

    Please select your preferences

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

    Add to List when Creating Item Selected by default:

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

    Display keyboard in PDA mode:

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

    Display keyboard in Normal mode:

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

    Columns in PDA mode:

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

    Columns in normal mode:

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

    Display status message:

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

    Please consult the man page about the print preference

    ]; - $html .= - qq[

    Print text cmd:

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

    Print html cmd:

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

    \n]; + $html .= qq[

    Add to List when Creating Item Selected by default:

    \n]; + $html .= qq[

    Display keyboard in PDA mode:

    \n]; + $html .= qq[

    Display keyboard in Normal mode:

    \n]; + $html .= qq[

    Columns in PDA mode:

    \n]; + $html .= qq[

    Columns in normal mode:

    \n]; + $html .= qq[

    Display status message:

    \n]; + $html .= qq[

    Please consult the man page about the print preference

    ]; + $html .= qq[

    Print text cmd:

    \n]; + $html .= qq[

    Print html cmd:

    \n]; + $html .= qq[

    \n]; $html .= qq[\n]; @@ -1504,16 +1433,14 @@ sub DisplaySearchItem { my $loc = ( $Config{'DisplayMode'} eq 'pda' ) ? 'left' : 'center'; if ( $Item2Search eq "Search" ) { - $html .= - qq[

    <$loc>Search: + $html .= qq[

    <$loc>Search:

    ]; # display keyboard InsertKeyboard('DisplaySearchItem') if $DisplayKeyboard; - $html .= - qq[

    <$loc>

    ]; + $html .= qq[

    <$loc>

    ]; } else { @@ -1538,8 +1465,7 @@ sub DisplaySearchItem { ]; - $html .= - qq[

    + $html .= qq[


    ]; @@ -1559,12 +1485,9 @@ sub DisplaySearchItem { : $CurrentItem{$DisplayCategory}{$Item}; $html .= qq[\n]; $html .= qq[\n\n] if $num % $NumColumns == 0; my $Count = "($CurrentItem{$DisplayCategory}{$Item})" if $CurrentItem{$DisplayCategory}{$Item} > 1; - $html .= - qq[\n]; + $html .= qq[\n]; $num++; } } @@ -2021,12 +1939,10 @@ sub PrintHtml { $Config{'PrintHtmlCommand'} =~ s/ FILE/ $temp/; system("$Config{'PrintHtmlCommand'}"); unlink $temp; - $html .= - qq[

    The following page has been print with "$Config{'PrintHtmlCommand'}"

    \n]; + $html .= qq[

    The following page has been print with "$Config{'PrintHtmlCommand'}"

    \n]; } else { - $html .= - "

    The print command hasn't been defined yet, please consult the Man page and then set the print preference

    "; + $html .= "

    The print command hasn't been defined yet, please consult the Man page and then set the print preference

    "; } 1; } @@ -2037,8 +1953,7 @@ sub PrintPreview { my $Msg = shift; $html .= qq[\n]; $html .= qq[

    $PrettyListName

    \n]; - foreach my $DisplayCategory (@Category) - { # no sort, to display in aisles way (shopping) + foreach my $DisplayCategory (@Category) { # no sort, to display in aisles way (shopping) my $HeaderPrint = 0; my $num = 0; foreach my $Item ( sort keys %{ $CurrentItem{$DisplayCategory} } ) { @@ -2048,8 +1963,7 @@ sub PrintPreview { $html .= qq[\n\n] if $num % $NumColumns == 0; my $Count = "($CurrentItem{$DisplayCategory}{$Item})" if $CurrentItem{$DisplayCategory}{$Item} > 1; - $html .= - qq[\n]; + $html .= qq[\n]; $num++; } } @@ -2083,16 +1997,14 @@ sub ReadConfig { print_log "Directory ListManager doesn't exist"; print_log " Creating $DirPath"; mkdir( "$DirPath", 0755 ) - or return shoppingListError( - "ReadConfig: Can't create directory $DirPath: $!"); + or return shoppingListError("ReadConfig: Can't create directory $DirPath: $!"); } # validate is we have a configuration file if ( !-f $ConfigFile ) { print_log "Creating basic configuration file $ConfigFile"; open CONFIG, ">$ConfigFile" - or return shoppingListError( - "ReadConfig: Can't open config file $ConfigFile: $!"); + or return shoppingListError("ReadConfig: Can't open config file $ConfigFile: $!"); print CONFIG ConfigFileHeader(); print CONFIG ConfigDefault($DefaultList); close CONFIG; @@ -2103,16 +2015,14 @@ sub ReadConfig { if ( $#List < 0 ) { open LIST, ">$DefaultList" - or return shoppingListError( - "ReadConfig: Can't open new list $DefaultList: $!"); + or return shoppingListError("ReadConfig: Can't open new list $DefaultList: $!"); print LIST DefaultList(); close LIST; } # now we could read the configuration open CONFIG, "$ConfigFile" - or return shoppingListError( - "ReadConfig: Can't open config file $ConfigFile: $!"); + or return shoppingListError("ReadConfig: Can't open config file $ConfigFile: $!"); while () { chomp; s/^\s*(.*?)\s*$/$1/; @@ -2296,13 +2206,11 @@ sub DisplayCurrentList { if $CurrentItem{$Category}{$Item} > 1; } } - $html .= - qq[
    \n]; - $html .= - qq[\n]; - $html .= - qq[\n]; - $html .= - qq[\n]; + $html .= qq[\n]; + $html .= qq[\n]; + $html .= qq[\n]; $html .= qq[$DisplayCategory: $Item\n]; $html .= qq[]; @@ -1933,8 +1856,7 @@ sub PrintText { $html .= qq[\n]; $html .= qq[

    \n]; if ( $Config{'PrintTextCommand'} ne '' ) { - foreach my $DisplayCategory (@Category) - { # no sort, to display in aisles way (shopping) + foreach my $DisplayCategory (@Category) { # no sort, to display in aisles way (shopping) my $HeaderPrint = 0; my $num = 0; my @ITEM; @@ -1966,12 +1888,10 @@ sub PrintText { $Config{'PrintTextCommand'} =~ s/ FILE/ $temp/; system("$Config{'PrintTextCommand'}"); unlink $temp; - $html .= - qq[

    $txt
    \n

    The following page has been print with "$Config{'PrintTextCommand'}"

    \n]; + $html .= qq[

    $txt
    \n

    The following page has been print with "$Config{'PrintTextCommand'}"

    \n]; } else { - $html .= - "

    The print command hasn't been defined yet, please consult the Man page and then set the print preference

    "; + $html .= "

    The print command hasn't been defined yet, please consult the Man page and then set the print preference

    "; } 1; @@ -1985,8 +1905,7 @@ sub PrintHtml { $html .= qq[

    \n]; if ( $Config{'PrintHtmlCommand'} ne '' ) { $html .= qq[

    $PrettyListName

    \n]; - foreach my $DisplayCategory (@Category) - { # no sort, to display in aisles way (shopping) + foreach my $DisplayCategory (@Category) { # no sort, to display in aisles way (shopping) my $HeaderPrint = 0; my $num = 0; foreach my $Item ( sort keys %{ $CurrentItem{$DisplayCategory} } ) { @@ -1996,8 +1915,7 @@ sub PrintHtml { $html .= qq[
    $Item $Count$Item $Count
    $Item $Count$Item $Count
    \n]; + $html .= qq[
    \n]; my $num = 0; foreach my $Item ( sort keys %ItemOn ) { $html .= qq[ \n] if $num % $NumColumns == 0; - $html .= - qq[ \n]; + $html .= qq[ \n]; $num++; $html .= qq[ \n] if $num % $NumColumns == 0; } @@ -2315,8 +2223,7 @@ sub DisplayCurrentList { #sub WriteConfig{{{ sub WriteConfig { open CONFIG, ">$ConfigFile" - or return shoppingListError( - "WriteConfig: Can't open config file $ConfigFile: $!"); + or return shoppingListError("WriteConfig: Can't open config file $ConfigFile: $!"); print CONFIG ConfigFileHeader(); foreach my $k ( sort keys %Config ) { $Config{$k} = lc( $Config{$k} ) if $k != 'CurrentList'; diff --git a/web/bin/button.pl b/web/bin/button.pl index 2228f27f3..d214e26b3 100644 --- a/web/bin/button.pl +++ b/web/bin/button.pl @@ -177,8 +177,7 @@ # calculate size of image and text for offset my $textsize = length $text; - my $font = gdMediumBoldFont - ; # Choices: gdTinyFont, gdLargeFont, gdSmallFont, gdMediumBoldFont + my $font = gdMediumBoldFont; # Choices: gdTinyFont, gdLargeFont, gdSmallFont, gdMediumBoldFont $font = gdTinyFont if $textsize > 14; my ( $imwidth, $imheight ) = $image->getBounds(); diff --git a/web/bin/button2.pl b/web/bin/button2.pl index b002da19b..20b02e6bd 100644 --- a/web/bin/button2.pl +++ b/web/bin/button2.pl @@ -87,8 +87,7 @@ # We hit the cache, so we give back the image and exit if ( -f "$config_parms{html_alias_cache}/$ImageFile" ) { - print - "$ScriptName: Hit cached file $config_parms{html_alias_cache}/$ImageFile\n" + print "$ScriptName: Hit cached file $config_parms{html_alias_cache}/$ImageFile\n" if $Debug{$ScriptName}; my $data = file_read("$config_parms{html_alias_cache}$ImageFile"); return &mime_header( "/cache" . $ImageFile, 1, length $data ) . $data; @@ -125,22 +124,18 @@ my $WorkAreaMinX = $config_parms{button_min_x} || 3; my $WorkAreaMinY = $config_parms{button_min_y} || 3; - print - "$ScriptName: WorkAreaMinX=$WorkAreaMinX WorkAreaMinY=$WorkAreaMinY\n" + print "$ScriptName: WorkAreaMinX=$WorkAreaMinX WorkAreaMinY=$WorkAreaMinY\n" if $Debug{$ScriptName}; my $WorkAreaMaxX = $config_parms{button_max_x} || $Twidth - 3; my $WorkAreaMaxY = $config_parms{button_max_y} || $THeight - 3; - print - "$ScriptName: WorkAreaMaxX=$WorkAreaMaxX WorkAreaMaxY=$WorkAreaMaxY\n" + print "$ScriptName: WorkAreaMaxX=$WorkAreaMaxX WorkAreaMaxY=$WorkAreaMaxY\n" if $Debug{$ScriptName}; # copy icon over template, try to center in height my ( $IWidth, $IHeight ) = $GDIcon->getBounds(); - my $IconY = - ( $WorkAreaMaxY - $WorkAreaMinY ) / 2 + $WorkAreaMinY - ( $IHeight / 2 ); - $GDTemplate->copy( $GDIcon, $WorkAreaMinX, $IconY, 0, 0, $IWidth, - $IHeight ); + my $IconY = ( $WorkAreaMaxY - $WorkAreaMinY ) / 2 + $WorkAreaMinY - ( $IHeight / 2 ); + $GDTemplate->copy( $GDIcon, $WorkAreaMinX, $IconY, 0, 0, $IWidth, $IHeight ); print "$ScriptName: Icon upper left [ $WorkAreaMinX, $IconY] \n" if $Debug{$ScriptName}; @@ -152,8 +147,7 @@ # determine how many pixels available for text my $MaxTextPixelX = $WorkAreaMaxX - ( $IWidth + $WorkAreaMinX ); my $MaxTextPixelY = $WorkAreaMaxY - $WorkAreaMinY; - print - "$ScriptName: Maximum Text pixels available X=$MaxTextPixelX Y=$MaxTextPixelY\n" + print "$ScriptName: Maximum Text pixels available X=$MaxTextPixelX Y=$MaxTextPixelY\n" if $Debug{$ScriptName}; my @lines = split( '_', $Text ); @@ -179,12 +173,10 @@ # validate ttf fonts if ( -f $config_parms{button_ttf} ) { $PTSize = $config_parms{button_ttf_ptsize} || 8.0; - my @Bounds = GD::Image->stringTTF( "0,0,0", $config_parms{button_ttf}, - $PTSize, 0.0, 0, 0, "Nothing" ); + my @Bounds = GD::Image->stringTTF( "0,0,0", $config_parms{button_ttf}, $PTSize, 0.0, 0, 0, "Nothing" ); if ( scalar @Bounds == 0 ) { print "$ScriptName: Invalid TTF fonts $FontUse\n"; - print - "$ScriptName: maybe GD not configured for TTF, or invalid file\n"; + print "$ScriptName: maybe GD not configured for TTF, or invalid file\n"; $ButtonOK = 0; } else { @@ -200,12 +192,9 @@ $PTSize = $config_parms{button_ttf_ptsize} || 8.0; my $AllChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $AllChar ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $AllChar ); my $FontHeight = $Bounds[7] - $Bounds[1]; - print - "$ScriptName: text will be written with font $FontUse and PTSize $PTSize\n" + print "$ScriptName: text will be written with font $FontUse and PTSize $PTSize\n" if $Debug{$ScriptName}; print "$ScriptName: Font height $FontHeight pixels\n" if $Debug{$ScriptName}; @@ -214,86 +203,62 @@ if ( $NumLines == 1 ) { #determine text position - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $lines[0] ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $lines[0] ); print "$ScriptName: Bounds for line \"$lines[0]\" @Bounds\n" if $Debug{$ScriptName}; - $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); - $y = $WorkAreaMaxY - 2; - @Bounds = - $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, - $lines[0] ); + $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); + $y = $WorkAreaMaxY - 2; + @Bounds = $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, $lines[0] ); print "$ScriptName: Text location for line \"$lines[0]\" [$x,$y]\n" if $Debug{$ScriptName}; } elsif ( $NumLines == 2 ) { - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $lines[1] ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $lines[1] ); print "$ScriptName: Bounds for line \"$lines[1]\" @Bounds\n" if $Debug{$ScriptName}; $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[1] ); $y = $WorkAreaMaxY - 2; my $nexty = $y + $Bounds[7] - 2; - @Bounds = - $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, - $lines[1] ); + @Bounds = $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, $lines[1] ); print "$ScriptName: Text location for line \"$lines[1]\" [$x,$y]\n" if $Debug{$ScriptName}; - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $lines[0] ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $lines[0] ); print "$ScriptName: Bounds for line \"$lines[0]\" @Bounds\n" if $Debug{$ScriptName}; - $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); - $y = $nexty; - @Bounds = - $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, - $lines[0] ); + $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); + $y = $nexty; + @Bounds = $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, $lines[0] ); print "$ScriptName: Text location for line \"$lines[0]\" [$x,$y]\n" if $Debug{$ScriptName}; } else { - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $lines[2] ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $lines[2] ); print "$ScriptName: Bounds for line \"$lines[2]\" @Bounds\n" if $Debug{$ScriptName}; - $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); - $y = $WorkAreaMaxY - 2; - $nexty = $y + ( $Bounds[7] ) - 2; - @Bounds = - $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, - $lines[2] ); + $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); + $y = $WorkAreaMaxY - 2; + $nexty = $y + ( $Bounds[7] ) - 2; + @Bounds = $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, $lines[2] ); print "$ScriptName: Text location for line \"$lines[2]\" [$x,$y]\n" if $Debug{$ScriptName}; - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $lines[1] ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $lines[1] ); print "$ScriptName: Bounds for line \"$lines[1]\" @Bounds\n" if $Debug{$ScriptName}; - $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); - $y = $nexty; - $nexty = $y + $Bounds[7] - 2; - @Bounds = - $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, - $lines[1] ); + $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); + $y = $nexty; + $nexty = $y + $Bounds[7] - 2; + @Bounds = $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, $lines[1] ); print "$ScriptName: Text location for line \"$lines[1]\" [$x,$y]\n" if $Debug{$ScriptName}; - @Bounds = - GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, - $lines[0] ); + @Bounds = GD::Image->stringTTF( $TextColor, $FontUse, $PTSize, 0.0, 0, 0, $lines[0] ); print "$ScriptName: Bounds for line \"$lines[0]\" @Bounds\n" if $Debug{$ScriptName}; - $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); - $y = $nexty; - @Bounds = - $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, - $lines[0] ); + $x = $WorkAreaMaxX - ( $Bounds[2] + 2 - $Bounds[0] ); + $y = $nexty; + @Bounds = $GDTemplate->stringTTF( $TextColor, $FontUse, $PTSize, 0, $x, $y, $lines[0] ); print "$ScriptName: Text location for line \"$lines[0]\" [$x,$y]\n" if $Debug{$ScriptName}; } @@ -302,9 +267,7 @@ else { if ( $config_parms{button_ttf} ) { - print( - "$ScriptName: Invalid ttf font $config_parms{button_ttf}, use default GD font\n" - ); + print("$ScriptName: Invalid ttf font $config_parms{button_ttf}, use default GD font\n"); $ButtonOK = 0; } @@ -333,45 +296,39 @@ if ( $NumLines == 1 ) { my $x = $WorkAreaMaxX - ( length( $lines[0] ) * $FontWidth ); my $y = $WorkAreaMaxY - $FontHeight - 1; - $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[0], - $TextColor ); + $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[0], $TextColor ); print "$ScriptName: Text location for line \"$lines[0]\" [$x,$y]\n" if $Debug{$ScriptName}; } elsif ( $NumLines == 2 ) { my $x = $WorkAreaMaxX - ( length( $lines[1] ) * $FontWidth ); my $y = $WorkAreaMaxY - $FontHeight - 1; - $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[1], - $TextColor ); + $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[1], $TextColor ); print "$ScriptName: Text location for line \"$lines[1]\" [$x,$y]\n" if $Debug{$ScriptName}; $x = $WorkAreaMaxX - ( length( $lines[0] ) * $FontWidth ); $y -= $FontHeight; - $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[0], - $TextColor ); + $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[0], $TextColor ); print "$ScriptName: Text location for line \"$lines[0]\" [$x,$y]\n" if $Debug{$ScriptName}; } else { my $x = $WorkAreaMaxX - ( length( $lines[2] ) * $FontWidth ); my $y = $WorkAreaMaxY - $FontHeight - 1; - $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[2], - $TextColor ); + $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[2], $TextColor ); print "$ScriptName: Text location for line \"$lines[2]\" [$x,$y]\n" if $Debug{$ScriptName}; $x = $WorkAreaMaxX - ( length( $lines[1] ) * $FontWidth ); $y -= $FontHeight; - $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[1], - $TextColor ); + $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[1], $TextColor ); print "$ScriptName: Text location for line \"$lines[1]\" [$x,$y]\n" if $Debug{$ScriptName}; $x = $WorkAreaMaxX - ( length( $lines[0] ) * $FontWidth ); $y -= $FontHeight; - $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[0], - $TextColor ); + $GDTemplate->string( $FontUse, $x, $y, ucfirst $lines[0], $TextColor ); print "$ScriptName: Text location for line \"$lines[0]\" [$x,$y]\n" if $Debug{$ScriptName}; } @@ -385,18 +342,15 @@ } my $ButtonFile = $GDTemplate->$ButtonType(); if ($ButtonOK) { - print - "$ScriptName: Writing button to cache: $config_parms{html_alias_cache}/$ImageFile\n" + print "$ScriptName: Writing button to cache: $config_parms{html_alias_cache}/$ImageFile\n" if $Debug{$ScriptName}; file_write( "$config_parms{html_alias_cache}/$ImageFile", $ButtonFile ); } else { - print - "$ScriptName: Button $config_parms{html_alias_cache}/$ImageFile not written to cache\n"; + print "$ScriptName: Button $config_parms{html_alias_cache}/$ImageFile not written to cache\n"; } - return &mime_header( "/cache" . $ImageFile, 1, length $ButtonFile ) - . $ButtonFile; + return &mime_header( "/cache" . $ImageFile, 1, length $ButtonFile ) . $ButtonFile; } else { print "$ScriptName: Error generating image\n"; @@ -416,8 +370,7 @@ sub ValidIcon { # name could have 1 dot, to define type (icon.png) my $DotCount = $IconName =~ tr/././; if ( $DotCount > 1 ) { - print - "ValidIcon: Invalid icon name $IconName, no more than 1 dot (.) allow in name, using EmptyIcon.png\n"; + print "ValidIcon: Invalid icon name $IconName, no more than 1 dot (.) allow in name, using EmptyIcon.png\n"; return ( "NOICON", "NOICON", "" ); } @@ -432,8 +385,7 @@ sub ValidIcon { # is it a valid GD type if ( index( "JPG JPEG PNG XBM WMP XPM", uc($Type) ) < 0 ) { - print - "$ScriptName: ValidIcon: Invalid icon type [$Type], only JPG JPEG PNG XBM WMP XPM allowed\n"; + print "$ScriptName: ValidIcon: Invalid icon type [$Type], only JPG JPEG PNG XBM WMP XPM allowed\n"; return ( "NOICON", "NOICON", "" ); } @@ -510,24 +462,13 @@ sub OpenImage { elsif ( $FileName eq "NOTEMPLATE" ) { # we have no template - $TemplateObject = - GD::Image->new( $GenericTemplateX, $GenericTemplateY ); + $TemplateObject = GD::Image->new( $GenericTemplateX, $GenericTemplateY ); my $white = $TemplateObject->colorAllocate( 255, 255, 255 ); my $black = $TemplateObject->colorAllocate( 0, 0, 0 ); - $TemplateObject->rectangle( 0, 0, $GenericTemplateX, $GenericTemplateY, - $white ); - $TemplateObject->rectangle( - 1, 1, - $GenericTemplateX - 1, - $GenericTemplateY - 1, $black - ); - $TemplateObject->rectangle( - 3, 3, - $GenericTemplateX - 3, - $GenericTemplateY - 3, $white - ); - print - "$ScriptName: Template file not defined in configuration file, will create a simple one\n"; + $TemplateObject->rectangle( 0, 0, $GenericTemplateX, $GenericTemplateY, $white ); + $TemplateObject->rectangle( 1, 1, $GenericTemplateX - 1, $GenericTemplateY - 1, $black ); + $TemplateObject->rectangle( 3, 3, $GenericTemplateX - 3, $GenericTemplateY - 3, $white ); + print "$ScriptName: Template file not defined in configuration file, will create a simple one\n"; $ButtonOK = 0; } elsif ( $FileName eq "NOICON" ) { @@ -569,8 +510,7 @@ sub FindFile { return "$Dirname/$FileName"; } } - print - "$ScriptName: FindFile: $ImageType $FileName not found in any icons dir"; + print "$ScriptName: FindFile: $ImageType $FileName not found in any icons dir"; return "NO$ImageType"; } diff --git a/web/bin/callerid.pl b/web/bin/callerid.pl index 6bb0ce1fc..5788679f7 100644 --- a/web/bin/callerid.pl +++ b/web/bin/callerid.pl @@ -35,8 +35,7 @@ sub web_callerid_list { \n$html Use this page to review or update your $::config_parms{caller_id_file} file.|; - $html .= - qq|
    Read-Only: Login as admin| + $html .= qq|
    Read-Only:
    Login as admin| unless ( ( $Authorized eq $authorized ) or ( $Authorized eq 'admin' ) ); $html .= qq|or $authorized| if $authorized @@ -64,7 +63,7 @@ sub web_callerid_list { $c_number = $request{cidnumber} if $request{cidnumber} ne ''; $c_name = $request{cidname} if $request{cidname} ne ''; - $c_wav = $Caller_ID::wav_by_number{$c_number} + $c_wav = $Caller_ID::wav_by_number{$c_number} if $Caller_ID::wav_by_number{$c_number} ne ''; $c_group = $Caller_ID::group_by_number{$c_number} if $Caller_ID::group_by_number{$c_number} ne ''; @@ -77,8 +76,7 @@ sub web_callerid_list { #display help on top, not pop-up to prevent audrey from dying if ($display_help) { - $html .= - "

    HELP Information on $display_help field:
    "; + $html .= "

    HELP Information on $display_help field:
    "; $html .= &web_callerid_help($display_help); $html .= "
    \n"; @@ -88,8 +86,7 @@ sub web_callerid_list { $html .= "
    $Item $ItemCount{"$Item"}$Item $ItemCount{"$Item"}
    \n"; $html .= ""; for my $header ( '', @headers ) { - $html .= - qq[]; } @@ -145,13 +142,11 @@ sub web_callerid_list { my $headers = @headers; $html .= "
    $header
    \n"; + $html .= "(back to top)\n"; $html .= ""; for my $header ( '', @headers ) { - $html .= - qq[]; + $html .= qq[]; } $html .= "\n"; @@ -167,10 +162,7 @@ sub web_callerid_list { or ( $Authorized eq 'admin' ) ); $html .= " "; for my $field ( 0 .. $headers - 1 ) { - $html .= &html_form_input_set_func( - 'web_callerid_set_field', "/bin/callerid.pl", - "$pos,$field", $cid_info[$field] - ); + $html .= &html_form_input_set_func( 'web_callerid_set_field', "/bin/callerid.pl", "$pos,$field", $cid_info[$field] ); } $html .= "\n"; } @@ -251,15 +243,11 @@ sub web_callerid_help { my ($field) = @_; my %help = ( - Number => - 'Telephone number, without formatting or spaces. Older format may use dashes.', - Name => - 'The caller ID name of the entry (what will be announced and logged)', - Group => - 'The caller ID group this entry belongs to. Add to the "rejected" group for some special processing. :)', - Wav => - 'Location and name of a wav file to play over the PA, rather than announcing the callers name.', - Data => 'Caller ID name or WAV file to play. (older format)' + Number => 'Telephone number, without formatting or spaces. Older format may use dashes.', + Name => 'The caller ID name of the entry (what will be announced and logged)', + Group => 'The caller ID group this entry belongs to. Add to the "rejected" group for some special processing. :)', + Wav => 'Location and name of a wav file to play over the PA, rather than announcing the callers name.', + Data => 'Caller ID name or WAV file to play. (older format)' ); my $help = $help{$field}; diff --git a/web/bin/code_search.pl b/web/bin/code_search.pl index 599d5b2f1..260729c6e 100644 --- a/web/bin/code_search.pl +++ b/web/bin/code_search.pl @@ -18,8 +18,7 @@ my $font_size = ( &http_agent_size < 800 ) ? 1 : 3; -$results = - qq[
    Searching for "$string" in code scripts:

    $results
    ]; +$results = qq[
    Searching for "$string" in code scripts:

    $results
    ]; return &html_page( '', $results, ' ' ); diff --git a/web/bin/code_select.pl b/web/bin/code_select.pl index aa375efda..da582cf06 100644 --- a/web/bin/code_select.pl +++ b/web/bin/code_select.pl @@ -39,8 +39,8 @@ sub select_code_form { modifications, but which may require settings in your ini file to activate properly.|; $html .= qq| Simply check those that you'd like to run and they'll be automatically activated within MisterHouse.| if $Authorized eq 'admin'; - my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); - + my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); + $html .= qq|
    Read-Only: Login as admin to edit| unless $Authorized eq 'admin'; $html .= qq|
    @@ -50,13 +50,10 @@ sub select_code_form { $html .= " Searching for: \"$search\"" if $search; $html .= ""; - my %files_selected = map { $_, 1 } - &file_read( "$config_parms{data_dir}/$config_parms{code_select}", 2 ); - my %standard_parms = map { $_, 1 } - qw(html_dir code_dir code_dir_common data_dir http_port debug); + my %files_selected = map { $_, 1 } &file_read( "$config_parms{data_dir}/$config_parms{code_select}", 2 ); + my %standard_parms = map { $_, 1 } qw(html_dir code_dir code_dir_common data_dir http_port debug); opendir( MYDIR, $config_parms{code_dir_common} ) - or return - "Error, can not open directory $config_parms{code_dir_common}.\n"; + or return "Error, can not open directory $config_parms{code_dir_common}.\n"; my @files_read = readdir(MYDIR); close MYDIR; my ( %modules, %categories ); @@ -82,11 +79,7 @@ sub select_code_form { } next if $search - and !( - $category =~ /$search/i - or $file =~ /$search/i - or $description =~ /$search/i - ); + and !( $category =~ /$search/i or $file =~ /$search/i or $description =~ /$search/i ); if (%file_parms) { my $description_part; @@ -95,20 +88,17 @@ sub select_code_form { $description_part .= ", "; $parms_list .= ','; } - $description_part .= - ""; + $description_part .= ""; if ( $config_parms{$f_parm} ) { $description_part .= "$f_parm"; } else { - $description_part .= - "$f_parm"; + $description_part .= "$f_parm"; } $description_part .= ""; $parms_list .= $f_parm; } - $description .= - "
    EDIT" + $description .= "
    EDIT" if ( $Authorized eq 'admin' ); $description .= " Config parms: $description_part"; } @@ -137,8 +127,7 @@ sub select_code_form { for my $module ( sort { lc($a) cmp lc($b) } keys %modules ) { my ($category) = $module =~ /(.*)\|.*/; if ( $category ne $lastcategory ) { - $html .= - "
    "; + $html .= ""; $lastcategory = $category; $rowcount = 0; } @@ -166,8 +155,7 @@ sub select_code_update { # Allow un-authorized users to browse, but only admin users to update return 'Not authorized to make updates' unless $Authorized eq 'admin'; - my %modules_selected = map { $_, 1 } - &file_read( "$config_parms{data_dir}/$config_parms{code_select}", 2 ); + my %modules_selected = map { $_, 1 } &file_read( "$config_parms{data_dir}/$config_parms{code_select}", 2 ); my ( %modules_added, %modules_dropped, %modules_deleted ); @@ -206,22 +194,16 @@ sub select_code_update { my $file = "$config_parms{data_dir}/$config_parms{code_select}"; &file_backup($file); &file_write( $file, - "# This file is auto-generated by web code_select.pl.\n" - . "# It lists all files you have selected from the common code dir\n" - . $modules_selected ); + "# This file is auto-generated by web code_select.pl.\n" . "# It lists all files you have selected from the common code dir\n" . $modules_selected ); run_voice_cmd "reload code" if $modules_added or $modules_dropped; $html = "

    MisterHouse Code Activation Confirmation

    "; - $html .= - "The following code files are now activated:

    $modules_added
    "; - $html .= - "The following code files are now de-activated:

    $modules_dropped
    "; - $html .= - "The following code were deleted:

    $modules_deleted
    " + $html .= "The following code files are now activated:

    $modules_added
    "; + $html .= "The following code files are now de-activated:

    $modules_dropped
    "; + $html .= "The following code were deleted:

    $modules_deleted
    " if $modules_deleted; - $html .= - "Here are all your activated files:

    $modules_selected
    "; + $html .= "Here are all your activated files:

    $modules_selected
    "; return &html_page( '', $html ); } diff --git a/web/bin/code_unselect.pl b/web/bin/code_unselect.pl index 1269e5253..6fb06624d 100644 --- a/web/bin/code_unselect.pl +++ b/web/bin/code_unselect.pl @@ -36,8 +36,7 @@ sub select_code_form { $html The following fils are all the code files in you code_dir list: $config_parms{code_dir}.|; - $html .= - qq|
    Read-Only: Login as admin to edit| + $html .= qq|
    Read-Only: Login as admin to edit| unless $Authorized eq 'admin'; $html .= qq|Simply uncheck files you want to disable or check to re-enable.| if $Authorized eq 'admin'; @@ -49,10 +48,8 @@ sub select_code_form { $html .= " Searching for: \"$search\"" if $search; $html .= ""; - my %files_deselected = map { $_, 1 } - &file_read( "$config_parms{data_dir}/$config_parms{code_unselect}", 2 ); - my %standard_parms = map { $_, 1 } - qw(html_dir code_dir code_dir_common data_dir http_port debug); + my %files_deselected = map { $_, 1 } &file_read( "$config_parms{data_dir}/$config_parms{code_unselect}", 2 ); + my %standard_parms = map { $_, 1 } qw(html_dir code_dir code_dir_common data_dir http_port debug); my @files_read; for my $file_dir (@Code_Dirs) { @@ -62,8 +59,7 @@ sub select_code_form { unless $file_dir eq $config_parms{code_dir_common}; close MYDIR; } - @files_read = grep( /^[a-z0-9].*\.(pl|mhp)$/i, @files_read ) - ; # Must start with alphanumeric ... emacs edited checkpoints can start with # + @files_read = grep( /^[a-z0-9].*\.(pl|mhp)$/i, @files_read ); # Must start with alphanumeric ... emacs edited checkpoints can start with # my ( %modules, %categories ); @@ -99,11 +95,7 @@ sub select_code_form { } next if $search - and !( - $category =~ /$search/i - or $file =~ /$search/i - or $description =~ /$search/i - ); + and !( $category =~ /$search/i or $file =~ /$search/i or $description =~ /$search/i ); if (%file_parms) { my $description_part; @@ -112,20 +104,17 @@ sub select_code_form { $description_part .= ", "; $parms_list .= ','; } - $description_part .= - ""; + $description_part .= ""; if ( $config_parms{$f_parm} ) { $description_part .= "$f_parm"; } else { - $description_part .= - "$f_parm"; + $description_part .= "$f_parm"; } $description_part .= ""; $parms_list .= $f_parm; } - $description .= - "
    EDIT Config parms: $description_part"; + $description .= "
    EDIT Config parms: $description_part"; } $categories{$category}++; @@ -153,8 +142,7 @@ sub select_code_form { for my $module ( sort { lc($a) cmp lc($b) } keys %modules ) { my ($category) = $module =~ /(.*)\|.*/; if ( $category ne $lastcategory ) { - $html .= - "
    "; + $html .= ""; $lastcategory = $category; $rowcount = 0; } @@ -182,8 +170,7 @@ sub select_code_update { # Allow un-authorized users to browse only (if listed in password_allow) return 'Not authorized to make updates' unless $Authorized eq 'admin'; - my %modules_deselected = map { $_, 1 } - &file_read( "$config_parms{data_dir}/$config_parms{code_unselect}", 2 ); + my %modules_deselected = map { $_, 1 } &file_read( "$config_parms{data_dir}/$config_parms{code_unselect}", 2 ); my ( %modules_added, %modules_dropped, %modules_selected ); @@ -225,14 +212,10 @@ sub select_code_update { run_voice_cmd "reload code" if $modules_added or $modules_dropped; $html = "

    MisterHouse Code Activation Confirmation

    "; - $html .= - "The following code files are now activated:

    $modules_added
    "; - $html .= - "The following code files are now de-activated:

    $modules_dropped
    "; - $html .= - "Here are all your deactivated files:

    $modules_deselected
    "; - $html .= - "Here are all your activated files:

    $modules_selected
    "; + $html .= "The following code files are now activated:

    $modules_added
    "; + $html .= "The following code files are now de-activated:

    $modules_dropped
    "; + $html .= "Here are all your deactivated files:

    $modules_deselected
    "; + $html .= "Here are all your activated files:

    $modules_selected
    "; return &html_page( '', $html ); } diff --git a/web/bin/command_search.pl b/web/bin/command_search.pl index e7208b2c2..4a32d9973 100644 --- a/web/bin/command_search.pl +++ b/web/bin/command_search.pl @@ -14,12 +14,9 @@ my ($string) = @ARGV; $string =~ s/search=//; # Allow for ?string or ?search=string -my $html = &html_header( - "Search results for: $string    " - . &html_authorized ); +my $html = &html_header( "Search results for: $string    " . &html_authorized ); -$html .= qq|Search String:| - . qq||; +$html .= qq|
    Search String:| . qq||; $html .= &search_commands($string); @@ -35,8 +32,7 @@ sub search_commands { # Now find object name my ( $file, $cmd2 ) = $cmd =~ /(.+)\:(.+)/; - my ( $object, $said, $vocab_cmd ) = - &Voice_Cmd::voice_item_by_text( lc $cmd2 ); + my ( $object, $said, $vocab_cmd ) = &Voice_Cmd::voice_item_by_text( lc $cmd2 ); my $object_name = $object->{object_name}; next if $seen{$object_name}++; push @object_list, $object_name; diff --git a/web/bin/counter.pl b/web/bin/counter.pl index 2f0cb9d02..5e7805d50 100644 --- a/web/bin/counter.pl +++ b/web/bin/counter.pl @@ -11,11 +11,7 @@ my @parms = @ARGV; for my $parm (@parms) { if ( $parm =~ /new/ ) { - return - "HTTP/1.0 200 OK\n\n" - . '

    Page Views: ' - . ++$Save{"web_count_$page"} - . "

    \n"; + return "HTTP/1.0 200 OK\n\n" . '

    Page Views: ' . ++$Save{"web_count_$page"} . "

    \n"; } } return "HTTP/1.0 200 OK\n\n" . 'Page Views: ' . ++$Save{"web_count_$page"}; diff --git a/web/bin/dbmedit.cgi b/web/bin/dbmedit.cgi index 20ee608bc..643f46d1e 100755 --- a/web/bin/dbmedit.cgi +++ b/web/bin/dbmedit.cgi @@ -241,8 +241,7 @@ elsif ( $in{'cmd'} eq 'delete' ) { } else { - &HTMLdie( "The command $safein{'cmd'} is not supported.", - "Command not supported" ); + &HTMLdie( "The command $safein{'cmd'} is not supported.", "Command not supported" ); } @@ -283,9 +282,7 @@ sub updaterecord { unless ( $in{'confirm'} ) { unless ( defined( $dbdata{ $in{'key'} } ) ) { - &verifycmd( "That record has apparently been deleted recently. " - . "Would you like to add it back with the values you just " - . "entered?" ); + &verifycmd( "That record has apparently been deleted recently. " . "Would you like to add it back with the values you just " . "entered?" ); } if ( $in{'time'} && $in{'time'} < ( stat($dbfilename) )[9] ) { @@ -406,10 +403,8 @@ sub calcglobals { @safein{ keys %in } = map { &HTMLescape($_) } values %in; # Save database definition to send to script again - $dbdefnget = - &urlencodelist( &subhash( *in, qw(file delim columns referer) ) ); - $dbdefnpost = - &hiddenvars( &subhash( *in, qw(file delim columns referer) ) ); + $dbdefnget = &urlencodelist( &subhash( *in, qw(file delim columns referer) ) ); + $dbdefnpost = &hiddenvars( &subhash( *in, qw(file delim columns referer) ) ); $delim = ( $in{'delim'} =~ /\d/ ) @@ -557,8 +552,7 @@ EOF } print "
    $type\n"; - $html .= - "(back to top)
    $header$header
    $category (back to top)$submit_html
    $category (back to top)$submit_html
    $category (back to top)$submit_html
    $category (back to top)$submit_html
    \n\n"; - print - "

    * Backslashes indicate escaped characters, as in Perl.\n\n" + print "

    * Backslashes indicate escaped characters, as in Perl.\n\n" if $has_backslashes; print < 1 ? $maxheight[$_] + 1 : 2; $height = $height < 10 ? $height : 10; - print - qq(

    $safefieldta
    $safefieldta
    $safefield[$_]
    $safefield[$_]
    \n"; - for ( my $x = 0; - $x <= $l_xmax; - $x++ ) #initialize table with (hopefully) accurate sizing + $l_html .= "
    \n"; + for ( my $x = 0; $x <= $l_xmax; $x++ ) #initialize table with (hopefully) accurate sizing { $l_html .= ""; } - for ( my $y = 0; - $y <= $l_ymax; - $y++ ) #Create HTML Table stucture of Virtual Frame buffer + for ( my $y = 0; $y <= $l_ymax; $y++ ) #Create HTML Table stucture of Virtual Frame buffer { $l_html .= "\n"; - $l_html .= - "\t\n"; for ( my $x = 0; $x <= $l_xmax; $x++ ) { $l_obj = $l_fp[$x][$y]; - if ( $l_obj ne "" ) { #Only do if object is at coordinates + if ( $l_obj ne "" ) { #Only do if object is at coordinates if ( $l_rendered{$l_obj} eq '' ) { $l_rendered{$l_obj} = 1; - ( $l_x, $l_y, $l_w, $l_h ) = - $l_obj->get_fp_location(); + ( $l_x, $l_y, $l_w, $l_h ) = $l_obj->get_fp_location(); if ( $l_x eq '' ) { $l_x = 1; } if ( $l_y eq '' ) { $l_y = 1; } if ( $l_w eq '' ) { $l_w = 1; } if ( $l_h eq '' ) { $l_h = 1; } - if ( $l_obj->isa('Group') ) { #recurse groups + if ( $l_obj->isa('Group') ) { #recurse groups $l_html .= - "\t\n"; } } - else { #Blank space - $l_html .= - "\t\n"; } } @@ -335,11 +309,7 @@ sub web_fp_item #render all items based on type if ( $l_state ne '' ) { my ($l_str) = $l_text =~ /\$(.*)/; - $l_html .= - ""; + $l_html .= ""; } if ( $l_image ne '' ) { $l_html .= "$l_text"; @@ -353,17 +323,15 @@ sub web_fp_item #render all items based on type return $l_html; } -sub web_fp_idle_color #Fade color from acolor to bcolor over idle_time of object +sub web_fp_idle_color #Fade color from acolor to bcolor over idle_time of object { my ( $p_object, $p_acolor, $p_bcolor, $p_maxtime ) = @_; my ( $l_ared, $l_agreen, $l_ablue ) = $p_acolor =~ /#*(..)(..)(..)/; my ( $l_bred, $l_bgreen, $l_bblue ) = $p_bcolor =~ /#*(..)(..)(..)/; - ( $l_ared, $l_agreen, $l_ablue ) = - ( hex $l_ared, hex $l_agreen, hex $l_ablue ); - ( $l_bred, $l_bgreen, $l_bblue ) = - ( hex $l_bred, hex $l_bgreen, hex $l_bblue ); + ( $l_ared, $l_agreen, $l_ablue ) = ( hex $l_ared, hex $l_agreen, hex $l_ablue ); + ( $l_bred, $l_bgreen, $l_bblue ) = ( hex $l_bred, hex $l_bgreen, hex $l_bblue ); my ( $l_red, $l_green, $l_blue ); my $l_time; @@ -401,10 +369,7 @@ sub web_fp_idle_color #Fade color from acolor to bcolor over idle_time of object $l_blue = int( $l_ablue + ( ( $l_bblue - $l_ablue ) * ($l_percent) ) ); } - return ("#" - . sprintf( "%02X", $l_red ) - . sprintf( "%02X", $l_green ) - . sprintf( "%02X", $l_blue ) ); + return ( "#" . sprintf( "%02X", $l_red ) . sprintf( "%02X", $l_green ) . sprintf( "%02X", $l_blue ) ); } sub web_fp_camera_popup { diff --git a/web/bin/floorplan_svg.pl b/web/bin/floorplan_svg.pl index a1c123ab8..f10351300 100644 --- a/web/bin/floorplan_svg.pl +++ b/web/bin/floorplan_svg.pl @@ -112,8 +112,7 @@ sub web_fp #render table representation of objects and their co-ordinates my $l_bcolor = '#CCCCCC'; my $l_acolor = '#00FF00'; - my $title_room = $svg->text( id => "title", x => 50, y => 75 ) - ->cdata( web_fp_filter_name($object_name) ); + my $title_room = $svg->text( id => "title", x => 50, y => 75 )->cdata( web_fp_filter_name($object_name) ); my $y = $svg->group( id => 'group_y', style => { stroke => 'black', fill => 'white' } @@ -128,11 +127,9 @@ sub web_fp #render table representation of objects and their co-ordinates # It was 10, I'm not sure that 12 is correct # times 10, the rooms are given in feet (I guess) $l_x *= 12; - $l_x += $xOffset - ; # Corrective offset to move it of the right edge of the display area + $l_x += $xOffset; # Corrective offset to move it of the right edge of the display area $l_y *= 12; - $l_y += $yOffset - ; # Corrective offset to move it of the top edge of the display area + $l_y += $yOffset; # Corrective offset to move it of the top edge of the display area $l_w *= 12; $l_h *= 12; @@ -153,7 +150,7 @@ sub web_fp #render table representation of objects and their co-ordinates )->cdata( web_fp_filter_name( $obj->{object_name} ) ); $i++; } - @n_objs = @{ $$obj{members} }; # This is the Devices within the Room + @n_objs = @{ $$obj{members} }; # This is the Devices within the Room for my $item (@n_objs) { my ( $width, $height ); my $ob = Ob($item); @@ -204,8 +201,7 @@ sub web_fp #render table representation of objects and their co-ordinates else { ( $l_x, $l_y, $l_w, $l_h ) = $p_obj->get_fp_location(); my ( $l_text, $l_state, $l_image ) = web_fp_item($p_obj); - $svg->text( x => $l_x + 6, y => $l_y + 6 ) - ->cdata( web_fp_filter_name($l_text) ); + $svg->text( x => $l_x + 6, y => $l_y + 6 )->cdata( web_fp_filter_name($l_text) ); $svg->image( x => $l_x, y => $l_y, diff --git a/web/bin/headercontrol.pl b/web/bin/headercontrol.pl index 3b06dd4ea..887de2ef7 100644 --- a/web/bin/headercontrol.pl +++ b/web/bin/headercontrol.pl @@ -17,8 +17,7 @@ &save_header_control_list(@parms); } -my $phone_dir = - "$config_parms{data_dir}/headerallow.tab"; # Avoid redefined sub msgs +my $phone_dir = "$config_parms{data_dir}/headerallow.tab"; # Avoid redefined sub msgs my $form_type = &html_form_select( 'type', 0, 'True', 'True', 'False', ); return &headercontrol_list(); my $GuestValueTrue; @@ -39,8 +38,7 @@ sub headercontrol_list { my $html_calls; my @calls = &read_headercontrol_list; for my $r (@calls) { - my ( $HeaderName, $family, $guest ) = - $r =~ /Name=(.*) Family=(.*) Guest=(.*)/; + my ( $HeaderName, $family, $guest ) = $r =~ /Name=(.*) Family=(.*) Guest=(.*)/; #print "$HeaderName, $family, $guest\n"; if ( $Authorized eq 'admin' ) { @@ -131,8 +129,7 @@ sub save_header_control_list { #print_log "counter is $counter:$writedata"; #now we need to write this to the file - my ( $HeaderToWrite, $FamilyValue, $junk, $GuestValue ) = - $writedata =~ /(.*)=(true|false)(.*)=(true|false)/i; + my ( $HeaderToWrite, $FamilyValue, $junk, $GuestValue ) = $writedata =~ /(.*)=(true|false)(.*)=(true|false)/i; #print_log "Header:$HeaderToWrite, $FamilyValue, $GuestValue"; my $WritingData = "$HeaderToWrite\t$FamilyValue\t$GuestValue"; @@ -194,8 +191,7 @@ sub read_headercontrol_list { #ok, we have the number and name is filled with the rest, lets break up name a little #print_log "HeaderName;$HeaderName, Family;$family, Guest;$guest\n"; - push @header, - sprintf( "Name=%s Family=%s Guest=%s", $HeaderName, $family, $guest ); + push @header, sprintf( "Name=%s Family=%s Guest=%s", $HeaderName, $family, $guest ); } close MYFILE; diff --git a/web/bin/hvac.pl b/web/bin/hvac.pl index 1c798e72c..c47b3ddc3 100644 --- a/web/bin/hvac.pl +++ b/web/bin/hvac.pl @@ -68,8 +68,7 @@ sub hvac_ask { $html .= "
    "; + $l_html .= "\t"; $l_html .= ""; + "\t"; $l_html .= web_fp($l_obj); } else { - $l_html .= "\t"; + $l_html .= "\t"; $l_html .= web_fp_item($l_obj); } $l_html .= ""; + else { #Blank space + $l_html .= "\t"; $l_html .= "
    "; if ( $state eq 'hold' ) { - $html .= - '

    Click here to cancel the hold.

    '; + $html .= '

    Click here to cancel the hold.

    '; } else { $html .= qq| diff --git a/web/bin/icon_auth.pl b/web/bin/icon_auth.pl index 4a52bb3ea..81c8fb194 100644 --- a/web/bin/icon_auth.pl +++ b/web/bin/icon_auth.pl @@ -18,7 +18,4 @@ : "/SET_PASSWORD?user=$Authorized"; #print "\ndbx a=$Authorized i=$icon a=$action\n"; -return - ""
-  . ( ($Authorized) ? "; +return "" . ( ($Authorized) ? "; diff --git a/web/bin/icon_mode.pl b/web/bin/icon_mode.pl index 1f89b83ee..d53c8fc58 100644 --- a/web/bin/icon_mode.pl +++ b/web/bin/icon_mode.pl @@ -3,6 +3,5 @@ # and cycle them when users click on it. # Is being called from modes/main.shtml. -return - "$Save{mode} Mode"; +return "$Save{mode} Mode"; diff --git a/web/bin/iniedit.pl b/web/bin/iniedit.pl index 33ecb1591..7440532f3 100644 --- a/web/bin/iniedit.pl +++ b/web/bin/iniedit.pl @@ -46,8 +46,7 @@ return localize() if $testing and !@ARGV; # For testing only -my ( %dparms, %cparms, @order, %pparms, %categories, @catorder, %changed, - %help ); +my ( %dparms, %cparms, @order, %pparms, %categories, @catorder, %changed, %help ); my $private_parms = $Pgm_Path . "/mh.private.ini"; $private_parms = $ENV{mh_parms} if $ENV{mh_parms}; @@ -131,15 +130,11 @@ sub commit { # print_log "Error in backup up parms file $private_parms: $!"; open( PRIVATE, ">$private_parms" ) - or return $head - . "\nError, could not open $private_parms for writing.

    \n" - . $tail; - print PRIVATE - "# Misterhouse configuration file generated by iniedit.pl\n\n"; + or return $head . "\nError, could not open $private_parms for writing.

    \n" . $tail; + print PRIVATE "# Misterhouse configuration file generated by iniedit.pl\n\n"; my $data; if ( defined $args{edit_list} ) { - $data = - 'Close'; + $data = 'Close'; } else { $data = 'Back'; @@ -168,8 +163,7 @@ sub commit { sub edit { my $data = '
    '; - $data .= - 'Read-Only: Login as admin to edit
    ' + $data .= 'Read-Only: Login as admin to edit
    ' unless $Authorized eq 'admin'; $data .= ' Note: Commit will resort and filter out comments.' @@ -184,15 +178,13 @@ sub edit { '; foreach ( @catorder, 'Other' ) { - $data .= - ' ' . $_ . "\n"; + $data .= ' ' . $_ . "\n"; } $data .= ' '; $data .= '  '; - $data .= - '' + $data .= '' if $Authorized eq 'admin'; $data .= '

    "; - $data .= - ''; - $data .= - '" + $data .= ( defined $dparms{$_} ? map_chars( $dparms{$_} ) : '' ) . "'\">" if $Authorized eq 'admin'; $data .= "
    "; $data .= '
    \n"; + $html .= "
    Which .mht file to edit?\n"; - $html .= &html_form_select( 'file', 1, $web_item_file_name, @file_paths ) - . "
    \n"; # Create form to add an item my $form_type = &html_form_select( - 'type', 0, - 'X10 Light (X10I)', 'Analog Sensor (ANALOG_SENSOR)', - 'AUDIOTRON', 'COMPOOL', - 'EIB Switch (EIB1)', 'EIB Switch Group (EIB1G)', - 'EIB Dimmer (EIB2)', 'EIB Value (EIB5)', - 'EIB Drive (EIB7)', 'GENERIC', - 'GROUP', 'IBUTTON', - 'INSTEON_PLM', 'INSTEON_LAMPLINC', - 'INSTEON_BULBLINC', 'INSTEON_APPLIANCELINC', - 'INSTEON_SWITCHLINC', 'INSTEON_SWITCHLINCRELAY', - 'INSTEON_KEYPADLINC', 'INSTEON_KEYPADLINCRELAY', - 'INSTEON_REMOTELINC', 'INSTEON_MOTIONSENSOR', - 'INSTEON_TRIGGERLINC', 'INSTEON_ICONTROLLER', - 'MP3PLAYER', 'One-Wire xAP Connector (OWX)', - 'RF', 'SERIAL', - 'SG485LCD', 'SG485RCSTHRM', - 'STARGATEDIN', 'STARGATEVAR', - 'STARGATEFLAG', 'STARGATERELAY', - 'STARGATETHERM', 'STARGATEPHONE', - 'VOICE', 'WEATHER', - 'X10 Appliance (X10A)', 'X10 Light (X10I)', - 'X10 Ote (X10O)', 'X10 SwitchLinc (X10SL)', - 'X10 Garage Door (X10G)', 'X10 Irrigation (X10S)', - 'X10 RCS (X10T)', 'X10 Motion Sensor (X10MS)', + 'type', 0, 'X10 Light (X10I)', 'Analog Sensor (ANALOG_SENSOR)', + 'AUDIOTRON', 'COMPOOL', 'EIB Switch (EIB1)', 'EIB Switch Group (EIB1G)', + 'EIB Dimmer (EIB2)', 'EIB Value (EIB5)', 'EIB Drive (EIB7)', 'GENERIC', + 'GROUP', 'IBUTTON', 'INSTEON_PLM', 'INSTEON_LAMPLINC', + 'INSTEON_BULBLINC', 'INSTEON_APPLIANCELINC', 'INSTEON_SWITCHLINC', 'INSTEON_SWITCHLINCRELAY', + 'INSTEON_KEYPADLINC', 'INSTEON_KEYPADLINCRELAY', 'INSTEON_REMOTELINC', 'INSTEON_MOTIONSENSOR', + 'INSTEON_TRIGGERLINC', 'INSTEON_ICONTROLLER', 'MP3PLAYER', 'One-Wire xAP Connector (OWX)', + 'RF', 'SERIAL', 'SG485LCD', 'SG485RCSTHRM', + 'STARGATEDIN', 'STARGATEVAR', 'STARGATEFLAG', 'STARGATERELAY', + 'STARGATETHERM', 'STARGATEPHONE', 'VOICE', 'WEATHER', + 'X10 Appliance (X10A)', 'X10 Light (X10I)', 'X10 Ote (X10O)', 'X10 SwitchLinc (X10SL)', + 'X10 Garage Door (X10G)', 'X10 Irrigation (X10S)', 'X10 RCS (X10T)', 'X10 Motion Sensor (X10MS)', 'X10 6 Button Remote (X106BUTTON)', 'XANTECH', ); @@ -134,10 +118,8 @@ sub web_items_list { } # Add an index - $html .= - ""; + $html_calls .= ""; # $html_calls .= ""; $html_calls .= diff --git a/web/bin/phone_in_old.pl b/web/bin/phone_in_old.pl index ee79c3810..6e420745e 100644 --- a/web/bin/phone_in_old.pl +++ b/web/bin/phone_in_old.pl @@ -6,8 +6,7 @@ for my $r (@calls) { my ( $time, $num, $name ) = $r =~ /(.+\d+:\d+:\d+) (\S+) (.+)/; next unless $num; - $html_calls .= - ""; + $html_calls .= ""; } #my $html_calls; diff --git a/web/bin/phone_list.pl b/web/bin/phone_list.pl index fb436846c..d31ef4607 100644 --- a/web/bin/phone_list.pl +++ b/web/bin/phone_list.pl @@ -24,8 +24,7 @@ sub rejected_call_list { my $html_calls; my @calls = &read_reject_call_list(); for my $r (@calls) { - my ( $number, $name, $sound, $type ) = - $r =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; + my ( $number, $name, $sound, $type ) = $r =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; $html_calls .= ""; $pos = $pos + 1; @@ -49,9 +48,7 @@ sub rejected_call_list {
    Which .mht file to edit?\n"; + $html .= &html_form_select( 'file', 1, $web_item_file_name, @file_paths ) . "
    Refresh\n"; - $html .= - "  ReLoad Code \n"; + $html .= "
    Refresh\n"; + $html .= "  ReLoad Code \n"; $html .= "Item Index: \n"; for my $type ( sort keys %item_pos ) { $html .= "$type\n"; @@ -146,31 +128,30 @@ sub web_items_list { # Define fields by type my %headers = ( - ANALOG_SENSOR => - [ 'Identifier', 'Name', 'Conduit', 'Groups', 'Type', 'Tokens' ], - EIB1 => [ 'Address', 'Name', 'Groups', 'Mode' ], - EIB1G => [ 'Address', 'Name', 'Groups', 'Addresses' ], - EIB2 => [ 'Address', 'Name', 'Groups' ], - EIB5 => [qw(Address Name Groups Mode)], - EIB7 => [ 'Address', 'Name', 'Groups' ], - GENERIC => [qw(Name Groups)], - GROUP => [qw(Name FloorPlan Groups)], - IBUTTON => [qw(ID Name Port Channel)], - SERIAL => [qw(String Name Groups State Port)], - VOICE => [qw(Item Phrase)], - X10A => [qw(Address Name Groups Interface)], - X10I => [qw(Address Name Groups Interface Options)], - X10SL => [qw(Address Name Groups Interface Options)], - X10MS => [qw(Address Name Groups Type)], - X106BUTTON => [qw(Address Name)], - UPBPIM => [qw(Name NetworkID Password Address)], - UPBD => [qw(Name Interface NetworkID Address Groups)], - UPBL => [qw(Name Interface NetworkID Address Groups)], - INSTEON_PLM => [qw(Name)], - INSTEON_LAMPLINC => [qw(Address Name Groups)], - INSTEON_BULBLINC => [qw(Address Name Groups)], - INSTEON_APPLIANCELINC => [qw(Address Name Groups)], - INSTEON_SWITCHLINC => [qw(Address Name Groups)], + ANALOG_SENSOR => [ 'Identifier', 'Name', 'Conduit', 'Groups', 'Type', 'Tokens' ], + EIB1 => [ 'Address', 'Name', 'Groups', 'Mode' ], + EIB1G => [ 'Address', 'Name', 'Groups', 'Addresses' ], + EIB2 => [ 'Address', 'Name', 'Groups' ], + EIB5 => [qw(Address Name Groups Mode)], + EIB7 => [ 'Address', 'Name', 'Groups' ], + GENERIC => [qw(Name Groups)], + GROUP => [qw(Name FloorPlan Groups)], + IBUTTON => [qw(ID Name Port Channel)], + SERIAL => [qw(String Name Groups State Port)], + VOICE => [qw(Item Phrase)], + X10A => [qw(Address Name Groups Interface)], + X10I => [qw(Address Name Groups Interface Options)], + X10SL => [qw(Address Name Groups Interface Options)], + X10MS => [qw(Address Name Groups Type)], + X106BUTTON => [qw(Address Name)], + UPBPIM => [qw(Name NetworkID Password Address)], + UPBD => [qw(Name Interface NetworkID Address Groups)], + UPBL => [qw(Name Interface NetworkID Address Groups)], + INSTEON_PLM => [qw(Name)], + INSTEON_LAMPLINC => [qw(Address Name Groups)], + INSTEON_BULBLINC => [qw(Address Name Groups)], + INSTEON_APPLIANCELINC => [qw(Address Name Groups)], + INSTEON_SWITCHLINC => [qw(Address Name Groups)], INSTEON_SWITCHLINCRELAY => [qw(Address Name Groups)], INSTEON_KEYPADLINC => [qw(Address Name Groups)], INSTEON_KEYPADLINCRELAY => [qw(Address Name Groups)], @@ -179,7 +160,7 @@ sub web_items_list { INSTEON_TRIGGERLINC => [qw(Address Name Groups)], INSTEON_ICONTROLLER => [qw(Address Name Groups)], SCENE_MEMBER => [qw(MemberName LinkName OnLevel RampRate)], - CODE => [qw(Code)], + CODE => [qw(Code)], default => [qw(Address Name Groups Other)] ); @@ -192,12 +173,11 @@ sub web_items_list { $html .= "
    \n"; - $headers--; - + $headers--; + $html .= ""; for my $header ( '', 'Type', @headers ) { - $html .= - qq[]; + $html .= qq[]; # $html .= " "; } @@ -211,16 +191,12 @@ sub web_items_list { $html .= " "; $html .= " "; for my $field ( 1 .. $headers - 1 ) { - $html .= &html_form_input_set_func( - 'web_item_set_field', "/bin/items.pl", - "$pos,$field", $item_info[$field] - ); + $html .= &html_form_input_set_func( 'web_item_set_field', "/bin/items.pl", "$pos,$field", $item_info[$field] ); } $html .= "\n"; } @@ -326,9 +302,8 @@ sub web_item_add { $other2 =~ s/,$//; # write out new record to mht file - $file_data[@file_data] = sprintf( "%-20s%-20s%-20s%-20s%-20s%s", - $type, $address, $name, $group, $other1, $other2 ); - + $file_data[@file_data] = sprintf( "%-20s%-20s%-20s%-20s%-20s%s", $type, $address, $name, $group, $other1, $other2 ); + #&main::print_log("DB: in webitem, $type, $address, $name, $group, $other1, $other2"); &mht_item_file_write( $web_item_file_name, \@file_data ); @@ -346,21 +321,17 @@ sub web_item_help { Groups => 'List of groups the item belongs to, seperated by |', Interface => 'The X10 Interface to use (e.g. CM17, CM11)', X10_Type => 'The type of X10 device (e.g. LM14 or preset)', - String => - 'The serial characters to match (e.g. XA1A1 to match 2 A1 button pushes)', - State => 'The state name to correlate to this items serial String', - Port => 'Which port to look for the serial data on', - ID => - 'Ibutton ID: type|serial|crc crc is optional. type=01 (1990) type=10 (1820)', - Channel => 'When using a switch, choose channel A or B', - Item => 'Item name to tie this voice command to', - Phrase => 'Voice_Cmd Text', - Mode => - '\'R\' (readable): generate read request to learn current state at initialization', - Options => - 'List the device options separated by | (e.g. preset, resume=80)', + String => 'The serial characters to match (e.g. XA1A1 to match 2 A1 button pushes)', + State => 'The state name to correlate to this items serial String', + Port => 'Which port to look for the serial data on', + ID => 'Ibutton ID: type|serial|crc crc is optional. type=01 (1990) type=10 (1820)', + Channel => 'When using a switch, choose channel A or B', + Item => 'Item name to tie this voice command to', + Phrase => 'Voice_Cmd Text', + Mode => '\'R\' (readable): generate read request to learn current state at initialization', + Options => 'List the device options separated by | (e.g. preset, resume=80)', FloorPlan => 'Floor Plan location', - Code => 'Perl code executed at startup', + Code => 'Perl code executed at startup', Other => 'Other stuff :)' ); diff --git a/web/bin/k8055.pl b/web/bin/k8055.pl index 62172e46f..49a4038ba 100644 --- a/web/bin/k8055.pl +++ b/web/bin/k8055.pl @@ -23,8 +23,7 @@ ]; if ( $param{autorefresh} > 0 ) { - $html .= - qq[\n]; + $html .= qq[\n]; } $html .= qq[ @@ -49,8 +48,7 @@ for ( $i = 1; $i <= $numCounters; $i++ ) { if ( $param{"c$i"} ne '' ) { $k8055->setDebounce( $i, $param{"c$i"} ); - push( @result, - "Setting Counter $i Debounce to " . $param{"c$i"} . " ms" ); + push( @result, "Setting Counter $i Debounce to " . $param{"c$i"} . " ms" ); } } } @@ -74,8 +72,7 @@ } elsif ( $param{action} eq 'autoupdate' ) { $k8055->setUpdatePeriod( $param{autoupdatetime} ); - push( @result, - "Setting auto-update period to $param{autoupdatetime} seconds" ); + push( @result, "Setting auto-update period to $param{autoupdatetime} seconds" ); } elsif ( $param{action} eq 'update' ) { $k8055->update(); @@ -147,8 +144,7 @@ ]; for ( $i = 1; $i <= $numAnalogueOutputPorts; $i++ ) { - $html .= - qq[

    Port $i:

    ]; + $html .= qq[

    Port $i:

    ]; } $html .= qq[

    \n]; @@ -161,8 +157,7 @@ $html .= "

    "; for ( $i = 1; $i <= $numCounters; $i++ ) { - $html .= - qq[

    Counter $i:

    ]; + $html .= qq[

    Counter $i:

    ]; } $html .= qq[

    \n]; diff --git a/web/bin/list_buttons.pl b/web/bin/list_buttons.pl index 09a3119a6..f48b0bedb 100644 --- a/web/bin/list_buttons.pl +++ b/web/bin/list_buttons.pl @@ -83,8 +83,7 @@ $image = "/bin/button.pl?$item&item&$state" unless -e $file; # $image = "/bin/button.pl?$item&item&$state" unless -e "$config_parms{html_dir}$image"; - $icon = - "$name $state"; + $icon = "$name $state"; # $icon = ""; $html .= " @@ -110,10 +109,7 @@ #$html = "\n\n" . if ($html_refrate) { - $htm_hdr = - "<\/head>\n"; + $htm_hdr = "<\/head>\n"; } else { $htm_hdr = "\n"; diff --git a/web/bin/list_buttons2.pl b/web/bin/list_buttons2.pl index 2e1bd609a..a242a2c8d 100644 --- a/web/bin/list_buttons2.pl +++ b/web/bin/list_buttons2.pl @@ -32,8 +32,7 @@ elsif ( $state ne 'on' and $state ne 'off' ) { $icon = 'dim'; # Need a new icon here } - $icon = - "$name $state"; + $icon = "$name $state"; $html .= " diff --git a/web/bin/list_categories.pl b/web/bin/list_categories.pl index 348635bba..9dc083e72 100644 --- a/web/bin/list_categories.pl +++ b/web/bin/list_categories.pl @@ -14,29 +14,24 @@ # Use custom icons if they exist my $image = "/graphics/category-$category2.gif"; if ( &http_get_local_file($image) ) { - $h .= - qq[$category2\n]; + $h .= qq[$category2\n]; } # Create buttons with GD module if available elsif ( $Info{module_GD} ) { my $name = &pretty_object_name($category); - $h .= - qq[$name\n]; + $h .= qq[$name\n]; } # Otherwise use text else { - $h .= - &html_active_href( "list?$category", &pretty_object_name($category) ) - . "\n"; + $h .= &html_active_href( "list?$category", &pretty_object_name($category) ) . "\n"; } $html .= $h . "\n"; $html .= "\n" unless ++$count % 3; } -$html = "\n\n" - . &html_header('Browse Categories') . " +$html = "\n\n" . &html_header('Browse Categories') . "
    $type\n"; $html .= "(back to top)
    $header$header$header"; $html .= "Copy" if $Authorized eq 'admin'; - $html .= - " Delete" + $html .= " Delete" if $Authorized eq 'admin'; $html .= "$item_info[0]
    diff --git a/web/bin/list_items.pl b/web/bin/list_items.pl index 45bd63e4e..97b4aa512 100644 --- a/web/bin/list_items.pl +++ b/web/bin/list_items.pl @@ -17,24 +17,19 @@ my $name = &pretty_object_name($object_type); if ( &http_get_local_file($image) ) { - $h .= - qq[$name\n]; + $h .= qq[$name\n]; } # Create buttons with GD module if available elsif ( $Info{module_GD} ) { # $name =~ s/ /%20/g; - $h .= - qq[$name\n]; + $h .= qq[$name\n]; } # Otherwise use text else { - $h .= - &html_active_href( "list?$object_type", - &pretty_object_name($object_type) ) - . "\n"; + $h .= &html_active_href( "list?$object_type", &pretty_object_name($object_type) ) . "\n"; } $html .= $h . "\n"; $html .= "\n" unless ++$count % 3; diff --git a/web/bin/list_widgets.pl b/web/bin/list_widgets.pl index 02fc72def..e03487d24 100644 --- a/web/bin/list_widgets.pl +++ b/web/bin/list_widgets.pl @@ -5,13 +5,8 @@ my ( $html, $count ); -for my $type ( - 'Widgets', 'Widgets_Label', - 'Widgets_Entry', 'Widgets_RadioButton', - 'Widgets_Checkbox', 'Vars_Global', - 'Vars_Save' - ) -{ +for my $type ( 'Widgets', 'Widgets_Label', 'Widgets_Entry', 'Widgets_RadioButton', 'Widgets_Checkbox', 'Vars_Global', 'Vars_Save' ) { + # Look for custom icon my $type2 = lc $type; $type2 =~ s/[: _\$]//g; @@ -27,8 +22,7 @@ # Create buttons with GD module if available elsif ( $Info{module_GD} ) { my $name = &pretty_object_name($type); - $h .= - qq[$name\n]; + $h .= qq[$name\n]; } # Otherwise use text diff --git a/web/bin/menu.pl b/web/bin/menu.pl index 139b24b7e..29d38f130 100644 --- a/web/bin/menu.pl +++ b/web/bin/menu.pl @@ -110,8 +110,7 @@ sub menu_list_top { $html .= &menu_button( $menu_group, "/bin/menu.pl?$menu_group" ); $html .= "    " unless $menu_buttons; } - $html .= - 'No menus defined. Enable menu.pl in the common code selector' + $html .= 'No menus defined. Enable menu.pl in the common code selector' unless %Menus > 1; return &html_top . $html; } @@ -137,8 +136,7 @@ sub menu_list { $menus_prev1 .= $menu_prev; # $html .= &menu_button($menu_prev, ($menu_prev eq $menus[-1]) ? '' : "/bin/menu.pl?$menu_group&$menus_prev1"); - $html .= - &menu_button( $menu_prev, "/bin/menu.pl?$menu_group&$menus_prev1" ); + $html .= &menu_button( $menu_prev, "/bin/menu.pl?$menu_group&$menus_prev1" ); } $html .= "
    \n"; @@ -187,16 +185,14 @@ sub menu_list { $href = "/bin/menu.pl?$menu_group&$menus|$goto"; $text = $goto; } - my $color = - ( $target eq 'speech' or $target eq 'main' ) ? 'blue' : 'white'; + my $color = ( $target eq 'speech' or $target eq 'main' ) ? 'blue' : 'white'; # Use an active button if 2 states and tied to an object my $states = @{ $$ptr2{Dstates} } if $$ptr2{Dstates}; my $action = $$ptr2{A}; if ( $states >= 1 and $action =~ /^ *set +(\$\S+)/ and eval "ref $1" ) { - $html .= - &menu_button2( $1, $text, $menu_group, $menus, $menu, $item ); + $html .= &menu_button2( $1, $text, $menu_group, $menus, $menu, $item ); } else { $html .= &menu_button( $text, $href, $target, $color ); @@ -220,22 +216,16 @@ sub menu_states { my $border = ( $buttons and $menu_buttons ) ? 0 : 1; # my $html = "
    \n"; - my $html = - "
    "; + my $html = "
    $$ptr2{Dprefix}
    "; my $state = 0; for my $state_name ( @{ $$ptr2{Dstates} } ) { my $href = "sub?menu_run($menu_group,$menu,$item,$state,h)"; $href =~ s/ /%20/g; if ($buttons) { - $html .= "\n"; + $html .= "\n"; } else { - $html .= - "\n"; + $html .= "\n"; } $state++; } @@ -314,8 +304,7 @@ sub menu_button2 { # my $href = "sub?menu_run($menu_group,$menu,$item,$state_next,hr,/bin/menu.pl?$menu_group&$menus)"; # &button_action is in mh/code/common/html_functions.pl # Need the function form, not the .pl form, since the argument has ? and & in it - my $href = - "sub?button_action($object_name,$state_next,/bin/menu.pl?$menu_group&$menus)"; + my $href = "sub?button_action($object_name,$state_next,/bin/menu.pl?$menu_group&$menus)"; $href =~ s/ /%20/g; return "$link\n"; } @@ -323,8 +312,7 @@ sub menu_button2 { sub key_pointer { $menu_key_index++; return unless $menu_keys; - return - ""; + return ""; } sub html_top { @@ -336,28 +324,17 @@ sub html_top { # $html =~ s||\n|; if ($menu_keys) { - my $script = "\n" - . "\n"; + my $script = "\n" . "\n"; $html =~ s//$script/; $html =~ s/body /body onload='load(); self.focus()' /; } $html .= "
    $$ptr2{Dprefix}" - . &menu_button( $state_name, $href, 'speech', 'blue' ) - . "" . &menu_button( $state_name, $href, 'speech', 'blue' ) . "$state_name " - . &key_pointer - . "$state_name " . &key_pointer . "
    \n"; - $html .= "\n" + $html .= "\n" if $menu_keys_allowed; - $html .= "\n"; - $html .= "\n"; - $html .= - "\n"; + $html .= "\n"; + $html .= "\n"; + $html .= "\n"; $html .= "
    " - . &html_file( undef, '../web/bin/set_cookie.pl', - 'menu_keys&&Keyboard Control', 1 ) - . "" . &html_file( undef, '../web/bin/set_cookie.pl', 'menu_keys&&Keyboard Control', 1 ) . "" - . &html_file( undef, '../web/bin/set_cookie.pl', - 'menu_buttons&&Buttons', 1 ) - . "" - . &html_file( undef, '../web/bin/set_cookie.pl', - 'webmute&&Webmute&&&&/bin/menu.pl', 1 ) - . "Help" . &html_file( undef, '../web/bin/set_cookie.pl', 'menu_buttons&&Buttons', 1 ) . "" . &html_file( undef, '../web/bin/set_cookie.pl', 'webmute&&Webmute&&&&/bin/menu.pl', 1 ) . "Help
    \n"; return $html; } diff --git a/web/bin/menu_old.pl b/web/bin/menu_old.pl index a632a9a13..ead5fdcc0 100644 --- a/web/bin/menu_old.pl +++ b/web/bin/menu_old.pl @@ -35,8 +35,7 @@ sub menu_list { $target++; $target = 'speech' if $target eq 'menu4'; my $html = "\n"; - $html .= - "\n"; + $html .= "\n"; my $item = 0; my $ptr = $Menus{$menu_group}; for my $ptr2 ( @{ $$ptr{$menu}{items} } ) { @@ -96,11 +95,9 @@ sub menu_states { my $ptr2 = $Menus{$menu_group}{$menu}{items}[$item]; my $state = 0; - my $html = - "
    $menu
    $menu
    "; + my $html = "
    $$ptr2{Dprefix}
    "; for my $state_name ( @{ $$ptr2{Dstates} } ) { - $html .= - "\n"; + $html .= "\n"; $state++; } $html .= "
    $$ptr2{Dprefix}$state_name$state_name
    "; diff --git a/web/bin/mh_usage.pl b/web/bin/mh_usage.pl index bc41488a9..bffe3bac2 100644 --- a/web/bin/mh_usage.pl +++ b/web/bin/mh_usage.pl @@ -13,12 +13,10 @@ $msg = "City and State/Country not filled in, so no data was sent."; } else { - $msg = - "Usage data logged:\n\n city=$city state/country=$state name=$name\n"; + $msg = "Usage data logged:\n\n city=$city state/country=$state name=$name\n"; $msg .= "\nThanks!\n"; display $msg, 0, "$Time_Date: Usage survey"; - logit "$config_parms{data_dir}/mh_usage.txt", - "city=$city state=$state name=$name"; + logit "$config_parms{data_dir}/mh_usage.txt", "city=$city state=$state name=$name"; } return html_page '', $msg; diff --git a/web/bin/mp3_applet_playlist.pl b/web/bin/mp3_applet_playlist.pl index ec35de755..6ddf6b5e0 100644 --- a/web/bin/mp3_applet_playlist.pl +++ b/web/bin/mp3_applet_playlist.pl @@ -14,12 +14,10 @@ my $serv = $config_parms{http_server}; my $Mp3List; - $Mp3List = - "MisterHouse Jukebox Playlist"; + $Mp3List = "MisterHouse Jukebox Playlist"; $Mp3List = "$Mp3List "; $Mp3List = "$Mp3List "; - $Mp3List = - "$Mp3List "; + $Mp3List = "$Mp3List "; $Mp3List = "$Mp3List "; - $Mp3List = - "$Mp3List "; + $Mp3List = "$Mp3List
    "; $Mp3List = "$Mp3List"; @@ -49,8 +46,7 @@ my $pos = 1; foreach my $item (@$titles) { my $Time = &mp3_get_playlist_timestr( $pos - 1 ); - my $Str = - " "; + my $Str = " "; $Str = substr( "$pos. $item", 1 ); my $my_pos = $pos - 1; @@ -58,16 +54,14 @@ # Only highlight the current song otherwise just add to the list # v 1.10 and Add a Track Jump URL if ( $pos - 1 == $currPos ) { - $Mp3List = $Mp3List - . ""; + $Mp3List = $Mp3List . ""; } # if ( $pos - 1 == $currPos ) { $Mp3List = $Mp3List ."" ; } # if ( $pos - 1 != $currPos ) { $Mp3List = $Mp3List ."" ; } if ( $pos - 1 != $currPos ) { - $Mp3List = $Mp3List - . ""; } diff --git a/web/bin/mp3_search.pl b/web/bin/mp3_search.pl index ed9b2defa..23fa44273 100644 --- a/web/bin/mp3_search.pl +++ b/web/bin/mp3_search.pl @@ -38,18 +38,15 @@ my $href = encode_url( '"' . $playfiles{$playlist} . '"' ); $href = ""; my $icon = $href; - $icon .= - "playlist"; + $icon .= "playlist"; $html .= ""; - $html .= - ""; + $html .= ""; $html .= "\n" unless ++$i % 3; } } elsif ( $string eq 'stations' ) { my @stations = &mp3_radio_stations; - $html .= - ""; + $html .= ""; $html .= ""; $html .= ""; $html .= ""; @@ -60,8 +57,7 @@ my ( $station, $url, $bandwidth, $style ) = split $;, $station; my $href = ""; my $icon = $href; - $icon .= - "playlist"; + $icon .= "playlist"; $html .= ""; $html .= ""; $html .= ""; @@ -70,8 +66,7 @@ $html .= "\n"; } $html .= "\n"; + $html .= "Get internet radio station list\n"; } elsif ( $string eq 'artists' ) { my %artists = &mp3_find_all('artist'); @@ -90,14 +85,11 @@ my $href = encode_url( quotemeta($artist) ); my $n = $artists{$artist}; $artist = 'none' if $artist eq ''; - $href = - ""; + $href = ""; my $icon = $href; - $icon .= - "playlist"; + $icon .= "playlist"; $html .= ""; - $html .= - ""; + $html .= ""; $html .= "\n" unless ++$i % 3; } } @@ -119,14 +111,11 @@ next if $album eq '' or $albums{$artistalbum} < 3; my $href = encode_url( quotemeta($album) ); my $n = $albums{$artistalbum}; - $href = - ""; + $href = ""; my $icon = $href; - $icon .= - "playlist"; + $icon .= "playlist"; $html .= ""; - $html .= - ""; + $html .= ""; $html .= "\n" unless ++$i % 3; } } @@ -138,14 +127,11 @@ my $href = encode_url( quotemeta($year) ); my $n = $years{$year}; $year = 'none' if $year eq ''; - $href = - ""; + $href = ""; my $icon = $href; - $icon .= - "playlist"; + $icon .= "playlist"; $html .= ""; - $html .= - ""; + $html .= ""; $html .= "\n" unless ++$i % 3; } } @@ -157,14 +143,11 @@ my $href = encode_url( quotemeta($genre) ); my $n = $genres{$genre}; $genre = 'none' if $genre eq ''; - $href = - ""; + $href = ""; my $icon = $href; - $icon .= - "playlist"; + $icon .= "playlist"; $html .= ""; - $html .= - ""; + $html .= ""; $html .= "\n" unless ++$i % 3; } } @@ -177,28 +160,20 @@ $string =~ s/album=.+//; my $year = $1 if $string =~ /year=(.+)/; $string =~ s/year=.+//; - my ( $results1, $results2, $count1, $count2 ) = - &mp3_search( $string, $genre, $artist, $album, $year ); + my ( $results1, $results2, $count1, $count2 ) = &mp3_search( $string, $genre, $artist, $album, $year ); # print "db r=$results1\n"; if ($count2) { # $html .= "\n"; - $html .= - "Play All\n"; - $html .= - " Add All

    \n"; + $html .= "Play All\n"; + $html .= " Add All

    \n"; if ($show_details) { - $html .= - "

    "; - $html .= - ""; - $html .= - ""; - $html .= - ""; - $html .= - ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; $html .= "\n"; } } @@ -206,12 +181,8 @@ $html .= "No matches.

    "; } - while ( $results1 =~ - /Title: (.+?) *Album: (.+?) *Year: (.+?) *Genre: *(.+?) *- Artist: (.+?) *Comments: *(.+?) *- File: (.+?)$/smg - ) - { - my ( $title, $album, $year, $genre, $artist, $comments, $file ) = - ( $1, $2, $3, $4, $5, $6, $7 ); + while ( $results1 =~ /Title: (.+?) *Album: (.+?) *Year: (.+?) *Genre: *(.+?) *- Artist: (.+?) *Comments: *(.+?) *- File: (.+?)$/smg ) { + my ( $title, $album, $year, $genre, $artist, $comments, $file ) = ( $1, $2, $3, $4, $5, $6, $7 ); # print "db t=$title a=$album y=$year g=$genre art=$artist c=$comments f=$file \n"; last unless $title; @@ -220,29 +191,22 @@ my ( $href, $arg ); if ($show_details) { $href = encode_url( '"' . $file . '"' ); - $href =~ - s/\\/\//g; # Forward slashes work ok in windows, and are easier - $html .= - "

    "; + $href =~ s/\\/\//g; # Forward slashes work ok in windows, and are easier + $html .= ""; $href = encode_url( quotemeta($artist) ); $html .= ""; $href = encode_url( quotemeta($album) ); - $html .= - ""; + $html .= ""; $href = encode_url( quotemeta($year) ); - $html .= - ""; + $html .= ""; $href = encode_url( quotemeta($genre) ); - $html .= - ""; + $html .= ""; $html .= "\n"; } else { - $html .= - ""; + $html .= ""; $html .= "\n" unless ++$i % 3; } } diff --git a/web/bin/mrwiki.pl b/web/bin/mrwiki.pl index 722c30534..672359cc0 100644 --- a/web/bin/mrwiki.pl +++ b/web/bin/mrwiki.pl @@ -36,71 +36,71 @@ package UseModWiki; $q $Now $UserID $TimeZoneOffset $ScriptName $BrowseCode $OtherCode); # == Configuration ===================================================== -$DataDir = $main::config_parms{wiki_data}; -$UseConfig = 0; # 1 = use config file, 0 = do not look for config +$DataDir = $main::config_parms{wiki_data}; +$UseConfig = 0; # 1 = use config file, 0 = do not look for config # Default configuration (used if UseConfig is 0) -$CookieName = "MrWiki"; # Name for this wiki (for multi-wiki sites) -$SiteName = "MrWiki"; # Name of site (used for titles) -$HomePage = "HomePage"; # Home page (change space to _) -$RCName = "RecentChanges"; # Name of changes page (change space to _) -$LogoUrl = "/graphics/mrwiki.gif"; # URL for site logo ("" for no logo) +$CookieName = "MrWiki"; # Name for this wiki (for multi-wiki sites) +$SiteName = "MrWiki"; # Name of site (used for titles) +$HomePage = "HomePage"; # Home page (change space to _) +$RCName = "RecentChanges"; # Name of changes page (change space to _) +$LogoUrl = "/graphics/mrwiki.gif"; # URL for site logo ("" for no logo) #$ENV{PATH} = $main::config_parms{wiki_diff}; #Path to find "diff" -$ScriptTZ = ""; # Local time zone ("" means do not print) -$RcDefault = 30; # Default number of RecentChanges days -@RcDays = qw(1 3 7 30 90); # Days for links on RecentChanges -$KeepDays = 14; # Days to keep old revisions -$SiteBase = ""; # Full URL for header -$FullUrl = ""; # Set if the auto-detected URL is wrong -$RedirType = 1; # 1 = CGI.pm, 2 = script, 3 = no redirect -$AdminPass = ""; # Set to non-blank to enable password(s) -$EditPass = ""; # Like AdminPass, but for editing only -$StyleSheet = "/ia5/default.css"; # URL for CSS stylesheet (like "/wiki.css") -$NotFoundPg = ""; # Page for not-found links ("" for blank pg) -$EmailFrom = "MrWiki"; # Text for "From: " field of email notes. -$SendMail = "/usr/sbin/sendmail"; # Full path to sendmail executable -$FooterNote = ""; # HTML for bottom of every page -$EditNote = ""; # HTML notice above buttons on edit page -$MaxPost = 1024 * 210; # Maximum 210K posts (about 200K for pages) -$NewText = ""; # New page text ("" for default message) -$HttpCharset = ""; # Charset for pages, like "iso-8859-2" -$UserGotoBar = ""; # HTML added to end of goto bar +$ScriptTZ = ""; # Local time zone ("" means do not print) +$RcDefault = 30; # Default number of RecentChanges days +@RcDays = qw(1 3 7 30 90); # Days for links on RecentChanges +$KeepDays = 14; # Days to keep old revisions +$SiteBase = ""; # Full URL for header +$FullUrl = ""; # Set if the auto-detected URL is wrong +$RedirType = 1; # 1 = CGI.pm, 2 = script, 3 = no redirect +$AdminPass = ""; # Set to non-blank to enable password(s) +$EditPass = ""; # Like AdminPass, but for editing only +$StyleSheet = "/ia5/default.css"; # URL for CSS stylesheet (like "/wiki.css") +$NotFoundPg = ""; # Page for not-found links ("" for blank pg) +$EmailFrom = "MrWiki"; # Text for "From: " field of email notes. +$SendMail = "/usr/sbin/sendmail"; # Full path to sendmail executable +$FooterNote = ""; # HTML for bottom of every page +$EditNote = ""; # HTML notice above buttons on edit page +$MaxPost = 1024 * 210; # Maximum 210K posts (about 200K for pages) +$NewText = ""; # New page text ("" for default message) +$HttpCharset = ""; # Charset for pages, like "iso-8859-2" +$UserGotoBar = ""; # HTML added to end of goto bar # Major options: -$UseSubpage = 1; # 1 = use subpages, 0 = do not use subpages -$UseCache = 0; # 1 = cache HTML pages, 0 = generate every page -$EditAllowed = 1; # 1 = editing allowed, 0 = read-only -$RawHtml = 0; # 1 = allow tag, 0 = no raw HTML in pages -$HtmlTags = 0; # 1 = "unsafe" HTML tags, 0 = only minimal tags -$UseDiff = 1; # 1 = use diff features, 0 = do not use diff -$FreeLinks = 1; # 1 = use [[word]] links, 0 = LinkPattern only -$WikiLinks = 1; # 1 = use LinkPattern, 0 = use [[word]] only -$AdminDelete = 1; # 1 = Admin only page, 0 = Editor can delete pages -$RunCGI = 1; # 1 = Run script as CGI, 0 = Load but do not run -$EmailNotify = 0; # 1 = use email notices, 0 = no email on changes -$EmbedWiki = 0; # 1 = no headers/footers, 0 = normal wiki pages +$UseSubpage = 1; # 1 = use subpages, 0 = do not use subpages +$UseCache = 0; # 1 = cache HTML pages, 0 = generate every page +$EditAllowed = 1; # 1 = editing allowed, 0 = read-only +$RawHtml = 0; # 1 = allow tag, 0 = no raw HTML in pages +$HtmlTags = 0; # 1 = "unsafe" HTML tags, 0 = only minimal tags +$UseDiff = 1; # 1 = use diff features, 0 = do not use diff +$FreeLinks = 1; # 1 = use [[word]] links, 0 = LinkPattern only +$WikiLinks = 1; # 1 = use LinkPattern, 0 = use [[word]] only +$AdminDelete = 1; # 1 = Admin only page, 0 = Editor can delete pages +$RunCGI = 1; # 1 = Run script as CGI, 0 = Load but do not run +$EmailNotify = 0; # 1 = use email notices, 0 = no email on changes +$EmbedWiki = 0; # 1 = no headers/footers, 0 = normal wiki pages # Minor options: -$LogoLeft = 0; # 1 = logo on left, 0 = logo on right -$RecentTop = 1; # 1 = recent on top, 0 = recent on bottom -$UseDiffLog = 1; # 1 = save diffs to log, 0 = do not save diffs -$KeepMajor = 1; # 1 = keep major rev, 0 = expire all revisions -$KeepAuthor = 1; # 1 = keep author rev, 0 = expire all revisions -$ShowEdits = 0; # 1 = show minor edits, 0 = hide edits by default -$HtmlLinks = 0; # 1 = allow A HREF links, 0 = no raw HTML links -$SimpleLinks = 0; # 1 = only letters, 0 = allow _ and numbers -$NonEnglish = 0; # 1 = extra link chars, 0 = only A-Za-z chars -$ThinLine = 0; # 1 = fancy
    tags, 0 = classic wiki
    -$BracketText = 1; # 1 = allow [URL text], 0 = no link descriptions -$UseAmPm = 1; # 1 = use am/pm in times, 0 = use 24-hour times -$UseIndex = 0; # 1 = use index file, 0 = slow/reliable method -$UseHeadings = 1; # 1 = allow = h1 text =, 0 = no header formatting -$NetworkFile = 1; # 1 = allow remote file:, 0 = no file:// links -$BracketWiki = 0; # 1 = [WikiLnk txt] link, 0 = no local descriptions -$UseLookup = 1; # 1 = lookup host names, 0 = skip lookup (IP only) -$FreeUpper = 1; # 1 = force upper case, 0 = do not force case -$FastGlob = 1; # 1 = new faster code, 0 = old compatible code +$LogoLeft = 0; # 1 = logo on left, 0 = logo on right +$RecentTop = 1; # 1 = recent on top, 0 = recent on bottom +$UseDiffLog = 1; # 1 = save diffs to log, 0 = do not save diffs +$KeepMajor = 1; # 1 = keep major rev, 0 = expire all revisions +$KeepAuthor = 1; # 1 = keep author rev, 0 = expire all revisions +$ShowEdits = 0; # 1 = show minor edits, 0 = hide edits by default +$HtmlLinks = 0; # 1 = allow A HREF links, 0 = no raw HTML links +$SimpleLinks = 0; # 1 = only letters, 0 = allow _ and numbers +$NonEnglish = 0; # 1 = extra link chars, 0 = only A-Za-z chars +$ThinLine = 0; # 1 = fancy
    tags, 0 = classic wiki
    +$BracketText = 1; # 1 = allow [URL text], 0 = no link descriptions +$UseAmPm = 1; # 1 = use am/pm in times, 0 = use 24-hour times +$UseIndex = 0; # 1 = use index file, 0 = slow/reliable method +$UseHeadings = 1; # 1 = allow = h1 text =, 0 = no header formatting +$NetworkFile = 1; # 1 = allow remote file:, 0 = no file:// links +$BracketWiki = 0; # 1 = [WikiLnk txt] link, 0 = no local descriptions +$UseLookup = 1; # 1 = lookup host names, 0 = skip lookup (IP only) +$FreeUpper = 1; # 1 = force upper case, 0 = do not force case +$FastGlob = 1; # 1 = new faster code, 0 = old compatible code # HTML tag lists, enabled if $HtmlTags is set. # Scripting is currently possible with these tags, @@ -176,8 +176,7 @@ sub InitLinkPatterns { $AnyLetter .= "]"; # Main link pattern: lowercase between uppercase, then anything - $LpA = - $UpperLetter . "+" . $LowerLetter . "+" . $UpperLetter . $AnyLetter . "*"; + $LpA = $UpperLetter . "+" . $LowerLetter . "+" . $UpperLetter . $AnyLetter . "*"; # Optional subpage link pattern: uppercase, lowercase, then anything $LpB = $UpperLetter . "+" . $LowerLetter . "+" . $AnyLetter . "*"; @@ -225,8 +224,7 @@ sub InitLinkPatterns { # 5. A $FS (field separator) character (kept in output) # 6. A double double-quote ("") (removed from output) - $UrlProtocols = "http|https|ftp|afs|news|nntp|mid|cid|mailto|wais|" - . "prospero|telnet|gopher"; + $UrlProtocols = "http|https|ftp|afs|news|nntp|mid|cid|mailto|wais|" . "prospero|telnet|gopher"; $UrlProtocols .= '|file' if $NetworkFile; $UrlPattern = "((?:(?:$UrlProtocols):[^\\]\\s\"<>$FS]+)$QDelim)"; $ImageExtensions = "(gif|jpg|png|bmp|jpeg)"; @@ -313,24 +311,24 @@ sub InitRequest { $IndexInit = 0; # Must be reset for each request $InterSiteInit = 0; %InterSite = (); - $MainPage = "."; # For subpages only, the name of the top-level page - $OpenPageName = ""; # Currently open page - &CreateDir($DataDir); # Create directory if it doesn't exist + $MainPage = "."; # For subpages only, the name of the top-level page + $OpenPageName = ""; # Currently open page + &CreateDir($DataDir); # Create directory if it doesn't exist if ( !-d $DataDir ) { &ReportError( Ts( 'Could not create %s', $DataDir ) . ": $!" ); return 0; } - &InitCookie(); # Reads in user data + &InitCookie(); # Reads in user data return 1; } sub InitCookie { %SetCookie = (); $TimeZoneOffset = 0; - undef $q->{'.cookies'}; # Clear cache if it exists (for SpeedyCGI) + undef $q->{'.cookies'}; # Clear cache if it exists (for SpeedyCGI) %UserCookie = $q->cookie($CookieName); $UserID = $UserCookie{'id'}; - $UserID =~ s/\D//g; # Numeric only + $UserID =~ s/\D//g; # Numeric only if ( $UserID < 200 ) { $UserID = 111; } @@ -342,7 +340,7 @@ sub InitCookie { || ( $UserData{'randkey'} != $UserCookie{'randkey'} ) ) { $UserID = 113; - %UserData = (); # Invalid. Later consider warning message. + %UserData = (); # Invalid. Later consider warning message. } } if ( $UserData{'tzoffset'} != 0 ) { @@ -353,12 +351,12 @@ sub InitCookie { sub DoBrowseRequest { my ( $id, $action, $text ); - if ( !$q->param ) { # No parameter + if ( !$q->param ) { # No parameter &BrowsePage($HomePage); return 1; } $id = &GetParam( 'keywords', '' ); - if ($id) { # Just script?PageName + if ($id) { # Just script?PageName if ( $FreeLinks && ( !-f &GetPageFile($id) ) ) { $id = &FreeToNormal($id); } @@ -450,26 +448,18 @@ sub BrowsePage { # Later maybe add edit time? if ( $goodRevision ne '' ) { - $fullHtml .= - '' . Ts( 'Showing revision %s', $revision ) . "
    "; + $fullHtml .= '' . Ts( 'Showing revision %s', $revision ) . "
    "; } else { - $fullHtml .= '' - . Ts( 'Revision %s not available', $revision ) . ' (' - . T('showing current revision instead') - . ')
    '; + $fullHtml .= '' . Ts( 'Revision %s not available', $revision ) . ' (' . T('showing current revision instead') . ')
    '; } } $allDiff = &GetParam( 'alldiff', 0 ); if ( $allDiff != 0 ) { $allDiff = &GetParam( 'defaultdiff', 1 ); } - if ( - ( - ( $id eq $RCName ) || ( T($RCName) eq $id ) || ( T($id) eq $RCName ) - ) - && &GetParam( 'norcdiff', 1 ) - ) + if ( ( ( $id eq $RCName ) || ( T($RCName) eq $id ) || ( T($id) eq $RCName ) ) + && &GetParam( 'norcdiff', 1 ) ) { $allDiff = 0; # Only show if specifically requested } @@ -484,8 +474,7 @@ sub BrowsePage { } $fullHtml .= &WikiToHTML( $Text{'text'} ); $fullHtml .= "
    \n" if ( !&GetParam( 'embed', $EmbedWiki ) ); - if ( ( $id eq $RCName ) || ( T($RCName) eq $id ) || ( T($id) eq $RCName ) ) - { + if ( ( $id eq $RCName ) || ( T($RCName) eq $id ) || ( T($id) eq $RCName ) ) { print $fullHtml; &DoRc(); print "
    \n" if ( !&GetParam( 'embed', $EmbedWiki ) ); @@ -502,9 +491,8 @@ sub BrowsePage { sub ReBrowsePage { my ( $id, $oldId, $isEdit ) = @_; - if ( $oldId ne "" ) { # Target of #REDIRECT (loop breaking) - print &GetRedirectPage( "action=browse&id=$id&oldid=$oldId", - $id, $isEdit ); + if ( $oldId ne "" ) { # Target of #REDIRECT (loop breaking) + print &GetRedirectPage( "action=browse&id=$id&oldid=$oldId", $id, $isEdit ); } else { print &GetRedirectPage( $id, $id, $isEdit ); @@ -519,19 +507,14 @@ sub DoRc { if ( &GetParam( "from", 0 ) ) { $starttime = &GetParam( "from", 0 ); - print "

    " . Ts( 'Updates since %s', &TimeToText($starttime) ) - . "

    \n"; + print "

    " . Ts( 'Updates since %s', &TimeToText($starttime) ) . "

    \n"; } else { $daysago = &GetParam( "days", 0 ); $daysago = &GetParam( "rcdays", 0 ) if ( $daysago == 0 ); if ($daysago) { $starttime = $Now - ( ( 24 * 60 * 60 ) * $daysago ); - print "

    " - . Ts( - 'Updates in the last %s day' . ( ( $daysago != 1 ) ? "s" : "" ), - $daysago - ) . "

    \n"; + print "

    " . Ts( 'Updates in the last %s day' . ( ( $daysago != 1 ) ? "s" : "" ), $daysago ) . "

    \n"; # Note: must have two translations (for "day" and "days") # Following comment line is for translation helper script @@ -540,11 +523,7 @@ sub DoRc { } if ( $starttime == 0 ) { $starttime = $Now - ( ( 24 * 60 * 60 ) * $RcDefault ); - print "

    " - . Ts( - 'Updates in the last %s day' . ( ( $RcDefault != 1 ) ? "s" : "" ), - $RcDefault ) - . "

    \n"; + print "

    " . Ts( 'Updates in the last %s day' . ( ( $RcDefault != 1 ) ? "s" : "" ), $RcDefault ) . "

    \n"; # Translation of above line is identical to previous version } @@ -561,8 +540,7 @@ sub DoRc { . ": $RcFile

    " . T('Error was') . ":\n

    $!
    \n" . '

    ' - . T('Note: This error is normal if no changes have been made.') - . "\n"; + . T('Note: This error is normal if no changes have been made.') . "\n"; } @fullrc = split( /\n/, $fileData ); $firstTs = 0; @@ -577,11 +555,7 @@ sub DoRc { else { if ( $errorText ne "" ) { # could not open either rclog file print $errorText; - print "

    " - . Ts( 'Could not open old %s log file', $RCName ) - . ": $RcOldFile

    " - . T('Error was') - . ":\n

    $!
    \n"; + print "

    " . Ts( 'Could not open old %s log file', $RCName ) . ": $RcOldFile

    " . T('Error was') . ":\n

    $!
    \n"; return; } } @@ -594,22 +568,18 @@ sub DoRc { $idOnly = &GetParam( "rcidonly", "" ); if ( $idOnly ne "" ) { - print '(' . Ts( 'for %s only', &ScriptLink( $idOnly, $idOnly ) ) - . ')
    '; + print '(' . Ts( 'for %s only', &ScriptLink( $idOnly, $idOnly ) ) . ')
    '; } foreach $i (@RcDays) { print " | " if $showbar; $showbar = 1; - print &ScriptLink( "action=rc&days=$i", - Ts( '%s day' . ( ( $i != 1 ) ? 's' : '' ), $i ) ); + print &ScriptLink( "action=rc&days=$i", Ts( '%s day' . ( ( $i != 1 ) ? 's' : '' ), $i ) ); # Note: must have two translations (for "day" and "days") # Following comment line is for translation helper script # Ts('%s days', ''); } - print "
    " - . &ScriptLink( "action=rc&from=$lastTs", - T('List new changes starting from') ); + print "
    " . &ScriptLink( "action=rc&from=$lastTs", T('List new changes starting from') ); print " " . &TimeToText($lastTs) . "
    \n"; # Later consider a binary search? @@ -628,13 +598,11 @@ sub DoRc { last if ( $ts >= $starttime ); } if ( $i == @fullrc ) { - print '
    ' - . Ts( 'No updates since %s', &TimeToText($starttime) ) - . "
    \n"; + print '
    ' . Ts( 'No updates since %s', &TimeToText($starttime) ) . "
    \n"; } else { splice( @fullrc, 0, $i ); # Remove items before index $i - # Later consider an end-time limit (items older than X) + # Later consider an end-time limit (items older than X) print &GetRcHtml(@fullrc); } print '

    ' . Ts( 'Page generated %s', &TimeToText($Now) ), "
    \n"; @@ -650,7 +618,7 @@ sub GetRcHtml { my %changetime = (); my %pagecount = (); - $tEdit = T('(edit)'); # Optimize translations out of main loop + $tEdit = T('(edit)'); # Optimize translations out of main loop $tDiff = T('(diff)'); $tChanges = T('changes'); $showedit = &GetParam( "rcshowedit", $ShowEdits ); @@ -660,10 +628,10 @@ sub GetRcHtml { foreach $rcline (@outrc) { ( $ts, $pagename, $summary, $isEdit, $host ) = split( /$FS3/, $rcline ); - if ( $showedit == 0 ) { # 0 = No edits + if ( $showedit == 0 ) { # 0 = No edits push( @temprc, $rcline ) if ( !$isEdit ); } - else { # 2 = Only edits + else { # 2 = Only edits push( @temprc, $rcline ) if ($isEdit); } } @@ -741,7 +709,7 @@ sub GetRcHtml { # Later do new-RC looping here. $html .= &CalcTime($ts) . " $count$edit" . " $sum"; - $html .= ". . . . . $author\n"; # Make dots optional? + $html .= ". . . . . $author\n"; # Make dots optional? } $html .= "\n" if ($inlist); return $html; @@ -759,17 +727,16 @@ sub DoHistory { my ($id) = @_; my ( $html, $canEdit ); - print &GetHeader( "", &QuoteHtml( Ts( 'History of %s', $id ) ), "" ) - . "
    "; + print &GetHeader( "", &QuoteHtml( Ts( 'History of %s', $id ) ), "" ) . "
    "; &OpenPage($id); &OpenDefaultText(); $canEdit = &UserCanEdit($id); - $canEdit = 0; # Turn off direct "Edit" links - $html = &GetHistoryLine( $id, $Page{'text_default'}, $canEdit, 1 ); + $canEdit = 0; # Turn off direct "Edit" links + $html = &GetHistoryLine( $id, $Page{'text_default'}, $canEdit, 1 ); &OpenKeptRevisions('text_default'); foreach ( reverse sort { $a <=> $b } keys %KeptRevisions ) { - next if ( $_ eq "" ); # (needed?) + next if ( $_ eq "" ); # (needed?) $html .= &GetHistoryLine( $id, $KeptRevisions{$_}, $canEdit, 0 ); } print $html; @@ -917,10 +884,10 @@ sub GetSearchLink { my ($id) = @_; my $name = $id; - $id =~ s|.+/|/|; # Subpage match: search for just /SubName + $id =~ s|.+/|/|; # Subpage match: search for just /SubName if ($FreeLinks) { - $name =~ s/_/ /g; # Display with spaces - $id =~ s/_/+/g; # Search for url-escaped spaces + $name =~ s/_/ /g; # Display with spaces + $id =~ s/_/+/g; # Search for url-escaped spaces } return &ScriptLink( "search=$id", $name ); } @@ -973,8 +940,7 @@ sub GetAuthorLink { # Later have user preference for link titles and/or host text? if ( ( $uid > 0 ) && ( $userName ne "" ) ) { - $html = &ScriptLinkTitle( $userName, $userNameShow, - Ts( 'ID %s', $uid ) . ' ' . Ts( 'from %s', $host ) ); + $html = &ScriptLinkTitle( $userName, $userNameShow, Ts( 'ID %s', $uid ) . ' ' . Ts( 'from %s', $host ) ); } else { $html = $host; @@ -1007,10 +973,7 @@ sub GetHeader { return $result if ($embed); if ( $oldId ne '' ) { - $result .= - $q->h3( '(' - . Ts( 'redirected from %s', &GetEditLink( $oldId, $oldId ) ) - . ')' ); + $result .= $q->h3( '(' . Ts( 'redirected from %s', &GetEditLink( $oldId, $oldId ) ) . ')' ); } if ( ( !$embed ) && ( $LogoUrl ne "" ) ) { $logoImage = "img src=\"$LogoUrl\" alt=\"$altText\" border=0"; @@ -1036,12 +999,7 @@ sub GetHeader { sub GetHttpHeader { my $cookie; if ( defined( $SetCookie{'id'} ) ) { - $cookie = - "$CookieName=" . "rev&" - . $SetCookie{'rev'} . "&id&" - . $SetCookie{'id'} - . "&randkey&" - . $SetCookie{'randkey'}; + $cookie = "$CookieName=" . "rev&" . $SetCookie{'rev'} . "&id&" . $SetCookie{'id'} . "&randkey&" . $SetCookie{'randkey'}; $cookie .= ";expires=Fri, 08-Sep-2010 19:48:23 GMT"; if ( $HttpCharset ne '' ) { return $q->header( @@ -1097,8 +1055,7 @@ sub GetFooterText { $result .= &GetGotoBar($id); if ( &UserCanEdit( $id, 0 ) ) { if ( $rev ne '' ) { - $result .= &GetOldPageLink( 'edit', $id, $rev, - Ts( 'Edit revision %s of this page', $rev ) ); + $result .= &GetOldPageLink( 'edit', $id, $rev, Ts( 'Edit revision %s of this page', $rev ) ); } else { $result .= &GetEditLink( $id, T('Edit text of this page') ); @@ -1128,12 +1085,7 @@ sub GetFooterText { } $result .= '
    ' . &GetSearchForm(); if ( $DataDir =~ m|/tmp/| ) { - $result .= - '
    ' - . T('Warning') - . ': ' - . Ts( 'Database is stored in temporary directory %s', $DataDir ) - . '
    '; + $result .= '
    ' . T('Warning') . ': ' . Ts( 'Database is stored in temporary directory %s', $DataDir ) . '
    '; } $result .= $q->endform; $result .= &GetMinimumFooter(); @@ -1141,13 +1093,7 @@ sub GetFooterText { } sub GetCommonFooter { - return - "


    " - . &GetFormStart() - . &GetGotoBar("") - . &GetSearchForm() - . $q->endform - . &GetMinimumFooter(); + return "
    " . &GetFormStart() . &GetGotoBar("") . &GetSearchForm() . $q->endform . &GetMinimumFooter(); } sub GetMinimumFooter { @@ -1158,8 +1104,7 @@ sub GetMinimumFooter { } sub GetFormStart { - return $q->startform( "POST", "$ScriptName", - "application/x-www-form-urlencoded" ); + return $q->startform( "POST", "$ScriptName", "application/x-www-form-urlencoded" ); } sub GetGotoBar { @@ -1187,10 +1132,7 @@ sub GetGotoBar { sub GetSearchForm { my ($result); - $result = - T('Search:') . ' ' - . $q->textfield( -name => 'search', -size => 20 ) - . &GetHiddenValue( "dosearch", 1 ); + $result = T('Search:') . ' ' . $q->textfield( -name => 'search', -size => 20 ) . &GetHiddenValue( "dosearch", 1 ); return $result; } @@ -1205,14 +1147,14 @@ sub GetRedirectPage { $nameLink = "$name"; if ( $RedirType < 3 ) { if ( $RedirType == 1 ) { # Use CGI.pm - # NOTE: do NOT use -method (does not work with old CGI.pm versions) - # Thanks to Daniel Neri for fixing this problem. + # NOTE: do NOT use -method (does not work with old CGI.pm versions) + # Thanks to Daniel Neri for fixing this problem. $html = $q->redirect( -uri => $url ); } - else { # Minimal header + else { # Minimal header $html = "Status: 302 Moved\n"; $html .= "Location: $url\n"; - $html .= "Content-Type: text/html\n"; # Needed for browser failure + $html .= "Content-Type: text/html\n"; # Needed for browser failure $html .= "\n"; } $html .= "\n" . Ts( 'Your browser should go to the %s page.', $newid ); @@ -1259,7 +1201,7 @@ sub CommonMarkup { local $_ = $text; if ( $doLines < 2 ) { # 2 = do line-oriented only - # The tag stores text with no markup (except quoting HTML) + # The tag stores text with no markup (except quoting HTML) s/\<nowiki\>((.|\n)*?)\<\/nowiki\>/&StoreRaw($1)/ige; # The
     tag wraps the stored text with the HTML 
     tag
    @@ -1317,8 +1259,8 @@ sub CommonMarkup {
             }
         }
         if ($doLines) {    # 0 = no line-oriented, 1 or 2 = do line-oriented
    -            # The quote markup patterns avoid overlapping tags (with 5 quotes)
    -            # by matching the inner quotes for the strong pattern.
    +                       # The quote markup patterns avoid overlapping tags (with 5 quotes)
    +                       # by matching the inner quotes for the strong pattern.
             s/('*)'''(.*?)'''/$1$2<\/strong>/g;
             s/''(.*?)''/$1<\/em>/g;
             if ($UseHeadings) {
    @@ -1514,8 +1456,7 @@ sub UrlLink {
         }
     
         # Restricted image URLs so that mailto:foo@bar.gif is not an image
    -    if ( $useImage && ( $name =~ /^(http:|https:|ftp:).+\.$ImageExtensions$/ ) )
    -    {
    +    if ( $useImage && ( $name =~ /^(http:|https:|ftp:).+\.$ImageExtensions$/ ) ) {
             return ( "", $punct );
         }
         return ( "$name", $punct );
    @@ -1578,17 +1519,10 @@ sub ISBNLink {
         if ( length($num) != 10 ) {
             return "ISBN $rawnum";
         }
    -    $first = "";
    -    $second =
    -        ""
    -      . T('alternate') . "";
    -    $third =
    -        ""
    -      . T('search') . "";
    -    $html = $first . "ISBN " . $rawprint . " ";
    +    $first  = "";
    +    $second = "" . T('alternate') . "";
    +    $third  = "" . T('search') . "";
    +    $html   = $first . "ISBN " . $rawprint . " ";
         $html .= "($second, $third)";
         $html .= " " if ( $rawnum =~ / $/ );    # Add space if old ISBN had space.
         return $html;
    @@ -1627,8 +1561,7 @@ sub WikiHeading {
     sub GetDiffHTML {
         my ( $diffType, $id, $rev, $newText ) = @_;
         my ( $html, $diffText, $diffTextTwo, $priorName, $links, $usecomma );
    -    my ( $major, $minor, $author, $useMajor, $useMinor, $useAuthor,
    -        $cacheName );
    +    my ( $major, $minor, $author, $useMajor, $useMinor, $useAuthor, $cacheName );
     
         $links     = "(";
         $usecomma  = 0;
    @@ -1697,12 +1630,7 @@ sub GetDiffHTML {
             $diffText = T('No diff available.');
         }
         if ( $rev ne "" ) {
    -        $html = ''
    -          . Ts( 'Difference (from revision %s to current revision)', $rev )
    -          . "\n"
    -          . "$links
    " - . &DiffToHTML($diffText) - . "
    \n"; + $html = '' . Ts( 'Difference (from revision %s to current revision)', $rev ) . "\n" . "$links
    " . &DiffToHTML($diffText) . "
    \n"; } else { if ( @@ -1711,17 +1639,10 @@ sub GetDiffHTML { || ( &GetPageCache("old$cacheName") < 1 ) ) ) { - $html = '' - . Ts( 'No diff available--this is the first %s revision.', - $priorName ) - . "\n$links
    "; + $html = '' . Ts( 'No diff available--this is the first %s revision.', $priorName ) . "\n$links
    "; } else { - $html = '' - . Ts( 'Difference (from prior %s revision)', $priorName ) - . "\n$links
    " - . &DiffToHTML($diffText) - . "
    \n"; + $html = '' . Ts( 'Difference (from prior %s revision)', $priorName ) . "\n$links
    " . &DiffToHTML($diffText) . "
    \n"; } } return $html; @@ -1770,7 +1691,7 @@ sub GetDiff { print STDERR "
    diff=$diff_out\n"; &ReleaseDiffLock() if ($lock); $diff_out =~ s/\\ No newline.*\n//g; # Get rid of common complaint. - # No need to unlink temp files--next diff will just overwrite. + # No need to unlink temp files--next diff will just overwrite. return $diff_out; } @@ -1808,8 +1729,7 @@ sub ColorDiff { $diff =~ s/$FS(\d+)$FS/$SaveUrl{$1}/ge; # Restore saved text $diff =~ s/$FS(\d+)$FS/$SaveUrl{$1}/ge; # Restore nested saved text $diff =~ s/\r?\n/
    /g; - return "
    Track
    (del)
    Song Title
    (jump)
    Play
    Time
    $pos$item$Time
    $pos$item$Time
    $pos$item$Time
    $pos\ # $item$Time
    $pos \ + $Mp3List = $Mp3List . "
    $pos \ $item $Time
    $icon$href$playlist
    $href$playlist
    Station
    Station
    URL
    Bandwidth
    Style
    $icon$station$href$url
    "; - $html .= - "Get internet radio station list
    $icon$href$artist ($n)
    $href$artist ($n)
    $icon$href$artist - $album ($n)
    $href$artist - $album ($n)
    $icon$href$year ($n)
    $href$year ($n)
    $icon$href$genre ($n)
    $href$genre ($n)
    Play All
    Title
    Artist
    Album
    Year
    Genre
    Title
    Artist
    Album
    Year
    Genre
    [add]"; - $html .= - " $title
    [add]"; + $html .= " $title
    $artist
    $album
    $album
    $year
    $year
    $genre
    $genre
    $href$title
    $href$title
    \n" . $diff - . "
    \n"; + return "
    \n" . $diff . "
    \n"; } # ==== Database (Page, Section, Text, Kept, User) functions ==== @@ -1833,15 +1753,15 @@ sub OpenNewSection { $Section{'tscreate'} = $Now; # Set once at creation $Section{'ts'} = $Now; # Updated every edit $Section{'ip'} = $ENV{REMOTE_ADDR}; - $Section{'host'} = ''; # Updated only for real edits (can be slow) - $Section{'id'} = $UserID; + $Section{'host'} = ''; # Updated only for real edits (can be slow) + $Section{'id'} = $UserID; $Section{'username'} = &GetParam( "username", "" ); - $Section{'data'} = $data; - $Page{$name} = join( $FS2, %Section ); # Replace with save? + $Section{'data'} = $data; + $Page{$name} = join( $FS2, %Section ); # Replace with save? } sub OpenNewText { - my ($name) = @_; # Name of text (usually "default") + my ($name) = @_; # Name of text (usually "default") %Text = (); # Later consider translation of new-page message? (per-user difference?) @@ -1852,8 +1772,8 @@ sub OpenNewText { $Text{'text'} = T('Describe the new page here.') . "\n"; } $Text{'text'} .= "\n" if ( substr( $Text{'text'}, -1, 1 ) ne "\n" ); - $Text{'minor'} = 0; # Default as major edit - $Text{'newauthor'} = 1; # Default as new author + $Text{'minor'} = 0; # Default as major edit + $Text{'newauthor'} = 1; # Default as new author $Text{'summary'} = ''; &OpenNewSection( "text_$name", join( $FS3, %Text ) ); } @@ -1971,8 +1891,7 @@ sub UpdatePageVersion { } sub KeepFileName { - return $KeepDir . "/" . &GetPageDirectory($OpenPageName) - . "/$OpenPageName.kp"; + return $KeepDir . "/" . &GetPageDirectory($OpenPageName) . "/$OpenPageName.kp"; } sub SaveKeepSection { @@ -2172,7 +2091,7 @@ sub UserCanEdit { # Optimized for the "everyone can edit" case (don't check passwords) if ( ( $id ne "" ) && ( -f &GetLockedPageFile($id) ) ) { return 1 if ( &UserIsAdmin() ); # Requires more privledges - # Later option for editor-level to edit these pages? + # Later option for editor-level to edit these pages? return 0; } if ( !$EditAllowed ) { @@ -2183,7 +2102,7 @@ sub UserCanEdit { return 1 if ( &UserIsEditor() ); return 0; } - if ($deepCheck) { # Deeper but slower checks (not every page) + if ($deepCheck) { # Deeper but slower checks (not every page) return 1 if ( &UserIsEditor() ); return 0 if ( &UserIsBanned() ); } @@ -2194,7 +2113,7 @@ sub UserIsBanned { my ( $host, $ip, $data, $status ); ( $status, $data ) = &ReadFile("$DataDir/banlist"); - return 0 if ( !$status ); # No file exists, so no ban + return 0 if ( !$status ); # No file exists, so no ban $ip = $ENV{'REMOTE_ADDR'}; $host = &GetRemoteHost(0); foreach ( split( /\n/, $data ) ) { @@ -2480,12 +2399,8 @@ sub CalcDay { $ts += $TimeZoneOffset; my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime($ts); - return ( - "January", "February", "March", "April", - "May", "June", "July", "August", - "September", "October", "November", "December" - )[$mon] - . " " + return + ( "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" )[$mon] . " " . $mday . ", " . ( $year + 1900 ); } @@ -2555,8 +2470,7 @@ sub GetRemoteHost { if ( $UseLookup && ( $rhost eq "" ) ) { # Catch errors (including bad input) without aborting the script - eval 'use Socket; $iaddr = inet_aton($ENV{REMOTE_ADDR});' - . '$rhost = gethostbyaddr($iaddr, AF_INET)'; + eval 'use Socket; $iaddr = inet_aton($ENV{REMOTE_ADDR});' . '$rhost = gethostbyaddr($iaddr, AF_INET)'; } if ( $rhost eq "" ) { $rhost = $ENV{REMOTE_ADDR}; @@ -2570,7 +2484,7 @@ sub FreeToNormal { $id =~ s/ /_/g; $id = ucfirst($id); - if ( index( $id, '_' ) > -1 ) { # Quick check for any space/underscores + if ( index( $id, '_' ) > -1 ) { # Quick check for any space/underscores $id =~ s/__+/_/g; $id =~ s/^_//; $id =~ s/_$//; @@ -2582,7 +2496,7 @@ sub FreeToNormal { if ($FreeUpper) { # Note that letters after ' are *not* capitalized - if ( $id =~ m|[-_.,\(\)/][a-z]| ) { # Quick check for non-canonical case + if ( $id =~ m|[-_.,\(\)/][a-z]| ) { # Quick check for non-canonical case $id =~ s|([-_.,\(\)/])([a-z])|$1 . uc($2)|ge; } } @@ -2735,8 +2649,7 @@ sub DoEdit { if ( $revision ne '' ) { print "\n" . Ts( 'Editing old revision %s.', $revision ) . " " - . T( - 'Saving this page will replace the latest revision with this text.') + . T('Saving this page will replace the latest revision with this text.') . '
    '; } if ($isConflict) { @@ -2750,17 +2663,13 @@ sub DoEdit { } print "

    ", T('Someone saved this page after you started editing.'), " ", - T('The top textbox contains the saved text.'), " ", - T('Only the text in the top textbox will be saved.'), + T('The top textbox contains the saved text.'), " ", T('Only the text in the top textbox will be saved.'), "
    \n", T('Scroll down to see your edited text.'), "
    \n"; - print T('Last save time:'), ' ', &TimeToText($oldTime), - " (", T('Current time is:'), ' ', &TimeToText($Now), ")
    \n"; + print T('Last save time:'), ' ', &TimeToText($oldTime), " (", T('Current time is:'), ' ', &TimeToText($Now), ")
    \n"; } print &GetFormStart(); - print &GetHiddenValue( "title", $id ), "\n", - &GetHiddenValue( "oldtime", $pageTime ), "\n", - &GetHiddenValue( "oldconflict", $isConflict ), "\n"; + print &GetHiddenValue( "title", $id ), "\n", &GetHiddenValue( "oldtime", $pageTime ), "\n", &GetHiddenValue( "oldconflict", $isConflict ), "\n"; if ( $revision ne "" ) { print &GetHiddenValue( "revision", $revision ), "\n"; } @@ -2792,9 +2701,8 @@ sub DoEdit { if ($EmailNotify) { print "   " . $q->checkbox( - -name => 'do_email_notify', - -label => - Ts( 'Send email notification that %s has been changed.', $id ) + -name => 'do_email_notify', + -label => Ts( 'Send email notification that %s has been changed.', $id ) ); } print "
    "; @@ -2807,24 +2715,18 @@ sub DoEdit { print ' (', T('Your user name is'), ' ', &GetPageLink($userName) . ') '; } else { - print ' (', Ts( 'Visit %s to set your user name.', &GetPrefsLink() ), - ') '; + print ' (', Ts( 'Visit %s to set your user name.', &GetPrefsLink() ), ') '; } print $q->submit( -name => 'Preview', -value => T('Preview') ), "\n"; if ($isConflict) { - print "\n


    ", T('This is the text you submitted:'), - "

    ", - &GetTextArea( 'newtext', $newText, $editRows, $editCols ), - "

    \n"; + print "\n


    ", T('This is the text you submitted:'), "

    ", &GetTextArea( 'newtext', $newText, $editRows, $editCols ), "

    \n"; } print "


    \n"; if ($preview) { print "

    ", T('Preview:'), "

    \n"; if ($isConflict) { - print "", - T('NOTE: This preview shows the revision of the other author.'), - "
    \n"; + print "", T('NOTE: This preview shows the revision of the other author.'), "
    \n"; } $MainPage = $id; $MainPage =~ s|/.*||; # Only the main page name (remove subpage) @@ -2881,8 +2783,7 @@ sub DoEditPrefs { -size => 15, -maxlength => 50 ), - ' ', T('(blank to remove password)'), '
    (', - T('Passwords allow sharing preferences between multiple systems.'), + ' ', T('(blank to remove password)'), '
    (', T('Passwords allow sharing preferences between multiple systems.'), ' ', T('Passwords are completely optional.'), ')'; if ( $AdminPass ne '' ) { @@ -2898,20 +2799,13 @@ sub DoEditPrefs { } if ($EmailNotify) { print "
    "; - print &GetFormCheck( 'notify', 1, - T('Include this address in the site email list.') ), - ' ', - T('(Uncheck the box to remove the address.)'); - print '
    ', T('Email Address:'), ' ', - &GetFormText( 'email', "", 30, 60 ); + print &GetFormCheck( 'notify', 1, T('Include this address in the site email list.') ), ' ', T('(Uncheck the box to remove the address.)'); + print '
    ', T('Email Address:'), ' ', &GetFormText( 'email', "", 30, 60 ); } print "
    $recentName:\n"; - print '
    ', T('Default days to display:'), ' ', - &GetFormText( 'rcdays', $RcDefault, 4, 9 ); - print "
    ", - &GetFormCheck( 'rcnewtop', $RecentTop, T('Most recent changes on top') ); - print "
    ", - &GetFormCheck( 'rcall', 0, T('Show all changes (not just most recent)') ); + print '
    ', T('Default days to display:'), ' ', &GetFormText( 'rcdays', $RcDefault, 4, 9 ); + print "
    ", &GetFormCheck( 'rcnewtop', $RecentTop, T('Most recent changes on top') ); + print "
    ", &GetFormCheck( 'rcall', 0, T('Show all changes (not just most recent)') ); %labels = ( 0 => T('Hide minor edits'), 1 => T('Show minor edits'), @@ -2924,20 +2818,13 @@ sub DoEditPrefs { -labels => \%labels, -default => &GetParam( "rcshowedit", $ShowEdits ) ); - print "
    ", - &GetFormCheck( 'rcchangehist', 1, T('Use "changes" as link to history') ); + print "
    ", &GetFormCheck( 'rcchangehist', 1, T('Use "changes" as link to history') ); if ($UseDiff) { print '
    ', T('Differences:'), "\n"; - print "
    ", - &GetFormCheck( 'diffrclink', 1, - Ts( 'Show (diff) links on %s', $recentName ) ); - print "
    ", - &GetFormCheck( 'alldiff', 0, T('Show differences on all pages') ); - print " (", - &GetFormCheck( 'norcdiff', 1, - Ts( 'No differences on %s', $recentName ) ), - ")"; + print "
    ", &GetFormCheck( 'diffrclink', 1, Ts( 'Show (diff) links on %s', $recentName ) ); + print "
    ", &GetFormCheck( 'alldiff', 0, T('Show differences on all pages') ); + print " (", &GetFormCheck( 'norcdiff', 1, Ts( 'No differences on %s', $recentName ) ), ")"; %labels = ( 1 => T('Major'), 2 => T('Minor'), 3 => T('Author') ); print '
    ', T('Default difference type:'), ' '; print $q->popup_menu( @@ -2951,18 +2838,12 @@ sub DoEditPrefs { # Note: TZ offset is added by TimeToText, so pre-subtract to cancel. print '
    ', T('Server time:'), ' ', &TimeToText( $Now - $TimeZoneOffset ); - print '
    ', T('Time Zone offset (hours):'), ' ', - &GetFormText( 'tzoffset', 0, 4, 9 ); - print '
    ', - &GetFormCheck( 'editwide', 1, - T('Use 100% wide edit area (if supported)') ); - print '
    ', - T('Edit area rows:'), ' ', &GetFormText( 'editrows', 20, 4, 4 ), - ' ', T('columns:'), ' ', &GetFormText( 'editcols', 65, 4, 4 ); + print '
    ', T('Time Zone offset (hours):'), ' ', &GetFormText( 'tzoffset', 0, 4, 9 ); + print '
    ', &GetFormCheck( 'editwide', 1, T('Use 100% wide edit area (if supported)') ); + print '
    ', T('Edit area rows:'), ' ', &GetFormText( 'editrows', 20, 4, 4 ), ' ', T('columns:'), ' ', &GetFormText( 'editcols', 65, 4, 4 ); print '
    ', &GetFormCheck( 'toplinkbar', 1, T('Show link bar on top') ); - print '
    ', - &GetFormCheck( 'linkrandom', 0, T('Add "Random Page" link to link bar') ); + print '
    ', &GetFormCheck( 'linkrandom', 0, T('Add "Random Page" link to link bar') ); print '
    ', $q->submit( -name => 'Save', -value => T('Save') ), "\n"; print "
    \n"; print &GetGotoBar(''); @@ -3004,11 +2885,9 @@ sub DoUpdatePrefs { print &GetHeader( '', T('Saving Preferences'), '' ); print '
    '; if ( $UserID < 1001 ) { - print '', - Ts( 'Invalid UserID %s, preferences not saved.', $UserID ), ''; + print '', Ts( 'Invalid UserID %s, preferences not saved.', $UserID ), ''; if ( $UserID == 111 ) { - print '
    ', - T('(Preferences require cookies, but no cookie was sent.)'); + print '
    ', T('(Preferences require cookies, but no cookie was sent.)'); } print &GetCommonFooter(); return; @@ -3030,8 +2909,7 @@ sub DoUpdatePrefs { print Ts( 'Invalid UserName %s: not saved.', $username ), "
    \n"; } elsif ( length($username) > 50 ) { # Too long - print T('UserName must be 50 characters or less. (not saved)'), - "
    \n"; + print T('UserName must be 50 characters or less. (not saved)'), "
    \n"; } else { print Ts( 'UserName %s saved.', $username ), '
    '; @@ -3062,9 +2940,7 @@ sub DoUpdatePrefs { print T('User has editor abilities.'), '
    '; } else { - print T('User does not have administrative abilities.'), ' ', - T('(Password does not match administrative password(s).)'), - '
    '; + print T('User does not have administrative abilities.'), ' ', T('(Password does not match administrative password(s).)'), '
    '; } } } @@ -3105,8 +2981,7 @@ sub UpdateEmailList { my $notify = $UserData{'notify'}; if ( -f "$DataDir/emails" ) { open( NOTIFY, "$DataDir/emails" ) - or die( - Ts( 'Could not read from %s:', "$DataDir/emails" ) . " $!\n" ); + or die( Ts( 'Could not read from %s:', "$DataDir/emails" ) . " $!\n" ); @old_emails = ; close(NOTIFY); } @@ -3117,8 +2992,7 @@ sub UpdateEmailList { if ( $notify and ( not $already_in_list ) ) { &RequestLock() or die( T('Could not get mail lock') ); open( NOTIFY, ">>$DataDir/emails" ) - or die( - Ts( 'Could not append to %s:', "$DataDir/emails" ) . " $!\n" ); + or die( Ts( 'Could not append to %s:', "$DataDir/emails" ) . " $!\n" ); print NOTIFY $new_email, "\n"; close(NOTIFY); &ReleaseLock(); @@ -3126,8 +3000,7 @@ sub UpdateEmailList { elsif ( ( not $notify ) and $already_in_list ) { &RequestLock() or die( T('Could not get mail lock') ); open( NOTIFY, ">$DataDir/emails" ) - or die( - Ts( 'Could not overwrite %s:', "$DataDir/emails" ) . " $!\n" ); + or die( Ts( 'Could not overwrite %s:', "$DataDir/emails" ) . " $!\n" ); foreach (@old_emails) { print NOTIFY "$_" unless /$new_email/; } @@ -3452,8 +3325,7 @@ sub GetPageLinks { if ($pagelink) { if ($FreeLinks) { my $fl = $FreeLinkPattern; - $text =~ - s/\[\[$fl\|[^\]]+\]\]/push(@links, &FreeToNormal($1)), ' '/ge; + $text =~ s/\[\[$fl\|[^\]]+\]\]/push(@links, &FreeToNormal($1)), ' '/ge; $text =~ s/\[\[$fl\]\]/push(@links, &FreeToNormal($1)), ' '/ge; } if ($WikiLinks) { @@ -3526,12 +3398,12 @@ sub DoPost { else { $newAuthor = ( $Section{'ip'} ne $authorAddr ); # hostname fallback } - $newAuthor = 1 if ( $oldrev == 0 ); # New page - $newAuthor = 0 if ( !$newAuthor ); # Standard flag form, not empty - # Detect editing conflicts and resubmit edit + $newAuthor = 1 if ( $oldrev == 0 ); # New page + $newAuthor = 0 if ( !$newAuthor ); # Standard flag form, not empty + # Detect editing conflicts and resubmit edit if ( ( $oldrev > 0 ) && ( $newAuthor && ( $oldtime != $pgtime ) ) ) { &ReleaseLock(); - if ( $oldconflict > 0 ) { # Conflict again... + if ( $oldconflict > 0 ) { # Conflict again... print STDERR "db1b\n"; &DoEdit( $id, 2, $pgtime, $string, $preview ); @@ -3609,8 +3481,7 @@ sub UpdateDiffs { &SetPageCache( 'diff_default_major', "1" ); } else { - &SetPageCache( 'diff_default_major', - &GetKeptDiff( $new, $oldMajor, 0 ) ); + &SetPageCache( 'diff_default_major', &GetKeptDiff( $new, $oldMajor, 0 ) ); } if ($newAuthor) { &SetPageCache( 'diff_default_author', "1" ); @@ -3619,8 +3490,7 @@ sub UpdateDiffs { &SetPageCache( 'diff_default_author', "2" ); } else { - &SetPageCache( 'diff_default_author', - &GetKeptDiff( $new, $oldAuthor, 0 ) ); + &SetPageCache( 'diff_default_author', &GetKeptDiff( $new, $oldAuthor, 0 ) ); } } @@ -3779,8 +3649,7 @@ sub WriteRcLog { $extraTemp = join( $FS2, %extra ); # The two fields at the end of a line are kind and extension-hash - my $rc_line = - join( $FS3, $editTime, $id, $summary, $isEdit, $rhost, "0", $extraTemp ); + my $rc_line = join( $FS3, $editTime, $id, $summary, $isEdit, $rhost, "0", $extraTemp ); if ( !open( OUT, ">>$RcFile" ) ) { die( Ts( '%s log error:', $RCName ) . " $!" ); } @@ -3846,8 +3715,7 @@ sub UserIsEditorOrError { sub UserIsAdminOrError { if ( !&UserIsAdmin() ) { - print '

    ', - T('This operation is restricted to administrators only...'); + print '

    ', T('This operation is restricted to administrators only...'); print &GetCommonFooter(); return 0; } @@ -4089,8 +3957,7 @@ sub EditRecentChangesFile { if ( !$status ) { # Save error text if needed. - $errorText = "

    Could not open $RCName log file:" - . " $fname

    Error was:\n

    $!
    \n"; + $errorText = "

    Could not open $RCName log file:" . " $fname

    Error was:\n

    $!
    \n"; print $errorText; # Maybe handle differently later? return; } @@ -4136,7 +4003,7 @@ sub DeletePage { unlink($fname) if ( -f $fname ); unlink($IndexFile) if ($UseIndex); &EditRecentChanges( 1, $page, "" ) if ($doRC); # Delete page - # Currently don't do anything with page text + # Currently don't do anything with page text } # Given text, returns substituted text @@ -4146,7 +4013,7 @@ sub SubstituteTextLinks { # Much of this is taken from the common markup %SaveUrl = (); $SaveUrlIndex = 0; - $text =~ s/$FS//g; # Remove separators (paranoia) + $text =~ s/$FS//g; # Remove separators (paranoia) if ($RawHtml) { $text =~ s/(((.|\n)*?)<\/html>)/&StoreRaw($1)/ige; } @@ -4155,11 +4022,10 @@ sub SubstituteTextLinks { $text =~ s/(((.|\n)*?)<\/nowiki>)/&StoreRaw($1)/ige; if ($FreeLinks) { - $text =~ - s/\[\[$FreeLinkPattern\|([^\]]+)\]\]/&SubFreeLink($1,$2,$old,$new)/geo; + $text =~ s/\[\[$FreeLinkPattern\|([^\]]+)\]\]/&SubFreeLink($1,$2,$old,$new)/geo; $text =~ s/\[\[$FreeLinkPattern\]\]/&SubFreeLink($1,"",$old,$new)/geo; } - if ($BracketText) { # Links like [URL text of link] + if ($BracketText) { # Links like [URL text of link] $text =~ s/(\[$UrlPattern\s+([^\]]+?)\])/&StoreRaw($1)/geo; $text =~ s/(\[$InterLinkPattern\s+([^\]]+?)\])/&StoreRaw($1)/geo; } @@ -4169,7 +4035,7 @@ sub SubstituteTextLinks { $text =~ s/$LinkPattern/&SubWikiLink($1, $old, $new)/geo; } - $text =~ s/$FS(\d+)$FS/$SaveUrl{$1}/ge; # Restore saved text + $text =~ s/$FS(\d+)$FS/$SaveUrl{$1}/ge; # Restore saved text return $text; } @@ -4180,8 +4046,7 @@ sub SubFreeLink { $oldlink = $link; $link =~ s/^\s+//; $link =~ s/\s+$//; - if ( ( $link eq $old ) || ( &FreeToNormal($old) eq &FreeToNormal($link) ) ) - { + if ( ( $link eq $old ) || ( &FreeToNormal($old) eq &FreeToNormal($link) ) ) { $link = $new; } else { @@ -4374,6 +4239,6 @@ sub DoShowVersion { #END_OF_OTHER_CODE &DoWikiRequest() if ( $RunCGI && ( $_ ne 'nocgi' ) ); # Do everything. -1; # In case we are loaded from elsewhere +1; # In case we are loaded from elsewhere # == End of UseModWiki script. =========================================== diff --git a/web/bin/phone_in.pl b/web/bin/phone_in.pl index 45626f4e9..47251fd93 100644 --- a/web/bin/phone_in.pl +++ b/web/bin/phone_in.pl @@ -5,8 +5,7 @@ my @logs = &read_phone_logs1('callerid'); my @calls = &read_phone_logs2( 100, @logs ); for my $r (@calls) { - my ( $time, $num, $name, $line, $type ) = - $r =~ /date=(.+) number=(.+) name=(.+) line=(.*) type=(.*)/; + my ( $time, $num, $name, $line, $type ) = $r =~ /date=(.+) number=(.+) name=(.+) line=(.*) type=(.*)/; ( $time, $num, $name ) = $r =~ /(.+\d+:\d+:\d+) (\S+) (.+)/ unless $name; $display_name = $name; $display_name =~ s/_/ /g; # remove underscores to make it print pretty @@ -14,8 +13,7 @@ # next unless $line; - $html_calls .= - "
    Show last call from $num Show last call from $num
    $time$num$name
    $time$num$name
    Delete $number$name$sound$type
    "; print "dbx2\n"; - my $form_type = - &html_form_select( 'type', 0, 'Friend', 'Friend', 'Business', 'reject', - 'Family' ); + my $form_type = &html_form_select( 'type', 0, 'Friend', 'Friend', 'Business', 'reject', 'Family' ); #form action='/bin/items.pl?add' method=post> @@ -116,8 +113,7 @@ sub rej_call_item_delete { my @ReadRejFile = &read_reject_call_list(); unlink $phone_dir; for $line (@ReadRejFile) { - ( $number, $name, $sound, $type ) = - $line =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; + ( $number, $name, $sound, $type ) = $line =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; $number =~ s/\s*$//; #trim the fat off the end $name =~ s/\s*$//; #trim the fat off the end @@ -179,9 +175,7 @@ sub read_reject_call_list { $type = 'general' unless $type; # print_log "Number;$number, Name;$name, Sound;$sound, Type;$type\n"; - push @calls, - sprintf( "number=%-12s name=%s sound=%s type=%s", - $number, $name, $sound, $type ); + push @calls, sprintf( "number=%-12s name=%s sound=%s type=%s", $number, $name, $sound, $type ); } close MYFILE; diff --git a/web/bin/phone_out.pl b/web/bin/phone_out.pl index ef1b1d882..a5b9e2922 100644 --- a/web/bin/phone_out.pl +++ b/web/bin/phone_out.pl @@ -11,8 +11,7 @@ my ( $time, $num, $name, $line, $type, $dur, $ext, $color, $coloroff ); $coloroff = ""; - ( $time, $num, $name, $line, $type ) = $r =~ - /date=(.+\d+:\d+:\d+) number=(\S+) +name=(.*?) line=(\S*) type=(\S*)/; + ( $time, $num, $name, $line, $type ) = $r =~ /date=(.+\d+:\d+:\d+) number=(\S+) +name=(.*?) line=(\S*) type=(\S*)/; ( $dur, $ext ) = $r =~ / dur=(\S*) ext=(\S*)/; $name = '' unless $name; @@ -35,8 +34,7 @@ # $color = "" if ( $name ne 'NA' ) ; - $html_calls .= - "
    $color$time$coloroff $color$num$coloroff $color$name$coloroff
    $color$time$coloroff $color$num$coloroff $color$name$coloroff
    # Calls<\/th>Phone<\/th>Name<\/th>Last Call<\/th><\/tr>/i; - $results = - "
    $results"; + $results = ""; $pos = $pos + 1; @@ -45,9 +44,7 @@ sub rejected_call_list {
    $results"; $results =~ s/\n\n/\n/g; $results =~ s/ *([\d-P]*) calls= *(\d*) *last= ([a-zA-Z0-9 :]{24}) *([^\n]*)\n/\n
     $2 <\/td> $1 <\/td> $4 <\/td> $3 <\/td><\/tr>\n/g; diff --git a/web/bin/photo_search.pl b/web/bin/photo_search.pl index cee28fc22..79a98389c 100644 --- a/web/bin/photo_search.pl +++ b/web/bin/photo_search.pl @@ -21,7 +21,7 @@ $string =~ s/search=//; # Allow for ?string or ?search=string -use vars '@photos'; # This will be persistent across passes and code reloads +use vars '@photos'; # This will be persistent across passes and code reloads @photos = file_read $config_parms{photo_index} unless @photos; my @match = sort grep /$string/i, @photos; @@ -33,8 +33,7 @@ print_log $results; my $html .= "$results"; $html .= " (only first $limit are shown)" if $count1 > $limit; -$html .= - ". Search Again. Back to photo slideshow\n
    \n"; +$html .= ". Search Again. Back to photo slideshow\n
    \n"; my $i; my ( $index, $photos ); @@ -49,8 +48,7 @@ if ( $i < $limit ) { $index .= "
    $name\n"; $href++; - $photos .= - "

    Back to top. $name
    \n"; + $photos .= "

    Back to top. $name
    \n"; } else { $index .= "
    $name\n"; diff --git a/web/bin/photos.pl b/web/bin/photos.pl index 8fa326597..9492be789 100644 --- a/web/bin/photos.pl +++ b/web/bin/photos.pl @@ -29,8 +29,7 @@ my $time = $config_parms{photo_time}; $time = 60 unless defined $time; $config_parms{photo_url} = '/ia5' unless $config_parms{photo_url}; -my $background = - ( $config_parms{photo_background} =~ /$Http{'User-Agent'}/ ) ? 1 : 0; +my $background = ( $config_parms{photo_background} =~ /$Http{'User-Agent'}/ ) ? 1 : 0; #$background = 1; my $browser_size = &http_agent_size; @@ -44,13 +43,8 @@ # If sync, and requested photo does not match up with the last # photo served, then change the refresh time to sync up the 2 browsers -elsif ( - $parm eq 'sync' - and !( - $i == $Save{photo_index} + 1 - or ( $i == 0 and $Save{photo_index} == $#photos ) - ) - ) +elsif ( $parm eq 'sync' + and !( $i == $Save{photo_index} + 1 or ( $i == 0 and $Save{photo_index} == $#photos ) ) ) { $i = $Save{photo_index}; my $time_diff = $Time - $Save{photo_index_time}; @@ -131,8 +125,7 @@ my $time2 = $time * 1000; # This is simplier. -my $refresh = - ""; +my $refresh = ""; # This can cause problems. When the java timer expires # to triggers a refresh on the Audrey, it will interrupt any other voyager browser activity @@ -150,8 +143,7 @@ # Create header html with optional search my $header = ''; -$header = - "$i : +$header = "$i : $photo_name" unless $config_parms{photo_no_title}; $header = "
    diff --git a/web/bin/photos_slideshow.pl b/web/bin/photos_slideshow.pl index 44d04e4ee..4e691d9ac 100644 --- a/web/bin/photos_slideshow.pl +++ b/web/bin/photos_slideshow.pl @@ -76,8 +76,7 @@ if ( $captions ne '0' ) { $sseffect .= "captions: true, "; } -$sseffect .= - "controller: true, delay: ${time}000, duration: 1000, height: $height, hu: '$config_parms{photo_dirs}', "; +$sseffect .= "controller: true, delay: ${time}000, duration: 1000, height: $height, hu: '$config_parms{photo_dirs}', "; if ( $thumbs ne '0' ) { $sseffect .= "thumbnails: true, "; } diff --git a/web/bin/rejectcall.pl b/web/bin/rejectcall.pl index 00530414a..92b3b2698 100644 --- a/web/bin/rejectcall.pl +++ b/web/bin/rejectcall.pl @@ -20,8 +20,7 @@ sub rejected_call_list { my $html_calls; my @calls = read_reject_call_list; for my $r (@calls) { - my ( $number, $name, $sound, $type ) = - $r =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; + my ( $number, $name, $sound, $type ) = $r =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; $html_calls .= "
    Delete $number$name$sound$type
    "; - my $form_type = - &html_form_select( 'type', 0, 'Friend', 'Friend', 'Business', 'reject', - 'Family' ); + my $form_type = &html_form_select( 'type', 0, 'Friend', 'Friend', 'Business', 'reject', 'Family' ); #form action='/bin/items.pl?add' method=post> @@ -106,8 +103,7 @@ sub rej_call_item_delete { my @ReadRejFile = read_reject_call_list; unlink "$phone_dir/phone.caller_id.list"; for $line (@ReadRejFile) { - ( $number, $name, $sound, $type ) = - $line =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; + ( $number, $name, $sound, $type ) = $line =~ /number=(.+) name=(.+) sound=(.*) type=(.*)/; $number =~ s/\s*$//; #trim the fat off the end $name =~ s/\s*$//; #trim the fat off the end @@ -115,8 +111,7 @@ sub rej_call_item_delete { $type =~ s/\s*$//; #trim the fat off the end $writeparms = "$number\t\t$name\t\t$sound\t\t$type"; if ( $filePos ne $deletepos ) { - &rej_call_file_write( "$phone_dir/phone.caller_id.list", - $writeparms ); + &rej_call_file_write( "$phone_dir/phone.caller_id.list", $writeparms ); } diff --git a/web/bin/rss_logs.pl b/web/bin/rss_logs.pl index c28ff87f9..76a0a5f8e 100644 --- a/web/bin/rss_logs.pl +++ b/web/bin/rss_logs.pl @@ -53,8 +53,7 @@ use vars '%rss_logs_data'; if ($article) { - return &html_page( "RSS $log article $article", - "Log: $log
    Article: $article
    $rss_logs_data{$log}{$article}" ); + return &html_page( "RSS $log article $article", "Log: $log
    Article: $article
    $rss_logs_data{$log}{$article}" ); } else { return &rss_log($log); @@ -70,9 +69,7 @@ sub rss_log { $config_parms{rss_maxitems} = 100 unless $config_parms{rss_maxitems}; $config_parms{rss_image} = "http://misterhouse.sourceforge.net/mh_logo.gif" unless $config_parms{rss_image}; - $config_parms{rss_image} = - "http://$config_parms{http_server}" - . ":$config_parms{http_port}/ia5/images/mhlogo.gif" + $config_parms{rss_image} = "http://$config_parms{http_server}" . ":$config_parms{http_port}/ia5/images/mhlogo.gif" if $config_parms{http_server} and $config_parms{http_port} and not $config_parms{rss_image}; @@ -100,8 +97,7 @@ sub rss_log { # still working on the link for each item. can we follow this particular item for more detail? # maybe hook up phone number entry -> address book lookup, etc. obviously TODO - my $rss_link = - "http://$config_parms{http_server}:$config_parms{http_port}/bin/rss_logs.pl?$log&"; + my $rss_link = "http://$config_parms{http_server}:$config_parms{http_port}/bin/rss_logs.pl?$log&"; print "\tparsing $log log\n" if $Debug{rss}; my $article = 0; @@ -114,8 +110,7 @@ sub rss_log { # Create rss item from each line in log for my $r (@calls) { $article++; - my ( $time_date, $num, $name, $line, $type ) = - $r =~ /date=(.+) number=(.+) name=(.+) line=(.*) type=(.*)/; + my ( $time_date, $num, $name, $line, $type ) = $r =~ /date=(.+) number=(.+) name=(.+) line=(.*) type=(.*)/; ( $time_date, $num, $name ) = $r =~ /(.+\d+:\d+:\d+) (\S+) (.+)/ unless $name; next unless $num; @@ -124,11 +119,10 @@ sub rss_log { # print "db t=$time_date t2=$time_date2 t3=$time_date3\n"; $rss->add_item( - title => $time_date, - link => $rss_link . $article, - pubDate => $time_date3, - description => - "Incoming call from $num ($name). line=$line type=$type." + title => $time_date, + link => $rss_link . $article, + pubDate => $time_date3, + description => "Incoming call from $num ($name). line=$line type=$type." ); $rss_logs_data{$log}{$article} = $r; last if $article >= $config_parms{rss_maxitems}; @@ -136,9 +130,7 @@ sub rss_log { } else { # Reverse logdata to show newest items first - for - my $line ( reverse file_read "$config_parms{data_dir}/logs/$log.log" ) - { + for my $line ( reverse file_read "$config_parms{data_dir}/logs/$log.log" ) { next unless $line; $article++; my ( $time_date, $data ) = $line =~ /(.+ [AP]M) (.+)$/; diff --git a/web/bin/runit.pl b/web/bin/runit.pl index 801133b20..a325ab621 100644 --- a/web/bin/runit.pl +++ b/web/bin/runit.pl @@ -23,8 +23,7 @@ sub runit { $cmd =~ s/switch //g; if ( &process_external_command( $cmd, 1, 'android', 'speak' ) ) { - print_log - "-------------------- Exact Command Match removing articles $cmd"; + print_log "-------------------- Exact Command Match removing articles $cmd"; return &html_page( '', 'done' ); } diff --git a/web/bin/set_cookie.pl b/web/bin/set_cookie.pl index 9df2b5639..1257ebe6e 100644 --- a/web/bin/set_cookie.pl +++ b/web/bin/set_cookie.pl @@ -60,10 +60,8 @@ my $html = "Off\n"; - $html .= - " On\n"; + $html .= " Off\n"; + $html .= " On\n"; $html .= "\n"; # Checkboxes do not return values when not checked :( diff --git a/web/bin/set_cookie2.pl b/web/bin/set_cookie2.pl index e26611dfa..c2f0add8a 100644 --- a/web/bin/set_cookie2.pl +++ b/web/bin/set_cookie2.pl @@ -60,7 +60,6 @@ $state2 = '' if defined $Cookies{$string} and $Cookies{$string} eq '0'; my $image = "/graphics/${string}_${state1}.gif"; - return - "$string $state1\n"; + return "$string $state1\n"; } diff --git a/web/bin/set_func.pl b/web/bin/set_func.pl index 8fd77d5c7..c7bfdd798 100644 --- a/web/bin/set_func.pl +++ b/web/bin/set_func.pl @@ -35,12 +35,15 @@ } } $func = "$func($args)"; - #&main::print_log("func=$func($args)"); + + #&main::print_log("func=$func($args)"); my $html = eval $func; print "\nError in set_func.pl: $@\n" if $@; -#print_log "html=$html"; + + #print_log "html=$html"; return $html if $html; # Allow function to override response -#print_log "redir=" &http_redirect($resp). + + #print_log "redir=" &http_redirect($resp). return &http_redirect($resp); } diff --git a/web/bin/set_parm_tv_provider.pl b/web/bin/set_parm_tv_provider.pl index b165f68ab..fd187c074 100644 --- a/web/bin/set_parm_tv_provider.pl +++ b/web/bin/set_parm_tv_provider.pl @@ -66,13 +66,9 @@ sub edit { # Update tv_provider_name if changed if ( $args{provider} and $providers{ $args{provider} } - and $providers{ $args{provider} } ne - $config_parms{tv_provider_name} ) + and $providers{ $args{provider} } ne $config_parms{tv_provider_name} ) { - my %parms = ( - 'tv_provider', '', 'tv_provider_name', - $providers{ $args{provider} } - ); + my %parms = ( 'tv_provider', '', 'tv_provider_name', $providers{ $args{provider} } ); print "db writing out new parm: $config_parms{tv_provider_name}\n"; &write_mh_opts( \%parms ); &read_parms; # Re-read parms @@ -91,9 +87,7 @@ sub edit { $html .= ' selected' if $id eq $config_parms{tv_provider} or $name eq $config_parms{tv_provider_name}; - $html .= '>' - . $name - . '          ' . "\n"; + $html .= '>' . $name . '          ' . "\n"; } $html .= ''; diff --git a/web/bin/set_parm_weather_local.pl b/web/bin/set_parm_weather_local.pl index 8d67f9c89..87d92be3c 100644 --- a/web/bin/set_parm_weather_local.pl +++ b/web/bin/set_parm_weather_local.pl @@ -14,8 +14,7 @@ if ( &get_parm_file( \%config_parms, 'web_href_weather_local_image' ) ) { $html = '
    Our local weather.'; } - $html .= - '
    Reset the mh.ini parm for this page according to your zip code'; + $html .= '
    Reset the mh.ini parm for this page according to your zip code'; return html_page '', $html; } else { @@ -27,24 +26,18 @@ return &html_page( '', "Connect to the internet and try again" ) unless net_ping 'www.google.com'; - $html = get - "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=$config_parms{zip_code}"; - my ($id) = - $html =~ m|Local Radar|i; + $html = get "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=$config_parms{zip_code}"; + my ($id) = $html =~ m|Local Radar|i; speak "Weather i d is $id"; # Update the mh.ini parm if changed - my $parm1 = - "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=$config_parms{zip_code}"; + my $parm1 = "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=$config_parms{zip_code}"; my $parm2 = "http://radar.wunderground.com/data/nids/${id}_0.gif"; if ( $parm1 ne $config_parms{web_href_weather_local} or $parm2 ne $config_parms{web_href_weather_local_image} ) { - my %parms = ( - 'web_href_weather_local', $parm1, - 'web_href_weather_local_image', $parm2 - ); + my %parms = ( 'web_href_weather_local', $parm1, 'web_href_weather_local_image', $parm2 ); main::write_mh_opts( \%parms ); # my $parmfile = $Pgm_Path . "/mh.private.ini"; @@ -53,7 +46,7 @@ } $html = - "Weather page result:
    • Zipcode used: $config_parms{zip_code}" + "Weather page result:
      • Zipcode used: $config_parms{zip_code}" . "
      • ID: $id
      • web_href_weather_local: $parm1
      • web_href_weather_local_image: $parm2
      "; return html_page '', $html; diff --git a/web/bin/shopping_list.pl b/web/bin/shopping_list.pl index 3bb574daf..9beb7990f 100644 --- a/web/bin/shopping_list.pl +++ b/web/bin/shopping_list.pl @@ -191,20 +191,14 @@ sub shoppingListError { $param{'action'} = 'list' unless defined( $param{'action'} ); $param{'action'} = 'list' if $param{'action'} eq 'cancel'; -my $printing = ( - ( $param{'action'} eq 'print' ) - or ( $param{'action'} eq 'print preview' ) - or ( $param{'action'} eq 'e-mail' ) -); -my $atShop = ( - ( $param{'action'} eq 'at shop' ) - or ( $param{'action'} eq 'remove items' ) -); +my $printing = + ( ( $param{'action'} eq 'print' ) or ( $param{'action'} eq 'print preview' ) or ( $param{'action'} eq 'e-mail' ) ); +my $atShop = + ( ( $param{'action'} eq 'at shop' ) or ( $param{'action'} eq 'remove items' ) ); if ( $param{'action'} eq 'add item' ) { if ( ( $param{'category'} eq 'select' ) or ( $param{'item'} eq '' ) ) { - $html .= - qq[

      Category: \n\n]; open( SHOPLIST, $file ) || return shoppingListError("$file: $!"); while () { chomp; @@ -213,24 +207,20 @@ sub shoppingListError { } close(SHOPLIST); $html .= qq[

       

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

      Item:


      ]; - $html .= - qq[

      Add to $prettyName Now:


      ]; + $html .= qq[

      Item:


      ]; + $html .= qq[

      Add to $prettyName Now:


      ]; $html .= qq[

      \n]; $html .= qq[

      \n]; $html .= qq[\n]; $html .= ''; - $html .= &insert_keyboard( - { form => 'main', target => 'item', autocap => 'yes' } ); + $html .= &insert_keyboard( { form => 'main', target => 'item', autocap => 'yes' } ); $html .= qq[\n]; return html_page( undef, $html ); } open( OLDLIST, $file ) || return shoppingListError("$file: $!"); my $duplicate = 0; my $newvalue = $param{'onlistnow'} eq 'on' ? '1' : '0'; - $html .= - "

      Param onlistnow is $param{'onlistnow'} so newvalue is $newvalue

      \n" + $html .= "

      Param onlistnow is $param{'onlistnow'} so newvalue is $newvalue

      \n" if $shoppinglistdebug; while () { @@ -369,30 +359,19 @@ sub shoppingListError { } else { if ($atShop) { - $sectionHeader .= - qq[\n

      $1

      ]; - $sectionHeader .= - qq[

      \n]; + $sectionHeader .= qq[\n

      $1

      ]; + $sectionHeader .= qq[

      \n]; } else { - $sectionHeader .= - qq[

      \n]; - $sectionHeader .= - qq[\n]; - $sectionHeader .= - qq[\n]; - $sectionHeader .= - qq[\n]; - $sectionHeader .= - qq[\n]; - $sectionHeader .= - qq[\n]; - $sectionHeader .= - qq[\n]; - $sectionHeader .= - qq[

      \n]; - $sectionHeader .= - qq[

      $1

      \n]; + $sectionHeader .= qq[

      \n]; + $sectionHeader .= qq[\n]; + $sectionHeader .= qq[\n]; + $sectionHeader .= qq[\n]; + $sectionHeader .= qq[\n]; + $sectionHeader .= qq[\n]; + $sectionHeader .= qq[\n]; + $sectionHeader .= qq[

      \n]; + $sectionHeader .= qq[

      $1

      \n]; } } next; @@ -427,10 +406,8 @@ sub shoppingListError { $html .= qq[\n]; } else { - $html .= qq[\n]; + $html .= qq[\n]; } } } diff --git a/web/bin/soapcgi.pl b/web/bin/soapcgi.pl index dfd0aec41..8c24ed3c5 100644 --- a/web/bin/soapcgi.pl +++ b/web/bin/soapcgi.pl @@ -24,8 +24,7 @@ my ( $key, $header ); while ( ( $key, $header ) = each %Http ) { logit "$config_parms{data_dir}/logs/soapdebug.log", "$key $header"; - logit "$config_parms{data_dir}/logs/soapdebug.log", - "$ENV{HTTP_QUERY_STRING}"; + logit "$config_parms{data_dir}/logs/soapdebug.log", "$ENV{HTTP_QUERY_STRING}"; } } diff --git a/web/bin/status_line.pl b/web/bin/status_line.pl index 27aa9819a..ec909ce31 100644 --- a/web/bin/status_line.pl +++ b/web/bin/status_line.pl @@ -71,8 +71,7 @@ $html .= qq[\n]; -$html .= - qq[
      $item'; - $html .= - qq[$item'; + $html .= qq[$item
      \n]; +$html .= qq[
      \n]; $html .= qq[\n"; if ( $parms{date} ) { - $html .= - qq[\n]; + $html .= qq[\n]; } if ( $parms{clock} ) { $html .= qq[\n]; } if ( $parms{jclock1} ) { - $html .= - qq[\n]; + $html .= qq[\n]; } if ( $parms{jclock2} ) { - $html .= - qq[\n]; + $html .= qq[\n]; } $html .= qq[
      $fontstart\n]; # Do parms in specified order @@ -86,18 +85,13 @@ # Allow for sun (auto-pick), sunrise, or sunset elsif ( $parm =~ /sun/ ) { if ( $parm eq 'sun' ) { - $parm = ( - time_less_than "$Time_Sunrise + 2:00" - or time_greater_than "$Time_Sunset + 2:00" - ) ? 'sunrise' : 'sunset'; + $parm = ( time_less_than "$Time_Sunrise + 2:00" or time_greater_than "$Time_Sunset + 2:00" ) ? 'sunrise' : 'sunset'; } if ( $parm eq 'sunrise' ) { - $html .= - qq[  Rise $Time_Sunrise\n]; + $html .= qq[  Rise $Time_Sunrise\n]; } else { - $html .= - qq[  Set $Time_Sunset\n]; + $html .= qq[  Set $Time_Sunset\n]; } } @@ -124,27 +118,23 @@ elsif ( $parm eq 'email' ) { $Save{email_flag} = '' unless $Save{email_flag}; - $html .= - qq[ $Save{email_flag}\n]; + $html .= qq[ $Save{email_flag}\n]; } elsif ( $parm eq 'weather' ) { $Weather{Summary_Short} = '' unless $Weather{Summary_Short}; - $html .= - qq[  $Weather{Summary_Short}\n]; + $html .= qq[  $Weather{Summary_Short}\n]; } elsif ( $parm eq 'weather_long' ) { $Weather{Summary} = '' unless $Weather{Summary}; - $html .= - qq[  $Weather{Summary}\n]; + $html .= qq[  $Weather{Summary}\n]; } elsif ( $parm eq 'wind' ) { my $html_wind = $Weather{Wind}; $html_wind = '' unless $html_wind; $html_wind =~ s/from the/ /; - $html .= - qq[  $html_wind\n]; + $html .= qq[  $html_wind\n]; } # Allow for user defined html (e.g. code/bruce/web_sub.pl) @@ -157,19 +147,16 @@ $html .= "$fontstart$Date_Now$fontstart$Date_Now${fontstart} $Time_Now
      ${fontstart} $Time_Now
      ${fontstart} $Time_Now
      \n]; diff --git a/web/bin/status_panel.pl b/web/bin/status_panel.pl index 3370f3836..0d9d98a49 100644 --- a/web/bin/status_panel.pl +++ b/web/bin/status_panel.pl @@ -22,10 +22,7 @@ # Allow for sun (auto-pick), sunrise, or sunset elsif ( $parm =~ /sun/ ) { if ( $parm eq 'sun' ) { - $parm = ( - time_less_than "$Time_Sunrise + 2:00" - or time_greater_than "$Time_Sunset + 2:00" - ) ? 'sunrise' : 'sunset'; + $parm = ( time_less_than "$Time_Sunrise + 2:00" or time_greater_than "$Time_Sunset + 2:00" ) ? 'sunrise' : 'sunset'; } if ( $parm eq 'sunrise' ) { $html .= qq[

      Sunrise: $Time_Sunrise

      \n]; diff --git a/web/bin/triggers.pl b/web/bin/triggers.pl index c55cfbd3f..6b3873324 100644 --- a/web/bin/triggers.pl +++ b/web/bin/triggers.pl @@ -12,7 +12,7 @@ $^W = 0; # Avoid redefined sub msgs my ( $function, @parms ) = @ARGV; -my ($mode) = ($Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\//); +my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); #print "dbx a=@ARGV.\n"; @@ -31,12 +31,9 @@ sub web_trigger_list { my $html = &html_header('Triggers Menu'); my $form_trigger = &html_form_select( 'trigger1', 0, 'time_now', '', - qw(time_now time_cron time_random new_second new_minute new_hour $New_Hour $New_Day $New_Week $New_Month $New_Year) - ); + qw(time_now time_cron time_random new_second new_minute new_hour $New_Hour $New_Day $New_Week $New_Month $New_Year) ); - my $form_code = &html_form_select( 'code1', 0, 'speak', '', - qw(speak play display print_log set run run_voice_cmd net_im_send net_mail_send get) - ); + my $form_code = &html_form_select( 'code1', 0, 'speak', '', qw(speak play display print_log set run run_voice_cmd net_im_send net_mail_send get) ); $html = qq| @@ -102,17 +99,14 @@ sub web_trigger_list { ) { - my ( $trigger, $code, $type, $triggered, $trigger_error, $code_error ) - = trigger_get($name); + my ( $trigger, $code, $type, $triggered, $trigger_error, $code_error ) = trigger_get($name); if ( $type_prev ne $type ) { $html .= "
    \n" if $type_prev; $type_prev = $type; - $html .= - "

    $type: (back to top)\n"; + $html .= "

    $type: (back to top)\n"; $html .= qq|\n|; - $html .= - "\n"; + $html .= "\n"; } my $name2 = $name; @@ -122,45 +116,33 @@ sub web_trigger_list { $code =~ s/"/"/g; $html .= "\n"; - - $html .= - &html_form_input_set_func( 'trigger_rename', '/bin/triggers.pl', - $name, $name ); + $html .= "Copy\n"; + $html .= " Delete\n"; + $html .= " Run\n"; + + $html .= &html_form_input_set_func( 'trigger_rename', '/bin/triggers.pl', $name, $name ); $html .= qq|\n| if $trigger_error; - $html .= - &html_form_input_set_func( 'trigger_set_trigger', '/bin/triggers.pl', - $name, $trigger ); + $html .= &html_form_input_set_func( 'trigger_set_trigger', '/bin/triggers.pl', $name, $trigger ); $html .= "\n" if $trigger_error; $html .= qq|\n| if $code_error; - $html .= - &html_form_input_set_func( 'trigger_set_code', '/bin/triggers.pl', - $name, $code ); + $html .= &html_form_input_set_func( 'trigger_set_code', '/bin/triggers.pl', $name, $code ); $html .= "\n" if $code_error; - $html .= - &html_form_select_set_func( 'trigger_set_type', '/bin/triggers.pl', - $name, $type, 'OneShot', 'NoExpire', 'Disabled', 'Expired' ); + $html .= &html_form_select_set_func( 'trigger_set_type', '/bin/triggers.pl', $name, $type, 'OneShot', 'NoExpire', 'Disabled', 'Expired' ); if ($triggered) { my $triggered_date = &time_date_stamp( 7, $triggered ) if $triggered; $html .= "\n"; - } else { - $html .= "\n"; #put in a blank cell. + } + else { + $html .= "\n"; #put in a blank cell. } $html .= "\n\n"; - $html .= - qq|\n\n\n| + $html .= qq|\n\n\n| if $trigger_error; - $html .= - qq|\n\n\n| + $html .= qq|\n\n\n| if $code_error; } $html .= "
    NameTrigger EventAction CodeTypeLast Run
    NameTrigger EventAction CodeTypeLast Run
    "; - $html .= - "Copy\n"; - $html .= - " Delete\n"; - $html .= - " Run$triggered_date
    Trigger Event Error: $trigger_error
    Trigger Event Error: $trigger_error
    Action Code Error: $code_error
    Action Code Error: $code_error
    \n"; @@ -202,13 +184,10 @@ sub web_trigger_add { # Create form else { - my $html = - "Add a trigger:

    \n"; + my $html = "Add a trigger:\n"; $html .= qq|
    Name \n|; - $html .= - qq|
    Trigger\n|; - $html .= - qq|
    Event \n|; + $html .= qq|
    Trigger\n|; + $html .= qq|
    Event \n|; $html .= qq|
    Type @@ -20,7 +19,7 @@ elsif ( 'asterisk' eq lc $config_parms{phone_voicemail_type} ) { my $mailbox = ($Authorized) ? $config_parms{phone_voicemail_number} : '25'; - my $pin = ($Authorized) ? $config_parms{phone_voicemail_pin} : '9999'; + my $pin = ($Authorized) ? $config_parms{phone_voicemail_pin} : '9999'; return qq[ @@ -32,12 +31,10 @@
    ]; } elsif ( 'vocp' eq lc $config_parms{phone_voicemail_type} ) { - return - qq[Voice Mail]; + return qq[Voice Mail]; } else { - return - qq[Voice mail
    ]; + return qq[Voice mail
    ]; } diff --git a/web/bin/wc_settings.pl b/web/bin/wc_settings.pl index 43f4dc97c..9a50cdebd 100644 --- a/web/bin/wc_settings.pl +++ b/web/bin/wc_settings.pl @@ -41,8 +41,8 @@ # We use the wc_address_x from 0 to wc_max - 1 # if we pass anything in output changes to html snippet -my $outmode = @ARGV[0]; # arg is number of columns to format to -my $arg2 = @ARGV[1]; # if we have a second arg it is the url for full frame +my $outmode = @ARGV[0]; # arg is number of columns to format to +my $arg2 = @ARGV[1]; # if we have a second arg it is the url for full frame my ( $outimage, $rest ) = split / /, $arg2; # because we may have extra stuff my $html = " \n "; @@ -72,8 +72,7 @@ #Add this cameras settings into the string my $wcURL = $config_parms{$wcThis}; my ( $wcURL, $wcDescr ) = split( /\,/, $wcURL ); - $scriptlet .= - "webCamRegisterCam(\"" . $wcx . ": $wcDescr\", \"$wcURL\", $wcx); \n"; + $scriptlet .= "webCamRegisterCam(\"" . $wcx . ": $wcDescr\", \"$wcURL\", $wcx); \n"; $html .= "
    [0] . ".png?" . int(100000*rand) . "'>\n"; - $html .= - "\'/rrd/weather_" - . $typegraph . "_" - . $periodgraph->[0] - . ".$rrd_format?" - . time() - . '\' border=0>

    '; + $html .= "\'/rrd/weather_" . $typegraph . "_" . $periodgraph->[0] . ".$rrd_format?" . time() . '\' border=0>

    '; } } } diff --git a/web/bin/weather_graph_zoom.pl b/web/bin/weather_graph_zoom.pl index 74b43060c..90b38b8ed 100755 --- a/web/bin/weather_graph_zoom.pl +++ b/web/bin/weather_graph_zoom.pl @@ -58,9 +58,7 @@ $native_rrd_format = 'png'; } else { - &print_log( - 'weather_graph_zoom: you need to define weather_convert_png_to_gif in order for me to create GIFs with rrdtool version 1.2+' - ); + &print_log('weather_graph_zoom: you need to define weather_convert_png_to_gif in order for me to create GIFs with rrdtool version 1.2+'); $rrd_format = 'png'; } } @@ -100,33 +98,25 @@ # my $day = $cgi->param('debday'); # my $month = $cgi->param('debmonth'); # my $year = $cgi->param('debyear'); - my $debepochtime = timelocal( - 0, 0, 0, - $cgi->param('debday'), - $cgi->param('debmonth') - 1, - $cgi->param('debyear') - 1900 - ); + my $debepochtime = timelocal( 0, 0, 0, $cgi->param('debday'), $cgi->param('debmonth') - 1, $cgi->param('debyear') - 1900 ); # Convert end date in Epoch date - my $endepochtime = timelocal( - 59, 59, 23, - $cgi->param('endday'), - $cgi->param('endmonth') - 1, - $cgi->param('endyear') - 1900 - ); + my $endepochtime = timelocal( 59, 59, 23, $cgi->param('endday'), $cgi->param('endmonth') - 1, $cgi->param('endyear') - 1900 ); if ( $endepochtime > time() ) { $endepochtime = time(); } if ( $debepochtime <= $endepochtime ) { my %sensor_names; - &main::read_parm_hash( \%sensor_names, - $main::config_parms{weather_graph_sensor_names} ); + &main::read_parm_hash( \%sensor_names, $main::config_parms{weather_graph_sensor_names} ); &create_rrdgraph_zoom( - $debepochtime, $endepochtime, - $cgi->param('width'), $cgi->param('height'), - $cgi->param('sensor1'), $sensor_names{ $cgi->param('sensor1') }, - $cgi->param('sensor2'), $sensor_names{ $cgi->param('sensor2') } + $debepochtime, $endepochtime, + $cgi->param('width'), + $cgi->param('height'), + $cgi->param('sensor1'), + $sensor_names{ $cgi->param('sensor1') }, + $cgi->param('sensor2'), + $sensor_names{ $cgi->param('sensor2') } ); } else { @@ -176,9 +166,7 @@ sub print_graph { ); print $cgi->start_Tr; print $cgi->start_td( { -align => 'center' } ); - print $cgi->img( - { src => "/rrd/weather_zoom.$rrd_format?" . time(), align => 'center' } - ); + print $cgi->img( { src => "/rrd/weather_zoom.$rrd_format?" . time(), align => 'center' } ); print $cgi->end_td; print $cgi->end_Tr; print $cgi->end_table(); @@ -232,13 +220,11 @@ sub print_prompts { -cellspacing => "0" } ); - print $cgi->start_form( - { -method => 'GET', -action => '/bin/weather_graph_zoom.pl' } ); + print $cgi->start_form( { -method => 'GET', -action => '/bin/weather_graph_zoom.pl' } ); # Choose RRD database print $cgi->start_Tr; - print $cgi->start_td( - { -align => 'right', valign => "center", width => "90" } ); + print $cgi->start_td( { -align => 'right', valign => "center", width => "90" } ); print 'Round Robin Database'; print $cgi->end_td; $RRDDIR = "$config_parms{rrd_dir}"; @@ -260,10 +246,8 @@ sub print_prompts { print 'Sensor 2'; print $cgi->end_td; - print $cgi->start_td( - { -align => 'left', -colspan => 1, valign => "center" } ); - &main::read_parm_hash( \%sensor_names, - $main::config_parms{weather_graph_sensor_names} ); + print $cgi->start_td( { -align => 'left', -colspan => 1, valign => "center" } ); + &main::read_parm_hash( \%sensor_names, $main::config_parms{weather_graph_sensor_names} ); for my $sensor_name ( sort keys %sensor_names ) { push @list_sensors, $sensor_name; } @@ -292,15 +276,13 @@ sub print_prompts { ); print $cgi->end_td; - print $cgi->start_td( - { -align => 'right', -colspan => 1, valign => "center" } ); + print $cgi->start_td( { -align => 'right', -colspan => 1, valign => "center" } ); print $cgi->start_b; print "Width "; print $cgi->br; print "Height "; print $cgi->end_td; - print $cgi->start_td( - { -align => 'left', -colspan => 1, valign => "center" } ); + print $cgi->start_td( { -align => 'left', -colspan => 1, valign => "center" } ); print $cgi->popup_menu( -name => 'width', -size => 1, @@ -485,10 +467,7 @@ sub get_footer1 { else { $colon = ':'; } - $footer = - "Step size$colon " - . convertstepz($step) - . " Data points$colon $datapoint"; + $footer = "Step size$colon " . convertstepz($step) . " Data points$colon $datapoint"; return $footer; } @@ -511,10 +490,9 @@ sub get_footer2 { sub create_rrdgraph_zoom { my $celgtime; my $create_graph; - my $colordatamoy = 'ff0000'; # color of primary variable average line (red) - my $colordatamoy2 = '330099'; # color of primary variable average line (red) - my $colorna = - 'C0C0C0'; # color for unknown area or 0 for gaps (barre noire verticale) + my $colordatamoy = 'ff0000'; # color of primary variable average line (red) + my $colordatamoy2 = '330099'; # color of primary variable average line (red) + my $colorna = 'C0C0C0'; # color for unknown area or 0 for gaps (barre noire verticale) my $colordatamax = '330099'; # color of min and max my $colordatamax2 = 'FFFF00'; # color of min and max my $colorwhite = 'ffffff'; # color white @@ -529,10 +507,7 @@ sub create_rrdgraph_zoom { my $secs; my $titrerrd; - my ( - $time1, $time2, $gwidth, $gheight, - $sensor1, $libsensor1, $sensor2, $libsensor2 - ) = @_; + my ( $time1, $time2, $gwidth, $gheight, $sensor1, $libsensor1, $sensor2, $libsensor2 ) = @_; print "SENSOR1=$sensor1 LIBSENSOR1=$libsensor1" if $debug; print "SENSOR2=$sensor2 LIBSENSOR2=$libsensor2" if $debug; @@ -554,12 +529,9 @@ sub create_rrdgraph_zoom { $time1 = int( $time1 / $secs ) * $secs; $time2 = int( $time2 / $secs ) * $secs; - $titrerrd = - ( $sensor2 ne "nosensor" ) ? "$libsensor1, $libsensor2" : "$libsensor1"; + $titrerrd = ( $sensor2 ne "nosensor" ) ? "$libsensor1, $libsensor2" : "$libsensor1"; - ( $start, $step, $names, $array ) = - RRDs::fetch "$config_parms{weather_data_rrd}", "AVERAGE", "-s", "$time1", - "-e", "$time2"; + ( $start, $step, $names, $array ) = RRDs::fetch "$config_parms{weather_data_rrd}", "AVERAGE", "-s", "$time1", "-e", "$time2"; $err = RRDs::error; die "ERROR : function RRDs::fetch : $err" if $err; $datapoint = $#$array + 1; @@ -736,8 +708,7 @@ sub create_rrdgraph_zoom { ( $strminvar2 = $strminvar ) =~ s/mindata/mindata2/g; ( $strmaxvar2 = $strmaxvar ) =~ s/maxdata/maxdata2/g; - $str_graph = - qq^RRDs::graph("$rrd_graph_dir/weather_zoom.$native_rrd_format", + $str_graph = qq^RRDs::graph("$rrd_graph_dir/weather_zoom.$native_rrd_format", "--title", "Environmental data : $titrerrd", "--height","$gheight", "--width", "$gwidth", @@ -749,12 +720,7 @@ sub create_rrdgraph_zoom { "--color","SHADEA#0000CC", "--color","SHADEB#0000CC", ^ - . "\"--vertical-label\"," - . "$libuom" - . "\"--start\"," - . "\"$time1\"," - . "\"--end\"," - . "\"$time2\"," + . "\"--vertical-label\"," . "$libuom" . "\"--start\"," . "\"$time1\"," . "\"--end\"," . "\"$time2\"," . "\"DEF:var=$rrd_dir:" . $sensor1 @@ -770,20 +736,17 @@ sub create_rrdgraph_zoom { . "$strmaxvar" . ( - $sensor2 ne "nosensor" - ? "\"DEF:var2=$rrd_dir:" . $sensor2 . ":AVERAGE\"," + $sensor2 ne "nosensor" ? "\"DEF:var2=$rrd_dir:" . $sensor2 . ":AVERAGE\"," : '' ) . ( $sensor2 ne "nosensor" ? "$strvar2" : '' ) . ( - $sensor2 ne "nosensor" - ? "\"DEF:mindata2=$rrd_dir:" . $sensor2 . ":MIN\"," + $sensor2 ne "nosensor" ? "\"DEF:mindata2=$rrd_dir:" . $sensor2 . ":MIN\"," : '' ) . ( $sensor2 ne "nosensor" ? "$strminvar2" : '' ) . ( - $sensor2 ne "nosensor" - ? "\"DEF:maxdata2=$rrd_dir:" . $sensor2 . ":MAX\"," + $sensor2 ne "nosensor" ? "\"DEF:maxdata2=$rrd_dir:" . $sensor2 . ":MAX\"," : '' ) . ( $sensor2 ne "nosensor" ? "$strmaxvar2" : '' ) @@ -798,10 +761,7 @@ sub create_rrdgraph_zoom { "GPRINT:fvar:LAST:Last \\\\: %5.1lf\\\\n", ^ . ( - $sensor2 ne "nosensor" - ? "\"LINE2:fvar2#$colordatamoy2:" - . sprintf( "%-${max}s", $libsensor2 ) - . " (Average)\"," + $sensor2 ne "nosensor" ? "\"LINE2:fvar2#$colordatamoy2:" . sprintf( "%-${max}s", $libsensor2 ) . " (Average)\"," : '' ) . ( @@ -817,8 +777,7 @@ sub create_rrdgraph_zoom { : '' ) . ( - $sensor2 ne "nosensor" - ? "\"GPRINT:fvar2:LAST:Last \\\\: %5.1lf\\\\n\"," + $sensor2 ne "nosensor" ? "\"GPRINT:fvar2:LAST:Last \\\\: %5.1lf\\\\n\"," : '' ) . qq^ @@ -833,18 +792,13 @@ sub create_rrdgraph_zoom { die "ERROR : function RRDs::graph : $err\n" if $err; if ( $rrd_format ne $native_rrd_format ) { if ( $rrd_format eq 'gif' and $native_rrd_format eq 'png' ) { - my $pngFilename = - File::Spec->catfile( $rrd_graph_dir, "weather_zoom.png" ); - my $gifFilename = - File::Spec->catfile( $rrd_graph_dir, "weather_zoom.gif" ); + my $pngFilename = File::Spec->catfile( $rrd_graph_dir, "weather_zoom.png" ); + my $gifFilename = File::Spec->catfile( $rrd_graph_dir, "weather_zoom.gif" ); - system( $config_parms{weather_convert_png_to_gif}, - $pngFilename, $gifFilename ); + system( $config_parms{weather_convert_png_to_gif}, $pngFilename, $gifFilename ); } else { - &print_log( - "weather_graph_zoom: sorry, don't know how to convert from $native_rrd_format to $rrd_format" - ); + &print_log("weather_graph_zoom: sorry, don't know how to convert from $native_rrd_format to $rrd_format"); } } } diff --git a/web/bin/webcam_dirs.pl b/web/bin/webcam_dirs.pl index b8ab92759..ca459927c 100644 --- a/web/bin/webcam_dirs.pl +++ b/web/bin/webcam_dirs.pl @@ -29,19 +29,18 @@ my $html = " -"; # basic html page to the target - # setup our starting point -my @dirs = $search_from; # Start with a simple array of One entry -my @endDirs = ""; # We fill this as we find the ending dirs +"; # basic html page to the target + # setup our starting point +my @dirs = $search_from; # Start with a simple array of One entry +my @endDirs = ""; # We fill this as we find the ending dirs -&recurse_dirs(); # Here we go off and find all the dirs ! +&recurse_dirs(); # Here we go off and find all the dirs ! -@endDirs = sort @endDirs; # Sort our entries because its easier to read +@endDirs = sort @endDirs; # Sort our entries because its easier to read -$html .= - ""; +$html .= "
    "; -my $mycol = 1; # table column counter +my $mycol = 1; # table column counter #my $columns =3; # and limits (width) my $columns = $wcMax - 1; @@ -74,14 +73,7 @@ my ( $jnk, $theName ) = split( /\/\//, $endirs ); my ( $jnk, $myPath ) = split( /\/web/, $endirs ); if ( ($theName) and ( $theName ne "images" ) ) { - $html .= - ""; + $html .= ""; if ( $mycol <= $columns ) { $html .= ""; } if ( $mycol > $columns ) { $html .= ""; $mycol = 0; } $mycol++; @@ -119,16 +111,16 @@ sub get_dirs { next if ( $files =~ /^\./ ); # no dot dirs $thisDir = $search_this . "/" . $files; # usable path - # here we need to strip off everything up to and incl ./web/ + # here we need to strip off everything up to and incl ./web/ ( $base, $here ) = split( /\/web/, $thisDir ); if ( -d $thisDir ) { # we need to see if we're at the end of the line - my $Status = &test_for_dirs(); # use the quick check - if ( $Status eq "end" ) { # to see if were done on this one - push @endDirs, "$thisDir"; # yep save it to the display list + my $Status = &test_for_dirs(); # use the quick check + if ( $Status eq "end" ) { # to see if were done on this one + push @endDirs, "$thisDir"; # yep save it to the display list } - push @dirs, "$thisDir"; #and put on recurson stack + push @dirs, "$thisDir"; #and put on recurson stack } } return; diff --git a/web/bin/webcam_shows.pl b/web/bin/webcam_shows.pl index aab25a01c..2934e2a70 100644 --- a/web/bin/webcam_shows.pl +++ b/web/bin/webcam_shows.pl @@ -16,8 +16,7 @@ my $wcx = "" unless $config_parms{wc_address_1}; # 1 ? # Get the list of directories in the camera directory -my $abs_dir = - $config_parms{html_dir} . "/" . $config_parms{wc_slide_dir} . "/cams"; +my $abs_dir = $config_parms{html_dir} . "/" . $config_parms{wc_slide_dir} . "/cams"; opendir( DIR, $abs_dir ); my @files = grep { !/\.$/ && -d "$abs_dir/$_" } readdir(DIR); #readdir(DIR); closedir(DIR); @@ -27,10 +26,8 @@ my $bgcolor = "#333366"; -my $webdir = - $config_parms{wc_slide_dir} . $config_parms{wc_slide_dir} . "/cams/"; -my $html = - "\n"; +my $webdir = $config_parms{wc_slide_dir} . $config_parms{wc_slide_dir} . "/cams/"; +my $html = "\n"; $html .= "
    " - . $theName - . " (" - . $numImages - . " Images) " . $theName . " (" . $numImages . " Images)
    \n"; # make the links to the movie files ... @@ -56,25 +53,14 @@ #Add this cameras settings into the string my $wcURL = $config_parms{$wcThis}; my ( $wcURL, $wcDescr ) = split( /\,/, $wcURL ); - my $currDir = - "/bin/webcam_movie.pl?" . $config_parms{wc_slide_dir} . "/cams/" . $file; - my $href = ""; + my $currDir = "/bin/webcam_movie.pl?" . $config_parms{wc_slide_dir} . "/cams/" . $file; + my $href = ""; $html .= "\n"; - $html .= - ""; - $html .= - ""; + $html .= ""; # my $last_image = $files[$num_images - 1] ; my ( $last_image, $jnk ) = split /\-/, $files[ $num_images - 1 ]; @@ -91,17 +77,9 @@ #$last_image = sprintf ('%04s/2%02s%02d%02d%02d%02d%02d',$last_image, $last_image); } else { $jnk = "None"; } - $html .= - ""; - - $html .= - ""; + + $html .= ""; $html .= "\n"; diff --git a/web/comics/dailystrips/dailystrips-update b/web/comics/dailystrips/dailystrips-update index 8ea5ad1bb..4ea076d73 100644 --- a/web/comics/dailystrips/dailystrips-update +++ b/web/comics/dailystrips/dailystrips-update @@ -28,11 +28,7 @@ $version = "1.0.0"; $dailystrips_version = "1.0.28"; # Get options -GetOptions( - \%options, 'quiet|q', 'verbose', 'proxy=s', - 'proxyauth=s', 'noenvproxy', 'version|v', 'help|h', - 'retries=s', 'updates=s', 'minage=s' -) or exit 1; +GetOptions( \%options, 'quiet|q', 'verbose', 'proxy=s', 'proxyauth=s', 'noenvproxy', 'version|v', 'help|h', 'retries=s', 'updates=s', 'minage=s' ) or exit 1; # Process options: # Note: Blocks have been ordered so that we only do as much as absolutely @@ -83,8 +79,7 @@ unless ( $options{'retries'} ) { if ( $options{'proxy'} ) { $options{'proxy'} =~ /^(http:\/\/)?(.*?):(.+?)\/?$/i; unless ( $2 and $3 ) { - die - "Error: incorrectly formatted proxy server ('http://server:port' expected)\n"; + die "Error: incorrectly formatted proxy server ('http://server:port' expected)\n"; } $options{'proxy'} = "http://$2:$3"; @@ -93,8 +88,7 @@ if ( $options{'proxy'} ) { if ( !$options{'noenvproxy'} and !$options{'proxy'} and $ENV{'http_proxy'} ) { $ENV{'http_proxy'} =~ /(http:\/\/)?(.*?):(.+?)\/?$/i; unless ( $2 and $3 ) { - die - "Error: incorrectly formatted proxy server environment variable\n('http://server:port' expected)\n"; + die "Error: incorrectly formatted proxy server environment variable\n('http://server:port' expected)\n"; } $options{'proxy'} = "http://$2:$3"; @@ -102,8 +96,7 @@ if ( !$options{'noenvproxy'} and !$options{'proxy'} and $ENV{'http_proxy'} ) { if ( $options{'proxyauth'} ) { unless ( $options{'proxyauth'} =~ /^.+?:.+?$/ ) { - die - "Error: incorrectly formatted proxy credentials ('user:pass' expected)\n"; + die "Error: incorrectly formatted proxy credentials ('user:pass' expected)\n"; } } @@ -163,8 +156,7 @@ sub http_get { if ( $response->is_error() ) { if ( $options{'verbose'} ) { - warn - "Warning: could not download $url: $status (attempt $_ of $options{'retries'})\n"; + warn "Warning: could not download $url: $status (attempt $_ of $options{'retries'})\n"; } } else { @@ -180,12 +172,9 @@ sub http_get { sub get_updated_defs { # set parameters - my $updates_file = shift; - my $updates_rev_url = - "http://dailystrips.sourceforge.net/UPDATES/ds-update-" - . $dailystrips_version . ".rev"; - my $updates_url = "http://dailystrips.sourceforge.net/UPDATES/ds-update-" - . $dailystrips_version; + my $updates_file = shift; + my $updates_rev_url = "http://dailystrips.sourceforge.net/UPDATES/ds-update-" . $dailystrips_version . ".rev"; + my $updates_url = "http://dailystrips.sourceforge.net/UPDATES/ds-update-" . $dailystrips_version; my $local_revision = 0; my $remote_revision = 0; @@ -198,9 +187,7 @@ sub get_updated_defs { } # check revision of local file - if ( - open( UPDATES, "<$updates_file" ) - ) # or die "Error: cannot read updates file: $!\n"; + if ( open( UPDATES, "<$updates_file" ) ) # or die "Error: cannot read updates file: $!\n"; { while () { if (/^#REVISION:\s*(\-?\d+)/i) { diff --git a/web/comics/dailystrips/install.pl b/web/comics/dailystrips/install.pl index 996b9b6d4..2c42538fd 100644 --- a/web/comics/dailystrips/install.pl +++ b/web/comics/dailystrips/install.pl @@ -20,8 +20,7 @@ # Not for Win32 if ( $^O =~ /Win32/ ) { - die - "install.pl is not for use on Win32 systems. Please see INSTALL file.\n"; + die "install.pl is not for use on Win32 systems. Please see INSTALL file.\n"; } # Editable paths @@ -100,12 +99,7 @@ die "Error creating documentation directory. See above for reason.\n"; } -if ( - system( - "install BUGS CHANGELOG CONTRIBUTORS COPYING INSTALL README README.DEFS README.LOCAL TODO $options{'docdir'}" - ) - ) -{ +if ( system("install BUGS CHANGELOG CONTRIBUTORS COPYING INSTALL README README.DEFS README.LOCAL TODO $options{'docdir'}") ) { die "Error installing documentation files. See above for reason.\n"; } @@ -118,12 +112,7 @@ die "Error creating scripts directory. See above for reason.\n"; } -if ( - system( - "install dailystrips dailystrips-clean dailystrips-update $options{'scriptdir'}" - ) - ) -{ +if ( system("install dailystrips dailystrips-clean dailystrips-update $options{'scriptdir'}") ) { die "Error installing script. See above for reason.\n"; } diff --git a/web/ia4/web_sub.pl b/web/ia4/web_sub.pl index e5bc2d63c..d93281490 100644 --- a/web/ia4/web_sub.pl +++ b/web/ia4/web_sub.pl @@ -11,18 +11,13 @@ sub X10Lamp { $o = &get_object_by_name($arg1); return 'not found' unless $o; $objState = $o->state; - $icon = - "" - . $objState - . ""; + $icon = "" . $objState . ""; if ( $objState eq 'on' ) { - $icon = - ""; + $icon = ""; } if ( $objState eq 'off' ) { - $icon = - ""; + $icon = ""; } return $icon; } @@ -54,12 +49,10 @@ sub housemode { $icon = "/graphics/$arg3.gif"; if ( $objState eq 'on' ) { - $icon = - ""; + $icon = ""; } if ( $objState eq 'off' ) { - $icon = - ""; + $icon = ""; } return $icon; } @@ -101,14 +94,12 @@ sub web_phonelog { print_log "Announced Recent Callers."; $NumofCalls = 0; - $log_out = - "
    " - . $href - . " + $html .= "" . $href . " " - . $href . "#" - . $wcx . " " - . $wcDescr . "
    " - . $num_images - . " images
    " - . "
    " . $href . "#" . $wcx . " " . $wcDescr . "
    " . $num_images . " images
    " . "
    " - . $href - . " Last Image
    " . " " - . $jnk - . "
    " - . $href - . " + $html .= "" . $href . " Last Image
    " . " " . $jnk . "
    " . $href . "
    "; + $log_out = "
    "; foreach $CallLogTempLine (@callloglines) { $NumofCalls = $NumofCalls + 1; ( $PhoneDateLog, $PhoneTimeLog, $PhoneNameLog, $PhoneNumberLog ) = ( split( '`', $CallLogTempLine ) )[ 0, 1, 2, 3 ]; - $log_out .= - ""; + $log_out .= ""; $log_out .= ""; } @@ -116,10 +107,8 @@ sub web_phonelog { $log_out .= ""; diff --git a/web/ia5/modes/house_mode.pl b/web/ia5/modes/house_mode.pl index 85fa0e14d..ab203a661 100644 --- a/web/ia5/modes/house_mode.pl +++ b/web/ia5/modes/house_mode.pl @@ -5,5 +5,4 @@ # Authority: anyone -return - "Mode $Save{mode}"; +return "Mode $Save{mode}"; diff --git a/web/ia5/phone/voicemail.pl b/web/ia5/phone/voicemail.pl index 9188db484..6107c0b84 100644 --- a/web/ia5/phone/voicemail.pl +++ b/web/ia5/phone/voicemail.pl @@ -5,9 +5,8 @@ # Authority: anyone if ( 'mci' eq lc $config_parms{phone_voicemail_type} ) { - my $phone = - ($Authorized) ? $config_parms{phone_voicemail_number} : '0001112222'; - my $pin = ($Authorized) ? $config_parms{phone_voicemail_pin} : '9999'; + my $phone = ($Authorized) ? $config_parms{phone_voicemail_number} : '0001112222'; + my $pin = ($Authorized) ? $config_parms{phone_voicemail_pin} : '9999'; return qq[
    @@ -20,7 +19,7 @@ elsif ( 'asterisk' eq lc $config_parms{phone_voicemail_type} ) { my $mailbox = ($Authorized) ? $config_parms{phone_voicemail_number} : '25'; - my $pin = ($Authorized) ? $config_parms{phone_voicemail_pin} : '9999'; + my $pin = ($Authorized) ? $config_parms{phone_voicemail_pin} : '9999'; return qq[ @@ -32,12 +31,10 @@ ]; } elsif ( 'vocp' eq lc $config_parms{phone_voicemail_type} ) { - return - qq[Voice Mail]; + return qq[Voice Mail]; } else { - return - qq[Voice mail
    ]; + return qq[Voice mail
    ]; } diff --git a/web/ia5/pictures/getcomics.pl b/web/ia5/pictures/getcomics.pl index 78454994b..4539ab73f 100644 --- a/web/ia5/pictures/getcomics.pl +++ b/web/ia5/pictures/getcomics.pl @@ -15,10 +15,7 @@ BEGIN my $base; my %pages; -$pages{'dilbert'} = [ - 'http://www.dilbert.com/comics/dilbert/archive/', - 'img.*?src="(/comics/dilbert/archive/images/dilbert\d+.gif)"' -]; +$pages{'dilbert'} = [ 'http://www.dilbert.com/comics/dilbert/archive/', 'img.*?src="(/comics/dilbert/archive/images/dilbert\d+.gif)"' ]; #$pages{'bobbins'} = # [ @@ -157,10 +154,7 @@ BEGIN $filename =~ s|/|.|g; if ( -f $filename ) { - ( - undef, undef, undef, undef, undef, undef, undef, - undef, undef, $mtime, undef, undef, undef - ) = stat($filename); + ( undef, undef, undef, undef, undef, undef, undef, undef, undef, $mtime, undef, undef, undef ) = stat($filename); if ( $mtime > $utime ) { $cached = 1; } @@ -206,8 +200,7 @@ BEGIN next if !defined($content); next if $n < $numrules; - print - " Item $page, content type $contenttype successfully fetched.\n"; + print " Item $page, content type $contenttype successfully fetched.\n"; # Now, filter the page. if ( defined( $filters{$page} ) ) { @@ -276,15 +269,8 @@ BEGIN my $srcurl = ""; $srcurl = " (from $url)
    "; $srcurl .= " ($date)" if $date; - if ( - !( - $compilation =~ - s|()|$page$srcurl\n| - ) - ) - { - $compilation =~ - s|()|$page$srcurl\n$1|; + if ( !( $compilation =~ s|()|$page$srcurl\n| ) ) { + $compilation =~ s|()|$page$srcurl\n$1|; } } } @@ -344,8 +330,7 @@ sub carve_image { } `pnmcut $x $y $w $h $name/$filename 2>/dev/null | ppmtogif 2>/dev/null > $name/$ {name}_$ {x}_$ {y}.gif`; - $html .= - qq(
    ); + $html .= qq(); } $html .= "\n"; } @@ -365,12 +350,12 @@ sub patchurl { eval { if ( !defined( $uri->scheme ) or !$uri->scheme ) { - $uri = new URI $url, ( $base->scheme || 'http' ); # what the hell? + $uri = new URI $url, ( $base->scheme || 'http' ); # what the hell? } # Gack! relative URL! if ( $uri->path !~ m|^/| ) { - local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1; # gack gack + local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1; # gack gack $uri = URI->new($url)->abs($base); } @@ -380,9 +365,9 @@ sub patchurl { } }; - $uri->scheme('http') unless $uri->scheme; # thanks, slashdot + $uri->scheme('http') unless $uri->scheme; # thanks, slashdot - return $url if $@; # bail out if there's an error. + return $url if $@; # bail out if there's an error. $uri->as_string; } diff --git a/web/ia6/date.pl b/web/ia6/date.pl index 7dd47916f..88e0f7052 100644 --- a/web/ia6/date.pl +++ b/web/ia6/date.pl @@ -1,13 +1,7 @@ my $html; -my @months = ( - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December' -); -my @days = ( - 'Sunday', 'Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday' -); +my @months = ( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ); +my @days = ( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ); my ( $sec, $min, $hour, $day, $month, $year, $day2 ) = ( localtime(time) )[ 0, 1, 2, 3, 4, 5, 6 ]; diff --git a/web/ia6/weatherpoint.pl b/web/ia6/weatherpoint.pl index ec4b2fff8..c22d138b1 100644 --- a/web/ia6/weatherpoint.pl +++ b/web/ia6/weatherpoint.pl @@ -9,9 +9,8 @@ my $f_weatherpoint_page = "$config_parms{data_dir}/web/weatherpoint.txt"; my $f_weatherpoint_html = "$config_parms{data_dir}/web/weatherpoint.html"; -$p_weatherpoint_page = new Process_Item( - "get_url \"$config_parms{weatherpointURL}\" \"$f_weatherpoint_html\""); -$v_get_weatherpoint = new Voice_Cmd('Get weather point'); +$p_weatherpoint_page = new Process_Item("get_url \"$config_parms{weatherpointURL}\" \"$f_weatherpoint_html\""); +$v_get_weatherpoint = new Voice_Cmd('Get weather point'); if ( $Startup or $Reload ) { # Used for testing @@ -28,8 +27,7 @@ $text = file_read $f_weatherpoint_html; # Find beginning of table and replace table tag - $text =~ - s/.+5-Day Forecast.+?width=468>(.+)/
    $PhoneDateLog $PhoneTimeLog $PhoneNameLog $PhoneNumberLog$PhoneDateLog $PhoneTimeLog $PhoneNameLog $PhoneNumberLog
    "; $log_out .= ""; - $log_out .= - ""; - $log_out .= - ""; + $log_out .= ""; + $log_out .= ""; $log_out .= "
    NUMBER OF CALLS: $NumofCallsNUMBER OF CALLS: $NumofCalls
    "; $log_out .= "
    $1/s; + $text =~ s/.+5-Day Forecast.+?width=468>(.+)/
    $1/s; # Find last TH tag and add all needed closing tags $text =~ s/(.+)<\/th>.+/$1<\/th><\/tr><\/table>/s; diff --git a/web/misc/mp3Rip.pl b/web/misc/mp3Rip.pl index 5480ea0be..ad8235e85 100644 --- a/web/misc/mp3Rip.pl +++ b/web/misc/mp3Rip.pl @@ -42,8 +42,7 @@ sub abort { my $dir = &mp3Rip_abort($cddbid); my $html = &html_header("Misterhouse mp3Rip: Aborted Rip $cddbid"); $html .= "

    The MP3 Rip process has been aborted.\n"; - $html .= - "

    The data will be stored as an incomplete process and you can delete it or try to resume.\n"; + $html .= "

    The data will be stored as an incomplete process and you can delete it or try to resume.\n"; if ( -d $dir ) { $html .= "

    The directory '$dir' exists and may contain a partially ripped CD. You may want to examine this directory manually to see if any manual actions are necessary.\n"; @@ -102,8 +101,7 @@ sub resume_rip { } $html .= "

    The MP3 Rip is in progress.

    Since this disc was somehow lost by the system before, you should monitor the progress closely to verify it is completing sucessfully. \n"; - $html .= - "

    Return to the mp3Rip Homepage to check the status.\n"; + $html .= "

    Return to the mp3Rip Homepage to check the status.\n"; $html .= "


    Questions, bugs, comments, suggestions related to the Misterhouse mp3Rip system? Contact Kirk Bauer\n"; return &html_page( "Misterhouse mp3Rip: Rip Resumed", $html ); @@ -112,8 +110,7 @@ sub resume_rip { sub view_log { my $cddbid = $_[0]; my $html = &html_header("Misterhouse mp3Rip Log Viewer: CDDBID $cddbid"); - $html .= - "

    NOTE: Most recent log entry is at the top of the page (i.e. the log entry is displayed in reversed order)\n"; + $html .= "

    NOTE: Most recent log entry is at the top of the page (i.e. the log entry is displayed in reversed order)\n"; $html .= "

    Return to mp3Rip Homepage\n"; if ( -f "$config_parms{mp3Rip_work_dir}/$cddbid.log" ) { $html = qq| @@ -121,20 +118,13 @@ sub view_log { $html

     |;
    -        foreach (
    -            reverse &main::file_read(
    -                "$config_parms{mp3Rip_work_dir}/$cddbid.log") )
    -        {
    +        foreach ( reverse &main::file_read("$config_parms{mp3Rip_work_dir}/$cddbid.log") ) {
                 $html .= "$_\n";
             }
         }
         elsif ( -f "$config_parms{mp3Rip_archive_dir}/$cddbid.log" ) {
    -        $html .=
    -          "

    NOTE: This is an archived log file as the ripping process is complete.

    \n";
    -        foreach (
    -            reverse &main::file_read(
    -                "$config_parms{mp3Rip_archive_dir}/$cddbid.log") )
    -        {
    +        $html .= "

    NOTE: This is an archived log file as the ripping process is complete.

    \n";
    +        foreach ( reverse &main::file_read("$config_parms{mp3Rip_archive_dir}/$cddbid.log") ) {
                 $html .= "$_\n";
             }
         }
    @@ -146,8 +136,7 @@ sub view_log {
     
     sub get_cdinfo {
         &mp3Rip_get_cdinfo();
    -    my $html =
    -      &html_header("Misterhouse mp3Rip Step 1/$numsteps: Retrieving CD Info");
    +    my $html = &html_header("Misterhouse mp3Rip Step 1/$numsteps: Retrieving CD Info");
         $html = qq|
     
     $html
    @@ -156,8 +145,7 @@ sub get_cdinfo {
     |;
         $html .=
           "

    Questions, bugs, comments, suggestions related to the Misterhouse mp3Rip system? Contact Kirk Bauer\n"; - return &html_page( - "Misterhouse mp3Rip Step 1/$numsteps: Retrieving CD Info", $html ); + return &html_page( "Misterhouse mp3Rip Step 1/$numsteps: Retrieving CD Info", $html ); } sub verify_cdinfo { @@ -166,18 +154,14 @@ sub verify_cdinfo { $html .= "

    Still processing... please wait.\n"; return $html; } - my ( $cddbid, $track_numbers, $track_lengths, $total_seconds ) = - &mp3Rip_parse_cdinfo(); + my ( $cddbid, $track_numbers, $track_lengths, $total_seconds ) = &mp3Rip_parse_cdinfo(); unless ($track_numbers) { - my $html .= - "

    ERROR READING CD INFORMATION!\n"; - $html .= "

    Things to check:

    \n
      \n"; - $html .= "
    • Is there a CD in the drive?\n"; - $html .= - "
    • Try running '$config_parms{mp3Rip_cdinfo}' as the Misterhouse user ('$ENV{USER}') to see if it is working okay. \n"; - $html .= "
    \n"; - $html .= - "

    Click here when you are ready to try again\n"; + my $html .= "

    ERROR READING CD INFORMATION!\n"; + $html .= "

    Things to check:

    \n
      \n"; + $html .= "
    • Is there a CD in the drive?\n"; + $html .= "
    • Try running '$config_parms{mp3Rip_cdinfo}' as the Misterhouse user ('$ENV{USER}') to see if it is working okay. \n"; + $html .= "
    \n"; + $html .= "

    Click here when you are ready to try again\n"; return $html; } return ( undef, $cddbid, $track_numbers, $track_lengths, $total_seconds ); @@ -198,36 +182,22 @@ sub confirm_files { } my $html = &html_header("Misterhouse mp3Rip Step 4/$numsteps: Filenames"); unless (@tracks) { - $html .= - "

    NO TRACKS SELECTED!\n"; - $html .= - "

    Hit the Back button on your browser and select one or more tracks to rip.\n"; - return &html_page( "Misterhouse mp3Rip Step 4/$numsteps: Filenames", - $html ); + $html .= "

    NO TRACKS SELECTED!\n"; + $html .= "

    Hit the Back button on your browser and select one or more tracks to rip.\n"; + return &html_page( "Misterhouse mp3Rip Step 4/$numsteps: Filenames", $html ); } $html .= "

    IMPORTANT: The following directory should not exist or at least be empty since existing files might be deleted or overwritten!

    \n"; - my $dir = - &mp3Rip_get_dir_name( $names{'genre'}, $names{'artist'}, - $names{'album'} ); - $html .= - "\n"; + my $dir = &mp3Rip_get_dir_name( $names{'genre'}, $names{'artist'}, $names{'album'} ); + $html .= "\n"; foreach my $track ( sort @tracks ) { - my $trackname = &mp3Rip_get_filename( - $track, - $names{"track${track}artist"}, - $names{"track${track}title"}, - $names{'album'}, $names{'genre'} - ); + my $trackname = &mp3Rip_get_filename( $track, $names{"track${track}artist"}, $names{"track${track}title"}, $names{'album'}, $names{'genre'} ); $html .= - "\n"; } - $html .= - "\n"; + $html .= "\n"; $html .= "
    Directory
    Directory
    Track $trackTrack $track
    \n"; $html .= "\n"; $html .= "\n"; @@ -237,20 +207,15 @@ sub confirm_files { $html .= "\n"; foreach my $track (@tracks) { - $html .= "\n"; - $html .= "\n"; - $html .= "\n"; - $html .= "\n"; + $html .= "\n"; + $html .= "\n"; + $html .= "\n"; + $html .= "\n"; } $html .= "\n"; $html .= "

    Questions, bugs, comments, suggestions related to the Misterhouse mp3Rip system? Contact Kirk Bauer\n"; - return &html_page( "Misterhouse mp3Rip Step 4/$numsteps: Filenames", - $html ); + return &html_page( "Misterhouse mp3Rip Step 4/$numsteps: Filenames", $html ); } sub start_rip { @@ -258,15 +223,12 @@ sub start_rip { my $html = &html_header("Misterhouse mp3Rip Step 5/$numsteps: Ripping CD"); if ($error) { $html .= "

    ERROR: $error!\n"; - return &html_page( "Misterhouse mp3Rip Step 5/$numsteps: Ripping CD", - $html ); + return &html_page( "Misterhouse mp3Rip Step 5/$numsteps: Ripping CD", $html ); } - $html .= - "

    The MP3 Rip is in progress.

    Return to the mp3Rip Homepage to check the status.\n"; + $html .= "

    The MP3 Rip is in progress.

    Return to the mp3Rip Homepage to check the status.\n"; $html .= "


    Questions, bugs, comments, suggestions related to the Misterhouse mp3Rip system? Contact Kirk Bauer\n"; - return &html_page( "Misterhouse mp3Rip Step 5/$numsteps: Ripping CD", - $html ); + return &html_page( "Misterhouse mp3Rip Step 5/$numsteps: Ripping CD", $html ); } my $default_artist = ''; @@ -290,8 +252,7 @@ sub do_select_row { if ( $special eq 'track' ) { my $shortname = $name; $shortname =~ s/title$//; - $html .= - "

    $label"; } else { @@ -324,14 +285,11 @@ sub do_select_row { $shortname =~ s/title$//; my $id = $label; $id =~ s/^\s*(\d+)\s+.*$/$1/; - $html .= - "\n"; - $html .= - "\n"; + $html .= "\n"; + $html .= "\n"; } if ( $special eq 'artist' ) { - $html .= - "(Changes are also applied to tracks below)\n"; + $html .= "(Changes are also applied to tracks below)\n"; } $html .= "
    \n"; - $html .= - &do_select_row( '', 'Genre', 'genre', - &remove_dups( @{ $combined{'genre'} } ), - '------------', sort( &mp3Rip_get_id3_genres() ) ); - $html .= - &do_select_row( 'artist', 'Artist', 'artist', - &remove_dups( &fix_caps( @{ $combined{'artist'} } ) ) ); - $html .= - &do_select_row( '', 'Album', 'album', - &remove_dups( &fix_caps( @{ $combined{'album'} } ) ) ); - $html .= &do_select_row( '', 'Year', 'year' ); + $html .= "
    \n"; + $html .= &do_select_row( '', 'Genre', 'genre', &remove_dups( @{ $combined{'genre'} } ), '------------', sort( &mp3Rip_get_id3_genres() ) ); + $html .= &do_select_row( 'artist', 'Artist', 'artist', &remove_dups( &fix_caps( @{ $combined{'artist'} } ) ) ); + $html .= &do_select_row( '', 'Album', 'album', &remove_dups( &fix_caps( @{ $combined{'album'} } ) ) ); + $html .= &do_select_row( '', 'Year', 'year' ); $html .= "
    \n"; $html .= "

    Track Info\n"; $html .= "\n"; - $html .= - "\n"; + $html .= "\n"; foreach ( my $i = 1; $i <= ( $#{@$track_numbers} + 1 ); $i++ ) { - $html .= &do_select_row( 'track', "$i ($track_lengths->[$i-1])", - "track${i}title", - &remove_dups( &fix_caps( @{ $combined{'tracks'}->[$i] } ) ) ); + $html .= &do_select_row( 'track', "$i ($track_lengths->[$i-1])", "track${i}title", &remove_dups( &fix_caps( @{ $combined{'tracks'}->[$i] } ) ) ); } - $html .= - "
    Rip?TrackTrack TitleTrack ArtistTrack Comment
    Rip?TrackTrack TitleTrack ArtistTrack Comment

    \n"; + $html .= "

    \n"; foreach ( my $i = 1; $i <= ( $#{@$track_numbers} + 1 ); $i++ ) { - $html .= "[ $i - 1 ] . "\">\n"; + $html .= "[ $i - 1 ] . "\">\n"; } $html .= "\n"; $html .= "

    Go back to Step 2\n"; $html .= "


    Questions, bugs, comments, suggestions related to the Misterhouse mp3Rip system? Contact Kirk Bauer\n"; - return &html_page( "Misterhouse mp3Rip Step 3/$numsteps: Finalize Naming", - $html ); + return &html_page( "Misterhouse mp3Rip Step 3/$numsteps: Finalize Naming", $html ); } sub do_cddb_list { my $html = &html_header("Misterhouse mp3Rip Step 2/$numsteps: CDDB List"); - my ( $error, $cddbid, $track_numbers, $track_lengths, $total_seconds ) = - &verify_cdinfo( 'cddb_list', $html ); + my ( $error, $cddbid, $track_numbers, $track_lengths, $total_seconds ) = &verify_cdinfo( 'cddb_list', $html ); if ($error) { - return &html_page( "Misterhouse mp3Rip Step 2/$numsteps: CDDB List", - $error ); + return &html_page( "Misterhouse mp3Rip Step 2/$numsteps: CDDB List", $error ); } $html .= "Audio CD Found

      \n"; - $html .= - "
    • Total Length: " . &mp3Rip_format_time($total_seconds) . "\n"; - $html .= - "
    • Number of Tracks: " . ( $#{@$track_numbers} + 1 ) . "\n"; + $html .= "
    • Total Length: " . &mp3Rip_format_time($total_seconds) . "\n"; + $html .= "
    • Number of Tracks: " . ( $#{@$track_numbers} + 1 ) . "\n"; $html .= "

    Select at least one CDDB entry to pre-populate your CD info. If you select more than one you will be presented with a list box for each item on the next page. You will still be able to make manual changes as well.\n"; - $html .= - "

    If you don't select any discs below, you will have to manually enter information about this CD.\n"; + $html .= "

    If you don't select any discs below, you will have to manually enter information about this CD.\n"; $html .= "

    \n"; - $html .= - "\n"; + $html .= "\n"; foreach my $disc ( &mp3Rip_get_cddb_discs() ) { my ( $genre, $cddbid, $album ) = @$disc; @@ -473,15 +406,12 @@ sub do_cddb_list { $html .= "\n"; } - $html .= - "\n"; + $html .= "\n"; $html .= "
    Select?GenreArtist / AlbumCDDB DiscID
    Select?GenreArtist / AlbumCDDB DiscID
    $genre$album$cddbid
    \n"; - $html .= - "

    TIP: Select all discs that appear to match your CD so you will have a wider variety of choices on the next page.\n"; + $html .= "

    TIP: Select all discs that appear to match your CD so you will have a wider variety of choices on the next page.\n"; $html .= "


    Questions, bugs, comments, suggestions related to the Misterhouse mp3Rip system? Contact Kirk Bauer\n"; - return &html_page( "Misterhouse mp3Rip Step 2/$numsteps: CDDB List", - $html ); + return &html_page( "Misterhouse mp3Rip Step 2/$numsteps: CDDB List", $html ); } sub main_page { @@ -508,35 +438,26 @@ sub main_page { my @pending = &mp3Rip_get_pending(); if (@pending) { $html .= "\n"; - $html .= - "\n"; + $html .= "\n"; $html .= "\n"; foreach (@pending) { my ( - $cddbid, $artist, - $album, $current, - $rip_status, $rip_percent, - $compress_status, $compress_percent, - $rip_time, $rip_predicted_remaining, - $compress_time, $compress_predicted_remaining + $cddbid, $artist, $album, $current, + $rip_status, $rip_percent, $compress_status, $compress_percent, + $rip_time, $rip_predicted_remaining, $compress_time, $compress_predicted_remaining ) = @$_; - $html .= - "\n"; - $html .= - "\n"; + $html .= "\n"; - $html .= - "\n"; + $html .= "\n"; } $html .= "
    Ripping in Progress
    Ripping in Progress
    CDDBIDArtistAlbum TitleCurrent ActivityRip StatusCompress StatusActions
    $cddbid$artist$album$current$rip_status\n"; + $html .= "
    $cddbid$artist$album$current$rip_status\n"; $html .= "
    Elapsed: " . &mp3Rip_format_time($rip_time); - $html .= "
    Remaining: " - . &mp3Rip_format_time($rip_predicted_remaining); + $html .= "
    Remaining: " . &mp3Rip_format_time($rip_predicted_remaining); $html .= &show_percent_bar($rip_percent); $html .= "
    $compress_status\n"; $html .= "
    Elapsed: " . &mp3Rip_format_time($compress_time); - $html .= "
    Remaining: " - . &mp3Rip_format_time($compress_predicted_remaining); + $html .= "
    Remaining: " . &mp3Rip_format_time($compress_predicted_remaining); $html .= &show_percent_bar($compress_percent); $html .= "
    View Log
    Abort
    View Log
    Abort
    \n"; } @@ -544,14 +465,11 @@ sub main_page { my @incomplete = &mp3Rip_get_incomplete(); if (@incomplete) { $html .= "

    \n"; - $html .= - "\n"; - $html .= - "\n"; + $html .= "\n"; + $html .= "\n"; foreach (@incomplete) { my ( $cddbid, $artist, $album, $status ) = @$_; - $html .= - "\n"; + $HTTP = $HTTP . "\n"; } $HTTP = $HTTP . "
    Incomplete CDs
    CDDBIDArtistAlbum TitleStatusActions
    Incomplete CDs
    CDDBIDArtistAlbum TitleStatusActions
    $cddbid$artist$album$status\n"; + $html .= "
    $cddbid$artist$album$status\n"; $html .= "View Log \n"; $html .= "Resume \n"; $html .= "Delete\n"; @@ -563,8 +481,7 @@ sub main_page { my @completed = &mp3Rip_get_recently_completed(); if (@completed) { $html .= "

    \n"; - $html .= - "\n"; + $html .= "\n"; foreach (@completed) { $html .= "\n"; } diff --git a/web/music/MP3_WebCtrl.pl b/web/music/MP3_WebCtrl.pl index 16730cae7..47aaf9523 100644 --- a/web/music/MP3_WebCtrl.pl +++ b/web/music/MP3_WebCtrl.pl @@ -32,8 +32,7 @@ my $Song = &mp3_get_playlist_title(); $Song =~ tr/_/ /; # replace _ by " " to make it clear my $Volume = &mp3_get_volume(); -$Volume = ( int( ( $Volume + 2 ) / 5 ) * 5 ) - ; # volume by slice of 5, xmms doesn't change exactly +$Volume = ( int( ( $Volume + 2 ) / 5 ) * 5 ); # volume by slice of 5, xmms doesn't change exactly my $Pos = &mp3_get_playlist_pos(); my $SongTime = &mp3_get_output_timestr(); @@ -63,11 +62,7 @@ sub mp3_top { "; my $Value; - for $Value ( - 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, - 60, 65, 70, 75, 80, 85, 90, 95, 100 - ) - { + for $Value ( 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100 ) { if ( $Volume == $Value ) { $HTTP = $HTTP . "
    Recently Completed CDs
    Recently Completed CDs
    $_
    \n"; - $HTTP = $HTTP - . "\n"; - $HTTP = $HTTP - . "\n"; - $HTTP = $HTTP - . "\n"; - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; + $HTTP = $HTTP . "\n"; + $HTTP = $HTTP . "\n"; + $HTTP = $HTTP . "\n"; my ( $playlists, %playfiles ) = &mp3_playlists; for my $playlist ( sort keys %playfiles ) { - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; } my $PlaylistLength = &mp3_get_playlist_length(); - $HTTP = $HTTP - . ""; + $HTTP = $HTTP . ""; $HTTP = $HTTP . "
    Refresh
    Clear
    Shuffle
    Sort
    Refresh
    Clear
    Shuffle
    Sort
    $playlist
    $playlist
    ($PlaylistLength)
    ($PlaylistLength)
    \n"; $HTTP = $HTTP . Footer(); @@ -145,8 +134,7 @@ sub PlaylistCtrl { my $HTTP = "\n"; $HTTP = $HTTP . "\n"; $HTTP = $HTTP . "\n"; - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; $HTTP = $HTTP . "\n"; return $HTTP; } diff --git a/web/music/MP3_WebPlaylist.pl b/web/music/MP3_WebPlaylist.pl index dbb3bddcf..4587995eb 100644 --- a/web/music/MP3_WebPlaylist.pl +++ b/web/music/MP3_WebPlaylist.pl @@ -44,8 +44,7 @@ $DisplayName =~ tr/_/ /; $DisplayName =~ s/-/ - /g; $DisplayName =~ s/.m3u$//; - $HTTP = $HTTP - . "

    $DisplayName
    $DisplayName
    \n"; } @@ -63,8 +62,7 @@ sub DisplayPlaylist { my $titles = &mp3_get_playlist(); if ( @$titles == 0 ) { - $HTTP = $HTTP - . "

    There is no track in the playlist

    \n"; + $HTTP = $HTTP . "

    There is no track in the playlist

    \n"; } else { $HTTP = $HTTP . "\n"; @@ -72,11 +70,9 @@ sub DisplayPlaylist { foreach my $item (@$titles) { my $Time = &mp3_get_playlist_timestr( $pos - 1 ); - my $Str = - " "; + my $Str = " "; $Str = substr( "$pos. $item", 1 ); - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; $pos++; } $HTTP = $HTTP . "
    $pos. $item .... $Time
    $pos. $item .... $Time
    \n"; @@ -88,8 +84,7 @@ sub Header { my $HTTP = "\n"; $HTTP = $HTTP . "\n"; $HTTP = $HTTP . "\n"; - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; $HTTP = $HTTP . "\n"; return $HTTP; } diff --git a/web/music/xmms/MP3_WebXmmsCtrl.pl b/web/music/xmms/MP3_WebXmmsCtrl.pl index beea793a0..12e06c6ff 100644 --- a/web/music/xmms/MP3_WebXmmsCtrl.pl +++ b/web/music/xmms/MP3_WebXmmsCtrl.pl @@ -36,8 +36,7 @@ my $Song = Xmms_Control("get_playlist_title"); $Song =~ tr/_/ /; # replace _ by " " to make it clear my $Volume = Xmms_Control("get_volume"); -$Volume = ( int( ( $Volume + 2 ) / 5 ) * 5 ) - ; # volume by slice of 5, xmms doesn't change exactly +$Volume = ( int( ( $Volume + 2 ) / 5 ) * 5 ); # volume by slice of 5, xmms doesn't change exactly my $Pos = Xmms_Control("get_playlist_pos"); my $SongTime = Xmms_Control("get_output_timestr"); @@ -67,11 +66,7 @@ sub mp3_top { "; my $Value; - for $Value ( - 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, - 60, 65, 70, 75, 80, 85, 90, 95, 100 - ) - { + for $Value ( 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100 ) { if ( $Volume == $Value ) { $HTTP = $HTTP . "
    $DisplayName
    $DisplayName
    \n"; @@ -100,8 +99,7 @@ sub DisplayPlaylist { my $LIST = Xmms_Control("get_playlist_titles"); if ( @$LIST == 0 ) { - $HTTP = $HTTP - . "

    There is no track in the playlist

    \n"; + $HTTP = $HTTP . "

    There is no track in the playlist

    \n"; } else { $HTTP = $HTTP . "\n"; @@ -109,11 +107,9 @@ sub DisplayPlaylist { foreach $item (@$LIST) { my $Time = Xmms_Control( "get_playlist_timestr", $pos - 1 ); - my $Str = - " "; + my $Str = " "; $Str = substr( "$pos. $item", 1 ); - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; $pos++; } $HTTP = $HTTP . "
    $pos. $item .... $Time
    $pos. $item .... $Time
    \n"; @@ -125,8 +121,7 @@ sub Header { my $HTTP = "\n"; $HTTP = $HTTP . "\n"; $HTTP = $HTTP . "\n"; - $HTTP = $HTTP - . "\n"; + $HTTP = $HTTP . "\n"; $HTTP = $HTTP . "\n"; return $HTTP; } diff --git a/web/organizer/calendar.pl b/web/organizer/calendar.pl index aed23a37d..4802228a0 100644 --- a/web/organizer/calendar.pl +++ b/web/organizer/calendar.pl @@ -98,8 +98,7 @@ BEGIN # i give up! user is going to have to set it manually print "Content-type: text/html\n\n"; print "Installation path could not be determined.\n"; - print - "

    Please edit the script and set \$ENV{\"CWD\"} to the full path in which the script is installed."; + print "

    Please edit the script and set \$ENV{\"CWD\"} to the full path in which the script is installed."; exit 1; } } # / BEGIN @@ -279,8 +278,7 @@ BEGIN # _____________________________________________________________________________ sub PrintDefault { - print - "\n"; + print "
    \n"; print "
    \n"; print "\n"; @@ -307,44 +305,23 @@ sub PrintDefault { # # display the navigation - print - "
    \n"; + print "
    \n"; print "\n"; - print "<<\n"; - print - " <\n"; - print - "\n"; + print "\n"; + print "\n"; my ($nM) = $month; my ($nY) = $year; for ( my $count = 1; $count < 12; $count++ ) { NextMonth( \$nM, \$nY ); - print "\n"; + print "\n"; } print "\n"; - print - " >\n"; - print - " >>\n"; + print " >\n"; + print " >>\n"; print "\n"; print "
    "; @@ -392,32 +369,23 @@ sub PrintDay { my $thisDate = "$year.$month.$day"; # my @days = ('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'); - my @months = ( - 'January', 'February', 'March', 'April', - 'May', 'June', 'July', 'August', - 'September', 'October', 'November', 'December' - ); + my @months = ( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ); $objMyDb->RemoveFilter; $objMyDb->Filter( "DATE", "eq", $thisDate ); print "\n"; - print - "Details For $months[$month-1] $day, $year
    \n"; + print "Details For $months[$month-1] $day, $year
    \n"; print "\n"; - print - "\n"; - print - "\n"; + print "\n"; + print "\n"; if ( $objMyDb->EOF ) { - print - "\n"; + print "\n"; } while ( !$objMyDb->EOF ) { - unless ( $objMyDb->FieldValue("CONTROL") eq "on" ) - { #Don't display CONTROL calendars + unless ( $objMyDb->FieldValue("CONTROL") eq "on" ) { #Don't display CONTROL calendars my $custcolor = ""; $custcolor = " bgcolor='$dataHolidayColor' " if ( $objMyDb->FieldValue("HOLIDAY") eq "on" ); @@ -425,26 +393,18 @@ sub PrintDay { if ( $objMyDb->FieldValue("VACATION") eq "on" ); $custcolor = " bgcolor='$dataMultipleColor' " if ( ( $objMyDb->FieldValue("VACATION") eq "on" ) - and ( $objMyDb->FieldValue("HOLIDAY") eq "on" ) ) - ; # if a day is both vacation and holiday + and ( $objMyDb->FieldValue("HOLIDAY") eq "on" ) ); # if a day is both vacation and holiday my $icon = $detailIcon; my $source = $objMyDb->FieldValue("SOURCE"); $icon = "images/ical_1.jpg" if ( $source =~ /^ical=/ ); $icon = $img_prefix . $icon; my $link = - "FieldValue("ID") . "&" . $ia7_suffix . "'>"; - print ""; - print ""; - print "\n"; + print ""; + print ""; + print "\n"; } $objMyDb->MoveNext; } @@ -464,11 +424,7 @@ sub PrintMonth { my ( $firstDay, $numDays, $numWeeks ) = &GetMonthInfo( $month, $year ); my @days = ( 'Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa' ); - my @months = ( - 'January', 'February', 'March', 'April', - 'May', 'June', 'July', 'August', - 'September', 'October', 'November', 'December' - ); + my @months = ( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ); my $temp; my $dayCount = 0; my $weekDayCount = 0; @@ -483,35 +439,26 @@ sub PrintMonth { my $highlightDate = shift || $today; print "

    \n"; - print - "$months[$month-1] $year\n"; + print "$months[$month-1] $year\n"; if ($showDayDetails) { - print - " [Hide Details]\n"; + print " [Hide Details]\n"; } else { - print - " [Show Details]\n"; + print " [Show Details]\n"; } print "

     TimeEvent
    TimeEvent
    No Events
    No Events
    " . $link - . "" - . $objMyDb->FieldValue("TIME") - . " " - . $link - . "" - . $objMyDb->FieldValue("EVENT") - . " 
    " . $link . "" . $objMyDb->FieldValue("TIME") . " " . $link . "" . $objMyDb->FieldValue("EVENT") . " 
    \n"; # print the days of the week print "\n"; foreach $temp (@days) { - print - ""; + print ""; } print "\n"; for ( my $cellCount = 1; $cellCount <= $numWeeks; $cellCount++ ) { print "\n"; foreach $temp (@days) { - if ( ( $dayCount > $firstDay - 1 ) && ( $weekDayCount < $numDays ) ) - { + if ( ( $dayCount > $firstDay - 1 ) && ( $weekDayCount < $numDays ) ) { $weekDayCount++; $thisDate = $year . "." . $month . "." . $weekDayCount; @@ -546,12 +493,10 @@ sub PrintMonth { } if ( defined $bgcolor ) { - print - "\n"; } else { - print - "\n"; + print "\n"; } $dayCount++; @@ -634,12 +574,7 @@ sub GetMonthInfo { ( localtime( Time::Local::timelocal( 0, 0, 0, 1, $month, $year ) ) )[6]; # numDays is one day prior to 1st of month after - $numDays = ( - localtime( - Time::Local::timelocal( 0, 0, 0, 1, $nmonth, $nyear ) - - 60 * 60 * 24 - ) - )[3]; + $numDays = ( localtime( Time::Local::timelocal( 0, 0, 0, 1, $nmonth, $nyear ) - 60 * 60 * 24 ) )[3]; # figure out the number of weeks the month spans across my $numWeeks = ( $numDays + $firstDow ) / 7; @@ -663,21 +598,17 @@ sub PrintCurrentRecord { foreach $fieldName ( $objMyDB->FieldNames ) { if ( $fieldName eq "ID" ) { - print "\n"; + print "\n"; } elsif ( $fieldName eq "DATE" ) { if ( $objMyDB->FieldValue("ID") ) { - print "\n"; + print "\n"; $date_entry = $objMyDB->FieldValue($fieldName); } } elsif ( $fieldName eq "DETAILS" ) { print "\n"; - print "\n"; + print "\n"; print "\n"; + print "\n"; print "\n"; } else { print "\n"; print "\n"; - print - "\n"; + print "\n"; print "\n"; } } @@ -427,8 +405,7 @@ sub PrintCurrentRecord { print "\n"; } print "\n"; - print - "\n"; + print "\n"; print "

    \n"; } @@ -459,19 +436,15 @@ sub FatalError { my ($strMessage) = shift || "Unknown Error"; print "Content-type: text/html\n\n" unless defined($HEADER_PRINTED); print "

    \n"; - print - "A fatal error occured. The script cannot continue. Details are below:"; + print "A fatal error occured. The script cannot continue. Details are below:"; print "

    " . $strMessage . ""; print "

    The most common causes of fatal errors are:\n"; print "

      \n"; - print - "
    1. One of the script files was uploaded via FTP in Binary mode instead of ASCII\n"; - print - "
    2. The file permissions for the data directory and all .tab and .cfg files is not readable/writable\n"; + print "
    3. One of the script files was uploaded via FTP in Binary mode instead of ASCII\n"; + print "
    4. The file permissions for the data directory and all .tab and .cfg files is not readable/writable\n"; print "
    \n"; print "

    If you have already tried these, you may want to visit the "; - print - "VerySimple Support Forum \n"; + print "VerySimple Support Forum \n"; print "to see if there is a solution available.\n"; print "\n"; exit 1; diff --git a/web/organizer/tasks.pl b/web/organizer/tasks.pl index de8a8344d..4b10e0a7f 100644 --- a/web/organizer/tasks.pl +++ b/web/organizer/tasks.pl @@ -90,8 +90,7 @@ BEGIN # i give up! user is going to have to set it manually print "Content-type: text/html\n\n"; print "Installation path could not be determined.\n"; - print - "

    Please edit the script and set \$ENV{\"CWD\"} to the full path in which the script is installed."; + print "

    Please edit the script and set \$ENV{\"CWD\"} to the full path in which the script is installed."; exit 1; } } # / BEGIN @@ -260,14 +259,10 @@ sub PrintAllRecords { #$activePage = $objMyDB->ActivePage; # (in case we specified one out of range) my ($pageCount) = $objMyDB->PageCount; - print - "

    $temp$temp
    "; + print ""; } else { - print - ""; + print ""; } if ( $thisDate eq $today ) { @@ -562,15 +507,13 @@ sub PrintMonth { } if ( $objMyDb->EOF ) { - print - "$weekDayCount
    "; } else { #$style = "style=\"color:$dataHolidayColor\"" if ($objMyDb->FieldValue("HOLIDAY") eq "on"); - print - "$weekDayCount
    "; } @@ -578,14 +521,12 @@ sub PrintMonth { if ($showDayDetails) { print ""; while ( !$objMyDb->EOF ) { - print - "FieldValue("ID") . "&" . $ia7_suffix . "'>" . $objMyDb->FieldValue("EVENT") . "
    " - unless ( $objMyDb->FieldValue("CONTROL") eq "on" ) - ; #Don't display CONTROL calendars; + unless ( $objMyDb->FieldValue("CONTROL") eq "on" ); #Don't display CONTROL calendars; $objMyDb->MoveNext; } print "
    "; @@ -596,8 +537,7 @@ sub PrintMonth { print "
      
    " - . $fieldName - . "" . $fieldName . "
    $fieldName
    \n"; if ( $fieldName eq "Complete" ) { - print "\n"; + print "\n"; print ""; } elsif ( $fieldName eq "Notes" ) { - print "\n"; - print - "\n"; + print "\n"; + print "\n"; } elsif ( $fieldName eq "SPEAK" ) { - print "\n"; - print - "\n"; + print "\n"; } elsif ( $fieldName eq "SOURCE" ) { #do nothing } else { - print "\n"; - print "\n"; + print "\n"; + print "\n"; } print "\n"; } @@ -541,8 +487,7 @@ sub PrintBlankRecord { print "\n"; print "\n"; - print - "\n"; + print "\n"; print "

    \n"; } @@ -589,19 +534,15 @@ sub FatalError { my ($strMessage) = shift || "Unknown Error"; print "Content-type: text/html\n\n" unless defined($HEADER_PRINTED); print "

    \n"; - print - "A fatal error occured. The script cannot continue. Details are below:"; + print "A fatal error occured. The script cannot continue. Details are below:"; print "

    " . $strMessage . ""; print "

    The most common causes of fatal errors are:\n"; print "

      \n"; - print - "
    1. One of the script files was uploaded via FTP in Binary mode instead of ASCII\n"; - print - "
    2. The file permissions for the data directory and all .tab and .cfg files is not readable/writable\n"; + print "
    3. One of the script files was uploaded via FTP in Binary mode instead of ASCII\n"; + print "
    4. The file permissions for the data directory and all .tab and .cfg files is not readable/writable\n"; print "
    \n"; print "

    If you have already tried these, you may want to visit the "; - print - "VerySimple Support Forum \n"; + print "VerySimple Support Forum \n"; print "to see if there is a solution available.\n"; print "\n"; exit 1; diff --git a/web/test/test1.pl b/web/test/test1.pl index 0dc99dc4d..4a36e17b4 100644 --- a/web/test/test1.pl +++ b/web/test/test1.pl @@ -12,13 +12,9 @@ print "Returning Time: $Time_Now\n"; $data .= "Time: $Time_Now\n"; -$data .= - "Current Temperature: Indoor $weather{TempIndoor} degrees Outdoor$weather{TempOutdoor} degrees"; -$data .= - "

    "; -$data .= - "

    "; -$data .= - "

    "; +$data .= "Current Temperature: Indoor $weather{TempIndoor} degrees Outdoor$weather{TempOutdoor} degrees"; +$data .= "

    "; +$data .= "

    "; +$data .= "

    "; return $data; From 9dc53ddffc4d65416948ac8b3b34ea83b17a4c2e Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Tue, 7 Mar 2017 12:29:23 -0700 Subject: [PATCH 140/209] Upped version to 4.2 --- VERSION | 2 +- docs/download.pod | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 7d5c902e7..bf77d5496 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1 +4.2 diff --git a/docs/download.pod b/docs/download.pod index 72e330517..88bc0b5f5 100644 --- a/docs/download.pod +++ b/docs/download.pod @@ -10,7 +10,7 @@ modify the code/common/mh_release.pl file to search the new format!! =head1 Current Stable -B - released on 03/31/2014 +B - released on 03/08/2017 =over @@ -28,7 +28,7 @@ A large (30MB) zip of optional files (mainly sound files) is here: L - released on 06/20/2013 +B - released on 02/28/2016 =over From fb4d6ff1a3e0427279549b6af7362ef850d86a2a Mon Sep 17 00:00:00 2001 From: hplato Date: Tue, 7 Mar 2017 18:34:46 -0700 Subject: [PATCH 141/209] v4.2 - fix conflict files --- lib/http_server.pl | 58 +++-- lib/json_server.pl | 48 +--- web/bin/code_select.pl | 2 +- web/bin/code_unselect.pl | 2 +- web/bin/iniedit.pl | 4 +- web/bin/items.pl | 20 +- web/ia7/house/main.shtml | 12 +- web/ia7/include/javascript.js | 399 +++++++++++++++++++--------------- web/ia7/include/tables.css | 8 +- web/ia7/index.shtml | 36 ++- web/organizer/contacts.pl | 2 +- 11 files changed, 342 insertions(+), 249 deletions(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index a97f7cfed..6629eff3a 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -186,12 +186,19 @@ sub http_process_request { unless ($header) { # Ignore empty requests, like from 'check the http server' command - print "http: Error, not header request. header=$temp\n" + print "http: Error, not header request. header=[$temp]\n" if $main::Debug{http} and $temp; + my ( $type, $file ) = ( $temp =~ /^(\S*)\s(.*)\sHTTP/ ); + print "t=[$type] f=[$file]\n" if $main::Debug{http2}; + if ( $type eq "HEAD" ) { + print "yes\n"; + print $socket &mime_header( $file, 1, 4592100 ); + } return; } $Socket_Ports{http}{data_record} = $header; + print "http: Header = $header\n" if $main::Debug{http}; #print Dumper %Http if $main::Debug{http}; print "http: Range Header $Http{Range} encountered for $header" @@ -427,6 +434,7 @@ sub http_process_request { my ($mode) = ( $Http{Referer} =~ /https?:\/\/\S+:?\D*\/(\S+)\// ); if ( $config_parms{password_menu} eq 'html' ) { + my $html = &html_authorized; if ( $get_req =~ /^\/UNSET_PASSWORD$/ ) { $Authorized = 0; $Cookie .= "Set-Cookie: password=xyz ; ; path=/;\n"; @@ -471,6 +479,7 @@ sub http_process_request { 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'); @@ -498,7 +507,9 @@ sub http_process_request { # &speak("app=admin Password NOT set by $name_short"); } + print $socket &html_page( undef, $html ); + return; } @@ -804,12 +815,18 @@ sub html_password { sub html_authorized { my $html = "Status: "; if ($Authorized) { - return - "Status: Logged In as $Authorized
    "; + $html .= ""; + $html .= "Logged In as $Authorized"; + $html .= ""; + $html .= "

    "; } else { - return "Status: Not Logged In
    "; + $html .= ""; + $html .= "Not Logged In"; + $html .= ""; + $html .= "
    "; } + return $html; } sub html_unauthorized { @@ -1052,7 +1069,7 @@ sub html_sub { } # Allow for &sub1 and &sub1(args) - if ( ( ( $sub_name, $sub_arg ) = $data =~ /\&([^\&]+?)\((.*)\)$/ ) + if ( ( ( $sub_name, $sub_arg ) = $data =~ /^\&(\S+?)\((.*)\)$/ ) or ( ($sub_name) = $data =~ /^\&(\S+)$/ ) ) { $sub_arg = '' unless defined $sub_arg; # Avoid uninit warninng @@ -1114,8 +1131,8 @@ sub html_response { # $leave_socket_open_action = "&speak_log_last(1)"; # Only show the last spoken text # $leave_socket_open_action = "&Voice_Text::last_spoken(1)"; # Only show the last spoken text } - elsif ( $h_response =~ /^https?:\S+$/i or $h_response =~ /^reff?erer/i ) - { + elsif ( $h_response =~ /^http:\S+$/i or $h_response =~ /^reff?erer/i ) { + # Allow to use just the base part of the referer # - some browsers (audrey) do not return full referer url :( # so allow for referer(url)... @@ -1509,7 +1526,7 @@ sub html_form_input_set_func { $size = 10 if $size < 10; $size = 30 if $size > 30; $html .= qq|\n|; - $html .= qq|\n|; + $html .= qq|\n|; return $html; } @@ -1524,7 +1541,7 @@ sub html_form_input_set_var { $html .= qq|\n|; $html .= qq|\n|; $html .= qq|\n|; - $html .= qq|\n|; + $html .= qq|\n|; return $html; } @@ -1589,8 +1606,10 @@ sub html_file { # Do not cach shtml files my ($cache) = ( $file =~ /\.shtm?l?$/ or $file =~ /\.vxml?$/ ) ? 0 : 1; + ($cache) = ( defined $Http{Range} ) ? 0 : 1; - # Return right away if the file has not changed + # Return right away if the file has not changed, don't return a cache entry if there is + # a http Range header though... #http: header key=If-Modified-Since value=Sat, 27 Mar 2004 02:49:29 GMT; length=1685. if ( $cache and $Http{'If-Modified-Since'} @@ -1599,7 +1618,7 @@ sub html_file { my $time2 = &str2time($1); my $time3 = ( stat($file) )[9]; print "db web file cache check: f=$file t=$time2/$time3\n" - if $main::Debug{http3}; + if $main::Debug{http}; if ( $time3 <= $time2 ) { return "HTTP/1.0 304 Not Modified\nServer: MisterHouse\n\n"; } @@ -1614,6 +1633,8 @@ sub html_file { return; } + print "http: processing file\n" if $main::Debug{http}; + # Allow for 'server side include' directives # if ( $file =~ /\.shtm?l?$/ @@ -1633,6 +1654,7 @@ sub html_file { # Note: These differ from classic .cgi in that they return # the results, rather than print them to stdout. elsif ( $file =~ /\.(pl|cgi)$/ ) { + print "Processing perl or CGI file: $file\n" if $main::Debug{http}; my $code = join( '', ); # Check if authorized @@ -1675,6 +1697,7 @@ sub html_file { # print "Http_server .pl file results:$html.\n" if $main::Debug{http}; } else { + print "http: Reading file: $file\n" if $main::Debug{http}; binmode HTML; # my $data = join '', ; @@ -1849,7 +1872,7 @@ sub html_cgi { } sub mime_header { - my ( $file_or_type, $cache, $length ) = @_; + my ( $file_or_type, $cache, $length, $range, $full_length ) = @_; # Allow for passing filename or filetype my ( $mime, $date ); @@ -1866,8 +1889,9 @@ sub mime_header { } # print "dbx2 m=$mime f=$file_or_type\n"; - - my $header = "HTTP/1.0 200 OK\nServer: MisterHouse\nContent-type: $mime\n"; + my $code = "HTTP/1.0 200 OK"; + $code = "HTTP/1.1 206 Partial Content" if $range; + my $header = "$code\nServer: MisterHouse\nContent-Type: $mime\n"; # $header .= ($cache) ? "Cache-Control: max-age=1000000\n" : "Cache-Control: no-cache\n"; if ($cache) { @@ -2995,10 +3019,16 @@ sub print_socket_fork { } } else { + my $keep_alive = 0; + $keep_alive = 1 + if ( ( defined $Http{Connection} ) + and ( $Http{Connection} eq "keep-alive" ) ); &print_socket_fork_unix( $socket, $html ); } } else { + print "http: printing with regular socket l=$length s=$socket\n" + if $main::Debug{http}; print $socket $html; } } diff --git a/lib/json_server.pl b/lib/json_server.pl index 8a8c93e82..33efe66ee 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -47,6 +47,7 @@ =head2 METHODS use JSON qw(decode_json); use IO::Compress::Gzip qw(gzip); use vars qw(%json_table); +my @json_notifications = (); #noloop sub json { my ( $request_type, $path_str, $arguments, $body ) = @_; @@ -263,6 +264,7 @@ sub json_get { my $celsius = 0; my $kph = 0; my $arg_time = 0; + my $xml_info; $arg_time = int( $args{time}[0] ) if ( defined int( $args{time}[0] ) ); $celsius = 1 if ( $config_parms{weather_uom_temp} eq 'C' ); $celsius = 1 @@ -327,13 +329,10 @@ sub json_get { } push @defs, @xports; - - #print "defs=" . join (',',@defs) . "\n"; - #print "start=$start end=$end\n"; - my $rrd = RRDTool::Rawish->new( rrdfile => "$path/$rrd_file" ); my $xml = $rrd->xport( [@defs], { '--start' => $start, '--end' => $end, } ); + $xml_info = $rrd->info if ( $default_timestamp eq "true" ); my @lines = split /\n/, $xml; my ($step) = $xml =~ /\(\d+)\<\/step\>/; #this is the width for any bar charts @@ -380,8 +379,6 @@ sub json_get { $data{'last_update'} = $xml_info->{'last_update'} * 1000 if ( ref($xml_info) eq 'HASH' && defined $xml_info->{'last_update'} ); $json_data{'rrd'} = \%data; - - #print Dumper %data; } # List object history @@ -619,8 +616,6 @@ sub json_get { if ( $args{var} ) { my $length = $#{ $json_table{ $args{var}[0] }->{data} } + 1; - #print "json_db: length = $length start=" . $args{start}[0] . " records=" . $args{records}[0] . "\n"; - #need to check if vars and keys exist my $start = 0; @@ -631,30 +626,13 @@ sub json_get { if ( defined $json_table{ $args{var}[0] }{page} ); $records = $args{records}[0] if ( $args{records}[0] ); - # if ($length < ($start + $records)) { - # print "db: will have to request data, $length, $start, $records\n"; - # print "&" . $json_table{$args{var}[0]}{hook} . "($start,$records)\n"; - # my $hook = $json_table{$args{var}[0]}{hook} . "($start,$records)"; - # - # #eval (&get_inbound_data($start,$records)); - # eval ("&$hook"); - # if ($@) { - # print_log "Json_Server.pl: WARNING: fetch data failed for " . $args{var}[0] . " " . $json_table{$args{var}[0]}{hook} . "!"; - # } else { - # $page++ if (scalar @{$json_table{$args{var}[0]}->{data}} > $json_table{$args{var}[0]}{page_size}); - # } - # #$json_table{$args{var}[0]}{page} = $page; - # } - #if requesting data beyond what's available, fetch it. - #test bad table - #remove data from hash - my $jt_time = int( $json_table{ $args{var}[0] }{time} ); + # TODO: At some point have a hook that pulls in more data into the table if it's missing + # ie read a file - #print "arg time = " . int($args{time}[0]) . " table time = " .$jt_time . "\n"; + my $jt_time = int( $json_table{ $args{var}[0] }{time} ); if ( ( $args{time} && int( $args{time}[0] ) < $jt_time ) or ( !$args{time} ) ) { - #$json_data->{'table_data'} = $json_table{$args{var}[0]}; #need to copy all the data since we can adjust starts and records $json_data{'table_data'}{exist} = @@ -675,8 +653,6 @@ sub json_get { if ( $args{records}[0] ); $json_data{'table_data'}{records} = scalar @{ $json_data{'table_data'}->{data} }; - - #print "db=$json_data{'table_data'}{records}\n"; } } } @@ -745,20 +721,16 @@ sub json_get { # Insert Data or Error Message if ($output_ref) { $json{data} = $output_ref; - - # foreach my $key (sort (keys(%{$output_ref}))) { - # print "db:key = $key\n"; - # $json{data}{$key} = $output_ref->{$key}; - # } } else { $json{error}{msg} = 'No data, or path does not exist.'; } #Insert Meta Data fields - $json{meta}{time} = $output_time; - $json{meta}{path} = \@path; - $json{meta}{args} = \%args; + $json{meta}{time} = $output_time; + $json{meta}{path} = \@path; + $json{meta}{args} = \%args; + $json{meta}{client_ip} = $Http{Client_address}; my $json_raw = JSON->new->allow_nonref; diff --git a/web/bin/code_select.pl b/web/bin/code_select.pl index e89dc717f..da582cf06 100644 --- a/web/bin/code_select.pl +++ b/web/bin/code_select.pl @@ -43,7 +43,7 @@ sub select_code_form { $html .= qq|
    Read-Only: Login as admin to edit| unless $Authorized eq 'admin'; $html .= qq| -

    +
    \n"; + print "\n"; print "\n"; foreach $fieldName (@showFields) { - print - "\n"; @@ -315,11 +307,7 @@ sub PrintAllRecords { . $objMyDB->FieldValue("ID") . "&" . $ia7_suffix . "'>"; $link_close = ""; - print "\n"; + print "\n"; foreach $fieldName (@showFields) { $fieldValue = $objMyDB->FieldValue($fieldName); @@ -327,22 +315,10 @@ sub PrintAllRecords { if ( $fieldName eq "SpecialField" ) { # not used at the moment, but maybe later... - print "\n"; + print "\n"; } elsif ( lc $fieldName eq "SPEAK" ) { - print "\n"; + print "\n"; } elsif ( $fieldName eq "SOURCE" ) { } @@ -353,13 +329,7 @@ sub PrintAllRecords { $link_open = ""; $link_close = ""; } - print "\n"; + print "\n"; } } print "\n"; @@ -397,14 +367,11 @@ sub PrintCurrentRecord { print "
    \n"; #if ($m_blnIsSysAdmin) { - print - "\n"; + print "\n"; # print "\n" if ($m_strSysAdminUserId || $m_strSysAdminPassword); #} else { @@ -276,14 +271,12 @@ sub PrintAllRecords { #} if ($showCompleted) { - print - " \n"; } else { - print - " \n"; } @@ -293,8 +286,7 @@ sub PrintAllRecords { print "
     " . $fieldName . "" - . $link_open - . "Details" - . $link_close - . "" . $link_open . "Details" . $link_close . "" - . $link_open - . "" - . $fieldValue - . "" - . $link_close - . "" . $link_open . "" . $fieldValue . "" . $link_close . "" - . $link_open - . "" - . $fieldValue - . "" - . $link_close - . "" . $link_open . "" . $fieldValue . "" . $link_close . "" - . $link_open - . "" - . $fieldValue - . "" - . $link_close - . "" . $link_open . "" . $fieldValue . "" . $link_close . "
    \n"; foreach $fieldName ( $objMyDB->FieldNames ) { if ( $fieldName eq "ID" ) { - print "\n"; + print "\n"; } else { print "\n"; - print "\n" + print "\n" if ( $fieldName ne "SOURCE" ); if ( $fieldName eq "Complete" ) { my ($yes) = ""; @@ -413,10 +380,8 @@ sub PrintCurrentRecord { if ( $objMyDB->FieldValue("Complete") eq "Yes" ); $no = "checked" if ( $objMyDB->FieldValue("Complete") eq "No" ); print ""; } elsif ( $fieldName eq "Notes" ) { @@ -427,8 +392,7 @@ sub PrintCurrentRecord { } elsif ( $fieldName eq "SPEAK" ) { $fieldValue = $objMyDB->FieldValue($fieldName); - print - "\n"; @@ -469,16 +431,14 @@ sub PrintCurrentRecord { "\n"; - print - "\n"; + print "\n"; print "

    \n"; } else { $source =~ /^ical=(\S*)\ssync=(.*)/; my $icalname = $1; my $icalsync = $2; - print - "

    " - . $fieldName - . "" . $fieldName . ""; - print - "Yes\n"; - print - "No\n"; + print "Yes\n"; + print "No\n"; print " "; } @@ -444,9 +408,7 @@ sub PrintCurrentRecord { } else { - print "FieldValue($fieldName); $fieldValue =~ s/\"/"/g; print $fieldValue . "\">
    iCal2vsdb (ical $icalname) $icalsync\n"; + print "
    iCal2vsdb (ical $icalname) $icalsync\n"; print "
    \n"; } } @@ -492,40 +452,26 @@ sub PrintBlankRecord { if ( $fieldName ne "ID" ) { print "
    " - . $fieldName - . "" . $fieldName . ""; - print - "Yes\n"; - print - "No\n"; + print "Yes\n"; + print "No\n"; print "" - . $fieldName - . "" . $fieldName . "" - . $fieldName - . ""; + print "" . $fieldName . ""; print "
    " - . $fieldName - . "" . $fieldName . "
    Search (file or description):   |; diff --git a/web/bin/code_unselect.pl b/web/bin/code_unselect.pl index 946910828..6fb06624d 100644 --- a/web/bin/code_unselect.pl +++ b/web/bin/code_unselect.pl @@ -41,7 +41,7 @@ sub select_code_form { $html .= qq|Simply uncheck files you want to disable or check to re-enable.| if $Authorized eq 'admin'; $html .= qq| -
    +
    r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?"
    Search (file or description):   |; diff --git a/web/bin/iniedit.pl b/web/bin/iniedit.pl index 89e1dfbde..7440532f3 100644 --- a/web/bin/iniedit.pl +++ b/web/bin/iniedit.pl @@ -169,7 +169,7 @@ sub edit { Note: Commit will resort and filter out comments.' if $Authorized eq 'admin'; $data .= ' - + '; $data .= ' @@ -249,7 +249,7 @@ sub edit { sub edit_list { my $data = ' - + '; $data .= ' diff --git a/web/bin/items.pl b/web/bin/items.pl index 873436b04..70da032a0 100644 --- a/web/bin/items.pl +++ b/web/bin/items.pl @@ -83,17 +83,17 @@ sub web_items_list { #form action='/bin/items.pl?add' method=post> $html .= qq| - + + + + + + | if $Authorized eq 'admin'; # Parse table data @@ -106,7 +106,7 @@ sub web_items_list { # Do not list comments unless ( $record =~ /^\s*\#/ or $record =~ /^\s*$/ - or $record =~ /^Format *=/ ) + or $record =~ /^Format *=/i ) { $record =~ s/#.*//; # Ignore comments $record =~ s/,? *$//; @@ -169,9 +169,9 @@ sub web_items_list { my @headers = ( $headers{$type} ) ? @{ $headers{$type} } : @{ $headers{default} }; - my $headers = 1 + @headers; + my $headers = 2 + @headers; - $html .= "
    + $form_type - - - - - -
    ",e.document[0]).appendTo(n)):"tr"===s?e._createTrPlaceholder(e.currentItem,n):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_createTrPlaceholder:function(e,i){var s=this;e.children().each(function(){t("",s.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(i)})},_contactContainers:function(e){var i,s,n,o,a,r,h,l,c,u,d=null,p=null;for(i=this.containers.length-1;i>=0;i--)if(!t.contains(this.currentItem[0],this.containers[i].element[0]))if(this._intersectsWith(this.containers[i].containerCache)){if(d&&t.contains(this.containers[i].element[0],d.element[0]))continue;d=this.containers[i],p=i}else this.containers[i].containerCache.over&&(this.containers[i]._trigger("out",e,this._uiHash(this)),this.containers[i].containerCache.over=0);if(d)if(1===this.containers.length)this.containers[p].containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1);else{for(n=1e4,o=null,c=d.floating||this._isFloating(this.currentItem),a=c?"left":"top",r=c?"width":"height",u=c?"pageX":"pageY",s=this.items.length-1;s>=0;s--)t.contains(this.containers[p].element[0],this.items[s].item[0])&&this.items[s].item[0]!==this.currentItem[0]&&(h=this.items[s].item.offset()[a],l=!1,e[u]-h>this.items[s][r]/2&&(l=!0),n>Math.abs(e[u]-h)&&(n=Math.abs(e[u]-h),o=this.items[s],this.direction=l?"up":"down"));if(!o&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[p])return this.currentContainer.containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash()),this.currentContainer.containerCache.over=1),void 0;o?this._rearrange(e,o,null,!0):this._rearrange(e,null,this.containers[p].element,!0),this._trigger("change",e,this._uiHash()),this.containers[p]._trigger("change",e,this._uiHash(this)),this.currentContainer=this.containers[p],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===this.document[0].body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,"document"===n.containment?this.document.width():this.window.width()-this.helperProportions.width-this.margins.left,("document"===n.containment?this.document.height()||document.body.parentNode.scrollHeight:this.window.height()||this.document[0].body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter; +this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!this.cancelHelperRemoval},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}}),t.widget("ui.spinner",{version:"1.12.1",defaultElement:"",widgetEventPrefix:"spin",options:{classes:{"ui-spinner":"ui-corner-all","ui-spinner-down":"ui-corner-br","ui-spinner-up":"ui-corner-tr"},culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var e=this._super(),i=this.element;return t.each(["min","max","step"],function(t,s){var n=i.attr(s);null!=n&&n.length&&(e[s]=n)}),e},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t),void 0)},mousewheel:function(t,e){if(e){if(!this.spinning&&!this._start(t))return!1;this._spin((e>0?1:-1)*this.options.step,t),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(t)},100),t.preventDefault()}},"mousedown .ui-spinner-button":function(e){function i(){var e=this.element[0]===t.ui.safeActiveElement(this.document[0]);e||(this.element.trigger("focus"),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===t.ui.safeActiveElement(this.document[0])?this.previous:this.element.val(),e.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(e)!==!1&&this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(e){return t(e.currentTarget).hasClass("ui-state-active")?this._start(e)===!1?!1:(this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_enhance:function(){this.uiSpinner=this.element.attr("autocomplete","off").wrap("").parent().append("")},_draw:function(){this._enhance(),this._addClass(this.uiSpinner,"ui-spinner","ui-widget ui-widget-content"),this._addClass("ui-spinner-input"),this.element.attr("role","spinbutton"),this.buttons=this.uiSpinner.children("a").attr("tabIndex",-1).attr("aria-hidden",!0).button({classes:{"ui-button":""}}),this._removeClass(this.buttons,"ui-corner-all"),this._addClass(this.buttons.first(),"ui-spinner-button ui-spinner-up"),this._addClass(this.buttons.last(),"ui-spinner-button ui-spinner-down"),this.buttons.first().button({icon:this.options.icons.up,showLabel:!1}),this.buttons.last().button({icon:this.options.icons.down,showLabel:!1}),this.buttons.height()>Math.ceil(.5*this.uiSpinner.height())&&this.uiSpinner.height()>0&&this.uiSpinner.height(this.uiSpinner.height())},_keydown:function(e){var i=this.options,s=t.ui.keyCode;switch(e.keyCode){case s.UP:return this._repeat(null,1,e),!0;case s.DOWN:return this._repeat(null,-1,e),!0;case s.PAGE_UP:return this._repeat(null,i.page,e),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,e),!0}return!1},_start:function(t){return this.spinning||this._trigger("start",t)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(t,e,i){t=t||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,e,i)},t),this._spin(e*this.options.step,i)},_spin:function(t,e){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+t*this._increment(this.counter)),this.spinning&&this._trigger("spin",e,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(e){var i=this.options.incremental;return i?t.isFunction(i)?i(e):Math.floor(e*e*e/5e4-e*e/500+17*e/200+1):1},_precision:function(){var t=this._precisionOf(this.options.step);return null!==this.options.min&&(t=Math.max(t,this._precisionOf(this.options.min))),t},_precisionOf:function(t){var e=""+t,i=e.indexOf(".");return-1===i?0:e.length-i-1},_adjustValue:function(t){var e,i,s=this.options;return e=null!==s.min?s.min:0,i=t-e,i=Math.round(i/s.step)*s.step,t=e+i,t=parseFloat(t.toFixed(this._precision())),null!==s.max&&t>s.max?s.max:null!==s.min&&s.min>t?s.min:t},_stop:function(t){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",t))},_setOption:function(t,e){var i,s,n;return"culture"===t||"numberFormat"===t?(i=this._parse(this.element.val()),this.options[t]=e,this.element.val(this._format(i)),void 0):(("max"===t||"min"===t||"step"===t)&&"string"==typeof e&&(e=this._parse(e)),"icons"===t&&(s=this.buttons.first().find(".ui-icon"),this._removeClass(s,null,this.options.icons.up),this._addClass(s,null,e.up),n=this.buttons.last().find(".ui-icon"),this._removeClass(n,null,this.options.icons.down),this._addClass(n,null,e.down)),this._super(t,e),void 0)},_setOptionDisabled:function(t){this._super(t),this._toggleClass(this.uiSpinner,null,"ui-state-disabled",!!t),this.element.prop("disabled",!!t),this.buttons.button(t?"disable":"enable")},_setOptions:r(function(t){this._super(t)}),_parse:function(t){return"string"==typeof t&&""!==t&&(t=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(t,10,this.options.culture):+t),""===t||isNaN(t)?null:t},_format:function(t){return""===t?"":window.Globalize&&this.options.numberFormat?Globalize.format(t,this.options.numberFormat,this.options.culture):t},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},isValid:function(){var t=this.value();return null===t?!1:t===this._adjustValue(t)},_value:function(t,e){var i;""!==t&&(i=this._parse(t),null!==i&&(e||(i=this._adjustValue(i)),t=this._format(i))),this.element.val(t),this._refresh()},_destroy:function(){this.element.prop("disabled",!1).removeAttr("autocomplete role aria-valuemin aria-valuemax aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:r(function(t){this._stepUp(t)}),_stepUp:function(t){this._start()&&(this._spin((t||1)*this.options.step),this._stop())},stepDown:r(function(t){this._stepDown(t)}),_stepDown:function(t){this._start()&&(this._spin((t||1)*-this.options.step),this._stop())},pageUp:r(function(t){this._stepUp((t||1)*this.options.page)}),pageDown:r(function(t){this._stepDown((t||1)*this.options.page)}),value:function(t){return arguments.length?(r(this._value).call(this,t),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}}),t.uiBackCompat!==!1&&t.widget("ui.spinner",t.ui.spinner,{_enhance:function(){this.uiSpinner=this.element.attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml())},_uiSpinnerHtml:function(){return""},_buttonHtml:function(){return""}}),t.ui.spinner,t.widget("ui.tabs",{version:"1.12.1",delay:300,options:{active:null,classes:{"ui-tabs":"ui-corner-all","ui-tabs-nav":"ui-corner-all","ui-tabs-panel":"ui-corner-bottom","ui-tabs-tab":"ui-corner-top"},collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_isLocal:function(){var t=/#.*$/;return function(e){var i,s;i=e.href.replace(t,""),s=location.href.replace(t,"");try{i=decodeURIComponent(i)}catch(n){}try{s=decodeURIComponent(s)}catch(n){}return e.hash.length>1&&i===s}}(),_create:function(){var e=this,i=this.options;this.running=!1,this._addClass("ui-tabs","ui-widget ui-widget-content"),this._toggleClass("ui-tabs-collapsible",null,i.collapsible),this._processTabs(),i.active=this._initialActive(),t.isArray(i.disabled)&&(i.disabled=t.unique(i.disabled.concat(t.map(this.tabs.filter(".ui-state-disabled"),function(t){return e.tabs.index(t)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):t(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var e=this.options.active,i=this.options.collapsible,s=location.hash.substring(1);return null===e&&(s&&this.tabs.each(function(i,n){return t(n).attr("aria-controls")===s?(e=i,!1):void 0}),null===e&&(e=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===e||-1===e)&&(e=this.tabs.length?0:!1)),e!==!1&&(e=this.tabs.index(this.tabs.eq(e)),-1===e&&(e=i?!1:0)),!i&&e===!1&&this.anchors.length&&(e=0),e},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):t()}},_tabKeydown:function(e){var i=t(t.ui.safeActiveElement(this.document[0])).closest("li"),s=this.tabs.index(i),n=!0;if(!this._handlePageNav(e)){switch(e.keyCode){case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:s++;break;case t.ui.keyCode.UP:case t.ui.keyCode.LEFT:n=!1,s--;break;case t.ui.keyCode.END:s=this.anchors.length-1;break;case t.ui.keyCode.HOME:s=0;break;case t.ui.keyCode.SPACE:return e.preventDefault(),clearTimeout(this.activating),this._activate(s),void 0;case t.ui.keyCode.ENTER:return e.preventDefault(),clearTimeout(this.activating),this._activate(s===this.options.active?!1:s),void 0;default:return}e.preventDefault(),clearTimeout(this.activating),s=this._focusNextTab(s,n),e.ctrlKey||e.metaKey||(i.attr("aria-selected","false"),this.tabs.eq(s).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",s)},this.delay))}},_panelKeydown:function(e){this._handlePageNav(e)||e.ctrlKey&&e.keyCode===t.ui.keyCode.UP&&(e.preventDefault(),this.active.trigger("focus"))},_handlePageNav:function(e){return e.altKey&&e.keyCode===t.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):e.altKey&&e.keyCode===t.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):void 0},_findNextTab:function(e,i){function s(){return e>n&&(e=0),0>e&&(e=n),e}for(var n=this.tabs.length-1;-1!==t.inArray(s(),this.options.disabled);)e=i?e+1:e-1;return e},_focusNextTab:function(t,e){return t=this._findNextTab(t,e),this.tabs.eq(t).trigger("focus"),t},_setOption:function(t,e){return"active"===t?(this._activate(e),void 0):(this._super(t,e),"collapsible"===t&&(this._toggleClass("ui-tabs-collapsible",null,e),e||this.options.active!==!1||this._activate(0)),"event"===t&&this._setupEvents(e),"heightStyle"===t&&this._setupHeightStyle(e),void 0)},_sanitizeSelector:function(t){return t?t.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var e=this.options,i=this.tablist.children(":has(a[href])");e.disabled=t.map(i.filter(".ui-state-disabled"),function(t){return i.index(t)}),this._processTabs(),e.active!==!1&&this.anchors.length?this.active.length&&!t.contains(this.tablist[0],this.active[0])?this.tabs.length===e.disabled.length?(e.active=!1,this.active=t()):this._activate(this._findNextTab(Math.max(0,e.active-1),!1)):e.active=this.tabs.index(this.active):(e.active=!1,this.active=t()),this._refresh()},_refresh:function(){this._setOptionDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-hidden":"true"}),this.active.length?(this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}),this._addClass(this.active,"ui-tabs-active","ui-state-active"),this._getPanelForTab(this.active).show().attr({"aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var e=this,i=this.tabs,s=this.anchors,n=this.panels;this.tablist=this._getList().attr("role","tablist"),this._addClass(this.tablist,"ui-tabs-nav","ui-helper-reset ui-helper-clearfix ui-widget-header"),this.tablist.on("mousedown"+this.eventNamespace,"> li",function(e){t(this).is(".ui-state-disabled")&&e.preventDefault()}).on("focus"+this.eventNamespace,".ui-tabs-anchor",function(){t(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this.tabs=this.tablist.find("> li:has(a[href])").attr({role:"tab",tabIndex:-1}),this._addClass(this.tabs,"ui-tabs-tab","ui-state-default"),this.anchors=this.tabs.map(function(){return t("a",this)[0]}).attr({role:"presentation",tabIndex:-1}),this._addClass(this.anchors,"ui-tabs-anchor"),this.panels=t(),this.anchors.each(function(i,s){var n,o,a,r=t(s).uniqueId().attr("id"),h=t(s).closest("li"),l=h.attr("aria-controls");e._isLocal(s)?(n=s.hash,a=n.substring(1),o=e.element.find(e._sanitizeSelector(n))):(a=h.attr("aria-controls")||t({}).uniqueId()[0].id,n="#"+a,o=e.element.find(n),o.length||(o=e._createPanel(a),o.insertAfter(e.panels[i-1]||e.tablist)),o.attr("aria-live","polite")),o.length&&(e.panels=e.panels.add(o)),l&&h.data("ui-tabs-aria-controls",l),h.attr({"aria-controls":a,"aria-labelledby":r}),o.attr("aria-labelledby",r)}),this.panels.attr("role","tabpanel"),this._addClass(this.panels,"ui-tabs-panel","ui-widget-content"),i&&(this._off(i.not(this.tabs)),this._off(s.not(this.anchors)),this._off(n.not(this.panels)))},_getList:function(){return this.tablist||this.element.find("ol, ul").eq(0)},_createPanel:function(e){return t("
    ").attr("id",e).data("ui-tabs-destroy",!0)},_setOptionDisabled:function(e){var i,s,n;for(t.isArray(e)&&(e.length?e.length===this.anchors.length&&(e=!0):e=!1),n=0;s=this.tabs[n];n++)i=t(s),e===!0||-1!==t.inArray(n,e)?(i.attr("aria-disabled","true"),this._addClass(i,null,"ui-state-disabled")):(i.removeAttr("aria-disabled"),this._removeClass(i,null,"ui-state-disabled"));this.options.disabled=e,this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,e===!0)},_setupEvents:function(e){var i={};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(!0,this.anchors,{click:function(t){t.preventDefault()}}),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(e){var i,s=this.element.parent();"fill"===e?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=t(this).outerHeight(!0)}),this.panels.each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.panels.each(function(){i=Math.max(i,t(this).height("").height())}).height(i))},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),o=n.closest("li"),a=o[0]===s[0],r=a&&i.collapsible,h=r?t():this._getPanelForTab(o),l=s.length?this._getPanelForTab(s):t(),c={oldTab:s,oldPanel:l,newTab:r?t():o,newPanel:h};e.preventDefault(),o.hasClass("ui-state-disabled")||o.hasClass("ui-tabs-loading")||this.running||a&&!i.collapsible||this._trigger("beforeActivate",e,c)===!1||(i.active=r?!1:this.tabs.index(o),this.active=a?t():o,this.xhr&&this.xhr.abort(),l.length||h.length||t.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(o),e),this._toggle(e,c))},_toggle:function(e,i){function s(){o.running=!1,o._trigger("activate",e,i)}function n(){o._addClass(i.newTab.closest("li"),"ui-tabs-active","ui-state-active"),a.length&&o.options.show?o._show(a,o.options.show,s):(a.show(),s())}var o=this,a=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){o._removeClass(i.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),n()}):(this._removeClass(i.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),r.hide(),n()),r.attr("aria-hidden","true"),i.oldTab.attr({"aria-selected":"false","aria-expanded":"false"}),a.length&&r.length?i.oldTab.attr("tabIndex",-1):a.length&&this.tabs.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),a.attr("aria-hidden","false"),i.newTab.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_activate:function(e){var i,s=this._findActive(e);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return e===!1?t():this.tabs.eq(e)},_getIndex:function(e){return"string"==typeof e&&(e=this.anchors.index(this.anchors.filter("[href$='"+t.ui.escapeSelector(e)+"']"))),e},_destroy:function(){this.xhr&&this.xhr.abort(),this.tablist.removeAttr("role").off(this.eventNamespace),this.anchors.removeAttr("role tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){t.data(this,"ui-tabs-destroy")?t(this).remove():t(this).removeAttr("role tabIndex aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded")}),this.tabs.each(function(){var e=t(this),i=e.data("ui-tabs-aria-controls");i?e.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):e.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(e){var i=this.options.disabled;i!==!1&&(void 0===e?i=!1:(e=this._getIndex(e),i=t.isArray(i)?t.map(i,function(t){return t!==e?t:null}):t.map(this.tabs,function(t,i){return i!==e?i:null})),this._setOptionDisabled(i))},disable:function(e){var i=this.options.disabled;if(i!==!0){if(void 0===e)i=!0;else{if(e=this._getIndex(e),-1!==t.inArray(e,i))return;i=t.isArray(i)?t.merge([e],i).sort():[e]}this._setOptionDisabled(i)}},load:function(e,i){e=this._getIndex(e);var s=this,n=this.tabs.eq(e),o=n.find(".ui-tabs-anchor"),a=this._getPanelForTab(n),r={tab:n,panel:a},h=function(t,e){"abort"===e&&s.panels.stop(!1,!0),s._removeClass(n,"ui-tabs-loading"),a.removeAttr("aria-busy"),t===s.xhr&&delete s.xhr};this._isLocal(o[0])||(this.xhr=t.ajax(this._ajaxSettings(o,i,r)),this.xhr&&"canceled"!==this.xhr.statusText&&(this._addClass(n,"ui-tabs-loading"),a.attr("aria-busy","true"),this.xhr.done(function(t,e,n){setTimeout(function(){a.html(t),s._trigger("load",i,r),h(n,e)},1)}).fail(function(t,e){setTimeout(function(){h(t,e)},1)})))},_ajaxSettings:function(e,i,s){var n=this;return{url:e.attr("href").replace(/#.*$/,""),beforeSend:function(e,o){return n._trigger("beforeLoad",i,t.extend({jqXHR:e,ajaxSettings:o},s))}}},_getPanelForTab:function(e){var i=t(e).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}}),t.uiBackCompat!==!1&&t.widget("ui.tabs",t.ui.tabs,{_processTabs:function(){this._superApply(arguments),this._addClass(this.tabs,"ui-tab")}}),t.ui.tabs,t.widget("ui.tooltip",{version:"1.12.1",options:{classes:{"ui-tooltip":"ui-corner-all ui-widget-shadow"},content:function(){var e=t(this).attr("title")||"";return t("").text(e).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,track:!1,close:null,open:null},_addDescribedBy:function(e,i){var s=(e.attr("aria-describedby")||"").split(/\s+/);s.push(i),e.data("ui-tooltip-id",i).attr("aria-describedby",t.trim(s.join(" ")))},_removeDescribedBy:function(e){var i=e.data("ui-tooltip-id"),s=(e.attr("aria-describedby")||"").split(/\s+/),n=t.inArray(i,s);-1!==n&&s.splice(n,1),e.removeData("ui-tooltip-id"),s=t.trim(s.join(" ")),s?e.attr("aria-describedby",s):e.removeAttr("aria-describedby")},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.liveRegion=t("
    ").attr({role:"log","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this.disabledTitles=t([])},_setOption:function(e,i){var s=this;this._super(e,i),"content"===e&&t.each(this.tooltips,function(t,e){s._updateContent(e.element)})},_setOptionDisabled:function(t){this[t?"_disable":"_enable"]()},_disable:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s.element[0],e.close(n,!0)}),this.disabledTitles=this.disabledTitles.add(this.element.find(this.options.items).addBack().filter(function(){var e=t(this);return e.is("[title]")?e.data("ui-tooltip-title",e.attr("title")).removeAttr("title"):void 0}))},_enable:function(){this.disabledTitles.each(function(){var e=t(this);e.data("ui-tooltip-title")&&e.attr("title",e.data("ui-tooltip-title"))}),this.disabledTitles=t([])},open:function(e){var i=this,s=t(e?e.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),e&&"mouseover"===e.type&&s.parents().each(function(){var e,s=t(this);s.data("ui-tooltip-open")&&(e=t.Event("blur"),e.target=e.currentTarget=this,i.close(e,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._registerCloseHandlers(e,s),this._updateContent(s,e))},_updateContent:function(t,e){var i,s=this.options.content,n=this,o=e?e.type:null;return"string"==typeof s||s.nodeType||s.jquery?this._open(e,t,s):(i=s.call(t[0],function(i){n._delay(function(){t.data("ui-tooltip-open")&&(e&&(e.type=o),this._open(e,t,i))})}),i&&this._open(e,t,i),void 0)},_open:function(e,i,s){function n(t){l.of=t,a.is(":hidden")||a.position(l)}var o,a,r,h,l=t.extend({},this.options.position);if(s){if(o=this._find(i))return o.tooltip.find(".ui-tooltip-content").html(s),void 0;i.is("[title]")&&(e&&"mouseover"===e.type?i.attr("title",""):i.removeAttr("title")),o=this._tooltip(i),a=o.tooltip,this._addDescribedBy(i,a.attr("id")),a.find(".ui-tooltip-content").html(s),this.liveRegion.children().hide(),h=t("
    ").html(a.find(".ui-tooltip-content").html()),h.removeAttr("name").find("[name]").removeAttr("name"),h.removeAttr("id").find("[id]").removeAttr("id"),h.appendTo(this.liveRegion),this.options.track&&e&&/^mouse/.test(e.type)?(this._on(this.document,{mousemove:n}),n(e)):a.position(t.extend({of:i},this.options.position)),a.hide(),this._show(a,this.options.show),this.options.track&&this.options.show&&this.options.show.delay&&(r=this.delayedShow=setInterval(function(){a.is(":visible")&&(n(l.of),clearInterval(r))},t.fx.interval)),this._trigger("open",e,{tooltip:a})}},_registerCloseHandlers:function(e,i){var s={keyup:function(e){if(e.keyCode===t.ui.keyCode.ESCAPE){var s=t.Event(e);s.currentTarget=i[0],this.close(s,!0)}}};i[0]!==this.element[0]&&(s.remove=function(){this._removeTooltip(this._find(i).tooltip)}),e&&"mouseover"!==e.type||(s.mouseleave="close"),e&&"focusin"!==e.type||(s.focusout="close"),this._on(!0,i,s)},close:function(e){var i,s=this,n=t(e?e.currentTarget:this.element),o=this._find(n);return o?(i=o.tooltip,o.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&!n.attr("title")&&n.attr("title",n.data("ui-tooltip-title")),this._removeDescribedBy(n),o.hiding=!0,i.stop(!0),this._hide(i,this.options.hide,function(){s._removeTooltip(t(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),e&&"mouseleave"===e.type&&t.each(this.parents,function(e,i){t(i.element).attr("title",i.title),delete s.parents[e]}),o.closing=!0,this._trigger("close",e,{tooltip:i}),o.hiding||(o.closing=!1)),void 0):(n.removeData("ui-tooltip-open"),void 0)},_tooltip:function(e){var i=t("
    ").attr("role","tooltip"),s=t("
    ").appendTo(i),n=i.uniqueId().attr("id");return this._addClass(s,"ui-tooltip-content"),this._addClass(i,"ui-tooltip","ui-widget ui-widget-content"),i.appendTo(this._appendTo(e)),this.tooltips[n]={element:e,tooltip:i}},_find:function(t){var e=t.data("ui-tooltip-id");return e?this.tooltips[e]:null},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){var e=t.closest(".ui-front, dialog");return e.length||(e=this.document[0].body),e},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur"),o=s.element;n.target=n.currentTarget=o[0],e.close(n,!0),t("#"+i).remove(),o.data("ui-tooltip-title")&&(o.attr("title")||o.attr("title",o.data("ui-tooltip-title")),o.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),t.uiBackCompat!==!1&&t.widget("ui.tooltip",t.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),t.ui.tooltip}); \ No newline at end of file diff --git a/web/ia7/include/jquery-ui.smoothness.1.12.1.css b/web/ia7/include/jquery-ui.smoothness.1.12.1.css new file mode 100644 index 000000000..294452f15 --- /dev/null +++ b/web/ia7/include/jquery-ui.smoothness.1.12.1.css @@ -0,0 +1,1311 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); /* support: IE8 */ +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; + pointer-events: none; +} + + +/* Icons +----------------------------------*/ +.ui-icon { + display: inline-block; + vertical-align: middle; + margin-top: -.25em; + position: relative; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + +.ui-widget-icon-block { + left: 50%; + margin-left: -8px; + display: block; +} + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin: 2px 0 0 0; + padding: .5em .5em .5em .7em; + font-size: 100%; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: 0; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + cursor: pointer; + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-item-wrapper { + position: relative; + padding: 3px 1em 3px .4em; +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item-wrapper { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} +.ui-button { + padding: .4em 1em; + display: inline-block; + position: relative; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + /* Support: IE <= 11 */ + overflow: visible; +} + +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} + +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2em; + box-sizing: border-box; + text-indent: -9999px; + white-space: nowrap; +} + +/* no icon support for input elements */ +input.ui-button.ui-button-icon-only { + text-indent: 0; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon { + position: absolute; + top: 50%; + left: 50%; + margin-top: -8px; + margin-left: -8px; +} + +.ui-button.ui-icon-notext .ui-icon { + padding: 0; + width: 2.1em; + height: 2.1em; + text-indent: -9999px; + white-space: nowrap; + +} + +input.ui-button.ui-icon-notext .ui-icon { + width: auto; + height: auto; + text-indent: 0; + white-space: normal; + padding: .4em 1em; +} + +/* workarounds */ +/* Support: Firefox 5 - 40 */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-controlgroup { + vertical-align: middle; + display: inline-block; +} +.ui-controlgroup > .ui-controlgroup-item { + float: left; + margin-left: 0; + margin-right: 0; +} +.ui-controlgroup > .ui-controlgroup-item:focus, +.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus { + z-index: 9999; +} +.ui-controlgroup-vertical > .ui-controlgroup-item { + display: block; + float: none; + width: 100%; + margin-top: 0; + margin-bottom: 0; + text-align: left; +} +.ui-controlgroup-vertical .ui-controlgroup-item { + box-sizing: border-box; +} +.ui-controlgroup .ui-controlgroup-label { + padding: .4em 1em; +} +.ui-controlgroup .ui-controlgroup-label span { + font-size: 80%; +} +.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item { + border-left: none; +} +.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item { + border-top: none; +} +.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content { + border-right: none; +} +.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content { + border-bottom: none; +} + +/* Spinner specific style fixes */ +.ui-controlgroup-vertical .ui-spinner-input { + + /* Support: IE8 only, Android < 4.4 only */ + width: 75%; + width: calc( 100% - 2.4em ); +} +.ui-controlgroup-vertical .ui-spinner .ui-spinner-up { + border-top-style: solid; +} + +.ui-checkboxradio-label .ui-icon-background { + box-shadow: inset 1px 1px 1px #ccc; + border-radius: .12em; + border: none; +} +.ui-checkboxradio-radio-label .ui-icon-background { + width: 16px; + height: 16px; + border-radius: 1em; + overflow: visible; + border: none; +} +.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon, +.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon { + background-image: none; + width: 8px; + height: 8px; + border-width: 4px; + border-style: solid; +} +.ui-checkboxradio-disabled { + pointer-events: none; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +/* Icons */ +.ui-datepicker .ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; + left: .5em; + top: .3em; +} +.ui-dialog { + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 20px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-n { + height: 2px; + top: 0; +} +.ui-dialog .ui-resizable-e { + width: 2px; + right: 0; +} +.ui-dialog .ui-resizable-s { + height: 2px; + bottom: 0; +} +.ui-dialog .ui-resizable-w { + width: 2px; + left: 0; +} +.ui-dialog .ui-resizable-se, +.ui-dialog .ui-resizable-sw, +.ui-dialog .ui-resizable-ne, +.ui-dialog .ui-resizable-nw { + width: 7px; + height: 7px; +} +.ui-dialog .ui-resizable-se { + right: 0; + bottom: 0; +} +.ui-dialog .ui-resizable-sw { + left: 0; + bottom: 0; +} +.ui-dialog .ui-resizable-ne { + right: 0; + top: 0; +} +.ui-dialog .ui-resizable-nw { + left: 0; + top: 0; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); + height: 100%; + filter: alpha(opacity=25); /* support: IE8 */ + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-selectable { + -ms-touch-action: none; + touch-action: none; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} +.ui-selectmenu-menu .ui-menu { + overflow: auto; + overflow-x: hidden; + padding-bottom: 1px; +} +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} +.ui-selectmenu-open { + display: block; +} +.ui-selectmenu-text { + display: block; + margin-right: 20px; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-selectmenu-button.ui-button { + text-align: left; + white-space: nowrap; + width: 14em; +} +.ui-selectmenu-icon.ui-icon { + float: right; + margin-top: 0; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; + -ms-touch-action: none; + touch-action: none; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* support: IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-sortable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: .222em 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 2em; +} +.ui-spinner-button { + width: 1.6em; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to override default borders */ +.ui-spinner a.ui-spinner-button { + border-top-style: none; + border-bottom-style: none; + border-right-style: none; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav .ui-tabs-anchor { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { + cursor: text; +} +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; +} +body .ui-tooltip { + border-width: 2px; +} +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget.ui-widget-content { + border: 1px solid #d3d3d3; +} +.ui-widget-content { + border: 1px solid #aaaaaa; + background: #ffffff; + color: #222222; +} +.ui-widget-content a { + color: #222222; +} +.ui-widget-header { + border: 1px solid #aaaaaa; + background: #cccccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x; + color: #222222; + font-weight: bold; +} +.ui-widget-header a { + color: #222222; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { + border: 1px solid #d3d3d3; + background: #e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x; + font-weight: normal; + color: #555555; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { + color: #555555; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { + border: 1px solid #999999; + background: #dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { + color: #212121; + text-decoration: none; +} + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + border: 1px solid #aaaaaa; + background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #aaaaaa; + background-color: #212121; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #212121; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fcefa1; + background: #fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x; + color: #363636; +} +.ui-state-checked { + border: 1px solid #fcefa1; + background: #fbf9ee; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x; + color: #cd0a0a; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #cd0a0a; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #cd0a0a; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon { + background-image: url("images/ui-icons_454545_256x240.png"); +} +.ui-state-active .ui-icon, +.ui-button:active .ui-icon { + background-image: url("images/ui-icons_454545_256x240.png"); +} +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon { + background-image: url("images/ui-icons_2e83ff_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_cd0a0a_256x240.png"); +} +.ui-button .ui-icon { + background-image: url("images/ui-icons_888888_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa; + opacity: .3; + filter: Alpha(Opacity=30); /* support: IE8 */ +} +.ui-widget-shadow { + -webkit-box-shadow: -8px -8px 8px #aaaaaa; + box-shadow: -8px -8px 8px #aaaaaa; +} diff --git a/web/ia7/include/jquery.1.12.4.min.js b/web/ia7/include/jquery.1.12.4.min.js new file mode 100644 index 000000000..e83647587 --- /dev/null +++ b/web/ia7/include/jquery.1.12.4.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; +}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
    $type\n"; + $html .= "
    \n"; $headers--; diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index a17b78b9c..f06676d43 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -1,12 +1,22 @@
    -
    +

    Version:
    Modified:
    OS:
    Perl:
    User:

    +
    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index a89b6e495..9c5526017 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -3,9 +3,19 @@ var entity_store = {}; //global storage of entities var json_store = {}; var updateSocket; +var updateSocketN; //Second socket for notifications var display_mode; if (display_mode == undefined) display_mode = "simple"; +var notifications; +var speech_sound; +var speech_banner; +var audio_init; +var audioElement = document.getElementById('sound_element'); +var authorized = "false"; +var developer = false; +var ctx; //audio context +var buf; //audio buffer //Takes the current location and parses the achor element into a hash function URLToHash() { @@ -109,7 +119,6 @@ function getJSONDataByPath (path){ return returnJSON; } - //Called anytime the page changes function changePage (){ var URLHash = URLToHash(); @@ -119,10 +128,7 @@ function changePage (){ URLHash.path = "collections"; } if (getJSONDataByPath("ia7_config") === undefined){ - // We need at minimum the basic collections data to render all pages - // (the breadcrumb) - // NOTE may want to think about how to handle dynamic changes to the - // collections list + // Load all the specific preferences $.ajax({ type: "GET", url: "/json/ia7_config", @@ -133,11 +139,34 @@ function changePage (){ } }); } else { - //console.log("x "+json_store.ia7_config.prefs.substate_percentages); - if (json_store.ia7_config.prefs.header_button == "no") { - $("#mhstatus").remove(); - } + if (json_store.ia7_config.prefs.header_button == "no") $("#mhstatus").remove(); + if (json_store.ia7_config.prefs.audio_controls !== undefined && json_store.ia7_config.prefs.audio_controls == "yes") { + $("#sound_element").attr("controls", "controls"); //Show audio Controls + } if (json_store.ia7_config.prefs.substate_percentages === undefined) json_store.ia7_config.prefs.substate_percentages = 20; + if (json_store.ia7_config.prefs.developer !== undefined) developer = json_store.ia7_config.prefs.developer; + // First time loading, set the default speech notifications + if (speech_sound === undefined) { + if ((json_store.ia7_config.prefs.speech_default !== undefined) && (json_store.ia7_config.prefs.speech_default.search("audio") >= 0 )) { + speech_sound = "yes"; + } else { + speech_sound = "no"; + } + } + if (speech_banner === undefined) { + if ((json_store.ia7_config.prefs.speech_default !== undefined) && (json_store.ia7_config.prefs.speech_default.search("banner") >= 0 )) { + speech_banner = "yes"; + } else { + speech_banner = "no"; + } + } + if ((json_store.ia7_config.prefs.notifications == undefined) || ((json_store.ia7_config.prefs.notifications !== undefined) && (json_store.ia7_config.prefs.notifications == "no" ))) { + notifications = "disabled"; + speech_sound = "no"; + speech_banner = "no"; + } else { + notifications = "enabled"; + } } if (getJSONDataByPath("collections") === undefined){ // We need at minimum the basic collections data to render all pages @@ -155,9 +184,14 @@ function changePage (){ }); } else { + // Check for authorize + authDetails(); // Clear Options Entity by Default $("#toolButton").attr('entity', ''); + // Remove the RRD Last Updated + $('#Last_updated').remove(); + //Trim leading and trailing slashes from path var path = URLHash.path.replace(/^\/|\/$/g, ""); if (path.indexOf('objects') === 0){ @@ -177,16 +211,13 @@ function changePage (){ args = args.replace(/\=undefined/img,''); //HP sometimes arguments are just items and not key=value... link += "?"+args; } - //alert("link="+link); - //$.get(URLHash.link, function( data ) { + $.get(link, function( data ) { - data = data.replace(/]*>/img, ''); //Remove stylesheets - data = data.replace(/]*>((\r|\n|.)*?)<\/title[^>]*>/img, ''); //Remove title - data = data.replace(/]*>/img, ''); //Remove meta refresh - data = data.replace(/]*>/img, ''); //Remove base target tags + $('#list_content').html("
    "); $('#buffer_page').append("
    "); - $('#row_page').html(data); + parseLinkData(link,data); //remove css & fix up Mr.House setup stuff + }); } else if(path.indexOf('print_log') === 0){ @@ -198,7 +229,7 @@ function changePage (){ else if(path.indexOf('display_table') === 0){ var path_arg = path.split('?'); display_table(path_arg[1]); - } + } else if(path.indexOf('floorplan') === 0){ var path_arg = path.split('?'); floorplan(path_arg[1]); @@ -525,6 +556,8 @@ var loadList = function() { // Sort that list if a sort exists, probably exists a shorter way to // write the sort + // Sorting code removed. Original design idea that buttons could be moved + // Around by the end user. Possible function for the future. // if (sort_list !== undefined){ // entity_list = sortArrayByArray(entity_list, sort_list); // } @@ -535,7 +568,7 @@ var loadList = function() { // This is not an entity, likely a value of the root obj continue; } - if (json_store.objects[entity].hidden != undefined){ + if (json_store.objects[entity].hidden !== undefined){ // This is an entity with the hidden property, so skip it continue; } @@ -557,8 +590,8 @@ var loadList = function() { button_html += '
    '; button_html += ''; button_html += '
    '; - button_html += ''; button_html += '
    +
    +
    -
    + - + +
    +
    - + + +
    +
    \ No newline at end of file diff --git a/web/organizer/contacts.pl b/web/organizer/contacts.pl index c145b2a37..84043208d 100644 --- a/web/organizer/contacts.pl +++ b/web/organizer/contacts.pl @@ -155,7 +155,7 @@ BEGIN my ($command) = $objCGI->param('vsCOM') || ""; my ($idNum) = $objCGI->param('vsID') || ""; my ($scriptName) = $ENV{'SCRIPT_NAME'} || "contacts.pl"; -$scriptName = $ia7_prefix . "/organizer/tasks.pl" if ( $web_mode eq "IA7" ); +$scriptName = $ia7_prefix . "/organizer/contacts.pl" if ( $web_mode eq "IA7" ); my ($filePath) = $ENV{"CWD"} . "/" . $fileName; $filePath = "$config_parms{organizer_dir}/$fileName"; my ($activePage) = $objCGI->param('vsAP') || "1"; From d8144eb0dee20102e72f747f569065468e10f33f Mon Sep 17 00:00:00 2001 From: waynieack Date: Tue, 7 Mar 2017 20:29:54 -0600 Subject: [PATCH 142/209] Fix for specifying a multicast interface when a specific interface is defined in the mh.private.ini with the alexaHttpIp option. --- lib/AlexaBridge.pm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/AlexaBridge.pm b/lib/AlexaBridge.pm index ebf44ab69..9e7cfcc0d 100644 --- a/lib/AlexaBridge.pm +++ b/lib/AlexaBridge.pm @@ -378,7 +378,13 @@ sub _constant { sub _mcast_add { my ( $sock, $addr ) = @_; - my $ip_mreq = inet_aton( $addr ) . INADDR_ANY; + my $ip_mreq; + if (defined $::config_parms{'alexaHttpIp'}) { + $ip_mreq = inet_aton($::config_parms{'alexaHttpIp'}); + } else { + $ip_mreq = inet_aton('0.0.0.0'); + } + $ip_mreq = inet_aton( $addr ) . $ip_mreq; setsockopt( $sock, From 1141468315c0a3410d41dc01c6cd0ecbad631a2e Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Thu, 9 Mar 2017 15:53:30 -0700 Subject: [PATCH 143/209] v4.2 - Syncronizing stable with Master --- web/bin/set_func.pl | 2 +- web/bin/triggers.pl | 8 +- web/ia7/graphics/help.gif | Bin 0 -> 1582 bytes web/ia7/graphics/important.gif | Bin 0 -> 1492 bytes web/ia7/graphics/info.gif | Bin 0 -> 1487 bytes web/ia7/graphics/title.gif | Bin 0 -> 317 bytes .../include/bootstrap-custom-button.3.3.5.css | 29 +++ web/ia7/include/bootstrap-theme.3.0.2.min.css | 9 - web/ia7/include/bootstrap.3.0.2.min.css | 9 - web/ia7/include/bootstrap.3.0.2.min.js | 9 - web/ia7/include/font-awesome.4.0.3.min.css | 4 - web/ia7/include/jquery.alerts.css | 57 +++++ web/ia7/include/jquery.alerts.js | 237 ++++++++++++++++++ 13 files changed, 328 insertions(+), 36 deletions(-) create mode 100755 web/ia7/graphics/help.gif create mode 100755 web/ia7/graphics/important.gif create mode 100755 web/ia7/graphics/info.gif create mode 100755 web/ia7/graphics/title.gif create mode 100644 web/ia7/include/bootstrap-custom-button.3.3.5.css delete mode 100644 web/ia7/include/bootstrap-theme.3.0.2.min.css delete mode 100644 web/ia7/include/bootstrap.3.0.2.min.css delete mode 100644 web/ia7/include/bootstrap.3.0.2.min.js delete mode 100644 web/ia7/include/font-awesome.4.0.3.min.css create mode 100755 web/ia7/include/jquery.alerts.css create mode 100644 web/ia7/include/jquery.alerts.js diff --git a/web/bin/set_func.pl b/web/bin/set_func.pl index 043eac577..c7bfdd798 100644 --- a/web/bin/set_func.pl +++ b/web/bin/set_func.pl @@ -11,7 +11,7 @@ use strict; -#print "db a=@ARGV\n"; +#&main::print_log("db a=@ARGV\n"); # Process form if ( @ARGV > 2 ) { diff --git a/web/bin/triggers.pl b/web/bin/triggers.pl index 18ae3cd3c..6b3873324 100644 --- a/web/bin/triggers.pl +++ b/web/bin/triggers.pl @@ -59,12 +59,12 @@ sub web_trigger_list { triggers, change the type to "Disabled". If you delete it, the trigger will be recreated the next time Misterhouse is restarted. -
    - + + $form_trigger - + $form_code - +
    '); + $("#popup_ok").click( function() { + $.alerts._hide(); + callback(true); + }); + $("#popup_ok").focus().keypress( function(e) { + if( e.keyCode == 13 || e.keyCode == 27 ) $("#popup_ok").trigger('click'); + }); + break; + case 'confirm': + $("#popup_message").after(''); + $("#popup_ok").click( function() { + $.alerts._hide(); + if( callback ) callback(true); + }); + $("#popup_cancel").click( function() { + $.alerts._hide(); + if( callback ) callback(false); + }); + $("#popup_ok").focus(); + $("#popup_ok, #popup_cancel").keypress( function(e) { + if( e.keyCode == 13 ) $("#popup_ok").trigger('click'); + if( e.keyCode == 27 ) $("#popup_cancel").trigger('click'); + }); + break; + case 'prompt': + $("#popup_message").append('
    ').after(''); + $("#popup_prompt").width( $("#popup_message").width() ); + $("#popup_ok").click( function() { + var val = $("#popup_prompt").val(); + $.alerts._hide(); + if( callback ) callback( val ); + }); + $("#popup_cancel").click( function() { + $.alerts._hide(); + if( callback ) callback( null ); + }); + $("#popup_prompt, #popup_ok, #popup_cancel").keypress( function(e) { + if( e.keyCode == 13 ) $("#popup_ok").trigger('click'); + if( e.keyCode == 27 ) $("#popup_cancel").trigger('click'); + }); + if( value ) $("#popup_prompt").val(value); + $("#popup_prompt").focus().select(); + break; + } + + // Make draggable + if( $.alerts.draggable ) { + try { + $("#popup_container").draggable({ handle: $("#popup_title") }); + $("#popup_title").css({ cursor: 'move' }); + } catch(e) { /* requires jQuery UI draggables */ } + } + }, + + _hide: function() { + $("#popup_container").remove(); + $.alerts._overlay('hide'); + $.alerts._maintainPosition(false); + }, + + _overlay: function(status) { + switch( status ) { + case 'show': + $.alerts._overlay('hide'); + $("BODY").append(''); + $("#popup_overlay").css({ + position: 'absolute', + zIndex: 99998, + top: '0px', + left: '0px', + width: '100%', + height: $(document).height(), + background: $.alerts.overlayColor, + opacity: $.alerts.overlayOpacity + }); + break; + case 'hide': + $("#popup_overlay").remove(); + break; + } + }, + + _reposition: function() { + var top = (($(window).height() / 2) - ($("#popup_container").outerHeight() / 2)) + $.alerts.verticalOffset; + var left = (($(window).width() / 2) - ($("#popup_container").outerWidth() / 2)) + $.alerts.horizontalOffset; + if( top < 0 ) top = 0; + if( left < 0 ) left = 0; + + // IE6 fix + //if( $.browser.msie && parseInt($.browser.version) <= 6 ) top = top + $(window).scrollTop(); + top = top + $(window).scrollTop(); + + $("#popup_container").css({ + top: top + 'px', + left: left + 'px' + }); + $("#popup_overlay").height( $(document).height() ); + }, + + _maintainPosition: function(status) { + if( $.alerts.repositionOnResize ) { + switch(status) { + case true: + $(window).bind('resize', $.alerts._reposition); + break; + case false: + $(window).unbind('resize', $.alerts._reposition); + break; + } + } + } + + } + + // Shortuct functions + jAlert = function(message, title, callback) { + $.alerts.alert(message, title, callback); + } + + jConfirm = function(message, title, callback) { + $.alerts.confirm(message, title, callback); + }; + + jPrompt = function(message, value, title, callback) { + $.alerts.prompt(message, value, title, callback); + }; + +})(jQuery); From 210c3d560d92b4c9d73f989f2b8b351814fadc98 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Thu, 9 Mar 2017 15:57:43 -0700 Subject: [PATCH 144/209] v4.2 - update download.pod version --- docs/download.pod | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/download.pod b/docs/download.pod index 0dc189b8e..88bc0b5f5 100644 --- a/docs/download.pod +++ b/docs/download.pod @@ -10,13 +10,13 @@ modify the code/common/mh_release.pl file to search the new format!! =head1 Current Stable -B - released on 06/20/2013 +B - released on 03/08/2017 =over =item Linux, Windows and MacOS -L +L The Windows only compiled version of Misterhouse is sadly no longer supported. @@ -28,7 +28,7 @@ A large (30MB) zip of optional files (mainly sound files) is here: L - released on 03/02/2012 +B - released on 02/28/2016 =over @@ -37,7 +37,7 @@ B - released on 03/02/2012 =begin HTML -https://api.github.com/repos/hollie/misterhouse/zipball/v2.200 +https://api.github.com/repos/hollie/misterhouse/zipball/v3.0 =end HTML From ec9703843fec39b22e24adc0478434fd0ebd0684 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Thu, 9 Mar 2017 16:00:49 -0700 Subject: [PATCH 145/209] v4.2 - updating ia7 config files --- data/web/collections.json | 13 +++++++------ data/web/ia7_config.json | 4 +++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/data/web/collections.json b/data/web/collections.json index 7efe8a56c..2d0618eb1 100644 --- a/data/web/collections.json +++ b/data/web/collections.json @@ -395,6 +395,7 @@ "28" : { "link" : "/bin/set_parm_tv_provider.pl", "name" : "Setup TV Provider", + "mode" : "advanced", "icon" : "fa-desktop" }, "5" : { @@ -487,6 +488,7 @@ "26" : { "link" : "/ia5/house/irman.shtml", "name" : "Program IRMAN", + "mode" : "advanced", "icon" : "fa-rss" }, "57" : { @@ -707,6 +709,7 @@ "27" : { "icon" : "fa-wrench", "name" : "Header Control", + "mode" : "advanced", "link" : "/bin/headercontrol.pl" }, "78" : { @@ -717,7 +720,7 @@ "102" : { "name" : "Picture Frame", "icon" : "fa-desktop", - "external" : "http://home.krkeegan.com:8081/misc/photos.shtml" + "external" : "$config_parms{html_dir}/misc/photos.shtml" }, "105" : { "link" : "/ia5/calendar/main.shtml", @@ -772,14 +775,12 @@ "icon" : "fa-home", "name" : "Gear Settings", "children" : [ - 501 + 700 ] }, - "501" : { - "link" : "/UNSET_PASSWORD?user=admin", - "name" : "Authorize", - "icon" : "fa-lock" + "700" : { + "user" : "$Authorized" }, "600" : { "link" : "/ia7/#path=/objects&parents=ia7_status_items", diff --git a/data/web/ia7_config.json b/data/web/ia7_config.json index 2fb7e729e..fec693a36 100644 --- a/data/web/ia7_config.json +++ b/data/web/ia7_config.json @@ -7,7 +7,9 @@ "fp_icon_size" : "32", "fp_state_popovers" : "yes", "substate_percentages" : "20", - "disable_current_state" : "yes" + "disable_current_state" : "yes", + "notifications" : "yes", + "speech_default" : "banner" }, "objects" : { "example_object" : { From 0d5e2f2fd10a967cf87d86e0824fe6eecfc8604e Mon Sep 17 00:00:00 2001 From: Tobi Date: Fri, 17 Mar 2017 07:22:27 +0100 Subject: [PATCH 146/209] entries of $main::Debug are still lowerecase... this was already fixed in the past, but seems to have found its way back into master... --- lib/raZberry.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 723c8976c..a98cd0966 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -215,7 +215,7 @@ sub new { $self->{port} = $port if ($port); $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug=(\s+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); - $self->{debug} = $main::Debug{raZberry} if ( defined $main::Debug{raZberry} ); + $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} ); From 61208d512bf390e16b9a30f8f0b38939f704762a Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Mon, 20 Mar 2017 20:09:16 -0600 Subject: [PATCH 147/209] Remove a debug statement in http_server.pl --- lib/http_server.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index 6629eff3a..549e02b46 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -479,7 +479,7 @@ sub http_process_request { my ( $user, $password_crypted ) = &password_check2($password); $Authorized = $user if $password_crypted; $html .= &html_authorized; - $html .= "REMOVEME = get_arg = " . $get_arg . "
    \n"; + #$html .= "REMOVEME = get_arg = " . $get_arg . "
    \n"; $html .= "
    Refresh: Main Page\n"; # $html .= &html_reload_link('/', 'Refresh Main Page'); From ce25504c3f2ae094aaf7e523a836bb723475a597 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Mon, 20 Mar 2017 20:14:19 -0600 Subject: [PATCH 148/209] Fix VERSION from last commit --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index bf77d5496..6842dbdf3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2 +unstable From d3f5bb478f482110059356ff7824e89f2c66db59 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Tue, 21 Mar 2017 18:52:21 -0600 Subject: [PATCH 149/209] IA7 v1.3.620 - fixed password parsing to allow a ! --- lib/http_server.pl | 4 ++-- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index 549e02b46..0b674bf19 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -328,13 +328,13 @@ sub http_process_request { $Http{'Content-Length'} || $Http{'Content-length'} || $Http{'content-length'}; # Netscape uses lower case l - print "http POST query has $cl bytes of args\n"; # if $main::Debug{http}; + print "http POST query has $cl bytes of args\n" if $main::Debug{http}; my $buf; read $socket, $buf, $cl; # Save the body into the global var $HTTP_BODY = $buf; - print "http POST buf=$buf get_arg=$get_arg\n" if $main::Debug{http}; + print "http POST buf=$buf get_arg=$get_arg\n";# if $main::Debug{http}; # This is a bad practice to merge the body and arguments together as the # body may not always contain an argument string. It may contain JSON diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index f06676d43..bedabfbb0 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.3.610 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.3.620 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 9c5526017..961cc4dde 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -2746,11 +2746,13 @@ var authorize_modal = function(user) { }); $('#LoginModalpw').submit( function (e) { e.preventDefault(); - //console.log("Custom submit function"); + var encoded_data = $(this).serialize(); + encoded_data = encoded_data.replace(/\!/g,"%21"); //for some reason serialize doesn't encode a !... + //console.log("Custom submit function: "+$(this).serialize()+" "+encoded_data); $.ajax({ type: "POST", url: "/SET_PASSWORD_FORM", - data: $(this).serialize(), + data: encoded_data, success: function(data){ var status=data.match(/\(.*)\<\/b\>/gm); //console.log("match="+status[2]); //3rd match is password status From d627f4c453aec8164ba49b5db1f1b17f0acab11b Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Tue, 21 Mar 2017 18:55:02 -0600 Subject: [PATCH 150/209] remove a debug statement --- lib/http_server.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http_server.pl b/lib/http_server.pl index 0b674bf19..f3a126bde 100644 --- a/lib/http_server.pl +++ b/lib/http_server.pl @@ -334,7 +334,7 @@ sub http_process_request { # Save the body into the global var $HTTP_BODY = $buf; - print "http POST buf=$buf get_arg=$get_arg\n";# if $main::Debug{http}; + print "http POST buf=$buf get_arg=$get_arg\n" if $main::Debug{http}; # This is a bad practice to merge the body and arguments together as the # body may not always contain an argument string. It may contain JSON From a63f28dc9a8c4d8324f2369a6da6ac7d8837bfe0 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Tue, 21 Mar 2017 19:03:07 -0600 Subject: [PATCH 151/209] fix debug --- lib/raZberry.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 723c8976c..a98cd0966 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -215,7 +215,7 @@ sub new { $self->{port} = $port if ($port); $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug=(\s+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); - $self->{debug} = $main::Debug{raZberry} if ( defined $main::Debug{raZberry} ); + $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} ); From 282f4b3bf7eeabedab86bfcbd22e109d24669d5f Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 24 Mar 2017 15:25:18 -0600 Subject: [PATCH 152/209] v2.0.1 - some checks around battery level --- lib/raZberry.pm | 56 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index 723c8976c..a737d4711 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0 +=head1 B v2.0.1 =head2 SYNOPSIS @@ -112,6 +112,9 @@ http calls can cause pauses. There are a few possible options around this; =head2 CHANGELOG +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 @@ -192,6 +195,7 @@ sub new { my ( $class, $addr, $poll, $options ) = @_; my $self = {}; bless $self, $class; + &main::print_log("[raZberry]: v2.0.1 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; $self->{config}->{poll_seconds} = 5; @@ -215,7 +219,7 @@ sub new { $self->{port} = $port if ($port); $self->{debug} = 0; ( $self->{debug} ) = ( $options =~ /debug=(\s+)/i ) if ( ( defined $options ) and ( $options =~ m/debug=/i ) ); - $self->{debug} = $main::Debug{raZberry} if ( defined $main::Debug{raZberry} ); + $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} ); @@ -305,11 +309,13 @@ sub get_controllerdata { } sub poll { - my ($self) = @_; + my ( $self, $option ) = @_; + $option = "" unless ( defined $option ); &main::print_log("[raZberry]: Polling initiated") if ( $self->{debug} ); my $cmd = ""; $cmd = "?since=" . $self->{lastupdate} if ( defined $self->{lastupdate} ); + $cmd = "" if ( lc $option eq "full" ); &main::print_log("[raZberry]: cmd=$cmd") if ( $self->{debug} > 1 ); for my $dev ( keys %{ $self->{data}->{force_update} } ) { @@ -335,10 +341,13 @@ sub poll { #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]: 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}; } @@ -1001,9 +1010,12 @@ sub battery_check { return; } - if ( $self->{battery_level} eq "" ) { - main::print_log("[raZberry_blind] INFO Battery level currently undefined"); - 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; + } } main::print_log( "[raZberry_blind] INFO Battery currently at " . $self->{battery_level} . "%" ); if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { @@ -1013,6 +1025,7 @@ sub battery_check { else { $self->{battery_alert} = 0; } + return $self->{battery_level}; } sub _battery_timer { @@ -1023,7 +1036,7 @@ sub _battery_timer { sub battery_level { my ($self) = @_; - + $$self{master_object}->poll("full") if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ); return ( $self->{battery_level} ); } @@ -1048,9 +1061,9 @@ sub new { $$self{devid} = $devid; $$self{devid_battery} = $devid_battery; $$self{type} = "Lock.Battery"; - - $object->register( $self, $devid, $options ); $object->register( $self, $devid_battery, $options ); + $$self{type} = "Lock"; + $object->register( $self, $devid, $options ); #$self->set($object->get_dev_status,$devid,'poll'); $self->{level} = ""; @@ -1109,7 +1122,7 @@ sub level { sub battery_level { my ($self) = @_; - + $$self{master_object}->poll("full") if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ); return ( $self->{battery_level} ); } @@ -1136,9 +1149,12 @@ sub update_data { sub battery_check { my ($self) = @_; - if ( $self->{battery_level} eq "" ) { - &main::print_log("[raZberry_lock] INFO Battery level currently undefined"); - 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_lock] INFO Battery level currently undefined"); + return; + } } &main::print_log( "[raZberry_lock] INFO Battery currently at " . $self->{battery_level} . "%" ); if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { @@ -1148,6 +1164,7 @@ sub battery_check { else { $self->{battery_alert} = 0; } + return $self->{battery_level}; } sub enable_user { @@ -1544,7 +1561,7 @@ sub new { sub battery_level { my ($self) = @_; - + $$self{master_object}->poll("full") if ( ( $self->{battery_level} eq "" ) or ( !defined $self->{battery_level} ) ); return ( $self->{battery_level} ); } @@ -1562,7 +1579,6 @@ sub isfailed { sub update_data { my ( $self, $data ) = @_; - $self->{battery_level} = $data->{battery_level}; $self->SUPER::set( $self->{battery_level} ); @@ -1570,9 +1586,12 @@ sub update_data { sub battery_check { my ($self) = @_; - if ( $self->{battery_level} eq "" ) { - main::print_log("[raZberry_battery] INFO Battery level currently undefined"); - 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_battery] INFO Battery level currently undefined"); + return; + } } main::print_log( "[raZberry_battery] INFO Battery currently at " . $self->{battery_level} . "%" ); if ( ( $self->{battery_level} < 30 ) and ( $self->{battery_alert} == 0 ) ) { @@ -1582,6 +1601,7 @@ sub battery_check { else { $self->{battery_alert} = 0; } + return $self->{battery_level}; } package raZberry_voltage; From 3fb2fb972db4d18c2a5b03e2706af47b68b6e312 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 26 Mar 2017 12:29:57 -0600 Subject: [PATCH 153/209] remove pod comment breaking lines --- code/common/news_email_breaking.pl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/common/news_email_breaking.pl b/code/common/news_email_breaking.pl index 238f83004..e99df8f94 100644 --- a/code/common/news_email_breaking.pl +++ b/code/common/news_email_breaking.pl @@ -34,12 +34,11 @@ Body: -- CNN confirms Sen. Barack Obama has chosen Delaware Sen. Joe Biden to be his vice-presidential running mate. ->+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+= +>+...[line break removed as it breaks pod comments] CNN covers the conventions: the Democrats live from Denver starting Monday and the Republicans live from Minneapolis-St. Paul starting September 1 on CNN and CNN.com. http://www.cnnpolitics.com ->+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+= - +>+...[line break removed as it breaks pod comments] You have opted-in in to receive this e-mail from CNN.com. From d54265e5b5f29309d720d7a83133798a65c130f6 Mon Sep 17 00:00:00 2001 From: H Plato Date: Tue, 28 Mar 2017 12:23:47 -0600 Subject: [PATCH 154/209] v2.1 - better handling of network communication issues --- lib/Venstar_Colortouch.pm | 165 +++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 93 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index 340e8021b..f1e125229 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,6 +1,6 @@ package Venstar_Colortouch; -# v2.0.16 +# v2.1 #added in https support and don't retry commands that have a valid error reason code. Only retry if the device doesn't respond. (ie error 500) @@ -40,6 +40,7 @@ use Data::Dumper; # "hum" doesn't return anything anymore. # v1.4.1 - API v5, working schedule, humidity setpoints # v2.0 - Background process +# v2.1 - fixed up some problems reconnecting to stat # Notes # - State can only be set by stat. Set mode will change the mode. @@ -76,6 +77,7 @@ $rest{settings} = "settings"; sub new { my ( $class, $host, $poll, $options ) = @_; + $options = "" unless ( defined $options ); my $self = {}; bless $self, $class; $self->{data} = undef; @@ -84,8 +86,7 @@ sub new { $self->{config}->{cache_time} = 30; #is this still necessary? $self->{config}->{cache_time} = $::config_params{venstar_config_cache_time} if defined $::config_params{venstar_config_cache_time}; - $self->{config}->{tz} = - $::config_params{time_zone}; #TODO Need to figure out DST for print runtimes + $self->{config}->{tz} = $::config_params{time_zone}; #TODO Need to figure out DST for print runtimes $self->{config}->{poll_seconds} = 10; $self->{config}->{poll_seconds} = $poll if ($poll); $self->{config}->{poll_seconds} = 1 @@ -96,8 +97,7 @@ sub new { $self->{method} = "http"; $self->{method} = "https" if ( $options =~ m/ssl/i ); $self->{debug} = 0; - ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) - if ( $options =~ m/debug\=/i ); + ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ) if ( $options =~ m/debug\=/i ); $self->{debug} = 0 if ( $self->{debug} < 0 ); $self->{loglevel} = 1; $self->{status} = ""; @@ -124,6 +124,9 @@ sub new { $self->{timer} = new Timer; $self->_init; $self->start_timer; + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Debug is " . $self->{debug} ) if ( $self->{debug} ); + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] v2.1 Controller Initialization Complete" ); + return $self; } @@ -173,13 +176,10 @@ sub _init { $self->{previous}->{tempunits} = $self->{data}->{tempunits}; $self->{previous}->{name} = $self->{data}->{name}; foreach my $key1 ( keys $self->{data}->{info} ) { - $self->{previous}->{info}->{$key1} = - $self->{data}->{info}->{$key1}; + $self->{previous}->{info}->{$key1} = $self->{data}->{info}->{$key1}; } - $self->{previous}->{sensors}->{sensors}[0]->{temp} = - $self->{data}->{sensors}->{sensors}[0]->{temp}; - $self->{previous}->{sensors}->{sensors}[0]->{hum} = - $self->{data}->{sensors}->{sensors}[0]->{hum}; + $self->{previous}->{sensors}->{sensors}[0]->{temp} = $self->{data}->{sensors}->{sensors}[0]->{temp}; + $self->{previous}->{sensors}->{sensors}[0]->{hum} = $self->{data}->{sensors}->{sensors}[0]->{hum}; ## set states based on available mode ### Strange, if this set is here, then the timer is not defined. #print "db: set " . $state[ $self->{data}->{info}->{state} ] . "=" . $self->{data}->{info}->{state} . "\n"; @@ -266,42 +266,45 @@ sub process_check { if ( $self->{debug} ); my $file_data = &main::file_read( $self->{poll_data_file} ); - - #for some reason get_url adds garbage to the output. Clean out the characters before and after the json + $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 =~ /({.*})/; + $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) ) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! bad data returned by poll" ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{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( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n" ); $com_status = "offline"; } else { - if ( keys $data ) { - $self->{poll_data_timestamp} = &main::get_tickcount(); - if ( $self->{poll_process_mode} eq "info" ) { - $self->{data}->{tempunits} = $data->{tempunits}; - $self->{data}->{name} = $data->{name}; - $self->{data}->{info} = $data; - $self->{data}->{timestamp} = &main::get_tickcount(); - } - elsif ( $self->{poll_process_mode} eq "sensors" ) { - $self->{data}->{sensors} = $data; - $self->{data}->{timestamp} = &main::get_tickcount(); - } - $self->process_data(); + my $data; + eval { $data = JSON::XS->new->decode($json_data); }; + + # catch crashes: + if ($@) { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; } else { - main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..." ); - $com_status = "offline"; + if ( keys $data ) { + $self->{poll_data_timestamp} = &main::get_tickcount(); + if ( $self->{poll_process_mode} eq "info" ) { + $self->{data}->{tempunits} = $data->{tempunits}; + $self->{data}->{name} = $data->{name}; + $self->{data}->{info} = $data; + $self->{data}->{timestamp} = &main::get_tickcount(); + } + elsif ( $self->{poll_process_mode} eq "sensors" ) { + $self->{data}->{sensors} = $data; + $self->{data}->{timestamp} = &main::get_tickcount(); + } + $self->process_data(); + } + else { + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] ERROR! Returned data not structured! Not processing..." ); + $com_status = "offline"; + } } } if ( scalar @{ $self->{poll_queue} } ) { @@ -426,6 +429,8 @@ sub _get_JSON_data { } else { +### Polls are expendable. If one doesn't trigger log it and move on. + if ( scalar @{ $self->{poll_queue} } < $self->{max_poll_queue} ) { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Queue is " . scalar @{ $self->{poll_queue} } . ". Queing command $mode, $cmd" ) @@ -436,20 +441,14 @@ sub _get_JSON_data { # if there are device issues. } else { - main::print_log( - "[Venstar Colortouch:" . $self->{data}->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Command discarded." ); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne "offline" ) { - main::print_log "[Venstar Colortouch:" - . $self->{data}->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } + main::print_log( "[Venstar Colortouch:" + . $self->{data}->{name} + . "] WARNING. Queue has grown past " + . $self->{max_poll_queue} + . ". Command discarded and background poll process stopped" ); + @{ $self->{poll_queue} } = (); + $self->{poll_process}->stop(); + $self->process_check(); } } } @@ -614,8 +613,8 @@ sub _push_JSON_data { my ($forceunocc) = $params =~ /forceunocc=(\d+)/; my ($sched) = $params =~ /schedule=(\d+)/; my $hum; - my ($humsp) = $params =~ /hum_setpoint=(\d+)/; - my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/; #need to add in dehumidifier stuff at some point + my ($humsp) = $params =~ /hum_setpoint=(\d+)/; + my ($dehumsp) = $params =~ /dehum_setpoint=(\d+)/; #need to add in dehumidifier stuff at some point my $humidity_change = 0; my ( $isSuccessResponse, $cunits, $caway, $csched, $chum, $chumsp, $cdehumsp ); @@ -1072,7 +1071,7 @@ sub print_info { else { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Direct mode enabled" ); } - + main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Communicating using $self->{method}" ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Fan mode is set to " . $fan[ $self->{data}->{info}->{fan} ] ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Fan is currently " . $fanstate[ $self->{data}->{info}->{fanstate} ] ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] System mode is " . $mode[ $self->{data}->{info}->{mode} ] ); @@ -1187,8 +1186,7 @@ sub process_data { . $self->{data}->{name} . "] Available Modes changed from $self->{previous}->{info}->{availablemodes} to $self->{data}->{info}->{availablemodes}" ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] This really isn't a regular operation. Should check Thermostat to confirm" ); - $self->{previous}->{info}->{availablemodes} = - $self->{data}->{info}->{availablemodes}; + $self->{previous}->{info}->{availablemodes} = $self->{data}->{info}->{availablemodes}; } if ( $self->{previous}->{info}->{fan} != $self->{data}->{info}->{fan} ) { @@ -1208,8 +1206,7 @@ sub process_data { . $self->{data}->{name} . "] Fan state changed from $fanstate[$self->{previous}->{info}->{fanstate}] to $fanstate[$self->{data}->{info}->{fanstate}]" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{fanstate} = - $self->{data}->{info}->{fanstate}; + $self->{previous}->{info}->{fanstate} = $self->{data}->{info}->{fanstate}; if ( defined $self->{child_object}->{fanstate} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Fan state Child object found. Updating..." if ( $self->{loglevel} ); @@ -1248,8 +1245,7 @@ sub process_data { $sch = " not " if ( !$self->{data}->{info}->{schedule} ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat state changed to" . $sch . "on a schedule" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{schedule} = - $self->{data}->{info}->{schedule}; + $self->{previous}->{info}->{schedule} = $self->{data}->{info}->{schedule}; if ( defined $self->{child_object}->{sched} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Schedule Child object found. Updating..." if ( $self->{loglevel} ); @@ -1262,8 +1258,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat schedule changed from $schedule[$self->{previous}->{info}->{schedulepart}] to $schedule[$self->{data}->{info}->{schedulepart}]" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{schedulepart} = - $self->{data}->{info}->{schedulepart}; + $self->{previous}->{info}->{schedulepart} = $self->{data}->{info}->{schedulepart}; } if ( $self->{type} eq "residential" @@ -1293,8 +1288,7 @@ sub process_data { $override = "on" if ( $self->{data}->{info}->{override} ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat override changed to" . $override ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{override} = - $self->{data}->{info}->{override}; + $self->{previous}->{info}->{override} = $self->{data}->{info}->{override}; } if ( $self->{type} eq "commercial" @@ -1302,8 +1296,7 @@ sub process_data { { main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat overridetime changed to" . $self->{data}->{info}->{overridetime} ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{overridetime} = - $self->{data}->{info}->{overridetime}; + $self->{previous}->{info}->{overridetime} = $self->{data}->{info}->{overridetime}; } if ( $self->{type} eq "commercial" @@ -1313,8 +1306,7 @@ sub process_data { $forceunocc = "on" if ( $self->{data}->{info}->{forceunocc} ); main::print_log( "[Venstar Colortouch:" . $self->{data}->{name} . "] Thermostat forceunocc changed to" . $forceunocc ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{forceunocc} = - $self->{data}->{info}->{forceunocc}; + $self->{previous}->{info}->{forceunocc} = $self->{data}->{info}->{forceunocc}; } if ( $self->{previous}->{info}->{spacetemp} != $self->{data}->{info}->{spacetemp} ) { @@ -1322,8 +1314,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat temperature changed from $self->{previous}->{info}->{spacetemp} to $self->{data}->{info}->{spacetemp}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{spacetemp} = - $self->{data}->{info}->{spacetemp}; + $self->{previous}->{info}->{spacetemp} = $self->{data}->{info}->{spacetemp}; if ( defined $self->{child_object}->{temp} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Temperature Sensor Child object found. Updating..." if ( $self->{loglevel} ); @@ -1336,8 +1327,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat heating setpoint changed from $self->{previous}->{info}->{heattemp} to $self->{data}->{info}->{heattemp}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{heattemp} = - $self->{data}->{info}->{heattemp}; + $self->{previous}->{info}->{heattemp} = $self->{data}->{info}->{heattemp}; if ( defined $self->{child_object}->{heat_sp} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Heat Setpoint Child object found. Updating..." if ( $self->{loglevel} ); @@ -1350,8 +1340,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat heating setpoint minimum changed from $self->{previous}->{info}->{heattempmin} to $self->{data}->{info}->{heattempmin}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{heattempmin} = - $self->{data}->{info}->{heattempmin}; + $self->{previous}->{info}->{heattempmin} = $self->{data}->{info}->{heattempmin}; } if ( $self->{previous}->{info}->{heattempmax} != $self->{data}->{info}->{heattempmax} ) { @@ -1359,8 +1348,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat heating setpoint maximum changed from $self->{previous}->{info}->{heattempmax} to $self->{data}->{info}->{heattempmax}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{heattempmax} = - $self->{data}->{info}->{heattempmax}; + $self->{previous}->{info}->{heattempmax} = $self->{data}->{info}->{heattempmax}; } if ( $self->{previous}->{info}->{cooltemp} != $self->{data}->{info}->{cooltemp} ) { @@ -1368,8 +1356,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat cooling setpoint changed from $self->{previous}->{info}->{cooltemp} to $self->{data}->{info}->{cooltemp}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{cooltemp} = - $self->{data}->{info}->{cooltemp}; + $self->{previous}->{info}->{cooltemp} = $self->{data}->{info}->{cooltemp}; if ( defined $self->{child_object}->{cool_sp} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Cooling Setpoint Child object found. Updating..." if ( $self->{loglevel} ); @@ -1382,8 +1369,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat cooling setpoint minimum changed from $self->{previous}->{info}->{cooltempmin} to $self->{data}->{info}->{cooltempmin}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{cooltempmin} = - $self->{data}->{info}->{cooltempmin}; + $self->{previous}->{info}->{cooltempmin} = $self->{data}->{info}->{cooltempmin}; } if ( $self->{previous}->{info}->{cooltempmax} != $self->{data}->{info}->{cooltempmax} ) { @@ -1391,8 +1377,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat cooling setpoint maximum changed from $self->{previous}->{info}->{cooltempmax} to $self->{data}->{info}->{cooltempmax}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{cooltempmax} = - $self->{data}->{info}->{cooltempmax}; + $self->{previous}->{info}->{cooltempmax} = $self->{data}->{info}->{cooltempmax}; } if ( $self->{previous}->{info}->{dehum_setpoint} != $self->{data}->{info}->{dehum_setpoint} ) { @@ -1401,8 +1386,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat humidity setpoint changed from $self->{previous}->{info}->{dehum_setpoint} to $self->{data}->{info}->{dehum_setpoint}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{dehum_setpoint} = - $self->{data}->{info}->{dehum_setpoint}; + $self->{previous}->{info}->{dehum_setpoint} = $self->{data}->{info}->{dehum_setpoint}; if ( defined $self->{child_object}->{hum_sp} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidify Child object found. Updating..." if ( $self->{loglevel} ); @@ -1414,8 +1398,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat dehumidity setpoint changed from $self->{previous}->{info}->{dehum_setpoint} to $self->{data}->{info}->{dehum_setpoint}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{dehum_setpoint} = - $self->{data}->{info}->{dehum_setpoint}; + $self->{previous}->{info}->{dehum_setpoint} = $self->{data}->{info}->{dehum_setpoint}; if ( defined $self->{child_object}->{dehum_sp} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Dehumidify Child object found. Updating..." if ( $self->{loglevel} ); @@ -1430,8 +1413,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat humidity setpoint changed from $self->{previous}->{info}->{hum_setpoint} to $self->{data}->{info}->{hum_setpoint}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{hum_setpoint} = - $self->{data}->{info}->{hum_setpoint}; + $self->{previous}->{info}->{hum_setpoint} = $self->{data}->{info}->{hum_setpoint}; if ( defined $self->{child_object}->{hum_sp} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidify Child object found. Updating..." if ( $self->{loglevel} ); @@ -1450,8 +1432,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat setpoint delta changed from $self->{previous}->{info}->{setpointdelta} to $self->{data}->{info}->{setpointdelta}" ) if ( $self->{loglevel} ); - $self->{previous}->{info}->{setpointdelta} = - $self->{data}->{info}->{setpointdelta}; + $self->{previous}->{info}->{setpointdelta} = $self->{data}->{info}->{setpointdelta}; } if ( $self->{previous}->{sensors}->{sensors}[0]->{hum} != $self->{data}->{sensors}->{sensors}[0]->{hum} ) { @@ -1459,8 +1440,7 @@ sub process_data { . $self->{data}->{name} . "] Thermostat Humidity Sensor changed from $self->{previous}->{sensors}->{sensors}[0]->{hum} to $self->{data}->{sensors}->{sensors}[0]->{hum}" ) if ( $self->{loglevel} ); - $self->{previous}->{sensors}->{sensors}[0]->{hum} = - $self->{data}->{sensors}->{sensors}[0]->{hum}; + $self->{previous}->{sensors}->{sensors}[0]->{hum} = $self->{data}->{sensors}->{sensors}[0]->{hum}; if ( defined $self->{child_object}->{hum} ) { main::print_log "[Venstar Colortouch:" . $self->{data}->{name} . "] Humidity Sensor Child object found. Updating..." if ( $self->{loglevel} ); @@ -1472,8 +1452,7 @@ sub process_data { sub print_runtimes { my ($self) = @_; - my ( $isSuccessResponse1, $data ) = - get_JSON_data( $self->{host}, 'runtimes' ); + my ( $isSuccessResponse1, $data ) = get_JSON_data( $self->{host}, 'runtimes' ); for my $tstamp ( 0 .. $#{ $data->{runtimes} } ) { From 311ce7373c05ee7c66f4428391fa7002f397409d Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Thu, 30 Mar 2017 14:35:13 -0600 Subject: [PATCH 155/209] IA7 v1.3.630 - get json_store.objects if opening up a non-root page --- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index bedabfbb0..1826494f9 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.3.620 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.3.630 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 961cc4dde..901826d61 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,4 @@ -// v1.3.610 +// v1.3.630 var entity_store = {}; //global storage of entities var json_store = {}; @@ -168,7 +168,7 @@ function changePage (){ notifications = "enabled"; } } - if (getJSONDataByPath("collections") === undefined){ + if (getJSONDataByPath("collections") === undefined || json_store.objects === undefined){ // We need at minimum the basic collections data to render all pages // (the breadcrumb) // NOTE may want to think about how to handle dynamic changes to the From 86b6888147bf8cd024eced589f0c7545db0c4cdc Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Thu, 30 Mar 2017 15:20:38 -0600 Subject: [PATCH 156/209] Add in time check for type=Type and type=Category and don't send the same data if present --- lib/json_server.pl | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/json_server.pl b/lib/json_server.pl index 33efe66ee..b8436e5da 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -528,8 +528,12 @@ sub json_get { 'type' => 'Category', 'members' => '' }; - if ( filter_object( $temp_object, \%args ) ) { - $json_data{objects}{$category} = $temp_object; + # if a time has been supplied, then the client data has been initialized, and we don't need to send it again + # if a category gets added, it won't refresh, but that's rare and this prevents the clients from continually sending data + unless ( $args{time} && $args{time}[0] > 0 ) { + if ( filter_object( $temp_object, \%args ) ) { + $json_data{objects}{$category} = $temp_object; + } } } @@ -541,8 +545,12 @@ sub json_get { 'type' => 'Type', 'members' => '' }; - if ( filter_object( $temp_object, \%args ) ) { - $json_data{objects}{$type} = $temp_object; + # if a time has been supplied, then the client data has been initialized, and we don't need to send it again + # if a category gets added, it won't refresh, but that's rare and this prevents the clients from continually sending data + unless ( $args{time} && $args{time}[0] > 0 ) { + if ( filter_object( $temp_object, \%args ) ) { + $json_data{objects}{$type} = $temp_object; + } } } } @@ -922,7 +930,7 @@ sub json_object_detail { #To avoid missed changes, since they can happen at the millisecond level, give a second's cushion #Object has not changed since time, so return undefined return; - } + } } my %json_objects; From 9f8f6dba62cd6ed33e10c0fdbc38f3fc93b5adf3 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Wed, 12 Apr 2017 18:53:54 -0600 Subject: [PATCH 157/209] IA7 v1.4.100 --- lib/json_server.pl | 9 +++- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 85 ++++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/lib/json_server.pl b/lib/json_server.pl index b8436e5da..cafd771df 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -241,7 +241,7 @@ sub json_get { if ( defined $json_data{'rrd_config'}->{'prefs'}->{'path'} ); my $rrd_file = "weather_data.rrd"; $rrd_file = $config_parms{weather_data_rrd} - if ( defined $config_parms{weather_data_rrd} ); + if ( ( defined $config_parms{weather_data_rrd} ) and ($config_parms{weather_data_rrd})); if ( $rrd_file =~ m/.*\/(.*\.rrd)/ ) { $rrd_file = $1; } @@ -365,7 +365,12 @@ sub json_get { $value1 =~ s/\.0*$// unless ( $value1 == 0 ); #remove unneccessary trailing decimals $value1 = "null" if ( lc $value1 eq "nan" ); - push @{ $dataset[$index]->{data} }, [ ( $db_start + ( $time_index * $step ) ) * 1000, $value1 ]; + if ($arg_time) { + push @{ $dataset[$index]->{data} }, [ ( $db_start + ( $time_index * $step ) ) * 1000, $value1 ] + if (($db_start + ( $time_index * $step ) ) * 1000 >= $arg_time ); #filter out all values less than time= if present + } else { + push @{ $dataset[$index]->{data} }, [ ( $db_start + ( $time_index * $step ) ) * 1000, $value1 ]; + } $index++; } $time_index++; diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 1826494f9..a00c26833 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.3.630 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.4.100 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 901826d61..31ea40791 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,4 @@ -// v1.3.630 +// v1.4.100 var entity_store = {}; //global storage of entities var json_store = {}; @@ -168,7 +168,7 @@ function changePage (){ notifications = "enabled"; } } - if (getJSONDataByPath("collections") === undefined || json_store.objects === undefined){ + if (getJSONDataByPath("collections") === undefined){ // We need at minimum the basic collections data to render all pages // (the breadcrumb) // NOTE may want to think about how to handle dynamic changes to the @@ -266,7 +266,7 @@ function changePage (){ nav_link = '#path=/objects&parents='+nav_name; if (collection_keys_arr.length > 2 && collection_keys_arr[collection_keys_arr.length-2].substring(0,1) == "$") nav_link = '#path=/objects&type='+nav_name; if (nav_name == "Group") nav_link = '#path=objects&type=Group'; //Hardcode this use case - if (json_store.objects[nav_name] !== undefined && json_store.objects[nav_name].label !== undefined) nav_name = (json_store.objects[nav_name].label); + if (json_store.objects !== undefined && json_store.objects[nav_name] !== undefined && json_store.objects[nav_name].label !== undefined) nav_name = (json_store.objects[nav_name].label); } else { if (json_store.collections[collection_keys_arr[i]] == undefined) continue; //last breadcrumb duplicated so we don't need it. @@ -1450,15 +1450,19 @@ var display_table = function(table,records,time) { var graph_rrd = function(start,group,time) { var URLHash = URLToHash(); + var new_data = 1; if (typeof time === 'undefined'){ $('#list_content').html("
    "); $('#top-graph').append("
    "); $('#top-graph').append("
    "); $('#top-graph').append("

    "); - + new_data = 0; time = 0; } - + var refresh = 20; //refresh data every 60 seconds by default + var refresh_loop; + if (json_store.ia7_config.prefs.rrd_graph_refresh !== undefined) refresh = json_store.ia7_config.prefs.rrd_graph_refresh; + URLHash.time = time; URLHash.long_poll = 'true'; if (updateSocket !== undefined && updateSocket.readyState != 4){ @@ -1490,17 +1494,38 @@ var graph_rrd = function(start,group,time) { $.each(json.data.periods, function(key, value) { if (start === value.split(",")[1]) { dropdown_current = value.split(",")[0]+" "; - } else { - dropdown_html_list += '
  • '; - } + dropdown_html_list += '
  • '; }); - dropdown_html += dropdown_current+'
    '; - - $('#rrd-periods').append(dropdown_html); + console.log("1 new_data="+new_data+" start="+start); + if (new_data == 0) { + $('#rrd-periods').append(dropdown_html); + //sort the legend + json.data.data.sort(function(a, b){ + if(a.label < b.label) return -1; + if(a.label > b.label) return 1; + return 0; + }) + console.log('in init data section'); + // put the selection list on the side. + for (var i = 0; i < json.data.data.length; i++){ + var legli = $('
  • ').appendTo('#rrd-legend'); + $('').appendTo(legli); + $('
  • ').appendTo('#rrd-legend'); - $('').appendTo(legli); - $('
  • diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 31ea40791..0db0924f1 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,5 @@ -// v1.4.100 +// v1.4.200 +//TODO : floorplan on IOS8, pretty error screen if no data var entity_store = {}; //global storage of entities var json_store = {}; @@ -13,6 +14,7 @@ var audio_init; var audioElement = document.getElementById('sound_element'); var authorized = "false"; var developer = false; +var show_tooltips = true; var ctx; //audio context var buf; //audio buffer @@ -145,6 +147,7 @@ function changePage (){ } if (json_store.ia7_config.prefs.substate_percentages === undefined) json_store.ia7_config.prefs.substate_percentages = 20; if (json_store.ia7_config.prefs.developer !== undefined) developer = json_store.ia7_config.prefs.developer; + if (json_store.ia7_config.prefs.tooltips !== undefined) show_tooltips = json_store.ia7_config.prefs.tooltips; // First time loading, set the default speech notifications if (speech_sound === undefined) { if ((json_store.ia7_config.prefs.speech_default !== undefined) && (json_store.ia7_config.prefs.speech_default.search("audio") >= 0 )) { @@ -191,7 +194,7 @@ function changePage (){ // Remove the RRD Last Updated $('#Last_updated').remove(); - + //Trim leading and trailing slashes from path var path = URLHash.path.replace(/^\/|\/$/g, ""); if (path.indexOf('objects') === 0){ @@ -248,6 +251,7 @@ function changePage (){ else { //default response is to load a collection loadCollection(URLHash._collection_key); } + //update the breadcrumb: // Weird end-case, The Group from browse items is broken with parents on the URL // Also have to change parents to type if the ending collection_keys are $, @@ -283,6 +287,7 @@ function changePage (){ } } } + } function loadPrefs (config_name){ //show ia7 prefs, args ia7_prefs, ia7_rrd_prefs if no arg then both @@ -670,6 +675,7 @@ var loadList = function() { $('#buffer'+row).append("
    "); } $('#row'+row).append("
    " + entity_arr[i] + "
    "); + if (column == 3){ column = 0; row++; @@ -682,6 +688,7 @@ var loadList = function() { var button_group = $(this).parents('.btn-group'); button_group.find('.leadcontainer > .dropdown-lead >u').html($(this).text()); e.preventDefault(); + generateTooltips(); }); $(".btn-voice-cmd").click( function () { var voice_cmd = $(this).text().replace(/ /g, "_"); @@ -720,8 +727,9 @@ var loadList = function() { $(".btn-state-cmd").mayTriggerLongClicks().on( 'longClick', function() { var entity = $(this).attr("entity"); create_state_modal(entity); - }); - + }); + + generateTooltips(); } }); // Continuously check for updates if this was a group type request @@ -729,6 +737,27 @@ var loadList = function() { };//loadlistfunction + //Add tooltips to any truncated buttons +var generateTooltips = function () { + if (show_tooltips) { + $(".btn").each(function( index ) { + if ($(this)[0].scrollWidth > 0) { + //if scrollWidth is greater than outerWidth then bootstrap has truncated the button text + if ($(this)[0].scrollWidth > $(this).outerWidth()) { + $(this).attr('data-toggle', 'tooltip'); + $(this).attr('data-placement', 'auto bottom'); + $(this).attr('data-original-title', $(this).text()); + $(this).attr('title', $(this).text()); + } else { + $(this).attr('data-original-title', ''); + $(this).attr('title', ''); + } + $('[data-toggle="tooltip"]').tooltip(); + } + }); + } +} + var getButtonColor = function (state) { var color = "default"; if (state !== undefined) state = state.toLowerCase(); @@ -998,7 +1027,9 @@ var updateStaticPage = function(link,time) { } }); } - } + } + generateTooltips(); + }); } if (jqXHR.status == 200 || jqXHR.status == 204) { @@ -1145,6 +1176,9 @@ var loadCollection = function(collection_keys) { } column++; } + + generateTooltips(); + // if any items present, then create modals and activate updateItem... if (items !== "") { items = items.slice(0,-1); //remove last comma @@ -1170,8 +1204,8 @@ var loadCollection = function(collection_keys) { else { create_state_modal(entity); } - } - ); + }); + $(".btn-state-cmd").mayTriggerLongClicks().on( 'longClick', function() { var entity = $(this).attr("entity"); create_state_modal(entity); @@ -1188,7 +1222,8 @@ var loadCollection = function(collection_keys) { show: true }); }); - }); + }); + // test multiple items at some point updateItem(items); } @@ -1216,7 +1251,7 @@ function fixIA7Nav() { var url = $(location).attr('href'); var collid = url.split("_collection_key="); $('a').each(function() { - if ($(this).attr('href').match("^/ia7/")) { + if (($(this).attr('href') !== undefined) && ($(this).attr('href').match("^/ia7/"))) { this.href += '&_collection_key='+collid[1]+','; } }); @@ -1630,8 +1665,10 @@ var graph_rrd = function(start,group,time) { //Call update again, if page is still here //KRK best way to handle this is likely to check the URL hash if ($('#top-graph').length !== 0){ -//TODO live updates + //If the graph page is still active request more data + //Just manually pull a refresh since json_server constantly polling RRD data is a performance problem + refresh_loop = setTimeout(function(){ graph_rrd(start,group,0); }, refresh * 1000); @@ -2853,6 +2890,7 @@ var trigger = function() { $(document).ready(function() { // Start + changePage(); //Watch for future changes in hash $(window).bind('hashchange', function() { From 9a4f1d5a71e043af6d00dc277b50ca342f0e8580 Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 1 May 2017 18:00:08 -0600 Subject: [PATCH 164/209] IA7 v1.4.350 - Fixes Floorplan on IOS devices --- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 74 ++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 4d99182e9..1bc9ae478 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.4.200 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the v4 web prototype, updates by H.Plato. IA7 v1.4.350 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 0db0924f1..f9da397c9 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,5 @@ -// v1.4.200 -//TODO : floorplan on IOS8, pretty error screen if no data +// v1.4.350 +//TODO : floorplan on IOS8 var entity_store = {}; //global storage of entities var json_store = {}; @@ -739,7 +739,7 @@ var loadList = function() { //Add tooltips to any truncated buttons var generateTooltips = function () { - if (show_tooltips) { + if ((show_tooltips) && (mobile_device() == "no") ){ //no sense in having tooltips on a touch device $(".btn").each(function( index ) { if ($(this)[0].scrollWidth > 0) { //if scrollWidth is greater than outerWidth then bootstrap has truncated the button text @@ -1047,7 +1047,7 @@ var updateStaticPage = function(link,time) { function authDetails() { if (json_store.collections[700] == undefined) { - alert("Warning, Collection ID 700: Authorize, is not defined in your collections.json!"); + if ($(".alert-err").length == 0 ) something_went_wrong("Collections","Collection ID 700: Authorize, is not defined in your collections.json!"); } else { if (json_store.collections[700].user !== undefined) { if (json_store.collections[700].user == "0") { @@ -1076,6 +1076,10 @@ var loadCollection = function(collection_keys) { var entity_arr = []; var items = ""; var entity_sort = json_store.collections[last_collection_key].children; + if (entity_sort == undefined) { + something_went_wrong("Collections","Problem with collections.json. Check the print log"); + return; + } if (entity_sort.length <= 0){ entity_arr.push("Childless Collection"); } @@ -1304,6 +1308,24 @@ var print_log = function(type,time) { }); }; +var something_went_wrong = function(module,text) { + + var type = "danger"; + var mobile = ""; + if ($(window).width() <= 768) { // override the responsive mobile top-buffer + mobile = "mobile-alert"; + } + var html = "
    "; + html += ""; + html += "
    "; + html += ""; + html += "
    "; + html += "

    ERROR

    " + module + " : " + text + "
    "; + + $("#alert-area").prepend($(html)); + +} + var get_notifications = function(time) { if (time === undefined) time = new Date().getTime(); @@ -1395,6 +1417,7 @@ function playWhenReady() } else if(audioElement.error) { var errorText=['(no error)','User interrupted download','Network error caused interruption','Miscellaneous problem with media data','Cannot actually decode this media']; console.log("Something went wrong!\n"+errorText[audioElement.error.code]); + something_went_wrong("audio","audioElement.error"); } else { //check for media ready again in half a second setTimeout(playWhenReady,500); } @@ -1486,6 +1509,7 @@ var graph_rrd = function(start,group,time) { var URLHash = URLToHash(); var new_data = 1; + var data_timeout = 0; if (typeof time === 'undefined'){ $('#list_content').html("
    "); $('#top-graph').append("
    "); @@ -1520,7 +1544,12 @@ var graph_rrd = function(start,group,time) { // HP jquery would allow selected values to be replaced in the future. if (json.data.data !== undefined) { //If no data, at least show the header and an error -//TODO + data_timeout++; + //sometimes the first call for data doesn't return anything. Try a few times. + if (data_timeout > 3) { + something_went_wrong('RRD','No Data Returned by MH Json Server'); + return; + } } var dropdown_html = ''; - console.log("1 new_data="+new_data+" start="+start); + //console.log("1 new_data="+new_data+" start="+start); if (new_data == 0) { $('#rrd-periods').append(dropdown_html); //sort the legend @@ -1548,7 +1577,6 @@ var graph_rrd = function(start,group,time) { if(a.label > b.label) return 1; return 0; }) - console.log('in init data section'); // put the selection list on the side. for (var i = 0; i < json.data.data.length; i++){ var legli = $('
  • ').appendTo('#rrd-legend'); @@ -1682,6 +1710,7 @@ var object_history = function(items,start,days,time) { var URLHash = URLToHash(); var graph = 0; + var data_timeout = 0; if (developer) graph = 1; //right now only show the graph if in developer mode if (typeof time === 'undefined'){ if (graph) { @@ -1719,7 +1748,12 @@ var object_history = function(items,start,days,time) { // HP jquery would allow selected values to be replaced in the future. if (json.data.data !== undefined) { //If no data, at least show the header and an error -//TODO + data_timeout++; + //sometimes the first call for data doesn't return anything. Try a few times. + if (data_timeout > 3) { + something_went_wrong('Object_History','No Data Returned by MH Json Server'); + return; + } } if (start == undefined) { start = new Date().getTime(); @@ -1946,7 +1980,7 @@ var fp_resize_floorplan_image = function(){ }; var fp_reposition_entities = function(){ - var t0 = performance.now(); + //var t0 = performance.now(); var fp_graphic_offset = $("#fp_graphic").offset(); // console.log("fp_graphic_offset: "+ JSON.stringify(fp_graphic_offset)); var width = fp_display_width; @@ -2009,7 +2043,7 @@ var fp_reposition_entities = function(){ $('.icon_select img').each(function(){ $(this).width(fp_scale_percent + "%"); }); - var t1 = performance.now(); + //var t1 = performance.now(); //console.log("FP: reposition and scale: " +Math.round(t1 - t0) + "ms "); }; @@ -2080,7 +2114,6 @@ var floorplan = function(group,time) { $('#fp_graphic').attr("src", base_img_dir+'/floorplan-'+group+'.png'); return; } - if (updateSocket !== undefined && updateSocket.readyState !== 4){ // Only allow one update thread to run at once updateSocket.abort(); @@ -2098,7 +2131,6 @@ var floorplan = function(group,time) { //var pos = Math.round((t/hight) *100) +"," + Math.round((l/width)*100); var pos = (t/hight) *100 +"," + (l/width)*100; - //console.log("floorplanpos: " + pos ); $('#debug_pos').text(pos); if (fp_grabbed_entity !== null){ //var itemCenterOffset = Math.round(fp_grabbed_entity.width/2); @@ -2108,8 +2140,6 @@ var floorplan = function(group,time) { "left": e.pageX - itemCenterOffset }; fp_set_pos(fp_grabbed_entity.id, newPos); - //console.log(fp_grabbed_entity.id +" pos: " +newPos.top + " x " + newPos.left); - //fp_grabbed_entity.class.replace("coords=.*", "coords="+pos); } }); @@ -2195,6 +2225,7 @@ var floorplan = function(group,time) { $("#fp_pos_perl_code").text(newCode); }; + // reposition on window size change window.onresize = function(){ if ($('#floorplan').length === 0) @@ -2203,16 +2234,13 @@ var floorplan = function(group,time) { return; } -// console.log("FP: window resized"); fp_resize_floorplan_image(); fp_reposition_entities(); }; var path_str = "/objects"; var fields = "fields=fp_location,state,states,fp_icons,schedule,logger_status,fp_icon_set,img,link,label,type"; - if (json_store.ia7_config.prefs.state_log_show === "yes") - fields += ",state_log"; - + if (json_store.ia7_config.prefs.state_log_show === "yes") fields += ",state_log"; var arg_str = "parents="+group+"&"+fields+"&long_poll=true&time="+time; updateSocket = $.ajax({ @@ -2220,13 +2248,12 @@ var floorplan = function(group,time) { url: "/LONG_POLL?json('GET','"+path_str+"','"+arg_str+"')", dataType: "json", error: function(xhr, textStatus, errorThrown){ - // console.log('FP: request failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"'); + console.log('FP: request failed: "' + textStatus + '" "'+JSON.stringify(errorThrown, undefined,2)+'"'); }, success: function( json, statusText, jqXHR ) { - // console.log('FP: request succeeded: "' + statusText + '" "'+JSON.stringify(jqXHR, undefined,2)+'"'); var requestTime = time; if (jqXHR.status === 200) { - var t0 = performance.now(); + //var t0 = performance.now(); JSONStore(json); for (var entity in json.data) { if (developer && requestTime === 0){ @@ -2441,7 +2468,7 @@ var floorplan = function(group,time) { }); } requestTime = json.meta.time; - var t1 = performance.now(); + //var t1 = performance.now(); } if (jqXHR.status === 200 || jqXHR.status === 204) { //Call update again, if page is still here @@ -2456,7 +2483,7 @@ var floorplan = function(group,time) { if (time === 0){ // hack to fix initial positions of the items var wait = 500; - console.log("FP: calling fp in " +wait+ "ms"); + //console.log("FP: calling fp in " +wait+ "ms"); setTimeout(function(){ //console.log("FP: calling fp after " +wait+ "ms"); fp_reposition_entities(); @@ -2465,6 +2492,7 @@ var floorplan = function(group,time) { } }); }; + var get_fp_image = function(item,size,orientation) { var image_name; var image_color = getButtonColor(item.state); From e9baf408024238fbcf0143ce35df987eacd4ae1e Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 6 May 2017 17:43:40 -0600 Subject: [PATCH 165/209] v1.0.9 - API v1 support, config_parms for settings --- lib/Nanoleaf_Aurora.pm | 570 +++++++++++++++++++++-------------------- 1 file changed, 294 insertions(+), 276 deletions(-) diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index 1f811abd3..4f8f8c237 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -1,6 +1,6 @@ package Nanoleaf_Aurora; -# v1.0.5 +# v1.0.9 #if any effect is changed, by definition the static child should be set to off. #cmd data returns, need to check by command @@ -8,7 +8,8 @@ package Nanoleaf_Aurora; #effects, turn static off and clear out static_check -#line 438 $mode|$cmd queue +#TODO +#- confirm v1 API use strict; use warnings; @@ -20,21 +21,49 @@ use Data::Dumper; use IO::Select; use IO::Socket::INET; +# REQUIRES AURORA FIRMWARE v1.4.39+ For v1.4.39 pass the option api=beta. +# +# To set up, first pair with mobile app -- the Aurora needs to be set up initially with the app +# to get it's wifi information. After it is successfully set up, it should be locatable via SSDP +# If multiple new Aurora's are plugged in at the same time, the SSDP query may not return unique +# addresses. Just add one at a time. +# +# To generate a token and allow local control, press and hold the power button for a few seconds +# until the controller light blinks. +# In a few seconds the MH print log should show that a token has been found. +# the location URL and tokens are stored in the mh.ini file + # Nanoleaf_Aurora Objects -# $aurora = new NanoLeaf_Aurora(,,,,); +# +# $aurora = new NanoLeaf_Aurora(,,,,); # $aurora_effects = new NanoLeaf_Aurora_Effects($aurora); # $aurora_static1 = new NanoLeaf_Aurora_Static($aurora, "effect string"); # $aurora_comm = new Nanoleaf_Aurora_Comm($aurora); -# Version History -# v1.0 - initial -# v1.0.1 - initial static support -# v1.0.2 - multiple auroras, brightness -# v1.0.3 - working static. turn on, overrides effect, turning off will restore previous effect -# v1.0.4 - Voice Commands -# v1.0.5 - working multi static +# MH.INI settings +# If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used +# instead of object defintions +# +# aurora__url = +# aurora__token = +# aurora__poll = +# aurora__options = +# +# for example +#aurora_1_url = http://10.10.0.20:16021 +#aurora_1_token = EfgrIHH887EHhftotNNSD818erhNWHR0 +#aurora_1_options = 'api=beta' + +# OPTIONS +# current options that can be passed are; +# - api= +# - debug= +# - loglevel= # Notes +# +# The best way to set up static strings is to configure them in the mobile app, and then use the voice command +# get static string to print out the static configuration to the print log. # Issues @@ -56,50 +85,67 @@ $rest{set_static} = "effects"; $rest{brightness} = "state/brightness"; $rest{brightness2} = "state/brightness"; $rest{get_static} = "effects"; +$rest{identify} = "identify"; our %opts; $opts{info} = "-ua"; $opts{auth} = "-json -post '{}'"; -$opts{on} = "-json -put '{\"on\":true}'"; -$opts{off} = "-json -put '{\"on\":false}'"; -$opts{set_effect} = "-json -put '{\"select\":"; -$opts{set_static} = "-json -put '{\"write\":{\"command\":\"display\",\"version\":\"1.0\",\"animType\":\"static\",\"animData\":"; -$opts{brightness} = "-json -put '{\"value\":"; -$opts{brightness2} = "-json -put '{\"increment\":"; -$opts{get_static} = "-json -put '{\"write\":{\"command\":\"request\",\"version\":\"1.0\",\"animName\":\"*Static*\"}}'"; - -my $api_path = "/api/beta"; +$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{get_static} = "-response_code -json -put '{\"write\":{\"command\":\"request\",\"version\":\"1.0\",\"animName\":\"*Static*\"}}'"; +$opts{identify} = "-response_code -json -put '{}'"; + +my $api_path = "/api/v1"; +our %active_auroras = (); sub new { - my ( $class, $id, $nl_deviceid, $location, $poll, $options ) = @_; + my ( $class, $id, $url, $token, $poll, $options ) = @_; my $self = {}; bless $self, $class; - $self->{data} = undef; - $self->{child_object} = undef; - $options = "" unless ( defined $options ); + $self->{name} = "1"; + $self->{name} = $id if ($id); + + $self->{data} = undef; + $self->{child_object} = undef; $self->{config}->{poll_seconds} = 10; + $self->{config}->{poll_seconds} = $::config_parms{ "aurora_" . $self->{name} . "_poll" } + if ( defined $::config_parms{ "aurora_" . $self->{name} . "_poll" } ); $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{name} = "1"; - $self->{module_version} = "v1.0.2"; + $self->{module_version} = "v1.0.9"; $self->{ssdp_timeout} = 4000; - $self->{name} = $id if ($id); - $self->{last_static} = ""; - $self->{nl_deviceid} = ""; - $self->{nl_deviceid} = $nl_deviceid if ( defined $nl_deviceid ); - $self->{location} = $location; - $self->{debug} = 5; + $self->{last_static} = ""; + + $self->{url} = $url; + $self->{url} = $::config_parms{ "aurora_" . $self->{name} . "_url" } if ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ); + $self->{token} = $token; + $self->{token} = $::config_parms{ "aurora_" . $self->{name} . "_token" } if ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ); + $options = "" unless ( defined $options ); + $options = $::config_parms{ "aurora_" . $self->{name} . "_options" } if ( $::config_parms{ "aurora_" . $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->{debug} = 0 if ( $self->{debug} < 0 ); + ( $self->{debug} ) = ( $options =~ /debug\=(\d+)/i ); + $self->{loglevel} = 5; + ( $self->{loglevel} ) = ( $options =~ /loglevel\=(\d+)/i ); + + $self->{api_path} = $api_path; + if ( $options =~ m/api\=/i ) { + my ($api_ver) = ( $options =~ /api\=(\w+)/i ); + $self->{api_path} = "/api/" . $api_ver; + } $self->{poll_data_timestamp} = 0; $self->{max_poll_queue} = 3; $self->{max_cmd_queue} = 5; $self->{cmd_process_retry_limit} = 6; - $self->{config_file} = "$::config_parms{data_dir}/Aurora_config.json"; @{ $self->{poll_queue} } = (); $self->{poll_data_file} = "$::config_parms{data_dir}/Aurora_poll_" . $self->{name} . ".data"; @@ -116,7 +162,6 @@ sub new { $self->get_data(); $self->{init} = 0; $self->{init_data} = 0; - $self->{file} = 0; push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); $self->{timer} = new Timer; $self->start_timer; @@ -140,41 +185,41 @@ sub get_data { my ($self) = @_; main::print_log( "[Aurora:" . $self->{name} . "] get_data initiated" ) if ( $self->{debug} ); - print "db: self->{token} [$self->{token}] self->{location} [$self->{location}]\n"; #Check that we have data - if ( ( !$self->{file} ) and ( -f $self->{config_file} ) and ( !defined $self->{location} ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] get file data" ) if ( $self->{debug} ); - ( $self->{location}, $self->{nl_deviceid}, $self->{token} ) = $self->get_config_data( $self->{nl_deviceid} ); - } - if ( !defined $self->{location} ) { - main::print_log( "[Aurora:" . $self->{name} . "] get ssdp data" ) if ( $self->{debug} ); - ( $self->{location}, $self->{nl_deviceid} ) = $self->get_ssdp_data( $self->{nl_deviceid} ); - - #TODO $self->update_config_data() if ($self->{location}); + if ( !defined $self->{url} ) { + main::print_log( "[Aurora:" . $self->{name} . "] get ssdp data" ) if ( $self->{debug} ); + $self->{url} = $self->get_ssdp_data(); + $self->update_config_data() if ( $self->{url} ); } #Check for a token - if ( ( !defined $self->{token} ) and ( defined $self->{location} ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] Location found ($self->{location})" ); + if ( ( !defined $self->{token} ) and ( defined $self->{url} ) ) { + main::print_log( "[Aurora:" . $self->{name} . "] Activating Location URL $self->{url}" ); main::print_log( "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token" ); - eval { ia7_notify( "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token" ); }; + eval { + my %data; + $data{text} = "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token"; + $data{color} = "yellow"; + &json_notification( "banner", {%data} ); + }; $self->get_auth(); #if we have a location and token, then get some data } - elsif ( $self->{token} and $self->{location} ) { + elsif ( $self->{token} and $self->{url} ) { $self->poll(); if ( ( defined $self->{data}->{panels} ) and ( $self->{init} == 0 ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] " . $self->{module_version} . "Configuration Loaded" ); + main::print_log( "[Aurora:" . $self->{name} . "] " . $self->{module_version} . " Configuration Loaded" ); + $active_auroras{ $self->{url} } = 1; $self->print_info(); $self->{init} = 1; } } else { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING, Did not poll: location: $self->{location}, token: $self->{token}" ) if ( $self->{debug} ); + main::print_log( "[Aurora:" . $self->{name} . "] WARNING, Did not poll: location: $self->{url}, token: $self->{token}" ) if ( $self->{debug} ); } } @@ -212,6 +257,10 @@ sub process_check { main::print_log( "[Aurora:" . $self->{name} . "] 403 Forbidden: Pair Button not pressed or auth token is incorrect" ); return; } + my ($responsecode) = $file_data =~ /^RESPONSECODE:(\d+)\n/; + $file_data =~ s/^RESPONSECODE:\d+\n// if ($responsecode); + + #print "debug: code=$responsecode\n" if ( $self->{debug} ); #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} ); @@ -219,7 +268,7 @@ sub process_check { print "debug: json_data=$json_data\n" if ( $self->{debug} ); unless ( ($file_data) and ($json_data) ) { main::print_log( "[Aurora:" . $self->{name} . "] ERROR! bad data returned by poll" ); - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! file data is $file_data. json data is $json_data" ); + main::print_log( "[Aurora:" . $self->{name} . "] ERROR! file data is [$file_data]. json data is [$json_data]" ); return; } my $data; @@ -239,23 +288,37 @@ sub process_check { # ($self->{data}->{panels}, $self->{data}->{panel_size}) = substr($data->{panelLayout}->{layout}->{layoutData},0,1); my $layout; - ( $self->{data}->{panels}, $self->{data}->{panel_size}, $layout ) = $data->{panelLayout}->{layout}->{layoutData} =~ /^(\d+)\s+(\d+)\s+(.*)/; - my @panels = split / /, $layout; - for ( my $i = 0; $i < $self->{data}->{panels}; $i++ ) { - $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{x} = $panels[ ( $i * 4 ) + 1 ]; - $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{y} = $panels[ ( $i * 4 ) + 2 ]; - $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{o} = $panels[ ( $i * 4 ) + 3 ]; + if ( defined $data->{panelLayout}->{layout}->{layoutData} ) { #v1.4.39 + ( $self->{data}->{panels}, $self->{data}->{panel_size}, $layout ) = + $data->{panelLayout}->{layout}->{layoutData} =~ /^(\d+)\s+(\d+)\s+(.*)/; + my @panels = split / /, $layout; + for ( my $i = 0; $i < $self->{data}->{panels}; $i++ ) { + $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{x} = $panels[ ( $i * 4 ) + 1 ]; + $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{y} = $panels[ ( $i * 4 ) + 2 ]; + $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{o} = $panels[ ( $i * 4 ) + 3 ]; + } + } + else { + $self->{data}->{panels} = $data->{panelLayout}->{layout}->{numPanels}; + $self->{data}->{panel_size} = $data->{panelLayout}->{layout}->{sideLength}; + 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}; + $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{y} = + @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{y}; + $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{o} = + @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{o}; + } } + $self->process_data(); } elsif ( $self->{poll_process_mode} eq "auth" ) { main::print_log( "[Aurora:" . $self->{name} . "] Authentication token found!" ); $self->{token} = $data->{auth_token}; main::print_log( "[Aurora:" . $self->{name} . "] Token $self->{token}" ) if ( $self->{debug} ); - - #TODO #$self->update_config_data(); + $self->update_config_data(); } - $self->process_data(); } else { main::print_log( "[Aurora:" . $self->{name} . "] ERROR! Returned data not structured! Not processing..." ); @@ -290,105 +353,101 @@ sub process_check { return unless ( defined $self->{cmd_process} ); if ( $self->{cmd_process}->done_now() ) { - my $com_status = "online"; main::print_log( "[Aurora:" . $self->{name} . "] Background Command " . $self->{cmd_process_mode} . " process completed" ) if ( $self->{debug} ); $self->get_data(); #poll since the command is done to get a new state my $file_data = &main::file_read( $self->{cmd_data_file} ); - unless ($file_data) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! no data returned by command" ); - return; - } - #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 =~ /({.*})/; - print "debug: json_data=$json_data\n" if ( $self->{debug} ); - my $data; - eval { $data = JSON::XS->new->decode($json_data); }; + my ($responsecode) = $file_data =~ /^RESPONSECODE:(\d+)\n/; + $file_data =~ s/^RESPONSECODE:\d+\n// if ($responsecode); + print "debug: code=$responsecode\n" if ( $self->{debug} ); - # catch crashes: - if ($@) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); - $com_status = "offline"; + #print "success\n\n" if (($responsecode == 204) or ($responsecode == 200)); + my $com_status = "offline"; - } - else { - #TODO - what are the possible command return strings - print "returned data is---------====================\n"; - print Dumper $data; + if ( ( $responsecode == 204 ) or ( $responsecode == 200 ) ) { + $com_status = "online"; - if ( keys $data ) { + # Successful commands should be [200 OK, 204 No Content] + shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful + $self->{cmd_process_retry} = 0; - if ( $self->{cmd_process_mode} eq "get_static" ) { - main::print_log( "[Aurora:" . $self->{name} . "] get_static returned" ); - $self->{last_static} = $data->{animData} if ( defined $data->{animData} ); #just checked controller so update the static string - print "string=$self->{static_check}->{string}\n"; - if ( ( defined $self->{static_check}->{string} ) and ( $self->{static_check}->{string} eq $data->{animData} ) ) { - $self->set_effect( $self->{static_check}->{effect} ); - $self->{static_check}->{string} = undef; - $self->{static_check}->{effect} = undef; - } - if ( ( defined $self->{static_check}->{print} ) and ( $self->{static_check}->{print} ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] Static details. Current configuration string is" ); - main::print_log( "[Aurora:" . $self->{name} . "] $data->{animData}" ); - $self->{static_check}->{print} = 0; + 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} ); + my ($json_data) = $file_data =~ /({.*})/; + print "debug: json_data=$json_data\n" if ( $self->{debug} ); + my $data; + eval { $data = JSON::XS->new->decode($json_data); }; + + # catch crashes: + if ($@) { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; + } + else { + + #print Dumper $data; + + if ( keys $data ) { + + #Process any returned data from a command straing + if ( $self->{cmd_process_mode} eq "get_static" ) { + main::print_log( "[Aurora:" . $self->{name} . "] get_static returned" ); + $self->{last_static} = $data->{animData} if ( defined $data->{animData} ); #just checked controller so update the static string + if ( ( defined $self->{static_check}->{string} ) and ( $self->{static_check}->{string} eq $data->{animData} ) ) { + $self->set_effect( $self->{static_check}->{effect} ); + $self->{static_check}->{string} = undef; + $self->{static_check}->{effect} = undef; + } + if ( ( defined $self->{static_check}->{print} ) and ( $self->{static_check}->{print} ) ) { + main::print_log( "[Aurora:" . $self->{name} . "] Static details. Current configuration string is" ); + main::print_log( "[Aurora:" . $self->{name} . "] $data->{animData}" ); + $self->{static_check}->{print} = 0; + } + + } } + $self->poll; } + } - # Successful commands should be [200 OK, 204 No Content] + if ( scalar @{ $self->{cmd_queue} } ) { + my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + main::print_log( "[Aurora:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) + if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); + } + + } + else { + + main::print_log( "[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful, retrying..." ); + if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); shift @{ $self->{cmd_queue} }; + $self->{cmd_process_retry} = 0; + $com_status = "offline"; + } + else { + $self->{cmd_process_retry}++; + } + } - # if ( $data eq "true" ) { - # shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful - # $self->{cmd_process_retry} = 0; - # $self->poll; - # } - # else { - # if ( defined $data->{reason} ) { - # main::print_log("[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful (reason=" . $data->{reason} . ")" ); - # shift @{ $self->{cmd_queue} }; - # } - # else { - # main::print_log( "[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful with no returned reason , retrying..." ); - # if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { - # main::print_log("[Aurora:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); - # shift @{ $self->{cmd_queue} }; - # $self->{cmd_process_retry} = 0; - # $com_status = "offline"; - # } - # else { - # $self->{cmd_process_retry}++; - # } - # } - # } - # } - # else { - # print_log( "[Aurora:" . $self->{name} . "] ERROR! Returned data not structured! Not processing..." ); - # $com_status = "offline"; - # } - # } - # if ( scalar @{ $self->{cmd_queue} } ) { - # my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. - # $self->{cmd_process}->set($cmd); - # $self->{cmd_process}->start(); - # main::print_log( "[Aurora:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) - # if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); - # } - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne $com_status ) { - main::print_log "[Aurora:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() . " to " - . $com_status . "..." - if ( $self->{loglevel} ); - $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); - } - } + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Aurora:" + . $self->{name} + . "] Communication Tracking object found. Updating from " + . $self->{child_object}->{comm}->state() . " to " + . $com_status . "..." + if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); } } } @@ -399,8 +458,8 @@ sub _get_JSON_data { my ( $self, $mode, $params ) = @_; $params = $opts{$mode} unless defined($params); my $token = ""; - $token = "/" . $self->{token} if ( defined $self->{location} and lc $mode ne "auth" ); - my $cmd = "get_url $params " . '"' . $self->{location} . $api_path . "$token/$rest{$mode}" . '"'; + $token = "/" . $self->{token} if ( defined $self->{url} and lc $mode ne "auth" ); + my $cmd = "get_url $params " . '"' . $self->{url} . $self->{api_path} . "$token/$rest{$mode}" . '"'; if ( $self->{poll_process}->done() ) { $self->{poll_process}->set($cmd); $self->{poll_process}->start(); @@ -441,7 +500,7 @@ sub _push_JSON_data { return; } - my $cmd = "get_url $params" . ' "' . $self->{location} . $api_path . "/" . $self->{token} . "/$rest{$mode}" . '"'; + my $cmd = "get_url $params" . ' "' . $self->{url} . $self->{api_path} . "/" . $self->{token} . "/$rest{$mode}" . '"'; if ( $self->{cmd_process}->done() ) { $self->{cmd_process}->set($cmd); $self->{cmd_process}->start(); @@ -476,6 +535,17 @@ sub _push_JSON_data { sub get_ssdp_data { my ( $self, $id, $timeout ) = @_; + #return a location that isn't in the $active_auroras hash + + my ( $devices, $locations, $serials ) = scan_ssdp_data($timeout); + &main::print_log( "[Aurora:" . $self->{name} . "] in get_ssdp_data devices=$devices" ) if ( $self->{debug} ); + &main::print_log( "[Aurora:" . $self->{name} . "] locations: " . join( ", ", @{$locations} ) ) if ( $self->{debug} ); + &main::print_log( "[Aurora:" . $self->{name} . "] nl_serials: " . join( ", ", @{$serials} ) ) if ( $self->{debug} ); + foreach my $dev ( @{$locations} ) { + &main::print_log( "[Aurora:" . $self->{name} . "] dev=$dev active_auroras{$dev}=$active_auroras{$dev}" ) if ( $self->{debug} ); + return $dev unless ( $active_auroras{$dev} ); + } + return; } sub scan_ssdp_data { @@ -508,7 +578,7 @@ EOT my $dest = sockaddr_in( $port, inet_aton($addr) ); send( $sock, $q, 0, $dest ); - my @dev_locs = (); + my @dev_urls = (); my @dev_ids = (); my $data; my $i; @@ -527,25 +597,25 @@ EOT if ( $data =~ m/ST: nanoleaf_aurora:light/ ) { #print "found" . $data; - my ($location) = $data =~ /Location:\s+(.*)/; + my ($url) = $data =~ /Location:\s+(.*)/; - #print "location " . $location; - $location =~ s/[^a-zA-Z0-9\:\.\/]*//g; - push @dev_locs, $location; + #print "location " . $url; + $url =~ s/[^a-zA-Z0-9\:\.\/]*//g; + push @dev_urls, $url; my ($devid) = $data =~ /nl\-deviceid:\s+(.*)/; - #print "location " . $location; + #print "location " . $url; $devid =~ s/[^a-zA-Z0-9\:\.\/]*//g; push @dev_ids, $devid; - &main::print_log( "[Aurora:" . $self->{name} . "] Found Aurora: $location, nl-deviceid: $devid" ); + &main::print_log( "[Aurora:" . $self->{name} . "] Found Aurora: $url, nl-deviceid: $devid" ); #last; } } - $discovery .= "< [" . scalar @dev_locs . "]\n"; + $discovery .= "< [" . scalar @dev_urls . "]\n"; &main::print_log( "[Aurora:" . $self->{name} . "] $discovery" ); - return ( scalar @dev_locs, \@dev_locs, \@dev_ids ); + return ( scalar @dev_urls, \@dev_urls, \@dev_ids ); } sub _mcast_add { @@ -576,113 +646,27 @@ sub _constant { return $ref->[$index]; } -sub read_config_data { - my ($self) = @_; - - my $file_data = &main::file_read( $self->{config_file} ); - unless ($file_data) { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING! Cannot read config file $self->{config_file}" ); - return; - } - - my $data; - eval { $data = JSON::XS->new->decode($file_data); }; - - # catch crashes: - if ($@) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed when reading config file! $@\n" ); - } - else { - print Dumper $data if ( $self->{debug} ); - - return $data; - } -} - -sub write_config_data { - my ( $self, $data ) = @_; - - my $file_data; - eval { $file_data = JSON::XS->new->encode($data); }; - - # catch crashes: - if ($@) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed when creating config file! $@\n" ); - } - else { - print Dumper $file_data if ( $self->{debug} ); - &main::file_write( $self->{config_file}, $file_data ); - } - -} - -sub get_config_data { - my ( $self, $id ) = @_; - - my $data = $self->read_config_data(); - - if ($data) { - my $nid = $self->{name}; - - #override $nid if a $nl_device is specified - if ( ( defined $id ) and ($id) ) { - print "checking id=$id\n"; - foreach my $key ( keys %$data ) { - if ( ( lc $self->{nl_deviceid} eq lc $data->{$key}->{device}->{nl_deviceid} ) - or ( lc $self->{location} eq lc $data->{$key}->{device}->{location} ) - or ( lc $self->{token} eq lc $data->{$key}->{device}->{token} ) ) - { - $nid = $key; - last; - } - } - } - $self->{file} = 1; - print "nid=$nid, loc=$data->{$nid}->{device}->{location}\n"; - return ( $data->{$nid}->{device}->{location}, $data->{$nid}->{device}->{nl_deviceid}, $data->{$nid}->{device}->{token} ); - } - else { - return; - } -} - sub update_config_data { my ($self) = @_; - my $nid = $self->{name}; - my $data; - $data = $self->read_config_data(); - - #override $nid if a $nl_device is specified - if ( defined $data ) { - foreach my $key ( keys %$data ) { - if ( ( lc $self->{nl_deviceid} eq lc $data->{$key}->{device}->{nl_deviceid} ) - or ( lc $self->{location} eq lc $data->{$key}->{device}->{location} ) - or ( lc $self->{token} eq lc $data->{$key}->{device}->{token} ) ) - { - $nid = $key; - last; - } - } - } - - #TODO add static definitions ? - - if ( ( defined $self->{location} ) and ( $data->{$nid}->{device}->{location} ne $self->{location} ) ) { - $data->{$nid}->{device}->{location} = $self->{location}; - main::print_log( "[Aurora:" . $self->{name} . "] Updating location ($self->{location}) in config file" ); + my $write = 0; + my %parms; - } - if ( ( defined $self->{token} ) and ( $data->{$nid}->{device}->{token} ne $self->{token} ) ) { - $data->{$nid}->{device}->{token} = $self->{token}; - main::print_log( "[Aurora:" . $self->{name} . "] Updating authentication token in config file" ); + if ( ( defined $self->{url} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ne $self->{url} ) ) { + $::config_parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; + $parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; + main::print_log( "[Aurora:" . $self->{name} . "] Updating location URL ($self->{url}) in mh.ini file" ); + $write = 1; } - if ( ( defined $self->{nl_device} ) and ( $data->{$nid}->{device}->{nl_device} ne $self->{nl_device} ) and ( $self->{nl_device} ) ) { - $data->{$nid}->{device}->{nl_device} = $self->{nl_device}; - main::print_log( "[Aurora:" . $self->{name} . "] Updating nl-device ($self->{nl_device} ) in config file" ); + if ( ( defined $self->{token} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ne $self->{token} ) ) { + $::config_parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; + $parms{ "aurora_" . $self->{name} . "_url" } = $self->{token}; + main::print_log( "[Aurora:" . $self->{name} . "] Updating authentication token in mh.ini file" ); + $write = 1; } - $self->write_config_data($data); + + &::write_mh_opts( \%parms ) if ($write); } sub register { @@ -721,13 +705,15 @@ sub stop_timer { sub print_info { my ($self) = @_; - main::print_log( "[Aurora:" . $self->{name} . "] Name: " . $self->{data}->{info}->{name} ); - main::print_log( "[Aurora:" . $self->{name} . "] Serial Number: " . $self->{data}->{info}->{serialNo} ); - 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} ); - 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} . "] Name: " . $self->{data}->{info}->{name} ); + main::print_log( "[Aurora:" . $self->{name} . "] Serial Number: " . $self->{data}->{info}->{serialNo} ); + 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} ); + 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} ); + main::print_log( "[Aurora:" . $self->{name} . "] MH Module version: " . $self->{module_version} ); main::print_log( "[Aurora:" . $self->{name} . "] -- Current Settings --" ); @@ -768,8 +754,16 @@ sub print_info { . $self->{data}->{info}->{state}->{brightness}->{max} . "]" ); main::print_log( "[Aurora:" . $self->{name} . "] -- Active Effects --" ); - foreach my $effect ( @{ $self->{data}->{info}->{effects}->{list} } ) { - main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); + if ( defined $self->{data}->{info}->{effects}->{list} ) { + + foreach my $effect ( @{ $self->{data}->{info}->{effects}->{list} } ) { + main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); + } + } + else { + foreach my $effect ( @{ $self->{data}->{info}->{effects}->{effectsList} } ) { + main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); + } } main::print_log( "[Aurora:" . $self->{name} . "] -- Layout --" ); main::print_log( "[Aurora:" @@ -810,9 +804,14 @@ sub process_data { $self->{previous}->{info}->{$key} = $self->{data}->{info}->{$key}; } $self->{previous}->{panels} = $self->{data}->{panels}; - @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{list} }; + if ( defined $self->{data}->{info}->{effects}->{list} ) { #beta API + @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{list} }; + } + else { + @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{effectsList} }; + } if ( defined $self->{child_object}->{effects} ) { - $self->{child_object}->{effects}->load_effects( @{ $self->{data}->{info}->{effects}->{list} } ); + $self->{child_object}->{effects}->load_effects( @{ $self->{previous}->{effects}->{list} } ); $self->{child_object}->{effects}->set( $self->{data}->{info}->{effects}->{select}, 'poll' ); $self->print_static if ( lc $self->{data}->{info}->{effects}->{select} eq "*static*" ); } @@ -874,8 +873,8 @@ sub process_data { if ( $self->{previous}->{info}->{state}->{brightness}->{value} != $self->{data}->{info}->{state}->{brightness}->{value} ) { main::print_log( "[Aurora:" . $self->{name} - . "] Brightness changed from $state{$self->{previous}->{info}->{state}->{brightness}->{value}} to $state{$self->{data}->{info}->{state}->{brightness}->{value}}" - ) if ( $self->{loglevel} ); + . "] Brightness changed from $self->{previous}->{info}->{state}->{brightness}->{value} to $self->{data}->{info}->{state}->{brightness}->{value}" ) + if ( $self->{loglevel} ); $self->{previous}->{info}->{state}->{brightness}->{value} = $self->{data}->{info}->{state}->{brightness}->{value}; $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); } @@ -911,7 +910,6 @@ sub process_data { if ( ( defined $self->{child_object}->{static} ) and ( lc $self->{data}->{info}->{effects}->{select} eq "*static*" ) ) { foreach my $key ( keys %{ $self->{child_object}->{static} } ) { - print "key=$key, gs=" . $self->{child_object}->{static}->{$key}->get_string() . " ls=" . $self->{last_static} . "\n"; unless ( $self->{child_object}->{static}->{$key}->get_string() eq $self->{last_static} ) { if ( lc $self->{child_object}->{static}->{$key}->state() ne 'off' ) { main::print_log "[Aurora:" . $self->{name} . "] Static Child object $key found. Updating..." if ( $self->{loglevel} ); @@ -1032,17 +1030,24 @@ sub get_static { sub print_discovery_info { my ($self) = @_; - my ( $devices, $dev_locs, $dev_ids ) = $self->scan_ssdp_data(); + my ( $devices, $dev_urls, $dev_ids ) = $self->scan_ssdp_data(); main::print_log( "Aurora:" . $self->{name} . "] ------------------------------------------------------------------" ); main::print_log( "Aurora:" . $self->{name} . "] Found $devices Aurora Controller" ); for my $i ( 1 .. $devices ) { - main::print_log( "Aurora:" . $self->{name} . "] Device $i Location: " . @$dev_locs[ $i - 1 ] ); + main::print_log( "Aurora:" . $self->{name} . "] Device $i Location URL: " . @$dev_urls[ $i - 1 ] ); main::print_log( "Aurora:" . $self->{name} . "] Device $i ID: " . @$dev_ids[ $i - 1 ] ); } main::print_log( "Aurora:" . $self->{name} . "] ------------------------------------------------------------------" ); } +sub identify { + my ($self) = @_; + + $self->_push_JSON_data('identify'); + return ('1'); +} + sub generate_voice_commands { my ($self) = @_; @@ -1076,7 +1081,6 @@ sub generate_voice_commands { #Evaluate the resulting object generating string package main; - print $object_string; eval $object_string; print "Error in nanoleaf_aurora_item_commands: $@\n" if $@; @@ -1086,8 +1090,9 @@ sub generate_voice_commands { sub get_voice_cmds { my ($self) = @_; my %voice_cmds = ( - 'Discover Auroras to print log' => $self->get_object_name . '->print_discovery_info', - 'Print Static string to print log' => $self->get_object_name . '->print_static' + 'Discover Auroras to print log' => $self->get_object_name . '->print_discovery_info', + 'Print Static string to print log' => $self->get_object_name . '->print_static', + 'Identify Aurora ' . $self->{name} . '(' . $self->get_object_name . ')' => $self->get_object_name . '->identify' ); return \%voice_cmds; @@ -1237,3 +1242,16 @@ sub set { } 1; + +# Version History +# v1.0.0 - initial module +# v1.0.1 - initial static support +# v1.0.2 - multiple auroras, brightness +# v1.0.3 - working static. turn on, overrides effect, turning off will restore previous effect +# v1.0.4 - Voice Commands +# v1.0.5 - working multi static +# v1.0.6 - better processing +# v1.0.7 - ability to specify API as an option +# v1.0.8 - initial v1.5.0 API v1 support +# v1.0.9 - use config_parms (mh.ini) instead of dedicated config file + From c1489b7f14087d2e819e63110f30442a9e220f56 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 7 May 2017 10:53:59 -0600 Subject: [PATCH 166/209] get_url can now output response codes --- bin/get_url | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/get_url b/bin/get_url index 255736668..3a62ca279 100755 --- a/bin/get_url +++ b/bin/get_url @@ -19,7 +19,7 @@ use Getopt::Long; #print "get_url: @ARGV\n"; if ( !&GetOptions( \%parms, 'h', 'help', 'quiet', 'cookies=s', 'cookie_file_in=s', 'cookie_file_out=s', 'post=s', 'header=s', 'userid=s', 'password=s', 'ua', - 'put=s', 'json' ) + 'put=s', 'json', 'response_code' ) or !@ARGV or $parms{h} or $parms{help} @@ -56,6 +56,8 @@ Usage: -ua: use UserAgent -header 'header_file': HTTP headers from the server are stored in this file + + -response_code: STDOUT only: Prepend output with RESPONSECODE: \n If local_file is specified, data is stored there. @@ -180,6 +182,7 @@ sub use_ua { } else { $response = $ua->request($request); + print "RESPONSECODE:" . $response->code() . "\n" if ( $parms{response_code} ); print $response->content(); } if ( $response->is_error() ) { From c44634e05a1c8118318ad88dae45d05a94ddcf55 Mon Sep 17 00:00:00 2001 From: hplato Date: Wed, 10 May 2017 21:20:47 -0600 Subject: [PATCH 167/209] v1.0.10 - perl fixes and token typo --- Nanoleaf_Aurora.pm | 1257 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1257 insertions(+) create mode 100644 Nanoleaf_Aurora.pm diff --git a/Nanoleaf_Aurora.pm b/Nanoleaf_Aurora.pm new file mode 100644 index 000000000..0339cd2c9 --- /dev/null +++ b/Nanoleaf_Aurora.pm @@ -0,0 +1,1257 @@ +package Nanoleaf_Aurora; + +# v1.0.10 + +#if any effect is changed, by definition the static child should be set to off. +#cmd data returns, need to check by command +#NB: if any effect gets set, or another static is active, then other statics should be set to off. + +#effects, turn static off and clear out static_check + +#TODO +#- confirm v1 API + +use strict; +use warnings; + +use LWP::UserAgent; +use HTTP::Request::Common qw(POST); +use JSON::XS; +use Data::Dumper; +use IO::Select; +use IO::Socket::INET; + +# REQUIRES AURORA FIRMWARE v1.4.39+ For v1.4.39 pass the option api=beta. +# +# To set up, first pair with mobile app -- the Aurora needs to be set up initially with the app +# to get it's wifi information. After it is successfully set up, it should be locatable via SSDP +# If multiple new Aurora's are plugged in at the same time, the SSDP query may not return unique +# addresses. Just add one at a time. +# +# To generate a token and allow local control, press and hold the power button for a few seconds +# until the controller light blinks. +# In a few seconds the MH print log should show that a token has been found. +# the location URL and tokens are stored in the mh.ini file + +# Nanoleaf_Aurora Objects +# +# $aurora = new Nanoleaf_Aurora('1'); +# $aurora_effects = new Nanoleaf_Aurora_Effects($aurora); +# $aurora_static1 = new Nanoleaf_Aurora_Static($aurora, "effect string"); +# $aurora_comm = new Nanoleaf_Aurora_Comm($aurora); + +# MH.INI settings +# If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used +# instead of object definitions +# +# aurora__url = +# aurora__token = +# aurora__poll = +# aurora__options = +# +# for example +#aurora_1_url = http://10.10.0.20:16021 +#aurora_1_token = EfgrIHH887EHhftotNNSD818erhNWHR0 +#aurora_1_options = 'api=beta' + +# OPTIONS +# current options that can be passed are; +# - api= +# - debug= +# - loglevel= + +# Notes +# +# The best way to set up static strings is to configure them in the mobile app, and then use the voice command +# get static string to print out the static configuration to the print log. + +# Issues + +# + +@Nanoleaf_Aurora::ISA = ('Generic_Item'); + +# -------------------- START OF SUBROUTINES -------------------- +# -------------------------------------------------------------- + +our %rest; +$rest{info} = ""; +$rest{effects} = "effects"; +$rest{auth} = "new"; +$rest{on} = "state/on"; +$rest{off} = "state/on"; +$rest{set_effect} = "effects/select"; +$rest{set_static} = "effects"; +$rest{brightness} = "state/brightness"; +$rest{brightness2} = "state/brightness"; +$rest{get_static} = "effects"; +$rest{identify} = "identify"; + +our %opts; +$opts{info} = "-ua"; +$opts{auth} = "-json -post '{}'"; +$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{get_static} = "-response_code -json -put '{\"write\":{\"command\":\"request\",\"version\":\"1.0\",\"animName\":\"*Static*\"}}'"; +$opts{identify} = "-response_code -json -put '{}'"; + +my $api_path = "/api/v1"; +our %active_auroras = (); + +sub new { + my ( $class, $id, $url, $token, $poll, $options ) = @_; + my $self = {}; + bless $self, $class; + $self->{name} = "1"; + $self->{name} = $id if ($id); + + $self->{data} = undef; + $self->{child_object} = undef; + $self->{config}->{poll_seconds} = 10; + $self->{config}->{poll_seconds} = $::config_parms{ "aurora_" . $self->{name} . "_poll" } + if ( defined $::config_parms{ "aurora_" . $self->{name} . "_poll" } ); + $self->{config}->{poll_seconds} = $poll if ($poll); + $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); + $self->{updating} = 0; + $self->{data}->{retry} = 0; + $self->{status} = ""; + $self->{module_version} = "v1.0.10"; + $self->{ssdp_timeout} = 4000; + $self->{last_static} = ""; + + $self->{url} = $url; + $self->{url} = $::config_parms{ "aurora_" . $self->{name} . "_url" } if ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ); + $self->{token} = $token; + $self->{token} = $::config_parms{ "aurora_" . $self->{name} . "_token" } if ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ); + $options = "" unless ( defined $options ); + $options = $::config_parms{ "aurora_" . $self->{name} . "_options" } if ( $::config_parms{ "aurora_" . $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->{debug} ) = ( $options =~ /debug\=(\d+)/i ); + $self->{loglevel} = 5; + ( $self->{loglevel} ) = ( $options =~ /loglevel\=(\d+)/i ); + + $self->{api_path} = $api_path; + if ( $options =~ m/api\=/i ) { + my ($api_ver) = ( $options =~ /api\=(\w+)/i ); + $self->{api_path} = "/api/" . $api_ver; + } + $self->{poll_data_timestamp} = 0; + $self->{max_poll_queue} = 3; + $self->{max_cmd_queue} = 5; + $self->{cmd_process_retry_limit} = 6; + + @{ $self->{poll_queue} } = (); + $self->{poll_data_file} = "$::config_parms{data_dir}/Aurora_poll_" . $self->{name} . ".data"; + unlink "$::config_parms{data_dir}/Aurora_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}/Aurora_cmd_" . $self->{name} . ".data"; + unlink "$::config_parms{data_dir}/Auroroa_cmd_" . $self->{name} . ".data"; + $self->{cmd_process} = new Process_Item; + $self->{cmd_process}->set_output( $self->{cmd_data_file} ); + &::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; + push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); + $self->{timer} = new Timer; + $self->start_timer; + return $self; +} + +sub start_timer { + my ($self) = @_; + unless ( defined $self->{timer} ) { + $self->{timer} = new Timer; #HP: why do timers get undefined?? + } + if ( defined $self->{timer} ) { + $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &Nanoleaf_Aurora::get_data($self) }, -1 ); + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] Warning, start_timer called but timer undefined" ); + } +} + +sub get_data { + my ($self) = @_; + + main::print_log( "[Aurora:" . $self->{name} . "] get_data initiated" ) if ( $self->{debug} ); + + #Check that we have data + + if ( !defined $self->{url} ) { + main::print_log( "[Aurora:" . $self->{name} . "] get ssdp data" ) if ( $self->{debug} ); + $self->{url} = $self->get_ssdp_data(); + $self->update_config_data() if ( $self->{url} ); + } + + #Check for a token + if ( ( !defined $self->{token} ) and ( defined $self->{url} ) ) { + main::print_log( "[Aurora:" . $self->{name} . "] Activating Location URL $self->{url}" ); + main::print_log( "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token" ); + eval { + my %data; + $data{text} = "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token"; + $data{color} = "yellow"; + &json_notification( "banner", {%data} ); + }; + $self->get_auth(); + + #if we have a location and token, then get some data + } + elsif ( $self->{token} and $self->{url} ) { + $self->poll(); + + if ( ( defined $self->{data}->{panels} ) and ( $self->{init} == 0 ) ) { + main::print_log( "[Aurora:" . $self->{name} . "] " . $self->{module_version} . " Configuration Loaded" ); + $active_auroras{ $self->{url} } = 1; + $self->print_info(); + $self->{init} = 1; + } + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] WARNING, Did not poll: location: $self->{url}, token: $self->{token}" ) if ( $self->{debug} ); + } +} + +sub get_auth { + my ($self) = @_; + + main::print_log( "[Aurora:" . $self->{name} . "] Sending Authentication request..." ); + $self->_get_JSON_data('auth'); + + return ('1'); +} + +sub poll { + my ($self) = @_; + + main::print_log( "[Aurora:" . $self->{name} . "] Background Polling initiated" ) if ( $self->{debug} ); + $self->_get_JSON_data('info'); + + return ('1'); +} + +sub process_check { + my ($self) = @_; + + return unless ( defined $self->{poll_process} ); + + if ( $self->{poll_process}->done_now() ) { + my $com_status = "online"; + main::print_log( "[Aurora:" . $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/403 Forbidden/i ) { + main::print_log( "[Aurora:" . $self->{name} . "] 403 Forbidden: Pair Button not pressed or auth token is incorrect" ); + return; + } + my ($responsecode) = $file_data =~ /^RESPONSECODE:(\d+)\n/; + $file_data =~ s/^RESPONSECODE:\d+\n// if ($responsecode); + + #print "debug: code=$responsecode\n" if ( $self->{debug} ); + + #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 =~ /({.*})/; + print "debug: json_data=$json_data\n" if ( $self->{debug} ); + unless ( ($file_data) and ($json_data) ) { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR! bad data returned by poll" ); + main::print_log( "[Aurora:" . $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( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; + } + else { + if ( keys %{$data} ) { + if ( $self->{poll_process_mode} eq "info" ) { + + #{"name":"Nanoleaf Aurora","serialNo":"XXXXXXXX","manufacturer":"Nanoleaf","firmwareVersion":"1.4.39","model":"NL22","state":{"on":{"value":true},"brightness":{"value":100,"max":100,"min":0},"hue":{"value":255,"max":360,"min":0},"sat":{"value":68,"max":100,"min":0},"ct":{"value":4000,"max":100,"min":0},"colorMode":"effect"},"effects":{"select":"Fireplace","list":["Color Burst","Flames","Forest","Inner Peace","Nemo","Northern Lights","Romantic","Snowfall","Fireplace","Sunset"]},"panelLayout":{"layout":{"layoutData":"2 150 195 -74 129 -120 149 -74 43 -60"},"globalOrientation":{"value":294,"max":360,"min":0} + $self->{data}->{info} = $data; + + # ($self->{data}->{panels}, $self->{data}->{panel_size}) = substr($data->{panelLayout}->{layout}->{layoutData},0,1); + my $layout; + if ( defined $data->{panelLayout}->{layout}->{layoutData} ) { #v1.4.39 + ( $self->{data}->{panels}, $self->{data}->{panel_size}, $layout ) = + $data->{panelLayout}->{layout}->{layoutData} =~ /^(\d+)\s+(\d+)\s+(.*)/; + my @panels = split / /, $layout; + for ( my $i = 0; $i < $self->{data}->{panels}; $i++ ) { + $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{x} = $panels[ ( $i * 4 ) + 1 ]; + $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{y} = $panels[ ( $i * 4 ) + 2 ]; + $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{o} = $panels[ ( $i * 4 ) + 3 ]; + } + } + else { + $self->{data}->{panels} = $data->{panelLayout}->{layout}->{numPanels}; + $self->{data}->{panel_size} = $data->{panelLayout}->{layout}->{sideLength}; + 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}; + $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{y} = + @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{y}; + $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{o} = + @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{o}; + } + } + + $self->process_data(); + } + elsif ( $self->{poll_process_mode} eq "auth" ) { + main::print_log( "[Aurora:" . $self->{name} . "] Authentication token found!" ); + $self->{token} = $data->{auth_token}; + main::print_log( "[Aurora:" . $self->{name} . "] Token $self->{token}" ) if ( $self->{debug} ); + $self->update_config_data(); + } + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR! Returned data not structured! Not processing..." ); + $com_status = "offline"; + } + } + + if ( scalar @{ $self->{poll_queue} } ) { + my $cmd_string = shift @{ $self->{poll_queue} }; + my ( $mode, $cmd ) = split /\|/, $cmd_string; + $self->{poll_process}->set($cmd); + $self->{poll_process}->start(); + $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; + $self->{poll_process_mode} = $mode; + main::print_log( "[Aurora:" . $self->{name} . "] Poll Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd" ) + if ( $self->{debug} ); + + } + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Aurora:" + . $self->{name} + . "] Communication Tracking object found. Updating from " + . $self->{child_object}->{comm}->state() . " to " + . $com_status . "..." + if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } + + return unless ( defined $self->{cmd_process} ); + if ( $self->{cmd_process}->done_now() ) { + main::print_log( "[Aurora:" . $self->{name} . "] Background Command " . $self->{cmd_process_mode} . " process completed" ) + if ( $self->{debug} ); + $self->get_data(); #poll since the command is done to get a new state + + my $file_data = &main::file_read( $self->{cmd_data_file} ); + + my ($responsecode) = $file_data =~ /^RESPONSECODE:(\d+)\n/; + $file_data =~ s/^RESPONSECODE:\d+\n// if ($responsecode); + print "debug: code=$responsecode\n" if ( $self->{debug} ); + + #print "success\n\n" if (($responsecode == 204) or ($responsecode == 200)); + my $com_status = "offline"; + + if ( ( $responsecode == 204 ) or ( $responsecode == 200 ) ) { + $com_status = "online"; + + # Successful commands should be [200 OK, 204 No Content] + shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful + $self->{cmd_process_retry} = 0; + + 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} ); + my ($json_data) = $file_data =~ /({.*})/; + print "debug: json_data=$json_data\n" if ( $self->{debug} ); + my $data; + eval { $data = JSON::XS->new->decode($json_data); }; + + # catch crashes: + if ($@) { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); + $com_status = "offline"; + } + else { + + #print Dumper $data; + + if ( keys %{$data} ) { + + #Process any returned data from a command straing + if ( $self->{cmd_process_mode} eq "get_static" ) { + main::print_log( "[Aurora:" . $self->{name} . "] get_static returned" ); + $self->{last_static} = $data->{animData} if ( defined $data->{animData} ); #just checked controller so update the static string + if ( ( defined $self->{static_check}->{string} ) and ( $self->{static_check}->{string} eq $data->{animData} ) ) { + $self->set_effect( $self->{static_check}->{effect} ); + $self->{static_check}->{string} = undef; + $self->{static_check}->{effect} = undef; + } + if ( ( defined $self->{static_check}->{print} ) and ( $self->{static_check}->{print} ) ) { + main::print_log( "[Aurora:" . $self->{name} . "] Static details. Current configuration string is" ); + main::print_log( "[Aurora:" . $self->{name} . "] $data->{animData}" ); + $self->{static_check}->{print} = 0; + } + + } + } + + $self->poll; + } + } + + if ( scalar @{ $self->{cmd_queue} } ) { + my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. + $self->{cmd_process}->set($cmd); + $self->{cmd_process}->start(); + main::print_log( "[Aurora:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) + if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); + } + + } + else { + + main::print_log( "[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful, retrying..." ); + if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); + shift @{ $self->{cmd_queue} }; + $self->{cmd_process_retry} = 0; + $com_status = "offline"; + } + else { + $self->{cmd_process_retry}++; + } + } + + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne $com_status ) { + main::print_log "[Aurora:" + . $self->{name} + . "] Communication Tracking object found. Updating from " + . $self->{child_object}->{comm}->state() . " to " + . $com_status . "..." + if ( $self->{loglevel} ); + $self->{status} = $com_status; + $self->{child_object}->{comm}->set( $com_status, 'poll' ); + } + } + } +} + +#polling process. Keep it separate from commands +sub _get_JSON_data { + my ( $self, $mode, $params ) = @_; + $params = $opts{$mode} unless defined($params); + my $token = ""; + $token = "/" . $self->{token} if ( defined $self->{url} and lc $mode ne "auth" ); + my $cmd = "get_url $params " . '"' . $self->{url} . $self->{api_path} . "$token/$rest{$mode}" . '"'; + 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( "[Aurora:" . $self->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd" ) if ( $self->{debug} ); + } + else { + if ( scalar @{ $self->{poll_queue} } < $self->{max_poll_queue} ) { + main::print_log( "[Aurora:" . $self->{name} . "] Queue is " . scalar @{ $self->{poll_queue} } . ". Queing command $mode, $cmd" ) + if ( $self->{debug} ); + push @{ $self->{poll_queue} }, "$mode|$cmd"; + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Command discarded." ); + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne "offline" ) { + main::print_log "[Aurora:" + . $self->{name} + . "] Communication Tracking object found. Updating from " + . $self->{child_object}->{comm}->state() + . " to offline..." + if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } + } + } +} + +#command process +sub _push_JSON_data { + my ( $self, $mode, $params ) = @_; + $params = $opts{$mode} unless defined($params); + unless ( defined $self->{token} ) { + main::print_log( "[Aurora:" . $self->{name} . "] ERROR no authentication token for command!" ); + return; + } + + my $cmd = "get_url $params" . ' "' . $self->{url} . $self->{api_path} . "/" . $self->{token} . "/$rest{$mode}" . '"'; + 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; + main::print_log( "[Aurora:" . $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( "[Aurora:" . $self->{name} . "] Queue is " . scalar @{ $self->{cmd_queue} } . ". Queing command $mode, $cmd" ) + if ( $self->{debug} ); +## push @{ $self->{cmd_queue} }, "$mode|$cmd"; + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); + if ( defined $self->{child_object}->{comm} ) { + if ( $self->{status} ne "offline" ) { + main::print_log "[Aurora:" + . $self->{name} + . "] Communication Tracking object found. Updating from " + . $self->{child_object}->{comm}->state() + . " to offline..." + if ( $self->{loglevel} ); + $self->{status} = "offline"; + $self->{child_object}->{comm}->set( "offline", 'poll' ); + } + } + } + } +} + +sub get_ssdp_data { + my ( $self, $id, $timeout ) = @_; + + #return a location that isn't in the $active_auroras hash + + my ( $devices, $locations, $serials ) = scan_ssdp_data($timeout); + &main::print_log( "[Aurora:" . $self->{name} . "] in get_ssdp_data devices=$devices" ) if ( $self->{debug} ); + &main::print_log( "[Aurora:" . $self->{name} . "] locations: " . join( ", ", @{$locations} ) ) if ( $self->{debug} ); + &main::print_log( "[Aurora:" . $self->{name} . "] nl_serials: " . join( ", ", @{$serials} ) ) if ( $self->{debug} ); + foreach my $dev ( @{$locations} ) { + &main::print_log( "[Aurora:" . $self->{name} . "] dev=$dev active_auroras{$dev}=$active_auroras{$dev}" ) if ( $self->{debug} ); + return $dev unless ( $active_auroras{$dev} ); + } + return; +} + +sub scan_ssdp_data { + my ( $self, $timeout ) = @_; + $timeout = 500 unless ($timeout); + my $addr = '239.255.255.250'; + my $port = 1900; + my $sock = new IO::Socket::INET->new( + LocalPort => $port, + Proto => 'udp', + Reuse => 1 + ) || &main::print_log( "[Aurora:" . $self->{name} . "] ERROR: SSDP Discovery, could not start a udp server on " . $port . $@ . "\n\n" ) && return; + + _mcast_add( $sock, '239.255.255.250' ); + setsockopt( $sock, getprotobyname('ip') || 0, _constant('IP_MULTICAST_TTL'), pack 'I', 4 ); + + my $sel = new IO::Select; + $sel->add($sock); + + my $q = <can_read(2); + last unless scalar @ready; + + $sock->recv( $data, 4096 ); + $discovery .= "-"; + + #print $data; + + if ( $data =~ m/ST: nanoleaf_aurora:light/ ) { + + #print "found" . $data; + my ($url) = $data =~ /Location:\s+(.*)/; + + #print "location " . $url; + $url =~ s/[^a-zA-Z0-9\:\.\/]*//g; + push @dev_urls, $url; + my ($devid) = $data =~ /nl\-deviceid:\s+(.*)/; + + #print "location " . $url; + $devid =~ s/[^a-zA-Z0-9\:\.\/]*//g; + push @dev_ids, $devid; + + &main::print_log( "[Aurora:" . $self->{name} . "] Found Aurora: $url, nl-deviceid: $devid" ); + + #last; + } + } + $discovery .= "< [" . scalar @dev_urls . "]\n"; + &main::print_log( "[Aurora:" . $self->{name} . "] $discovery" ); + return ( scalar @dev_urls, \@dev_urls, \@dev_ids ); +} + +sub _mcast_add { + my ( $sock, $addr ) = @_; + my $ip_mreq = inet_aton($addr) . INADDR_ANY; + + setsockopt( $sock, getprotobyname('ip') || 0, _constant('IP_ADD_MEMBERSHIP'), $ip_mreq ) || warn "Unable to add IGMP membership: $!\n"; +} + +sub _constant { + my $name = shift; + my %names = ( + IP_MULTICAST_TTL => 0, + IP_ADD_MEMBERSHIP => 1, + IP_MULTICAST_LOOP => 0, + ); + + my %constants = ( + MSWin32 => [ 10, 12 ], + cygwin => [ 3, 5 ], + darwin => [ 10, 12 ], + linux => [ 33, 35 ], + default => [ 33, 35 ], + ); + + my $index = $names{$name}; + my $ref = $constants{$^O} || $constants{default}; + return $ref->[$index]; +} + +sub update_config_data { + my ($self) = @_; + + my $write = 0; + my %parms; + + if ( ( defined $self->{url} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ne $self->{url} ) ) { + $::config_parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; + $parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; + main::print_log( "[Aurora:" . $self->{name} . "] Updating location URL ($self->{url}) in mh.ini file" ); + $write = 1; + + } + if ( ( defined $self->{token} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ne $self->{token} ) ) { + $::config_parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; + $parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; + main::print_log( "[Aurora:" . $self->{name} . "] Updating authentication token in mh.ini file" ); + $write = 1; + } + + &::write_mh_opts( \%parms ) if ($write); +} + +sub register { + my ( $self, $object, $type ) = @_; + my $keys = ""; + + #allow for multiple static entries + if ( lc $type eq 'static' ) { + if ( defined $self->{child_object}->{static} ) { + $keys = keys %{ $self->{child_object}->{static} }; + } + else { + $keys = 0; + } + $self->{child_object}->{static}->{$keys} = $object; + } + else { + $self->{child_object}->{$type} = $object; + } + $type .= " (" . $keys . ")" if ( $keys ne "" ); + &main::print_log( "[Aurora:" . $self->{name} . "] Registered $type child object" ); + +} + +sub stop_timer { + my ($self) = @_; + + if ( defined $self->{timer} ) { + $self->{timer}->stop() if ( $self->{timer}->active() ); + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] Warning, stop_timer called but timer undefined" ); + } +} + +sub print_info { + my ($self) = @_; + + main::print_log( "[Aurora:" . $self->{name} . "] Name: " . $self->{data}->{info}->{name} ); + main::print_log( "[Aurora:" . $self->{name} . "] Serial Number: " . $self->{data}->{info}->{serialNo} ); + 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} ); + 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} ); + main::print_log( "[Aurora:" . $self->{name} . "] MH Module version: " . $self->{module_version} ); + + main::print_log( "[Aurora:" . $self->{name} . "] -- Current Settings --" ); + + if ( $self->{data}->{info}->{state}->{on}->{value} ) { + main::print_log( "[Aurora:" . $self->{name} . "] State:\t ON" ); + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] State:\t OFF" ); + } + main::print_log( + "[Aurora:" . $self->{name} . "] Mode:\t " . $self->{data}->{info}->{state}->{colorMode} . " " . $self->{data}->{info}->{effects}->{select} ); + main::print_log( "[Aurora:" + . $self->{name} + . "] Brightness:\t " + . $self->{data}->{info}->{state}->{brightness}->{value} . "\t[" + . $self->{data}->{info}->{state}->{brightness}->{min} . "-" + . $self->{data}->{info}->{state}->{brightness}->{max} + . "]" ); + main::print_log( "[Aurora:" + . $self->{name} + . "] Hue:\t\t " + . $self->{data}->{info}->{state}->{hue}->{value} . "\t[" + . $self->{data}->{info}->{state}->{hue}->{min} . "-" + . $self->{data}->{info}->{state}->{hue}->{max} + . "]" ); + main::print_log( "[Aurora:" + . $self->{name} + . "] Saturation:\t " + . $self->{data}->{info}->{state}->{sat}->{value} . "\t[" + . $self->{data}->{info}->{state}->{sat}->{min} . "-" + . $self->{data}->{info}->{state}->{sat}->{max} + . "]" ); + 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} + . "]" ); + main::print_log( "[Aurora:" . $self->{name} . "] -- Active Effects --" ); + if ( defined $self->{data}->{info}->{effects}->{list} ) { + + foreach my $effect ( @{ $self->{data}->{info}->{effects}->{list} } ) { + main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); + } + } + else { + foreach my $effect ( @{ $self->{data}->{info}->{effects}->{effectsList} } ) { + main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); + } + } + main::print_log( "[Aurora:" . $self->{name} . "] -- Layout --" ); + main::print_log( "[Aurora:" + . $self->{name} + . "] Orientation:\t " + . $self->{data}->{info}->{panelLayout}->{globalOrientation}->{value} . "\t[" + . $self->{data}->{info}->{panelLayout}->{globalOrientation}->{min} . "-" + . $self->{data}->{info}->{panelLayout}->{globalOrientation}->{max} + . "]" ); + foreach my $key ( sort( keys %{ $self->{data}->{panel} } ) ) { + main::print_log( "[Aurora:" + . $self->{name} + . "] ID: " + . $key . "\tx:" + . $self->{data}->{panel}->{$key}->{x} . "\ty:" + . $self->{data}->{panel}->{$key}->{y} . "\to:" + . $self->{data}->{panel}->{$key}->{o} ); + } +} + +sub process_data { + my ($self) = @_; + + my (%state); + $state{true} = "on"; + $state{false} = "off"; + + # Main core of processing + # set state of self for state + # for any registered child selfs, update their state if + + main::print_log( "[Aurora:" . $self->{name} . "] Processing Data..." ) if ( $self->{debug} ); + + if ( ( !$self->{init_data} ) and ( defined $self->{data}->{info} ) ) { + main::print_log( "[Aurora:" . $self->{name} . "] Init: Setting startup values" ); + + foreach my $key ( keys %{$self->{data}->{info}} ) { + $self->{previous}->{info}->{$key} = $self->{data}->{info}->{$key}; + } + $self->{previous}->{panels} = $self->{data}->{panels}; + if ( defined $self->{data}->{info}->{effects}->{list} ) { #beta API + @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{list} }; + } + else { + @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{effectsList} }; + } + if ( defined $self->{child_object}->{effects} ) { + $self->{child_object}->{effects}->load_effects( @{ $self->{previous}->{effects}->{list} } ); + $self->{child_object}->{effects}->set( $self->{data}->{info}->{effects}->{select}, 'poll' ); + $self->print_static if ( lc $self->{data}->{info}->{effects}->{select} eq "*static*" ); + } + + if ( ( $self->{data}->{info}->{state}->{on}->{value} ) and ( $self->{data}->{info}->{state}->{brightness}->{value} != 100 ) ) { + $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); + } + else { + $self->set( $state{ $self->{data}->{info}->{state}->{on}->{value} }, 'poll' ); + } + $self->{init_data} = 1; + } + + if ( $self->{previous}->{info}->{firmwareVersion} ne $self->{data}->{info}->{firmwareVersion} ) { + main::print_log( + "[Aurora:" . $self->{name} . "] Firmware changed from $self->{previous}->{info}->{firmwareVersion} to $self->{data}->{info}->{firmwareVersion}" ); + main::print_log( "[Aurora:" . $self->{name} . "] This really isn't a regular operation. Should check Aurora to confirm" ); + $self->{previous}->{info}->{firmwareVersion} = $self->{data}->{info}->{firmwareVersion}; + } + + if ( $self->{previous}->{info}->{name} ne $self->{data}->{info}->{name} ) { + main::print_log( "[Aurora:" . $self->{name} . "] Device Name changed from $self->{previous}->{info}->{name} to $self->{data}->{info}->{name}" ); + main::print_log( "[Aurora:" . $self->{name} . "] This really isn't a regular operation. Should check Aurora to confirm" ); + $self->{previous}->{info}->{name} = $self->{data}->{info}->{name}; + } + + if ( $self->{previous}->{panels} != $self->{data}->{panels} ) { + main::print_log( "[Aurora:" . $self->{name} . "] Number of panels has changed from $self->{previous}->{panels} to $self->{data}->{panels}" ); + my $panel_added = ""; + my $panel_removed = ""; + foreach my $key ( sort( keys %{ $self->{data}->{panel} } ) ) { + $panel_added .= $key unless ( defined $self->{previous}->{panel}->{$key} ); + } + foreach my $key ( sort( keys %{ $self->{previous}->{panel} } ) ) { + $panel_removed .= $key unless ( defined $self->{data}->{panel}->{$key} ); + } + main::print_log( "[Aurora:" . $self->{name} . "] Panel ID(s) added: $panel_added" ) if ($panel_added); + main::print_log( "[Aurora:" . $self->{name} . "] Panel ID(s) removed: $panel_removed" ) if ($panel_removed); + $self->{previous}->{panels} = $self->{data}->{panels}; + $self->{previous}->{panel} = $self->{data}->{panel}; + } + + if ( $self->{previous}->{info}->{state}->{on}->{value} != $self->{data}->{info}->{state}->{on}->{value} ) { + main::print_log( "[Aurora:" + . $self->{name} + . "] State changed from $state{$self->{previous}->{info}->{state}->{on}->{value}} to $state{$self->{data}->{info}->{state}->{on}->{value}}" ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{state}->{on}->{value} = $self->{data}->{info}->{state}->{on}->{value}; + + #if on and brightness not 100 set brightness else set on or off + if ( ( $self->{data}->{info}->{state}->{on}->{value} ) and ( $self->{data}->{info}->{state}->{brightness}->{value} != 100 ) ) { + $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); + } + else { + $self->set( $state{ $self->{data}->{info}->{state}->{on}->{value} }, 'poll' ); + } + } + + if ( $self->{previous}->{info}->{state}->{brightness}->{value} != $self->{data}->{info}->{state}->{brightness}->{value} ) { + main::print_log( "[Aurora:" + . $self->{name} + . "] Brightness changed from $self->{previous}->{info}->{state}->{brightness}->{value} to $self->{data}->{info}->{state}->{brightness}->{value}" ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{state}->{brightness}->{value} = $self->{data}->{info}->{state}->{brightness}->{value}; + $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); + } + + if ( $self->{previous}->{info}->{state}->{colorMode} ne $self->{data}->{info}->{state}->{colorMode} ) { + main::print_log( "[Aurora:" + . $self->{name} + . "] State ColorMode changed from $self->{previous}->{info}->{state}->{colorMode} to $self->{data}->{info}->{state}->{colorMode}" ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{state}->{colorMode} = $self->{data}->{info}->{state}->{colorMode}; + } + + if ( $self->{previous}->{info}->{effects}->{select} ne $self->{data}->{info}->{effects}->{select} ) { + main::print_log( "[Aurora:" + . $self->{name} + . "] Selected Effect changed from $self->{previous}->{info}->{effects}->{select} to $self->{data}->{info}->{effects}->{select}" ) + if ( $self->{loglevel} ); + $self->{previous}->{info}->{effects}->{select} = $self->{data}->{info}->{effects}->{select}; + if ( defined $self->{child_object}->{effects} ) { + main::print_log "[Aurora:" . $self->{name} . "] Effects Child object found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{effects}->set( $self->{data}->{info}->{effects}->{select}, 'poll' ); + } + if ( ( defined $self->{child_object}->{static} ) and ( lc $self->{data}->{info}->{effects}->{select} ne "*static*" ) ) { + main::print_log "[Aurora:" . $self->{name} . "] Static Child object(s) found. Updating..." if ( $self->{loglevel} ); + foreach my $key ( keys %{ $self->{child_object}->{static} } ) { + $self->{child_object}->{static}->{$key}->set( 'off', 'poll' ); + } + } + } + + #if a non static effect is active, set all static items to off + #if it's set to *Static* then turn off all static entries that don't match the current key + + if ( ( defined $self->{child_object}->{static} ) and ( lc $self->{data}->{info}->{effects}->{select} eq "*static*" ) ) { + foreach my $key ( keys %{ $self->{child_object}->{static} } ) { + unless ( $self->{child_object}->{static}->{$key}->get_string() eq $self->{last_static} ) { + if ( lc $self->{child_object}->{static}->{$key}->state() ne 'off' ) { + main::print_log "[Aurora:" . $self->{name} . "] Static Child object $key found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{static}->{$key}->set( 'off', 'poll' ); + } + } + else { + if ( lc $self->{child_object}->{static}->{$key}->state() ne 'on' ) { + main::print_log "[Aurora:" . $self->{name} . "] Static Child object $key found. Updating..." if ( $self->{loglevel} ); + $self->{child_object}->{static}->{$key}->set( 'on', 'poll' ); + } + } + } + } + +} + +#------------ +# User access methods + +sub get_effect { + my ($self) = @_; + + return ( $self->{data}->{info}->{effects}->{select} ); + +} + +sub check_panelid { + my ( $self, $id ) = @_; + + my $found = 0; + $found = 1 if ( defined $self->{data}->{panel}->{$id} ); + + return $found; +} + +sub get_debug { + my ($self) = @_; + return $self->{debug}; +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $p_state .= "%" if ( $p_state =~ m/\d+(?!%)/ ); + main::print_log( "[Aurora:" . $self->{name} . "] DB super::set, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); + $self->SUPER::set($p_state); + $self->start_timer; + + } + else { + main::print_log( "[Aurora:" . $self->{name} . "] DB set_mode, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); + my $mode = lc $p_state; + if ( ( $mode eq "on" ) or ( $mode eq "off" ) ) { + $self->_push_JSON_data($mode); + } + elsif ( $mode =~ /^(\d+)/ ) { + my $params = $opts{brightness} . $1 . '}' . "'"; + $self->_push_JSON_data( 'brightness', $params ); + } + elsif ( $mode =~ /^([-+]\d+)/ ) { + my $params = $opts{brightness2} . $1 . '}' . "'"; + $self->_push_JSON_data( 'brightness2', $params ); + } + else { + main::print_log( "Aurora:" . $self->{name} . "] Error, unknown set state $p_state" ); + return ('0'); + } + return ('1'); + } +} + +sub set_effect { + my ( $self, $effect ) = @_; + + my $params = $opts{set_effect} . '"' . $effect . '"}' . "'"; + $self->_push_JSON_data( 'set_effect', $params ); + return ('1'); +} + +sub set_static { + my ( $self, $string, $loop ) = @_; + + my $jloop = "false"; + $jloop = "true" if ($loop); + my $params = $opts{set_static} . '"' . $string . '","loop":' . $jloop . "}}'"; + $self->{last_static} = $string; + $self->_push_JSON_data( 'set_static', $params ); + return ('1'); + +} + +sub check_static { + my ( $self, $string, $prev_effect ) = @_; + + $self->{static_check}->{string} = $string; + $self->{static_check}->{effect} = $prev_effect; + $self->_push_JSON_data('get_static'); + return ('1'); +} + +sub print_static { + my ($self) = @_; + + $self->{static_check}->{print} = 1; + $self->_push_JSON_data('get_static'); + return ('1'); +} + +sub get_static { + my ($self) = @_; + + $self->_push_JSON_data('get_static'); + return ('1'); +} + +sub print_discovery_info { + my ($self) = @_; + + my ( $devices, $dev_urls, $dev_ids ) = $self->scan_ssdp_data(); + main::print_log( "Aurora:" . $self->{name} . "] ------------------------------------------------------------------" ); + main::print_log( "Aurora:" . $self->{name} . "] Found $devices Aurora Controller" ); + for my $i ( 1 .. $devices ) { + main::print_log( "Aurora:" . $self->{name} . "] Device $i Location URL: " . @$dev_urls[ $i - 1 ] ); + main::print_log( "Aurora:" . $self->{name} . "] Device $i ID: " . @$dev_ids[ $i - 1 ] ); + } + main::print_log( "Aurora:" . $self->{name} . "] ------------------------------------------------------------------" ); + +} + +sub identify { + my ($self) = @_; + + $self->_push_JSON_data('identify'); + return ('1'); +} + +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"); + + my $voice_cmds = $self->get_voice_cmds(); + my $i = 1; + foreach my $cmd ( keys %$voice_cmds ) { + + #get object name to use as part of variable in voice command + my $object_name_v = $object_name . '_' . $i . '_v'; + $object_string .= "use vars '${object_name}_${i}_v';\n"; + + #Convert object name into readable voice command words + my $command = $object_name; + $command =~ s/^\$//; + $command =~ tr/_/ /; + + #Initialize the voice command with all of the possible device commands + $object_string .= $object_name . "_" . $i . "_v = new Voice_Cmd '$command $cmd';\n"; + + #Tie the proper routine to each voice command + $object_string .= $object_name . "_" . $i . "_v -> tie_event('" . $voice_cmds->{$cmd} . "');\n\n"; #, '$command $cmd');\n\n"; + + #Add this object to the list of Insteon Voice Commands on the Web Interface + $object_string .= ::store_object_data( $object_name_v, 'Voice_Cmd', 'Nanoleaf_Aurora', 'Controller_commands' ); + $i++; + } + + #Evaluate the resulting object generating string + package main; + eval $object_string; + print "Error in nanoleaf_aurora_item_commands: $@\n" if $@; + + package Nanoleaf_Aurora; +} + +sub get_voice_cmds { + my ($self) = @_; + my %voice_cmds = ( + 'Discover Auroras to print log' => $self->get_object_name . '->print_discovery_info', + 'Print Static string to print log' => $self->get_object_name . '->print_static', + 'Identify Aurora ' . $self->{name} . '(' . $self->get_object_name . ')' => $self->get_object_name . '->identify' + ); + + return \%voice_cmds; +} + +package Nanoleaf_Aurora_Effects; + +@Nanoleaf_Aurora_Effects::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + + $object->register( $self, 'effects' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + my $found = 0; + foreach my $eff ( @{ $$self{states} } ) { + $found = 1 if ( $p_state eq $eff ); + } + if ($found) { + $$self{master_object}->set_effect($p_state); + } + else { + main::print_log("[Aurora Effect] Error. Unknown effect state $p_state"); + } + } +} + +sub load_effects { + my ( $self, @effect_states ) = @_; + + @{ $$self{states} } = @effect_states; +} + +package Nanoleaf_Aurora_Static; + +@Nanoleaf_Aurora_Static::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object, $static_string ) = @_; + + my $self = {}; + bless $self, $class; + @{ $$self{states} } = ( 'on', 'off' ); + + $$self{master_object} = $object; + $$self{loop} = 0; + $$self{string} = $static_string if ( defined $static_string ); + $object->register( $self, 'static' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } + else { + #if ON then backup current configuration and set the new one. + if ( lc $p_state eq 'on' ) { + $$self{previous_effect} = $$self{master_object}->get_effect(); + $$self{master_object}->set_static( $$self{string} ); + + #if OFF then check if current is same as static, and if so restore the old one. + } + elsif ( lc $p_state eq 'off' ) { + $$self{master_object}->check_static( $$self{string}, $$self{previous_effect} ); + } + else { + main::print_log("[Aurora Static] Error. Unknown set mode $p_state"); + } + } +} + +sub configure { + my ( $self, $pid, $frames, $frame, $r, $g, $b, $w, $t ) = @_; + if ( $$self{master_object}->check_panelid($pid) ) { + $$self{data}{$pid}{frames} = $frames; + $$self{data}{$pid}{$frame} = "$r $g $b $w $t"; + } + else { + main::print_log("[Aurora Static] Error. Unknown panel ID $pid"); + } +} + +sub loop { + my ( $self, $value ) = @_; + + if ( defined $value ) { + if ($value) { + $$self{loop} = 1; + } + else { + $$self{loop} = 0; + } + } + else { + return $$self{loop}; + } +} + +sub get_string { + my ($self) = @_; + + return ( $$self{string} ); +} + +package Nanoleaf_Aurora_Comm; + +@Nanoleaf_Aurora_Comm::ISA = ('Generic_Item'); + +sub new { + my ( $class, $object ) = @_; + + my $self = {}; + bless $self, $class; + + $$self{master_object} = $object; + push( @{ $$self{states} }, 'online', 'offline' ); + $object->register( $self, 'comm' ); + return $self; + +} + +sub set { + my ( $self, $p_state, $p_setby ) = @_; + + if ( $p_setby eq 'poll' ) { + $self->SUPER::set($p_state); + } +} + +1; + +# Version History +# v1.0.0 - initial module +# v1.0.1 - initial static support +# v1.0.2 - multiple auroras, brightness +# v1.0.3 - working static. turn on, overrides effect, turning off will restore previous effect +# v1.0.4 - Voice Commands +# v1.0.5 - working multi static +# v1.0.6 - better processing +# v1.0.7 - ability to specify API as an option +# v1.0.8 - initial v1.5.0 API v1 support +# v1.0.9 - use config_parms (mh.ini) instead of dedicated config file +# v1.0.10 - Updated to work with other versions of perl, typo with mh.ini From 76163b63ed999029f3702f06681bcfe08793da62 Mon Sep 17 00:00:00 2001 From: hplato Date: Wed, 10 May 2017 21:22:00 -0600 Subject: [PATCH 168/209] Delete Nanoleaf_Aurora.pm --- Nanoleaf_Aurora.pm | 1257 -------------------------------------------- 1 file changed, 1257 deletions(-) delete mode 100644 Nanoleaf_Aurora.pm diff --git a/Nanoleaf_Aurora.pm b/Nanoleaf_Aurora.pm deleted file mode 100644 index 0339cd2c9..000000000 --- a/Nanoleaf_Aurora.pm +++ /dev/null @@ -1,1257 +0,0 @@ -package Nanoleaf_Aurora; - -# v1.0.10 - -#if any effect is changed, by definition the static child should be set to off. -#cmd data returns, need to check by command -#NB: if any effect gets set, or another static is active, then other statics should be set to off. - -#effects, turn static off and clear out static_check - -#TODO -#- confirm v1 API - -use strict; -use warnings; - -use LWP::UserAgent; -use HTTP::Request::Common qw(POST); -use JSON::XS; -use Data::Dumper; -use IO::Select; -use IO::Socket::INET; - -# REQUIRES AURORA FIRMWARE v1.4.39+ For v1.4.39 pass the option api=beta. -# -# To set up, first pair with mobile app -- the Aurora needs to be set up initially with the app -# to get it's wifi information. After it is successfully set up, it should be locatable via SSDP -# If multiple new Aurora's are plugged in at the same time, the SSDP query may not return unique -# addresses. Just add one at a time. -# -# To generate a token and allow local control, press and hold the power button for a few seconds -# until the controller light blinks. -# In a few seconds the MH print log should show that a token has been found. -# the location URL and tokens are stored in the mh.ini file - -# Nanoleaf_Aurora Objects -# -# $aurora = new Nanoleaf_Aurora('1'); -# $aurora_effects = new Nanoleaf_Aurora_Effects($aurora); -# $aurora_static1 = new Nanoleaf_Aurora_Static($aurora, "effect string"); -# $aurora_comm = new Nanoleaf_Aurora_Comm($aurora); - -# MH.INI settings -# If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used -# instead of object definitions -# -# aurora__url = -# aurora__token = -# aurora__poll = -# aurora__options = -# -# for example -#aurora_1_url = http://10.10.0.20:16021 -#aurora_1_token = EfgrIHH887EHhftotNNSD818erhNWHR0 -#aurora_1_options = 'api=beta' - -# OPTIONS -# current options that can be passed are; -# - api= -# - debug= -# - loglevel= - -# Notes -# -# The best way to set up static strings is to configure them in the mobile app, and then use the voice command -# get static string to print out the static configuration to the print log. - -# Issues - -# - -@Nanoleaf_Aurora::ISA = ('Generic_Item'); - -# -------------------- START OF SUBROUTINES -------------------- -# -------------------------------------------------------------- - -our %rest; -$rest{info} = ""; -$rest{effects} = "effects"; -$rest{auth} = "new"; -$rest{on} = "state/on"; -$rest{off} = "state/on"; -$rest{set_effect} = "effects/select"; -$rest{set_static} = "effects"; -$rest{brightness} = "state/brightness"; -$rest{brightness2} = "state/brightness"; -$rest{get_static} = "effects"; -$rest{identify} = "identify"; - -our %opts; -$opts{info} = "-ua"; -$opts{auth} = "-json -post '{}'"; -$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{get_static} = "-response_code -json -put '{\"write\":{\"command\":\"request\",\"version\":\"1.0\",\"animName\":\"*Static*\"}}'"; -$opts{identify} = "-response_code -json -put '{}'"; - -my $api_path = "/api/v1"; -our %active_auroras = (); - -sub new { - my ( $class, $id, $url, $token, $poll, $options ) = @_; - my $self = {}; - bless $self, $class; - $self->{name} = "1"; - $self->{name} = $id if ($id); - - $self->{data} = undef; - $self->{child_object} = undef; - $self->{config}->{poll_seconds} = 10; - $self->{config}->{poll_seconds} = $::config_parms{ "aurora_" . $self->{name} . "_poll" } - if ( defined $::config_parms{ "aurora_" . $self->{name} . "_poll" } ); - $self->{config}->{poll_seconds} = $poll if ($poll); - $self->{config}->{poll_seconds} = 1 if ( $self->{config}->{poll_seconds} < 1 ); - $self->{updating} = 0; - $self->{data}->{retry} = 0; - $self->{status} = ""; - $self->{module_version} = "v1.0.10"; - $self->{ssdp_timeout} = 4000; - $self->{last_static} = ""; - - $self->{url} = $url; - $self->{url} = $::config_parms{ "aurora_" . $self->{name} . "_url" } if ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ); - $self->{token} = $token; - $self->{token} = $::config_parms{ "aurora_" . $self->{name} . "_token" } if ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ); - $options = "" unless ( defined $options ); - $options = $::config_parms{ "aurora_" . $self->{name} . "_options" } if ( $::config_parms{ "aurora_" . $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->{debug} ) = ( $options =~ /debug\=(\d+)/i ); - $self->{loglevel} = 5; - ( $self->{loglevel} ) = ( $options =~ /loglevel\=(\d+)/i ); - - $self->{api_path} = $api_path; - if ( $options =~ m/api\=/i ) { - my ($api_ver) = ( $options =~ /api\=(\w+)/i ); - $self->{api_path} = "/api/" . $api_ver; - } - $self->{poll_data_timestamp} = 0; - $self->{max_poll_queue} = 3; - $self->{max_cmd_queue} = 5; - $self->{cmd_process_retry_limit} = 6; - - @{ $self->{poll_queue} } = (); - $self->{poll_data_file} = "$::config_parms{data_dir}/Aurora_poll_" . $self->{name} . ".data"; - unlink "$::config_parms{data_dir}/Aurora_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}/Aurora_cmd_" . $self->{name} . ".data"; - unlink "$::config_parms{data_dir}/Auroroa_cmd_" . $self->{name} . ".data"; - $self->{cmd_process} = new Process_Item; - $self->{cmd_process}->set_output( $self->{cmd_data_file} ); - &::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; - push( @{ $$self{states} }, 'off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'on' ); - $self->{timer} = new Timer; - $self->start_timer; - return $self; -} - -sub start_timer { - my ($self) = @_; - unless ( defined $self->{timer} ) { - $self->{timer} = new Timer; #HP: why do timers get undefined?? - } - if ( defined $self->{timer} ) { - $self->{timer}->set( $self->{config}->{poll_seconds}, sub { &Nanoleaf_Aurora::get_data($self) }, -1 ); - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] Warning, start_timer called but timer undefined" ); - } -} - -sub get_data { - my ($self) = @_; - - main::print_log( "[Aurora:" . $self->{name} . "] get_data initiated" ) if ( $self->{debug} ); - - #Check that we have data - - if ( !defined $self->{url} ) { - main::print_log( "[Aurora:" . $self->{name} . "] get ssdp data" ) if ( $self->{debug} ); - $self->{url} = $self->get_ssdp_data(); - $self->update_config_data() if ( $self->{url} ); - } - - #Check for a token - if ( ( !defined $self->{token} ) and ( defined $self->{url} ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] Activating Location URL $self->{url}" ); - main::print_log( "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token" ); - eval { - my %data; - $data{text} = "[Aurora:" . $self->{name} . "] Please hold down power button to generate access token"; - $data{color} = "yellow"; - &json_notification( "banner", {%data} ); - }; - $self->get_auth(); - - #if we have a location and token, then get some data - } - elsif ( $self->{token} and $self->{url} ) { - $self->poll(); - - if ( ( defined $self->{data}->{panels} ) and ( $self->{init} == 0 ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] " . $self->{module_version} . " Configuration Loaded" ); - $active_auroras{ $self->{url} } = 1; - $self->print_info(); - $self->{init} = 1; - } - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING, Did not poll: location: $self->{url}, token: $self->{token}" ) if ( $self->{debug} ); - } -} - -sub get_auth { - my ($self) = @_; - - main::print_log( "[Aurora:" . $self->{name} . "] Sending Authentication request..." ); - $self->_get_JSON_data('auth'); - - return ('1'); -} - -sub poll { - my ($self) = @_; - - main::print_log( "[Aurora:" . $self->{name} . "] Background Polling initiated" ) if ( $self->{debug} ); - $self->_get_JSON_data('info'); - - return ('1'); -} - -sub process_check { - my ($self) = @_; - - return unless ( defined $self->{poll_process} ); - - if ( $self->{poll_process}->done_now() ) { - my $com_status = "online"; - main::print_log( "[Aurora:" . $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/403 Forbidden/i ) { - main::print_log( "[Aurora:" . $self->{name} . "] 403 Forbidden: Pair Button not pressed or auth token is incorrect" ); - return; - } - my ($responsecode) = $file_data =~ /^RESPONSECODE:(\d+)\n/; - $file_data =~ s/^RESPONSECODE:\d+\n// if ($responsecode); - - #print "debug: code=$responsecode\n" if ( $self->{debug} ); - - #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 =~ /({.*})/; - print "debug: json_data=$json_data\n" if ( $self->{debug} ); - unless ( ($file_data) and ($json_data) ) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! bad data returned by poll" ); - main::print_log( "[Aurora:" . $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( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); - $com_status = "offline"; - } - else { - if ( keys %{$data} ) { - if ( $self->{poll_process_mode} eq "info" ) { - - #{"name":"Nanoleaf Aurora","serialNo":"XXXXXXXX","manufacturer":"Nanoleaf","firmwareVersion":"1.4.39","model":"NL22","state":{"on":{"value":true},"brightness":{"value":100,"max":100,"min":0},"hue":{"value":255,"max":360,"min":0},"sat":{"value":68,"max":100,"min":0},"ct":{"value":4000,"max":100,"min":0},"colorMode":"effect"},"effects":{"select":"Fireplace","list":["Color Burst","Flames","Forest","Inner Peace","Nemo","Northern Lights","Romantic","Snowfall","Fireplace","Sunset"]},"panelLayout":{"layout":{"layoutData":"2 150 195 -74 129 -120 149 -74 43 -60"},"globalOrientation":{"value":294,"max":360,"min":0} - $self->{data}->{info} = $data; - - # ($self->{data}->{panels}, $self->{data}->{panel_size}) = substr($data->{panelLayout}->{layout}->{layoutData},0,1); - my $layout; - if ( defined $data->{panelLayout}->{layout}->{layoutData} ) { #v1.4.39 - ( $self->{data}->{panels}, $self->{data}->{panel_size}, $layout ) = - $data->{panelLayout}->{layout}->{layoutData} =~ /^(\d+)\s+(\d+)\s+(.*)/; - my @panels = split / /, $layout; - for ( my $i = 0; $i < $self->{data}->{panels}; $i++ ) { - $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{x} = $panels[ ( $i * 4 ) + 1 ]; - $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{y} = $panels[ ( $i * 4 ) + 2 ]; - $self->{data}->{panel}->{ $panels[ $i * 4 ] }->{o} = $panels[ ( $i * 4 ) + 3 ]; - } - } - else { - $self->{data}->{panels} = $data->{panelLayout}->{layout}->{numPanels}; - $self->{data}->{panel_size} = $data->{panelLayout}->{layout}->{sideLength}; - 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}; - $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{y} = - @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{y}; - $self->{data}->{panel}->{ @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{panelId} }->{o} = - @{ $data->{panelLayout}->{layout}->{positionData} }[$i]->{o}; - } - } - - $self->process_data(); - } - elsif ( $self->{poll_process_mode} eq "auth" ) { - main::print_log( "[Aurora:" . $self->{name} . "] Authentication token found!" ); - $self->{token} = $data->{auth_token}; - main::print_log( "[Aurora:" . $self->{name} . "] Token $self->{token}" ) if ( $self->{debug} ); - $self->update_config_data(); - } - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! Returned data not structured! Not processing..." ); - $com_status = "offline"; - } - } - - if ( scalar @{ $self->{poll_queue} } ) { - my $cmd_string = shift @{ $self->{poll_queue} }; - my ( $mode, $cmd ) = split /\|/, $cmd_string; - $self->{poll_process}->set($cmd); - $self->{poll_process}->start(); - $self->{poll_process_pid}->{ $self->{poll_process}->pid() } = $mode; #capture the type of information requested in order to parse; - $self->{poll_process_mode} = $mode; - main::print_log( "[Aurora:" . $self->{name} . "] Poll Queue " . $self->{poll_process}->pid() . " mode=$mode cmd=$cmd" ) - if ( $self->{debug} ); - - } - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne $com_status ) { - main::print_log "[Aurora:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() . " to " - . $com_status . "..." - if ( $self->{loglevel} ); - $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); - } - } - } - - return unless ( defined $self->{cmd_process} ); - if ( $self->{cmd_process}->done_now() ) { - main::print_log( "[Aurora:" . $self->{name} . "] Background Command " . $self->{cmd_process_mode} . " process completed" ) - if ( $self->{debug} ); - $self->get_data(); #poll since the command is done to get a new state - - my $file_data = &main::file_read( $self->{cmd_data_file} ); - - my ($responsecode) = $file_data =~ /^RESPONSECODE:(\d+)\n/; - $file_data =~ s/^RESPONSECODE:\d+\n// if ($responsecode); - print "debug: code=$responsecode\n" if ( $self->{debug} ); - - #print "success\n\n" if (($responsecode == 204) or ($responsecode == 200)); - my $com_status = "offline"; - - if ( ( $responsecode == 204 ) or ( $responsecode == 200 ) ) { - $com_status = "online"; - - # Successful commands should be [200 OK, 204 No Content] - shift @{ $self->{cmd_queue} }; #remove the command from queue since it was successful - $self->{cmd_process_retry} = 0; - - 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} ); - my ($json_data) = $file_data =~ /({.*})/; - print "debug: json_data=$json_data\n" if ( $self->{debug} ); - my $data; - eval { $data = JSON::XS->new->decode($json_data); }; - - # catch crashes: - if ($@) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR! JSON file parser crashed! $@\n" ); - $com_status = "offline"; - } - else { - - #print Dumper $data; - - if ( keys %{$data} ) { - - #Process any returned data from a command straing - if ( $self->{cmd_process_mode} eq "get_static" ) { - main::print_log( "[Aurora:" . $self->{name} . "] get_static returned" ); - $self->{last_static} = $data->{animData} if ( defined $data->{animData} ); #just checked controller so update the static string - if ( ( defined $self->{static_check}->{string} ) and ( $self->{static_check}->{string} eq $data->{animData} ) ) { - $self->set_effect( $self->{static_check}->{effect} ); - $self->{static_check}->{string} = undef; - $self->{static_check}->{effect} = undef; - } - if ( ( defined $self->{static_check}->{print} ) and ( $self->{static_check}->{print} ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] Static details. Current configuration string is" ); - main::print_log( "[Aurora:" . $self->{name} . "] $data->{animData}" ); - $self->{static_check}->{print} = 0; - } - - } - } - - $self->poll; - } - } - - if ( scalar @{ $self->{cmd_queue} } ) { - my $cmd = @{ $self->{cmd_queue} }[0]; #grab the first command, but don't take it off. - $self->{cmd_process}->set($cmd); - $self->{cmd_process}->start(); - main::print_log( "[Aurora:" . $self->{name} . "] Command Queue " . $self->{cmd_process}->pid() . " cmd=$cmd" ) - if ( ( $self->{debug} ) or ( $self->{cmd_process_retry} ) ); - } - - } - else { - - main::print_log( "[Aurora:" . $self->{name} . "] WARNING Issued command was unsuccessful, retrying..." ); - if ( $self->{cmd_process_retry} > $self->{cmd_process_retry_limit} ) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR Issued command max retries reached. Abandoning command attempt..." ); - shift @{ $self->{cmd_queue} }; - $self->{cmd_process_retry} = 0; - $com_status = "offline"; - } - else { - $self->{cmd_process_retry}++; - } - } - - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne $com_status ) { - main::print_log "[Aurora:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() . " to " - . $com_status . "..." - if ( $self->{loglevel} ); - $self->{status} = $com_status; - $self->{child_object}->{comm}->set( $com_status, 'poll' ); - } - } - } -} - -#polling process. Keep it separate from commands -sub _get_JSON_data { - my ( $self, $mode, $params ) = @_; - $params = $opts{$mode} unless defined($params); - my $token = ""; - $token = "/" . $self->{token} if ( defined $self->{url} and lc $mode ne "auth" ); - my $cmd = "get_url $params " . '"' . $self->{url} . $self->{api_path} . "$token/$rest{$mode}" . '"'; - 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( "[Aurora:" . $self->{name} . "] Backgrounding " . $self->{poll_process}->pid() . " command $mode, $cmd" ) if ( $self->{debug} ); - } - else { - if ( scalar @{ $self->{poll_queue} } < $self->{max_poll_queue} ) { - main::print_log( "[Aurora:" . $self->{name} . "] Queue is " . scalar @{ $self->{poll_queue} } . ". Queing command $mode, $cmd" ) - if ( $self->{debug} ); - push @{ $self->{poll_queue} }, "$mode|$cmd"; - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_poll_queue} . ". Command discarded." ); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne "offline" ) { - main::print_log "[Aurora:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } - } - } -} - -#command process -sub _push_JSON_data { - my ( $self, $mode, $params ) = @_; - $params = $opts{$mode} unless defined($params); - unless ( defined $self->{token} ) { - main::print_log( "[Aurora:" . $self->{name} . "] ERROR no authentication token for command!" ); - return; - } - - my $cmd = "get_url $params" . ' "' . $self->{url} . $self->{api_path} . "/" . $self->{token} . "/$rest{$mode}" . '"'; - 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; - main::print_log( "[Aurora:" . $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( "[Aurora:" . $self->{name} . "] Queue is " . scalar @{ $self->{cmd_queue} } . ". Queing command $mode, $cmd" ) - if ( $self->{debug} ); -## push @{ $self->{cmd_queue} }, "$mode|$cmd"; - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] WARNING. Queue has grown past " . $self->{max_cmd_queue} . ". Command discarded." ); - if ( defined $self->{child_object}->{comm} ) { - if ( $self->{status} ne "offline" ) { - main::print_log "[Aurora:" - . $self->{name} - . "] Communication Tracking object found. Updating from " - . $self->{child_object}->{comm}->state() - . " to offline..." - if ( $self->{loglevel} ); - $self->{status} = "offline"; - $self->{child_object}->{comm}->set( "offline", 'poll' ); - } - } - } - } -} - -sub get_ssdp_data { - my ( $self, $id, $timeout ) = @_; - - #return a location that isn't in the $active_auroras hash - - my ( $devices, $locations, $serials ) = scan_ssdp_data($timeout); - &main::print_log( "[Aurora:" . $self->{name} . "] in get_ssdp_data devices=$devices" ) if ( $self->{debug} ); - &main::print_log( "[Aurora:" . $self->{name} . "] locations: " . join( ", ", @{$locations} ) ) if ( $self->{debug} ); - &main::print_log( "[Aurora:" . $self->{name} . "] nl_serials: " . join( ", ", @{$serials} ) ) if ( $self->{debug} ); - foreach my $dev ( @{$locations} ) { - &main::print_log( "[Aurora:" . $self->{name} . "] dev=$dev active_auroras{$dev}=$active_auroras{$dev}" ) if ( $self->{debug} ); - return $dev unless ( $active_auroras{$dev} ); - } - return; -} - -sub scan_ssdp_data { - my ( $self, $timeout ) = @_; - $timeout = 500 unless ($timeout); - my $addr = '239.255.255.250'; - my $port = 1900; - my $sock = new IO::Socket::INET->new( - LocalPort => $port, - Proto => 'udp', - Reuse => 1 - ) || &main::print_log( "[Aurora:" . $self->{name} . "] ERROR: SSDP Discovery, could not start a udp server on " . $port . $@ . "\n\n" ) && return; - - _mcast_add( $sock, '239.255.255.250' ); - setsockopt( $sock, getprotobyname('ip') || 0, _constant('IP_MULTICAST_TTL'), pack 'I', 4 ); - - my $sel = new IO::Select; - $sel->add($sock); - - my $q = <can_read(2); - last unless scalar @ready; - - $sock->recv( $data, 4096 ); - $discovery .= "-"; - - #print $data; - - if ( $data =~ m/ST: nanoleaf_aurora:light/ ) { - - #print "found" . $data; - my ($url) = $data =~ /Location:\s+(.*)/; - - #print "location " . $url; - $url =~ s/[^a-zA-Z0-9\:\.\/]*//g; - push @dev_urls, $url; - my ($devid) = $data =~ /nl\-deviceid:\s+(.*)/; - - #print "location " . $url; - $devid =~ s/[^a-zA-Z0-9\:\.\/]*//g; - push @dev_ids, $devid; - - &main::print_log( "[Aurora:" . $self->{name} . "] Found Aurora: $url, nl-deviceid: $devid" ); - - #last; - } - } - $discovery .= "< [" . scalar @dev_urls . "]\n"; - &main::print_log( "[Aurora:" . $self->{name} . "] $discovery" ); - return ( scalar @dev_urls, \@dev_urls, \@dev_ids ); -} - -sub _mcast_add { - my ( $sock, $addr ) = @_; - my $ip_mreq = inet_aton($addr) . INADDR_ANY; - - setsockopt( $sock, getprotobyname('ip') || 0, _constant('IP_ADD_MEMBERSHIP'), $ip_mreq ) || warn "Unable to add IGMP membership: $!\n"; -} - -sub _constant { - my $name = shift; - my %names = ( - IP_MULTICAST_TTL => 0, - IP_ADD_MEMBERSHIP => 1, - IP_MULTICAST_LOOP => 0, - ); - - my %constants = ( - MSWin32 => [ 10, 12 ], - cygwin => [ 3, 5 ], - darwin => [ 10, 12 ], - linux => [ 33, 35 ], - default => [ 33, 35 ], - ); - - my $index = $names{$name}; - my $ref = $constants{$^O} || $constants{default}; - return $ref->[$index]; -} - -sub update_config_data { - my ($self) = @_; - - my $write = 0; - my %parms; - - if ( ( defined $self->{url} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ne $self->{url} ) ) { - $::config_parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; - $parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; - main::print_log( "[Aurora:" . $self->{name} . "] Updating location URL ($self->{url}) in mh.ini file" ); - $write = 1; - - } - if ( ( defined $self->{token} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ne $self->{token} ) ) { - $::config_parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; - $parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; - main::print_log( "[Aurora:" . $self->{name} . "] Updating authentication token in mh.ini file" ); - $write = 1; - } - - &::write_mh_opts( \%parms ) if ($write); -} - -sub register { - my ( $self, $object, $type ) = @_; - my $keys = ""; - - #allow for multiple static entries - if ( lc $type eq 'static' ) { - if ( defined $self->{child_object}->{static} ) { - $keys = keys %{ $self->{child_object}->{static} }; - } - else { - $keys = 0; - } - $self->{child_object}->{static}->{$keys} = $object; - } - else { - $self->{child_object}->{$type} = $object; - } - $type .= " (" . $keys . ")" if ( $keys ne "" ); - &main::print_log( "[Aurora:" . $self->{name} . "] Registered $type child object" ); - -} - -sub stop_timer { - my ($self) = @_; - - if ( defined $self->{timer} ) { - $self->{timer}->stop() if ( $self->{timer}->active() ); - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] Warning, stop_timer called but timer undefined" ); - } -} - -sub print_info { - my ($self) = @_; - - main::print_log( "[Aurora:" . $self->{name} . "] Name: " . $self->{data}->{info}->{name} ); - main::print_log( "[Aurora:" . $self->{name} . "] Serial Number: " . $self->{data}->{info}->{serialNo} ); - 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} ); - 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} ); - main::print_log( "[Aurora:" . $self->{name} . "] MH Module version: " . $self->{module_version} ); - - main::print_log( "[Aurora:" . $self->{name} . "] -- Current Settings --" ); - - if ( $self->{data}->{info}->{state}->{on}->{value} ) { - main::print_log( "[Aurora:" . $self->{name} . "] State:\t ON" ); - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] State:\t OFF" ); - } - main::print_log( - "[Aurora:" . $self->{name} . "] Mode:\t " . $self->{data}->{info}->{state}->{colorMode} . " " . $self->{data}->{info}->{effects}->{select} ); - main::print_log( "[Aurora:" - . $self->{name} - . "] Brightness:\t " - . $self->{data}->{info}->{state}->{brightness}->{value} . "\t[" - . $self->{data}->{info}->{state}->{brightness}->{min} . "-" - . $self->{data}->{info}->{state}->{brightness}->{max} - . "]" ); - main::print_log( "[Aurora:" - . $self->{name} - . "] Hue:\t\t " - . $self->{data}->{info}->{state}->{hue}->{value} . "\t[" - . $self->{data}->{info}->{state}->{hue}->{min} . "-" - . $self->{data}->{info}->{state}->{hue}->{max} - . "]" ); - main::print_log( "[Aurora:" - . $self->{name} - . "] Saturation:\t " - . $self->{data}->{info}->{state}->{sat}->{value} . "\t[" - . $self->{data}->{info}->{state}->{sat}->{min} . "-" - . $self->{data}->{info}->{state}->{sat}->{max} - . "]" ); - 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} - . "]" ); - main::print_log( "[Aurora:" . $self->{name} . "] -- Active Effects --" ); - if ( defined $self->{data}->{info}->{effects}->{list} ) { - - foreach my $effect ( @{ $self->{data}->{info}->{effects}->{list} } ) { - main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); - } - } - else { - foreach my $effect ( @{ $self->{data}->{info}->{effects}->{effectsList} } ) { - main::print_log( "[Aurora:" . $self->{name} . "] - $effect" ); - } - } - main::print_log( "[Aurora:" . $self->{name} . "] -- Layout --" ); - main::print_log( "[Aurora:" - . $self->{name} - . "] Orientation:\t " - . $self->{data}->{info}->{panelLayout}->{globalOrientation}->{value} . "\t[" - . $self->{data}->{info}->{panelLayout}->{globalOrientation}->{min} . "-" - . $self->{data}->{info}->{panelLayout}->{globalOrientation}->{max} - . "]" ); - foreach my $key ( sort( keys %{ $self->{data}->{panel} } ) ) { - main::print_log( "[Aurora:" - . $self->{name} - . "] ID: " - . $key . "\tx:" - . $self->{data}->{panel}->{$key}->{x} . "\ty:" - . $self->{data}->{panel}->{$key}->{y} . "\to:" - . $self->{data}->{panel}->{$key}->{o} ); - } -} - -sub process_data { - my ($self) = @_; - - my (%state); - $state{true} = "on"; - $state{false} = "off"; - - # Main core of processing - # set state of self for state - # for any registered child selfs, update their state if - - main::print_log( "[Aurora:" . $self->{name} . "] Processing Data..." ) if ( $self->{debug} ); - - if ( ( !$self->{init_data} ) and ( defined $self->{data}->{info} ) ) { - main::print_log( "[Aurora:" . $self->{name} . "] Init: Setting startup values" ); - - foreach my $key ( keys %{$self->{data}->{info}} ) { - $self->{previous}->{info}->{$key} = $self->{data}->{info}->{$key}; - } - $self->{previous}->{panels} = $self->{data}->{panels}; - if ( defined $self->{data}->{info}->{effects}->{list} ) { #beta API - @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{list} }; - } - else { - @{ $self->{previous}->{effects}->{list} } = @{ $self->{data}->{info}->{effects}->{effectsList} }; - } - if ( defined $self->{child_object}->{effects} ) { - $self->{child_object}->{effects}->load_effects( @{ $self->{previous}->{effects}->{list} } ); - $self->{child_object}->{effects}->set( $self->{data}->{info}->{effects}->{select}, 'poll' ); - $self->print_static if ( lc $self->{data}->{info}->{effects}->{select} eq "*static*" ); - } - - if ( ( $self->{data}->{info}->{state}->{on}->{value} ) and ( $self->{data}->{info}->{state}->{brightness}->{value} != 100 ) ) { - $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); - } - else { - $self->set( $state{ $self->{data}->{info}->{state}->{on}->{value} }, 'poll' ); - } - $self->{init_data} = 1; - } - - if ( $self->{previous}->{info}->{firmwareVersion} ne $self->{data}->{info}->{firmwareVersion} ) { - main::print_log( - "[Aurora:" . $self->{name} . "] Firmware changed from $self->{previous}->{info}->{firmwareVersion} to $self->{data}->{info}->{firmwareVersion}" ); - main::print_log( "[Aurora:" . $self->{name} . "] This really isn't a regular operation. Should check Aurora to confirm" ); - $self->{previous}->{info}->{firmwareVersion} = $self->{data}->{info}->{firmwareVersion}; - } - - if ( $self->{previous}->{info}->{name} ne $self->{data}->{info}->{name} ) { - main::print_log( "[Aurora:" . $self->{name} . "] Device Name changed from $self->{previous}->{info}->{name} to $self->{data}->{info}->{name}" ); - main::print_log( "[Aurora:" . $self->{name} . "] This really isn't a regular operation. Should check Aurora to confirm" ); - $self->{previous}->{info}->{name} = $self->{data}->{info}->{name}; - } - - if ( $self->{previous}->{panels} != $self->{data}->{panels} ) { - main::print_log( "[Aurora:" . $self->{name} . "] Number of panels has changed from $self->{previous}->{panels} to $self->{data}->{panels}" ); - my $panel_added = ""; - my $panel_removed = ""; - foreach my $key ( sort( keys %{ $self->{data}->{panel} } ) ) { - $panel_added .= $key unless ( defined $self->{previous}->{panel}->{$key} ); - } - foreach my $key ( sort( keys %{ $self->{previous}->{panel} } ) ) { - $panel_removed .= $key unless ( defined $self->{data}->{panel}->{$key} ); - } - main::print_log( "[Aurora:" . $self->{name} . "] Panel ID(s) added: $panel_added" ) if ($panel_added); - main::print_log( "[Aurora:" . $self->{name} . "] Panel ID(s) removed: $panel_removed" ) if ($panel_removed); - $self->{previous}->{panels} = $self->{data}->{panels}; - $self->{previous}->{panel} = $self->{data}->{panel}; - } - - if ( $self->{previous}->{info}->{state}->{on}->{value} != $self->{data}->{info}->{state}->{on}->{value} ) { - main::print_log( "[Aurora:" - . $self->{name} - . "] State changed from $state{$self->{previous}->{info}->{state}->{on}->{value}} to $state{$self->{data}->{info}->{state}->{on}->{value}}" ) - if ( $self->{loglevel} ); - $self->{previous}->{info}->{state}->{on}->{value} = $self->{data}->{info}->{state}->{on}->{value}; - - #if on and brightness not 100 set brightness else set on or off - if ( ( $self->{data}->{info}->{state}->{on}->{value} ) and ( $self->{data}->{info}->{state}->{brightness}->{value} != 100 ) ) { - $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); - } - else { - $self->set( $state{ $self->{data}->{info}->{state}->{on}->{value} }, 'poll' ); - } - } - - if ( $self->{previous}->{info}->{state}->{brightness}->{value} != $self->{data}->{info}->{state}->{brightness}->{value} ) { - main::print_log( "[Aurora:" - . $self->{name} - . "] Brightness changed from $self->{previous}->{info}->{state}->{brightness}->{value} to $self->{data}->{info}->{state}->{brightness}->{value}" ) - if ( $self->{loglevel} ); - $self->{previous}->{info}->{state}->{brightness}->{value} = $self->{data}->{info}->{state}->{brightness}->{value}; - $self->set( $self->{data}->{info}->{state}->{brightness}->{value}, 'poll' ); - } - - if ( $self->{previous}->{info}->{state}->{colorMode} ne $self->{data}->{info}->{state}->{colorMode} ) { - main::print_log( "[Aurora:" - . $self->{name} - . "] State ColorMode changed from $self->{previous}->{info}->{state}->{colorMode} to $self->{data}->{info}->{state}->{colorMode}" ) - if ( $self->{loglevel} ); - $self->{previous}->{info}->{state}->{colorMode} = $self->{data}->{info}->{state}->{colorMode}; - } - - if ( $self->{previous}->{info}->{effects}->{select} ne $self->{data}->{info}->{effects}->{select} ) { - main::print_log( "[Aurora:" - . $self->{name} - . "] Selected Effect changed from $self->{previous}->{info}->{effects}->{select} to $self->{data}->{info}->{effects}->{select}" ) - if ( $self->{loglevel} ); - $self->{previous}->{info}->{effects}->{select} = $self->{data}->{info}->{effects}->{select}; - if ( defined $self->{child_object}->{effects} ) { - main::print_log "[Aurora:" . $self->{name} . "] Effects Child object found. Updating..." if ( $self->{loglevel} ); - $self->{child_object}->{effects}->set( $self->{data}->{info}->{effects}->{select}, 'poll' ); - } - if ( ( defined $self->{child_object}->{static} ) and ( lc $self->{data}->{info}->{effects}->{select} ne "*static*" ) ) { - main::print_log "[Aurora:" . $self->{name} . "] Static Child object(s) found. Updating..." if ( $self->{loglevel} ); - foreach my $key ( keys %{ $self->{child_object}->{static} } ) { - $self->{child_object}->{static}->{$key}->set( 'off', 'poll' ); - } - } - } - - #if a non static effect is active, set all static items to off - #if it's set to *Static* then turn off all static entries that don't match the current key - - if ( ( defined $self->{child_object}->{static} ) and ( lc $self->{data}->{info}->{effects}->{select} eq "*static*" ) ) { - foreach my $key ( keys %{ $self->{child_object}->{static} } ) { - unless ( $self->{child_object}->{static}->{$key}->get_string() eq $self->{last_static} ) { - if ( lc $self->{child_object}->{static}->{$key}->state() ne 'off' ) { - main::print_log "[Aurora:" . $self->{name} . "] Static Child object $key found. Updating..." if ( $self->{loglevel} ); - $self->{child_object}->{static}->{$key}->set( 'off', 'poll' ); - } - } - else { - if ( lc $self->{child_object}->{static}->{$key}->state() ne 'on' ) { - main::print_log "[Aurora:" . $self->{name} . "] Static Child object $key found. Updating..." if ( $self->{loglevel} ); - $self->{child_object}->{static}->{$key}->set( 'on', 'poll' ); - } - } - } - } - -} - -#------------ -# User access methods - -sub get_effect { - my ($self) = @_; - - return ( $self->{data}->{info}->{effects}->{select} ); - -} - -sub check_panelid { - my ( $self, $id ) = @_; - - my $found = 0; - $found = 1 if ( defined $self->{data}->{panel}->{$id} ); - - return $found; -} - -sub get_debug { - my ($self) = @_; - return $self->{debug}; -} - -sub set { - my ( $self, $p_state, $p_setby ) = @_; - - if ( $p_setby eq 'poll' ) { - $p_state .= "%" if ( $p_state =~ m/\d+(?!%)/ ); - main::print_log( "[Aurora:" . $self->{name} . "] DB super::set, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); - $self->SUPER::set($p_state); - $self->start_timer; - - } - else { - main::print_log( "[Aurora:" . $self->{name} . "] DB set_mode, in master set, p_state=$p_state, p_setby=$p_setby" ) if ( $self->{debug} ); - my $mode = lc $p_state; - if ( ( $mode eq "on" ) or ( $mode eq "off" ) ) { - $self->_push_JSON_data($mode); - } - elsif ( $mode =~ /^(\d+)/ ) { - my $params = $opts{brightness} . $1 . '}' . "'"; - $self->_push_JSON_data( 'brightness', $params ); - } - elsif ( $mode =~ /^([-+]\d+)/ ) { - my $params = $opts{brightness2} . $1 . '}' . "'"; - $self->_push_JSON_data( 'brightness2', $params ); - } - else { - main::print_log( "Aurora:" . $self->{name} . "] Error, unknown set state $p_state" ); - return ('0'); - } - return ('1'); - } -} - -sub set_effect { - my ( $self, $effect ) = @_; - - my $params = $opts{set_effect} . '"' . $effect . '"}' . "'"; - $self->_push_JSON_data( 'set_effect', $params ); - return ('1'); -} - -sub set_static { - my ( $self, $string, $loop ) = @_; - - my $jloop = "false"; - $jloop = "true" if ($loop); - my $params = $opts{set_static} . '"' . $string . '","loop":' . $jloop . "}}'"; - $self->{last_static} = $string; - $self->_push_JSON_data( 'set_static', $params ); - return ('1'); - -} - -sub check_static { - my ( $self, $string, $prev_effect ) = @_; - - $self->{static_check}->{string} = $string; - $self->{static_check}->{effect} = $prev_effect; - $self->_push_JSON_data('get_static'); - return ('1'); -} - -sub print_static { - my ($self) = @_; - - $self->{static_check}->{print} = 1; - $self->_push_JSON_data('get_static'); - return ('1'); -} - -sub get_static { - my ($self) = @_; - - $self->_push_JSON_data('get_static'); - return ('1'); -} - -sub print_discovery_info { - my ($self) = @_; - - my ( $devices, $dev_urls, $dev_ids ) = $self->scan_ssdp_data(); - main::print_log( "Aurora:" . $self->{name} . "] ------------------------------------------------------------------" ); - main::print_log( "Aurora:" . $self->{name} . "] Found $devices Aurora Controller" ); - for my $i ( 1 .. $devices ) { - main::print_log( "Aurora:" . $self->{name} . "] Device $i Location URL: " . @$dev_urls[ $i - 1 ] ); - main::print_log( "Aurora:" . $self->{name} . "] Device $i ID: " . @$dev_ids[ $i - 1 ] ); - } - main::print_log( "Aurora:" . $self->{name} . "] ------------------------------------------------------------------" ); - -} - -sub identify { - my ($self) = @_; - - $self->_push_JSON_data('identify'); - return ('1'); -} - -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"); - - my $voice_cmds = $self->get_voice_cmds(); - my $i = 1; - foreach my $cmd ( keys %$voice_cmds ) { - - #get object name to use as part of variable in voice command - my $object_name_v = $object_name . '_' . $i . '_v'; - $object_string .= "use vars '${object_name}_${i}_v';\n"; - - #Convert object name into readable voice command words - my $command = $object_name; - $command =~ s/^\$//; - $command =~ tr/_/ /; - - #Initialize the voice command with all of the possible device commands - $object_string .= $object_name . "_" . $i . "_v = new Voice_Cmd '$command $cmd';\n"; - - #Tie the proper routine to each voice command - $object_string .= $object_name . "_" . $i . "_v -> tie_event('" . $voice_cmds->{$cmd} . "');\n\n"; #, '$command $cmd');\n\n"; - - #Add this object to the list of Insteon Voice Commands on the Web Interface - $object_string .= ::store_object_data( $object_name_v, 'Voice_Cmd', 'Nanoleaf_Aurora', 'Controller_commands' ); - $i++; - } - - #Evaluate the resulting object generating string - package main; - eval $object_string; - print "Error in nanoleaf_aurora_item_commands: $@\n" if $@; - - package Nanoleaf_Aurora; -} - -sub get_voice_cmds { - my ($self) = @_; - my %voice_cmds = ( - 'Discover Auroras to print log' => $self->get_object_name . '->print_discovery_info', - 'Print Static string to print log' => $self->get_object_name . '->print_static', - 'Identify Aurora ' . $self->{name} . '(' . $self->get_object_name . ')' => $self->get_object_name . '->identify' - ); - - return \%voice_cmds; -} - -package Nanoleaf_Aurora_Effects; - -@Nanoleaf_Aurora_Effects::ISA = ('Generic_Item'); - -sub new { - my ( $class, $object ) = @_; - - my $self = {}; - bless $self, $class; - - $$self{master_object} = $object; - - $object->register( $self, 'effects' ); - return $self; - -} - -sub set { - my ( $self, $p_state, $p_setby ) = @_; - - if ( $p_setby eq 'poll' ) { - $self->SUPER::set($p_state); - } - else { - my $found = 0; - foreach my $eff ( @{ $$self{states} } ) { - $found = 1 if ( $p_state eq $eff ); - } - if ($found) { - $$self{master_object}->set_effect($p_state); - } - else { - main::print_log("[Aurora Effect] Error. Unknown effect state $p_state"); - } - } -} - -sub load_effects { - my ( $self, @effect_states ) = @_; - - @{ $$self{states} } = @effect_states; -} - -package Nanoleaf_Aurora_Static; - -@Nanoleaf_Aurora_Static::ISA = ('Generic_Item'); - -sub new { - my ( $class, $object, $static_string ) = @_; - - my $self = {}; - bless $self, $class; - @{ $$self{states} } = ( 'on', 'off' ); - - $$self{master_object} = $object; - $$self{loop} = 0; - $$self{string} = $static_string if ( defined $static_string ); - $object->register( $self, 'static' ); - return $self; - -} - -sub set { - my ( $self, $p_state, $p_setby ) = @_; - - if ( $p_setby eq 'poll' ) { - $self->SUPER::set($p_state); - } - else { - #if ON then backup current configuration and set the new one. - if ( lc $p_state eq 'on' ) { - $$self{previous_effect} = $$self{master_object}->get_effect(); - $$self{master_object}->set_static( $$self{string} ); - - #if OFF then check if current is same as static, and if so restore the old one. - } - elsif ( lc $p_state eq 'off' ) { - $$self{master_object}->check_static( $$self{string}, $$self{previous_effect} ); - } - else { - main::print_log("[Aurora Static] Error. Unknown set mode $p_state"); - } - } -} - -sub configure { - my ( $self, $pid, $frames, $frame, $r, $g, $b, $w, $t ) = @_; - if ( $$self{master_object}->check_panelid($pid) ) { - $$self{data}{$pid}{frames} = $frames; - $$self{data}{$pid}{$frame} = "$r $g $b $w $t"; - } - else { - main::print_log("[Aurora Static] Error. Unknown panel ID $pid"); - } -} - -sub loop { - my ( $self, $value ) = @_; - - if ( defined $value ) { - if ($value) { - $$self{loop} = 1; - } - else { - $$self{loop} = 0; - } - } - else { - return $$self{loop}; - } -} - -sub get_string { - my ($self) = @_; - - return ( $$self{string} ); -} - -package Nanoleaf_Aurora_Comm; - -@Nanoleaf_Aurora_Comm::ISA = ('Generic_Item'); - -sub new { - my ( $class, $object ) = @_; - - my $self = {}; - bless $self, $class; - - $$self{master_object} = $object; - push( @{ $$self{states} }, 'online', 'offline' ); - $object->register( $self, 'comm' ); - return $self; - -} - -sub set { - my ( $self, $p_state, $p_setby ) = @_; - - if ( $p_setby eq 'poll' ) { - $self->SUPER::set($p_state); - } -} - -1; - -# Version History -# v1.0.0 - initial module -# v1.0.1 - initial static support -# v1.0.2 - multiple auroras, brightness -# v1.0.3 - working static. turn on, overrides effect, turning off will restore previous effect -# v1.0.4 - Voice Commands -# v1.0.5 - working multi static -# v1.0.6 - better processing -# v1.0.7 - ability to specify API as an option -# v1.0.8 - initial v1.5.0 API v1 support -# v1.0.9 - use config_parms (mh.ini) instead of dedicated config file -# v1.0.10 - Updated to work with other versions of perl, typo with mh.ini From 320f38ce7e7244a0af834af87bdc45a7b824e258 Mon Sep 17 00:00:00 2001 From: hplato Date: Wed, 10 May 2017 21:22:40 -0600 Subject: [PATCH 169/209] v1.0.10 - perl version fixes and token mh.ini fix --- lib/Nanoleaf_Aurora.pm | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index 4f8f8c237..0339cd2c9 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -1,6 +1,6 @@ package Nanoleaf_Aurora; -# v1.0.9 +# v1.0.10 #if any effect is changed, by definition the static child should be set to off. #cmd data returns, need to check by command @@ -35,14 +35,14 @@ use IO::Socket::INET; # Nanoleaf_Aurora Objects # -# $aurora = new NanoLeaf_Aurora(,,,,); -# $aurora_effects = new NanoLeaf_Aurora_Effects($aurora); -# $aurora_static1 = new NanoLeaf_Aurora_Static($aurora, "effect string"); +# $aurora = new Nanoleaf_Aurora('1'); +# $aurora_effects = new Nanoleaf_Aurora_Effects($aurora); +# $aurora_static1 = new Nanoleaf_Aurora_Static($aurora, "effect string"); # $aurora_comm = new Nanoleaf_Aurora_Comm($aurora); # MH.INI settings # If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used -# instead of object defintions +# instead of object definitions # # aurora__url = # aurora__token = @@ -119,7 +119,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.0.9"; + $self->{module_version} = "v1.0.10"; $self->{ssdp_timeout} = 4000; $self->{last_static} = ""; @@ -280,7 +280,7 @@ sub process_check { $com_status = "offline"; } else { - if ( keys $data ) { + if ( keys %{$data} ) { if ( $self->{poll_process_mode} eq "info" ) { #{"name":"Nanoleaf Aurora","serialNo":"XXXXXXXX","manufacturer":"Nanoleaf","firmwareVersion":"1.4.39","model":"NL22","state":{"on":{"value":true},"brightness":{"value":100,"max":100,"min":0},"hue":{"value":255,"max":360,"min":0},"sat":{"value":68,"max":100,"min":0},"ct":{"value":4000,"max":100,"min":0},"colorMode":"effect"},"effects":{"select":"Fireplace","list":["Color Burst","Flames","Forest","Inner Peace","Nemo","Northern Lights","Romantic","Snowfall","Fireplace","Sunset"]},"panelLayout":{"layout":{"layoutData":"2 150 195 -74 129 -120 149 -74 43 -60"},"globalOrientation":{"value":294,"max":360,"min":0} @@ -391,7 +391,7 @@ sub process_check { #print Dumper $data; - if ( keys $data ) { + if ( keys %{$data} ) { #Process any returned data from a command straing if ( $self->{cmd_process_mode} eq "get_static" ) { @@ -661,7 +661,7 @@ sub update_config_data { } if ( ( defined $self->{token} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ne $self->{token} ) ) { $::config_parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; - $parms{ "aurora_" . $self->{name} . "_url" } = $self->{token}; + $parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; main::print_log( "[Aurora:" . $self->{name} . "] Updating authentication token in mh.ini file" ); $write = 1; } @@ -800,7 +800,7 @@ sub process_data { if ( ( !$self->{init_data} ) and ( defined $self->{data}->{info} ) ) { main::print_log( "[Aurora:" . $self->{name} . "] Init: Setting startup values" ); - foreach my $key ( keys $self->{data}->{info} ) { + foreach my $key ( keys %{$self->{data}->{info}} ) { $self->{previous}->{info}->{$key} = $self->{data}->{info}->{$key}; } $self->{previous}->{panels} = $self->{data}->{panels}; @@ -1254,4 +1254,4 @@ sub set { # v1.0.7 - ability to specify API as an option # v1.0.8 - initial v1.5.0 API v1 support # v1.0.9 - use config_parms (mh.ini) instead of dedicated config file - +# v1.0.10 - Updated to work with other versions of perl, typo with mh.ini From 78e4b1134eae6e0ae3a6ca1ff09e234123b31ae9 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 26 May 2017 14:41:50 -0600 Subject: [PATCH 170/209] v1.0.12 - logger, get_effects, perl version support --- lib/Nanoleaf_Aurora.pm | 68 +++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/lib/Nanoleaf_Aurora.pm b/lib/Nanoleaf_Aurora.pm index 4f8f8c237..2c724ef4d 100644 --- a/lib/Nanoleaf_Aurora.pm +++ b/lib/Nanoleaf_Aurora.pm @@ -1,6 +1,6 @@ package Nanoleaf_Aurora; -# v1.0.9 +# v1.0.12 #if any effect is changed, by definition the static child should be set to off. #cmd data returns, need to check by command @@ -35,14 +35,14 @@ use IO::Socket::INET; # Nanoleaf_Aurora Objects # -# $aurora = new NanoLeaf_Aurora(,,,,); -# $aurora_effects = new NanoLeaf_Aurora_Effects($aurora); -# $aurora_static1 = new NanoLeaf_Aurora_Static($aurora, "effect string"); +# $aurora = new Nanoleaf_Aurora('1'); +# $aurora_effects = new Nanoleaf_Aurora_Effects($aurora); +# $aurora_static1 = new Nanoleaf_Aurora_Static($aurora, "effect string"); # $aurora_comm = new Nanoleaf_Aurora_Comm($aurora); # MH.INI settings # If the token is auto generated, it will be written to the mh.ini. MH.INI settings can be used -# instead of object defintions +# instead of object definitions # # aurora__url = # aurora__token = @@ -104,10 +104,10 @@ our %active_auroras = (); sub new { my ( $class, $id, $url, $token, $poll, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $self->{name} = "1"; - $self->{name} = $id if ($id); + $self->{name} = $id if ( ( defined $id ) and ($id) ); $self->{data} = undef; $self->{child_object} = undef; @@ -119,7 +119,7 @@ sub new { $self->{updating} = 0; $self->{data}->{retry} = 0; $self->{status} = ""; - $self->{module_version} = "v1.0.9"; + $self->{module_version} = "v1.0.12"; $self->{ssdp_timeout} = 4000; $self->{last_static} = ""; @@ -280,7 +280,7 @@ sub process_check { $com_status = "offline"; } else { - if ( keys $data ) { + if ( keys %{$data} ) { if ( $self->{poll_process_mode} eq "info" ) { #{"name":"Nanoleaf Aurora","serialNo":"XXXXXXXX","manufacturer":"Nanoleaf","firmwareVersion":"1.4.39","model":"NL22","state":{"on":{"value":true},"brightness":{"value":100,"max":100,"min":0},"hue":{"value":255,"max":360,"min":0},"sat":{"value":68,"max":100,"min":0},"ct":{"value":4000,"max":100,"min":0},"colorMode":"effect"},"effects":{"select":"Fireplace","list":["Color Burst","Flames","Forest","Inner Peace","Nemo","Northern Lights","Romantic","Snowfall","Fireplace","Sunset"]},"panelLayout":{"layout":{"layoutData":"2 150 195 -74 129 -120 149 -74 43 -60"},"globalOrientation":{"value":294,"max":360,"min":0} @@ -391,7 +391,7 @@ sub process_check { #print Dumper $data; - if ( keys $data ) { + if ( keys %{$data} ) { #Process any returned data from a command straing if ( $self->{cmd_process_mode} eq "get_static" ) { @@ -652,16 +652,24 @@ sub update_config_data { my $write = 0; my %parms; - if ( ( defined $self->{url} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ne $self->{url} ) ) { + if ( ( defined $self->{url} ) + and ( ( !defined $::config_parms{ "aurora_" . $self->{name} . "_url" } ) or ( $::config_parms{ "aurora_" . $self->{name} . "_url" } ne $self->{url} ) ) + ) + { $::config_parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; $parms{ "aurora_" . $self->{name} . "_url" } = $self->{url}; main::print_log( "[Aurora:" . $self->{name} . "] Updating location URL ($self->{url}) in mh.ini file" ); $write = 1; } - if ( ( defined $self->{token} ) and ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ne $self->{token} ) ) { + if ( + ( defined $self->{token} ) + and ( ( !defined $::config_parms{ "aurora_" . $self->{name} . "_token" } ) + or ( $::config_parms{ "aurora_" . $self->{name} . "_token" } ne $self->{token} ) ) + ) + { $::config_parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; - $parms{ "aurora_" . $self->{name} . "_url" } = $self->{token}; + $parms{ "aurora_" . $self->{name} . "_token" } = $self->{token}; main::print_log( "[Aurora:" . $self->{name} . "] Updating authentication token in mh.ini file" ); $write = 1; } @@ -800,7 +808,7 @@ sub process_data { if ( ( !$self->{init_data} ) and ( defined $self->{data}->{info} ) ) { main::print_log( "[Aurora:" . $self->{name} . "] Init: Setting startup values" ); - foreach my $key ( keys $self->{data}->{info} ) { + foreach my $key ( keys %{ $self->{data}->{info} } ) { $self->{previous}->{info}->{$key} = $self->{data}->{info}->{$key}; } $self->{previous}->{panels} = $self->{data}->{panels}; @@ -937,6 +945,19 @@ sub get_effect { } +sub get_effects { + my ($self) = @_; + my @effect_array = (); + + if ( defined $self->{data}->{info}->{effects}->{list} ) { #beta structure + @effect_array = @{ $self->{data}->{info}->{effects}->{list} }; + } + else { + @effect_array = @{ $self->{data}->{info}->{effects}->{effectsList} }; + } + return @effect_array; +} + sub check_panelid { my ( $self, $id ) = @_; @@ -1105,7 +1126,7 @@ package Nanoleaf_Aurora_Effects; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1135,12 +1156,21 @@ sub set { } } +#doesn't do anything yet sub load_effects { my ( $self, @effect_states ) = @_; @{ $$self{states} } = @effect_states; } +sub get_effects { + my ($self) = @_; + my @effects = $$self{master_object}->get_effects(); + + return @effects; + +} + package Nanoleaf_Aurora_Static; @Nanoleaf_Aurora_Static::ISA = ('Generic_Item'); @@ -1148,7 +1178,7 @@ package Nanoleaf_Aurora_Static; sub new { my ( $class, $object, $static_string ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; @{ $$self{states} } = ( 'on', 'off' ); @@ -1223,7 +1253,7 @@ package Nanoleaf_Aurora_Comm; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1254,4 +1284,6 @@ sub set { # v1.0.7 - ability to specify API as an option # v1.0.8 - initial v1.5.0 API v1 support # v1.0.9 - use config_parms (mh.ini) instead of dedicated config file - +# v1.0.10 - Updated to work with other versions of perl, typo with mh.ini +# v1.0.11 - cosmetic fixes for undefined variables +# v1.0.12 - get_effects method to get array of available effects From 2cdf5e9c9eaecc6e5966c996fb1c924b1123ea36 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 26 May 2017 14:46:08 -0600 Subject: [PATCH 171/209] added object logger support --- lib/OpenSprinkler.pm | 63 ++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/lib/OpenSprinkler.pm b/lib/OpenSprinkler.pm index 592f38185..fc43ecba3 100644 --- a/lib/OpenSprinkler.pm +++ b/lib/OpenSprinkler.pm @@ -58,12 +58,13 @@ use Data::Dumper; # TODO # - be nice to pull runtimes and store it into a dbm file, or maybe RRD? # - disabling the opensprinkler doesn't turn off any running stations. -# - the architecture can create pauses -- long term adopt Kevin's approach w/ Nests +# - the architecture can create pauses -- long term adopt Venstar process_item model # - no ability to print logs. The built-in web interface does this well already # v1.0 release # v1.1 (May 2016) - added ability to change program runtimes # v1.11 (May 2016) - removed JSON::XS dependancy +# v1.11.1 (May 2017) - changed to support logger @OpenSprinkler::ISA = ('Generic_Item'); @@ -99,7 +100,7 @@ $result{9} = "unknown error"; sub new { my ( $class, $host, $pwd, $poll ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $self->{data} = undef; $self->{child_object} = undef; @@ -150,8 +151,7 @@ sub _init { for my $index ( 0 .. $#{ $stations->{snames} } ) { #print "$index: $stations->{snames}[$index]\n"; - $self->{data}->{stations}->[$index]->{name} = - $stations->{snames}[$index]; + $self->{data}->{stations}->[$index]->{name} = $stations->{snames}[$index]; } # Check to see if station is disabled, Bitwise operation @@ -160,8 +160,7 @@ sub _init { for my $bit ( 0 .. 7 ) { my $station_id = ( ( $stn_dis * 8 ) + $bit ); my $disabled = substr $bin, ( 7 - $bit ), 1; - $self->{data}->{stations}->[$station_id]->{status} = - ( $disabled == 0 ) ? "enabled" : "disabled"; + $self->{data}->{stations}->[$station_id]->{status} = ( $disabled == 0 ) ? "enabled" : "disabled"; } } @@ -214,20 +213,16 @@ sub poll { $self->{data}->{loc} = $vars->{loc}; $self->{data}->{options} = $options; $self->{data}->{vars} = $vars; - $self->{data}->{info}->{state} = - ( $vars->{en} == 0 ) ? "disabled" : "enabled"; - $self->{data}->{info}->{waterlevel} = $options->{wl}; - $self->{data}->{info}->{adjustment_method} = - ( $options->{uwt} == 0 ) ? "manual" : "zimmerman"; - $self->{data}->{info}->{rain_sensor_status} = - ( $vars->{rs} == 0 ) ? "off" : "on"; - $self->{data}->{info}->{sunrise} = $vars->{sunrise}; - $self->{data}->{info}->{sunset} = $vars->{sunset}; + $self->{data}->{info}->{state} = ( $vars->{en} == 0 ) ? "disabled" : "enabled"; + $self->{data}->{info}->{waterlevel} = $options->{wl}; + $self->{data}->{info}->{adjustment_method} = ( $options->{uwt} == 0 ) ? "manual" : "zimmerman"; + $self->{data}->{info}->{rain_sensor_status} = ( $vars->{rs} == 0 ) ? "off" : "on"; + $self->{data}->{info}->{sunrise} = $vars->{sunrise}; + $self->{data}->{info}->{sunset} = $vars->{sunset}; for my $index ( 0 .. $#{ $stations->{sn} } ) { print "$index: $stations->{sn}[$index]\n" if ( $self->{debug} ); - $self->{data}->{stations}->[$index]->{state} = - ( $stations->{sn}[$index] == 0 ) ? "off" : "on"; + $self->{data}->{stations}->[$index]->{state} = ( $stations->{sn}[$index] == 0 ) ? "off" : "on"; } for my $index ( 0 .. $#{ $programs->{pd} } ) { my $name = $programs->{pd}[$index][5]; @@ -237,9 +232,8 @@ sub poll { ( $programs->{pd}[$index][0] % 2 == 1 ) ? "enabled" : "disabled"; #if number is odd, then bit 0 set and disabled - $self->{data}->{programs}->{$name}->{flag} = - $programs->{pd}[$index][0]; - $self->{data}->{programs}->{$name}->{pid} = $index; + $self->{data}->{programs}->{$name}->{flag} = $programs->{pd}[$index][0]; + $self->{data}->{programs}->{$name}->{pid} = $index; $self->{data}->{programs}->{$name}->{data} = "$programs->{pd}[$index][1],$programs->{pd}[$index][2],[" . join( ",", @{ $programs->{pd}[$index][3] } ) . "],[" @@ -509,8 +503,7 @@ sub process_data { main::print_log( "[OpenSprinkler] Station $index $self->{data}->{stations}->[$index]->{name} changed from $previous to $self->{data}->{stations}->[$index]->{state}" ) if ( $self->{loglevel} ); - $self->{previous}->{data}->{stations}->[$index]->{state} = - $self->{data}->{stations}->[$index]->{state}; + $self->{previous}->{data}->{stations}->[$index]->{state} = $self->{data}->{stations}->[$index]->{state}; if ( defined $self->{child_object}->{station}->{$index} ) { main::print_log "Child object found. Updating..." if ( $self->{loglevel} ); @@ -525,8 +518,7 @@ sub process_data { if ( defined $self->{previous}->{data}->{programs}->{$key}->{status} ); if ( $previous ne $self->{data}->{programs}->{$key}->{status} ) { main::print_log("[OpenSprinkler] Program $key changed from $previous to $self->{data}->{programs}->{$key}->{status}") if ( $self->{loglevel} ); - $self->{previous}->{data}->{programs}->{$key}->{status} = - $self->{data}->{programs}->{$key}->{status}; + $self->{previous}->{data}->{programs}->{$key}->{status} = $self->{data}->{programs}->{$key}->{status}; if ( defined $self->{child_object}->{program}->{$key} ) { main::print_log "Child object found. Updating..." if ( $self->{loglevel} ); @@ -544,8 +536,7 @@ sub process_data { if ( $self->{previous}->{info}->{waterlevel} != $self->{data}->{info}->{waterlevel} ) { main::print_log("[OpenSprinkler] Waterlevel changed from $self->{previous}->{info}->{waterlevel} to $self->{data}->{info}->{waterlevel}") if ( $self->{loglevel} ); - $self->{previous}->{info}->{waterlevel} = - $self->{data}->{info}->{waterlevel}; + $self->{previous}->{info}->{waterlevel} = $self->{data}->{info}->{waterlevel}; if ( defined $self->{child_object}->{waterlevel} ) { main::print_log "Child object found. Updating..." if ( $self->{loglevel} ); @@ -557,8 +548,7 @@ sub process_data { main::print_log( "[OpenSprinkler] Rain Sensor changed from $self->{previous}->{info}->{rain_sensor_status} to $self->{data}->{info}->{rain_sensor_status}") if ( $self->{loglevel} ); - $self->{previous}->{info}->{rain_sensor_status} = - $self->{data}->{info}->{rain_sensor_status}; + $self->{previous}->{info}->{rain_sensor_status} = $self->{data}->{info}->{rain_sensor_status}; if ( defined $self->{child_object}->{rain_sensor_status} ) { main::print_log "Child object found. Updating..." if ( $self->{loglevel} ); @@ -582,8 +572,7 @@ sub process_data { main::print_log( "[OpenSprinkler] Adjustment Method changed from $self->{previous}->{info}->{adjustment_method} to $self->{data}->{info}->{adjustment_method}") if ( $self->{loglevel} ); - $self->{previous}->{info}->{adjustment_method} = - $self->{data}->{info}->{adjustment_method}; + $self->{previous}->{info}->{adjustment_method} = $self->{data}->{info}->{adjustment_method}; } } @@ -680,9 +669,9 @@ sub set_program_data { #intervals can always come later. return unless ( defined $self->{data}->{programs}->{$name} ); - $days =~ s/\s//g; #remove whitespace + $days =~ s/\s//g; #remove whitespace $start =~ s/\s//g; - $run =~ s/\s//g; + $run =~ s/\s//g; #set the program to schedule weekday , fixed time my $bin = sprintf "%08b", $self->{data}->{programs}->{$name}->{flag}; @@ -885,7 +874,7 @@ package OpenSprinkler_Station; sub new { my ( $class, $object, $number, $on_timeout ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -921,7 +910,7 @@ package OpenSprinkler_Program; sub new { my ( $class, $object, $name, $maxlimit ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -977,7 +966,7 @@ package OpenSprinkler_Comm; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1003,7 +992,7 @@ package OpenSprinkler_Waterlevel; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1030,7 +1019,7 @@ package OpenSprinkler_Rainstatus; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; From 43ea3eb10a139232b1b79c276c21520ad3bb0502 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 26 May 2017 14:46:40 -0600 Subject: [PATCH 172/209] added object logger support --- lib/raZberry.pm | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/raZberry.pm b/lib/raZberry.pm index a737d4711..ba293cf9c 100755 --- a/lib/raZberry.pm +++ b/lib/raZberry.pm @@ -1,5 +1,5 @@ -=head1 B v2.0.1 +=head1 B v2.0.2 =head2 SYNOPSIS @@ -112,6 +112,9 @@ http calls can cause pauses. There are a few possible options around this; =head2 CHANGELOG +v2.0.2 +- add generic items for logger + v2.0.1 - added full poll for getting battery data @@ -193,9 +196,9 @@ $rest{controller} = "Data/*"; sub new { my ( $class, $addr, $poll, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; - &main::print_log("[raZberry]: v2.0.1 Controller Initializing..."); + &main::print_log("[raZberry]: v2.0.2 Controller Initializing..."); $self->{data} = undef; $self->{child_object} = undef; $self->{config}->{poll_seconds} = 5; @@ -694,7 +697,7 @@ package raZberry_dimmer; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; push( @{ $$self{states} }, 'off', 'low', 'med', 'high', 'on', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%' ); @@ -792,7 +795,7 @@ package raZberry_switch; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; push( @{ $$self{states} }, 'off', 'on', ); @@ -879,7 +882,7 @@ package raZberry_blind; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1051,7 +1054,7 @@ package raZberry_lock; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; push( @{ $$self{states} }, 'locked', 'unlocked' ); @@ -1281,7 +1284,7 @@ sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1404,7 +1407,7 @@ package raZberry_temp_sensor; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -1456,7 +1459,7 @@ package raZberry_binary_sensor; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; #push( @{ $$self{states} }, 'on', 'off'); I'm not sure we should set the states here, since it's not a controlable item? @@ -1537,7 +1540,7 @@ package raZberry_battery; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; push( @{ $$self{states} }, 'locked', 'unlocked' ); @@ -1610,7 +1613,7 @@ package raZberry_voltage; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; #ZWayVDev_zway_x-0-50-0 - Power Meter kWh @@ -1685,7 +1688,7 @@ package raZberry_generic; sub new { my ( $class, $object, $devid, $options ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; From 2999a21def26f0b63f495fe338803a63da6f4a9c Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 26 May 2017 14:47:14 -0600 Subject: [PATCH 173/209] added object logger support --- lib/Venstar_Colortouch.pm | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/Venstar_Colortouch.pm b/lib/Venstar_Colortouch.pm index f1e125229..e50f76ab8 100644 --- a/lib/Venstar_Colortouch.pm +++ b/lib/Venstar_Colortouch.pm @@ -1,6 +1,6 @@ package Venstar_Colortouch; -# v2.1 +# v2.1.1 #added in https support and don't retry commands that have a valid error reason code. Only retry if the device doesn't respond. (ie error 500) @@ -41,6 +41,7 @@ use Data::Dumper; # v1.4.1 - API v5, working schedule, humidity setpoints # v2.0 - Background process # v2.1 - fixed up some problems reconnecting to stat +# v2.1.1 - added in logger ability # Notes # - State can only be set by stat. Set mode will change the mode. @@ -78,7 +79,7 @@ $rest{settings} = "settings"; sub new { my ( $class, $host, $poll, $options ) = @_; $options = "" unless ( defined $options ); - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $self->{data} = undef; $self->{api_ver} = 0; @@ -2020,7 +2021,7 @@ package Venstar_Colortouch_Temp; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2050,7 +2051,7 @@ package Venstar_Colortouch_Fan; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; push( @{ $$self{states} }, 'off', 'on' ); $self->{current_mode} = ""; @@ -2099,7 +2100,7 @@ package Venstar_Colortouch_Humidity; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2125,7 +2126,7 @@ package Venstar_Colortouch_Humidity_sp; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2160,7 +2161,7 @@ package Venstar_Colortouch_Schedule; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2194,7 +2195,7 @@ package Venstar_Colortouch_Comm; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2219,7 +2220,7 @@ package Venstar_Colortouch_Mode; sub new { my ( $class, $object ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2259,7 +2260,7 @@ package Venstar_Colortouch_Heat_sp; sub new { my ( $class, $object, $scale ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; @@ -2311,7 +2312,7 @@ package Venstar_Colortouch_Cool_sp; sub new { my ( $class, $object, $scale ) = @_; - my $self = {}; + my $self = new Generic_Item(); bless $self, $class; $$self{master_object} = $object; From 9206af629d8f06c10545c732f3df6cf9cdb30cc4 Mon Sep 17 00:00:00 2001 From: "H.Plato" Date: Mon, 29 May 2017 16:03:19 -0600 Subject: [PATCH 174/209] v1.4.400 - improved global vars, filtered bad JSON URLS --- lib/ia7_utilities.pl | 26 +++++++++++--- lib/json_server.pl | 66 +++++++++++++++++++++++++++++++---- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 30 ++++++++++++---- 4 files changed, 107 insertions(+), 17 deletions(-) diff --git a/lib/ia7_utilities.pl b/lib/ia7_utilities.pl index c880ceab9..4cf4a415a 100644 --- a/lib/ia7_utilities.pl +++ b/lib/ia7_utilities.pl @@ -24,7 +24,7 @@ sub main::ia7_update_schedule { sub ia7_update_collection { &main::print_log("[IA7_Collection_Updater] : Starting"); - my $ia7_coll_current_ver = 1.2; + my $ia7_coll_current_ver = 1.3; my @collection_files = ( "$main::Pgm_Root/data/web/collections.json", @@ -36,6 +36,7 @@ sub ia7_update_collection { &main::print_log("[IA7_Collection_Updater] : Reviewing $file to current version $ia7_coll_current_ver"); my $json_data; my $file_data; + my $updated; eval { $file_data = &main::file_read($file); $json_data = decode_json($file_data); #HP, wrap this in eval to prevent MH crashes @@ -45,11 +46,11 @@ sub ia7_update_collection { &main::print_log("[IA7_Collection_Updater] : WARNING: decode_json failed for $file. Please check this file!"); } else { - my $updated = 0; + $updated = 0; if ( ( !defined $json_data->{meta}->{version} ) or ( $json_data->{meta}->{version} < 1.2 ) ) - { #IA7 v1.2 required change + { #IA7 v1.2 required change $json_data->{700}->{user} = '$Authorized' unless ( defined $json_data->{700}->{user} ); my $found = 0; @@ -59,9 +60,26 @@ sub ia7_update_collection { push( @{ $json_data->{500}->{children} }, 700 ) unless ($found); $json_data->{meta}->{version} = "1.2"; - &main::print_log("[IA7_Collection_Updater] : Updating $file to version 1.2"); + &main::print_log("[IA7_Collection_Updater] : Updating $file to version 1.2 (MH 4.2 IA7 v1.2.100 support)"); $updated = 1; } + if ( $json_data->{meta}->{version} < 1.3 ) { + #IA7 v1.4 required change + #convert back to a file so we can globally change links + my $file_data2 = to_json( $json_data, { utf8 => 1, pretty => 1 } ); + $file_data2 =~ s/\"link\"[\s+]:[\s+]\"\/ia7\/\#path=\/vars\"/\"link\" : \"\/ia7\/\#path\=\/vars_global\"/g; + $file_data2 =~ s/\"link\"[\s+]:[\s+]\"\/ia7\/\#path=\/vars\/Save\"/\"link\" : \"\/ia7\/\#path\=\/vars_save\"/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.3 update."); + } else { + $json_data->{meta}->{version} = "1.3"; + &main::print_log("[IA7_Collection_Updater] : Updating $file to version 1.3 (MH 4.3 IA7 v1.4.400 support)"); + $updated = 1; + } + } 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 cafd771df..df679bd72 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -58,6 +58,7 @@ sub json { $request_type = $HTTP_REQ_TYPE; } my %arg_hash = %HTTP_ARGV; + if ( $arguments ne '' ) { %arg_hash = (); @@ -93,13 +94,19 @@ sub json { # Split Path into Array $path_str =~ s/^\/json//i; # Remove leading 'json' path $path_str =~ s/^\/|\/$//g; # Remove leadin trailing slash. + my @path = split( '/', $path_str ); - if ( lc($request_type) eq "get" ) { - return json_get( $request_type, \@path, \%args, $body ); - } - elsif ( lc($request_type) eq "put" ) { - json_put( $request_type, \@path, \%args, $body ); + if ($path_str eq '') { + print_log "Json_Server.pl: WARNING: null json request received. Ignoring..."; + } else { + + if ( lc($request_type) eq "get" ) { + return json_get( $request_type, \@path, \%args, $body ); + } + elsif ( lc($request_type) eq "put" ) { + json_put( $request_type, \@path, \%args, $body ); + } } } @@ -162,7 +169,7 @@ sub json_get { } } $fields{all} = 1 unless %fields; - + # List defined collections if ( $path[0] eq 'collections' || $path[0] eq '' ) { my $collection_file = "$Pgm_Root/data/web/collections.json"; @@ -600,6 +607,7 @@ sub json_get { next if $key eq 'INC'; next if $key eq 'ISA'; next if $key eq 'SIG'; + next if $key eq 'User_Code'; my $iref = ${$ref}{$key}; # this is for constants @@ -608,6 +616,52 @@ sub json_get { } $json_data{vars} = \%json_vars; } + + if ( $path[0] eq 'vars_global' || $path[0] eq '' ) { + my %json_vars_global; + for my $key ( sort keys %main:: ) { + + # Assume all the global vars we care about are $Ab... + next if $key !~ /^[A-Z][a-z]/ or $key =~ /\:/; + next if $key eq 'Save' or $key eq 'Tk_objects'; # Covered elsewhere + next if $key eq 'Socket_Ports'; + next if $key eq 'User_Code'; + + my $glob = $main::{$key}; + if ( ${$glob} ) { + my $value = ${$glob}; + next if $value =~ /HASH/; # Skip object pointers + next if $key eq 'Password'; + $value =~ s/[\r\n]+$//; + #print "db: [$key -> $value]\n"; + $json_vars_global{$key} = $value; + } + elsif ( %{$glob} ) { + for my $key2 ( sort keys %{$glob} ) { + my $value = ${$glob}{$key2};# . "\n"; + $value = '' unless $value; # Avoid -w uninitialized value msg + $value =~ s/[\r\n]+$//; + next if $value =~ /HASH\(/; # Skip object pointers + next if $value =~ /ARRAY\(/; + #print "db: [$key\{$key2\} -> $value]\n"; + $json_vars_global{"$key\{$key2\}"} = $value; + } + } + } + $json_data{vars_global} = \%json_vars_global; + } + + if ( $path[0] eq 'vars_save' || $path[0] eq '' ) { + my %json_vars_save; + + for my $key ( sort keys %Save ) { + my $value = ( $Save{$key} ) ? $Save{$key} : ''; + $value =~ s/[\r\n]+$//; + $json_vars_save{$key} = $value; + } + + $json_data{vars_save} = \%json_vars_save; + } if ( $path[0] eq 'notifications' ) { diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 1bc9ae478..bdeed6330 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the v4 web prototype, updates by H.Plato. IA7 v1.4.350 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the IA7 web prototype, updates by H.Plato. IA7 v1.4.400 Font Awesome by Dave Gandy - http://fontawesome.io

  • diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index f9da397c9..9a70fccf3 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -200,7 +200,7 @@ function changePage (){ if (path.indexOf('objects') === 0){ loadList(); } - else if (path.indexOf('vars') === 0){ + else if ((path.indexOf('vars') === 0) || (path.indexOf('vars_global') === 0) || (path.indexOf('vars_save') === 0)){ loadVars(); } else if (path.indexOf('prefs') === 0){ @@ -465,18 +465,36 @@ function loadVars (){ //variables list dataType: "json", success: function( json ) { JSONStore(json); + var table = false; + if (json.meta.path[0] != 'vars') table = true; var list_output = ""; var keys = []; for (var key in json.data) { keys.push(key); } keys.sort (); - for (var i = 0; i < keys.length; i++){ - var value = variableList(json.data[keys[i]]); - var name = keys[i]; - var list_html = "
    • " + name + ":" + value+"
    "; - list_output += (list_html); + + if (table) { + list_output = "
    $type\n"; $html .= "(back to top)
    "; + list_output += ""; + list_output += ""; + + for (var i = 0; i < keys.length; i++){ + var value = json.data[keys[i]]; + var name = keys[i]; + var list_html + list_output += ""; + } + list_output += "
    VariableValue
    "+name+""+value+"
    "; + } else { + for (var i = 0; i < keys.length; i++){ + var value = variableList(json.data[keys[i]]); + var name = keys[i]; + var list_html + list_output += "
    • " + name + ":" + value+"
    "; + } } + //list_output += (list_html); //Print list output if exists; if (list_output !== ""){ From 0b47fb933606690152284a12e54cea14c18da5be Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 16 Jun 2017 15:55:13 -0600 Subject: [PATCH 175/209] Added 35 Datasources to stock MH RRD --- lib/site/RRD/Simple.pm | 2155 ++++++++++++++++++++++++++++++++++++++ lib/upgrade_utilities.pl | 68 ++ 2 files changed, 2223 insertions(+) create mode 100644 lib/site/RRD/Simple.pm create mode 100644 lib/upgrade_utilities.pl diff --git a/lib/site/RRD/Simple.pm b/lib/site/RRD/Simple.pm new file mode 100644 index 000000000..cf3375e9d --- /dev/null +++ b/lib/site/RRD/Simple.pm @@ -0,0 +1,2155 @@ +############################################################ +# +# $Id: Simple.pm 1100 2008-01-24 17:39:35Z nicolaw $ +# RRD::Simple - Simple interface to create and store data in RRD files +# +# Copyright 2005,2006,2007,2008 Nicola Worthington +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################ + +package RRD::Simple; + +# vim:ts=8:sw=8:tw=78 + +use strict; +require Exporter; +use RRDs; +use POSIX qw(strftime); # Used for strftime in graph() method +use Carp qw(croak cluck confess carp); +use File::Spec qw(); # catfile catdir updir path rootdir tmpdir +use File::Basename qw(fileparse dirname basename); + +use vars qw($VERSION $DEBUG $DEFAULT_DSTYPE + @EXPORT @EXPORT_OK %EXPORT_TAGS @ISA); + +$VERSION = '1.44' || sprintf( '%d', q$Revision: 1100 $ =~ /(\d+)/g ); + +@ISA = qw(Exporter); +@EXPORT = qw(); +@EXPORT_OK = qw(create update last_update graph info rename_source + add_source sources retention_period last_values + heartbeat); + +# delete_source minimum maximum +%EXPORT_TAGS = ( all => \@EXPORT_OK ); + +$DEBUG ||= $ENV{DEBUG} ? 1 : 0; +$DEFAULT_DSTYPE ||= exists $ENV{DEFAULT_DSTYPE} ? $ENV{DEFAULT_DSTYPE} : 'GAUGE'; + +my $objstore = {}; + +# +# Methods +# + +# Create a new object +sub new { + TRACE(">>> new()"); + ref( my $class = shift ) && croak 'Class name required'; + croak 'Odd number of elements passed when even was expected' if @_ % 2; + + # Conjure up an invisible object + my $self = bless \( my $dummy ), $class; + $objstore->{ _refaddr($self) } = {@_}; + my $stor = $objstore->{ _refaddr($self) }; + + #my $self = { @_ }; + + # - Added "file" support in 1.42 - see sub _guess_filename. + # - Added "on_missing_ds"/"on_missing_source" support in 1.44 + # - Added "tmpdir" support in 1.44 + my @validkeys = qw(rrdtool cf default_dstype default_dst tmpdir + file on_missing_ds on_missing_source); + my $validkeys = join( '|', @validkeys ); + + cluck( 'Unrecognised parameters passed: ' . join( ', ', grep( !/^$validkeys$/, keys %{$stor} ) ) ) + if ( grep( !/^$validkeys$/, keys %{$stor} ) && $^W ); + + $stor->{rrdtool} = _find_binary( exists $stor->{rrdtool} ? $stor->{rrdtool} : 'rrdtool' ); + + # Check that "default_dstype" isn't complete rubbish (validation from v1.44+) + # GAUGE | COUNTER | DERIVE | ABSOLUTE | COMPUTE + # http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html + $stor->{default_dstype} ||= $stor->{default_dst}; + croak "Invalid value passed in parameter default_dstype; '$stor->{default_dstype}'" + if defined $stor->{default_dstype} + && $stor->{default_dstype} !~ /^(GAUGE|COUNTER|DERIVE|ABSOLUTE|COMPUTE|[A-Z]{1,10})$/i; + + # Check that "on_missing_ds" isn't complete rubbish. + # Added "on_missing_ds"/"on_missing_source" support in 1.44 + $stor->{on_missing_ds} ||= $stor->{on_missing_source}; + if ( defined $stor->{on_missing_ds} ) { + $stor->{on_missing_ds} = lc( $stor->{on_missing_ds} ); + croak "Invalid value passed in parameter on_missing_ds; '$stor->{on_missing_ds}'" + if $stor->{on_missing_ds} !~ /^\s*(add|ignore|die|croak)\s*$/i; + } + $stor->{on_missing_ds} ||= 'add'; # default to add + + #$stor->{cf} ||= [ qw(AVERAGE MIN MAX LAST) ]; + # By default, now only create RRAs for AVERAGE and MAX, like + # mrtg v2.13.2. This is to save disk space and processing time + # during updates etc. + $stor->{cf} ||= [qw(AVERAGE MAX)]; + $stor->{cf} = [ $stor->{cf} ] if !ref( $stor->{cf} ); + + DUMP( $class, $self ); + DUMP( '$stor', $stor ); + return $self; +} + +# Create a new RRD file +sub create { + TRACE(">>> create()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{ _refaddr($self) }; + + # + # + # + + # Grab or guess the filename + my $rrdfile = $stor->{file}; + + # Odd number of values and first is not a valid scheme + # then the first value is likely an RRD file name. + if ( @_ % 2 && !_valid_scheme( $_[0] ) ) { + $rrdfile = shift; + + # Even number of values and the second value is a valid + # scheme then the first value is likely an RRD file name. + } + elsif ( !( @_ % 2 ) && _valid_scheme( $_[1] ) ) { + $rrdfile = shift; + + # If we still don't have an RRD file name then try and + # guess what it is + } + elsif ( !defined $rrdfile ) { + $rrdfile = _guess_filename($stor); + } + + # + # + # + + # Barf if the rrd file already exists + croak "RRD file '$rrdfile' already exists" if -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + # We've been given a scheme specifier + # Until v1.32 'year' was the default. As of v1.33 'mrtg' + # is the new default scheme. + #my $scheme = 'year'; + my $scheme = 'mrtg'; + if ( @_ % 2 && _valid_scheme( $_[0] ) ) { + $scheme = _valid_scheme( $_[0] ); + shift @_; + } + TRACE("Using scheme: $scheme"); + + croak 'Odd number of elements passed when even was expected' if @_ % 2; + my %ds = @_; + DUMP( '%ds', \%ds ); + + my $rrdDef = _rrd_def($scheme); + my @def = ( '-b', time - _seconds_in( $scheme, 120 ) ); + push @def, '-s', ( $rrdDef->{step} || 300 ); + + # Add data sources + for my $ds ( sort keys %ds ) { + $ds =~ s/[^a-zA-Z0-9_-]//g; + push @def, sprintf( 'DS:%s:%s:%s:%s:%s', substr( $ds, 0, 19 ), uc( $ds{$ds} ), ( $rrdDef->{heartbeat} || 600 ), 'U', 'U' ); + } + + # Add RRA definitions + my %cf; + for my $cf ( @{ $stor->{cf} } ) { + $cf{$cf} = $rrdDef->{rra}; + } + for my $cf ( sort keys %cf ) { + for my $rra ( @{ $cf{$cf} } ) { + push @def, sprintf( 'RRA:%s:%s:%s:%s', $cf, 0.5, $rra->{step}, $rra->{rows} ); + } + } + + DUMP( '@def', \@def ); + + # Pass to RRDs for execution + my @rtn = RRDs::create( $rrdfile, @def ); + my $error = RRDs::error(); + croak($error) if $error; + DUMP( 'RRDs::info', RRDs::info($rrdfile) ); + return wantarray ? @rtn : \@rtn; +} + +# Update an RRD file with some data values +sub update { + TRACE(">>> update()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{ _refaddr($self) }; + + # + # + # + + # Grab or guess the filename + my $rrdfile = $stor->{file}; + + # Odd number of values and first is does not look + # like a recent unix time stamp then the first value + # is likely to be an RRD file name. + if ( @_ % 2 && $_[0] !~ /^[1-9][0-9]{8,10}$/i ) { + $rrdfile = shift; + + # Even number of values and the second value looks like + # a recent unix time stamp then the first value is + # likely to be an RRD file name. + } + elsif ( !( @_ % 2 ) && $_[1] =~ /^[1-9][0-9]{8,10}$/i ) { + $rrdfile = shift; + + # If we still don't have an RRD file name then try and + # guess what it is + } + elsif ( !defined $rrdfile ) { + $rrdfile = _guess_filename($stor); + } + + # + # + # + + # We've been given an update timestamp + my $time = time(); + if ( @_ % 2 && $_[0] =~ /^([1-9][0-9]{8,10})$/i ) { + $time = $1; + shift @_; + } + TRACE("Using update time: $time"); + + # Try to automatically create it + unless ( -f $rrdfile ) { + my $default_dstype = defined $stor->{default_dstype} ? $stor->{default_dstype} : $DEFAULT_DSTYPE; + cluck( "RRD file '$rrdfile' does not exist; attempting to create it ", "using default DS type of '$default_dstype'" ) if $^W; + my @args; + for ( my $i = 0; $i < @_; $i++ ) { + push @args, ( $_[$i], $default_dstype ) unless $i % 2; + } + $self->create( $rrdfile, @args ); + } + + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + croak 'Odd number of elements passed when even was expected' if @_ % 2; + + my %ds; + while ( my $ds = shift(@_) ) { + $ds =~ s/[^a-zA-Z0-9_-]//g; + $ds = substr( $ds, 0, 19 ); + $ds{$ds} = shift(@_); + $ds{$ds} = 'U' if !defined( $ds{$ds} ); + } + DUMP( '%ds', \%ds ); + + # Validate the data source names as we add them + my @sources = $self->sources($rrdfile); + for my $ds ( sort keys %ds ) { + + # Check the data source names + if ( !grep( /^$ds$/, @sources ) ) { + TRACE( "Supplied data source '$ds' does not exist in pre-existing " . "RRD data source list: " . join( ', ', @sources ) ); + + # If someone got the case wrong, remind and correct them + if ( grep( /^$ds$/i, @sources ) ) { + cluck( "Data source '$ds' does not exist; automatically ", "correcting it to '", ( grep( /^$ds$/i, @sources ) )[0], "' instead" ) if $^W; + $ds{ ( grep( /^$ds$/i, @sources ) )[0] } = $ds{$ds}; + delete $ds{$ds}; + + # If it's not just a case sensitivity typo and the data source + # name really doesn't exist in this RRD file at all, regardless + # of case, then ... + } + else { + # Ignore the offending missing data source name + if ( $stor->{on_missing_ds} eq 'ignore' ) { + TRACE("on_missing_ds = ignore; ignoring data supplied for missing data source '$ds'"); + + # Fall on our bum and die horribly if requested to do so + } + elsif ( $stor->{on_missing_ds} eq 'die' || $stor->{on_missing_ds} eq 'croak' ) { + croak "Supplied data source '$ds' does not exist in RRD file '$rrdfile'"; + + # Default behaviour is to automatically add the new data source + # to the RRD file in order to preserve the existing default + # functionality of RRD::Simple + } + else { + TRACE( "on_missing_ds = add (or not set at all/default); " . "automatically adding new data source '$ds'" ); + + # Otherwise add any missing or new data sources on the fly + # Decide what DS type and heartbeat to use + my $info = RRDs::info($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + + my %dsTypes; + for my $key ( grep( /^ds\[.+?\]\.type$/, keys %{$info} ) ) { + $dsTypes{ $info->{$key} }++; + } + DUMP( '%dsTypes', \%dsTypes ); + my $dstype = ( + sort { $dsTypes{$b} <=> $dsTypes{$a} } + keys %dsTypes + )[0]; + TRACE("\$dstype = $dstype"); + + $self->add_source( $rrdfile, $ds, $dstype ); + } + } + } + } + + # Build the def + my @def = ('--template'); + push @def, join( ':', sort keys %ds ); + push @def, join( ':', $time, map { $ds{$_} } sort keys %ds ); + DUMP( '@def', \@def ); + + # Pass to RRDs to execute the update + my @rtn = RRDs::update( $rrdfile, @def ); + my $error = RRDs::error(); + croak($error) if $error; + return wantarray ? @rtn : \@rtn; +} + +# Get the last time an RRD was updates +sub last_update { __PACKAGE__->last(@_); } + +sub last { + TRACE(">>> last()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = shift || _guess_filename($stor); + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + my $last = RRDs::last($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + return $last; +} + +# Get a list of data sources from an RRD file +sub sources { + TRACE(">>> sources()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = shift || _guess_filename($stor); + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + my $info = RRDs::info($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + + my @ds; + foreach ( keys %{$info} ) { + if (/^ds\[(.+)?\]\.type$/) { + push @ds, $1; + } + } + return wantarray ? @ds : \@ds; +} + +# Add a new data source to an RRD file +sub add_source { + TRACE(">>> add_source()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + unless ( -f $rrdfile ) { + cluck("RRD file '$rrdfile' does not exist; attempting to create it") + if $^W; + return $self->create( $rrdfile, @_ ); + } + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + # Check that we will understand this RRD file version first + my $info = $self->info($rrdfile); + + # croak "Unable to add a new data source to $rrdfile; ", + # "RRD version $info->{rrd_version} is too new" + # if ($info->{rrd_version}+1-1) > 1; + + my ( $ds, $dstype ) = @_; + TRACE("\$ds = $ds"); + TRACE("\$dstype = $dstype"); + + my $rrdfileBackup = "$rrdfile.bak"; + confess "$rrdfileBackup already exists; please investigate" + if -e $rrdfileBackup; + + # Decide what heartbeat to use + my $heartbeat = + $info->{ds}->{ ( sort { $info->{ds}->{$b}->{minimal_heartbeat} <=> $info->{ds}->{$b}->{minimal_heartbeat} } keys %{ $info->{ds} } )[0] } + ->{minimal_heartbeat}; + TRACE("\$heartbeat = $heartbeat"); + + # Make a list of expected sources after the addition + my $TgtSources = join( ',', sort( ( $self->sources($rrdfile), $ds ) ) ); + + # Add the data source + my $new_rrdfile = ''; + eval { $new_rrdfile = _modify_source( $rrdfile, $stor, $ds, 'add', $dstype, $heartbeat, ); }; + + # Barf if the eval{} got upset + if ($@) { + croak "Failed to add new data source '$ds' to RRD file '$rrdfile': $@"; + } + + # Barf of the new RRD file doesn't exist + unless ( -f $new_rrdfile ) { + croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ", "new RRD file '$new_rrdfile' does not exist"; + } + + # Barf is the new data source isn't in our new RRD file + unless ( $TgtSources eq join( ',', sort( $self->sources($new_rrdfile) ) ) ) { + croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ", "new RRD file '$new_rrdfile' does not contain expected data ", "source names"; + } + + # Try and move the new RRD file in to place over the existing one + # and then remove the backup RRD file if sucessfull + if ( File::Copy::move( $rrdfile, $rrdfileBackup ) + && File::Copy::move( $new_rrdfile, $rrdfile ) ) + { + unless ( unlink($rrdfileBackup) ) { + cluck("Failed to remove back RRD file '$rrdfileBackup': $!") + if $^W; + } + } + else { + croak "Failed to move new RRD file in to place: $!"; + } +} + +# Make a number of graphs for an RRD file +sub graph { + TRACE(">>> graph()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + + # How much data do we have to graph? + my $period = $self->retention_period($rrdfile); + + # Check at RRA CFs are available and graph the best one + my $info = $self->info($rrdfile); + my $cf = 'AVERAGE'; + for my $rra ( @{ $info->{rra} } ) { + if ( $rra->{cf} eq 'AVERAGE' ) { + $cf = 'AVERAGE'; + last; + } + elsif ( $rra->{cf} eq 'MAX' ) { + $cf = 'MAX'; + } + elsif ( $rra->{cf} eq 'MIN' && $cf ne 'MAX' ) { + $cf = 'MIN'; + } + elsif ( $cf ne 'MAX' && $cf ne 'MIN' ) { + $cf = $rra->{cf}; + } + } + TRACE("graph() - \$cf = $cf"); + + # Create graphs which we have enough data to populate + # Version 1.39 - Change the return from an array to a hash (semi backward compatible) + # my @rtn; + my %rtn; + +## +## TODO +## 1.45 Only generate hour, 6hour and 12hour graphs if the +### data resolution (stepping) is fine enough (sub minute) +## + + #i my @graph_periods = qw(hour 6hour 12hour day week month year 3years); + my @graph_periods; + my %param = @_; + if ( defined $param{'periods'} ) { + my %map = qw(daily day weekly week monthly month annual year 3years 3years); + for my $period ( _convert_to_array( $param{'periods'} ) ) { + $period = lc($period); + if ( _valid_scheme($period) ) { + push @graph_periods, $period; + } + elsif ( _valid_scheme( $map{$period} ) ) { + push @graph_periods, $map{$period}; + } + else { + croak "Invalid period value passed in parameter periods; '$period'"; + } + } + } + push @graph_periods, qw(day week month year 3years) unless @graph_periods; + + for my $type (@graph_periods) { + next if $period < _seconds_in($type); + TRACE("graph() - \$type = $type"); + + # push @rtn, [ ($self->_create_graph($rrdfile, $type, $cf, @_)) ]; + $rtn{ _alt_graph_name($type) } = [ ( $self->_create_graph( $rrdfile, $type, $cf, @_ ) ) ]; + } + + # return @rtn; + return wantarray ? %rtn : \%rtn; +} + +# Rename an existing data source +sub rename_source { + TRACE(">>> rename_source()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + my ( $old, $new ) = @_; + croak "No old data source name specified" unless defined $old && length($old); + croak "No new data source name specified" unless defined $new && length($new); + croak "Data source '$old' does not exist in RRD file '$rrdfile'" + unless grep( $_ eq $old, $self->sources($rrdfile) ); + + my @rtn = RRDs::tune( $rrdfile, '-r', "$old:$new" ); + my $error = RRDs::error(); + croak($error) if $error; + return wantarray ? @rtn : \@rtn; +} + +# Get or set a data source heartbeat +sub heartbeat { + TRACE(">>> heartbeat()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = + @_ >= 3 ? shift + : _isLegalDsName( $_[0] ) && $_[1] =~ /^[0-9]+$/ ? _guess_filename($stor) + : shift; + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + # Explode if we get no data source name + my ( $ds, $new_heartbeat ) = @_; + croak "No data source name was specified" unless defined $ds && length($ds); + + # Check the data source name exists + my $info = $self->info($rrdfile); + my $heartbeat = $info->{ds}->{$ds}->{minimal_heartbeat}; + croak "Data source '$ds' does not exist in RRD file '$rrdfile'" + unless defined $heartbeat && $heartbeat; + + if ( !defined $new_heartbeat ) { + return wantarray ? ($heartbeat) : $heartbeat; + } + + my @rtn = !defined $new_heartbeat ? ($heartbeat) : (); + + # Redefine the data source heartbeat + if ( defined $new_heartbeat ) { + croak "New minimal heartbeat '$new_heartbeat' is not a valid positive integer" + unless $new_heartbeat =~ /^[1-9][0-9]*$/; + my @rtn = RRDs::tune( $rrdfile, '-h', "$ds:$new_heartbeat" ); + my $error = RRDs::error(); + croak($error) if $error; + } + + return wantarray ? @rtn : \@rtn; +} + +# Fetch data point information from an RRD file +sub fetch { + TRACE(">>> fetch()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + +} + +# Fetch the last values inserted in to an RRD file +sub last_values { + TRACE(">>> last_values()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + + # When was the RRD last updated? + my $lastUpdated = $self->last($rrdfile); + + # Is there a LAST RRA? + my $info = $self->info($rrdfile); + my $hasLastRRA = 0; + for my $rra ( @{ $info->{rra} } ) { + $hasLastRRA++ if $rra->{cf} eq 'LAST'; + } + return if !$hasLastRRA; + + # What's the largest heartbeat in the RRD file data sources? + my $largestHeartbeat = 1; + for ( map { $info->{ds}->{$_}->{'minimal_heartbeat'} } keys( %{ $info->{ds} } ) ) { + $largestHeartbeat = $_ if $_ > $largestHeartbeat; + } + + my @def = ( 'LAST', '-s', $lastUpdated - ( $largestHeartbeat * 2 ), '-e', $lastUpdated ); + + # Pass to RRDs to execute + my ( $time, $heartbeat, $ds, $data ) = RRDs::fetch( $rrdfile, @def ); + my $error = RRDs::error(); + croak($error) if $error; + + # Put it in to a nice easy format + my %rtn = (); + for my $rec ( reverse @{$data} ) { + for ( my $i = 0; $i < @{$rec}; $i++ ) { + if ( defined $rec->[$i] && !exists( $rtn{ $ds->[$i] } ) ) { + $rtn{ $ds->[$i] } = $rec->[$i]; + } + } + } + + # Well, I'll be buggered if the LAST CF does what you'd think + # it's meant to do. If anybody can give me some decent documentation + # on what the LAST CF does, and/or how to get the last value put + # in to an RRD, then I'll admit that this method exists and export + # it too. + + return wantarray ? %rtn : \%rtn; +} + +# Return how long this RRD retains data for +sub retention_period { + TRACE(">>> retention_period()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $info = $self->info(@_); + return if !defined($info); + + my $duration = $info->{step}; + for my $rra ( @{ $info->{rra} } ) { + my $secs = ( $rra->{pdp_per_row} * $info->{step} ) * $rra->{rows}; + $duration = $secs if $secs > $duration; + } + + return wantarray ? ($duration) : $duration; +} + +# Fetch information about an RRD file +sub info { + TRACE(">>> info()"); + my $self = shift; + unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{ _refaddr($self) }; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + + my $info = RRDs::info($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + DUMP( '$info', $info ); + + my $rtn; + for my $key ( sort( keys( %{$info} ) ) ) { + if ( $key =~ /^rra\[(\d+)\]\.([a-z_]+)/ ) { + $rtn->{rra}->[$1]->{$2} = $info->{$key}; + } + elsif ( my (@dsKey) = $key =~ /^ds\[([[A-Za-z0-9\_]+)?\]\.([a-z_]+)/ ) { + $rtn->{ds}->{$1}->{$2} = $info->{$key}; + } + elsif ( $key !~ /\[[\d_a-z]+\]/i ) { + $rtn->{$key} = $info->{$key}; + } + } + + # Return the information + DUMP( '$rtn', $rtn ); + return $rtn; +} + +# Convert a string or an array reference to an array +sub _convert_to_array { + return unless defined $_[0]; + if ( !ref $_[0] ) { + $_[0] =~ /^\s+|\s+$/g; + return split( /(?:\s+|\s*,\s*)/, $_[0] ); + } + elsif ( ref( $_[0] ) eq 'ARRAY' ) { + return @{ $_[0] }; + } + return; +} + +# Make a single graph image +sub _create_graph { + TRACE(">>> _create_graph()"); + my $self = shift; + my $rrdfile = shift; + my $type = _valid_scheme(shift) || 'day'; + my $cf = shift || 'AVERAGE'; + + my $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK):.+/; + $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK|TEXTALIGN):.+/ + if $RRDs::VERSION >= 1.3; # http://oss.oetiker.ch/rrdtool-trac/wiki/RRDtool13 + + my %param; + my @command_param; + while ( my $k = shift ) { + if ( $k =~ /$command_regex/ ) { + push @command_param, $k; + shift; + } + else { + $k =~ s/_/-/g; + $param{ lc($k) } = shift; + } + } + + # If we get this custom parameter then it would have already + # been dealt with by the calling graph() method so we should + # ditch it right here and now! + delete $param{'periods'}; + + # Specify some default values + $param{'end'} ||= $self->last($rrdfile) || time(); + $param{'imgformat'} ||= 'PNG'; # RRDs >1.3 now support PDF, SVG and EPS + # $param{'alt-autoscale'} ||= ''; + # $param{'alt-y-grid'} ||= ''; + + # Define what to call the image + my $basename = + defined $param{'basename'} && $param{'basename'} =~ /^[0-9a-z_\.-]+$/i + ? $param{'basename'} + : ( fileparse( $rrdfile, '\.[^\.]+' ) )[0]; + delete $param{'basename'}; + + # Define where to write the image + my $image = sprintf( '%s-%s.%s', $basename, _alt_graph_name($type), lc( $param{'imgformat'} ) ); + if ( $param{'destination'} ) { + $image = File::Spec->catfile( $param{'destination'}, $image ); + } + delete $param{'destination'}; + + # Specify timestamps- new for version 1.41 + my $timestamp = + !defined $param{'timestamp'} || $param{'timestamp'} !~ /^(graph|rrd|both|none)$/i + ? 'graph' + : lc( $param{'timestamp'} ); + delete $param{'timestamp'}; + + # Specify extended legend - new for version 1.35 + my $extended_legend = defined $param{'extended-legend'} + && $param{'extended-legend'} ? 1 : 0; + delete $param{'extended-legend'}; + + # Define how thick the graph lines should be + my $line_thickness = defined $param{'line-thickness'} + && $param{'line-thickness'} =~ /^[123]$/ ? $param{'line-thickness'} : 1; + delete $param{'line-thickness'}; + + # Colours is an alias to colors + if ( exists $param{'source-colours'} && !exists $param{'source-colors'} ) { + $param{'source-colors'} = $param{'source-colours'}; + delete $param{'source-colours'}; + } + + # Allow source line colors to be set + my @source_colors = (); + my %source_colors = (); + if ( defined $param{'source-colors'} ) { + + #if (ref($param{'source-colors'}) eq 'ARRAY') { + # @source_colors = @{$param{'source-colors'}}; + if ( ref( $param{'source-colors'} ) eq 'HASH' ) { + %source_colors = %{ $param{'source-colors'} }; + } + else { + @source_colors = _convert_to_array( $param{'source-colors'} ); + } + } + delete $param{'source-colors'}; + + # Define which data sources we should plot + my @rrd_sources = $self->sources($rrdfile); + my @ds = !exists $param{'sources'} + ? @rrd_sources + + #: defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY' + #? @{$param{'sources'}} + : defined $param{'sources'} ? _convert_to_array( $param{'sources'} ) + : (); + + # Allow source legend source_labels to be set + my %source_labels = (); + if ( defined $param{'source-labels'} ) { + if ( ref( $param{'source-labels'} ) eq 'HASH' ) { + %source_labels = %{ $param{'source-labels'} }; + } + elsif ( ref( $param{'source-labels'} ) eq 'ARRAY' ) { + if ( defined $param{'sources'} && ref( $param{'sources'} ) eq 'ARRAY' ) { + for ( my $i = 0; $i < @{ $param{'source-labels'} }; $i++ ) { + $source_labels{ $ds[$i] } = $param{'source-labels'}->[$i] + if defined $ds[$i]; + } + } + elsif ($^W) { + carp "source_labels may only be an array if sources is also " . "an specified and valid array"; + } + } + } + delete $param{'source-labels'}; + + # Allow source legend source_drawtypes to be set + # ... "oops" ... yes, this is quite obviously + # copy and paste code from the chunk above. I'm + # sorry. I'll rationalise it some other day if + # it's necessary. + my %source_drawtypes = (); + if ( defined $param{'source-drawtypes'} ) { + if ( ref( $param{'source-drawtypes'} ) eq 'HASH' ) { + %source_drawtypes = %{ $param{'source-drawtypes'} }; + } + elsif ( ref( $param{'source-drawtypes'} ) eq 'ARRAY' ) { + if ( defined $param{'sources'} && ref( $param{'sources'} ) eq 'ARRAY' ) { + for ( my $i = 0; $i < @{ $param{'source-drawtypes'} }; $i++ ) { + $source_drawtypes{ $ds[$i] } = $param{'source-drawtypes'}->[$i] + if defined $ds[$i]; + } + } + elsif ($^W) { + carp "source_drawtypes may only be an array if sources is " . "also an specified and valid array"; + } + } + + # Validate the values we have and set default thickness + while ( my ( $k, $v ) = each %source_drawtypes ) { + if ( $v !~ /^(LINE[1-9]?|STACK|AREA)$/ ) { + delete $source_drawtypes{$k}; + carp "source_drawtypes may be LINE, LINEn, AREA or STACK " . "only; value '$v' is not valid" if $^W; + } + $source_drawtypes{$k} = uc($v); + $source_drawtypes{$k} .= $line_thickness if $v eq 'LINE'; + } + } + delete $param{'source-drawtypes'}; + delete $param{'sources'}; + + # Specify a default start time + $param{'start'} ||= $param{'end'} - _seconds_in( $type, 115 ); + + # Suffix the title with the period information + $param{'title'} ||= basename($rrdfile); + $param{'title'} .= ' - [Hourly Graph]' if $type eq 'hour'; + $param{'title'} .= ' - [6 Hour Graph]' if $type eq '6hour' || $type eq 'quarterday'; + $param{'title'} .= ' - [12 Hour Graph]' if $type eq '12hour' || $type eq 'halfday'; + $param{'title'} .= ' - [Daily Graph]' if $type eq 'day'; + $param{'title'} .= ' - [Weekly Graph]' if $type eq 'week'; + $param{'title'} .= ' - [Monthly Graph]' if $type eq 'month'; + $param{'title'} .= ' - [Annual Graph]' if $type eq 'year'; + $param{'title'} .= ' - [3 Year Graph]' if $type eq '3years'; + + # Convert our parameters in to an RRDs friendly defenition + my @def; + while ( my ( $k, $v ) = each %param ) { + if ( length($k) == 1 ) { # Short single character options + $k = '-' . uc($k); + } + else { # Long options + $k = "--$k"; + } + for my $v ( ( ref($v) eq 'ARRAY' ? @{$v} : ($v) ) ) { + if ( !defined $v || !length($v) ) { + push @def, $k; + } + else { + push @def, "$k=$v"; + } + } + } + + # Populate a cycling tied scalar for line colors + @source_colors = qw( + FF0000 00FF00 0000FF 00FFFF FF00FF FFFF00 000000 + 990000 009900 000099 009999 990099 999900 999999 + 552222 225522 222255 225555 552255 555522 555555 + ) unless @source_colors > 0; + + # Pre 1.35 colours + # FF0000 00FF00 0000FF FFFF00 00FFFF FF00FF 000000 + # 550000 005500 000055 555500 005555 550055 555555 + # AA0000 00AA00 0000AA AAAA00 00AAAA AA00AA AAAAAA + tie my $colour, 'RRD::Simple::_Colour', \@source_colors; + + my $fmt = '%s:%s#%s:%s%s'; + my $longest_label = 1; + if ($extended_legend) { + for my $ds (@ds) { + my $len = length( defined $source_labels{$ds} ? $source_labels{$ds} : $ds ); + $longest_label = $len if $len > $longest_label; + } + $fmt = "%s:%s#%s:%-${longest_label}s%s"; + } + +## +## +## + + # Create the @cmd + my @cmd = ( $image, @def ); + + # Add the data sources definitions to @cmd + for my $ds (@rrd_sources) { + + # Add the data source definition + push @cmd, sprintf( 'DEF:%s=%s:%s:%s', $ds, $rrdfile, $ds, $cf ); + } + + # Add the data source draw commands to the grap/@cmd + for my $ds (@ds) { + + # Stack operates differently in RRD 1.2 or higher + my $drawtype = + defined $source_drawtypes{$ds} + ? $source_drawtypes{$ds} + : "LINE$line_thickness"; + my $stack = ''; + if ( $RRDs::VERSION >= 1.2 && $drawtype eq 'STACK' ) { + $drawtype = 'AREA'; + $stack = ':STACK'; + } + + # Draw the line (and add to the legend) + push @cmd, + sprintf( $fmt, + $drawtype, $ds, + ( defined $source_colors{$ds} ? $source_colors{$ds} : $colour ), + ( defined $source_labels{$ds} ? $source_labels{$ds} : $ds ), $stack ); + + # New for version 1.39 + # Return the min,max,last information in the graph() return @rtn + if ( $RRDs::VERSION >= 1.2 ) { + push @cmd, sprintf( 'VDEF:%sMIN=%s,MINIMUM', $ds, $ds ); + push @cmd, sprintf( 'VDEF:%sMAX=%s,MAXIMUM', $ds, $ds ); + push @cmd, sprintf( 'VDEF:%sLAST=%s,LAST', $ds, $ds ); + + # Don't automatically add this unless we have to + # push @cmd, sprintf('VDEF:%sAVERAGE=%s,AVERAGE',$ds,$ds); + push @cmd, sprintf( 'PRINT:%sMIN:%s min %%1.2lf', $ds, $ds ); + push @cmd, sprintf( 'PRINT:%sMAX:%s max %%1.2lf', $ds, $ds ); + push @cmd, sprintf( 'PRINT:%sLAST:%s last %%1.2lf', $ds, $ds ); + } + else { + push @cmd, sprintf( 'PRINT:%s:MIN:%s min %%1.2lf', $ds, $ds ); + push @cmd, sprintf( 'PRINT:%s:MAX:%s max %%1.2lf', $ds, $ds ); + push @cmd, sprintf( 'PRINT:%s:LAST:%s last %%1.2lf', $ds, $ds ); + } + + # New for version 1.35 + if ($extended_legend) { + if ( $RRDs::VERSION >= 1.2 ) { + + # Moved the VDEFs to the block of code above which is + # always run, regardless of the extended legend + push @cmd, sprintf( 'GPRINT:%sMIN: min\:%%10.2lf\g', $ds ); + push @cmd, sprintf( 'GPRINT:%sMAX: max\:%%10.2lf\g', $ds ); + push @cmd, sprintf( 'GPRINT:%sLAST: last\:%%10.2lf\l', $ds ); + } + else { + push @cmd, sprintf( 'GPRINT:%s:MIN: min\:%%10.2lf\g', $ds ); + push @cmd, sprintf( 'GPRINT:%s:MAX: max\:%%10.2lf\g', $ds ); + push @cmd, sprintf( 'GPRINT:%s:LAST: last\:%%10.2lf\l', $ds ); + } + } + } + + # Push the post command defs on to the stack + push @cmd, @command_param; + + # Add a comment stating when the graph was last updated + if ( $timestamp ne 'none' ) { + + #push @cmd, ('COMMENT:\s','COMMENT:\s','COMMENT:\s'); + push @cmd, ( 'COMMENT:\s', 'COMMENT:\s' ); + push @cmd, 'COMMENT:\s' unless $extended_legend || !@ds; + my $timefmt = '%a %d/%b/%Y %T %Z'; + + if ( $timestamp eq 'rrd' || $timestamp eq 'both' ) { + my $time = sprintf( 'RRD last updated: %s\r', strftime( $timefmt, localtime( ( stat($rrdfile) )[9] ) ) ); + $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2 + push @cmd, "COMMENT:$time"; + } + + if ( $timestamp eq 'graph' || $timestamp eq 'both' ) { + my $time = sprintf( 'Graph last updated: %s\r', strftime( $timefmt, localtime(time) ) ); + $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2 + push @cmd, "COMMENT:$time"; + } + } + + DUMP( '@cmd', \@cmd ); + + # Generate the graph + my @rtn = RRDs::graph(@cmd); + my $error = RRDs::error(); + croak($error) if $error; + return ( $image, @rtn ); +} + +# +# Private subroutines +# + +no warnings 'redefine'; +sub UNIVERSAL::a_sub_not_likely_to_be_here { ref( $_[0] ) } +use warnings 'redefine'; + +sub _blessed ($) { + local ( $@, $SIG{__DIE__}, $SIG{__WARN__} ); + return length( ref( $_[0] ) ) + ? eval { $_[0]->a_sub_not_likely_to_be_here } + : undef; +} + +sub _refaddr($) { + my $pkg = ref( $_[0] ) or return undef; + if ( _blessed( $_[0] ) ) { + bless $_[0], 'Scalar::Util::Fake'; + } + else { + $pkg = undef; + } + "$_[0]" =~ /0x(\w+)/; + my $i = do { local $^W; hex $1 }; + bless $_[0], $pkg if defined $pkg; + return $i; +} + +sub _isLegalDsName { + + #rrdtool-1.0.49/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" + #rrdtool-1.2.11/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" + +## +## TODO +## 1.45 - Double check this with the latest 1.3 version of RRDtool +## to see if it has changed or not +## + + return $_[0] =~ /^[a-zA-Z0-9_-]{1,19}$/; +} + +sub _rrd_def { + croak('Pardon?!') if ref $_[0]; + my $type = _valid_scheme(shift); + + # This is calculated the same way as mrtg v2.13.2 + if ( $type eq 'mrtg' ) { + my $step = 5; # 5 minutes + return { + step => $step * 60, + heartbeat => $step * 60 * 2, + rra => [ + ( + { step => 1, rows => int( 4000 / $step ) }, # 800 + { step => int( 30 / $step ), rows => 800 }, # if $step < 30 + { step => int( 120 / $step ), rows => 800 }, + { step => int( 1440 / $step ), rows => 800 }, + ) + ], + }; + } + +## +## TODO +## 1.45 Add higher resolution for hour, 6hour and 12 hour +## + + my $step = 1; # 1 minute highest resolution + my $rra = { + step => $step * 60, + heartbeat => $step * 60 * 2, + rra => [ + ( + # Actual $step resolution (for 1.25 days retention) + { step => 1, rows => int( _minutes_in( 'day', 125 ) / $step ) }, + ) + ], + }; + + if ( $type =~ /^(week|month|year|3years)$/i ) { + push @{ $rra->{rra} }, + { + step => int( 30 / $step ), + rows => int( _minutes_in( 'week', 125 ) / int( 30 / $step ) ) + }; # 30 minute average + + push @{ $rra->{rra} }, + { + step => int( 120 / $step ), + rows => int( _minutes_in( $type eq 'week' ? 'week' : 'month', 125 ) / int( 120 / $step ) ) + }; # 2 hour average + } + + if ( $type =~ /^(year|3years)$/i ) { + push @{ $rra->{rra} }, + { + step => int( 1440 / $step ), + rows => int( _minutes_in( $type, 125 ) / int( 1440 / $step ) ) + }; # 1 day average + } + + return $rra; +} + +sub _odd { + return $_[0] % 2; +} + +sub _even { + return !( $_[0] % 2 ); +} + +sub _valid_scheme { + TRACE(">>> _valid_scheme()"); + croak('Pardon?!') if ref $_[0]; + + #if ($_[0] =~ /^(day|week|month|year|3years|mrtg)$/i) { + if ( $_[0] =~ /^((?:6|12)?hour|(?:half)?day|week|month|year|3years|mrtg)$/i ) { + TRACE( "'" . lc($1) . "' is a valid scheme." ); + return lc($1); + } + TRACE("'@_' is not a valid scheme."); + return undef; +} + +sub _hours_in { return int( ( _seconds_in(@_) / 60 ) / 60 ); } +sub _minutes_in { return int( _seconds_in(@_) / 60 ); } + +sub _seconds_in { + croak('Pardon?!') if ref $_[0]; + my $str = lc(shift); + my $scale = shift || 100; + + return undef if !defined( _valid_scheme($str) ); + + my %time = ( + + # New for version 1.44 of RRD::Simple by + # popular request + 'hour' => 60 * 60, + '6hour' => 60 * 60 * 6, + 'quarterday' => 60 * 60 * 6, + '12hour' => 60 * 60 * 12, + 'halfday' => 60 * 60 * 12, + + 'day' => 60 * 60 * 24, + 'week' => 60 * 60 * 24 * 7, + 'month' => 60 * 60 * 24 * 31, + 'year' => 60 * 60 * 24 * 365, + '3years' => 60 * 60 * 24 * 365 * 3, + 'mrtg' => ( int( ( 1440 / 5 ) ) * 800 ) * 60, # mrtg v2.13.2 + ); + + my $rtn = $time{$str} * ( $scale / 100 ); + return $rtn; +} + +sub _alt_graph_name { + croak('Pardon?!') if ref $_[0]; + my $type = _valid_scheme(shift); + return unless defined $type; + + # New for version 1.44 of RRD::Simple by popular request + return 'hourly' if $type eq 'hour'; + return '6hourly' if $type eq '6hour' || $type eq 'quarterday'; + return '12hourly' if $type eq '12hour' || $type eq 'halfday'; + + return 'daily' if $type eq 'day'; + return 'weekly' if $type eq 'week'; + return 'monthly' if $type eq 'month'; + return 'annual' if $type eq 'year'; + return '3years' if $type eq '3years'; + return $type; +} + +## +## TODO +## 1.45 - Check to see if there is now native support in RRDtool to +## add, remove or change existing sources - and if there is +## make this code only run for onler versions that do not have +## native support. +## + +sub _modify_source { + croak('Pardon?!') if ref $_[0]; + my ( $rrdfile, $stor, $ds, $action, $dstype, $heartbeat ) = @_; + my $rrdtool = $stor->{rrdtool}; + $rrdtool = '' unless defined $rrdtool; + + # Decide what action we should take + if ( $action !~ /^(add|del)$/ ) { + my $caller = ( caller(1) )[3]; + $action = + $caller =~ /\badd\b/i ? 'add' + : $caller =~ /\bdel(ete)?\b/i ? 'del' + : undef; + } + croak "Unknown or no action passed to method _modify_source()" + unless defined $action && $action =~ /^(add|del)$/; + + require File::Copy; + require File::Temp; + + # Generate an XML dump of the RRD file + # - Added "tmpdir" support in 1.44 + my $tmpdir = defined $stor->{tmpdir} ? $stor->{tmpdir} : File::Spec->tmpdir(); + my ( $tempXmlFileFH, $tempXmlFile ) = File::Temp::tempfile( + DIR => $tmpdir, + TEMPLATE => 'rrdXXXXX', + SUFFIX => '.tmp', + ); + + # Check that we managed to get a sane temporary filename + croak "File::Temp::tempfile() failed to return a temporary filename" + unless defined $tempXmlFile; + TRACE("_modify_source(): \$tempXmlFile = $tempXmlFile"); + + # Try the internal perl way first (portable) + eval { + # Patch to rrd_dump.c emailed to Tobi and developers + # list by nicolaw/heds on 2006/01/08 + if ( $RRDs::VERSION >= 1.2013 ) { + my @rtn = RRDs::dump( $rrdfile, $tempXmlFile ); + my $error = RRDs::error(); + croak($error) if $error; + } + }; + + # Do it the old fashioned way + if ( $@ || !-f $tempXmlFile || ( stat($tempXmlFile) )[7] < 200 ) { + croak "rrdtool binary '$rrdtool' does not exist or is not executable" + if !defined $rrdtool || !-f $rrdtool || !-x $rrdtool; + _safe_exec( sprintf( '%s dump %s > %s', $rrdtool, $rrdfile, $tempXmlFile ) ); + } + + # Read in the new temporary XML dump file + open( IN, "<$tempXmlFile" ) || croak "Unable to open '$tempXmlFile': $!"; + + # Open XML output file + # my $tempImportXmlFile = File::Temp::tmpnam(); + # - Added "tmpdir" support in 1.44 + my ( $tempImportXmlFileFH, $tempImportXmlFile ) = File::Temp::tempfile( + DIR => $tmpdir, + TEMPLATE => 'rrdXXXXX', + SUFFIX => '.tmp', + ); + open( OUT, ">$tempImportXmlFile" ) + || croak "Unable to open '$tempImportXmlFile': $!"; + + # Create a marker hash ref to store temporary state + my $marker = { + currentDSIndex => 0, + deleteDSIndex => undef, + addedNewDS => 0, + parse => 0, + version => 1, + }; + + # Parse the input XML file + while ( local $_ = ) { + chomp; + + # Find out what index number the existing DS definition is in + if ( $action eq 'del' && /\s*(\S+)\s*<\/name>/ ) { + $marker->{deleteIndex} = $marker->{currentDSIndex} if $1 eq $ds; + $marker->{currentDSIndex}++; + } + + # Add the DS definition + if ( $action eq 'add' && !$marker->{addedNewDS} && // ) { + print OUT < + $ds + $dstype + $heartbeat + 0.0000000000e+00 + NaN + + + UNKN + 0.0000000000e+00 + 0 + + +EndDS + $marker->{addedNewDS} = 1; + } + + # Insert DS under CDP_PREP entity + if ( $action eq 'add' && /<\/cdp_prep>/ ) { + + # Version 0003 RRD from rrdtool 1.2x + if ( $marker->{version} >= 3 ) { + print OUT " \n"; + print OUT " 0.0000000000e+00 \n"; + print OUT " 0.0000000000e+00 \n"; + print OUT " NaN \n"; + print OUT " 0 \n"; + print OUT " \n"; + + # Version 0001 RRD from rrdtool 1.0x + } + else { + print OUT " NaN 0 \n"; + } + } + + # Look for the end of an RRA + if (/<\/database>/) { + $marker->{parse} = 0; + + # Find the dumped RRD version (must take from the XML, not the RRD) + } + elsif (/\s*([0-9\.]+)\s*<\/version>/) { + $marker->{version} = ( $1 + 1 - 1 ); + } + + # Add the extra " NaN " under the RRAs. Just print normal lines + if ( $marker->{parse} == 1 ) { + if ( $_ =~ /^(.+ .+)(<\/row>.*)/ ) { + print OUT $1; + print OUT " NaN " if $action eq 'add'; + print OUT $2; + print OUT "\n"; + } + } + else { + print OUT "$_\n"; + } + + # Look for the start of an RRA + if (//) { + $marker->{parse} = 1; + } + } + + # Close the files + close(IN) || croak "Unable to close '$tempXmlFile': $!"; + close(OUT) || croak "Unable to close '$tempImportXmlFile': $!"; + + # Import the new output file in to the old RRD filename + my $new_rrdfile = File::Temp::tmpnam(); + TRACE("_modify_source(): \$new_rrdfile = $new_rrdfile"); + + # Try the internal perl way first (portable) + eval { + if ( $RRDs::VERSION >= 1.0049 ) + { + my @rtn = RRDs::restore( $tempImportXmlFile, $new_rrdfile ); + my $error = RRDs::error(); + croak($error) if $error; + } + }; + + # Do it the old fashioned way + if ( $@ || !-f $new_rrdfile || ( stat($new_rrdfile) )[7] < 200 ) { + croak "rrdtool binary '$rrdtool' does not exist or is not executable" + unless ( -f $rrdtool && -x $rrdtool ); + my $cmd = sprintf( '%s restore %s %s', $rrdtool, $tempImportXmlFile, $new_rrdfile ); + my $rtn = _safe_exec($cmd); + + # At least check the file is created + unless ( -f $new_rrdfile ) { + _nuke_tmp( $tempXmlFile, $tempImportXmlFile ); + croak "Command '$cmd' failed to create the new RRD file '$new_rrdfile': $rtn"; + } + } + + # Remove the temporary files + _nuke_tmp( $tempXmlFile, $tempImportXmlFile ); + + sub _nuke_tmp { + for (@_) { + unlink($_) + || carp("Unable to unlink temporary file '$_': $!"); + } + } + + # Return the new RRD filename + return wantarray ? ($new_rrdfile) : $new_rrdfile; +} + +## +## TODO +## 1.45 - Improve this _safe_exec function to see if it can be made +## more robust and use any better CPAN modules if that happen +## to already be installed on the users system (don't add any +## new module dependancies though) +## + +sub _safe_exec { + croak('Pardon?!') if ref $_[0]; + my $cmd = shift; + if ( $cmd =~ /^([\/\.\_\-a-zA-Z0-9 >]+)$/ ) { + $cmd = $1; + TRACE($cmd); + system($cmd); + if ( $? == -1 ) { + croak "Failed to execute command '$cmd': $!\n"; + } + elsif ( $? & 127 ) { + croak( + sprintf( "While executing command '%s', child died " . "with signal %d, %s coredump\n", $cmd, ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' ) + ); + } + my $exit_value = $? >> 8; + croak "Error caught from '$cmd'" if $exit_value != 0; + return $exit_value; + } + else { + croak "Unexpected potentially unsafe command will not be executed: $cmd"; + } +} + +sub _find_binary { + croak('Pardon?!') if ref $_[0]; + my $binary = shift || 'rrdtool'; + return $binary if -f $binary && -x $binary; + + my @paths = File::Spec->path(); + my $rrds_path = dirname( $INC{'RRDs.pm'} ); + push @paths, $rrds_path; + push @paths, File::Spec->catdir( $rrds_path, File::Spec->updir(), File::Spec->updir(), 'bin' ); + + for my $path (@paths) { + my $filename = File::Spec->catfile( $path, $binary ); + return $filename if -f $filename && -x $filename; + } + + my $path = File::Spec->catdir( File::Spec->rootdir(), 'usr', 'local' ); + if ( opendir( DH, $path ) ) { + my @dirs = sort { $b cmp $a } grep( /^rrdtool/, readdir(DH) ); + closedir(DH) || carp "Unable to close file handle: $!"; + for my $dir (@dirs) { + my $filename = File::Spec->catfile( $path, $dir, 'bin', $binary ); + return $filename if -f $filename && -x $filename; + } + } +} + +sub _guess_filename { + croak('Pardon?!') if !defined $_[0] || ref( $_[0] ) ne 'HASH'; + my $stor = shift; + if ( defined $stor->{file} ) { + TRACE("_guess_filename = \$stor->{file} = $stor->{file}"); + return $stor->{file}; + } + my ( $basename, $dirname, $extension ) = fileparse( $0, '\.[^\.]+' ); + TRACE("_guess_filename = calculated = $dirname$basename.rrd"); + return "$dirname$basename.rrd"; +} + +sub DESTROY { + my $self = shift; + delete $objstore->{ _refaddr($self) }; +} + +sub TRACE { + return unless $DEBUG; + carp( shift() ); +} + +sub DUMP { + return unless $DEBUG; + eval { + require Data::Dumper; + $Data::Dumper::Indent = 2; + $Data::Dumper::Terse = 1; + carp( shift() . ': ' . Data::Dumper::Dumper( shift() ) ); + }; +} + +BEGIN { + eval "use RRDs"; + if ($@) { + carp qq{ ++-----------------------------------------------------------------------------+ +| ERROR! -- Could not load RRDs.pm | +| | +| RRD::Simple requires RRDs.pm (a part of RRDtool) in order to function. You | +| can download a copy of RRDtool from http://www.rrdtool.org. See the INSTALL | +| document for more details. | ++-----------------------------------------------------------------------------+ + +} unless $ENV{AUTOMATED_TESTING}; + } +} + +1; + +############################################################### +# This tie code is from Tie::Cycle +# written by brian d foy, + +package RRD::Simple::_Colour; + +sub TIESCALAR { + my ( $class, $list_ref ) = @_; + my @shallow_copy = map { $_ } @$list_ref; + return unless UNIVERSAL::isa( $list_ref, 'ARRAY' ); + my $self = [ 0, scalar @shallow_copy, \@shallow_copy ]; + bless $self, $class; +} + +sub FETCH { + my $self = shift; + my $index = $$self[0]++; + $$self[0] %= $self->[1]; + return $self->[2]->[$index]; +} + +sub STORE { + my ( $self, $list_ref ) = @_; + return unless ref $list_ref eq ref []; + return unless @$list_ref > 1; + $self = [ 0, scalar @$list_ref, $list_ref ]; +} + +1; + +=pod + +=head1 NAME + +RRD::Simple - Simple interface to create and store data in RRD files + +=head1 SYNOPSIS + + use strict; + use RRD::Simple (); + + # Create an interface object + my $rrd = RRD::Simple->new( file => "myfile.rrd" ); + + # Create a new RRD file with 3 data sources called + # bytesIn, bytesOut and faultsPerSec. + $rrd->create( + bytesIn => "GAUGE", + bytesOut => "GAUGE", + faultsPerSec => "COUNTER" + ); + + # Put some arbitary data values in the RRD file for the same + # 3 data sources called bytesIn, bytesOut and faultsPerSec. + $rrd->update( + bytesIn => 10039, + bytesOut => 389, + faultsPerSec => 0.4 + ); + + # Generate graphs: + # /var/tmp/myfile-daily.png, /var/tmp/myfile-weekly.png + # /var/tmp/myfile-monthly.png, /var/tmp/myfile-annual.png + my %rtn = $rrd->graph( + destination => "/var/tmp", + title => "Network Interface eth0", + vertical_label => "Bytes/Faults", + interlaced => "" + ); + printf("Created %s\n",join(", ",map { $rtn{$_}->[0] } keys %rtn)); + + # Return information about an RRD file + my $info = $rrd->info; + require Data::Dumper; + print Data::Dumper::Dumper($info); + + # Get unixtime of when RRD file was last updated + my $lastUpdated = $rrd->last; + print "myfile.rrd was last updated at " . + scalar(localtime($lastUpdated)) . "\n"; + + # Get list of data source names from an RRD file + my @dsnames = $rrd->sources; + print "Available data sources: " . join(", ", @dsnames) . "\n"; + + # And for the ultimately lazy, you could create and update + # an RRD in one go using a one-liner like this: + perl -MRRD::Simple=:all -e"update(@ARGV)" myfile.rrd bytesIn 99999 + +=head1 DESCRIPTION + +RRD::Simple provides a simple interface to RRDTool's RRDs module. +This module does not currently offer a C method that is +available in the RRDs module. + +It does however create RRD files with a sensible set of default RRA +(Round Robin Archive) definitions, and can dynamically add new +data source names to an existing RRD file. + +This module is ideal for quick and simple storage of data within an +RRD file if you do not need to, nor want to, bother defining custom +RRA definitions. + +=head1 METHODS + +=head2 new + + my $rrd = RRD::Simple->new( + file => "myfile.rrd", + rrdtool => "/usr/local/rrdtool-1.2.11/bin/rrdtool", + tmpdir => "/var/tmp", + cf => [ qw(AVERAGE MAX) ], + default_dstype => "GAUGE", + on_missing_ds => "add", + ); + +The C parameter is currently optional but will become mandatory in +future releases, replacing the optional C<$rrdfile> parameters on subsequent +methods. This parameter specifies the RRD filename to be used. + +The C parameter is optional. It specifically defines where the +C binary can be found. If not specified, the module will search for +the C binary in your path, an additional location relative to where +the C module was loaded from, and in /usr/local/rrdtool*. + +The C parameter is option and is only used what automatically adding +a new data source to an existing RRD file. By default any temporary files +will be placed in your default system temp directory (typically /tmp on Linux, +or whatever your TMPDIR environment variable is set to). This parameter can +be used for force any temporary files to be created in a specific directory. + +The C binary is only used by the C method, and only +under certain circumstances. The C method may also be called +automatically by the C method, if data point values for a previously +undefined data source are provided for insertion. + +The C parameter is optional, but when specified expects an array +reference. The C parameter defines which consolidation functions are +used in round robin archives (RRAs) when creating new RRD files. Valid +values are AVERAGE, MIN, MAX and LAST. The default value is AVERAGE and +MAX. + +The C parameter is optional. Specifying the default data +source type (DST) through the new() method allows the DST to be localised +to the $rrd object instance rather than be global to the RRD::Simple package. +See L<$RRD::Simple::DEFAULT_DSTYPE>. + +The C parameter is optional and will default to "add" when +not defined. This parameter will determine what will happen if you try +to insert or update data for a data source name that does not exist in +the RRD file. Valid values are "add", "ignore" and "die". + +=head2 create + + $rrd->create($rrdfile, $period, + source_name => "TYPE", + source_name => "TYPE", + source_name => "TYPE" + ); + +This method will create a new RRD file on disk. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +C<$period> is optional and will default to C. Valid options are C, +C<6hour>/C, C<12hour>/C, C, C, C, +C, C<3years> and C. Specifying a data retention period value will +change how long data will be retained for within the RRD file. The C +scheme will try and mimic the data retention period used by MRTG v2.13.2 +(L. + +The C data retention period uses a data stepping resolution of 300 +seconds (5 minutes) and heartbeat of 600 seconds (10 minutes), whereas all the +other data retention periods use a data stepping resolution of 60 seconds +(1 minute) and heartbeat of 120 seconds (2 minutes). + +Each data source name should specify the data source type. Valid data source +types (DSTs) are GAUGE, COUNTER, DERIVE and ABSOLUTE. See the section +regrading DSTs at L +for further information. + +RRD::Simple will croak and die if you try to create an RRD file that already +exists. + +=head2 update + + $rrd->update($rrdfile, $unixtime, + source_name => "VALUE", + source_name => "VALUE", + source_name => "VALUE" + ); + +This method will update an RRD file by inserting new data point values +in to the RRD file. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +C<$unixtime> is optional and will default to C (the current unixtime). +Specifying this value will determine the date and time that your data point +values will be stored against in the RRD file. + +If you try to update a value for a data source that does not exist, it will +automatically be added for you. The data source type will be set to whatever +is contained in the C<$RRD::Simple::DEFAULT_DSTYPE> variable. (See the +VARIABLES section below). + +If you explicitly do not want this to happen, then you should check that you +are only updating pre-existing data source names using the C method. +You can manually add new data sources to an RRD file by using the C +method, which requires you to explicitly set the data source type. + +If you try to update an RRD file that does not exist, it will attept to create +the RRD file for you using the same behaviour as described above. A warning +message will be displayed indicating that the RRD file is being created for +you if have perl warnings turned on. + +=head2 last + + my $unixtime = $rrd->last($rrdfile); + +This method returns the last (most recent) data point entry time in the RRD +file in UNIX time (seconds since the epoch; Jan 1st 1970). This value should +not be confused with the last modified time of the RRD file. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +=head2 sources + + my @sources = $rrd->sources($rrdfile); + +This method returns a list of all of the data source names contained within +the RRD file. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +=head2 add_source + + $rrd->add_source($rrdfile, + source_name => "TYPE" + ); + +You may add a new data source to an existing RRD file using this method. Only +one data source name can be added at a time. You must also specify the data +source type. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +This method can be called internally by the C method to automatically +add missing data sources. + +=head2 rename_source + + $rrd->rename_source($rrdfile, "old_datasource", "new_datasource"); + +You may rename a data source in an existing RRD file using this method. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +=head2 graph + + my %rtn = $rrd->graph($rrdfile, + destination => "/path/to/write/graph/images", + basename => "graph_basename", + timestamp => "both", # graph, rrd, both or none + periods => [ qw(week month) ], # omit to generate all graphs + sources => [ qw(source_name1 source_name2 source_name3) ], + source_colors => [ qw(ff0000 aa3333 000000) ], + source_labels => [ ("My Source 1", "My Source Two", "Source 3") ], + source_drawtypes => [ qw(LINE1 AREA LINE) ], + line_thickness => 2, + extended_legend => 1, + rrd_graph_option => "value", + rrd_graph_option => "value", + rrd_graph_option => "value" + ); + +This method will render one or more graph images that show the data in the +RRD file. + +The number of image files that are created depends on the retention period +of the RRD file. Hourly, 6 hourly, 12 hourly, daily, weekly, monthly, annual +and 3year graphs will be created if there is enough data in the RRD file to +accomodate them. + +The image filenames will start with either the basename of the RRD +file, or whatever is specified by the C parameter. The second part +of the filename will be "-hourly", "-6hourly", "-12hourly", "-daily", +"-weekly", "-monthly", "-annual" or "-3year" depending on the period that +is being graphed. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +Graph options specific to RRD::Simple are: + +=over 4 + +=item destination + +The C parameter is optional, and it will default to the same +path location as that of the RRD file specified by C<$rrdfile>. Specifying +this value will force the resulting graph images to be written to this path +location. (The specified path must be a valid directory with the sufficient +permissions to write the graph images). + +=item basename + +The C parameter is optional. This parameter specifies the basename +of the graph image files that will be created. If not specified, it will +default to the name of the RRD file. For example, if you specify a basename +name of C, the following graph image files will be created in the +C directory: + + mygraph-daily.png + mygraph-weekly.png + mygraph-monthly.png + mygraph-annual.png + +The default file format is C, but this can be explicitly specified using +the standard RRDs options. (See below). + +=item timestamp + + my %rtn = $rrd->graph($rrdfile, + timestamp => "graph", # graph, rrd, both or none + ); + +The C parameter is optional, but will default to "graph". This +parameter specifies which "last updated" timestamps should be added to the +bottom right hand corner of the graph. + +Valid values are: "graph" - the timestamp of when the graph was last rendered +will be used, "rrd" - the timestamp of when the RRD file was last updated will +be used, "both" - both the timestamps of when the graph and RRD file were last +updated will be used, "none" - no timestamp will be used. + +=item periods + +The C parameter is an optional list of periods that graphs should +be generated for. If omitted, all possible graphs will be generated and not +restricted to any specific subset. See the L method for a list of +valid time periods. + +=item sources + +The C parameter is optional. This parameter should be an array of +data source names that you want to be plotted. All data sources will be +plotted by default. + +=item source_colors + + my %rtn = $rrd->graph($rrdfile, + source_colors => [ qw(ff3333 ff00ff ffcc99) ], + ); + + %rtn = $rrd->graph($rrdfile, + source_colors => { source_name1 => "ff3333", + source_name2 => "ff00ff", + source_name3 => "ffcc99", }, + ); + +The C parameter is optional. This parameter should be an +array or hash of hex triplet colors to be used for the plotted data source +lines. A selection of vivid primary colors will be set by default. + +=item source_labels + + my %rtn = $rrd->graph($rrdfile, + sources => [ qw(source_name1 source_name2 source_name3) ], + source_labels => [ ("My Source 1","My Source Two","Source 3") ], + ); + + %rtn = $rrd->graph($rrdfile, + source_labels => { source_name1 => "My Source 1", + source_name2 => "My Source Two", + source_name3 => "Source 3", }, + ); + +The C parameter is optional. The parameter should be an +array or hash of labels to be placed in the legend/key underneath the +graph. An array can only be used if the C parameter is also +specified, since the label index position in the array will directly +relate to the data source index position in the C array. + +The data source names will be used in the legend/key by default if no +C parameter is specified. + +=item source_drawtypes + + my %rtn = $rrd->graph($rrdfile, + source_drawtypes => [ qw(LINE1 AREA LINE) ], + ); + + %rtn = $rrd->graph($rrdfile, + source_colors => { source_name1 => "LINE1", + source_name2 => "AREA", + source_name3 => "LINE", }, + ); + + %rtn = $rrd->graph($rrdfile, + sources => [ qw(system user iowait idle) ] + source_colors => [ qw(AREA STACK STACK STACK) ], + ); + +The C parameter is optional. This parameter should be an +array or hash of drawing/plotting types to be used for the plotted data source +lines. By default all data sources are drawn as lines (LINE), but data sources +may also be drawn as filled areas (AREA). Valid values are, LINE, LINEI +(where I represents the thickness of the line in pixels), AREA or STACK. + +=item line_thickness + +Specifies the thickness of the data lines drawn on the graphs for +any data sources that have not had a specific line thickness already +specified using the C option. +Valid values are 1, 2 and 3 (pixels). + +=item extended_legend + +If set to boolean true, prints more detailed information in the graph legend +by adding the minimum, maximum and last values recorded on the graph for each +data source. + +=back + +Common RRD graph options are: + +=over 4 + +=item title + +A horizontal string at the top of the graph. + +=item vertical_label + +A vertically placed string at the left hand side of the graph. + +=item width + +The width of the canvas (the part of the graph with the actual data +and such). This defaults to 400 pixels. + +=item height + +The height of the canvas (the part of the graph with the actual data +and such). This defaults to 100 pixels. + +=back + +For examples on how to best use the C method, refer to the example +scripts that are bundled with this module in the examples/ directory. A +complete list of parameters can be found at +L. + +=head2 retention_period + + my $seconds = $rrd->retention_period($rrdfile); + +This method will return the maximum period of time (in seconds) that the RRD +file will store data for. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +=head2 info + + my $info = $rrd->info($rrdfile); + +This method will return a complex data structure containing details about +the RRD file, including RRA and data source information. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +=head2 heartbeat + + my $heartbeat = $rrd->heartbeat($rrdfile, "dsname"); + my @rtn = $rrd->heartbeat($rrdfile, "dsname", 600); + +This method will return the current heartbeat of a data source, or set a +new heartbeat of a data source. + +C<$rrdfile> is optional and will default to using the RRD filename specified +by the C constructor method, or C<$0.rrd>. (Script basename with the file +extension of .rrd). + +=head1 VARIABLES + +=head2 $RRD::Simple::DEBUG + +Debug and trace information will be printed to STDERR if this variable +is set to 1 (boolean true). + +This variable will take its value from C<$ENV{DEBUG}>, if it exists, +otherwise it will default to 0 (boolean false). This is a normal package +variable and may be safely modified at any time. + +=head2 $RRD::Simple::DEFAULT_DSTYPE + +This variable is used as the default data source type when creating or +adding new data sources, when no other data source type is explicitly +specified. + +This variable will take its value from C<$ENV{DEFAULT_DSTYPE}>, if it +exists, otherwise it will default to C. This is a normal package +variable and may be safely modified at any time. + +=head1 EXPORTS + +You can export the following functions if you do not wish to go through +the extra effort of using the OO interface: + + create + update + last_update (synonym for the last() method) + sources + add_source + rename_source + graph + retention_period + info + heartbeat + +The tag C is available to easily export everything: + + use RRD::Simple qw(:all); + +See the examples and unit tests in this distribution for more +details. + +=head1 SEE ALSO + +L, L, L, +L, examples/*.pl, +L, +L + +=head1 VERSION + +$Id: Simple.pm 1100 2008-01-24 17:39:35Z nicolaw $ + +=head1 AUTHOR + +Nicola Worthington + +L + +If you like this software, why not show your appreciation by sending the +author something nice from her +L? +( http://www.amazon.co.uk/gp/registry/1VZXC59ESWYK0?sort=priority ) + +=head1 COPYRIGHT + +Copyright 2005,2006,2007,2008 Nicola Worthington. + +This software is licensed under The Apache Software License, Version 2.0. + +L + +=cut + +__END__ + + + diff --git a/lib/upgrade_utilities.pl b/lib/upgrade_utilities.pl new file mode 100644 index 000000000..578d4cdf3 --- /dev/null +++ b/lib/upgrade_utilities.pl @@ -0,0 +1,68 @@ +package upgrade_utilities; + +#set of subroutines that will store code that perform system-wide updates for new MH versions +use strict; +use RRD::Simple; + +#added dependancy lib/site/RRD/Simple.pm +#weather_rrd_update.pl +#update 06/15/17 12:24:00 PM Oops2: /Users/howard/Develop/mh/data/rrd/weather_data.rrd: expected 107 data source readings (got 37) from 1497551040 + +sub upgrade_checks { + + &rrd_new_datasources(); +} + +sub rrd_new_datasources { + &main::print_log("[Updater] : Checking RRD Schemas"); + my $rrd = RRD::Simple->new(); + + my @sources = ( $main::config_parms{data_dir} . "/rrd/weather_data.rrd" ); + push @sources, $main::config_parms{weather_data_rrd} if ( defined $main::config_parms{weather_data_rrd} and $main::config_parms{weather_data_rrd} ); + + my %dschk; + my %newds; + + #for MH 4.3, add in some TempSpares as well as 30 placeholders + $dschk{'4.3'} = "dsgauge020"; + @{ $newds{'4.3'} } = ( + { "NAME" => 'tempspare11', "TYPE" => "GAUGE" }, + { "NAME" => 'tempspare12', "TYPE" => "GAUGE" }, + { "NAME" => 'tempspare13', "TYPE" => "GAUGE" }, + { "NAME" => 'tempspare14', "TYPE" => "GAUGE" }, + { "NAME" => 'tempspare15', "TYPE" => "GAUGE" } + ); + for ( my $i = 1; $i < 21; $i++ ) { + push @{ $newds{'4.3'} }, { "NAME" => 'dsgauge' . sprintf( "%03d", $i ), "TYPE" => "GAUGE" }; + } + for ( my $i = 1; $i < 11; $i++ ) { + push @{ $newds{'4.3'} }, { "NAME" => 'dsderive' . sprintf( "%03d", $i ), "TYPE" => "DERIVE" }; + } + + foreach my $rrdfile (@sources) { + if ( -e $rrdfile ) { + &main::print_log("[Updater::RRD] : Checking file $rrdfile..."); + + my %rrd_ds = map { $_ => 1 } $rrd->sources($rrdfile); + + foreach my $key ( keys %dschk ) { + + unless ( exists $rrd_ds{ $dschk{$key} } ) { + foreach my $ds ( @{ $newds{$key} } ) { + unless ( exists $rrd_ds{ $ds->{NAME} } ) { + &main::print_log("[Updater::RRD] : v$key Adding new Data Source name:$ds->{NAME} type:$ds->{TYPE}"); + $rrd->add_source( $rrdfile, $ds->{NAME} => $ds->{TYPE} ); #could also be DERIVE + } + else { + &main::print_log("[Updater::RRD] : v$key Skipping Existing Data Source $ds->{NAME}"); + + } + + } + } + } + } + } +} + +1; From 6828de3c21a8078b861c78f2d830592795405166 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 16 Jun 2017 16:00:22 -0600 Subject: [PATCH 176/209] Add new Datasources to MH --- bin/mh | 2 + code/common/weather_rrd_update.pl | 184 +++- lib/site/RRD/Simple.pm | 1553 +++++++++++++++++++++++++++++ lib/upgrade_utilities.pl | 52 + 4 files changed, 1778 insertions(+), 13 deletions(-) diff --git a/bin/mh b/bin/mh index 6e8b0594e..092514ede 100755 --- a/bin/mh +++ b/bin/mh @@ -790,6 +790,7 @@ sub setup { require 'xml_server.pl'; require 'menu_code.pl'; require 'trigger_code.pl'; + require 'upgrade_utilities.pl'; eval "use JSON"; # Used in mh/lib/json_server.pl JSON Web Attributes if ($@) { @@ -1247,6 +1248,7 @@ sub setup { print "Done with setup\n\n"; &ia7_utilities::ia7_update_collection; #Check if the collections need updating. + &upgrade_utilities::upgrade_checks; } diff --git a/code/common/weather_rrd_update.pl b/code/common/weather_rrd_update.pl index 812096efc..097e67759 100644 --- a/code/common/weather_rrd_update.pl +++ b/code/common/weather_rrd_update.pl @@ -2,7 +2,7 @@ # $Date$ # $Revision$ ##################################################################### -# NOM : weather_rrd_update.pl +# NAME : weather_rrd_update.pl # DESCRIPTION : #@ Create/update RRD database with the weather informations, #@ Create/update CSV file database with the weather informations, @@ -73,6 +73,8 @@ # - Initialize the altitude of the local weather station, # In feet, used to calculate the sea level barometric pressure # altitude = 0 +# - Don't convert a set of RRDs to Celcius +# rrd_noconvert = list,of,rrd,DS,names #-------------------------------------------------------------------- # HISTORY #-------------------------------------------------------------------- @@ -126,6 +128,9 @@ # - calculation for In/Hg when $config_params{weather_uom_baro} = "in" # - changed format for Sealevel pressure in In/Hg to have two (2) # - decimal places +# 6/20/17 2.1 H Plato +# - added in support for 35 new DSs. Also enabled a config_parm +# rrd_noconvert for any RRDS that shouldn't be converted to C automatically ##################################################################### use RRDs; @@ -171,6 +176,41 @@ HumidSpare9 TempSpare10 HumidSpare10 + TempSpare11 + TempSpare12 + TempSpare13 + TempSpare14 + TempSpare15 + DSGauge001 + DSGauge002 + DSGauge003 + DSGauge004 + DSGauge005 + DSGauge006 + DSGauge007 + DSGauge008 + DSGauge009 + DSGauge010 + DSGauge011 + DSGauge012 + DSGauge013 + DSGauge014 + DSGauge015 + DSGauge016 + DSGauge017 + DSGauge018 + DSGauge019 + DSGauge020 + DSDerive001 + DSDerive002 + DSDerive003 + DSDerive004 + DSDerive005 + DSDerive006 + DSDerive007 + DSDerive008 + DSDerive009 + DSDerive010 ); my $rrdDataTransferFile = $config_parms{data_dir} . '/rrdtransfer.pl'; @@ -247,7 +287,12 @@ sub weather_rrd_rename_chill_to_apparent { $rrd_HumidSpare2, $rrd_DewSpare2, $rrd_TempSpare3, $rrd_HumidSpare3, $rrd_DewSpare3, $rrd_TempSpare4, $rrd_HumidSpare4, $rrd_TempSpare5, $rrd_HumidSpare5, $rrd_TempSpare6, $rrd_HumidSpare6, $rrd_TempSpare7, $rrd_HumidSpare7, $rrd_TempSpare8, $rrd_HumidSpare8, $rrd_TempSpare9, $rrd_HumidSpare9, $rrd_TempSpare10, - $rrd_HumidSpare10 + $rrd_HumidSpare10, $rrd_TempSpare11, $rrd_TempSpare12, $rrd_TempSpare13, $rrd_TempSpare14, $rrd_TempSpare15, + $rrd_DSGauge001, $rrd_DSGauge002, $rrd_DSGauge003, $rrd_DSGauge004, $rrd_DSGauge005, $rrd_DSGauge006, + $rrd_DSGauge007, $rrd_DSGauge008, $rrd_DSGauge009, $rrd_DSGauge010, $rrd_DSGauge011, $rrd_DSGauge012, + $rrd_DSGauge013, $rrd_DSGauge014, $rrd_DSGauge015, $rrd_DSGauge016, $rrd_DSGauge017, $rrd_DSGauge018, + $rrd_DSGauge019, $rrd_DSGauge020, $rrd_DSDerive001, $rrd_DSDerive002, $rrd_DSDerive003, $rrd_DSDerive004, + $rrd_DSDerive005, $rrd_DSDerive006, $rrd_DSDerive007, $rrd_DSDerive008, $rrd_DSDerive009, $rrd_DSDerive010 ); foreach my $sensor (@rrd_sensors) { @@ -264,12 +309,28 @@ sub weather_rrd_rename_chill_to_apparent { my @d; &create_rrd($time) unless -e $RRD; - if ( $config_parms{weather_uom_temp} eq 'C' ) { - grep { $_ = convert_c2f($_) unless $_ eq 'U' } ( - $rrd_TempOutdoor, $rrd_TempIndoor, $rrd_TempSpare1, $rrd_TempSpare2, $rrd_TempSpare3, $rrd_TempSpare4, - $rrd_TempSpare5, $rrd_TempSpare6, $rrd_TempSpare7, $rrd_TempSpare8, $rrd_TempSpare9, $rrd_TempSpare10, - $rrd_DewOutdoor, $rrd_DewIndoor, $rrd_DewSpare1, $rrd_DewSpare2, $rrd_DewSpare3, $rrd_TempOutdoorApparent + my $i = 0; + my @conv = ( + 'rrd_TempOutdoor', 'rrd_TempIndoor', 'rrd_TempSpare1', 'rrd_TempSpare2', 'rrd_TempSpare3', 'rrd_TempSpare4', + 'rrd_TempSpare5', 'rrd_TempSpare6', 'rrd_TempSpare7', 'rrd_TempSpare8', 'rrd_TempSpare9', 'rrd_TempSpare10', + 'rrd_TempSpare11', 'rrd_TempSpare12', 'rrd_TempSpare13', 'rrd_TempSpare14', 'rrd_TempSpare15', + 'rrd_DewOutdoor', 'rrd_DewIndoor', 'rrd_DewSpare1', 'rrd_DewSpare2', 'rrd_DewSpare3', 'rrd_TempOutdoorApparent', + 'rrd_DSGauge001', 'rrd_DSGauge002', 'rrd_DSGauge003', 'rrd_DSGauge004', 'rrd_DSGauge005', 'rrd_DSGauge006', + 'rrd_DSGauge007', 'rrd_DSGauge008', 'rrd_DSGauge009', 'rrd_DSGauge010', 'rrd_DSGauge011', 'rrd_DSGauge012', + 'rrd_DSGauge013', 'rrd_DSGauge014', 'rrd_DSGauge015', 'rrd_DSGauge016', 'rrd_DSGauge017', 'rrd_DSGauge018', + 'rrd_DSGauge019', 'rrd_DSGauge020', 'rrd_DSDerive001', 'rrd_DSDerive002', 'rrd_DSDerive003', 'rrd_DSDerive004', + 'rrd_DSDerive005', 'rrd_DSDerive006', 'rrd_DSDerive007', 'rrd_DSDerive008', 'rrd_DSDerive009', 'rrd_DSDerive010' + ); + grep { $_ = convert_c2f($_) unless ($_ eq 'U' or &noconvert($conv[$i++]))} ( + $rrd_TempOutdoor, $rrd_TempIndoor, $rrd_TempSpare1, $rrd_TempSpare2, $rrd_TempSpare3, $rrd_TempSpare4, + $rrd_TempSpare5, $rrd_TempSpare6, $rrd_TempSpare12, $rrd_TempSpare13, $rrd_TempSpare14, $rrd_TempSpare15, + $rrd_DewOutdoor, $rrd_DewIndoor, $rrd_DewSpare1, $rrd_DewSpare2, $rrd_DewSpare3, $rrd_TempOutdoorApparent, + $rrd_DSGauge001, $rrd_DSGauge002, $rrd_DSGauge003, $rrd_DSGauge004, $rrd_DSGauge005, $rrd_DSGauge006, + $rrd_DSGauge007, $rrd_DSGauge008, $rrd_DSGauge009, $rrd_DSGauge010, $rrd_DSGauge011, $rrd_DSGauge012, + $rrd_DSGauge013, $rrd_DSGauge014, $rrd_DSGauge015, $rrd_DSGauge016, $rrd_DSGauge017, $rrd_DSGauge018, + $rrd_DSGauge019, $rrd_DSGauge020, $rrd_DSDerive001, $rrd_DSDerive002, $rrd_DSDerive003, $rrd_DSDerive004, + $rrd_DSDerive005, $rrd_DSDerive006, $rrd_DSDerive007, $rrd_DSDerive008, $rrd_DSDerive009, $rrd_DSDerive010 ); } @@ -300,7 +361,12 @@ sub weather_rrd_rename_chill_to_apparent { $rrd_HumidSpare2, $rrd_DewSpare2, $rrd_TempSpare3, $rrd_HumidSpare3, $rrd_DewSpare3, $rrd_TempSpare4, $rrd_HumidSpare4, $rrd_TempSpare5, $rrd_HumidSpare5, $rrd_TempSpare6, $rrd_HumidSpare6, $rrd_TempSpare7, $rrd_HumidSpare7, $rrd_TempSpare8, $rrd_HumidSpare8, $rrd_TempSpare9, $rrd_HumidSpare9, $rrd_TempSpare10, - $rrd_HumidSpare10 + $rrd_HumidSpare10, $rrd_TempSpare11, $rrd_TempSpare12, $rrd_TempSpare13, $rrd_TempSpare14, $rrd_TempSpare15, + $rrd_DSGauge001, $rrd_DSGauge002, $rrd_DSGauge003, $rrd_DSGauge004, $rrd_DSGauge005, $rrd_DSGauge006, + $rrd_DSGauge007, $rrd_DSGauge008, $rrd_DSGauge009, $rrd_DSGauge010, $rrd_DSGauge011, $rrd_DSGauge012, + $rrd_DSGauge013, $rrd_DSGauge014, $rrd_DSGauge015, $rrd_DSGauge016, $rrd_DSGauge017, $rrd_DSGauge018, + $rrd_DSGauge019, $rrd_DSGauge020, $rrd_DSDerive001, $rrd_DSDerive002, $rrd_DSDerive003, $rrd_DSDerive004, + $rrd_DSDerive005, $rrd_DSDerive006, $rrd_DSDerive007, $rrd_DSDerive008, $rrd_DSDerive009, $rrd_DSDerive010 ); #print "@d\n"; @@ -443,6 +509,41 @@ sub create_rrd { "DS:humidspare9:GAUGE:$RRD_HEARTBEAT:0:100", "DS:tempspare10:GAUGE:$RRD_HEARTBEAT:-150:150", "DS:humidspare10:GAUGE:$RRD_HEARTBEAT:0:100", + "DS:tempspare11:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:tempspare12:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:tempspare13:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:tempspare14:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:tempspare15:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge001:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge002:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge003:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge004:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge005:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge006:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge007:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge008:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge009:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge010:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge011:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge012:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge013:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge014:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge015:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge016:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge017:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge018:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge019:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsgauge020:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive001:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive002:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive003:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive004:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive005:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive006:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive007:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive008:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive009:GAUGE:$RRD_HEARTBEAT:-150:150", + "DS:dsderive010:GAUGE:$RRD_HEARTBEAT:-150:150", 'RRA:AVERAGE:0.5:1:801', # details for 6 hours (agregate 1 minute) @@ -599,20 +700,77 @@ sub update_csv { 'Humidity module 9', 'Temperature 10', 'Humidity module 10', + 'Temperature 11', + 'Temperature 12', + 'Temperature 13', + 'Temperature 14', + 'Temperature 15', + 'Gauge 001', + 'Gauge 002', + 'Gauge 003', + 'Gauge 004', + 'Gauge 005', + 'Gauge 006', + 'Gauge 007', + 'Gauge 008', + 'Gauge 009', + 'Gauge 010', + 'Gauge 011', + 'Gauge 012', + 'Gauge 013', + 'Gauge 014', + 'Gauge 015', + 'Gauge 016', + 'Gauge 017', + 'Gauge 018', + 'Gauge 019', + 'Gauge 020', + 'Derive 001', + 'Derive 002', + 'Derive 003', + 'Derive 004', + 'Derive 005', + 'Derive 006', + 'Derive 007', + 'Derive 008', + 'Derive 009', + 'Derive 010', "\n" ); &logit( "$config_parms{weather_data_csv}.$Year_Month_Now", $row_header, 0 ); } $row_data = join( ";", - $time, $log_annee, $log_mois, $log_jour, $log_heure, $log_minute, $log_seconde, $data[0], $data[1], - $data[2], $data[3], $data[4], $data[5], $data[6], $data[7], $data[8], $data[9], $data[10], - $data[11], $data[12], $data[13], $data[14], $data[15], $data[16], $data[17], $data[18], $data[19], - $data[20], $data[21], $data[22], $data[23], $data[24], $data[25], $data[26], $data[27], $data[28], - $data[29], $data[30], $data[31], $data[32], $data[33], $data[34], $data[35], $data[36], "\n" ); + $time, $log_annee, $log_mois, $log_jour, $log_heure, $log_minute, $log_seconde, $data[0], $data[1], $data[2], + $data[3], $data[4], $data[5], $data[6], $data[7], $data[8], $data[9], $data[10], $data[11], $data[12], + $data[13], $data[14], $data[15], $data[16], $data[17], $data[18], $data[19], $data[20], $data[21], $data[22], + $data[23], $data[24], $data[25], $data[26], $data[27], $data[28], $data[29], $data[30], $data[31], $data[32], + $data[33], $data[34], $data[35], $data[36], $data[37], $data[38], $data[39], $data[40], $data[41], $data[42], + $data[43], $data[44], $data[45], $data[46], $data[47], $data[48], $data[49], $data[50], $data[51], $data[52], + $data[53], $data[54], $data[55], $data[56], $data[57], $data[58], $data[59], $data[60], $data[61], $data[62], + $data[63], $data[64], $data[65], $data[66], $data[67], $data[68], $data[69], $data[70], $data[71], $data[72], + "\n" ); &logit( "$config_parms{weather_data_csv}.$Year_Month_Now", $row_data, 0 ); } +#============================================================================== +# Check if data source needs to be converted to C +#============================================================================== +sub noconvert { + my ($ds) = @_; + my $return = 0; + + $ds =~ s/^rrd_//; + + if (defined $config_parms{rrd_noconvert}) { + my $string = $config_parms{rrd_noconvert}; + $return = 1 if ($string =~ m/$ds/i); + } + print_log("test: ds=$ds, return=$return") if $debug; + return $return +} + + sub analyze_rrd_rain { my $RRD = "$config_parms{weather_data_rrd}"; diff --git a/lib/site/RRD/Simple.pm b/lib/site/RRD/Simple.pm index cf3375e9d..4db574622 100644 --- a/lib/site/RRD/Simple.pm +++ b/lib/site/RRD/Simple.pm @@ -20,12 +20,16 @@ ############################################################ package RRD::Simple; +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes # vim:ts=8:sw=8:tw=78 use strict; require Exporter; use RRDs; +<<<<<<< Updated upstream use POSIX qw(strftime); # Used for strftime in graph() method use Carp qw(croak cluck confess carp); use File::Spec qw(); # catfile catdir updir path rootdir tmpdir @@ -50,12 +54,41 @@ $DEFAULT_DSTYPE ||= exists $ENV{DEFAULT_DSTYPE} ? $ENV{DEFAULT_DSTYPE} : 'GAUGE' my $objstore = {}; +======= +use POSIX qw(strftime); # Used for strftime in graph() method +use Carp qw(croak cluck confess carp); +use File::Spec qw(); # catfile catdir updir path rootdir tmpdir +use File::Basename qw(fileparse dirname basename); + +use vars qw($VERSION $DEBUG $DEFAULT_DSTYPE + @EXPORT @EXPORT_OK %EXPORT_TAGS @ISA); + +$VERSION = '1.44' || sprintf('%d', q$Revision: 1100 $ =~ /(\d+)/g); + +@ISA = qw(Exporter); +@EXPORT = qw(); +@EXPORT_OK = qw(create update last_update graph info rename_source + add_source sources retention_period last_values + heartbeat); +# delete_source minimum maximum +%EXPORT_TAGS = (all => \@EXPORT_OK); + +$DEBUG ||= $ENV{DEBUG} ? 1 : 0; +$DEFAULT_DSTYPE ||= exists $ENV{DEFAULT_DSTYPE} + ? $ENV{DEFAULT_DSTYPE} : 'GAUGE'; + +my $objstore = {}; + + + +>>>>>>> Stashed changes # # Methods # # Create a new object sub new { +<<<<<<< Updated upstream TRACE(">>> new()"); ref( my $class = shift ) && croak 'Class name required'; croak 'Odd number of elements passed when even was expected' if @_ % 2; @@ -512,6 +545,474 @@ sub graph { # Version 1.39 - Change the return from an array to a hash (semi backward compatible) # my @rtn; my %rtn; +======= + TRACE(">>> new()"); + ref(my $class = shift) && croak 'Class name required'; + croak 'Odd number of elements passed when even was expected' if @_ % 2; + + # Conjure up an invisible object + my $self = bless \(my $dummy), $class; + $objstore->{_refaddr($self)} = {@_}; + my $stor = $objstore->{_refaddr($self)}; + #my $self = { @_ }; + + # - Added "file" support in 1.42 - see sub _guess_filename. + # - Added "on_missing_ds"/"on_missing_source" support in 1.44 + # - Added "tmpdir" support in 1.44 + my @validkeys = qw(rrdtool cf default_dstype default_dst tmpdir + file on_missing_ds on_missing_source); + my $validkeys = join('|', @validkeys); + + cluck('Unrecognised parameters passed: '. + join(', ',grep(!/^$validkeys$/,keys %{$stor}))) + if (grep(!/^$validkeys$/,keys %{$stor}) && $^W); + + $stor->{rrdtool} = _find_binary(exists $stor->{rrdtool} ? + $stor->{rrdtool} : 'rrdtool'); + + # Check that "default_dstype" isn't complete rubbish (validation from v1.44+) + # GAUGE | COUNTER | DERIVE | ABSOLUTE | COMPUTE + # http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html + $stor->{default_dstype} ||= $stor->{default_dst}; + croak "Invalid value passed in parameter default_dstype; '$stor->{default_dstype}'" + if defined $stor->{default_dstype} + && $stor->{default_dstype} !~ /^(GAUGE|COUNTER|DERIVE|ABSOLUTE|COMPUTE|[A-Z]{1,10})$/i; + + # Check that "on_missing_ds" isn't complete rubbish. + # Added "on_missing_ds"/"on_missing_source" support in 1.44 + $stor->{on_missing_ds} ||= $stor->{on_missing_source}; + if (defined $stor->{on_missing_ds}) { + $stor->{on_missing_ds} = lc($stor->{on_missing_ds}); + croak "Invalid value passed in parameter on_missing_ds; '$stor->{on_missing_ds}'" + if $stor->{on_missing_ds} !~ /^\s*(add|ignore|die|croak)\s*$/i; + } + $stor->{on_missing_ds} ||= 'add'; # default to add + + #$stor->{cf} ||= [ qw(AVERAGE MIN MAX LAST) ]; + # By default, now only create RRAs for AVERAGE and MAX, like + # mrtg v2.13.2. This is to save disk space and processing time + # during updates etc. + $stor->{cf} ||= [ qw(AVERAGE MAX) ]; + $stor->{cf} = [ $stor->{cf} ] if !ref($stor->{cf}); + + DUMP($class,$self); + DUMP('$stor',$stor); + return $self; +} + + +# Create a new RRD file +sub create { + TRACE(">>> create()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{_refaddr($self)}; + +# +# +# + + # Grab or guess the filename + my $rrdfile = $stor->{file}; + + # Odd number of values and first is not a valid scheme + # then the first value is likely an RRD file name. + if (@_ % 2 && !_valid_scheme($_[0])) { + $rrdfile = shift; + + # Even number of values and the second value is a valid + # scheme then the first value is likely an RRD file name. + } elsif (!(@_ % 2) && _valid_scheme($_[1])) { + $rrdfile = shift; + + # If we still don't have an RRD file name then try and + # guess what it is + } elsif (!defined $rrdfile) { + $rrdfile = _guess_filename($stor); + } + +# +# +# + + # Barf if the rrd file already exists + croak "RRD file '$rrdfile' already exists" if -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + # We've been given a scheme specifier + # Until v1.32 'year' was the default. As of v1.33 'mrtg' + # is the new default scheme. + #my $scheme = 'year'; + my $scheme = 'mrtg'; + if (@_ % 2 && _valid_scheme($_[0])) { + $scheme = _valid_scheme($_[0]); + shift @_; + } + TRACE("Using scheme: $scheme"); + + croak 'Odd number of elements passed when even was expected' if @_ % 2; + my %ds = @_; + DUMP('%ds',\%ds); + + my $rrdDef = _rrd_def($scheme); + my @def = ('-b', time - _seconds_in($scheme,120)); + push @def, '-s', ($rrdDef->{step} || 300); + + # Add data sources + for my $ds (sort keys %ds) { + $ds =~ s/[^a-zA-Z0-9_-]//g; + push @def, sprintf('DS:%s:%s:%s:%s:%s', + substr($ds,0,19), + uc($ds{$ds}), + ($rrdDef->{heartbeat} || 600), + 'U','U' + ); + } + + # Add RRA definitions + my %cf; + for my $cf (@{$stor->{cf}}) { + $cf{$cf} = $rrdDef->{rra}; + } + for my $cf (sort keys %cf) { + for my $rra (@{$cf{$cf}}) { + push @def, sprintf('RRA:%s:%s:%s:%s', + $cf, 0.5, $rra->{step}, $rra->{rows} + ); + } + } + + DUMP('@def',\@def); + + # Pass to RRDs for execution + my @rtn = RRDs::create($rrdfile, @def); + my $error = RRDs::error(); + croak($error) if $error; + DUMP('RRDs::info',RRDs::info($rrdfile)); + return wantarray ? @rtn : \@rtn; +} + + +# Update an RRD file with some data values +sub update { + TRACE(">>> update()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{_refaddr($self)}; + +# +# +# + + # Grab or guess the filename + my $rrdfile = $stor->{file}; + + # Odd number of values and first is does not look + # like a recent unix time stamp then the first value + # is likely to be an RRD file name. + if (@_ % 2 && $_[0] !~ /^[1-9][0-9]{8,10}$/i) { + $rrdfile = shift; + + # Even number of values and the second value looks like + # a recent unix time stamp then the first value is + # likely to be an RRD file name. + } elsif (!(@_ % 2) && $_[1] =~ /^[1-9][0-9]{8,10}$/i) { + $rrdfile = shift; + + # If we still don't have an RRD file name then try and + # guess what it is + } elsif (!defined $rrdfile) { + $rrdfile = _guess_filename($stor); + } + +# +# +# + + # We've been given an update timestamp + my $time = time(); + if (@_ % 2 && $_[0] =~ /^([1-9][0-9]{8,10})$/i) { + $time = $1; + shift @_; + } + TRACE("Using update time: $time"); + + # Try to automatically create it + unless (-f $rrdfile) { + my $default_dstype = defined $stor->{default_dstype} ? $stor->{default_dstype} : $DEFAULT_DSTYPE; + cluck("RRD file '$rrdfile' does not exist; attempting to create it ", + "using default DS type of '$default_dstype'") if $^W; + my @args; + for (my $i = 0; $i < @_; $i++) { + push @args, ($_[$i],$default_dstype) unless $i % 2; + } + $self->create($rrdfile,@args); + } + + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + croak 'Odd number of elements passed when even was expected' if @_ % 2; + + my %ds; + while (my $ds = shift(@_)) { + $ds =~ s/[^a-zA-Z0-9_-]//g; + $ds = substr($ds,0,19); + $ds{$ds} = shift(@_); + $ds{$ds} = 'U' if !defined($ds{$ds}); + } + DUMP('%ds',\%ds); + + # Validate the data source names as we add them + my @sources = $self->sources($rrdfile); + for my $ds (sort keys %ds) { + # Check the data source names + if (!grep(/^$ds$/,@sources)) { + TRACE("Supplied data source '$ds' does not exist in pre-existing ". + "RRD data source list: ". join(', ',@sources)); + + # If someone got the case wrong, remind and correct them + if (grep(/^$ds$/i,@sources)) { + cluck("Data source '$ds' does not exist; automatically ", + "correcting it to '",(grep(/^$ds$/i,@sources))[0], + "' instead") if $^W; + $ds{(grep(/^$ds$/i,@sources))[0]} = $ds{$ds}; + delete $ds{$ds}; + + # If it's not just a case sensitivity typo and the data source + # name really doesn't exist in this RRD file at all, regardless + # of case, then ... + } else { + # Ignore the offending missing data source name + if ($stor->{on_missing_ds} eq 'ignore') { + TRACE("on_missing_ds = ignore; ignoring data supplied for missing data source '$ds'"); + + # Fall on our bum and die horribly if requested to do so + } elsif ($stor->{on_missing_ds} eq 'die' || $stor->{on_missing_ds} eq 'croak') { + croak "Supplied data source '$ds' does not exist in RRD file '$rrdfile'"; + + # Default behaviour is to automatically add the new data source + # to the RRD file in order to preserve the existing default + # functionality of RRD::Simple + } else { + TRACE("on_missing_ds = add (or not set at all/default); ". + "automatically adding new data source '$ds'"); + + # Otherwise add any missing or new data sources on the fly + # Decide what DS type and heartbeat to use + my $info = RRDs::info($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + + my %dsTypes; + for my $key (grep(/^ds\[.+?\]\.type$/,keys %{$info})) { + $dsTypes{$info->{$key}}++; + } + DUMP('%dsTypes',\%dsTypes); + my $dstype = (sort { $dsTypes{$b} <=> $dsTypes{$a} } + keys %dsTypes)[0]; + TRACE("\$dstype = $dstype"); + + $self->add_source($rrdfile,$ds,$dstype); + } + } + } + } + + # Build the def + my @def = ('--template'); + push @def, join(':',sort keys %ds); + push @def, join(':',$time,map { $ds{$_} } sort keys %ds); + DUMP('@def',\@def); + + # Pass to RRDs to execute the update + my @rtn = RRDs::update($rrdfile, @def); + my $error = RRDs::error(); + croak($error) if $error; + return wantarray ? @rtn : \@rtn; +} + + +# Get the last time an RRD was updates +sub last_update { __PACKAGE__->last(@_); } +sub last { + TRACE(">>> last()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = shift || _guess_filename($stor); + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + my $last = RRDs::last($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + return $last; +} + + +# Get a list of data sources from an RRD file +sub sources { + TRACE(">>> sources()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = shift || _guess_filename($stor); + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + my $info = RRDs::info($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + + my @ds; + foreach (keys %{$info}) { + if (/^ds\[(.+)?\]\.type$/) { + push @ds, $1; + } + } + return wantarray ? @ds : \@ds; +} + + +# Add a new data source to an RRD file +sub add_source { + TRACE(">>> add_source()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + unless (-f $rrdfile) { + cluck("RRD file '$rrdfile' does not exist; attempting to create it") + if $^W; + return $self->create($rrdfile,@_); + } + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + # Check that we will understand this RRD file version first + my $info = $self->info($rrdfile); +# croak "Unable to add a new data source to $rrdfile; ", +# "RRD version $info->{rrd_version} is too new" +# if ($info->{rrd_version}+1-1) > 1; + + my ($ds,$dstype) = @_; + TRACE("\$ds = $ds"); + TRACE("\$dstype = $dstype"); + + my $rrdfileBackup = "$rrdfile.bak"; + confess "$rrdfileBackup already exists; please investigate" + if -e $rrdfileBackup; + + # Decide what heartbeat to use + my $heartbeat = $info->{ds}->{(sort { + $info->{ds}->{$b}->{minimal_heartbeat} <=> + $info->{ds}->{$b}->{minimal_heartbeat} + } keys %{$info->{ds}})[0]}->{minimal_heartbeat}; + TRACE("\$heartbeat = $heartbeat"); + + # Make a list of expected sources after the addition + my $TgtSources = join(',',sort(($self->sources($rrdfile),$ds))); + + # Add the data source + my $new_rrdfile = ''; + eval { + $new_rrdfile = _modify_source( + $rrdfile,$stor,$ds, + 'add',$dstype,$heartbeat, + ); + }; + + # Barf if the eval{} got upset + if ($@) { + croak "Failed to add new data source '$ds' to RRD file '$rrdfile': $@"; + } + + # Barf of the new RRD file doesn't exist + unless (-f $new_rrdfile) { + croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ", + "new RRD file '$new_rrdfile' does not exist"; + } + + # Barf is the new data source isn't in our new RRD file + unless ($TgtSources eq join(',',sort($self->sources($new_rrdfile)))) { + croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ", + "new RRD file '$new_rrdfile' does not contain expected data ", + "source names"; + } + + # Try and move the new RRD file in to place over the existing one + # and then remove the backup RRD file if sucessfull + if (File::Copy::move($rrdfile,$rrdfileBackup) && + File::Copy::move($new_rrdfile,$rrdfile)) { + unless (unlink($rrdfileBackup)) { + cluck("Failed to remove back RRD file '$rrdfileBackup': $!") + if $^W; + } + } else { + croak "Failed to move new RRD file in to place: $!"; + } +} + + +# Make a number of graphs for an RRD file +sub graph { + TRACE(">>> graph()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + + # How much data do we have to graph? + my $period = $self->retention_period($rrdfile); + + # Check at RRA CFs are available and graph the best one + my $info = $self->info($rrdfile); + my $cf = 'AVERAGE'; + for my $rra (@{$info->{rra}}) { + if ($rra->{cf} eq 'AVERAGE') { + $cf = 'AVERAGE'; last; + } elsif ($rra->{cf} eq 'MAX') { + $cf = 'MAX'; + } elsif ($rra->{cf} eq 'MIN' && $cf ne 'MAX') { + $cf = 'MIN'; + } elsif ($cf ne 'MAX' && $cf ne 'MIN') { + $cf = $rra->{cf}; + } + } + TRACE("graph() - \$cf = $cf"); + + # Create graphs which we have enough data to populate + # Version 1.39 - Change the return from an array to a hash (semi backward compatible) + # my @rtn; + my %rtn; +>>>>>>> Stashed changes ## ## TODO @@ -519,6 +1020,7 @@ sub graph { ### data resolution (stepping) is fine enough (sub minute) ## +<<<<<<< Updated upstream #i my @graph_periods = qw(hour 6hour 12hour day week month year 3years); my @graph_periods; my %param = @_; @@ -974,11 +1476,470 @@ sub _create_graph { } $fmt = "%s:%s#%s:%-${longest_label}s%s"; } +======= + #i my @graph_periods = qw(hour 6hour 12hour day week month year 3years); + my @graph_periods; + my %param = @_; + if (defined $param{'periods'}) { + my %map = qw(daily day weekly week monthly month annual year 3years 3years); + for my $period (_convert_to_array($param{'periods'})) { + $period = lc($period); + if (_valid_scheme($period)) { + push @graph_periods, $period; + } elsif (_valid_scheme($map{$period})) { + push @graph_periods, $map{$period}; + } else { + croak "Invalid period value passed in parameter periods; '$period'"; + } + } + } + push @graph_periods, qw(day week month year 3years) unless @graph_periods; + + for my $type (@graph_periods) { + next if $period < _seconds_in($type); + TRACE("graph() - \$type = $type"); + # push @rtn, [ ($self->_create_graph($rrdfile, $type, $cf, @_)) ]; + $rtn{_alt_graph_name($type)} = [ ($self->_create_graph($rrdfile, $type, $cf, @_)) ]; + } + + # return @rtn; + return wantarray ? %rtn : \%rtn; +} + + +# Rename an existing data source +sub rename_source { + TRACE(">>> rename_source()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + my ($old,$new) = @_; + croak "No old data source name specified" unless defined $old && length($old); + croak "No new data source name specified" unless defined $new && length($new); + croak "Data source '$old' does not exist in RRD file '$rrdfile'" + unless grep($_ eq $old, $self->sources($rrdfile)); + + my @rtn = RRDs::tune($rrdfile,'-r',"$old:$new"); + my $error = RRDs::error(); + croak($error) if $error; + return wantarray ? @rtn : \@rtn; +} + + +# Get or set a data source heartbeat +sub heartbeat { + TRACE(">>> heartbeat()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ >= 3 ? shift : + _isLegalDsName($_[0]) && $_[1] =~ /^[0-9]+$/ ? + _guess_filename($stor) : shift; + croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; + TRACE("Using filename: $rrdfile"); + + # Explode if we get no data source name + my ($ds,$new_heartbeat) = @_; + croak "No data source name was specified" unless defined $ds && length($ds); + + # Check the data source name exists + my $info = $self->info($rrdfile); + my $heartbeat = $info->{ds}->{$ds}->{minimal_heartbeat}; + croak "Data source '$ds' does not exist in RRD file '$rrdfile'" + unless defined $heartbeat && $heartbeat; + + if (!defined $new_heartbeat) { + return wantarray ? ($heartbeat) : $heartbeat; + } + + my @rtn = !defined $new_heartbeat ? ($heartbeat) : (); + # Redefine the data source heartbeat + if (defined $new_heartbeat) { + croak "New minimal heartbeat '$new_heartbeat' is not a valid positive integer" + unless $new_heartbeat =~ /^[1-9][0-9]*$/; + my @rtn = RRDs::tune($rrdfile,'-h',"$ds:$new_heartbeat"); + my $error = RRDs::error(); + croak($error) if $error; + } + + return wantarray ? @rtn : \@rtn; +} + + +# Fetch data point information from an RRD file +sub fetch { + TRACE(">>> fetch()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + +} + + +# Fetch the last values inserted in to an RRD file +sub last_values { + TRACE(">>> last_values()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + + # When was the RRD last updated? + my $lastUpdated = $self->last($rrdfile); + + # Is there a LAST RRA? + my $info = $self->info($rrdfile); + my $hasLastRRA = 0; + for my $rra (@{$info->{rra}}) { + $hasLastRRA++ if $rra->{cf} eq 'LAST'; + } + return if !$hasLastRRA; + + # What's the largest heartbeat in the RRD file data sources? + my $largestHeartbeat = 1; + for (map { $info->{ds}->{$_}->{'minimal_heartbeat'} } keys(%{$info->{ds}})) { + $largestHeartbeat = $_ if $_ > $largestHeartbeat; + } + + my @def = ('LAST', + '-s', $lastUpdated - ($largestHeartbeat * 2), + '-e', $lastUpdated + ); + + # Pass to RRDs to execute + my ($time,$heartbeat,$ds,$data) = RRDs::fetch($rrdfile, @def); + my $error = RRDs::error(); + croak($error) if $error; + + # Put it in to a nice easy format + my %rtn = (); + for my $rec (reverse @{$data}) { + for (my $i = 0; $i < @{$rec}; $i++) { + if (defined $rec->[$i] && !exists($rtn{$ds->[$i]})) { + $rtn{$ds->[$i]} = $rec->[$i]; + } + } + } + + # Well, I'll be buggered if the LAST CF does what you'd think + # it's meant to do. If anybody can give me some decent documentation + # on what the LAST CF does, and/or how to get the last value put + # in to an RRD, then I'll admit that this method exists and export + # it too. + + return wantarray ? %rtn : \%rtn; +} + + +# Return how long this RRD retains data for +sub retention_period { + TRACE(">>> retention_period()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + my $info = $self->info(@_); + return if !defined($info); + + my $duration = $info->{step}; + for my $rra (@{$info->{rra}}) { + my $secs = ($rra->{pdp_per_row} * $info->{step}) * $rra->{rows}; + $duration = $secs if $secs > $duration; + } + + return wantarray ? ($duration) : $duration; +} + + +# Fetch information about an RRD file +sub info { + TRACE(">>> info()"); + my $self = shift; + unless (ref $self && UNIVERSAL::isa($self, __PACKAGE__)) { + unshift @_, $self unless $self eq __PACKAGE__; + $self = new __PACKAGE__; + } + + # Grab or guess the filename + my $stor = $objstore->{_refaddr($self)}; + my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); + + my $info = RRDs::info($rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + DUMP('$info',$info); + + my $rtn; + for my $key (sort(keys(%{$info}))) { + if ($key =~ /^rra\[(\d+)\]\.([a-z_]+)/) { + $rtn->{rra}->[$1]->{$2} = $info->{$key}; + } elsif (my (@dsKey) = $key =~ /^ds\[([[A-Za-z0-9\_]+)?\]\.([a-z_]+)/) { + $rtn->{ds}->{$1}->{$2} = $info->{$key}; + } elsif ($key !~ /\[[\d_a-z]+\]/i) { + $rtn->{$key} = $info->{$key}; + } + } + + # Return the information + DUMP('$rtn',$rtn); + return $rtn; +} + + +# Convert a string or an array reference to an array +sub _convert_to_array { + return unless defined $_[0]; + if (!ref $_[0]) { + $_[0] =~ /^\s+|\s+$/g; + return split(/(?:\s+|\s*,\s*)/,$_[0]); + } elsif (ref($_[0]) eq 'ARRAY') { + return @{$_[0]}; + } + return; +} + + +# Make a single graph image +sub _create_graph { + TRACE(">>> _create_graph()"); + my $self = shift; + my $rrdfile = shift; + my $type = _valid_scheme(shift) || 'day'; + my $cf = shift || 'AVERAGE'; + + my $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK):.+/; + $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK|TEXTALIGN):.+/ + if $RRDs::VERSION >= 1.3; # http://oss.oetiker.ch/rrdtool-trac/wiki/RRDtool13 + + my %param; + my @command_param; + while (my $k = shift) { + if ($k =~ /$command_regex/) { + push @command_param, $k; + shift; + } else { + $k =~ s/_/-/g; + $param{lc($k)} = shift; + } + } + + # If we get this custom parameter then it would have already + # been dealt with by the calling graph() method so we should + # ditch it right here and now! + delete $param{'periods'}; + + # Specify some default values + $param{'end'} ||= $self->last($rrdfile) || time(); + $param{'imgformat'} ||= 'PNG'; # RRDs >1.3 now support PDF, SVG and EPS + # $param{'alt-autoscale'} ||= ''; + # $param{'alt-y-grid'} ||= ''; + + # Define what to call the image + my $basename = defined $param{'basename'} && + $param{'basename'} =~ /^[0-9a-z_\.-]+$/i ? + $param{'basename'} : + (fileparse($rrdfile,'\.[^\.]+'))[0]; + delete $param{'basename'}; + + # Define where to write the image + my $image = sprintf('%s-%s.%s',$basename, + _alt_graph_name($type), lc($param{'imgformat'})); + if ($param{'destination'}) { + $image = File::Spec->catfile($param{'destination'},$image); + } + delete $param{'destination'}; + + # Specify timestamps- new for version 1.41 + my $timestamp = !defined $param{'timestamp'} || + $param{'timestamp'} !~ /^(graph|rrd|both|none)$/i + ? 'graph' + : lc($param{'timestamp'}); + delete $param{'timestamp'}; + + # Specify extended legend - new for version 1.35 + my $extended_legend = defined $param{'extended-legend'} && + $param{'extended-legend'} ? 1 : 0; + delete $param{'extended-legend'}; + + # Define how thick the graph lines should be + my $line_thickness = defined $param{'line-thickness'} && + $param{'line-thickness'} =~ /^[123]$/ ? + $param{'line-thickness'} : 1; + delete $param{'line-thickness'}; + + # Colours is an alias to colors + if (exists $param{'source-colours'} && !exists $param{'source-colors'}) { + $param{'source-colors'} = $param{'source-colours'}; + delete $param{'source-colours'}; + } + + # Allow source line colors to be set + my @source_colors = (); + my %source_colors = (); + if (defined $param{'source-colors'}) { + #if (ref($param{'source-colors'}) eq 'ARRAY') { + # @source_colors = @{$param{'source-colors'}}; + if (ref($param{'source-colors'}) eq 'HASH') { + %source_colors = %{$param{'source-colors'}}; + } else { + @source_colors = _convert_to_array($param{'source-colors'}); + } + } + delete $param{'source-colors'}; + + # Define which data sources we should plot + my @rrd_sources = $self->sources($rrdfile); + my @ds = !exists $param{'sources'} + ? @rrd_sources + #: defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY' + #? @{$param{'sources'}} + : defined $param{'sources'} + ? _convert_to_array($param{'sources'}) + : (); + + # Allow source legend source_labels to be set + my %source_labels = (); + if (defined $param{'source-labels'}) { + if (ref($param{'source-labels'}) eq 'HASH') { + %source_labels = %{$param{'source-labels'}}; + } elsif (ref($param{'source-labels'}) eq 'ARRAY') { + if (defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY') { + for (my $i = 0; $i < @{$param{'source-labels'}}; $i++) { + $source_labels{$ds[$i]} = $param{'source-labels'}->[$i] + if defined $ds[$i]; + } + } elsif ($^W) { + carp "source_labels may only be an array if sources is also ". + "an specified and valid array"; + } + } + } + delete $param{'source-labels'}; + + # Allow source legend source_drawtypes to be set + # ... "oops" ... yes, this is quite obviously + # copy and paste code from the chunk above. I'm + # sorry. I'll rationalise it some other day if + # it's necessary. + my %source_drawtypes = (); + if (defined $param{'source-drawtypes'}) { + if (ref($param{'source-drawtypes'}) eq 'HASH') { + %source_drawtypes = %{$param{'source-drawtypes'}}; + } elsif (ref($param{'source-drawtypes'}) eq 'ARRAY') { + if (defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY') { + for (my $i = 0; $i < @{$param{'source-drawtypes'}}; $i++) { + $source_drawtypes{$ds[$i]} = $param{'source-drawtypes'}->[$i] + if defined $ds[$i]; + } + } elsif ($^W) { + carp "source_drawtypes may only be an array if sources is ". + "also an specified and valid array" + } + } + + # Validate the values we have and set default thickness + while (my ($k,$v) = each %source_drawtypes) { + if ($v !~ /^(LINE[1-9]?|STACK|AREA)$/) { + delete $source_drawtypes{$k}; + carp "source_drawtypes may be LINE, LINEn, AREA or STACK ". + "only; value '$v' is not valid" if $^W; + } + $source_drawtypes{$k} = uc($v); + $source_drawtypes{$k} .= $line_thickness if $v eq 'LINE'; + } + } + delete $param{'source-drawtypes'}; + delete $param{'sources'}; + + # Specify a default start time + $param{'start'} ||= $param{'end'} - _seconds_in($type,115); + + # Suffix the title with the period information + $param{'title'} ||= basename($rrdfile); + $param{'title'} .= ' - [Hourly Graph]' if $type eq 'hour'; + $param{'title'} .= ' - [6 Hour Graph]' if $type eq '6hour' || $type eq 'quarterday'; + $param{'title'} .= ' - [12 Hour Graph]' if $type eq '12hour' || $type eq 'halfday'; + $param{'title'} .= ' - [Daily Graph]' if $type eq 'day'; + $param{'title'} .= ' - [Weekly Graph]' if $type eq 'week'; + $param{'title'} .= ' - [Monthly Graph]' if $type eq 'month'; + $param{'title'} .= ' - [Annual Graph]' if $type eq 'year'; + $param{'title'} .= ' - [3 Year Graph]' if $type eq '3years'; + + # Convert our parameters in to an RRDs friendly defenition + my @def; + while (my ($k,$v) = each %param) { + if (length($k) == 1) { # Short single character options + $k = '-'.uc($k); + } else { # Long options + $k = "--$k"; + } + for my $v ((ref($v) eq 'ARRAY' ? @{$v} : ($v))) { + if (!defined $v || !length($v)) { + push @def, $k; + } else { + push @def, "$k=$v"; + } + } + } + + # Populate a cycling tied scalar for line colors + @source_colors = qw( + FF0000 00FF00 0000FF 00FFFF FF00FF FFFF00 000000 + 990000 009900 000099 009999 990099 999900 999999 + 552222 225522 222255 225555 552255 555522 555555 + ) unless @source_colors > 0; + # Pre 1.35 colours + # FF0000 00FF00 0000FF FFFF00 00FFFF FF00FF 000000 + # 550000 005500 000055 555500 005555 550055 555555 + # AA0000 00AA00 0000AA AAAA00 00AAAA AA00AA AAAAAA + tie my $colour, 'RRD::Simple::_Colour', \@source_colors; + + my $fmt = '%s:%s#%s:%s%s'; + my $longest_label = 1; + if ($extended_legend) { + for my $ds (@ds) { + my $len = length( defined $source_labels{$ds} ? + $source_labels{$ds} : $ds ); + $longest_label = $len if $len > $longest_label; + } + $fmt = "%s:%s#%s:%-${longest_label}s%s"; + } + + +>>>>>>> Stashed changes ## ## ## +<<<<<<< Updated upstream # Create the @cmd my @cmd = ( $image, @def ); @@ -1080,11 +2041,120 @@ sub _create_graph { return ( $image, @rtn ); } +======= + # Create the @cmd + my @cmd = ($image,@def); + + # Add the data sources definitions to @cmd + for my $ds (@rrd_sources) { + # Add the data source definition + push @cmd, sprintf('DEF:%s=%s:%s:%s',$ds,$rrdfile,$ds,$cf); + } + + # Add the data source draw commands to the grap/@cmd + for my $ds (@ds) { + # Stack operates differently in RRD 1.2 or higher + my $drawtype = defined $source_drawtypes{$ds} ? $source_drawtypes{$ds} + : "LINE$line_thickness"; + my $stack = ''; + if ($RRDs::VERSION >= 1.2 && $drawtype eq 'STACK') { + $drawtype = 'AREA'; + $stack = ':STACK'; + } + + # Draw the line (and add to the legend) + push @cmd, sprintf($fmt, + $drawtype, + $ds, + (defined $source_colors{$ds} ? $source_colors{$ds} : $colour), + (defined $source_labels{$ds} ? $source_labels{$ds} : $ds), + $stack + ); + + # New for version 1.39 + # Return the min,max,last information in the graph() return @rtn + if ($RRDs::VERSION >= 1.2) { + push @cmd, sprintf('VDEF:%sMIN=%s,MINIMUM',$ds,$ds); + push @cmd, sprintf('VDEF:%sMAX=%s,MAXIMUM',$ds,$ds); + push @cmd, sprintf('VDEF:%sLAST=%s,LAST',$ds,$ds); + # Don't automatically add this unless we have to + # push @cmd, sprintf('VDEF:%sAVERAGE=%s,AVERAGE',$ds,$ds); + push @cmd, sprintf('PRINT:%sMIN:%s min %%1.2lf',$ds,$ds); + push @cmd, sprintf('PRINT:%sMAX:%s max %%1.2lf',$ds,$ds); + push @cmd, sprintf('PRINT:%sLAST:%s last %%1.2lf',$ds,$ds); + } else { + push @cmd, sprintf('PRINT:%s:MIN:%s min %%1.2lf',$ds,$ds); + push @cmd, sprintf('PRINT:%s:MAX:%s max %%1.2lf',$ds,$ds); + push @cmd, sprintf('PRINT:%s:LAST:%s last %%1.2lf',$ds,$ds); + } + + # New for version 1.35 + if ($extended_legend) { + if ($RRDs::VERSION >= 1.2) { + # Moved the VDEFs to the block of code above which is + # always run, regardless of the extended legend + push @cmd, sprintf('GPRINT:%sMIN: min\:%%10.2lf\g',$ds); + push @cmd, sprintf('GPRINT:%sMAX: max\:%%10.2lf\g',$ds); + push @cmd, sprintf('GPRINT:%sLAST: last\:%%10.2lf\l',$ds); + } else { + push @cmd, sprintf('GPRINT:%s:MIN: min\:%%10.2lf\g',$ds); + push @cmd, sprintf('GPRINT:%s:MAX: max\:%%10.2lf\g',$ds); + push @cmd, sprintf('GPRINT:%s:LAST: last\:%%10.2lf\l',$ds); + } + } + } + + + + + + + # Push the post command defs on to the stack + push @cmd, @command_param; + + # Add a comment stating when the graph was last updated + if ($timestamp ne 'none') { + #push @cmd, ('COMMENT:\s','COMMENT:\s','COMMENT:\s'); + push @cmd, ('COMMENT:\s','COMMENT:\s'); + push @cmd, 'COMMENT:\s' unless $extended_legend || !@ds; + my $timefmt = '%a %d/%b/%Y %T %Z'; + + if ($timestamp eq 'rrd' || $timestamp eq 'both') { + my $time = sprintf('RRD last updated: %s\r', + strftime($timefmt,localtime((stat($rrdfile))[9])) + ); + $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2 + push @cmd, "COMMENT:$time"; + } + + if ($timestamp eq 'graph' || $timestamp eq 'both') { + my $time = sprintf('Graph last updated: %s\r', + strftime($timefmt,localtime(time)) + ); + $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2 + push @cmd, "COMMENT:$time"; + } + } + + DUMP('@cmd',\@cmd); + + # Generate the graph + my @rtn = RRDs::graph(@cmd); + my $error = RRDs::error(); + croak($error) if $error; + return ($image,@rtn); +} + + + + +>>>>>>> Stashed changes # # Private subroutines # no warnings 'redefine'; +<<<<<<< Updated upstream sub UNIVERSAL::a_sub_not_likely_to_be_here { ref( $_[0] ) } use warnings 'redefine'; @@ -1113,6 +2183,37 @@ sub _isLegalDsName { #rrdtool-1.0.49/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" #rrdtool-1.2.11/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" +======= +sub UNIVERSAL::a_sub_not_likely_to_be_here { ref($_[0]) } +use warnings 'redefine'; + + +sub _blessed ($) { + local($@, $SIG{__DIE__}, $SIG{__WARN__}); + return length(ref($_[0])) + ? eval { $_[0]->a_sub_not_likely_to_be_here } + : undef +} + + +sub _refaddr($) { + my $pkg = ref($_[0]) or return undef; + if (_blessed($_[0])) { + bless $_[0], 'Scalar::Util::Fake'; + } else { + $pkg = undef; + } + "$_[0]" =~ /0x(\w+)/; + my $i = do { local $^W; hex $1 }; + bless $_[0], $pkg if defined $pkg; + return $i; +} + + +sub _isLegalDsName { +#rrdtool-1.0.49/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" +#rrdtool-1.2.11/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" +>>>>>>> Stashed changes ## ## TODO @@ -1120,6 +2221,7 @@ sub _isLegalDsName { ## to see if it has changed or not ## +<<<<<<< Updated upstream return $_[0] =~ /^[a-zA-Z0-9_-]{1,19}$/; } @@ -1143,12 +2245,37 @@ sub _rrd_def { ], }; } +======= + return $_[0] =~ /^[a-zA-Z0-9_-]{1,19}$/; +} + + +sub _rrd_def { + croak('Pardon?!') if ref $_[0]; + my $type = _valid_scheme(shift); + + # This is calculated the same way as mrtg v2.13.2 + if ($type eq 'mrtg') { + my $step = 5; # 5 minutes + return { + step => $step * 60, + heartbeat => $step * 60 * 2, + rra => [( + { step => 1, rows => int(4000 / $step) }, # 800 + { step => int( 30 / $step), rows => 800 }, # if $step < 30 + { step => int( 120 / $step), rows => 800 }, + { step => int(1440 / $step), rows => 800 }, + )], + }; + } +>>>>>>> Stashed changes ## ## TODO ## 1.45 Add higher resolution for hour, 6hour and 12 hour ## +<<<<<<< Updated upstream my $step = 1; # 1 minute highest resolution my $rra = { step => $step * 60, @@ -1257,6 +2384,115 @@ sub _alt_graph_name { return $type; } +======= + my $step = 1; # 1 minute highest resolution + my $rra = { + step => $step * 60, + heartbeat => $step * 60 * 2, + rra => [( + # Actual $step resolution (for 1.25 days retention) + { step => 1, rows => int( _minutes_in('day',125) / $step) }, + )], + }; + + if ($type =~ /^(week|month|year|3years)$/i) { + push @{$rra->{rra}}, { + step => int( 30 / $step), + rows => int( _minutes_in('week',125) / int(30/$step) ) + }; # 30 minute average + + push @{$rra->{rra}}, { + step => int( 120 / $step), + rows => int( _minutes_in($type eq 'week' ? 'week' : 'month',125) + / int(120/$step) ) + }; # 2 hour average + } + + if ($type =~ /^(year|3years)$/i) { + push @{$rra->{rra}}, { + step => int(1440 / $step), + rows => int( _minutes_in($type,125) / int(1440/$step) ) + }; # 1 day average + } + + return $rra; +} + + +sub _odd { + return $_[0] % 2; +} + + +sub _even { + return !($_[0] % 2); +} + + +sub _valid_scheme { + TRACE(">>> _valid_scheme()"); + croak('Pardon?!') if ref $_[0]; + #if ($_[0] =~ /^(day|week|month|year|3years|mrtg)$/i) { + if ($_[0] =~ /^((?:6|12)?hour|(?:half)?day|week|month|year|3years|mrtg)$/i) { + TRACE("'".lc($1)."' is a valid scheme."); + return lc($1); + } + TRACE("'@_' is not a valid scheme."); + return undef; +} + + +sub _hours_in { return int((_seconds_in(@_)/60)/60); } +sub _minutes_in { return int(_seconds_in(@_)/60); } +sub _seconds_in { + croak('Pardon?!') if ref $_[0]; + my $str = lc(shift); + my $scale = shift || 100; + + return undef if !defined(_valid_scheme($str)); + + my %time = ( + # New for version 1.44 of RRD::Simple by + # popular request + 'hour' => 60 * 60, + '6hour' => 60 * 60 * 6, + 'quarterday' => 60 * 60 * 6, + '12hour' => 60 * 60 * 12, + 'halfday' => 60 * 60 * 12, + + 'day' => 60 * 60 * 24, + 'week' => 60 * 60 * 24 * 7, + 'month' => 60 * 60 * 24 * 31, + 'year' => 60 * 60 * 24 * 365, + '3years' => 60 * 60 * 24 * 365 * 3, + 'mrtg' => ( int(( 1440 / 5 )) * 800 ) * 60, # mrtg v2.13.2 + ); + + my $rtn = $time{$str} * ($scale / 100); + return $rtn; +} + + +sub _alt_graph_name { + croak('Pardon?!') if ref $_[0]; + my $type = _valid_scheme(shift); + return unless defined $type; + + # New for version 1.44 of RRD::Simple by popular request + return 'hourly' if $type eq 'hour'; + return '6hourly' if $type eq '6hour' || $type eq 'quarterday'; + return '12hourly' if $type eq '12hour' || $type eq 'halfday'; + + return 'daily' if $type eq 'day'; + return 'weekly' if $type eq 'week'; + return 'monthly' if $type eq 'month'; + return 'annual' if $type eq 'year'; + return '3years' if $type eq '3years'; + return $type; +} + + +>>>>>>> Stashed changes ## ## TODO ## 1.45 - Check to see if there is now native support in RRDtool to @@ -1266,6 +2502,7 @@ sub _alt_graph_name { ## sub _modify_source { +<<<<<<< Updated upstream croak('Pardon?!') if ref $_[0]; my ( $rrdfile, $stor, $ds, $action, $dstype, $heartbeat ) = @_; my $rrdtool = $stor->{rrdtool}; @@ -1353,6 +2590,93 @@ sub _modify_source { # Add the DS definition if ( $action eq 'add' && !$marker->{addedNewDS} && // ) { print OUT <{rrdtool}; + $rrdtool = '' unless defined $rrdtool; + + # Decide what action we should take + if ($action !~ /^(add|del)$/) { + my $caller = (caller(1))[3]; + $action = $caller =~ /\badd\b/i ? 'add' : + $caller =~ /\bdel(ete)?\b/i ? 'del' : undef; + } + croak "Unknown or no action passed to method _modify_source()" + unless defined $action && $action =~ /^(add|del)$/; + + require File::Copy; + require File::Temp; + + # Generate an XML dump of the RRD file + # - Added "tmpdir" support in 1.44 + my $tmpdir = defined $stor->{tmpdir} ? $stor->{tmpdir} : File::Spec->tmpdir(); + my ($tempXmlFileFH,$tempXmlFile) = File::Temp::tempfile( + DIR => $tmpdir, + TEMPLATE => 'rrdXXXXX', + SUFFIX => '.tmp', + ); + + # Check that we managed to get a sane temporary filename + croak "File::Temp::tempfile() failed to return a temporary filename" + unless defined $tempXmlFile; + TRACE("_modify_source(): \$tempXmlFile = $tempXmlFile"); + + # Try the internal perl way first (portable) + eval { + # Patch to rrd_dump.c emailed to Tobi and developers + # list by nicolaw/heds on 2006/01/08 + if ($RRDs::VERSION >= 1.2013) { + my @rtn = RRDs::dump($rrdfile,$tempXmlFile); + my $error = RRDs::error(); + croak($error) if $error; + } + }; + + # Do it the old fashioned way + if ($@ || !-f $tempXmlFile || (stat($tempXmlFile))[7] < 200) { + croak "rrdtool binary '$rrdtool' does not exist or is not executable" + if !defined $rrdtool || !-f $rrdtool || !-x $rrdtool; + _safe_exec(sprintf('%s dump %s > %s',$rrdtool,$rrdfile,$tempXmlFile)); + } + + # Read in the new temporary XML dump file + open(IN, "<$tempXmlFile") || croak "Unable to open '$tempXmlFile': $!"; + + # Open XML output file + # my $tempImportXmlFile = File::Temp::tmpnam(); + # - Added "tmpdir" support in 1.44 + my ($tempImportXmlFileFH,$tempImportXmlFile) = File::Temp::tempfile( + DIR => $tmpdir, + TEMPLATE => 'rrdXXXXX', + SUFFIX => '.tmp', + ); + open(OUT, ">$tempImportXmlFile") + || croak "Unable to open '$tempImportXmlFile': $!"; + + # Create a marker hash ref to store temporary state + my $marker = { + currentDSIndex => 0, + deleteDSIndex => undef, + addedNewDS => 0, + parse => 0, + version => 1, + }; + + # Parse the input XML file + while (local $_ = ) { + chomp; + + # Find out what index number the existing DS definition is in + if ($action eq 'del' && /\s*(\S+)\s*<\/name>/) { + $marker->{deleteIndex} = $marker->{currentDSIndex} if $1 eq $ds; + $marker->{currentDSIndex}++; + } + + # Add the DS definition + if ($action eq 'add' && !$marker->{addedNewDS} && //) { + print OUT <>>>>>> Stashed changes $ds $dstype @@ -1367,6 +2691,7 @@ sub _modify_source { EndDS +<<<<<<< Updated upstream $marker->{addedNewDS} = 1; } @@ -1464,6 +2789,100 @@ EndDS return wantarray ? ($new_rrdfile) : $new_rrdfile; } +======= + $marker->{addedNewDS} = 1; + } + + # Insert DS under CDP_PREP entity + if ($action eq 'add' && /<\/cdp_prep>/) { + # Version 0003 RRD from rrdtool 1.2x + if ($marker->{version} >= 3) { + print OUT " \n"; + print OUT " 0.0000000000e+00 \n"; + print OUT " 0.0000000000e+00 \n"; + print OUT " NaN \n"; + print OUT " 0 \n"; + print OUT " \n"; + + # Version 0001 RRD from rrdtool 1.0x + } else { + print OUT " NaN 0 \n"; + } + } + + # Look for the end of an RRA + if (/<\/database>/) { + $marker->{parse} = 0; + + # Find the dumped RRD version (must take from the XML, not the RRD) + } elsif (/\s*([0-9\.]+)\s*<\/version>/) { + $marker->{version} = ($1 + 1 - 1); + } + + # Add the extra " NaN " under the RRAs. Just print normal lines + if ($marker->{parse} == 1) { + if ($_ =~ /^(.+ .+)(<\/row>.*)/) { + print OUT $1; + print OUT " NaN " if $action eq 'add'; + print OUT $2; + print OUT "\n"; + } + } else { + print OUT "$_\n"; + } + + # Look for the start of an RRA + if (//) { + $marker->{parse} = 1; + } + } + + # Close the files + close(IN) || croak "Unable to close '$tempXmlFile': $!"; + close(OUT) || croak "Unable to close '$tempImportXmlFile': $!"; + + # Import the new output file in to the old RRD filename + my $new_rrdfile = File::Temp::tmpnam(); + TRACE("_modify_source(): \$new_rrdfile = $new_rrdfile"); + + # Try the internal perl way first (portable) + eval { + if ($RRDs::VERSION >= 1.0049) { + my @rtn = RRDs::restore($tempImportXmlFile,$new_rrdfile); + my $error = RRDs::error(); + croak($error) if $error; + } + }; + + # Do it the old fashioned way + if ($@ || !-f $new_rrdfile || (stat($new_rrdfile))[7] < 200) { + croak "rrdtool binary '$rrdtool' does not exist or is not executable" + unless (-f $rrdtool && -x $rrdtool); + my $cmd = sprintf('%s restore %s %s',$rrdtool,$tempImportXmlFile,$new_rrdfile); + my $rtn = _safe_exec($cmd); + + # At least check the file is created + unless (-f $new_rrdfile) { + _nuke_tmp($tempXmlFile,$tempImportXmlFile); + croak "Command '$cmd' failed to create the new RRD file '$new_rrdfile': $rtn"; + } + } + + # Remove the temporary files + _nuke_tmp($tempXmlFile,$tempImportXmlFile); + sub _nuke_tmp { + for (@_) { + unlink($_) || + carp("Unable to unlink temporary file '$_': $!"); + } + } + + # Return the new RRD filename + return wantarray ? ($new_rrdfile) : $new_rrdfile; +} + + +>>>>>>> Stashed changes ## ## TODO ## 1.45 - Improve this _safe_exec function to see if it can be made @@ -1473,6 +2892,7 @@ EndDS ## sub _safe_exec { +<<<<<<< Updated upstream croak('Pardon?!') if ref $_[0]; my $cmd = shift; if ( $cmd =~ /^([\/\.\_\-a-zA-Z0-9 >]+)$/ ) { @@ -1558,6 +2978,97 @@ BEGIN { eval "use RRDs"; if ($@) { carp qq{ +======= + croak('Pardon?!') if ref $_[0]; + my $cmd = shift; + if ($cmd =~ /^([\/\.\_\-a-zA-Z0-9 >]+)$/) { + $cmd = $1; + TRACE($cmd); + system($cmd); + if ($? == -1) { + croak "Failed to execute command '$cmd': $!\n"; + } elsif ($? & 127) { + croak(sprintf("While executing command '%s', child died ". + "with signal %d, %s coredump\n", $cmd, + ($? & 127), ($? & 128) ? 'with' : 'without')); + } + my $exit_value = $? >> 8; + croak "Error caught from '$cmd'" if $exit_value != 0; + return $exit_value; + } else { + croak "Unexpected potentially unsafe command will not be executed: $cmd"; + } +} + + +sub _find_binary { + croak('Pardon?!') if ref $_[0]; + my $binary = shift || 'rrdtool'; + return $binary if -f $binary && -x $binary; + + my @paths = File::Spec->path(); + my $rrds_path = dirname($INC{'RRDs.pm'}); + push @paths, $rrds_path; + push @paths, File::Spec->catdir($rrds_path, + File::Spec->updir(),File::Spec->updir(),'bin'); + + for my $path (@paths) { + my $filename = File::Spec->catfile($path,$binary); + return $filename if -f $filename && -x $filename; + } + + my $path = File::Spec->catdir(File::Spec->rootdir(),'usr','local'); + if (opendir(DH,$path)) { + my @dirs = sort { $b cmp $a } grep(/^rrdtool/,readdir(DH)); + closedir(DH) || carp "Unable to close file handle: $!"; + for my $dir (@dirs) { + my $filename = File::Spec->catfile($path,$dir,'bin',$binary); + return $filename if -f $filename && -x $filename; + } + } +} + + +sub _guess_filename { + croak('Pardon?!') if !defined $_[0] || ref($_[0]) ne 'HASH'; + my $stor = shift; + if (defined $stor->{file}) { + TRACE("_guess_filename = \$stor->{file} = $stor->{file}"); + return $stor->{file}; + } + my ($basename, $dirname, $extension) = fileparse($0, '\.[^\.]+'); + TRACE("_guess_filename = calculated = $dirname$basename.rrd"); + return "$dirname$basename.rrd"; +} + + +sub DESTROY { + my $self = shift; + delete $objstore->{_refaddr($self)}; +} + + +sub TRACE { + return unless $DEBUG; + carp(shift()); +} + + +sub DUMP { + return unless $DEBUG; + eval { + require Data::Dumper; + $Data::Dumper::Indent = 2; + $Data::Dumper::Terse = 1; + carp(shift().': '.Data::Dumper::Dumper(shift())); + } +} + +BEGIN { + eval "use RRDs"; + if ($@) { + carp qq{ +>>>>>>> Stashed changes +-----------------------------------------------------------------------------+ | ERROR! -- Could not load RRDs.pm | | | @@ -1567,11 +3078,21 @@ BEGIN { +-----------------------------------------------------------------------------+ } unless $ENV{AUTOMATED_TESTING}; +<<<<<<< Updated upstream } } 1; +======= + } +} + + +1; + + +>>>>>>> Stashed changes ############################################################### # This tie code is from Tie::Cycle # written by brian d foy, @@ -1579,6 +3100,7 @@ BEGIN { package RRD::Simple::_Colour; sub TIESCALAR { +<<<<<<< Updated upstream my ( $class, $list_ref ) = @_; my @shallow_copy = map { $_ } @$list_ref; return unless UNIVERSAL::isa( $list_ref, 'ARRAY' ); @@ -1598,10 +3120,37 @@ sub STORE { return unless ref $list_ref eq ref []; return unless @$list_ref > 1; $self = [ 0, scalar @$list_ref, $list_ref ]; +======= + my ($class,$list_ref) = @_; + my @shallow_copy = map { $_ } @$list_ref; + return unless UNIVERSAL::isa( $list_ref, 'ARRAY' ); + my $self = [ 0, scalar @shallow_copy, \@shallow_copy ]; + bless $self, $class; +} + +sub FETCH { + my $self = shift; + my $index = $$self[0]++; + $$self[0] %= $self->[1]; + return $self->[2]->[ $index ]; +} + +sub STORE { + my ($self,$list_ref) = @_; + return unless ref $list_ref eq ref []; + return unless @$list_ref > 1; + $self = [ 0, scalar @$list_ref, $list_ref ]; +>>>>>>> Stashed changes } 1; +<<<<<<< Updated upstream +======= + + + +>>>>>>> Stashed changes =pod =head1 NAME @@ -2149,6 +3698,10 @@ L =cut +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes __END__ diff --git a/lib/upgrade_utilities.pl b/lib/upgrade_utilities.pl index 578d4cdf3..70b78ae7c 100644 --- a/lib/upgrade_utilities.pl +++ b/lib/upgrade_utilities.pl @@ -1,9 +1,14 @@ package upgrade_utilities; #set of subroutines that will store code that perform system-wide updates for new MH versions +<<<<<<< Updated upstream use strict; use RRD::Simple; +======= +use strict; +use RRD::Simple; +>>>>>>> Stashed changes #added dependancy lib/site/RRD/Simple.pm #weather_rrd_update.pl #update 06/15/17 12:24:00 PM Oops2: /Users/howard/Develop/mh/data/rrd/weather_data.rrd: expected 107 data source readings (got 37) from 1497551040 @@ -16,6 +21,7 @@ sub upgrade_checks { sub rrd_new_datasources { &main::print_log("[Updater] : Checking RRD Schemas"); my $rrd = RRD::Simple->new(); +<<<<<<< Updated upstream my @sources = ( $main::config_parms{data_dir} . "/rrd/weather_data.rrd" ); push @sources, $main::config_parms{weather_data_rrd} if ( defined $main::config_parms{weather_data_rrd} and $main::config_parms{weather_data_rrd} ); @@ -59,10 +65,56 @@ sub rrd_new_datasources { } } +======= + + my @sources = ($main::config_parms{data_dir} . "/rrd/weather_data.rrd"); + push @sources, $main::config_parms{weather_data_rrd} if (defined $main::config_parms{weather_data_rrd} and $main::config_parms{weather_data_rrd}); + + my %dschk; + my %newds; + #for MH 4.3, add in some TempSpares as well as 30 placeholders + $dschk{'4.3'} = "dsgauge020"; + @{$newds{'4.3'}} = ({"NAME" => 'tempspare11', "TYPE" => "GAUGE"}, + {"NAME" =>'tempspare12',"TYPE" => "GAUGE"}, + {"NAME" =>'tempspare13', "TYPE" => "GAUGE"}, + {"NAME" =>'tempspare14', "TYPE" => "GAUGE"}, + {"NAME" =>'tempspare15', "TYPE" => "GAUGE"}); + for (my $i=1; $i<21; $i++) { + push @{$newds{'4.3'}}, {"NAME" => 'dsgauge' . sprintf("%03d",$i), "TYPE" => "GAUGE"}; + } + for (my $i=1; $i<11; $i++) { + push @{$newds{'4.3'}}, {"NAME" => 'dsderive' . sprintf("%03d",$i), "TYPE" => "DERIVE"}; + } + + + foreach my $rrdfile (@sources) { + if (-e $rrdfile) { + &main::print_log("[Updater::RRD] : Checking file $rrdfile..."); + + my %rrd_ds = map { $_ => 1 } $rrd->sources($rrdfile); + + foreach my $key (keys %dschk) { + + unless (exists $rrd_ds{$dschk{$key}}) { + foreach my $ds (@{$newds{$key}}) { + unless (exists $rrd_ds{$ds->{NAME}}) { + &main::print_log("[Updater::RRD] : v$key Adding new Data Source name:$ds->{NAME} type:$ds->{TYPE}"); + $rrd->add_source($rrdfile, $ds->{NAME} => $ds->{TYPE}); #could also be DERIVE + } else { + &main::print_log("[Updater::RRD] : v$key Skipping Existing Data Source $ds->{NAME}"); + + } + + } +>>>>>>> Stashed changes } } } } } +<<<<<<< Updated upstream +1; +======= 1; +>>>>>>> Stashed changes From cee8c04f0c6b1b44d785421ee35e598b2d62895b Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 16 Jun 2017 16:30:52 -0600 Subject: [PATCH 177/209] upgrader utility --- lib/upgrade_utilities.pl | 58 +--------------------------------------- 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/lib/upgrade_utilities.pl b/lib/upgrade_utilities.pl index 70b78ae7c..0fb20e74a 100644 --- a/lib/upgrade_utilities.pl +++ b/lib/upgrade_utilities.pl @@ -1,14 +1,8 @@ package upgrade_utilities; #set of subroutines that will store code that perform system-wide updates for new MH versions -<<<<<<< Updated upstream -use strict; -use RRD::Simple; - -======= use strict; use RRD::Simple; ->>>>>>> Stashed changes #added dependancy lib/site/RRD/Simple.pm #weather_rrd_update.pl #update 06/15/17 12:24:00 PM Oops2: /Users/howard/Develop/mh/data/rrd/weather_data.rrd: expected 107 data source readings (got 37) from 1497551040 @@ -21,51 +15,6 @@ sub upgrade_checks { sub rrd_new_datasources { &main::print_log("[Updater] : Checking RRD Schemas"); my $rrd = RRD::Simple->new(); -<<<<<<< Updated upstream - - my @sources = ( $main::config_parms{data_dir} . "/rrd/weather_data.rrd" ); - push @sources, $main::config_parms{weather_data_rrd} if ( defined $main::config_parms{weather_data_rrd} and $main::config_parms{weather_data_rrd} ); - - my %dschk; - my %newds; - - #for MH 4.3, add in some TempSpares as well as 30 placeholders - $dschk{'4.3'} = "dsgauge020"; - @{ $newds{'4.3'} } = ( - { "NAME" => 'tempspare11', "TYPE" => "GAUGE" }, - { "NAME" => 'tempspare12', "TYPE" => "GAUGE" }, - { "NAME" => 'tempspare13', "TYPE" => "GAUGE" }, - { "NAME" => 'tempspare14', "TYPE" => "GAUGE" }, - { "NAME" => 'tempspare15', "TYPE" => "GAUGE" } - ); - for ( my $i = 1; $i < 21; $i++ ) { - push @{ $newds{'4.3'} }, { "NAME" => 'dsgauge' . sprintf( "%03d", $i ), "TYPE" => "GAUGE" }; - } - for ( my $i = 1; $i < 11; $i++ ) { - push @{ $newds{'4.3'} }, { "NAME" => 'dsderive' . sprintf( "%03d", $i ), "TYPE" => "DERIVE" }; - } - - foreach my $rrdfile (@sources) { - if ( -e $rrdfile ) { - &main::print_log("[Updater::RRD] : Checking file $rrdfile..."); - - my %rrd_ds = map { $_ => 1 } $rrd->sources($rrdfile); - - foreach my $key ( keys %dschk ) { - - unless ( exists $rrd_ds{ $dschk{$key} } ) { - foreach my $ds ( @{ $newds{$key} } ) { - unless ( exists $rrd_ds{ $ds->{NAME} } ) { - &main::print_log("[Updater::RRD] : v$key Adding new Data Source name:$ds->{NAME} type:$ds->{TYPE}"); - $rrd->add_source( $rrdfile, $ds->{NAME} => $ds->{TYPE} ); #could also be DERIVE - } - else { - &main::print_log("[Updater::RRD] : v$key Skipping Existing Data Source $ds->{NAME}"); - - } - - } -======= my @sources = ($main::config_parms{data_dir} . "/rrd/weather_data.rrd"); push @sources, $main::config_parms{weather_data_rrd} if (defined $main::config_parms{weather_data_rrd} and $main::config_parms{weather_data_rrd}); @@ -106,15 +55,10 @@ sub rrd_new_datasources { } } ->>>>>>> Stashed changes } } } } } -<<<<<<< Updated upstream -1; -======= -1; ->>>>>>> Stashed changes +1; \ No newline at end of file From fb59f5ed6adbe517985e010c274e689268730993 Mon Sep 17 00:00:00 2001 From: H Plato Date: Fri, 16 Jun 2017 20:38:07 -0600 Subject: [PATCH 178/209] un-perltidy CPAN module --- lib/site/RRD/Simple.pm | 1534 ---------------------------------------- 1 file changed, 1534 deletions(-) diff --git a/lib/site/RRD/Simple.pm b/lib/site/RRD/Simple.pm index 4db574622..cf9e0b547 100644 --- a/lib/site/RRD/Simple.pm +++ b/lib/site/RRD/Simple.pm @@ -20,41 +20,11 @@ ############################################################ package RRD::Simple; -<<<<<<< Updated upstream - -======= ->>>>>>> Stashed changes # vim:ts=8:sw=8:tw=78 use strict; require Exporter; use RRDs; -<<<<<<< Updated upstream -use POSIX qw(strftime); # Used for strftime in graph() method -use Carp qw(croak cluck confess carp); -use File::Spec qw(); # catfile catdir updir path rootdir tmpdir -use File::Basename qw(fileparse dirname basename); - -use vars qw($VERSION $DEBUG $DEFAULT_DSTYPE - @EXPORT @EXPORT_OK %EXPORT_TAGS @ISA); - -$VERSION = '1.44' || sprintf( '%d', q$Revision: 1100 $ =~ /(\d+)/g ); - -@ISA = qw(Exporter); -@EXPORT = qw(); -@EXPORT_OK = qw(create update last_update graph info rename_source - add_source sources retention_period last_values - heartbeat); - -# delete_source minimum maximum -%EXPORT_TAGS = ( all => \@EXPORT_OK ); - -$DEBUG ||= $ENV{DEBUG} ? 1 : 0; -$DEFAULT_DSTYPE ||= exists $ENV{DEFAULT_DSTYPE} ? $ENV{DEFAULT_DSTYPE} : 'GAUGE'; - -my $objstore = {}; - -======= use POSIX qw(strftime); # Used for strftime in graph() method use Carp qw(croak cluck confess carp); use File::Spec qw(); # catfile catdir updir path rootdir tmpdir @@ -81,471 +51,12 @@ my $objstore = {}; ->>>>>>> Stashed changes # # Methods # # Create a new object sub new { -<<<<<<< Updated upstream - TRACE(">>> new()"); - ref( my $class = shift ) && croak 'Class name required'; - croak 'Odd number of elements passed when even was expected' if @_ % 2; - - # Conjure up an invisible object - my $self = bless \( my $dummy ), $class; - $objstore->{ _refaddr($self) } = {@_}; - my $stor = $objstore->{ _refaddr($self) }; - - #my $self = { @_ }; - - # - Added "file" support in 1.42 - see sub _guess_filename. - # - Added "on_missing_ds"/"on_missing_source" support in 1.44 - # - Added "tmpdir" support in 1.44 - my @validkeys = qw(rrdtool cf default_dstype default_dst tmpdir - file on_missing_ds on_missing_source); - my $validkeys = join( '|', @validkeys ); - - cluck( 'Unrecognised parameters passed: ' . join( ', ', grep( !/^$validkeys$/, keys %{$stor} ) ) ) - if ( grep( !/^$validkeys$/, keys %{$stor} ) && $^W ); - - $stor->{rrdtool} = _find_binary( exists $stor->{rrdtool} ? $stor->{rrdtool} : 'rrdtool' ); - - # Check that "default_dstype" isn't complete rubbish (validation from v1.44+) - # GAUGE | COUNTER | DERIVE | ABSOLUTE | COMPUTE - # http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html - $stor->{default_dstype} ||= $stor->{default_dst}; - croak "Invalid value passed in parameter default_dstype; '$stor->{default_dstype}'" - if defined $stor->{default_dstype} - && $stor->{default_dstype} !~ /^(GAUGE|COUNTER|DERIVE|ABSOLUTE|COMPUTE|[A-Z]{1,10})$/i; - - # Check that "on_missing_ds" isn't complete rubbish. - # Added "on_missing_ds"/"on_missing_source" support in 1.44 - $stor->{on_missing_ds} ||= $stor->{on_missing_source}; - if ( defined $stor->{on_missing_ds} ) { - $stor->{on_missing_ds} = lc( $stor->{on_missing_ds} ); - croak "Invalid value passed in parameter on_missing_ds; '$stor->{on_missing_ds}'" - if $stor->{on_missing_ds} !~ /^\s*(add|ignore|die|croak)\s*$/i; - } - $stor->{on_missing_ds} ||= 'add'; # default to add - - #$stor->{cf} ||= [ qw(AVERAGE MIN MAX LAST) ]; - # By default, now only create RRAs for AVERAGE and MAX, like - # mrtg v2.13.2. This is to save disk space and processing time - # during updates etc. - $stor->{cf} ||= [qw(AVERAGE MAX)]; - $stor->{cf} = [ $stor->{cf} ] if !ref( $stor->{cf} ); - - DUMP( $class, $self ); - DUMP( '$stor', $stor ); - return $self; -} - -# Create a new RRD file -sub create { - TRACE(">>> create()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - my $stor = $objstore->{ _refaddr($self) }; - - # - # - # - - # Grab or guess the filename - my $rrdfile = $stor->{file}; - - # Odd number of values and first is not a valid scheme - # then the first value is likely an RRD file name. - if ( @_ % 2 && !_valid_scheme( $_[0] ) ) { - $rrdfile = shift; - - # Even number of values and the second value is a valid - # scheme then the first value is likely an RRD file name. - } - elsif ( !( @_ % 2 ) && _valid_scheme( $_[1] ) ) { - $rrdfile = shift; - - # If we still don't have an RRD file name then try and - # guess what it is - } - elsif ( !defined $rrdfile ) { - $rrdfile = _guess_filename($stor); - } - - # - # - # - - # Barf if the rrd file already exists - croak "RRD file '$rrdfile' already exists" if -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - # We've been given a scheme specifier - # Until v1.32 'year' was the default. As of v1.33 'mrtg' - # is the new default scheme. - #my $scheme = 'year'; - my $scheme = 'mrtg'; - if ( @_ % 2 && _valid_scheme( $_[0] ) ) { - $scheme = _valid_scheme( $_[0] ); - shift @_; - } - TRACE("Using scheme: $scheme"); - - croak 'Odd number of elements passed when even was expected' if @_ % 2; - my %ds = @_; - DUMP( '%ds', \%ds ); - - my $rrdDef = _rrd_def($scheme); - my @def = ( '-b', time - _seconds_in( $scheme, 120 ) ); - push @def, '-s', ( $rrdDef->{step} || 300 ); - - # Add data sources - for my $ds ( sort keys %ds ) { - $ds =~ s/[^a-zA-Z0-9_-]//g; - push @def, sprintf( 'DS:%s:%s:%s:%s:%s', substr( $ds, 0, 19 ), uc( $ds{$ds} ), ( $rrdDef->{heartbeat} || 600 ), 'U', 'U' ); - } - - # Add RRA definitions - my %cf; - for my $cf ( @{ $stor->{cf} } ) { - $cf{$cf} = $rrdDef->{rra}; - } - for my $cf ( sort keys %cf ) { - for my $rra ( @{ $cf{$cf} } ) { - push @def, sprintf( 'RRA:%s:%s:%s:%s', $cf, 0.5, $rra->{step}, $rra->{rows} ); - } - } - - DUMP( '@def', \@def ); - - # Pass to RRDs for execution - my @rtn = RRDs::create( $rrdfile, @def ); - my $error = RRDs::error(); - croak($error) if $error; - DUMP( 'RRDs::info', RRDs::info($rrdfile) ); - return wantarray ? @rtn : \@rtn; -} - -# Update an RRD file with some data values -sub update { - TRACE(">>> update()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - my $stor = $objstore->{ _refaddr($self) }; - - # - # - # - - # Grab or guess the filename - my $rrdfile = $stor->{file}; - - # Odd number of values and first is does not look - # like a recent unix time stamp then the first value - # is likely to be an RRD file name. - if ( @_ % 2 && $_[0] !~ /^[1-9][0-9]{8,10}$/i ) { - $rrdfile = shift; - - # Even number of values and the second value looks like - # a recent unix time stamp then the first value is - # likely to be an RRD file name. - } - elsif ( !( @_ % 2 ) && $_[1] =~ /^[1-9][0-9]{8,10}$/i ) { - $rrdfile = shift; - - # If we still don't have an RRD file name then try and - # guess what it is - } - elsif ( !defined $rrdfile ) { - $rrdfile = _guess_filename($stor); - } - - # - # - # - - # We've been given an update timestamp - my $time = time(); - if ( @_ % 2 && $_[0] =~ /^([1-9][0-9]{8,10})$/i ) { - $time = $1; - shift @_; - } - TRACE("Using update time: $time"); - - # Try to automatically create it - unless ( -f $rrdfile ) { - my $default_dstype = defined $stor->{default_dstype} ? $stor->{default_dstype} : $DEFAULT_DSTYPE; - cluck( "RRD file '$rrdfile' does not exist; attempting to create it ", "using default DS type of '$default_dstype'" ) if $^W; - my @args; - for ( my $i = 0; $i < @_; $i++ ) { - push @args, ( $_[$i], $default_dstype ) unless $i % 2; - } - $self->create( $rrdfile, @args ); - } - - croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - croak 'Odd number of elements passed when even was expected' if @_ % 2; - - my %ds; - while ( my $ds = shift(@_) ) { - $ds =~ s/[^a-zA-Z0-9_-]//g; - $ds = substr( $ds, 0, 19 ); - $ds{$ds} = shift(@_); - $ds{$ds} = 'U' if !defined( $ds{$ds} ); - } - DUMP( '%ds', \%ds ); - - # Validate the data source names as we add them - my @sources = $self->sources($rrdfile); - for my $ds ( sort keys %ds ) { - - # Check the data source names - if ( !grep( /^$ds$/, @sources ) ) { - TRACE( "Supplied data source '$ds' does not exist in pre-existing " . "RRD data source list: " . join( ', ', @sources ) ); - - # If someone got the case wrong, remind and correct them - if ( grep( /^$ds$/i, @sources ) ) { - cluck( "Data source '$ds' does not exist; automatically ", "correcting it to '", ( grep( /^$ds$/i, @sources ) )[0], "' instead" ) if $^W; - $ds{ ( grep( /^$ds$/i, @sources ) )[0] } = $ds{$ds}; - delete $ds{$ds}; - - # If it's not just a case sensitivity typo and the data source - # name really doesn't exist in this RRD file at all, regardless - # of case, then ... - } - else { - # Ignore the offending missing data source name - if ( $stor->{on_missing_ds} eq 'ignore' ) { - TRACE("on_missing_ds = ignore; ignoring data supplied for missing data source '$ds'"); - - # Fall on our bum and die horribly if requested to do so - } - elsif ( $stor->{on_missing_ds} eq 'die' || $stor->{on_missing_ds} eq 'croak' ) { - croak "Supplied data source '$ds' does not exist in RRD file '$rrdfile'"; - - # Default behaviour is to automatically add the new data source - # to the RRD file in order to preserve the existing default - # functionality of RRD::Simple - } - else { - TRACE( "on_missing_ds = add (or not set at all/default); " . "automatically adding new data source '$ds'" ); - - # Otherwise add any missing or new data sources on the fly - # Decide what DS type and heartbeat to use - my $info = RRDs::info($rrdfile); - my $error = RRDs::error(); - croak($error) if $error; - - my %dsTypes; - for my $key ( grep( /^ds\[.+?\]\.type$/, keys %{$info} ) ) { - $dsTypes{ $info->{$key} }++; - } - DUMP( '%dsTypes', \%dsTypes ); - my $dstype = ( - sort { $dsTypes{$b} <=> $dsTypes{$a} } - keys %dsTypes - )[0]; - TRACE("\$dstype = $dstype"); - - $self->add_source( $rrdfile, $ds, $dstype ); - } - } - } - } - - # Build the def - my @def = ('--template'); - push @def, join( ':', sort keys %ds ); - push @def, join( ':', $time, map { $ds{$_} } sort keys %ds ); - DUMP( '@def', \@def ); - - # Pass to RRDs to execute the update - my @rtn = RRDs::update( $rrdfile, @def ); - my $error = RRDs::error(); - croak($error) if $error; - return wantarray ? @rtn : \@rtn; -} - -# Get the last time an RRD was updates -sub last_update { __PACKAGE__->last(@_); } - -sub last { - TRACE(">>> last()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = shift || _guess_filename($stor); - croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - my $last = RRDs::last($rrdfile); - my $error = RRDs::error(); - croak($error) if $error; - return $last; -} - -# Get a list of data sources from an RRD file -sub sources { - TRACE(">>> sources()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = shift || _guess_filename($stor); - croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - my $info = RRDs::info($rrdfile); - my $error = RRDs::error(); - croak($error) if $error; - - my @ds; - foreach ( keys %{$info} ) { - if (/^ds\[(.+)?\]\.type$/) { - push @ds, $1; - } - } - return wantarray ? @ds : \@ds; -} - -# Add a new data source to an RRD file -sub add_source { - TRACE(">>> add_source()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); - unless ( -f $rrdfile ) { - cluck("RRD file '$rrdfile' does not exist; attempting to create it") - if $^W; - return $self->create( $rrdfile, @_ ); - } - croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - # Check that we will understand this RRD file version first - my $info = $self->info($rrdfile); - - # croak "Unable to add a new data source to $rrdfile; ", - # "RRD version $info->{rrd_version} is too new" - # if ($info->{rrd_version}+1-1) > 1; - - my ( $ds, $dstype ) = @_; - TRACE("\$ds = $ds"); - TRACE("\$dstype = $dstype"); - - my $rrdfileBackup = "$rrdfile.bak"; - confess "$rrdfileBackup already exists; please investigate" - if -e $rrdfileBackup; - - # Decide what heartbeat to use - my $heartbeat = - $info->{ds}->{ ( sort { $info->{ds}->{$b}->{minimal_heartbeat} <=> $info->{ds}->{$b}->{minimal_heartbeat} } keys %{ $info->{ds} } )[0] } - ->{minimal_heartbeat}; - TRACE("\$heartbeat = $heartbeat"); - - # Make a list of expected sources after the addition - my $TgtSources = join( ',', sort( ( $self->sources($rrdfile), $ds ) ) ); - - # Add the data source - my $new_rrdfile = ''; - eval { $new_rrdfile = _modify_source( $rrdfile, $stor, $ds, 'add', $dstype, $heartbeat, ); }; - - # Barf if the eval{} got upset - if ($@) { - croak "Failed to add new data source '$ds' to RRD file '$rrdfile': $@"; - } - - # Barf of the new RRD file doesn't exist - unless ( -f $new_rrdfile ) { - croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ", "new RRD file '$new_rrdfile' does not exist"; - } - - # Barf is the new data source isn't in our new RRD file - unless ( $TgtSources eq join( ',', sort( $self->sources($new_rrdfile) ) ) ) { - croak "Failed to add new data source '$ds' to RRD file '$rrdfile': ", "new RRD file '$new_rrdfile' does not contain expected data ", "source names"; - } - - # Try and move the new RRD file in to place over the existing one - # and then remove the backup RRD file if sucessfull - if ( File::Copy::move( $rrdfile, $rrdfileBackup ) - && File::Copy::move( $new_rrdfile, $rrdfile ) ) - { - unless ( unlink($rrdfileBackup) ) { - cluck("Failed to remove back RRD file '$rrdfileBackup': $!") - if $^W; - } - } - else { - croak "Failed to move new RRD file in to place: $!"; - } -} - -# Make a number of graphs for an RRD file -sub graph { - TRACE(">>> graph()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); - - # How much data do we have to graph? - my $period = $self->retention_period($rrdfile); - - # Check at RRA CFs are available and graph the best one - my $info = $self->info($rrdfile); - my $cf = 'AVERAGE'; - for my $rra ( @{ $info->{rra} } ) { - if ( $rra->{cf} eq 'AVERAGE' ) { - $cf = 'AVERAGE'; - last; - } - elsif ( $rra->{cf} eq 'MAX' ) { - $cf = 'MAX'; - } - elsif ( $rra->{cf} eq 'MIN' && $cf ne 'MAX' ) { - $cf = 'MIN'; - } - elsif ( $cf ne 'MAX' && $cf ne 'MIN' ) { - $cf = $rra->{cf}; - } - } - TRACE("graph() - \$cf = $cf"); - - # Create graphs which we have enough data to populate - # Version 1.39 - Change the return from an array to a hash (semi backward compatible) - # my @rtn; - my %rtn; -======= TRACE(">>> new()"); ref(my $class = shift) && croak 'Class name required'; croak 'Odd number of elements passed when even was expected' if @_ % 2; @@ -1012,7 +523,6 @@ sub graph { # Version 1.39 - Change the return from an array to a hash (semi backward compatible) # my @rtn; my %rtn; ->>>>>>> Stashed changes ## ## TODO @@ -1020,463 +530,6 @@ sub graph { ### data resolution (stepping) is fine enough (sub minute) ## -<<<<<<< Updated upstream - #i my @graph_periods = qw(hour 6hour 12hour day week month year 3years); - my @graph_periods; - my %param = @_; - if ( defined $param{'periods'} ) { - my %map = qw(daily day weekly week monthly month annual year 3years 3years); - for my $period ( _convert_to_array( $param{'periods'} ) ) { - $period = lc($period); - if ( _valid_scheme($period) ) { - push @graph_periods, $period; - } - elsif ( _valid_scheme( $map{$period} ) ) { - push @graph_periods, $map{$period}; - } - else { - croak "Invalid period value passed in parameter periods; '$period'"; - } - } - } - push @graph_periods, qw(day week month year 3years) unless @graph_periods; - - for my $type (@graph_periods) { - next if $period < _seconds_in($type); - TRACE("graph() - \$type = $type"); - - # push @rtn, [ ($self->_create_graph($rrdfile, $type, $cf, @_)) ]; - $rtn{ _alt_graph_name($type) } = [ ( $self->_create_graph( $rrdfile, $type, $cf, @_ ) ) ]; - } - - # return @rtn; - return wantarray ? %rtn : \%rtn; -} - -# Rename an existing data source -sub rename_source { - TRACE(">>> rename_source()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); - croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - my ( $old, $new ) = @_; - croak "No old data source name specified" unless defined $old && length($old); - croak "No new data source name specified" unless defined $new && length($new); - croak "Data source '$old' does not exist in RRD file '$rrdfile'" - unless grep( $_ eq $old, $self->sources($rrdfile) ); - - my @rtn = RRDs::tune( $rrdfile, '-r', "$old:$new" ); - my $error = RRDs::error(); - croak($error) if $error; - return wantarray ? @rtn : \@rtn; -} - -# Get or set a data source heartbeat -sub heartbeat { - TRACE(">>> heartbeat()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = - @_ >= 3 ? shift - : _isLegalDsName( $_[0] ) && $_[1] =~ /^[0-9]+$/ ? _guess_filename($stor) - : shift; - croak "RRD file '$rrdfile' does not exist" unless -f $rrdfile; - TRACE("Using filename: $rrdfile"); - - # Explode if we get no data source name - my ( $ds, $new_heartbeat ) = @_; - croak "No data source name was specified" unless defined $ds && length($ds); - - # Check the data source name exists - my $info = $self->info($rrdfile); - my $heartbeat = $info->{ds}->{$ds}->{minimal_heartbeat}; - croak "Data source '$ds' does not exist in RRD file '$rrdfile'" - unless defined $heartbeat && $heartbeat; - - if ( !defined $new_heartbeat ) { - return wantarray ? ($heartbeat) : $heartbeat; - } - - my @rtn = !defined $new_heartbeat ? ($heartbeat) : (); - - # Redefine the data source heartbeat - if ( defined $new_heartbeat ) { - croak "New minimal heartbeat '$new_heartbeat' is not a valid positive integer" - unless $new_heartbeat =~ /^[1-9][0-9]*$/; - my @rtn = RRDs::tune( $rrdfile, '-h', "$ds:$new_heartbeat" ); - my $error = RRDs::error(); - croak($error) if $error; - } - - return wantarray ? @rtn : \@rtn; -} - -# Fetch data point information from an RRD file -sub fetch { - TRACE(">>> fetch()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); - -} - -# Fetch the last values inserted in to an RRD file -sub last_values { - TRACE(">>> last_values()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); - - # When was the RRD last updated? - my $lastUpdated = $self->last($rrdfile); - - # Is there a LAST RRA? - my $info = $self->info($rrdfile); - my $hasLastRRA = 0; - for my $rra ( @{ $info->{rra} } ) { - $hasLastRRA++ if $rra->{cf} eq 'LAST'; - } - return if !$hasLastRRA; - - # What's the largest heartbeat in the RRD file data sources? - my $largestHeartbeat = 1; - for ( map { $info->{ds}->{$_}->{'minimal_heartbeat'} } keys( %{ $info->{ds} } ) ) { - $largestHeartbeat = $_ if $_ > $largestHeartbeat; - } - - my @def = ( 'LAST', '-s', $lastUpdated - ( $largestHeartbeat * 2 ), '-e', $lastUpdated ); - - # Pass to RRDs to execute - my ( $time, $heartbeat, $ds, $data ) = RRDs::fetch( $rrdfile, @def ); - my $error = RRDs::error(); - croak($error) if $error; - - # Put it in to a nice easy format - my %rtn = (); - for my $rec ( reverse @{$data} ) { - for ( my $i = 0; $i < @{$rec}; $i++ ) { - if ( defined $rec->[$i] && !exists( $rtn{ $ds->[$i] } ) ) { - $rtn{ $ds->[$i] } = $rec->[$i]; - } - } - } - - # Well, I'll be buggered if the LAST CF does what you'd think - # it's meant to do. If anybody can give me some decent documentation - # on what the LAST CF does, and/or how to get the last value put - # in to an RRD, then I'll admit that this method exists and export - # it too. - - return wantarray ? %rtn : \%rtn; -} - -# Return how long this RRD retains data for -sub retention_period { - TRACE(">>> retention_period()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - my $info = $self->info(@_); - return if !defined($info); - - my $duration = $info->{step}; - for my $rra ( @{ $info->{rra} } ) { - my $secs = ( $rra->{pdp_per_row} * $info->{step} ) * $rra->{rows}; - $duration = $secs if $secs > $duration; - } - - return wantarray ? ($duration) : $duration; -} - -# Fetch information about an RRD file -sub info { - TRACE(">>> info()"); - my $self = shift; - unless ( ref $self && UNIVERSAL::isa( $self, __PACKAGE__ ) ) { - unshift @_, $self unless $self eq __PACKAGE__; - $self = new __PACKAGE__; - } - - # Grab or guess the filename - my $stor = $objstore->{ _refaddr($self) }; - my $rrdfile = @_ % 2 ? shift : _guess_filename($stor); - - my $info = RRDs::info($rrdfile); - my $error = RRDs::error(); - croak($error) if $error; - DUMP( '$info', $info ); - - my $rtn; - for my $key ( sort( keys( %{$info} ) ) ) { - if ( $key =~ /^rra\[(\d+)\]\.([a-z_]+)/ ) { - $rtn->{rra}->[$1]->{$2} = $info->{$key}; - } - elsif ( my (@dsKey) = $key =~ /^ds\[([[A-Za-z0-9\_]+)?\]\.([a-z_]+)/ ) { - $rtn->{ds}->{$1}->{$2} = $info->{$key}; - } - elsif ( $key !~ /\[[\d_a-z]+\]/i ) { - $rtn->{$key} = $info->{$key}; - } - } - - # Return the information - DUMP( '$rtn', $rtn ); - return $rtn; -} - -# Convert a string or an array reference to an array -sub _convert_to_array { - return unless defined $_[0]; - if ( !ref $_[0] ) { - $_[0] =~ /^\s+|\s+$/g; - return split( /(?:\s+|\s*,\s*)/, $_[0] ); - } - elsif ( ref( $_[0] ) eq 'ARRAY' ) { - return @{ $_[0] }; - } - return; -} - -# Make a single graph image -sub _create_graph { - TRACE(">>> _create_graph()"); - my $self = shift; - my $rrdfile = shift; - my $type = _valid_scheme(shift) || 'day'; - my $cf = shift || 'AVERAGE'; - - my $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK):.+/; - $command_regex = qr/^([VC]?DEF|G?PRINT|COMMENT|[HV]RULE\d*|LINE\d*|AREA|TICK|SHIFT|STACK|TEXTALIGN):.+/ - if $RRDs::VERSION >= 1.3; # http://oss.oetiker.ch/rrdtool-trac/wiki/RRDtool13 - - my %param; - my @command_param; - while ( my $k = shift ) { - if ( $k =~ /$command_regex/ ) { - push @command_param, $k; - shift; - } - else { - $k =~ s/_/-/g; - $param{ lc($k) } = shift; - } - } - - # If we get this custom parameter then it would have already - # been dealt with by the calling graph() method so we should - # ditch it right here and now! - delete $param{'periods'}; - - # Specify some default values - $param{'end'} ||= $self->last($rrdfile) || time(); - $param{'imgformat'} ||= 'PNG'; # RRDs >1.3 now support PDF, SVG and EPS - # $param{'alt-autoscale'} ||= ''; - # $param{'alt-y-grid'} ||= ''; - - # Define what to call the image - my $basename = - defined $param{'basename'} && $param{'basename'} =~ /^[0-9a-z_\.-]+$/i - ? $param{'basename'} - : ( fileparse( $rrdfile, '\.[^\.]+' ) )[0]; - delete $param{'basename'}; - - # Define where to write the image - my $image = sprintf( '%s-%s.%s', $basename, _alt_graph_name($type), lc( $param{'imgformat'} ) ); - if ( $param{'destination'} ) { - $image = File::Spec->catfile( $param{'destination'}, $image ); - } - delete $param{'destination'}; - - # Specify timestamps- new for version 1.41 - my $timestamp = - !defined $param{'timestamp'} || $param{'timestamp'} !~ /^(graph|rrd|both|none)$/i - ? 'graph' - : lc( $param{'timestamp'} ); - delete $param{'timestamp'}; - - # Specify extended legend - new for version 1.35 - my $extended_legend = defined $param{'extended-legend'} - && $param{'extended-legend'} ? 1 : 0; - delete $param{'extended-legend'}; - - # Define how thick the graph lines should be - my $line_thickness = defined $param{'line-thickness'} - && $param{'line-thickness'} =~ /^[123]$/ ? $param{'line-thickness'} : 1; - delete $param{'line-thickness'}; - - # Colours is an alias to colors - if ( exists $param{'source-colours'} && !exists $param{'source-colors'} ) { - $param{'source-colors'} = $param{'source-colours'}; - delete $param{'source-colours'}; - } - - # Allow source line colors to be set - my @source_colors = (); - my %source_colors = (); - if ( defined $param{'source-colors'} ) { - - #if (ref($param{'source-colors'}) eq 'ARRAY') { - # @source_colors = @{$param{'source-colors'}}; - if ( ref( $param{'source-colors'} ) eq 'HASH' ) { - %source_colors = %{ $param{'source-colors'} }; - } - else { - @source_colors = _convert_to_array( $param{'source-colors'} ); - } - } - delete $param{'source-colors'}; - - # Define which data sources we should plot - my @rrd_sources = $self->sources($rrdfile); - my @ds = !exists $param{'sources'} - ? @rrd_sources - - #: defined $param{'sources'} && ref($param{'sources'}) eq 'ARRAY' - #? @{$param{'sources'}} - : defined $param{'sources'} ? _convert_to_array( $param{'sources'} ) - : (); - - # Allow source legend source_labels to be set - my %source_labels = (); - if ( defined $param{'source-labels'} ) { - if ( ref( $param{'source-labels'} ) eq 'HASH' ) { - %source_labels = %{ $param{'source-labels'} }; - } - elsif ( ref( $param{'source-labels'} ) eq 'ARRAY' ) { - if ( defined $param{'sources'} && ref( $param{'sources'} ) eq 'ARRAY' ) { - for ( my $i = 0; $i < @{ $param{'source-labels'} }; $i++ ) { - $source_labels{ $ds[$i] } = $param{'source-labels'}->[$i] - if defined $ds[$i]; - } - } - elsif ($^W) { - carp "source_labels may only be an array if sources is also " . "an specified and valid array"; - } - } - } - delete $param{'source-labels'}; - - # Allow source legend source_drawtypes to be set - # ... "oops" ... yes, this is quite obviously - # copy and paste code from the chunk above. I'm - # sorry. I'll rationalise it some other day if - # it's necessary. - my %source_drawtypes = (); - if ( defined $param{'source-drawtypes'} ) { - if ( ref( $param{'source-drawtypes'} ) eq 'HASH' ) { - %source_drawtypes = %{ $param{'source-drawtypes'} }; - } - elsif ( ref( $param{'source-drawtypes'} ) eq 'ARRAY' ) { - if ( defined $param{'sources'} && ref( $param{'sources'} ) eq 'ARRAY' ) { - for ( my $i = 0; $i < @{ $param{'source-drawtypes'} }; $i++ ) { - $source_drawtypes{ $ds[$i] } = $param{'source-drawtypes'}->[$i] - if defined $ds[$i]; - } - } - elsif ($^W) { - carp "source_drawtypes may only be an array if sources is " . "also an specified and valid array"; - } - } - - # Validate the values we have and set default thickness - while ( my ( $k, $v ) = each %source_drawtypes ) { - if ( $v !~ /^(LINE[1-9]?|STACK|AREA)$/ ) { - delete $source_drawtypes{$k}; - carp "source_drawtypes may be LINE, LINEn, AREA or STACK " . "only; value '$v' is not valid" if $^W; - } - $source_drawtypes{$k} = uc($v); - $source_drawtypes{$k} .= $line_thickness if $v eq 'LINE'; - } - } - delete $param{'source-drawtypes'}; - delete $param{'sources'}; - - # Specify a default start time - $param{'start'} ||= $param{'end'} - _seconds_in( $type, 115 ); - - # Suffix the title with the period information - $param{'title'} ||= basename($rrdfile); - $param{'title'} .= ' - [Hourly Graph]' if $type eq 'hour'; - $param{'title'} .= ' - [6 Hour Graph]' if $type eq '6hour' || $type eq 'quarterday'; - $param{'title'} .= ' - [12 Hour Graph]' if $type eq '12hour' || $type eq 'halfday'; - $param{'title'} .= ' - [Daily Graph]' if $type eq 'day'; - $param{'title'} .= ' - [Weekly Graph]' if $type eq 'week'; - $param{'title'} .= ' - [Monthly Graph]' if $type eq 'month'; - $param{'title'} .= ' - [Annual Graph]' if $type eq 'year'; - $param{'title'} .= ' - [3 Year Graph]' if $type eq '3years'; - - # Convert our parameters in to an RRDs friendly defenition - my @def; - while ( my ( $k, $v ) = each %param ) { - if ( length($k) == 1 ) { # Short single character options - $k = '-' . uc($k); - } - else { # Long options - $k = "--$k"; - } - for my $v ( ( ref($v) eq 'ARRAY' ? @{$v} : ($v) ) ) { - if ( !defined $v || !length($v) ) { - push @def, $k; - } - else { - push @def, "$k=$v"; - } - } - } - - # Populate a cycling tied scalar for line colors - @source_colors = qw( - FF0000 00FF00 0000FF 00FFFF FF00FF FFFF00 000000 - 990000 009900 000099 009999 990099 999900 999999 - 552222 225522 222255 225555 552255 555522 555555 - ) unless @source_colors > 0; - - # Pre 1.35 colours - # FF0000 00FF00 0000FF FFFF00 00FFFF FF00FF 000000 - # 550000 005500 000055 555500 005555 550055 555555 - # AA0000 00AA00 0000AA AAAA00 00AAAA AA00AA AAAAAA - tie my $colour, 'RRD::Simple::_Colour', \@source_colors; - - my $fmt = '%s:%s#%s:%s%s'; - my $longest_label = 1; - if ($extended_legend) { - for my $ds (@ds) { - my $len = length( defined $source_labels{$ds} ? $source_labels{$ds} : $ds ); - $longest_label = $len if $len > $longest_label; - } - $fmt = "%s:%s#%s:%-${longest_label}s%s"; - } -======= #i my @graph_periods = qw(hour 6hour 12hour day week month year 3years); my @graph_periods; my %param = @_; @@ -1933,115 +986,11 @@ sub _create_graph { } ->>>>>>> Stashed changes ## ## ## -<<<<<<< Updated upstream - # Create the @cmd - my @cmd = ( $image, @def ); - - # Add the data sources definitions to @cmd - for my $ds (@rrd_sources) { - - # Add the data source definition - push @cmd, sprintf( 'DEF:%s=%s:%s:%s', $ds, $rrdfile, $ds, $cf ); - } - - # Add the data source draw commands to the grap/@cmd - for my $ds (@ds) { - - # Stack operates differently in RRD 1.2 or higher - my $drawtype = - defined $source_drawtypes{$ds} - ? $source_drawtypes{$ds} - : "LINE$line_thickness"; - my $stack = ''; - if ( $RRDs::VERSION >= 1.2 && $drawtype eq 'STACK' ) { - $drawtype = 'AREA'; - $stack = ':STACK'; - } - - # Draw the line (and add to the legend) - push @cmd, - sprintf( $fmt, - $drawtype, $ds, - ( defined $source_colors{$ds} ? $source_colors{$ds} : $colour ), - ( defined $source_labels{$ds} ? $source_labels{$ds} : $ds ), $stack ); - - # New for version 1.39 - # Return the min,max,last information in the graph() return @rtn - if ( $RRDs::VERSION >= 1.2 ) { - push @cmd, sprintf( 'VDEF:%sMIN=%s,MINIMUM', $ds, $ds ); - push @cmd, sprintf( 'VDEF:%sMAX=%s,MAXIMUM', $ds, $ds ); - push @cmd, sprintf( 'VDEF:%sLAST=%s,LAST', $ds, $ds ); - - # Don't automatically add this unless we have to - # push @cmd, sprintf('VDEF:%sAVERAGE=%s,AVERAGE',$ds,$ds); - push @cmd, sprintf( 'PRINT:%sMIN:%s min %%1.2lf', $ds, $ds ); - push @cmd, sprintf( 'PRINT:%sMAX:%s max %%1.2lf', $ds, $ds ); - push @cmd, sprintf( 'PRINT:%sLAST:%s last %%1.2lf', $ds, $ds ); - } - else { - push @cmd, sprintf( 'PRINT:%s:MIN:%s min %%1.2lf', $ds, $ds ); - push @cmd, sprintf( 'PRINT:%s:MAX:%s max %%1.2lf', $ds, $ds ); - push @cmd, sprintf( 'PRINT:%s:LAST:%s last %%1.2lf', $ds, $ds ); - } - - # New for version 1.35 - if ($extended_legend) { - if ( $RRDs::VERSION >= 1.2 ) { - - # Moved the VDEFs to the block of code above which is - # always run, regardless of the extended legend - push @cmd, sprintf( 'GPRINT:%sMIN: min\:%%10.2lf\g', $ds ); - push @cmd, sprintf( 'GPRINT:%sMAX: max\:%%10.2lf\g', $ds ); - push @cmd, sprintf( 'GPRINT:%sLAST: last\:%%10.2lf\l', $ds ); - } - else { - push @cmd, sprintf( 'GPRINT:%s:MIN: min\:%%10.2lf\g', $ds ); - push @cmd, sprintf( 'GPRINT:%s:MAX: max\:%%10.2lf\g', $ds ); - push @cmd, sprintf( 'GPRINT:%s:LAST: last\:%%10.2lf\l', $ds ); - } - } - } - - # Push the post command defs on to the stack - push @cmd, @command_param; - - # Add a comment stating when the graph was last updated - if ( $timestamp ne 'none' ) { - - #push @cmd, ('COMMENT:\s','COMMENT:\s','COMMENT:\s'); - push @cmd, ( 'COMMENT:\s', 'COMMENT:\s' ); - push @cmd, 'COMMENT:\s' unless $extended_legend || !@ds; - my $timefmt = '%a %d/%b/%Y %T %Z'; - - if ( $timestamp eq 'rrd' || $timestamp eq 'both' ) { - my $time = sprintf( 'RRD last updated: %s\r', strftime( $timefmt, localtime( ( stat($rrdfile) )[9] ) ) ); - $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2 - push @cmd, "COMMENT:$time"; - } - - if ( $timestamp eq 'graph' || $timestamp eq 'both' ) { - my $time = sprintf( 'Graph last updated: %s\r', strftime( $timefmt, localtime(time) ) ); - $time =~ s/:/\\:/g if $RRDs::VERSION >= 1.2; # Only escape for 1.2 - push @cmd, "COMMENT:$time"; - } - } - - DUMP( '@cmd', \@cmd ); - - # Generate the graph - my @rtn = RRDs::graph(@cmd); - my $error = RRDs::error(); - croak($error) if $error; - return ( $image, @rtn ); -} - -======= # Create the @cmd my @cmd = ($image,@def); @@ -2148,42 +1097,11 @@ sub _create_graph { ->>>>>>> Stashed changes # # Private subroutines # no warnings 'redefine'; -<<<<<<< Updated upstream -sub UNIVERSAL::a_sub_not_likely_to_be_here { ref( $_[0] ) } -use warnings 'redefine'; - -sub _blessed ($) { - local ( $@, $SIG{__DIE__}, $SIG{__WARN__} ); - return length( ref( $_[0] ) ) - ? eval { $_[0]->a_sub_not_likely_to_be_here } - : undef; -} - -sub _refaddr($) { - my $pkg = ref( $_[0] ) or return undef; - if ( _blessed( $_[0] ) ) { - bless $_[0], 'Scalar::Util::Fake'; - } - else { - $pkg = undef; - } - "$_[0]" =~ /0x(\w+)/; - my $i = do { local $^W; hex $1 }; - bless $_[0], $pkg if defined $pkg; - return $i; -} - -sub _isLegalDsName { - - #rrdtool-1.0.49/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" - #rrdtool-1.2.11/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" -======= sub UNIVERSAL::a_sub_not_likely_to_be_here { ref($_[0]) } use warnings 'redefine'; @@ -2213,7 +1131,6 @@ sub _refaddr($) { sub _isLegalDsName { #rrdtool-1.0.49/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" #rrdtool-1.2.11/src/rrd_format.h:#define DS_NAM_FMT "%19[a-zA-Z0-9_-]" ->>>>>>> Stashed changes ## ## TODO @@ -2221,31 +1138,6 @@ sub _isLegalDsName { ## to see if it has changed or not ## -<<<<<<< Updated upstream - return $_[0] =~ /^[a-zA-Z0-9_-]{1,19}$/; -} - -sub _rrd_def { - croak('Pardon?!') if ref $_[0]; - my $type = _valid_scheme(shift); - - # This is calculated the same way as mrtg v2.13.2 - if ( $type eq 'mrtg' ) { - my $step = 5; # 5 minutes - return { - step => $step * 60, - heartbeat => $step * 60 * 2, - rra => [ - ( - { step => 1, rows => int( 4000 / $step ) }, # 800 - { step => int( 30 / $step ), rows => 800 }, # if $step < 30 - { step => int( 120 / $step ), rows => 800 }, - { step => int( 1440 / $step ), rows => 800 }, - ) - ], - }; - } -======= return $_[0] =~ /^[a-zA-Z0-9_-]{1,19}$/; } @@ -2268,123 +1160,12 @@ sub _rrd_def { )], }; } ->>>>>>> Stashed changes ## ## TODO ## 1.45 Add higher resolution for hour, 6hour and 12 hour ## -<<<<<<< Updated upstream - my $step = 1; # 1 minute highest resolution - my $rra = { - step => $step * 60, - heartbeat => $step * 60 * 2, - rra => [ - ( - # Actual $step resolution (for 1.25 days retention) - { step => 1, rows => int( _minutes_in( 'day', 125 ) / $step ) }, - ) - ], - }; - - if ( $type =~ /^(week|month|year|3years)$/i ) { - push @{ $rra->{rra} }, - { - step => int( 30 / $step ), - rows => int( _minutes_in( 'week', 125 ) / int( 30 / $step ) ) - }; # 30 minute average - - push @{ $rra->{rra} }, - { - step => int( 120 / $step ), - rows => int( _minutes_in( $type eq 'week' ? 'week' : 'month', 125 ) / int( 120 / $step ) ) - }; # 2 hour average - } - - if ( $type =~ /^(year|3years)$/i ) { - push @{ $rra->{rra} }, - { - step => int( 1440 / $step ), - rows => int( _minutes_in( $type, 125 ) / int( 1440 / $step ) ) - }; # 1 day average - } - - return $rra; -} - -sub _odd { - return $_[0] % 2; -} - -sub _even { - return !( $_[0] % 2 ); -} - -sub _valid_scheme { - TRACE(">>> _valid_scheme()"); - croak('Pardon?!') if ref $_[0]; - - #if ($_[0] =~ /^(day|week|month|year|3years|mrtg)$/i) { - if ( $_[0] =~ /^((?:6|12)?hour|(?:half)?day|week|month|year|3years|mrtg)$/i ) { - TRACE( "'" . lc($1) . "' is a valid scheme." ); - return lc($1); - } - TRACE("'@_' is not a valid scheme."); - return undef; -} - -sub _hours_in { return int( ( _seconds_in(@_) / 60 ) / 60 ); } -sub _minutes_in { return int( _seconds_in(@_) / 60 ); } - -sub _seconds_in { - croak('Pardon?!') if ref $_[0]; - my $str = lc(shift); - my $scale = shift || 100; - - return undef if !defined( _valid_scheme($str) ); - - my %time = ( - - # New for version 1.44 of RRD::Simple by - # popular request - 'hour' => 60 * 60, - '6hour' => 60 * 60 * 6, - 'quarterday' => 60 * 60 * 6, - '12hour' => 60 * 60 * 12, - 'halfday' => 60 * 60 * 12, - - 'day' => 60 * 60 * 24, - 'week' => 60 * 60 * 24 * 7, - 'month' => 60 * 60 * 24 * 31, - 'year' => 60 * 60 * 24 * 365, - '3years' => 60 * 60 * 24 * 365 * 3, - 'mrtg' => ( int( ( 1440 / 5 ) ) * 800 ) * 60, # mrtg v2.13.2 - ); - - my $rtn = $time{$str} * ( $scale / 100 ); - return $rtn; -} - -sub _alt_graph_name { - croak('Pardon?!') if ref $_[0]; - my $type = _valid_scheme(shift); - return unless defined $type; - - # New for version 1.44 of RRD::Simple by popular request - return 'hourly' if $type eq 'hour'; - return '6hourly' if $type eq '6hour' || $type eq 'quarterday'; - return '12hourly' if $type eq '12hour' || $type eq 'halfday'; - - return 'daily' if $type eq 'day'; - return 'weekly' if $type eq 'week'; - return 'monthly' if $type eq 'month'; - return 'annual' if $type eq 'year'; - return '3years' if $type eq '3years'; - return $type; -} - -======= my $step = 1; # 1 minute highest resolution my $rra = { step => $step * 60, @@ -2492,7 +1273,6 @@ sub _alt_graph_name { } ->>>>>>> Stashed changes ## ## TODO ## 1.45 - Check to see if there is now native support in RRDtool to @@ -2502,95 +1282,6 @@ sub _alt_graph_name { ## sub _modify_source { -<<<<<<< Updated upstream - croak('Pardon?!') if ref $_[0]; - my ( $rrdfile, $stor, $ds, $action, $dstype, $heartbeat ) = @_; - my $rrdtool = $stor->{rrdtool}; - $rrdtool = '' unless defined $rrdtool; - - # Decide what action we should take - if ( $action !~ /^(add|del)$/ ) { - my $caller = ( caller(1) )[3]; - $action = - $caller =~ /\badd\b/i ? 'add' - : $caller =~ /\bdel(ete)?\b/i ? 'del' - : undef; - } - croak "Unknown or no action passed to method _modify_source()" - unless defined $action && $action =~ /^(add|del)$/; - - require File::Copy; - require File::Temp; - - # Generate an XML dump of the RRD file - # - Added "tmpdir" support in 1.44 - my $tmpdir = defined $stor->{tmpdir} ? $stor->{tmpdir} : File::Spec->tmpdir(); - my ( $tempXmlFileFH, $tempXmlFile ) = File::Temp::tempfile( - DIR => $tmpdir, - TEMPLATE => 'rrdXXXXX', - SUFFIX => '.tmp', - ); - - # Check that we managed to get a sane temporary filename - croak "File::Temp::tempfile() failed to return a temporary filename" - unless defined $tempXmlFile; - TRACE("_modify_source(): \$tempXmlFile = $tempXmlFile"); - - # Try the internal perl way first (portable) - eval { - # Patch to rrd_dump.c emailed to Tobi and developers - # list by nicolaw/heds on 2006/01/08 - if ( $RRDs::VERSION >= 1.2013 ) { - my @rtn = RRDs::dump( $rrdfile, $tempXmlFile ); - my $error = RRDs::error(); - croak($error) if $error; - } - }; - - # Do it the old fashioned way - if ( $@ || !-f $tempXmlFile || ( stat($tempXmlFile) )[7] < 200 ) { - croak "rrdtool binary '$rrdtool' does not exist or is not executable" - if !defined $rrdtool || !-f $rrdtool || !-x $rrdtool; - _safe_exec( sprintf( '%s dump %s > %s', $rrdtool, $rrdfile, $tempXmlFile ) ); - } - - # Read in the new temporary XML dump file - open( IN, "<$tempXmlFile" ) || croak "Unable to open '$tempXmlFile': $!"; - - # Open XML output file - # my $tempImportXmlFile = File::Temp::tmpnam(); - # - Added "tmpdir" support in 1.44 - my ( $tempImportXmlFileFH, $tempImportXmlFile ) = File::Temp::tempfile( - DIR => $tmpdir, - TEMPLATE => 'rrdXXXXX', - SUFFIX => '.tmp', - ); - open( OUT, ">$tempImportXmlFile" ) - || croak "Unable to open '$tempImportXmlFile': $!"; - - # Create a marker hash ref to store temporary state - my $marker = { - currentDSIndex => 0, - deleteDSIndex => undef, - addedNewDS => 0, - parse => 0, - version => 1, - }; - - # Parse the input XML file - while ( local $_ = ) { - chomp; - - # Find out what index number the existing DS definition is in - if ( $action eq 'del' && /\s*(\S+)\s*<\/name>/ ) { - $marker->{deleteIndex} = $marker->{currentDSIndex} if $1 eq $ds; - $marker->{currentDSIndex}++; - } - - # Add the DS definition - if ( $action eq 'add' && !$marker->{addedNewDS} && // ) { - print OUT <{rrdtool}; @@ -2676,7 +1367,6 @@ sub _modify_source { # Add the DS definition if ($action eq 'add' && !$marker->{addedNewDS} && //) { print OUT <>>>>>> Stashed changes $ds $dstype @@ -2691,105 +1381,6 @@ sub _modify_source { EndDS -<<<<<<< Updated upstream - $marker->{addedNewDS} = 1; - } - - # Insert DS under CDP_PREP entity - if ( $action eq 'add' && /<\/cdp_prep>/ ) { - - # Version 0003 RRD from rrdtool 1.2x - if ( $marker->{version} >= 3 ) { - print OUT " \n"; - print OUT " 0.0000000000e+00 \n"; - print OUT " 0.0000000000e+00 \n"; - print OUT " NaN \n"; - print OUT " 0 \n"; - print OUT " \n"; - - # Version 0001 RRD from rrdtool 1.0x - } - else { - print OUT " NaN 0 \n"; - } - } - - # Look for the end of an RRA - if (/<\/database>/) { - $marker->{parse} = 0; - - # Find the dumped RRD version (must take from the XML, not the RRD) - } - elsif (/\s*([0-9\.]+)\s*<\/version>/) { - $marker->{version} = ( $1 + 1 - 1 ); - } - - # Add the extra " NaN " under the RRAs. Just print normal lines - if ( $marker->{parse} == 1 ) { - if ( $_ =~ /^(.+ .+)(<\/row>.*)/ ) { - print OUT $1; - print OUT " NaN " if $action eq 'add'; - print OUT $2; - print OUT "\n"; - } - } - else { - print OUT "$_\n"; - } - - # Look for the start of an RRA - if (//) { - $marker->{parse} = 1; - } - } - - # Close the files - close(IN) || croak "Unable to close '$tempXmlFile': $!"; - close(OUT) || croak "Unable to close '$tempImportXmlFile': $!"; - - # Import the new output file in to the old RRD filename - my $new_rrdfile = File::Temp::tmpnam(); - TRACE("_modify_source(): \$new_rrdfile = $new_rrdfile"); - - # Try the internal perl way first (portable) - eval { - if ( $RRDs::VERSION >= 1.0049 ) - { - my @rtn = RRDs::restore( $tempImportXmlFile, $new_rrdfile ); - my $error = RRDs::error(); - croak($error) if $error; - } - }; - - # Do it the old fashioned way - if ( $@ || !-f $new_rrdfile || ( stat($new_rrdfile) )[7] < 200 ) { - croak "rrdtool binary '$rrdtool' does not exist or is not executable" - unless ( -f $rrdtool && -x $rrdtool ); - my $cmd = sprintf( '%s restore %s %s', $rrdtool, $tempImportXmlFile, $new_rrdfile ); - my $rtn = _safe_exec($cmd); - - # At least check the file is created - unless ( -f $new_rrdfile ) { - _nuke_tmp( $tempXmlFile, $tempImportXmlFile ); - croak "Command '$cmd' failed to create the new RRD file '$new_rrdfile': $rtn"; - } - } - - # Remove the temporary files - _nuke_tmp( $tempXmlFile, $tempImportXmlFile ); - - sub _nuke_tmp { - for (@_) { - unlink($_) - || carp("Unable to unlink temporary file '$_': $!"); - } - } - - # Return the new RRD filename - return wantarray ? ($new_rrdfile) : $new_rrdfile; -} - -======= $marker->{addedNewDS} = 1; } @@ -2882,7 +1473,6 @@ EndDS } ->>>>>>> Stashed changes ## ## TODO ## 1.45 - Improve this _safe_exec function to see if it can be made @@ -2892,93 +1482,6 @@ EndDS ## sub _safe_exec { -<<<<<<< Updated upstream - croak('Pardon?!') if ref $_[0]; - my $cmd = shift; - if ( $cmd =~ /^([\/\.\_\-a-zA-Z0-9 >]+)$/ ) { - $cmd = $1; - TRACE($cmd); - system($cmd); - if ( $? == -1 ) { - croak "Failed to execute command '$cmd': $!\n"; - } - elsif ( $? & 127 ) { - croak( - sprintf( "While executing command '%s', child died " . "with signal %d, %s coredump\n", $cmd, ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' ) - ); - } - my $exit_value = $? >> 8; - croak "Error caught from '$cmd'" if $exit_value != 0; - return $exit_value; - } - else { - croak "Unexpected potentially unsafe command will not be executed: $cmd"; - } -} - -sub _find_binary { - croak('Pardon?!') if ref $_[0]; - my $binary = shift || 'rrdtool'; - return $binary if -f $binary && -x $binary; - - my @paths = File::Spec->path(); - my $rrds_path = dirname( $INC{'RRDs.pm'} ); - push @paths, $rrds_path; - push @paths, File::Spec->catdir( $rrds_path, File::Spec->updir(), File::Spec->updir(), 'bin' ); - - for my $path (@paths) { - my $filename = File::Spec->catfile( $path, $binary ); - return $filename if -f $filename && -x $filename; - } - - my $path = File::Spec->catdir( File::Spec->rootdir(), 'usr', 'local' ); - if ( opendir( DH, $path ) ) { - my @dirs = sort { $b cmp $a } grep( /^rrdtool/, readdir(DH) ); - closedir(DH) || carp "Unable to close file handle: $!"; - for my $dir (@dirs) { - my $filename = File::Spec->catfile( $path, $dir, 'bin', $binary ); - return $filename if -f $filename && -x $filename; - } - } -} - -sub _guess_filename { - croak('Pardon?!') if !defined $_[0] || ref( $_[0] ) ne 'HASH'; - my $stor = shift; - if ( defined $stor->{file} ) { - TRACE("_guess_filename = \$stor->{file} = $stor->{file}"); - return $stor->{file}; - } - my ( $basename, $dirname, $extension ) = fileparse( $0, '\.[^\.]+' ); - TRACE("_guess_filename = calculated = $dirname$basename.rrd"); - return "$dirname$basename.rrd"; -} - -sub DESTROY { - my $self = shift; - delete $objstore->{ _refaddr($self) }; -} - -sub TRACE { - return unless $DEBUG; - carp( shift() ); -} - -sub DUMP { - return unless $DEBUG; - eval { - require Data::Dumper; - $Data::Dumper::Indent = 2; - $Data::Dumper::Terse = 1; - carp( shift() . ': ' . Data::Dumper::Dumper( shift() ) ); - }; -} - -BEGIN { - eval "use RRDs"; - if ($@) { - carp qq{ -======= croak('Pardon?!') if ref $_[0]; my $cmd = shift; if ($cmd =~ /^([\/\.\_\-a-zA-Z0-9 >]+)$/) { @@ -3068,7 +1571,6 @@ BEGIN { eval "use RRDs"; if ($@) { carp qq{ ->>>>>>> Stashed changes +-----------------------------------------------------------------------------+ | ERROR! -- Could not load RRDs.pm | | | @@ -3078,13 +1580,6 @@ BEGIN { +-----------------------------------------------------------------------------+ } unless $ENV{AUTOMATED_TESTING}; -<<<<<<< Updated upstream - } -} - -1; - -======= } } @@ -3092,7 +1587,6 @@ BEGIN { 1; ->>>>>>> Stashed changes ############################################################### # This tie code is from Tie::Cycle # written by brian d foy, @@ -3100,27 +1594,6 @@ BEGIN { package RRD::Simple::_Colour; sub TIESCALAR { -<<<<<<< Updated upstream - my ( $class, $list_ref ) = @_; - my @shallow_copy = map { $_ } @$list_ref; - return unless UNIVERSAL::isa( $list_ref, 'ARRAY' ); - my $self = [ 0, scalar @shallow_copy, \@shallow_copy ]; - bless $self, $class; -} - -sub FETCH { - my $self = shift; - my $index = $$self[0]++; - $$self[0] %= $self->[1]; - return $self->[2]->[$index]; -} - -sub STORE { - my ( $self, $list_ref ) = @_; - return unless ref $list_ref eq ref []; - return unless @$list_ref > 1; - $self = [ 0, scalar @$list_ref, $list_ref ]; -======= my ($class,$list_ref) = @_; my @shallow_copy = map { $_ } @$list_ref; return unless UNIVERSAL::isa( $list_ref, 'ARRAY' ); @@ -3140,17 +1613,13 @@ sub STORE { return unless ref $list_ref eq ref []; return unless @$list_ref > 1; $self = [ 0, scalar @$list_ref, $list_ref ]; ->>>>>>> Stashed changes } 1; -<<<<<<< Updated upstream -======= ->>>>>>> Stashed changes =pod =head1 NAME @@ -3698,10 +2167,7 @@ L =cut -<<<<<<< Updated upstream -======= ->>>>>>> Stashed changes __END__ From 157d9b429b826f20aa13b0f17b6ea628698e7ef6 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 17 Jun 2017 10:13:53 -0600 Subject: [PATCH 179/209] Check for RRDs module, remove dead link to www.domotix.net --- code/common/weather_rrd_update.pl | 13 ++----------- lib/upgrade_utilities.pl | 12 ++++++++---- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/code/common/weather_rrd_update.pl b/code/common/weather_rrd_update.pl index 097e67759..12eb23489 100644 --- a/code/common/weather_rrd_update.pl +++ b/code/common/weather_rrd_update.pl @@ -10,20 +10,11 @@ #@ - temperature graphs #@ - others graphs for next release #@ To allow for logging of data into a RRD (Round Robin) database, -#@ install the perl RRDs.pm module, and fill in the mh.ini parm. -#@ -#@ Windows users can install RRD by extracting the files in -#@ http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/pub/ -#@ rrdtool-1.0.40.x86distr.zip-5.8.zip, -#@ (or similar) then cd to perl-shared and type: ppm install rrds.ppd -#@ RRD is available from -#@ http://ee-staff.ethz.ch/~oetiker/webtools/rrdtool -#@ Examples graphs : http://www.domotix.net # Script inspired from T. Oetiker wx200 monitor # http://wx200d.sourceforge.net #-------------------------------------------------------------------- # If you use the graphs on an Internet Web site, please add a link -# to www.misterhouse.net and www.domotix.net for your contribution +# to www.misterhouse.net for your contribution #-------------------------------------------------------------------- # In input, mh variables $Weather{...} are in the unit of measure : # Temperature �F @@ -230,7 +221,7 @@ unless $config_parms{weather_data_rrd}; $config_parms{weather_graph_dir} = "$config_parms{data_dir}/rrd" unless $config_parms{weather_graph_dir}; - $config_parms{weather_graph_footer} = 'Last updated $Time_Date, Dominique Benoliel, www.domotix.net' + $config_parms{weather_graph_footer} = 'Last updated $Time_Date, Dominique Benoliel' unless $config_parms{weather_graph_footer}; mkdir $config_parms{weather_graph_dir} unless -d $config_parms{weather_graph_dir}; diff --git a/lib/upgrade_utilities.pl b/lib/upgrade_utilities.pl index 0fb20e74a..83ef6aeb5 100644 --- a/lib/upgrade_utilities.pl +++ b/lib/upgrade_utilities.pl @@ -1,19 +1,23 @@ package upgrade_utilities; #set of subroutines that will store code that perform system-wide updates for new MH versions -use strict; -use RRD::Simple; #added dependancy lib/site/RRD/Simple.pm -#weather_rrd_update.pl -#update 06/15/17 12:24:00 PM Oops2: /Users/howard/Develop/mh/data/rrd/weather_data.rrd: expected 107 data source readings (got 37) from 1497551040 sub upgrade_checks { + eval("use RRDs"); + if ($@) { + &main::print_log("[Updater] : RRDs module not installed, skipping databse update"); + } else { &rrd_new_datasources(); + } +} + } sub rrd_new_datasources { &main::print_log("[Updater] : Checking RRD Schemas"); + use RRD::Simple; my $rrd = RRD::Simple->new(); my @sources = ($main::config_parms{data_dir} . "/rrd/weather_data.rrd"); From 8c7f3513ce85f31a0ac0a83dde6db8d4ba205c07 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 17 Jun 2017 10:18:55 -0600 Subject: [PATCH 180/209] typo --- code/common/weather_rrd_update.pl | 10 +++++++++- lib/upgrade_utilities.pl | 2 -- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/code/common/weather_rrd_update.pl b/code/common/weather_rrd_update.pl index 12eb23489..37e6f1321 100644 --- a/code/common/weather_rrd_update.pl +++ b/code/common/weather_rrd_update.pl @@ -10,11 +10,19 @@ #@ - temperature graphs #@ - others graphs for next release #@ To allow for logging of data into a RRD (Round Robin) database, +#@ install the perl RRDs.pm module, and fill in the mh.ini parm. +#@ +#@ Windows users can install RRD by extracting the files in +#@ http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/pub/ +#@ rrdtool-1.0.40.x86distr.zip-5.8.zip, +#@ (or similar) then cd to perl-shared and type: ppm install rrds.ppd +#@ RRD is available from +#@ http://ee-staff.ethz.ch/~oetiker/webtools/rrdtool # Script inspired from T. Oetiker wx200 monitor # http://wx200d.sourceforge.net #-------------------------------------------------------------------- # If you use the graphs on an Internet Web site, please add a link -# to www.misterhouse.net for your contribution +# to www.misterhouse.net and for your contribution #-------------------------------------------------------------------- # In input, mh variables $Weather{...} are in the unit of measure : # Temperature �F diff --git a/lib/upgrade_utilities.pl b/lib/upgrade_utilities.pl index 83ef6aeb5..efcdac8f2 100644 --- a/lib/upgrade_utilities.pl +++ b/lib/upgrade_utilities.pl @@ -12,8 +12,6 @@ sub upgrade_checks { &rrd_new_datasources(); } } - -} sub rrd_new_datasources { &main::print_log("[Updater] : Checking RRD Schemas"); From 029274a0535b588e2ff4ca2420651985d4685cd5 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 17 Jun 2017 12:42:15 -0600 Subject: [PATCH 181/209] keep RRD DB update local to weather_rrd_update.pl usercode --- bin/mh | 3 -- code/common/weather_rrd_update.pl | 55 ++++++++++++++++++++++++++ lib/upgrade_utilities.pl | 66 ------------------------------- 3 files changed, 55 insertions(+), 69 deletions(-) delete mode 100644 lib/upgrade_utilities.pl diff --git a/bin/mh b/bin/mh index 092514ede..cfa242581 100755 --- a/bin/mh +++ b/bin/mh @@ -790,7 +790,6 @@ sub setup { require 'xml_server.pl'; require 'menu_code.pl'; require 'trigger_code.pl'; - require 'upgrade_utilities.pl'; eval "use JSON"; # Used in mh/lib/json_server.pl JSON Web Attributes if ($@) { @@ -1248,8 +1247,6 @@ sub setup { print "Done with setup\n\n"; &ia7_utilities::ia7_update_collection; #Check if the collections need updating. - &upgrade_utilities::upgrade_checks; - } # Called from generic item *** and undo sub diff --git a/code/common/weather_rrd_update.pl b/code/common/weather_rrd_update.pl index 37e6f1321..2e08d5be7 100644 --- a/code/common/weather_rrd_update.pl +++ b/code/common/weather_rrd_update.pl @@ -904,3 +904,58 @@ sub uninstall_weather_rrd_update { unless &trigger_get('update rain totals from RRD database'); &analyze_rrd_rain; } + +if ($Startup) { + + &update_rrd_database(); +} + +sub update_rrd_database { + &main::print_log("Checking RRD Schemas"); + use RRD::Simple; + my $rrd = RRD::Simple->new(); + + my @sources = ($config_parms{data_dir} . "/rrd/weather_data.rrd"); + push @sources, $config_parms{weather_data_rrd} if (defined $config_parms{weather_data_rrd} and $config_parms{weather_data_rrd}); + + my %dschk; + my %newds; + #for MH 4.3, add in some TempSpares as well as 30 placeholders + $dschk{'4.3'} = "dsgauge020"; + @{$newds{'4.3'}} = ({"NAME" => 'tempspare11', "TYPE" => "GAUGE"}, + {"NAME" =>'tempspare12',"TYPE" => "GAUGE"}, + {"NAME" =>'tempspare13', "TYPE" => "GAUGE"}, + {"NAME" =>'tempspare14', "TYPE" => "GAUGE"}, + {"NAME" =>'tempspare15', "TYPE" => "GAUGE"}); + for (my $i=1; $i<21; $i++) { + push @{$newds{'4.3'}}, {"NAME" => 'dsgauge' . sprintf("%03d",$i), "TYPE" => "GAUGE"}; + } + for (my $i=1; $i<11; $i++) { + push @{$newds{'4.3'}}, {"NAME" => 'dsderive' . sprintf("%03d",$i), "TYPE" => "DERIVE"}; + } + + + foreach my $rrdfile (@sources) { + if (-e $rrdfile) { + &main::print_log("RRD: Checking file $rrdfile..."); + + my %rrd_ds = map { $_ => 1 } $rrd->sources($rrdfile); + + foreach my $key (keys %dschk) { + + unless (exists $rrd_ds{$dschk{$key}}) { + foreach my $ds (@{$newds{$key}}) { + unless (exists $rrd_ds{$ds->{NAME}}) { + &main::print_log("RRD: v$key Adding new Data Source name:$ds->{NAME} type:$ds->{TYPE}"); + $rrd->add_source($rrdfile, $ds->{NAME} => $ds->{TYPE}); #could also be DERIVE + } else { + &main::print_log("RRD: v$key Skipping Existing Data Source $ds->{NAME}"); + + } + + } + } + } + } + } +} diff --git a/lib/upgrade_utilities.pl b/lib/upgrade_utilities.pl deleted file mode 100644 index efcdac8f2..000000000 --- a/lib/upgrade_utilities.pl +++ /dev/null @@ -1,66 +0,0 @@ -package upgrade_utilities; - -#set of subroutines that will store code that perform system-wide updates for new MH versions -#added dependancy lib/site/RRD/Simple.pm - -sub upgrade_checks { - - eval("use RRDs"); - if ($@) { - &main::print_log("[Updater] : RRDs module not installed, skipping databse update"); - } else { - &rrd_new_datasources(); - } -} - -sub rrd_new_datasources { - &main::print_log("[Updater] : Checking RRD Schemas"); - use RRD::Simple; - my $rrd = RRD::Simple->new(); - - my @sources = ($main::config_parms{data_dir} . "/rrd/weather_data.rrd"); - push @sources, $main::config_parms{weather_data_rrd} if (defined $main::config_parms{weather_data_rrd} and $main::config_parms{weather_data_rrd}); - - my %dschk; - my %newds; - #for MH 4.3, add in some TempSpares as well as 30 placeholders - $dschk{'4.3'} = "dsgauge020"; - @{$newds{'4.3'}} = ({"NAME" => 'tempspare11', "TYPE" => "GAUGE"}, - {"NAME" =>'tempspare12',"TYPE" => "GAUGE"}, - {"NAME" =>'tempspare13', "TYPE" => "GAUGE"}, - {"NAME" =>'tempspare14', "TYPE" => "GAUGE"}, - {"NAME" =>'tempspare15', "TYPE" => "GAUGE"}); - for (my $i=1; $i<21; $i++) { - push @{$newds{'4.3'}}, {"NAME" => 'dsgauge' . sprintf("%03d",$i), "TYPE" => "GAUGE"}; - } - for (my $i=1; $i<11; $i++) { - push @{$newds{'4.3'}}, {"NAME" => 'dsderive' . sprintf("%03d",$i), "TYPE" => "DERIVE"}; - } - - - foreach my $rrdfile (@sources) { - if (-e $rrdfile) { - &main::print_log("[Updater::RRD] : Checking file $rrdfile..."); - - my %rrd_ds = map { $_ => 1 } $rrd->sources($rrdfile); - - foreach my $key (keys %dschk) { - - unless (exists $rrd_ds{$dschk{$key}}) { - foreach my $ds (@{$newds{$key}}) { - unless (exists $rrd_ds{$ds->{NAME}}) { - &main::print_log("[Updater::RRD] : v$key Adding new Data Source name:$ds->{NAME} type:$ds->{TYPE}"); - $rrd->add_source($rrdfile, $ds->{NAME} => $ds->{TYPE}); #could also be DERIVE - } else { - &main::print_log("[Updater::RRD] : v$key Skipping Existing Data Source $ds->{NAME}"); - - } - - } - } - } - } - } -} - -1; \ No newline at end of file From c4ecac1f2e9de18856fce914af8edc72472540ca Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 17 Jun 2017 12:43:35 -0600 Subject: [PATCH 182/209] Add ALEXA_BRIDGE type --- lib/read_table_A.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index d93172a86..9c9f36d2c 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1492,7 +1492,7 @@ sub read_table_A { #-------------- End AD2 Objects ------------- #-------------- Alexa Objects ----------------- - elsif ( $type eq "ALEX_BRIDGE" ) { + elsif (( $type eq "ALEX_BRIDGE" ) or (( $type eq "ALEXA_BRIDGE" )) { require 'AlexaBridge.pm'; ($name) = @item_info; $object = "AlexaBridge('$other')"; From 273b7774f13eaa3d10f4774f95902fa305b1a610 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sat, 17 Jun 2017 12:47:25 -0600 Subject: [PATCH 183/209] Add ALEXA_BRIDGE type --- lib/read_table_A.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/read_table_A.pl b/lib/read_table_A.pl index 9c9f36d2c..b6bf99557 100644 --- a/lib/read_table_A.pl +++ b/lib/read_table_A.pl @@ -1492,7 +1492,7 @@ sub read_table_A { #-------------- End AD2 Objects ------------- #-------------- Alexa Objects ----------------- - elsif (( $type eq "ALEX_BRIDGE" ) or (( $type eq "ALEXA_BRIDGE" )) { + elsif (( $type eq "ALEX_BRIDGE" ) or ( $type eq "ALEXA_BRIDGE" )) { require 'AlexaBridge.pm'; ($name) = @item_info; $object = "AlexaBridge('$other')"; From 66c2955305bd9250d29a361f9144216aaa6bcee9 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 18 Jun 2017 11:16:14 -0600 Subject: [PATCH 184/209] allow other RRDs for json server --- lib/json_server.pl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/json_server.pl b/lib/json_server.pl index df679bd72..27286481b 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -249,6 +249,11 @@ sub json_get { my $rrd_file = "weather_data.rrd"; $rrd_file = $config_parms{weather_data_rrd} if ( ( defined $config_parms{weather_data_rrd} ) and ($config_parms{weather_data_rrd})); + my $rrd_source = ""; + $rrd_source = $args{source}[0] if (defined $args{source}[0]); + $rrd_file = $config_parms{"rrd_source_" . $rrd_source} if (defined $config_parms{"rrd_source_" . $rrd_source} and $config_parms{"rrd_source_" . $rrd_source}); + $path = $config_parms{"rrd_source_" .$rrd_source . "_path"} if (defined $config_parms{"rrd_source_" . $rrd_source . "_path"} and $config_parms{"rrd_source_" . $rrd_source . "_path"}); +#print "JSON: source=$rrd_source path=$path file=$rrd_file\n"; if ( $rrd_file =~ m/.*\/(.*\.rrd)/ ) { $rrd_file = $1; } From ced8d68445b551e371140b9710ac08a55d42cabc Mon Sep 17 00:00:00 2001 From: H Plato Date: Mon, 26 Jun 2017 17:28:51 -0600 Subject: [PATCH 185/209] update json/IA7 to support external RRDs --- code/common/weather_rrd_update.pl | 4 ++-- lib/json_server.pl | 33 ++++++++++++++++--------------- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 13 ++++++++---- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/code/common/weather_rrd_update.pl b/code/common/weather_rrd_update.pl index 2e08d5be7..180361d1c 100644 --- a/code/common/weather_rrd_update.pl +++ b/code/common/weather_rrd_update.pl @@ -915,8 +915,8 @@ sub update_rrd_database { use RRD::Simple; my $rrd = RRD::Simple->new(); - my @sources = ($config_parms{data_dir} . "/rrd/weather_data.rrd"); - push @sources, $config_parms{weather_data_rrd} if (defined $config_parms{weather_data_rrd} and $config_parms{weather_data_rrd}); + my @sources = ("$config_parms{data_dir}/rrd/weather_data.rrd"); + push @sources, $config_parms{weather_data_rrd} if (defined $config_parms{weather_data_rrd} and $config_parms{weather_data_rrd} and ($config_parms{weather_data_rrd} ne "$config_parms{data_dir}/rrd/weather_data.rrd")); my %dschk; my %newds; diff --git a/lib/json_server.pl b/lib/json_server.pl index 27286481b..bc7eb64c1 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -266,6 +266,8 @@ sub json_get { $default_timestamp = $json_data{'rrd_config'}->{'prefs'}->{'get_last_update'} if ( defined $json_data{'rrd_config'}->{'prefs'}->{'get_last_update'} ); + &main::print_log("json_server.pl: WARNING. could not find ds.$rrd_source section in ia7_rrd_config.json!") if (($rrd_source) and !(defined $json_data{'rrd_config'}->{'ds.' . $rrd_source})); + my @dss = (); my @defs = (); my @xports = (); @@ -296,11 +298,12 @@ sub json_get { if ( defined $args{group}[0] ) { @{ $args{ds} } = (); #override any DSs specified in the URL - for my $dsg ( keys %{ $json_data{'rrd_config'}->{'ds'} } ) { - if ( defined $json_data{'rrd_config'}->{'ds'}->{$dsg}->{'group'} ) { - foreach my $group ( split /,/, $json_data{'rrd_config'}->{'ds'}->{$dsg}->{'group'} ) { - push @{ $args{ds} }, $dsg - if ( lc $group ) eq ( lc $args{group}[0] ); + my $ds_config = $json_data{'rrd_config'}->{'ds'}; + $ds_config = $json_data{'rrd_config'}->{'ds.' . $rrd_source} if ($rrd_source); #use ds. if an external source is specified + for my $dsg ( keys %{ $ds_config } ) { + if ( defined $ds_config->{$dsg}->{'group'} ) { + foreach my $group ( split /,/, $ds_config->{$dsg}->{'group'} ) { + push @{ $args{ds} }, $dsg if ( lc $group ) eq ( lc $args{group}[0] ); } } } @@ -309,17 +312,17 @@ sub json_get { foreach my $ds ( @{ $args{ds} } ) { push @dss, $ds; + my $ds_config = $json_data{'rrd_config'}->{'ds'}; + $ds_config = $json_data{'rrd_config'}->{'ds.' . $rrd_source} if ($rrd_source); #use ds. if an external source is specified + #if it doesn't exist as a ds then skip my $cf = $default_cf; - $cf = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'cf'} - if ( defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'cf'} ); + $cf = $ds_config->{$ds}->{'cf'} if ( defined $ds_config->{$ds}->{'cf'} ); push @defs, "DEF:$ds=$path/$rrd_file:$ds:$cf"; push @xports, "XPORT:$ds"; - $dataset[$index]->{'label'} = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'label'} - if ( defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'label'} ); - $dataset[$index]->{'color'} = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'color'} - if ( defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'color'} ); - if ( lc $json_data{'rrd_config'}->{'ds'}->{$ds}->{'type'} eq "bar" ) { + $dataset[$index]->{'label'} = $ds_config->{$ds}->{'label'} if ( defined $ds_config->{$ds}->{'label'} ); + $dataset[$index]->{'color'} = $ds_config->{$ds}->{'color'} if ( defined $ds_config->{$ds}->{'color'} ); + if ( lc $ds_config->{$ds}->{'type'} eq "bar" ) { $dataset[$index]->{'bars'}->{'show'} = "true"; $dataset[$index]->{'bars'}->{'fill'} = "0"; $dataset[$index]->{'bars'}->{'lineWidth'} = 0; @@ -333,10 +336,8 @@ sub json_get { else { $dataset[$index]->{'lines'}->{'show'} = "true"; } - $round[$index] = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'round'} - if ( defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'round'} ); - $type[$index] = $json_data{'rrd_config'}->{'ds'}->{$ds}->{'type'} - if ( defined $json_data{'rrd_config'}->{'ds'}->{$ds}->{'type'} ); + $round[$index] = $ds_config->{$ds}->{'round'} if ( defined $ds_config->{$ds}->{'round'} ); + $type[$index] = $ds_config->{$ds}->{'type'} if ( defined $ds_config->{$ds}->{'type'} ); $index++; } diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index bdeed6330..95d056ce9 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the IA7 web prototype, updates by H.Plato. IA7 v1.4.400 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the IA7 web prototype, updates by H.Plato. IA7 v1.4.500 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 9a70fccf3..6c15ea18c 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,5 +1,4 @@ -// v1.4.350 -//TODO : floorplan on IOS8 +// v1.4.500 var entity_store = {}; //global storage of entities var json_store = {}; @@ -1547,8 +1546,14 @@ var graph_rrd = function(start,group,time) { updateSocket.abort(); } var path_str = "/rrd" - //var arg_str = "start="+start+"&group="+group+"&long_poll=true&time="+time; - var arg_str = "start="+start+"&group="+group+"&time="+time; + //if the group has a dot, then it is a separate source + var source = "&group="+group; + if (group.indexOf(".") !== -1) { + var rrd_source = group.split("."); + source = "&source="+rrd_source[0]+"&group="+rrd_source[1]; + //console.log("source found:"+source); + } + var arg_str = "start="+start+source+"&time="+time; updateSocket = $.ajax({ type: "GET", //url: "/LONG_POLL?json('GET','"+path_str+"','"+arg_str+"')", From c60c09c94a7a9489ea425e9e4ec3833cdb447be4 Mon Sep 17 00:00:00 2001 From: H Plato Date: Thu, 29 Jun 2017 11:14:43 -0600 Subject: [PATCH 186/209] add in option to compress json --- lib/json_server.pl | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/json_server.pl b/lib/json_server.pl index bc7eb64c1..133528cc4 100755 --- a/lib/json_server.pl +++ b/lib/json_server.pl @@ -149,7 +149,9 @@ sub json_put { # Translate special characters $json_raw = $json_raw->pretty->encode( \%json ); - return &json_page($json_raw); + my $options = ""; + $options = "compress" if ($Http{'Accept-Encoding'} =~ m/gzip/); + return &json_page($json_raw,$options); } # Handles Get (READ) Requests @@ -810,7 +812,9 @@ sub json_get { # Translate special characters $json_raw->canonical(1); #Order the data so that objects show alphabetically $json_raw = $json_raw->pretty->encode( \%json ); - return &json_page($json_raw); + my $options = ""; + $options = "compress" if ($Http{'Accept-Encoding'} =~ m/gzip/); + return &json_page($json_raw,$options); } @@ -1146,18 +1150,23 @@ sub filter_object { } sub json_page { - my ($json_raw) = @_; - my $json; + my ($json_raw,$options) = @_; ## utf8::encode( $json_raw ); #may need to wrap gzip in an eval and encode it if errors develop. It crashes if a < is in the text - gzip \$json_raw => \$json; my $output = "HTTP/1.0 200 OK\r\n"; $output .= "Server: MisterHouse\r\n"; $output .= "Content-type: application/json\r\n"; - $output .= "Content-Encoding: gzip\r\n"; - $output .= "\r\n"; - $output .= $json; -## $output .= $json_raw; + if ($options =~ m/compress/) { + print_log("json_server.pl: DEBUG: Compressing Data as requested by client") if $Debug{json}; + my $json; + gzip \$json_raw => \$json; + $output .= "Content-Encoding: gzip\r\n"; + $output .= "\r\n"; + $output .= $json; + } else { + $output .= "\r\n"; + $output .= $json_raw; + } return $output; } From 61c7d7e373ef70d403273eee005180589de71187 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 2 Jul 2017 15:22:22 -0600 Subject: [PATCH 187/209] IA7 v1.5.100 - slider controls for brightness objects --- web/ia7/house/main.shtml | 2 +- web/ia7/include/javascript.js | 129 +++++++++++++++++++++++++--------- web/ia7/index.shtml | 14 +++- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/web/ia7/house/main.shtml b/web/ia7/house/main.shtml index 95d056ce9..790fd5abd 100644 --- a/web/ia7/house/main.shtml +++ b/web/ia7/house/main.shtml @@ -82,7 +82,7 @@

    MisterHouse was created by Bruce Winter. Ron Klinkien developed the v2.3 web interface. Kevin Robert Keegan - designed the IA7 web prototype, updates by H.Plato. IA7 v1.4.500 Font Awesome by Dave Gandy - http://fontawesome.io

    + designed the IA7 web prototype, updates by H.Plato. IA7 v1.5.100 Font Awesome by Dave Gandy - http://fontawesome.io

    diff --git a/web/ia7/include/javascript.js b/web/ia7/include/javascript.js index 6c15ea18c..aa36321ad 100644 --- a/web/ia7/include/javascript.js +++ b/web/ia7/include/javascript.js @@ -1,4 +1,4 @@ -// v1.4.500 +// v1.5.100 var entity_store = {}; //global storage of entities var json_store = {}; @@ -854,7 +854,17 @@ var filterSubstate = function (state) { return filter; }; +var brightnessStates = function (states) { + for(var i = 0; i < states.length; i++) + { + if(states[i].indexOf('%') != -1) + { + return 1 + } + } + return 0; +} var sortArrayByArray = function (listArray, sortArray){ listArray.sort(function(a,b) { @@ -2570,11 +2580,12 @@ var create_state_modal = function(entity) { if (json_store.objects[entity].label !== undefined) name = json_store.objects[entity].label; $('#control').modal('show'); var modal_state = json_store.objects[entity].state; - $('#control').find('.object-title').html(name + " - " + json_store.objects[entity].state); + $('#control').find('.object-title').html(name + " - " + json_store.objects[entity].state + ""); $('#control').find('.control-dialog').attr("entity", entity); var modal_states = json_store.objects[entity].states; // HP need to have at least 2 states to be a controllable object... if (modal_states == undefined) modal_states = 1; + console.log("brightness obj :"+brightnessStates(json_store.objects[entity].states)); if (modal_states.length > 1) { $('#control').find('.states').html('
    '); var modal_states = json_store.objects[entity].states; @@ -2596,45 +2607,93 @@ var create_state_modal = function(entity) { grid_buttons = 4; group_buttons = 3; } - - for (var i = 0; i < modal_states.length; i++){ - if (filterSubstate(modal_states[i]) == 1) { - advanced_html += ""; - continue - } else { - //buttonlength += 2 + modal_states[i].length - buttonlength ++; - } - //if (buttonlength >= 25) { - if (buttonlength > group_buttons) { - stategrp++; - $('#control').find('.states').append("
    "); - buttonlength = 1; - } - var color = getButtonColor(modal_states[i]) - var disabled = "" - if (modal_states[i] == json_store.objects[entity].state) { - disabled = "disabled"; - } - //global override - if (json_store.ia7_config.prefs.disable_current_state !== undefined && json_store.ia7_config.prefs.disable_current_state == "no") { - disabled = ""; - } - //per object override - if (json_store.ia7_config.objects !== undefined && json_store.ia7_config.objects[entity] !== undefined) { - if (json_store.ia7_config.objects[entity].disable_current_state !== undefined && json_store.ia7_config.objects[entity].disable_current_state == "yes") { - disabled = "disabled"; - } else { + //if it's a brightness object then put ON/OFF and a slider unless overriden in the prefs file + if (!brightnessStates(json_store.objects[entity].states) || (json_store.ia7_config.prefs.brightness_slider !== undefined && json_store.ia7_config.prefs.brightness_slider == "no")) { + for (var i = 0; i < modal_states.length; i++){ + if (filterSubstate(modal_states[i]) == 1) { + advanced_html += ""; + continue + } else { + //buttonlength += 2 + modal_states[i].length + buttonlength ++; + } + //if (buttonlength >= 25) { + if (buttonlength > group_buttons) { + stategrp++; + $('#control').find('.states').append("
    "); + buttonlength = 1; + } + var color = getButtonColor(modal_states[i]) + var disabled = "" + if (modal_states[i] == json_store.objects[entity].state) { + disabled = "disabled"; + } + //global override + if (json_store.ia7_config.prefs.disable_current_state !== undefined && json_store.ia7_config.prefs.disable_current_state == "no") { disabled = ""; - } - } - $('#control').find('.states').find(".stategrp"+stategrp).append(""); - } + } + //per object override + if (json_store.ia7_config.objects !== undefined && json_store.ia7_config.objects[entity] !== undefined) { + if (json_store.ia7_config.objects[entity].disable_current_state !== undefined && json_store.ia7_config.objects[entity].disable_current_state == "yes") { + disabled = "disabled"; + } else { + disabled = ""; + } + } + $('#control').find('.states').find(".stategrp"+stategrp).append(""); + } + } else { + $('#control').find('.states').find(".stategrp0").append(""); + $('#control').find('.states').find(".stategrp0").append(""); + $('#control').find('.states').append("
    "); + var val = $(".object-state").text(); + if (val == "on") { + val = 100; + } else if (val == "off") { + val = 0; + } else { + val = parseInt(val); + } + $('#slider' ).slider({ + min: 0, + max: 100, + value: val, + }); + $( "#slider" ).on( "slide", function(event, ui) { + var sliderstate = ui.value; + if (sliderstate == "100") { + sliderstate = "on"; + } else if (sliderstate == "0") { + sliderstate = "off"; + } else { + sliderstate += "%"; + } + //console.log("Slider Change "+ui.value+":"+sliderstate); + $('#control').find('.object-state').text(sliderstate); + + }); + $( "#slider" ).on( "slidechange", function(event, ui) { + var sliderstate = ui.value; + if (sliderstate == "100") { + sliderstate = "on"; + } else if (sliderstate == "0") { + sliderstate = "off"; + } else { + sliderstate += "%"; + } + url= '/SET;none?select_item='+$(this).parents('.control-dialog').attr("entity")+'&select_state='+sliderstate; + $('#control').modal('hide'); + $.get( url); + console.log("Slider Value "+url); + }); + + } $('#control').find('.states').append("
    "+advanced_html+"
    "); $('#control').find('.states').find('.btn').click(function (){ url= '/SET;none?select_item='+$(this).parents('.control-dialog').attr("entity")+'&select_state='+$(this).text(); $('#control').modal('hide'); $.get( url); + console.log("button Value "+url); }); } else { //remove states from anything that doesn't have more than 1 state diff --git a/web/ia7/index.shtml b/web/ia7/index.shtml index 9ea21d99f..43559b135 100644 --- a/web/ia7/index.shtml +++ b/web/ia7/index.shtml @@ -21,7 +21,7 @@ - + @@ -44,7 +44,10 @@ - + + + + @@ -216,6 +219,13 @@ margin-left: 14px !important; } + .brightness-slider { + margin-left: 5px; + margin-right: 5px; + margin-top: 10px; + margin-bottom: -20px; + } + From 6a7ae1cde28941fd3cf90565eaad17d532ff50e0 Mon Sep 17 00:00:00 2001 From: H Plato Date: Sun, 2 Jul 2017 15:27:31 -0600 Subject: [PATCH 188/209] Updated jQuery components for slider --- web/ia7/include/jquery-ui-1.12.1.min.js | 13 + .../include/jquery-ui.smoothness.1.12.1.css | 1311 +++++++++++++++++ web/ia7/include/jquery.1.12.4.min.js | 5 + 3 files changed, 1329 insertions(+) create mode 100644 web/ia7/include/jquery-ui-1.12.1.min.js create mode 100644 web/ia7/include/jquery-ui.smoothness.1.12.1.css create mode 100644 web/ia7/include/jquery.1.12.4.min.js diff --git a/web/ia7/include/jquery-ui-1.12.1.min.js b/web/ia7/include/jquery-ui-1.12.1.min.js new file mode 100644 index 000000000..117cb35e4 --- /dev/null +++ b/web/ia7/include/jquery-ui-1.12.1.min.js @@ -0,0 +1,13 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function e(t){for(var e=t.css("visibility");"inherit"===e;)t=t.parent(),e=t.css("visibility");return"hidden"!==e}function i(t){for(var e,i;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(i=parseInt(t.css("zIndex"),10),!isNaN(i)&&0!==i))return i;t=t.parent()}return 0}function s(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=n(t("
    "))}function n(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,o)}function o(){t.datepicker._isDisabledDatepicker(m.inline?m.dpDiv.parent()[0]:m.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function a(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}function r(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.ui=t.ui||{},t.ui.version="1.12.1";var h=0,l=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},h=e.split(".")[0];e=e.split(".")[1];var l=h+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][l.toLowerCase()]=function(e){return!!t.data(e,l)},t[h]=t[h]||{},n=t[h][e],o=t[h][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:h,widgetName:e,widgetFullName:l}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=l.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=l.call(arguments,1),r=this;return o?this.length||"instance"!==n?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(r=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(r=i&&i.jquery?r.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):r=void 0:(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),r}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
    ",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=h++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+o.eventNamespace,c=h[2];c?n.on(l,c,r):i.on(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(u.test(t[0])?e/100:1),parseFloat(t[1])*(u.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o=Math.max,a=Math.abs,r=/left|center|right/,h=/top|center|bottom/,l=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,u=/%$/,d=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
    "),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widthi?"left":e>0?"right":"center",vertical:0>r?"top":s>0?"bottom":"middle"};l>p&&p>a(e+i)&&(u.horizontal="center"),c>f&&f>a(s+r)&&(u.vertical="middle"),u.important=o(a(e),a(i))>o(a(s),a(r))?"horizontal":"vertical",n.using.call(this,t,u)}),h.offset(t.extend(D,{using:r}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,r=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-r-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-r-o,(0>i||a(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>a(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,r=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-r-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-r-o,(0>s||a(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-h,(i>0||u>a(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}});var c="ui-effects-",u="ui-effects-style",d="ui-effects-animated",p=t;t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,o){var a,r=o.re.exec(i),h=r&&o.parse(r),l=o.space||"rgba";return h?(a=s[l](h),s[c[l].cache]=a[c[l].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("

    ")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,a,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,h],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),o=c[n],a=0===this.alpha()?l("transparent"):this,r=a[o.cache]||o.to(a._rgba),h=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],l=s[o],c=u[n.type]||{};null!==l&&(null===a?h[o]=l:(c.mod&&(l-a>c.mod/2?a+=c.mod:a-l>c.mod/2&&(a-=c.mod)),h[o]=i((l-a)*e+a,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),h=Math.min(s,n,o),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-o)/l+360:n===r?60*(o-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[a]&&(this[a]=h(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[a]=d,n):l(d)},f(o,function(e,i){l.fn[e]||(l.fn[e]=function(n){var o,a=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=l(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(p),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,o,a={};for(s in i)o=i[s],e[s]!==o&&(n[s]||(t.fx.step[s]||!isNaN(parseFloat(o)))&&(a[s]=o));return a}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(p.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var h=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",l=h.children?a.find("*").addBack():a;l=l.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),l=l.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function e(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function i(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function s(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(d)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,e){for(var i=0,s=e.length;s>i;i++)null!==e[i]&&t.data(c+e[i],t[0].style[e[i]])},restore:function(t,e){for(var i,s=0,n=e.length;n>s;s++)null!==e[s]&&(i=t.data(c+e[s]),t.css(e[s],i))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(u,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(u)||"",t.removeData(u)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(e){var i,s=e.css("position"),n=e.position();return e.css({marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()),/^(static|relative)/.test(s)&&(s="absolute",i=t("<"+e[0].nodeName+">").insertAfter(e).css({display:/^(inline|ruby)/.test(e.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight"),"float":e.css("float")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).addClass("ui-effects-placeholder"),e.data(c+"placeholder",i)),e.css({position:s,left:n.left,top:n.top}),i},removePlaceholder:function(t){var e=c+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function i(e){function i(){r.removeData(d),t.effects.cleanUp(r),"hide"===s.mode&&r.hide(),a()}function a(){t.isFunction(h)&&h.call(r[0]),t.isFunction(e)&&e()}var r=t(this);s.mode=c.shift(),t.uiBackCompat===!1||o?"none"===s.mode?(r[l](),a()):n.call(r[0],s,i):(r.is(":hidden")?"hide"===l:"show"===l)?(r[l](),a()):n.call(r[0],s,a)}var s=e.apply(this,arguments),n=t.effects.effect[s.effect],o=n.mode,a=s.queue,r=a||"fx",h=s.complete,l=s.mode,c=[],u=function(e){var i=t(this),s=t.effects.mode(i,l)||o;i.data(d,!0),c.push(s),o&&("show"===s||s===o&&"hide"===s)&&i.show(),o&&"none"===s||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!n?l?this[l](s.duration,h):this.each(function(){h&&h.call(this)}):a===!1?this.each(u).each(i):this.queue(r,u).queue(r,i)},show:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="show",this.effect.call(this,n) +}}(t.fn.show),hide:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(t.fn.hide),toggle:function(t){return function(s){if(i(s)||"boolean"==typeof s)return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):s(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,h=o?a.scrollLeft():0,l=n.offset(),c={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
    ").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-h,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=s(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}();var f=t.effects;t.effects.define("blind","hide",function(e,i){var s={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},n=t(this),o=e.direction||"up",a=n.cssClip(),r={clip:t.extend({},a)},h=t.effects.createPlaceholder(n);r.clip[s[o][0]]=r.clip[s[o][1]],"show"===e.mode&&(n.cssClip(r.clip),h&&h.css(t.effects.clipToBox(r)),r.clip=a),h&&h.animate(t.effects.clipToBox(r),e.duration,e.easing),n.animate(r,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("bounce",function(e,i){var s,n,o,a=t(this),r=e.mode,h="hide"===r,l="show"===r,c=e.direction||"up",u=e.distance,d=e.times||5,p=2*d+(l||h?1:0),f=e.duration/p,g=e.easing,m="up"===c||"down"===c?"top":"left",_="up"===c||"left"===c,v=0,b=a.queue().length;for(t.effects.createPlaceholder(a),o=a.css(m),u||(u=a["top"===m?"outerHeight":"outerWidth"]()/3),l&&(n={opacity:1},n[m]=o,a.css("opacity",0).css(m,_?2*-u:2*u).animate(n,f,g)),h&&(u/=Math.pow(2,d-1)),n={},n[m]=o;d>v;v++)s={},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g).animate(n,f,g),u=h?2*u:u/2;h&&(s={opacity:0},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g)),a.queue(i),t.effects.unshift(a,b,p+1)}),t.effects.define("clip","hide",function(e,i){var s,n={},o=t(this),a=e.direction||"vertical",r="both"===a,h=r||"horizontal"===a,l=r||"vertical"===a;s=o.cssClip(),n.clip={top:l?(s.bottom-s.top)/2:s.top,right:h?(s.right-s.left)/2:s.right,bottom:l?(s.bottom-s.top)/2:s.bottom,left:h?(s.right-s.left)/2:s.left},t.effects.createPlaceholder(o),"show"===e.mode&&(o.cssClip(n.clip),n.clip=s),o.animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("drop","hide",function(e,i){var s,n=t(this),o=e.mode,a="show"===o,r=e.direction||"left",h="up"===r||"down"===r?"top":"left",l="up"===r||"left"===r?"-=":"+=",c="+="===l?"-=":"+=",u={opacity:0};t.effects.createPlaceholder(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,u[h]=l+s,a&&(n.css(u),u[h]=c+s,u.opacity=1),n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("explode","hide",function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),i()}var o,a,r,h,l,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=e.mode,g="show"===f,m=p.show().css("visibility","hidden").offset(),_=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/u),b=[];for(o=0;u>o;o++)for(h=m.top+o*v,c=o-(u-1)/2,a=0;d>a;a++)r=m.left+a*_,l=a-(d-1)/2,p.clone().appendTo("body").wrap("
    ").css({position:"absolute",visibility:"visible",left:-a*_,top:-o*v}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:_,height:v,left:r+(g?l*_:0),top:h+(g?c*v:0),opacity:g?0:1}).animate({left:r+(g?0:l*_),top:h+(g?0:c*v),opacity:g?1:0},e.duration||500,e.easing,s)}),t.effects.define("fade","toggle",function(e,i){var s="show"===e.mode;t(this).css("opacity",s?0:1).animate({opacity:s?1:0},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("fold","hide",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=e.size||15,h=/([0-9]+)%/.exec(r),l=!!e.horizFirst,c=l?["right","bottom"]:["bottom","right"],u=e.duration/2,d=t.effects.createPlaceholder(s),p=s.cssClip(),f={clip:t.extend({},p)},g={clip:t.extend({},p)},m=[p[c[0]],p[c[1]]],_=s.queue().length;h&&(r=parseInt(h[1],10)/100*m[a?0:1]),f.clip[c[0]]=r,g.clip[c[0]]=r,g.clip[c[1]]=0,o&&(s.cssClip(g.clip),d&&d.css(t.effects.clipToBox(g)),g.clip=p),s.queue(function(i){d&&d.animate(t.effects.clipToBox(f),u,e.easing).animate(t.effects.clipToBox(g),u,e.easing),i()}).animate(f,u,e.easing).animate(g,u,e.easing).queue(i),t.effects.unshift(s,_,4)}),t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("size",function(e,i){var s,n,o,a=t(this),r=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],l=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],c=e.mode,u="effect"!==c,d=e.scale||"both",p=e.origin||["middle","center"],f=a.css("position"),g=a.position(),m=t.effects.scaledDimensions(a),_=e.from||m,v=e.to||t.effects.scaledDimensions(a,0);t.effects.createPlaceholder(a),"show"===c&&(o=_,_=v,v=o),n={from:{y:_.height/m.height,x:_.width/m.width},to:{y:v.height/m.height,x:v.width/m.width}},("box"===d||"both"===d)&&(n.from.y!==n.to.y&&(_=t.effects.setTransition(a,h,n.from.y,_),v=t.effects.setTransition(a,h,n.to.y,v)),n.from.x!==n.to.x&&(_=t.effects.setTransition(a,l,n.from.x,_),v=t.effects.setTransition(a,l,n.to.x,v))),("content"===d||"both"===d)&&n.from.y!==n.to.y&&(_=t.effects.setTransition(a,r,n.from.y,_),v=t.effects.setTransition(a,r,n.to.y,v)),p&&(s=t.effects.getBaseline(p,m),_.top=(m.outerHeight-_.outerHeight)*s.y+g.top,_.left=(m.outerWidth-_.outerWidth)*s.x+g.left,v.top=(m.outerHeight-v.outerHeight)*s.y+g.top,v.left=(m.outerWidth-v.outerWidth)*s.x+g.left),a.css(_),("content"===d||"both"===d)&&(h=h.concat(["marginTop","marginBottom"]).concat(r),l=l.concat(["marginLeft","marginRight"]),a.find("*[width]").each(function(){var i=t(this),s=t.effects.scaledDimensions(i),o={height:s.height*n.from.y,width:s.width*n.from.x,outerHeight:s.outerHeight*n.from.y,outerWidth:s.outerWidth*n.from.x},a={height:s.height*n.to.y,width:s.width*n.to.x,outerHeight:s.height*n.to.y,outerWidth:s.width*n.to.x};n.from.y!==n.to.y&&(o=t.effects.setTransition(i,h,n.from.y,o),a=t.effects.setTransition(i,h,n.to.y,a)),n.from.x!==n.to.x&&(o=t.effects.setTransition(i,l,n.from.x,o),a=t.effects.setTransition(i,l,n.to.x,a)),u&&t.effects.saveStyle(i),i.css(o),i.animate(a,e.duration,e.easing,function(){u&&t.effects.restoreStyle(i)})})),a.animate(v,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){var e=a.offset();0===v.opacity&&a.css("opacity",_.opacity),u||(a.css("position","static"===f?"relative":f).offset(e),t.effects.saveStyle(a)),i()}})}),t.effects.define("scale",function(e,i){var s=t(this),n=e.mode,o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"effect"!==n?0:100),a=t.extend(!0,{from:t.effects.scaledDimensions(s),to:t.effects.scaledDimensions(s,o,e.direction||"both"),origin:e.origin||["middle","center"]},e);e.fade&&(a.from.opacity=1,a.to.opacity=0),t.effects.effect.size.call(this,a,i)}),t.effects.define("puff","hide",function(e,i){var s=t.extend(!0,{},e,{fade:!0,percent:parseInt(e.percent,10)||150});t.effects.effect.scale.call(this,s,i)}),t.effects.define("pulsate","show",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=o||a,h=2*(e.times||5)+(r?1:0),l=e.duration/h,c=0,u=1,d=s.queue().length;for((o||!s.is(":visible"))&&(s.css("opacity",0).show(),c=1);h>u;u++)s.animate({opacity:c},l,e.easing),c=1-c;s.animate({opacity:c},l,e.easing),s.queue(i),t.effects.unshift(s,d,h+1)}),t.effects.define("shake",function(e,i){var s=1,n=t(this),o=e.direction||"left",a=e.distance||20,r=e.times||3,h=2*r+1,l=Math.round(e.duration/h),c="up"===o||"down"===o?"top":"left",u="up"===o||"left"===o,d={},p={},f={},g=n.queue().length;for(t.effects.createPlaceholder(n),d[c]=(u?"-=":"+=")+a,p[c]=(u?"+=":"-=")+2*a,f[c]=(u?"-=":"+=")+2*a,n.animate(d,l,e.easing);r>s;s++)n.animate(p,l,e.easing).animate(f,l,e.easing);n.animate(p,l,e.easing).animate(d,l/2,e.easing).queue(i),t.effects.unshift(n,g,h+1)}),t.effects.define("slide","show",function(e,i){var s,n,o=t(this),a={up:["bottom","top"],down:["top","bottom"],left:["right","left"],right:["left","right"]},r=e.mode,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h,u=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0),d={};t.effects.createPlaceholder(o),s=o.cssClip(),n=o.position()[l],d[l]=(c?-1:1)*u+n,d.clip=o.cssClip(),d.clip[a[h][1]]=d.clip[a[h][0]],"show"===r&&(o.cssClip(d.clip),o.css(l,d[l]),d.clip=s,d[l]=n),o.animate(d,{queue:!1,duration:e.duration,easing:e.easing,complete:i})});var f;t.uiBackCompat!==!1&&(f=t.effects.define("transfer",function(e,i){t(this).transfer(e,i)})),t.ui.focusable=function(i,s){var n,o,a,r,h,l=i.nodeName.toLowerCase();return"area"===l?(n=i.parentNode,o=n.name,i.href&&o&&"map"===n.nodeName.toLowerCase()?(a=t("img[usemap='#"+o+"']"),a.length>0&&a.is(":visible")):!1):(/^(input|select|textarea|button|object)$/.test(l)?(r=!i.disabled,r&&(h=t(i).closest("fieldset")[0],h&&(r=!h.disabled))):r="a"===l?i.href||s:s,r&&t(i).is(":visible")&&e(t(i)))},t.extend(t.expr[":"],{focusable:function(e){return t.ui.focusable(e,null!=t.attr(e,"tabindex"))}}),t.ui.focusable,t.fn.form=function(){return"string"==typeof this[0].form?this.closest("form"):t(this[0].form)},t.ui.formResetMixin={_formResetHandler:function(){var e=t(this);setTimeout(function(){var i=e.data("ui-form-reset-instances");t.each(i,function(){this.refresh()})})},_bindFormResetHandler:function(){if(this.form=this.element.form(),this.form.length){var t=this.form.data("ui-form-reset-instances")||[];t.length||this.form.on("reset.ui-form-reset",this._formResetHandler),t.push(this),this.form.data("ui-form-reset-instances",t)}},_unbindFormResetHandler:function(){if(this.form.length){var e=this.form.data("ui-form-reset-instances");e.splice(t.inArray(this,e),1),e.length?this.form.data("ui-form-reset-instances",e):this.form.removeData("ui-form-reset-instances").off("reset.ui-form-reset")}}},"1.7"===t.fn.jquery.substring(0,3)&&(t.each(["Width","Height"],function(e,i){function s(e,i,s,o){return t.each(n,function(){i-=parseFloat(t.css(e,"padding"+this))||0,s&&(i-=parseFloat(t.css(e,"border"+this+"Width"))||0),o&&(i-=parseFloat(t.css(e,"margin"+this))||0)}),i}var n="Width"===i?["Left","Right"]:["Top","Bottom"],o=i.toLowerCase(),a={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+i]=function(e){return void 0===e?a["inner"+i].call(this):this.each(function(){t(this).css(o,s(this,e)+"px")})},t.fn["outer"+i]=function(e,n){return"number"!=typeof e?a["outer"+i].call(this,e):this.each(function(){t(this).css(o,s(this,e,!0,n)+"px")})}}),t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.ui.escapeSelector=function(){var t=/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g;return function(e){return e.replace(t,"\\$1")}}(),t.fn.labels=function(){var e,i,s,n,o;return this[0].labels&&this[0].labels.length?this.pushStack(this[0].labels):(n=this.eq(0).parents("label"),s=this.attr("id"),s&&(e=this.eq(0).parents().last(),o=e.add(e.length?e.siblings():this.siblings()),i="label[for='"+t.ui.escapeSelector(s)+"']",n=n.add(o.find(i).addBack(i))),this.pushStack(n))},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.extend(t.expr[":"],{tabbable:function(e){var i=t.attr(e,"tabindex"),s=null!=i;return(!s||i>=0)&&t.ui.focusable(e,s)}}),t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.widget("ui.accordion",{version:"1.12.1",options:{active:0,animate:{},classes:{"ui-accordion-header":"ui-corner-top","ui-accordion-header-collapsed":"ui-corner-all","ui-accordion-content":"ui-corner-bottom"},collapsible:!1,event:"click",header:"> li > :first-child, > :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var e=this.options;this.prevShow=this.prevHide=t(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),e.collapsible||e.active!==!1&&null!=e.active||(e.active=0),this._processPanels(),0>e.active&&(e.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():t()}},_createIcons:function(){var e,i,s=this.options.icons;s&&(e=t(""),this._addClass(e,"ui-accordion-header-icon","ui-icon "+s.header),e.prependTo(this.headers),i=this.active.children(".ui-accordion-header-icon"),this._removeClass(i,s.header)._addClass(i,null,s.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){return"active"===t?(this._activate(e),void 0):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||this.options.active!==!1||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons()),void 0)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t),this._toggleClass(this.headers.add(this.headers.next()),null,"ui-state-disabled",!!t)},_keydown:function(e){if(!e.altKey&&!e.ctrlKey){var i=t.ui.keyCode,s=this.headers.length,n=this.headers.index(e.target),o=!1;switch(e.keyCode){case i.RIGHT:case i.DOWN:o=this.headers[(n+1)%s];break;case i.LEFT:case i.UP:o=this.headers[(n-1+s)%s];break;case i.SPACE:case i.ENTER:this._eventHandler(e);break;case i.HOME:o=this.headers[0];break;case i.END:o=this.headers[s-1]}o&&(t(e.target).attr("tabIndex",-1),t(o).attr("tabIndex",0),t(o).trigger("focus"),e.preventDefault())}},_panelKeyDown:function(e){e.keyCode===t.ui.keyCode.UP&&e.ctrlKey&&t(e.currentTarget).prev().trigger("focus")},refresh:function(){var e=this.options;this._processPanels(),e.active===!1&&e.collapsible===!0||!this.headers.length?(e.active=!1,this.active=t()):e.active===!1?this._activate(0):this.active.length&&!t.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(e.active=!1,this.active=t()):this._activate(Math.max(0,e.active-1)):e.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var e,i=this.options,s=i.heightStyle,n=this.element.parent();this.active=this._findActive(i.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var e=t(this),i=e.uniqueId().attr("id"),s=e.next(),n=s.uniqueId().attr("id");e.attr("aria-controls",n),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(i.event),"fill"===s?(e=n.height(),this.element.siblings(":visible").each(function(){var i=t(this),s=i.css("position");"absolute"!==s&&"fixed"!==s&&(e-=i.outerHeight(!0))}),this.headers.each(function(){e-=t(this).outerHeight(!0)}),this.headers.next().each(function(){t(this).height(Math.max(0,e-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===s&&(e=0,this.headers.next().each(function(){var i=t(this).is(":visible");i||t(this).show(),e=Math.max(e,t(this).css("height","").height()),i||t(this).hide()}).height(e))},_activate:function(e){var i=this._findActive(e)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return"number"==typeof e?this.headers.eq(e):t()},_setupEvents:function(e){var i={keydown:"_keydown"};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(e){var i,s,n=this.options,o=this.active,a=t(e.currentTarget),r=a[0]===o[0],h=r&&n.collapsible,l=h?t():a.next(),c=o.next(),u={oldHeader:o,oldPanel:c,newHeader:h?t():a,newPanel:l};e.preventDefault(),r&&!n.collapsible||this._trigger("beforeActivate",e,u)===!1||(n.active=h?!1:this.headers.index(a),this.active=r?t():a,this._toggle(u),this._removeClass(o,"ui-accordion-header-active","ui-state-active"),n.icons&&(i=o.children(".ui-accordion-header-icon"),this._removeClass(i,null,n.icons.activeHeader)._addClass(i,null,n.icons.header)),r||(this._removeClass(a,"ui-accordion-header-collapsed")._addClass(a,"ui-accordion-header-active","ui-state-active"),n.icons&&(s=a.children(".ui-accordion-header-icon"),this._removeClass(s,null,n.icons.header)._addClass(s,null,n.icons.activeHeader)),this._addClass(a.next(),"ui-accordion-content-active")))},_toggle:function(e){var i=e.newPanel,s=this.prevShow.length?this.prevShow:e.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=s,this.options.animate?this._animate(i,s,e):(s.hide(),i.show(),this._toggleComplete(e)),s.attr({"aria-hidden":"true"}),s.prev().attr({"aria-selected":"false","aria-expanded":"false"}),i.length&&s.length?s.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===parseInt(t(this).attr("tabIndex"),10)}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,e,i){var s,n,o,a=this,r=0,h=t.css("box-sizing"),l=t.length&&(!e.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",o=!1,n=e.keyCode>=96&&105>=e.keyCode?""+(e.keyCode-96):String.fromCharCode(e.keyCode),clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,h=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=h.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=h.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n; +this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t("
     
    a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:l.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/
    "!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("