98_TRAFFIC.pm 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. #########################################################################
  2. # $Id: 98_TRAFFIC.pm 16436 2018-03-18 16:05:13Z 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. # 2017-04-21 reduced log entries if verbose is not set, fixed JSON error, Map available through FHEM-Web-toggle, and direct link
  42. # Map https, with APIKey, Traffic & customizable, new attributes GoogleMapsStyle,GoogleMapsSize,GoogleMapsLocation,GoogleMapsStroke,GoogleMapsDisableUI
  43. # 2017-04-21 added buttons to save current map settings, renamed attribute GoogleMapsLocation to GoogleMapsCenter
  44. # 2017-04-22 v1.3.2 stroke supports weight and opacity, minor fixes
  45. # 2017-12-51 v1.3.3 catch JSON decode issue, addedn Dbog_splitFn, added reading summary, new attr GoogleMapsFixedMap, net attr alternatives, new reading alternatives, alternatives, lighter&thinner on map
  46. # 2018-01-26 v1.3.4 fixed Dbog_splitFn, improved exception handling
  47. # 2018-01-28 v1.3.5 fixed Dbog_splitFn again
  48. # 2018-01-28 v1.3.6 removed perl warning on module load
  49. # 2018-03-02 v1.3.7 fixed issue with special character in readings, updateschedule supports multiple timeframes per day
  50. #
  51. ##############################################################################
  52. package main;
  53. use strict;
  54. use warnings;
  55. use Data::Dumper;
  56. use Time::HiRes qw(gettimeofday);
  57. use Time::Local;
  58. use LWP::Simple qw($ua get);
  59. use Blocking;
  60. use POSIX;
  61. use JSON;
  62. die "MIME::Base64 missing!" unless(eval{require MIME::Base64});
  63. die "JSON missing!" unless(eval{require JSON});
  64. sub TRAFFIC_Initialize($);
  65. sub TRAFFIC_Define($$);
  66. sub TRAFFIC_Undef($$);
  67. sub TRAFFIC_Set($@);
  68. sub TRAFFIC_Attr(@);
  69. sub TRAFFIC_GetUpdate($);
  70. sub TRAFFIC_DbLog_split($);
  71. my %TRcmds = (
  72. 'update' => 'noArg',
  73. );
  74. my $TRVersion = '1.3.7';
  75. sub TRAFFIC_Initialize($){
  76. my ($hash) = @_;
  77. $hash->{DefFn} = "TRAFFIC_Define";
  78. $hash->{UndefFn} = "TRAFFIC_Undef";
  79. $hash->{SetFn} = "TRAFFIC_Set";
  80. $hash->{AttrFn} = "TRAFFIC_Attr";
  81. $hash->{AttrList} =
  82. "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 GoogleMapsStyle:default,silver,dark,night GoogleMapsSize GoogleMapsZoom GoogleMapsCenter GoogleMapsStroke GoogleMapsTrafficLayer:0,1 GoogleMapsDisableUI:0,1 GoogleMapsFixedMap:0,1 alternatives:0,1 " .
  83. $readingFnAttributes;
  84. $data{FWEXT}{"/TRAFFIC"}{FUNC} = "TRAFFIC";
  85. $data{FWEXT}{"/TRAFFIC"}{FORKABLE} = 1;
  86. $hash->{FW_detailFn} = "TRAFFIC_fhemwebFn";
  87. $hash->{DbLog_splitFn} = "TRAFFIC_DbLog_split";
  88. }
  89. sub TRAFFIC_Define($$){
  90. my ($hash, $allDefs) = @_;
  91. my @deflines = split('\n',$allDefs);
  92. my @apiDefs = split('[ \t]+', shift @deflines);
  93. if(int(@apiDefs) < 3) {
  94. return "too few parameters: 'define <name> TRAFFIC <APIKEY>'";
  95. }
  96. $hash->{NAME} = $apiDefs[0];
  97. $hash->{APIKEY} = $apiDefs[2];
  98. $hash->{VERSION} = $TRVersion;
  99. delete($hash->{BURSTCOUNT}) if $hash->{BURSTCOUNT};
  100. delete($hash->{BURSTINTERVAL}) if $hash->{BURSTINTERVAL};
  101. my $name = $hash->{NAME};
  102. #clear all readings
  103. foreach my $clearReading ( keys %{$hash->{READINGS}}){
  104. Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted";
  105. delete($hash->{READINGS}{$clearReading});
  106. }
  107. #clear all helpers
  108. foreach my $helperName ( keys %{$hash->{helper}}){
  109. delete($hash->{helper}{$helperName});
  110. }
  111. # clear weblink
  112. FW_fC("delete ".$name."_weblink");
  113. # basic update INTERVAL
  114. if(scalar(@apiDefs) > 3 && $apiDefs[3] =~ m/^\d+$/){
  115. $hash->{INTERVAL} = $apiDefs[3];
  116. }else{
  117. $hash->{INTERVAL} = 3600;
  118. }
  119. Log3 $hash, 4, "TRAFFIC: ($name) defined ".$hash->{NAME}.' with interval set to '.$hash->{INTERVAL};
  120. # put in default verbose level
  121. $attr{$name}{"verbose"} = 1 if !$attr{$name}{"verbose"};
  122. $attr{$name}{"outputReadings"} = "text" if !$attr{$name}{"outputReadings"};
  123. readingsSingleUpdate( $hash, "state", "Initialized", 1 );
  124. my $firstTrigger = gettimeofday() + 2;
  125. $hash->{TRIGGERTIME} = $firstTrigger;
  126. $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger);
  127. RemoveInternalTimer($hash);
  128. InternalTimer($firstTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  129. Log3 $hash, 5, "TRAFFIC: ($name) InternalTimer set to call GetUpdate in 2 seconds for the first time";
  130. return undef;
  131. }
  132. sub TRAFFIC_Undef($$){
  133. my ( $hash, $arg ) = @_;
  134. RemoveInternalTimer ($hash);
  135. return undef;
  136. }
  137. sub TRAFFIC_fhemwebFn($$$$) {
  138. my ($FW_wname, $device, $room, $pageHash) = @_; # pageHash is set for summaryFn.
  139. my $name = $device;
  140. my $hash = $defs{$name};
  141. my $mapState = ReadingsVal($device,".map", "off") eq "on" ? "off" : "on";
  142. my $web = "<span><a href=\"$FW_ME?detail=$device&amp;cmd.$device=setreading $device .map $mapState$FW_CSRF\">toggle Map</a>&nbsp;&nbsp;</span><br>";
  143. if (ReadingsVal($device,".map","off") eq "on") {
  144. $web .= TRAFFIC_GetMap($device);
  145. $web .= TRAFFIC_weblink($device);
  146. $web .= "<form method=\"$FW_formmethod\" action=\"$FW_ME$FW_subdir\" >";
  147. $web .= FW_hidden("fwcsrf", $defs{$FW_wname}{CSRFTOKEN}) if($FW_CSRF);
  148. $web .= FW_hidden("detail", $device);
  149. $web .= FW_hidden("dev.attr$device", $device);
  150. $web .= "<input style='display:none' type='submit' value='save Zoom' class='attr' id='currentMapZoomSubmit'>";
  151. $web .= "<input type='hidden' name='val.attr$device' value='' id='currentMapZoom'>";
  152. $web .= "<input type='hidden' name='cmd.attr$device' value='attr'>";
  153. $web .= "<input type='hidden' name='arg.attr$device' value='GoogleMapsZoom'>";
  154. $web .= "</form>";
  155. $web .= "<form method=\"$FW_formmethod\" action=\"$FW_ME$FW_subdir\" >";
  156. $web .= FW_hidden("fwcsrf", $defs{$FW_wname}{CSRFTOKEN}) if($FW_CSRF);
  157. $web .= FW_hidden("detail", $device);
  158. $web .= FW_hidden("dev.attr$device", $device);
  159. $web .= "<input style='display:none' type='submit' value='save Center' class='attr' id='currentMapCenterSubmit'>";
  160. $web .= "<input type='hidden' name='val.attr$device' value='' id='currentMapCenter'>";
  161. $web .= "<input type='hidden' name='cmd.attr$device' value='attr'>";
  162. $web .= "<input type='hidden' name='arg.attr$device' value='GoogleMapsCenter'>";
  163. $web .= "</form>";
  164. }
  165. return $web;
  166. }
  167. sub TRAFFIC_GetMap($@){
  168. my $device = shift();
  169. my $name = $device;
  170. my $hash = $defs{$name};
  171. my $debugPoly=1;
  172. my @alternativesPoly = split(',',decode_base64($hash->{helper}{'Poly'}));
  173. my $returnDebugPoly = $hash->{helper}{'return_Poly'};
  174. my $GoogleMapsCenter = AttrVal($name, "GoogleMapsCenter", $hash->{helper}{'GoogleMapsCenter'});
  175. if(!$debugPoly || !$GoogleMapsCenter){
  176. return "<div>please update your device first</div>";
  177. }
  178. my%GoogleMapsStyles=(
  179. 'default' => "[]",
  180. 'silver' => '[{"elementType":"geometry","stylers":[{"color":"#f5f5f5"}]},{"elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#f5f5f5"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#bdbdbd"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#eeeeee"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#e5e5e5"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#ffffff"}]},{"featureType":"road.arterial","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#dadada"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"transit.line","elementType":"geometry","stylers":[{"color":"#e5e5e5"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#eeeeee"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#c9c9c9"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]}]',
  181. 'dark' => '[{"elementType":"geometry","stylers":[{"color":"#212121"}]},{"elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#212121"}]},{"featureType":"administrative","elementType":"geometry","stylers":[{"color":"#757575"}]},{"featureType":"administrative.country","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"administrative.land_parcel","stylers":[{"visibility":"off"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#bdbdbd"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#181818"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"poi.park","elementType":"labels.text.stroke","stylers":[{"color":"#1b1b1b"}]},{"featureType":"road","elementType":"geometry.fill","stylers":[{"color":"#2c2c2c"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#8a8a8a"}]},{"featureType":"road.arterial","elementType":"geometry","stylers":[{"color":"#373737"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#3c3c3c"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry","stylers":[{"color":"#4e4e4e"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"transit","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#000000"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#3d3d3d"}]}]',
  182. 'night' => '[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#746855"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#242f3e"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#263c3f"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#6b9a76"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#38414e"}]},{"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#212a37"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#9ca5b3"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#746855"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#1f2835"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#f3d19c"}]},{"featureType":"transit","elementType":"geometry","stylers":[{"color":"#2f3948"}]},{"featureType":"transit.station","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#17263c"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#515c6d"}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"color":"#17263c"}]}]',
  183. );
  184. my $selectedGoogleMapsStyle = $GoogleMapsStyles{ AttrVal($name, "GoogleMapsStyle", 'default' )};
  185. if(!$selectedGoogleMapsStyle){$selectedGoogleMapsStyle = $GoogleMapsStyles{'default'}}; #catch attribute mistake here
  186. # load map scale and zoom from attr, override if empty/na
  187. my ( $GoogleMapsWidth, $GoogleMapsHeight ) = AttrVal($name, "GoogleMapsSize", '800,600') =~ m/(\d+),(\d+)/;
  188. my ( $GoogleMapsZoom ) = AttrVal($name, "GoogleMapsZoom", '10');
  189. my ( $GoogleMapsStroke1Color, $GoogleMapsStroke1Weight, $GoogleMapsStroke1Opacity, $GoogleMapsStroke2Color, $GoogleMapsStroke2Weight, $GoogleMapsStroke2Opacity ) = AttrVal($name, "GoogleMapsStroke", '#4cde44,6,100,#FF0000,1,100') =~ m/^(#[a-zA-z0-9]+),?(\d*),?(\d*),?(#[a-zA-z0-9]+)?,?(\d*),?(\d*)/;
  190. # catch incomplete configuration here and put in defaults
  191. $GoogleMapsStroke1Color = '#4cde44' if !$GoogleMapsStroke1Color;
  192. $GoogleMapsStroke1Weight = '6' if !$GoogleMapsStroke1Weight;
  193. $GoogleMapsStroke1Opacity = '100' if !$GoogleMapsStroke1Opacity;
  194. $GoogleMapsStroke2Color = '#FF0000' if !$GoogleMapsStroke2Color;
  195. $GoogleMapsStroke2Weight = '1' if !$GoogleMapsStroke2Weight;
  196. $GoogleMapsStroke2Opacity = '100' if !$GoogleMapsStroke2Opacity;
  197. # make percent value to 50 to 0.5 etc
  198. $GoogleMapsStroke1Opacity = ($GoogleMapsStroke1Opacity / 100);
  199. $GoogleMapsStroke2Opacity = ($GoogleMapsStroke2Opacity / 100);
  200. # pregenerate the alternatives colors, bit darker and thinner than the primary route
  201. my $GoogleMapsStrokeAColor = '#'.lightHex($GoogleMapsStroke1Color, '0.3');
  202. my $GoogleMapsStrokeAWeight = int($GoogleMapsStroke1Weight - 3);
  203. my $GoogleMapsStrokeAOpacity = $GoogleMapsStroke1Opacity;
  204. my $GoogleMapsDisableUI = '';
  205. $GoogleMapsDisableUI = "disableDefaultUI: true," if AttrVal($name, "GoogleMapsDisableUI", 0) eq 1;
  206. my $GoogleMapsFixedMap = '';
  207. $GoogleMapsFixedMap = "draggable: false," if AttrVal($name, "GoogleMapsFixedMap", 0) eq 1;
  208. Log3 $hash, 4, "TRAFFIC: ($name) drawing map in style ".AttrVal($name, "GoogleMapsStyle", 'default' )." in $GoogleMapsWidth x $GoogleMapsHeight px";
  209. my $map;
  210. $map .= '<div><script type="text/javascript" src="https://maps.google.com/maps/api/js?key='.$hash->{APIKEY}.'&libraries=geometry&amp"></script>';
  211. foreach my $polyIndex (0..$#alternativesPoly){
  212. $map .= '<input size="200" type="hidden" id="path'.$polyIndex.'" value="'.$alternativesPoly[$polyIndex].'">';
  213. }
  214. $map .= '<input size="200" type="hidden" id="pathR" value="'.decode_base64($returnDebugPoly).'">' if $returnDebugPoly && decode_base64($returnDebugPoly);
  215. $map .= '
  216. <div id="map"></div>
  217. <style>
  218. #map {width:'.$GoogleMapsWidth.'px;height:'.$GoogleMapsHeight.'px;}
  219. </style>
  220. <script type="text/javascript">
  221. function initialize() {
  222. var myLatlng = new google.maps.LatLng('.$GoogleMapsCenter.');
  223. var myOptions = {
  224. zoom: '.$GoogleMapsZoom.',
  225. '.$GoogleMapsFixedMap.'
  226. center: myLatlng,
  227. '.$GoogleMapsDisableUI.'
  228. mapTypeId: google.maps.MapTypeId.ROADMAP,
  229. styles: '.$selectedGoogleMapsStyle.'
  230. }
  231. var map = new google.maps.Map(document.getElementById("map"), myOptions);
  232. ';
  233. foreach my $polyIndex (1..$#alternativesPoly){
  234. $map .='var decodedPath = google.maps.geometry.encoding.decodePath(document.getElementById("path'.$polyIndex.'").value);
  235. var decodedLevels = decodeLevels("");
  236. var setRegion = new google.maps.Polyline({
  237. path: decodedPath,
  238. levels: decodedLevels,
  239. strokeColor: "'.$GoogleMapsStrokeAColor.'",
  240. strokeOpacity: '.$GoogleMapsStrokeAOpacity.',
  241. strokeWeight: '.$GoogleMapsStrokeAWeight.',
  242. map: map
  243. });
  244. ';
  245. }
  246. $map .= 'var decodedPathR = google.maps.geometry.encoding.decodePath(document.getElementById("pathR").value);
  247. var decodedLevelsR = decodeLevels("");
  248. var setRegionR = new google.maps.Polyline({
  249. path: decodedPathR,
  250. levels: decodedLevels,
  251. strokeColor: "'.$GoogleMapsStroke2Color.'",
  252. strokeOpacity: '.$GoogleMapsStroke2Opacity.',
  253. strokeWeight: '.$GoogleMapsStroke2Weight.',
  254. map: map
  255. });' if $returnDebugPoly && decode_base64($returnDebugPoly );
  256. $map .='var decodedPath = google.maps.geometry.encoding.decodePath(document.getElementById("path0").value);
  257. var decodedLevels = decodeLevels("");
  258. var setRegion = new google.maps.Polyline({
  259. path: decodedPath,
  260. levels: decodedLevels,
  261. strokeColor: "'.$GoogleMapsStroke1Color.'",
  262. strokeOpacity: '.$GoogleMapsStroke1Opacity.',
  263. strokeWeight: '.$GoogleMapsStroke1Weight.',
  264. map: map
  265. });
  266. ';
  267. $map .= 'var trafficLayer = new google.maps.TrafficLayer();
  268. trafficLayer.setMap(map);' if AttrVal($name, "GoogleMapsTrafficLayer", 0) eq 1;
  269. $map .='
  270. map.addListener("zoom_changed", function() {
  271. document.getElementById("currentMapZoom").value = map.getZoom();
  272. document.getElementById("currentMapZoomSubmit").style.display = "block";
  273. });
  274. map.addListener("dragend", function() {
  275. document.getElementById("currentMapCenter").value = map.getCenter().lat() + "," + map.getCenter().lng();
  276. document.getElementById("currentMapCenterSubmit").style.display = "block";
  277. });
  278. }
  279. function decodeLevels(encodedLevelsString) {
  280. var decodedLevels = [];
  281. for (var i = 0; i < encodedLevelsString.length; ++i) {
  282. var level = encodedLevelsString.charCodeAt(i) - 63;
  283. decodedLevels.push(level);
  284. }
  285. return decodedLevels;
  286. }
  287. initialize();
  288. </script></div>';
  289. return $map;
  290. }
  291. #
  292. # Attr command
  293. #########################################################################
  294. sub TRAFFIC_Attr(@){
  295. my ($cmd,$name,$attrName,$attrValue) = @_;
  296. # $cmd can be "del" or "set"
  297. # $name is device name
  298. my $hash = $defs{$name};
  299. if ($cmd eq "set") {
  300. addToDevAttrList($name, $attrName);
  301. Log3 $hash, 4, "TRAFFIC: ($name) attrName $attrName set to attrValue $attrValue";
  302. }
  303. if($attrName eq "disable" && $attrValue eq "1"){
  304. readingsSingleUpdate( $hash, "state", "disabled", 1 );
  305. }
  306. if($attrName eq "outputReadings" || $attrName eq "includeReturn" || $attrName eq "verbose"){
  307. #clear all readings
  308. foreach my $clearReading ( keys %{$hash->{READINGS}}){
  309. Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted";
  310. delete($hash->{READINGS}{$clearReading});
  311. }
  312. #clear all helpers
  313. foreach my $helperName ( keys %{$hash->{helper}}){
  314. delete($hash->{helper}{$helperName});
  315. }
  316. # start update
  317. InternalTimer(gettimeofday() + 1, "TRAFFIC_StartUpdate", $hash, 0);
  318. }
  319. return undef;
  320. }
  321. sub TRAFFIC_Set($@){
  322. my ($hash, @param) = @_;
  323. return "\"set <TRAFFIC>\" needs at least one argument: \n".join(" ",keys %TRcmds) if (int(@param) < 2);
  324. my $name = shift @param;
  325. my $set = shift @param;
  326. $hash->{VERSION} = $TRVersion if $hash->{VERSION} ne $TRVersion;
  327. if(AttrVal($name, "disable", 0 ) == 1){
  328. readingsSingleUpdate( $hash, "state", "disabled", 1 );
  329. Log3 $hash, 3, "TRAFFIC: ($name) is disabled, $set not set!";
  330. return undef;
  331. }else{
  332. Log3 $hash, 5, "TRAFFIC: ($name) set $name $set";
  333. }
  334. my $validCmds = join("|",keys %TRcmds);
  335. if($set !~ m/$validCmds/ ) {
  336. return join(' ', keys %TRcmds);
  337. }elsif($set =~ m/update/){
  338. Log3 $hash, 5, "TRAFFIC: ($name) update command recieved";
  339. # if update burst ist specified
  340. if( (my $burstCount = shift @param) && (my $burstInterval = shift @param)){
  341. Log3 $hash, 5, "TRAFFIC: ($name) update burst is set to $burstCount $burstInterval";
  342. $hash->{BURSTCOUNT} = $burstCount;
  343. $hash->{BURSTINTERVAL} = $burstInterval;
  344. }else{
  345. Log3 $hash, 5, "TRAFFIC: ($name) no update burst set";
  346. }
  347. # update internal timer and update NOW
  348. my $updateTrigger = gettimeofday() + 1;
  349. $hash->{TRIGGERTIME} = $updateTrigger;
  350. $hash->{TRIGGERTIME_FMT} = FmtDateTime($updateTrigger);
  351. RemoveInternalTimer($hash);
  352. # start update
  353. InternalTimer($updateTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  354. return undef;
  355. }
  356. }
  357. sub TRAFFIC_StartUpdate($){
  358. my ( $hash ) = @_;
  359. my $name = $hash->{NAME};
  360. my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time);
  361. $wday=7 if $wday == 0; #sunday 0 -> sunday 7, monday 0 -> monday 1 ...
  362. if(AttrVal($name, "disable", 0 ) == 1){
  363. RemoveInternalTimer ($hash);
  364. Log3 $hash, 3, "TRAFFIC: ($name) is disabled";
  365. return undef;
  366. }
  367. if ( $hash->{INTERVAL}) {
  368. RemoveInternalTimer ($hash);
  369. delete($hash->{UPDATESCHEDULE});
  370. my $nextTrigger = gettimeofday() + $hash->{INTERVAL};
  371. if(defined(AttrVal($name, "updateSchedule", undef ))){
  372. Log3 $hash, 5, "TRAFFIC: ($name) flexible update Schedule defined";
  373. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  374. my @updateScheduleDef = split('\|', AttrVal($name, "updateSchedule", undef ));
  375. foreach my $upSched (@updateScheduleDef){
  376. my ($upFrom, $upTo, $upDay, $upInterval ) = $upSched =~ m/(\d+)-(\d+)\s(\d{1,})\s?(\d{1,})?/;
  377. if (!$upInterval){
  378. $upInterval = $upDay;
  379. $upDay='';
  380. }
  381. Log3 $hash, 5, "TRAFFIC: ($name) parsed schedule to upFrom $upFrom, upTo $upTo, upDay $upDay, upInterval $upInterval";
  382. Log3 $hash, 2, "TRAFFIC: ($name) DEBUG parsed schedule to upFrom $upFrom, upTo $upTo, upDay $upDay, upInterval $upInterval";
  383. if(!$upFrom || !$upTo || !$upInterval){
  384. Log3 $hash, 1, "TRAFIC: ($name) updateSchedule $upSched not defined correctly";
  385. }else{
  386. if($hour >= $upFrom && $hour < $upTo){ #if we are INSIDE the updateSchedule
  387. if(!$upDay || $upDay == $wday ){
  388. $nextTrigger = gettimeofday() + $upInterval;
  389. Log3 $hash, 4, "TRAFFIC: ($name) schedule $upSched matches ($upFrom to $upTo (on day $upDay) every $upInterval seconds), matches NOW (current hour $hour day $wday), nextTrigger set to $nextTrigger";
  390. Log3 $hash, 2, "TRAFFIC: ($name) DEBUG schedule $upSched matches ($upFrom to $upTo (on day $upDay) every $upInterval seconds), matches NOW (current hour $hour day $wday), nextTrigger set to $nextTrigger";
  391. $hash->{UPDATESCHEDULE} = $upSched;
  392. last; # we have our next match, end the search
  393. }else{
  394. Log3 $hash, 4, "TRAFFIC: ($name) $upSched does match the time but not the day ($wday)";
  395. Log3 $hash, 2, "TRAFFIC: ($name) DEBUG $upSched does match the time but not the day ($wday)";
  396. }
  397. }elsif($hour < $upFrom && ( $wday == $upDay || !$upDay) ){ #get the next upcoming updateSchedule for today
  398. my $upcomingTrigger = timelocal(0,0,$upFrom,$mday,$mon,$year);
  399. Log3 $hash, 2, "TRAFFIC: ($name) DEBUG $upcomingTrigger <= $nextTrigger";
  400. if($upcomingTrigger <= $nextTrigger){
  401. $nextTrigger = $upcomingTrigger;
  402. Log3 $hash, 2, "TRAFFIC: ($name) DEBUG $upSched is the next upcoming updateSchedule, nextTrigger is generated to $nextTrigger";
  403. }
  404. }else{
  405. Log3 $hash, 5, "TRAFFIC: ($name) schedule $upSched does not match hour ($hour)";
  406. }
  407. }
  408. }
  409. }
  410. if(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} > 0){
  411. $nextTrigger = gettimeofday() + $hash->{BURSTINTERVAL};
  412. Log3 $hash, 3, "TRAFFIC: ($name) next update defined by burst";
  413. $hash->{BURSTCOUNT}--;
  414. }elsif(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} == 0){
  415. delete($hash->{BURSTCOUNT});
  416. delete($hash->{BURSTINTERVAL});
  417. Log3 $hash, 4, "TRAFFIC: ($name) burst update is done";
  418. }
  419. $hash->{TRIGGERTIME} = $nextTrigger;
  420. $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
  421. InternalTimer($nextTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  422. Log3 $hash, 4, "TRAFFIC: ($name) internal interval timer set to call StartUpdate again at " . $hash->{TRIGGERTIME_FMT};
  423. }
  424. if(defined(AttrVal($name, "start_address", undef )) && defined(AttrVal($name, "end_address", undef ))){
  425. BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;normal',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash);
  426. if(defined(AttrVal($name, "includeReturn", undef )) && AttrVal($name, "includeReturn", undef ) eq 1){
  427. BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;return',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash);
  428. }
  429. }else{
  430. readingsSingleUpdate( $hash, "state", "incomplete configuration", 1 );
  431. Log3 $hash, 1, "TRAFFIC: ($name) is not configured correctly, please add start_address and end_address";
  432. }
  433. }
  434. sub TRAFFIC_AbortUpdate($){
  435. # doto
  436. }
  437. sub TRAFFIC_DoUpdate(){
  438. my ($string) = @_;
  439. my ($hName, $direction) = split(";;;", $string); # direction is normal or return
  440. my $hash = $defs{$hName};
  441. my $dotrigger = 1;
  442. my $name = $hash->{NAME};
  443. my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time);
  444. Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC DoUpdate start";
  445. if ( $hash->{INTERVAL}) {
  446. RemoveInternalTimer ($hash);
  447. my $nextTrigger = gettimeofday() + $hash->{INTERVAL};
  448. $hash->{TRIGGERTIME} = $nextTrigger;
  449. $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
  450. InternalTimer($nextTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  451. Log3 $hash, 4, "TRAFFIC: ($name) internal interval timer set to call GetUpdate again in " . int($hash->{INTERVAL}). " seconds";
  452. }
  453. my $returnJSON;
  454. my $TRlanguage = '';
  455. if(defined(AttrVal($name,"language",undef))){
  456. $TRlanguage = '&language='.AttrVal($name,"language","");
  457. }else{
  458. Log3 $hash, 5, "TRAFFIC: ($name) no language specified";
  459. }
  460. my $TRwaypoints = '';
  461. if(defined(AttrVal($name,"waypoints",undef))){
  462. $TRwaypoints = '&waypoints=via:' . join('|via:', split('\|', AttrVal($name,"waypoints",undef)));
  463. }else{
  464. Log3 $hash, 4, "TRAFFIC: ($name) no waypoints specified";
  465. }
  466. if($direction eq "return"){
  467. if(defined(AttrVal($name,"returnWaypoints",undef))){
  468. $TRwaypoints = '&waypoints=via:' . join('|via:', split('\|', AttrVal($name,"returnWaypoints",undef)));
  469. Log3 $hash, 4, "TRAFFIC: ($name) using returnWaypoints";
  470. }elsif(defined(AttrVal($name,"waypoints",undef))){
  471. $TRwaypoints = '&waypoints=via:' . join('|via:', reverse split('\|', AttrVal($name,"waypoints",undef)));
  472. Log3 $hash, 4, "TRAFFIC: ($name) reversing waypoints";
  473. }else{
  474. Log3 $hash, 4, "TRAFFIC: ($name) no waypoints for return specified";
  475. }
  476. }
  477. my $origin = AttrVal($name, "start_address", 0 );
  478. my $destination = AttrVal($name, "end_address", 0 );
  479. my $travelMode = AttrVal($name, "travelMode", 'driving' );
  480. my $alternatives = 'false';
  481. $alternatives = 'true' if (AttrVal($name, "alternatives", undef ));
  482. if($direction eq "return"){
  483. $origin = AttrVal($name, "end_address", 0 );
  484. $destination = AttrVal($name, "start_address", 0 );
  485. $alternatives = 'false';
  486. }
  487. my $url = 'https://maps.googleapis.com/maps/api/directions/json?origin='.$origin.'&destination='.$destination.'&mode='.$travelMode.$TRlanguage.'&departure_time=now'.$TRwaypoints.'&key='.$hash->{APIKEY}.'&alternatives='.$alternatives;
  488. Log3 $hash, 4, "TRAFFIC: ($name) using $url";
  489. my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } );
  490. $ua->default_header("HTTP_REFERER" => "www.google.de");
  491. my $body = $ua->get($url);
  492. # test json decode and catch error nicely
  493. eval {
  494. my $testJson = decode_json($body->decoded_content);
  495. 1;
  496. };
  497. if($@) {
  498. my $e = $@;
  499. Log3 $hash, 1, "TRAFFIC: ($name) decode_json on googles return failed, cant continue";
  500. Log3 $hash, 5, "TRAFFIC: ($name) received: ".Dumper($body->decoded_content);
  501. my %errorReturn = ('status' => 'API error','action' => 'retry');
  502. return "$name;;;$direction;;;".encode_json(\%errorReturn);
  503. };
  504. my $json = JSON->new->utf8(0)->decode($body->decoded_content); #utf8 decoding to support special characters in return & readings
  505. my $duration_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'value'} ;
  506. my $duration_in_traffic_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'value'};
  507. $returnJSON->{'READINGS'}->{'duration'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  508. $returnJSON->{'READINGS'}->{'duration_in_traffic'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  509. $returnJSON->{'READINGS'}->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  510. $returnJSON->{'READINGS'}->{'state'} = $json->{'status'};
  511. $returnJSON->{'READINGS'}->{'status'} = $json->{'status'};
  512. $returnJSON->{'READINGS'}->{'eta'} = FmtTime( gettimeofday() + $duration_in_traffic_sec ) if defined($duration_in_traffic_sec);
  513. $returnJSON->{'READINGS'}->{'summary'} = $json->{'routes'}[0]->{'summary'};
  514. # handling alternatives
  515. $returnJSON->{'READINGS'}->{'alternatives'} = join( ", ", map { $_->{summary}.' - '.$_->{'legs'}[0]->{'duration_in_traffic'}->{'text'} } @{$json->{'routes'}} );
  516. $returnJSON->{'HELPER'}->{'Poly'} = encode_base64 (join(',', map{ $_->{overview_polyline}->{points} } @{$json->{'routes'}} ));
  517. $returnJSON->{'HELPER'}->{'GoogleMapsCenter'} = $json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lat}.','.$json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lng};
  518. if($duration_in_traffic_sec && $duration_sec){
  519. $returnJSON->{'READINGS'}->{'delay'} = prettySeconds($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/text/;
  520. Log3 $hash, 4, "TRAFFIC: ($name) delay in seconds = $duration_in_traffic_sec - $duration_sec";
  521. if (AttrVal($name, "outputReadings", "" ) =~ m/min/ && defined($duration_in_traffic_sec) && defined($duration_sec)){
  522. $returnJSON->{'READINGS'}->{'delay_min'} = int($duration_in_traffic_sec - $duration_sec);
  523. }
  524. if(defined($returnJSON->{'READINGS'}->{'delay_min'})){
  525. if( ( $returnJSON->{'READINGS'}->{'delay_min'} && $returnJSON->{'READINGS'}->{'delay_min'} =~ m/^-/ ) || $returnJSON->{'READINGS'}->{'delay_min'} < 60){
  526. Log3 $hash, 5, "TRAFFIC: ($name) delay_min was negative or less than 1min (".$returnJSON->{'READINGS'}->{'delay_min'}."), set to 0";
  527. $returnJSON->{'READINGS'}->{'delay_min'} = 0;
  528. }else{
  529. $returnJSON->{'READINGS'}->{'delay_min'} = int($returnJSON->{'READINGS'}->{'delay_min'} / 60 + 0.5); #divide 60 and round
  530. }
  531. }
  532. }else{
  533. Log3 $hash, 1, "TRAFFIC: ($name) did not receive duration_in_traffic, not able to calculate delay";
  534. }
  535. # condition based values
  536. $returnJSON->{'READINGS'}->{'error_message'} = $json->{'error_message'} if $json->{'error_message'};
  537. # output readings
  538. $returnJSON->{'READINGS'}->{'duration_min'} = int($duration_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/ && defined($duration_sec);
  539. $returnJSON->{'READINGS'}->{'duration_in_traffic_min'} = int($duration_in_traffic_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/ && defined($duration_in_traffic_sec);
  540. $returnJSON->{'READINGS'}->{'duration_sec'} = $duration_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/;
  541. $returnJSON->{'READINGS'}->{'duration_in_traffic_sec'} = $duration_in_traffic_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/;
  542. # raw data (seconds)
  543. $returnJSON->{'READINGS'}->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'value'} if AttrVal($name, "raw_data", 0);
  544. # average readings
  545. if(AttrVal($name, "outputReadings", "" ) =~ m/average/){
  546. # calc average
  547. $returnJSON->{'READINGS'}->{'average_duration_min'} = int($hash->{READINGS}{'average_duration_min'}{VAL} + $returnJSON->{'READINGS'}->{'duration_min'}) / 2 if $returnJSON->{'READINGS'}->{'duration_min'};
  548. $returnJSON->{'READINGS'}->{'average_duration_in_traffic_min'} = int($hash->{READINGS}{'average_duration_in_traffic_min'}{VAL} + $returnJSON->{'READINGS'}->{'duration_in_traffic_min'}) / 2 if $returnJSON->{'READINGS'}->{'duration_in_traffic_min'};
  549. $returnJSON->{'READINGS'}->{'average_delay_min'} = int($hash->{READINGS}{'average_delay_min'}{VAL} + $returnJSON->{'READINGS'}->{'delay_min'}) / 2 if $returnJSON->{'READINGS'}->{'delay_min'};
  550. # override if this is the first average
  551. $returnJSON->{'READINGS'}->{'average_duration_min'} = $returnJSON->{'READINGS'}->{'duration_min'} if !$hash->{READINGS}{'average_duration_min'}{VAL};
  552. $returnJSON->{'READINGS'}->{'average_duration_in_traffic_min'} = $returnJSON->{'READINGS'}->{'duration_in_traffic_min'} if !$hash->{READINGS}{'average_duration_in_traffic_min'}{VAL};
  553. $returnJSON->{'READINGS'}->{'average_delay_min'} = $returnJSON->{'READINGS'}->{'delay_min'} if !$hash->{READINGS}{'average_delay_min'}{VAL};
  554. }
  555. Log3 $hash, 5, "TRAFFIC: ($name) returning from TRAFFIC DoUpdate: ".encode_json($returnJSON);
  556. Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC DoUpdate done";
  557. return "$name;;;$direction;;;".encode_json($returnJSON);
  558. }
  559. sub TRAFFIC_FinishUpdate($){
  560. my ($name,$direction,$rawJson) = split(/;;;/,shift);
  561. my $hash = $defs{$name};
  562. my %sensors;
  563. my $dotrigger = 1;
  564. my $json = decode_json($rawJson);
  565. # before we update anything, check if the status contains error, if yes -> retry
  566. if(defined($json->{'status'}) && $json->{'status'} =~ m/error/i){ # this handles potential JSON decode issues and retries
  567. if ($json->{'action'} eq 'retry'){
  568. Log3 $hash, 1, "TRAFFIC: ($name) TRAFFIC doUpdate returned an error \"".$json->{'status'}. "\" will schedule a retry in 5 seconds";
  569. RemoveInternalTimer ($hash);
  570. my $nextTrigger = gettimeofday() + 5;
  571. $hash->{TRIGGERTIME} = $nextTrigger;
  572. $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger);
  573. InternalTimer($nextTrigger, "TRAFFIC_StartUpdate", $hash, 0);
  574. }else{
  575. Log3 $hash, 1, "TRAFFIC: ($name) TRAFFIC doUpdate returned an error: ".$json->{'status'};
  576. }
  577. }else{ #JSON decode did not return an error, lets update the device
  578. Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC_FinishUpdate start";
  579. readingsBeginUpdate($hash);
  580. my $readings = $json->{'READINGS'};
  581. my $helper = $json->{'HELPER'};
  582. foreach my $helperName (keys %{$helper}){
  583. if($direction eq 'return'){
  584. Log3 $hash, 4, "TRAFFIC: ($name) HelperUpdate: return_".$helperName." - ".$helper->{$helperName};
  585. $hash->{helper}{'return_'.$helperName} = $helper->{$helperName}; #testme
  586. }else{
  587. Log3 $hash, 4, "TRAFFIC: ($name) HelperUpdate: $helperName - ".$helper->{$helperName};
  588. $hash->{helper}{$helperName} = $helper->{$helperName}; #testme
  589. }
  590. }
  591. foreach my $readingName (keys %{$readings}){
  592. Log3 $hash, 4, "TRAFFIC: ($name) ReadingsUpdate: $readingName - ".$readings->{$readingName};
  593. if($direction eq 'return'){
  594. readingsBulkUpdate($hash,'return_'.$readingName,$readings->{$readingName});
  595. }else{
  596. readingsBulkUpdate($hash,$readingName,$readings->{$readingName});
  597. }
  598. }
  599. if(my $stateReading = AttrVal($name,"stateReading",undef)){
  600. Log3 $hash, 5, "TRAFFIC: ($name) stateReading defined, override state";
  601. if(defined($json->{'READINGS'}->{$stateReading})){
  602. readingsBulkUpdate($hash,'state',$json->{'READINGS'}->{$stateReading});
  603. }else{
  604. Log3 $hash, 1, "TRAFFIC: ($name) stateReading $stateReading not found";
  605. }
  606. }
  607. # if Google returned an error, we gonna try again in 3 seconds
  608. if(defined($json->{'READINGS'}->{'status'}) && $json->{'READINGS'}->{'status'} =~ m/error/i){ # UNKNOWN_ERROR indicates a directions request could not be processed due to a server error. The request may succeed if you try again.
  609. Log3 $hash, 1, "TRAFFIC: ($name) auto-retry as Google returned an error: ".$json->{'READINGS'}->{'status'};
  610. InternalTimer(gettimeofday() + 3, "TRAFFIC_StartUpdate", $hash, 0);
  611. }elsif(defined($hash->{READINGS}->{'error_message'})){
  612. Log3 $hash, 3, "TRAFFIC: ($name) removing reading error_message, status: ".$json->{'READINGS'}->{'status'};
  613. fhem("deletereading $name error_message");
  614. }
  615. readingsEndUpdate($hash, $dotrigger);
  616. Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC_FinishUpdate done";
  617. Log3 $hash, 5, "TRAFFIC: ($name) Helper: ".Dumper($hash->{helper});
  618. }# not an error
  619. }
  620. sub TRAFFIC_weblink{
  621. my $name = shift();
  622. return "<a href='$FW_ME/TRAFFIC?name=$name'>$FW_ME/TRAFFIC?name=$name</a><br>";
  623. }
  624. sub TRAFFIC(){
  625. my $name = $FW_webArgs{name};
  626. return if(!defined($name));
  627. $FW_RETTYPE = "text/html; charset=UTF-8";
  628. $FW_RET="";
  629. my $web .= TRAFFIC_GetMap($name);
  630. FW_pO $web;
  631. return ($FW_RETTYPE, $FW_RET);
  632. }
  633. sub TRAFFIC_DbLog_split($) {
  634. my ($event, $device) = @_;
  635. my $hash = $defs{$device};
  636. Log3 $hash, 5, "TRAFFIC: ($device) TRAFFIC_DbLog_split received event $event on device $device";
  637. my $readings; # this holds all possible readings and their units
  638. $readings->{'update'} = 'text';
  639. $readings->{'duration'} = 'text';
  640. $readings->{'duration_in_traffic'} = 'text';
  641. $readings->{'distance'} = 'text';
  642. $readings->{'state'} = 'text';
  643. $readings->{'status'} = 'text';
  644. $readings->{'eta'} = 'time';
  645. $readings->{'summary'} = 'text';
  646. $readings->{'alternatives'} = 'text';
  647. $readings->{'delay'} = 'text';
  648. $readings->{'delay_min'} = 'min';
  649. $readings->{'error_message'} = 'text';
  650. $readings->{'duration_min'} = 'min';
  651. $readings->{'duration_in_traffic_min'} = 'min';
  652. $readings->{'duration_sec'} = 'sec';
  653. $readings->{'duration_in_traffic_sec'} = 'sec';
  654. $readings->{'distance'} = 'km';
  655. $readings->{'average_duration_min'} = 'min';
  656. $readings->{'average_duration_in_traffic_min'} = 'min';
  657. $readings->{'average_delay_min'} = 'min';
  658. $readings->{'average_duration_min'} = 'min';
  659. $readings->{'average_duration_in_traffic_min'} = 'min';
  660. $readings->{'average_delay_min'} = 'min';
  661. my ($reading, $value, $unit) = "";
  662. my @parts = split(/ /,$event);
  663. $reading = shift @parts;
  664. $reading =~ tr/://d;
  665. my $alternativeReading = $reading;
  666. $alternativeReading =~ s/^return_//;
  667. $value = join(" ",@parts);
  668. if($readings->{$reading}){
  669. $unit = $readings->{$reading};
  670. $value =~ s/$unit$//; #try to remove the unit from the value
  671. }elsif($readings->{$alternativeReading}){
  672. $unit = $readings->{$alternativeReading};
  673. $value =~ s/$unit$//; #try to remove the unit from the value
  674. }else{
  675. Log3 $hash, 5, "TRAFFIC: ($device) TRAFFIC_DbLog_split auto detect unit for reading $reading value $value";
  676. $unit = 'min' if ($reading) =~ m/_min$/;
  677. $unit = 'sec' if ($reading) =~ m/_sec$/;
  678. $unit = 'km' if ($reading) =~ m/_km$/;
  679. }
  680. Log3 $hash, 5, "TRAFFIC: ($device) TRAFFIC_DbLog_split returning $reading, $value, $unit";
  681. return ($reading, $value, $unit);
  682. }
  683. sub prettySeconds {
  684. my $time = shift;
  685. if($time =~ m/^-/){
  686. return "0 min";
  687. }
  688. my $days = int($time / 86400);
  689. $time -= ($days * 86400);
  690. my $hours = int($time / 3600);
  691. $time -= ($hours * 3600);
  692. my $minutes = int($time / 60);
  693. my $seconds = $time % 60;
  694. $days = $days < 1 ? '' : $days .' days ';
  695. $hours = $hours < 1 ? '' : $hours .' hours ';
  696. $minutes = $minutes < 1 ? '' : $minutes . ' min ';
  697. $time = $days . $hours . $minutes;
  698. if(!$time){
  699. return "0 min";
  700. }else{
  701. return $time;
  702. }
  703. }
  704. sub minHex{ $_[0]<$_[1] ? $_[0] : $_[1] }
  705. sub degradeHex{
  706. my ($rgb, $degr) = (hex(shift), pop);
  707. $rgb -= minHex( $rgb&(0xff<<$_), $degr<<$_ ) for (0,8,16);
  708. return '%06x', $rgb;
  709. }
  710. sub lightHex {
  711. $_[0] =~ s/#//g;
  712. return sprintf '%02x'x3,
  713. map{ ($_ *= 1+$_[1]) > 0xff ? 0xff : $_ }
  714. map hex, unpack 'A2'x3, $_[0];
  715. }
  716. 1;
  717. #======================================================================
  718. #======================================================================
  719. #
  720. # HTML Documentation for help and commandref
  721. #
  722. #======================================================================
  723. #======================================================================
  724. =pod
  725. =item device
  726. =item summary provide traffic details with Google Distance API
  727. =item summary_DE stellt Verkehrsdaten mittels Google Distance API bereit
  728. =begin html
  729. <a name="TRAFFIC"></a>
  730. <h3>TRAFFIC</h3>
  731. <ul>
  732. <u><b>TRAFFIC - google maps directions module</b></u>
  733. <br>
  734. <br>
  735. This FHEM module collects and displays data obtained via the google maps directions api<br>
  736. requirements:<br>
  737. perl JSON module<br>
  738. perl LWP::SIMPLE module<br>
  739. perl MIME::Base64 module<br>
  740. Google maps API key<br>
  741. <br>
  742. <b>Features:</b>
  743. <br>
  744. <ul>
  745. <li>get distance between start and end location</li>
  746. <li>get travel time for route</li>
  747. <li>get travel time in traffic for route</li>
  748. <li>define additional waypoints</li>
  749. <li>calculate delay between travel-time and travel-time-in-traffic</li>
  750. <li>choose default language</li>
  751. <li>disable the device</li>
  752. <li>5 log levels</li>
  753. <li>get outputs in seconds / meter (raw_data)</li>
  754. <li>state of google maps returned in error reading (i.e. The provided API key is invalid)</li>
  755. <li>customize update interval (default 3600 seconds)</li>
  756. <li>calculate ETA with localtime and delay</li>
  757. <li>configure the output readings with attribute outputReadings, text, min sec</li>
  758. <li>configure the state-reading </li>
  759. <li>optionally display the same route in return</li>
  760. <li>one-time-burst, specify the amount and interval between updates</li>
  761. <li>different Travel Modes (driving, walking, bicycling and transit)</li>
  762. <li>flexible update schedule</li>
  763. <li>integrated Map to visualize configured route or embed to external GUI</li>
  764. </ul>
  765. <br>
  766. <br>
  767. <a name="TRAFFICdefine"></a>
  768. <b>Define:</b>
  769. <ul><br>
  770. <code>define &lt;name&gt; TRAFFIC &lt;YOUR-API-KEY&gt; [UPDATE-INTERVAL]</code>
  771. <br><br>
  772. example:<br>
  773. <code>define muc2berlin TRAFFIC ABCDEFGHIJKLMNOPQRSTVWYZ 600</code><br>
  774. </ul>
  775. <br>
  776. <br>
  777. <b>Attributes:</b>
  778. <ul>
  779. <li>"start_address" - Street, zipcode City <b>(mandatory)</b></li>
  780. <li>"end_address" - Street, zipcode City <b>(mandatory)</b></li>
  781. <li>"raw_data" - 0:1</li>
  782. <li>"alternatives" - 0:1, include alternative routes into readings and Map</li>
  783. <li>"language" - de, en etc.</li>
  784. <li>"waypoints" - Lat, Long coordinates, separated by | </li>
  785. <li>"returnWaypoints" - Lat, Long coordinates, separated by | </li>
  786. <li>"disable" - 0:1</li>
  787. <li>"stateReading" - name the reading which will be used in device state</li>
  788. <li>"outputReadings" - define what kind of readings you want to get: text, min, sec, average</li>
  789. <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>
  790. <li>"travelMode" - default: driving, options walking, bicycling or transit </li>
  791. <li>"GoogleMapsStyle" - choose your colors from: default,silver,dark,night</li>
  792. <li>"GoogleMapsSize" - Map size in pixel, &lt;width&gt;,&lt;height&gt;</li>
  793. <li>"GoogleMapsCenter" - Lat, Long coordinates of your map center, spearated by ,</li>
  794. <li>"GoogleMapsZoom" - sets your map zoom level</li>
  795. <li>"GoogleMapsStroke" - customize your map poly-strokes in color, weight and opacity <br> &lt;hex-color-code&gt;,[stroke-weight],[stroke-opacity],&lt;hex-color-code-of-return&gt;,[stroke-weight-of-return],[stroke-opacity-of-return]<br>must beginn with #color of each stroke, weight and opacity is optional<br><i>example:</i> #019cdf,#ffeb19<br><i>example:</i> #019cdf,20,#ffeb19<br><i>example:</i> #019cdf,20,#ffeb19,15<br><i>example:</i> #019cdf,#ffeb19,15<br><i>example:</i> #019cdf,20,80,#ffeb19<br><i>example:</i> #019cdf,#ffeb19,15,50<br><i>example:</i> #019cdf,20,80<br><i>default:</i> #4cde44,6,100,#FF0000,1,100</li>
  796. <li>"GoogleMapsTrafficLayer" - enable the basic Google Maps Traffic Layer</li>
  797. <li>"GoogleMapsDisableUI" - hide the map controls</li>
  798. </ul>
  799. <br>
  800. <br>
  801. <a name="TRAFFICreadings"></a>
  802. <b>Readings:</b>
  803. <ul>
  804. <li>alternatives</li>
  805. <li>delay</li>
  806. <li>delay_min</li>
  807. <li>distance</li>
  808. <li>duration</li>
  809. <li>duration_in_traffic</li>
  810. <li>duration_in_traffic_min</li>
  811. <li>duration_min</li>
  812. <li>error_message</li>
  813. <li>eta</li>
  814. <li>state</li>
  815. <li>summary</li>
  816. </ul>
  817. <br><br>
  818. <a name="TRAFFICset"></a>
  819. <b>Set</b>
  820. <ul>
  821. <li>update [burst-update-count] [burst-update-interval] - update readings manually</li>
  822. </ul>
  823. <br><br>
  824. </ul>
  825. =end html
  826. =cut