98_TRAFFIC.pm 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. #########################################################################
  2. # $Id: 98_TRAFFIC.pm 12838 2016-12-19 20:59:43Z jmike $
  3. # fhem Modul which provides traffic details with Google Distance API
  4. #
  5. # This file is part of fhem.
  6. #
  7. # Fhem is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Fhem is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. # versioning: MAJOR.MINOR.PATCH, increment the:
  21. # MAJOR version when you make incompatible API changes
  22. # - includes changing CLI options, changing log-messages
  23. # MINOR version when you add functionality in a backwards-compatible manner
  24. # - includes adding new features and log-messages (as long as they don't break anything existing)
  25. # PATCH version when you make backwards-compatible bug fixes.
  26. #
  27. ##############################################################################
  28. # Changelog:
  29. #
  30. # 2016-07-26 initial release
  31. # 2016-07-28 added eta, readings in minutes
  32. # 2016-08-01 changed JSON decoding/encofing, added stateReading attribute, added outputReadings attribute
  33. # 2016-08-02 added attribute includeReturn, round minutes & smart zero'ing, avoid negative values, added update burst
  34. # 2016-08-05 fixed 3 perl warnings
  35. # 2016-08-09 added auto-update if status returns UNKOWN_ERROR, added outputReading average
  36. # 2016-09-25 bugfix Blocking, improved errormessage
  37. # 2016-10-07 version 1.0, adding to SVN
  38. # 2016-10-15 adding attribute updateSchedule to provide flexible updates, changed internal interval to INTERVAL
  39. # 2016-12-13 adding travelMode, fixing stateReading with value 0
  40. # 2016-12-15 adding reverseWaypoints attribute, adding weblink with auto create route via gmaps on verbose 5
  41. package main;
  42. use strict;
  43. use warnings;
  44. use Data::Dumper;
  45. use Time::HiRes qw(gettimeofday);
  46. use LWP::Simple qw($ua get);
  47. use Blocking;
  48. use POSIX;
  49. die "MIME::Base64 missing!" unless(eval{require MIME::Base64});
  50. die "JSON missing!" unless(eval{require JSON});
  51. sub TRAFFIC_Initialize($);
  52. sub TRAFFIC_Define($$);
  53. sub TRAFFIC_Undef($$);
  54. sub TRAFFIC_Set($@);
  55. sub TRAFFIC_Attr(@);
  56. sub TRAFFIC_GetUpdate($);
  57. my %TRcmds = (
  58. 'update' => 'noArg',
  59. );
  60. my $TRVersion = '1.2';
  61. sub TRAFFIC_Initialize($){
  62. my ($hash) = @_;
  63. $hash->{DefFn} = "TRAFFIC_Define";
  64. $hash->{UndefFn} = "TRAFFIC_Undef";
  65. $hash->{SetFn} = "TRAFFIC_Set";
  66. $hash->{AttrFn} = "TRAFFIC_Attr";
  67. $hash->{AttrList} =
  68. "disable:0,1 start_address end_address raw_data:0,1 language waypoints returnWaypoints stateReading outputReadings travelMode:driving,walking,bicycling,transit includeReturn:0,1 updateSchedule " .
  69. $readingFnAttributes;
  70. $data{FWEXT}{"/TRAFFIC"}{FUNC} = "TRAFFIC_debug";
  71. $data{FWEXT}{"/TRAFFIC"}{FORKABLE} = 1;
  72. }
  73. sub TRAFFIC_Define($$){
  74. my ($hash, $allDefs) = @_;
  75. my @deflines = split('\n',$allDefs);
  76. my @apiDefs = split('[ \t]+', shift @deflines);
  77. if(int(@apiDefs) < 3) {
  78. return "too few parameters: 'define <name> TRAFFIC <APIKEY>'";
  79. }
  80. $hash->{NAME} = $apiDefs[0];
  81. $hash->{APIKEY} = $apiDefs[2];
  82. $hash->{VERSION} = $TRVersion;
  83. delete($hash->{BURSTCOUNT}) if $hash->{BURSTCOUNT};
  84. delete($hash->{BURSTINTERVAL}) if $hash->{BURSTINTERVAL};
  85. my $name = $hash->{NAME};
  86. #clear all readings
  87. foreach my $clearReading ( keys %{$hash->{READINGS}}){
  88. Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted";
  89. delete($hash->{READINGS}{$clearReading});
  90. }
  91. # basic update INTERVAL
  92. if(scalar(@apiDefs) > 3 && $apiDefs[3] =~ m/^\d+$/){
  93. $hash->{INTERVAL} = $apiDefs[3];
  94. }else{
  95. $hash->{INTERVAL} = 3600;
  96. }
  97. Log3 $hash, 3, "TRAFFIC: ($name) defined ".$hash->{NAME}.' with interval set to '.$hash->{INTERVAL};
  98. # put in default verbose level
  99. $attr{$name}{"verbose"} = 1 if !$attr{$name}{"verbose"};
  100. $attr{$name}{"outputReadings"} = "text" if !$attr{$name}{"outputReadings"};
  101. readingsSingleUpdate( $hash, "state", "Initialized", 1 );
  102. my $firstTrigger = gettimeofday() + 2;
  103. $hash->{TRIGGERTIME} = $firstTrigger;
  104. $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger);
  105. RemoveInternalTimer($hash);
  106. InternalTimer($firstTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  107. Log3 $hash, 5, "TRAFFIC: ($name) InternalTimer set to call GetUpdate in 2 seconds for the first time";
  108. return undef;
  109. }
  110. sub TRAFFIC_Undef($$){
  111. my ( $hash, $arg ) = @_;
  112. RemoveInternalTimer ($hash);
  113. return undef;
  114. }
  115. #
  116. # Attr command
  117. #########################################################################
  118. sub TRAFFIC_Attr(@){
  119. my ($cmd,$name,$attrName,$attrValue) = @_;
  120. # $cmd can be "del" or "set"
  121. # $name is device name
  122. my $hash = $defs{$name};
  123. if ($cmd eq "set") {
  124. addToDevAttrList($name, $attrName);
  125. Log3 $hash, 3, "TRAFFIC: ($name) attrName $attrName set to attrValue $attrValue";
  126. }
  127. if($attrName eq "disable" && $attrValue eq "1"){
  128. readingsSingleUpdate( $hash, "state", "disabled", 1 );
  129. }
  130. if($attrName eq "verbose" && $attrValue eq "5"){
  131. if (!defined $defs{$name."_weblink"}) {
  132. FW_fC("define ".$name."_weblink weblink htmlCode {TRAFFIC_weblink(\"".$name."\",0)}");
  133. FW_fC("attr ".$name."_weblink room TRAFFIC_debug");
  134. Log3 $hash, 5, "TRAFFIC: ($name) weblink created";
  135. }
  136. }elsif($attrName eq "verbose" && $attrValue < 5){
  137. FW_fC("delete ".$name."_weblink");
  138. }
  139. if($attrName eq "outputReadings" || $attrName eq "includeReturn" || $attrName eq "verbose"){
  140. #clear all readings
  141. foreach my $clearReading ( keys %{$hash->{READINGS}}){
  142. Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted";
  143. delete($hash->{READINGS}{$clearReading});
  144. }
  145. # start update
  146. InternalTimer(gettimeofday() + 1, "TRAFFIC_StartUpdate", $hash, 0);
  147. }
  148. return undef;
  149. }
  150. sub TRAFFIC_Set($@){
  151. my ($hash, @param) = @_;
  152. return "\"set <TRAFFIC>\" needs at least one argument: \n".join(" ",keys %TRcmds) if (int(@param) < 2);
  153. my $name = shift @param;
  154. my $set = shift @param;
  155. $hash->{VERSION} = $TRVersion if $hash->{VERSION} ne $TRVersion;
  156. if(AttrVal($name, "disable", 0 ) == 1){
  157. readingsSingleUpdate( $hash, "state", "disabled", 1 );
  158. Log3 $hash, 3, "TRAFFIC: ($name) is disabled, $set not set!";
  159. return undef;
  160. }else{
  161. Log3 $hash, 5, "TRAFFIC: ($name) set $name $set";
  162. }
  163. my $validCmds = join("|",keys %TRcmds);
  164. if($set !~ m/$validCmds/ ) {
  165. return join(' ', keys %TRcmds);
  166. }elsif($set =~ m/update/){
  167. Log3 $hash, 5, "TRAFFIC: ($name) update command recieved";
  168. # if update burst ist specified
  169. if( (my $burstCount = shift @param) && (my $burstInterval = shift @param)){
  170. Log3 $hash, 5, "TRAFFIC: ($name) update burst is set to $burstCount $burstInterval";
  171. $hash->{BURSTCOUNT} = $burstCount;
  172. $hash->{BURSTINTERVAL} = $burstInterval;
  173. }else{
  174. Log3 $hash, 5, "TRAFFIC: ($name) no update burst set";
  175. }
  176. # update internal timer and update NOW
  177. my $updateTrigger = gettimeofday() + 1;
  178. $hash->{TRIGGERTIME} = $updateTrigger;
  179. $hash->{TRIGGERTIME_FMT} = FmtDateTime($updateTrigger);
  180. RemoveInternalTimer($hash);
  181. # start update
  182. InternalTimer($updateTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  183. return undef;
  184. }elsif($set =~ m/debug/){
  185. # TRAFFIC_widget();
  186. }
  187. }
  188. sub TRAFFIC_StartUpdate($){
  189. my ( $hash ) = @_;
  190. my $name = $hash->{NAME};
  191. my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time);
  192. $wday=7 if $wday == 0; #sunday 0 -> sunday 7, monday 0 -> monday 1 ...
  193. if(AttrVal($name, "disable", 0 ) == 1){
  194. RemoveInternalTimer ($hash);
  195. Log3 $hash, 3, "TRAFFIC: ($name) is disabled";
  196. return undef;
  197. }
  198. if ( $hash->{INTERVAL}) {
  199. RemoveInternalTimer ($hash);
  200. delete($hash->{UPDATESCHEDULE});
  201. my $nextTrigger = gettimeofday() + $hash->{INTERVAL};
  202. if(defined(AttrVal($name, "updateSchedule", undef ))){
  203. Log3 $hash, 5, "TRAFFIC: ($name) flexible update Schedule defined";
  204. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  205. my @updateScheduleDef = split('\|', AttrVal($name, "updateSchedule", undef ));
  206. foreach my $upSched (@updateScheduleDef){
  207. my ($upFrom, $upTo, $upDay, $upInterval ) = $upSched =~ m/(\d+)-(\d+)\s(\d{1,})\s?(\d{1,})?/;
  208. if (!$upInterval){
  209. $upInterval = $upDay;
  210. $upDay='';
  211. }
  212. Log3 $hash, 5, "TRAFFIC: ($name) parsed schedule to upFrom $upFrom, upTo $upTo, upDay $upDay, upInterval $upInterval";
  213. if(!$upFrom || !$upTo || !$upInterval){
  214. Log3 $hash, 1, "TRAFIC: ($name) updateSchedule $upSched not defined correctly";
  215. }else{
  216. if($hour >= $upFrom && $hour < $upTo){
  217. if(!$upDay || $upDay == $wday ){
  218. $nextTrigger = gettimeofday() + $upInterval;
  219. Log3 $hash, 3, "TRAFFIC: ($name) schedule from $upFrom to $upTo (on day $upDay) every $upInterval seconds, matches (current hour $hour), nextTrigger set to $nextTrigger";
  220. $hash->{UPDATESCHEDULE} = $upSched;
  221. last;
  222. }else{
  223. Log3 $hash, 3, "TRAFFIC: ($name) $upSched does match the time but not the day ($wday)";
  224. }
  225. }else{
  226. Log3 $hash, 5, "TRAFFIC: ($name) schedule $upSched does not match ($hour)";
  227. }
  228. }
  229. }
  230. }
  231. if(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} > 0){
  232. $nextTrigger = gettimeofday() + $hash->{BURSTINTERVAL};
  233. Log3 $hash, 3, "TRAFFIC: ($name) next update defined by burst";
  234. $hash->{BURSTCOUNT}--;
  235. }elsif(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} == 0){
  236. delete($hash->{BURSTCOUNT});
  237. delete($hash->{BURSTINTERVAL});
  238. Log3 $hash, 3, "TRAFFIC: ($name) burst update is done";
  239. }
  240. $hash->{TRIGGERTIME} = $nextTrigger;
  241. $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
  242. InternalTimer($nextTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  243. Log3 $hash, 3, "TRAFFIC: ($name) internal interval timer set to call StartUpdate again at " . $hash->{TRIGGERTIME_FMT};
  244. }
  245. if(defined(AttrVal($name, "start_address", undef )) && defined(AttrVal($name, "end_address", undef ))){
  246. BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;normal',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash);
  247. if(defined(AttrVal($name, "includeReturn", undef )) && AttrVal($name, "includeReturn", undef ) eq 1){
  248. BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;return',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash);
  249. }
  250. }else{
  251. readingsSingleUpdate( $hash, "state", "incomplete configuration", 1 );
  252. Log3 $hash, 1, "TRAFFIC: ($name) is not configured correctly, please add start_address and end_address";
  253. }
  254. }
  255. sub TRAFFIC_AbortUpdate($){
  256. }
  257. sub TRAFFIC_DoUpdate(){
  258. my ($string) = @_;
  259. my ($hName, $direction) = split(";;;", $string); # direction is normal or return
  260. my $hash = $defs{$hName};
  261. my $dotrigger = 1;
  262. my $name = $hash->{NAME};
  263. my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time);
  264. Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_DoUpdate start";
  265. if ( $hash->{INTERVAL}) {
  266. RemoveInternalTimer ($hash);
  267. my $nextTrigger = gettimeofday() + $hash->{INTERVAL};
  268. $hash->{TRIGGERTIME} = $nextTrigger;
  269. $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
  270. InternalTimer($nextTrigger, "TRAFFIC_DoUpdate", $hash, 0);
  271. Log3 $hash, 3, "TRAFFIC: ($name) internal interval timer set to call GetUpdate again in " . int($hash->{INTERVAL}). " seconds";
  272. }
  273. my $returnJSON;
  274. my $TRlanguage = '';
  275. if(defined(AttrVal($name,"language",undef))){
  276. $TRlanguage = '&language='.AttrVal($name,"language","");
  277. }else{
  278. Log3 $hash, 5, "TRAFFIC: ($name) no language specified";
  279. }
  280. my $TRwaypoints = '';
  281. if(defined(AttrVal($name,"waypoints",undef))){
  282. $TRwaypoints = '&waypoints=via:' . join('|via:', split('\|', AttrVal($name,"waypoints",undef)));
  283. }else{
  284. Log3 $hash, 3, "TRAFFIC: ($name) no waypoints specified";
  285. }
  286. if($direction eq "return"){
  287. if(defined(AttrVal($name,"returnWaypoints",undef))){
  288. $TRwaypoints = '&waypoints=via:' . join('|via:', split('\|', AttrVal($name,"returnWaypoints",undef)));
  289. Log3 $hash, 3, "TRAFFIC: ($name) using returnWaypoints";
  290. }elsif(defined(AttrVal($name,"waypoints",undef))){
  291. $TRwaypoints = '&waypoints=via:' . join('|via:', reverse split('\|', AttrVal($name,"waypoints",undef)));
  292. Log3 $hash, 3, "TRAFFIC: ($name) reversing waypoints";
  293. }else{
  294. Log3 $hash, 3, "TRAFFIC: ($name) no waypoints for return specified";
  295. }
  296. }
  297. my $origin = AttrVal($name, "start_address", 0 );
  298. my $destination = AttrVal($name, "end_address", 0 );
  299. my $travelMode = AttrVal($name, "travelMode", 'driving' );
  300. if($direction eq "return"){
  301. $origin = AttrVal($name, "end_address", 0 );
  302. $destination = AttrVal($name, "start_address", 0 );
  303. }
  304. my $url = 'https://maps.googleapis.com/maps/api/directions/json?origin='.$origin.'&destination='.$destination.'&mode='.$travelMode.$TRlanguage.'&departure_time=now'.$TRwaypoints.'&key='.$hash->{APIKEY};
  305. Log3 $hash, 2, "TRAFFIC: ($name) using $url";
  306. my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } );
  307. $ua->default_header("HTTP_REFERER" => "www.google.de");
  308. my $body = $ua->get($url);
  309. my $json = decode_json($body->decoded_content);
  310. my $duration_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'value'} ;
  311. my $duration_in_traffic_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'value'};
  312. $returnJSON->{'duration'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  313. $returnJSON->{'duration_in_traffic'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  314. $returnJSON->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  315. $returnJSON->{'state'} = $json->{'status'};
  316. $returnJSON->{'status'} = $json->{'status'};
  317. $returnJSON->{'eta'} = FmtTime( gettimeofday() + $duration_in_traffic_sec ) if defined($duration_in_traffic_sec);
  318. $returnJSON->{'debugLocation'} = $json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lat}.','.$json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lng} if AttrVal($name, "verbose", 0 ) == 5;
  319. $returnJSON->{'debugPoly'} = encode_base64 ($json->{'routes'}[0]->{overview_polyline}->{points}) if AttrVal($name, "verbose", 0 ) == 5;
  320. if($duration_in_traffic_sec && $duration_sec){
  321. $returnJSON->{'delay'} = prettySeconds($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  322. Log3 $hash, 3, "TRAFFIC: ($name) delay in seconds = $duration_in_traffic_sec - $duration_sec";
  323. if (AttrVal($name, "outputReadings", "" ) =~ m/min/ && defined($duration_in_traffic_sec) && defined($duration_sec)){
  324. $returnJSON->{'delay_min'} = int($duration_in_traffic_sec - $duration_sec);
  325. }
  326. if(defined($returnJSON->{'delay_min'})){
  327. if( ( $returnJSON->{'delay_min'} && $returnJSON->{'delay_min'} =~ m/^-/ ) || $returnJSON->{'delay_min'} < 60){
  328. Log3 $hash, 5, "TRAFFIC: ($name) delay_min was negative or less than 1min (".$returnJSON->{'delay_min'}."), set to 0";
  329. $returnJSON->{'delay_min'} = 0;
  330. }else{
  331. $returnJSON->{'delay_min'} = int($returnJSON->{'delay_min'} / 60 + 0.5); #divide 60 and round
  332. }
  333. }
  334. }else{
  335. Log3 $hash, 1, "TRAFFIC: ($name) did not receive duration_in_traffic, not able to calculate delay";
  336. }
  337. # condition based values
  338. $returnJSON->{'error_message'} = $json->{'error_message'} if $json->{'error_message'};
  339. # output readings
  340. $returnJSON->{'duration_min'} = int($duration_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/ && defined($duration_sec);
  341. $returnJSON->{'duration_in_traffic_min'} = int($duration_in_traffic_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/ && defined($duration_in_traffic_sec);
  342. $returnJSON->{'duration_sec'} = $duration_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/;
  343. $returnJSON->{'duration_in_traffic_sec'} = $duration_in_traffic_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/;
  344. # raw data (seconds)
  345. $returnJSON->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'value'} if AttrVal($name, "raw_data", 0);
  346. # average readings
  347. if(AttrVal($name, "outputReadings", "" ) =~ m/average/){
  348. # calc average
  349. $returnJSON->{'average_duration_min'} = int($hash->{READINGS}{'average_duration_min'}{VAL} + $returnJSON->{'duration_min'}) / 2 if $returnJSON->{'duration_min'};
  350. $returnJSON->{'average_duration_in_traffic_min'} = int($hash->{READINGS}{'average_duration_in_traffic_min'}{VAL} + $returnJSON->{'duration_in_traffic_min'}) / 2 if $returnJSON->{'duration_in_traffic_min'};
  351. $returnJSON->{'average_delay_min'} = int($hash->{READINGS}{'average_delay_min'}{VAL} + $returnJSON->{'delay_min'}) / 2 if $returnJSON->{'delay_min'};
  352. # override if this is the first average
  353. $returnJSON->{'average_duration_min'} = $returnJSON->{'duration_min'} if !$hash->{READINGS}{'average_duration_min'}{VAL};
  354. $returnJSON->{'average_duration_in_traffic_min'} = $returnJSON->{'duration_in_traffic_min'} if !$hash->{READINGS}{'average_duration_in_traffic_min'}{VAL};
  355. $returnJSON->{'average_delay_min'} = $returnJSON->{'delay_min'} if !$hash->{READINGS}{'average_delay_min'}{VAL};
  356. }
  357. Log3 $hash, 5, "TRAFFIC: ($name) returning from TRAFFIC_DoUpdate: ".encode_json($returnJSON);
  358. Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_DoUpdate done";
  359. return "$name;;;$direction;;;".encode_json($returnJSON);
  360. }
  361. sub TRAFFIC_FinishUpdate($){
  362. my ($name,$direction,$rawJson) = split(/;;;/,shift);
  363. my $hash = $defs{$name};
  364. my %sensors;
  365. my $dotrigger = 1;
  366. Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_FinishUpdate start";
  367. my $json = decode_json($rawJson);
  368. readingsBeginUpdate($hash);
  369. foreach my $readingName (keys %{$json}){
  370. Log3 $hash, 3, "TRAFFIC: ($name) ReadingsUpdate: $readingName - ".$json->{$readingName};
  371. if($direction eq 'return'){
  372. readingsBulkUpdate($hash,'return_'.$readingName,$json->{$readingName});
  373. }else{
  374. readingsBulkUpdate($hash,$readingName,$json->{$readingName});
  375. }
  376. }
  377. if($json->{'status'} eq 'UNKNOWN_ERROR'){ # UNKNOWN_ERROR indicates a directions request could not be processed due to a server error. The request may succeed if you try again.
  378. InternalTimer(gettimeofday() + 3, "TRAFFIC_StartUpdate", $hash, 0);
  379. }
  380. if(my $stateReading = AttrVal($name,"stateReading",undef)){
  381. Log3 $hash, 5, "TRAFFIC: ($name) stateReading defined, override state";
  382. if(defined($json->{$stateReading})){
  383. readingsBulkUpdate($hash,'state',$json->{$stateReading});
  384. }else{
  385. Log3 $hash, 1, "TRAFFIC: ($name) stateReading $stateReading not found";
  386. }
  387. }
  388. readingsEndUpdate($hash, $dotrigger);
  389. Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_FinishUpdate done";
  390. }
  391. sub TRAFFIC_weblink{
  392. my $name = shift();
  393. my $return = shift();
  394. return "<a href='/fhem/TRAFFIC_debug?name=$name&return=$return'>open Map for TRAFFIC $name </a><br>";
  395. }
  396. sub TRAFFIC_debug(){
  397. my $name = $FW_webArgs{name};
  398. my $return = $FW_webArgs{return};
  399. return if(!defined($name));
  400. $FW_RETTYPE = "text/html; charset=UTF-8";
  401. $FW_RET="";
  402. Log 1,"[traffic debug] called for $name";
  403. my $debugPoly = 'debugPoly';
  404. my $debugLocation = 'debugLocation';
  405. if($return eq 1){
  406. $debugPoly = 'return_'.$debugPoly;
  407. $debugLocation = 'return_'.$debugLocation;
  408. }
  409. my $web = '<script type="text/javascript" src="http://maps.google.com/maps/api/js?libraries=geometry&amp;sensor=false"></script>
  410. <input size="200" type="hidden" id="path" value="'.decode_base64(ReadingsVal($name, "debugPoly", undef) ).'">';
  411. $web .= '<input size="200" type="hidden" id="pathR" value="'.decode_base64(ReadingsVal($name, "return_debugPoly", undef) ).'">' if decode_base64(ReadingsVal($name, "return_debugPoly", undef) );
  412. $web .= '
  413. <div id="map"></div>
  414. <style>
  415. #map {width:800px;height:800px;}
  416. </style>
  417. <script type="text/javascript">
  418. function initialize() {
  419. var myLatlng = new google.maps.LatLng('.ReadingsVal($name, "$debugLocation", undef ).');
  420. var myOptions = {
  421. zoom: 10,
  422. center: myLatlng,
  423. mapTypeId: google.maps.MapTypeId.ROADMAP
  424. }
  425. var map = new google.maps.Map(document.getElementById("map"), myOptions);
  426. var decodedPath = google.maps.geometry.encoding.decodePath(document.getElementById("path").value);
  427. var decodedLevels = decodeLevels("");
  428. var setRegion = new google.maps.Polyline({
  429. path: decodedPath,
  430. levels: decodedLevels,
  431. strokeColor: "#4cde44",
  432. strokeOpacity: 1.0,
  433. strokeWeight: 6,
  434. map: map
  435. });';
  436. $web .= 'var decodedPathR = google.maps.geometry.encoding.decodePath(document.getElementById("pathR").value);
  437. var decodedLevelsR = decodeLevels("");
  438. var setRegionR = new google.maps.Polyline({
  439. path: decodedPathR,
  440. levels: decodedLevels,
  441. strokeColor: "#FF0000",
  442. strokeOpacity: 1.0,
  443. strokeWeight: 2,
  444. map: map
  445. });' if decode_base64(ReadingsVal($name, "return_debugPoly", undef) );
  446. $web .='
  447. }
  448. function decodeLevels(encodedLevelsString) {
  449. var decodedLevels = [];
  450. for (var i = 0; i < encodedLevelsString.length; ++i) {
  451. var level = encodedLevelsString.charCodeAt(i) - 63;
  452. decodedLevels.push(level);
  453. }
  454. return decodedLevels;
  455. }
  456. initialize();
  457. </script>';
  458. FW_pO $web;
  459. return ($FW_RETTYPE, $FW_RET);
  460. }
  461. sub prettySeconds {
  462. my $time = shift;
  463. if($time =~ m/^-/){
  464. return "0 min";
  465. }
  466. my $days = int($time / 86400);
  467. $time -= ($days * 86400);
  468. my $hours = int($time / 3600);
  469. $time -= ($hours * 3600);
  470. my $minutes = int($time / 60);
  471. my $seconds = $time % 60;
  472. $days = $days < 1 ? '' : $days .' days ';
  473. $hours = $hours < 1 ? '' : $hours .' hours ';
  474. $minutes = $minutes < 1 ? '' : $minutes . ' min ';
  475. $time = $days . $hours . $minutes;
  476. if(!$time){
  477. return "0 min";
  478. }else{
  479. return $time;
  480. }
  481. }
  482. 1;
  483. #======================================================================
  484. #======================================================================
  485. #
  486. # HTML Documentation for help and commandref
  487. #
  488. #======================================================================
  489. #======================================================================
  490. =pod
  491. =item device
  492. =item summary provide traffic details with Google Distance API
  493. =item summary_DE stellt Verkehrsdaten mittels Google Distance API bereit
  494. =begin html
  495. <a name="TRAFFIC"></a>
  496. <h3>TRAFFIC</h3>
  497. <ul>
  498. <u><b>TRAFFIC - google maps directions module</b></u>
  499. <br>
  500. <br>
  501. This FHEM module collects and displays data obtained via the google maps directions api<br>
  502. requirements:<br>
  503. perl JSON module<br>
  504. perl LWP::SIMPLE module<br>
  505. perl MIME::Base64 module<br>
  506. Google maps API key<br>
  507. <br>
  508. <b>Features:</b>
  509. <br>
  510. <ul>
  511. <li>get distance between start and end location</li>
  512. <li>get travel time for route</li>
  513. <li>get travel time in traffic for route</li>
  514. <li>define additional waypoints</li>
  515. <li>calculate delay between travel-time and travel-time-in-traffic</li>
  516. <li>choose default language</li>
  517. <li>disable the device</li>
  518. <li>5 log levels</li>
  519. <li>get outputs in seconds / meter (raw_data)</li>
  520. <li>state of google maps returned in error reading (i.e. The provided API key is invalid)</li>
  521. <li>customize update interval (default 3600 seconds)</li>
  522. <li>calculate ETA with localtime and delay</li>
  523. <li>configure the output readings with attribute outputReadings, text, min sec</li>
  524. <li>configure the state-reading </li>
  525. <li>optionally display the same route in return</li>
  526. <li>one-time-burst, specify the amount and interval between updates</li>
  527. <li>different Travel Modes (driving, walking, bicycling and transit)</li>
  528. <li>flexible update schedule</li>
  529. <li>integrated Map to visualize configured route at verbose 5</li>
  530. </ul>
  531. <br>
  532. <br>
  533. <a name="TRAFFICdefine"></a>
  534. <b>Define:</b>
  535. <ul><br>
  536. <code>define &lt;name&gt; TRAFFIC &lt;YOUR-API-KEY&gt; [UPDATE-INTERVAL]</code>
  537. <br><br>
  538. example:<br>
  539. <code>define muc2berlin TRAFFIC ABCDEFGHIJKLMNOPQRSTVWYZ 600</code><br>
  540. </ul>
  541. <br>
  542. <br>
  543. <b>Attributes:</b>
  544. <ul>
  545. <li>"start_address" - Street, zipcode City <b>(mandatory)</b></li>
  546. <li>"end_address" - Street, zipcode City <b>(mandatory)</b></li>
  547. <li>"raw_data" - 0:1</li>
  548. <li>"language" - de, en etc.</li>
  549. <li>"waypoints" - Lat, Long coordinates, separated by | </li>
  550. <li>"returnWaypoints" - Lat, Long coordinates, separated by | </li>
  551. <li>"disable" - 0:1</li>
  552. <li>"stateReading" - name the reading which will be used in device state</li>
  553. <li>"outputReadings" - define what kind of readings you want to get: text, min, sec, average</li>
  554. <li>"updateSchedule" - define a flexible update schedule, syntax &lt;starthour&gt;-&lt;endhour&gt; [&lt;day&gt;] &lt;seconds&gt; , multiple entries by sparated by |<br> <i>example:</i> 7-9 1 120 - Monday between 7 and 9 every 2minutes <br> <i>example:</i> 17-19 120 - every Day between 17 and 19 every 2minutes <br> <i>example:</i> 6-8 1 60|6-8 2 60|6-8 3 60|6-8 4 60|6-8 5 60 - Monday till Friday, 60 seconds between 6 and 8 am</li>
  555. <li>"travelMode" - default: driving, options walking, bicycling or transit </li>
  556. <li>"includeReturn" - 0:1</li>
  557. </ul>
  558. <br>
  559. <br>
  560. <a name="TRAFFICreadings"></a>
  561. <b>Readings:</b>
  562. <ul>
  563. <li>delay </li>
  564. <li>distance </li>
  565. <li>duration </li>
  566. <li>duration_in_traffic </li>
  567. <li>state </li>
  568. <li>eta</li>
  569. <li>delay_min</li>
  570. <li>duration_min</li>
  571. <li>duration_in_traffic_min</li>
  572. <li>error_message</li>
  573. </ul>
  574. <br><br>
  575. <a name="TRAFFICset"></a>
  576. <b>Set</b>
  577. <ul>
  578. <li>update [burst-update-count] [burst-update-interval] - update readings manually</li>
  579. </ul>
  580. <br><br>
  581. </ul>
  582. =end html
  583. =cut