diff --git a/fhem/CHANGED b/fhem/CHANGED index 38da16f3b..009e076b7 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 98_TRAFFIC: v1.3.3, added alternatives, various improvements - feature: 70_BRAVIA: new command "application" to start an application on TV - feature: FHEMWEB: add structure to roomnames (Forum #63530) - feature: 98_dewpoint: Generated readings are now subject to standard diff --git a/fhem/FHEM/98_TRAFFIC.pm b/fhem/FHEM/98_TRAFFIC.pm index 58ba1dc54..29c377b62 100644 --- a/fhem/FHEM/98_TRAFFIC.pm +++ b/fhem/FHEM/98_TRAFFIC.pm @@ -42,6 +42,7 @@ # Map https, with APIKey, Traffic & customizable, new attributes GoogleMapsStyle,GoogleMapsSize,GoogleMapsLocation,GoogleMapsStroke,GoogleMapsDisableUI # 2017-04-21 added buttons to save current map settings, renamed attribute GoogleMapsLocation to GoogleMapsCenter # 2017-04-22 v1.3.2 stroke supports weight and opacity, minor fixes +# 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 # ############################################################################## @@ -69,7 +70,7 @@ sub TRAFFIC_GetUpdate($); my %TRcmds = ( 'update' => 'noArg', ); -my $TRVersion = '1.3.2'; +my $TRVersion = '1.3.3'; sub TRAFFIC_Initialize($){ @@ -81,13 +82,15 @@ sub TRAFFIC_Initialize($){ $hash->{AttrFn} = "TRAFFIC_Attr"; $hash->{AttrList} = - "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 " . + "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 " . $readingFnAttributes; $data{FWEXT}{"/TRAFFIC"}{FUNC} = "TRAFFIC"; $data{FWEXT}{"/TRAFFIC"}{FORKABLE} = 1; $hash->{FW_detailFn} = "TRAFFIC_fhemwebFn"; + $hash->{DbLog_splitFn} = "X_DbLog_split"; + } sub TRAFFIC_Define($$){ @@ -149,7 +152,6 @@ sub TRAFFIC_Define($$){ sub TRAFFIC_Undef($$){ - my ( $hash, $arg ) = @_; RemoveInternalTimer ($hash); return undef; @@ -195,9 +197,10 @@ sub TRAFFIC_GetMap($@){ my $name = $device; my $hash = $defs{$name}; - my $debugPoly = $hash->{helper}{'Poly'}; - my $returnDebugPoly = $hash->{helper}{'return_Poly'}; - my $GoogleMapsCenter = AttrVal($name, "GoogleMapsCenter", $hash->{helper}{'GoogleMapsCenter'}); + my $debugPoly=1; + my @alternativesPoly = split(',',decode_base64($hash->{helper}{'Poly'})); + my $returnDebugPoly = $hash->{helper}{'return_Poly'}; + my $GoogleMapsCenter = AttrVal($name, "GoogleMapsCenter", $hash->{helper}{'GoogleMapsCenter'}); if(!$debugPoly || !$GoogleMapsCenter){ return "
please update your device first
"; @@ -222,22 +225,35 @@ sub TRAFFIC_GetMap($@){ $GoogleMapsStroke1Color = '#4cde44' if !$GoogleMapsStroke1Color; $GoogleMapsStroke1Weight = '6' if !$GoogleMapsStroke1Weight; $GoogleMapsStroke1Opacity = '100' if !$GoogleMapsStroke1Opacity; + $GoogleMapsStroke2Color = '#FF0000' if !$GoogleMapsStroke2Color; $GoogleMapsStroke2Weight = '1' if !$GoogleMapsStroke2Weight; $GoogleMapsStroke2Opacity = '100' if !$GoogleMapsStroke2Opacity; - + # make percent value to 50 to 0.5 etc $GoogleMapsStroke1Opacity = ($GoogleMapsStroke1Opacity / 100); $GoogleMapsStroke2Opacity = ($GoogleMapsStroke2Opacity / 100); + # pregenerate the alternatives colors, bit darker and thinner than the primary route + my $GoogleMapsStrokeAColor = '#'.lightHex($GoogleMapsStroke1Color, '0.3'); + my $GoogleMapsStrokeAWeight = int($GoogleMapsStroke1Weight - 3); + my $GoogleMapsStrokeAOpacity = $GoogleMapsStroke1Opacity; + my $GoogleMapsDisableUI = ''; $GoogleMapsDisableUI = "disableDefaultUI: true," if AttrVal($name, "GoogleMapsDisableUI", 0) eq 1; + my $GoogleMapsFixedMap = ''; + $GoogleMapsFixedMap = "draggable: false," if AttrVal($name, "GoogleMapsFixedMap", 0) eq 1; + Log3 $hash, 4, "TRAFFIC: ($name) drawing map in style ".AttrVal($name, "GoogleMapsStyle", 'default' )." in $GoogleMapsWidth x $GoogleMapsHeight px"; my $map; - $map .= '
- '; + $map .= '
'; + + foreach my $polyIndex (0..$#alternativesPoly){ + $map .= ''; + } + $map .= '' if $returnDebugPoly && decode_base64($returnDebugPoly); $map .= '
@@ -250,23 +266,29 @@ sub TRAFFIC_GetMap($@){ var myLatlng = new google.maps.LatLng('.$GoogleMapsCenter.'); var myOptions = { zoom: '.$GoogleMapsZoom.', + '.$GoogleMapsFixedMap.' center: myLatlng, '.$GoogleMapsDisableUI.' mapTypeId: google.maps.MapTypeId.ROADMAP, styles: '.$selectedGoogleMapsStyle.' } var map = new google.maps.Map(document.getElementById("map"), myOptions); - var decodedPath = google.maps.geometry.encoding.decodePath(document.getElementById("path").value); + '; + + foreach my $polyIndex (1..$#alternativesPoly){ + $map .='var decodedPath = google.maps.geometry.encoding.decodePath(document.getElementById("path'.$polyIndex.'").value); var decodedLevels = decodeLevels(""); var setRegion = new google.maps.Polyline({ path: decodedPath, levels: decodedLevels, - strokeColor: "'.$GoogleMapsStroke1Color.'", - strokeOpacity: '.$GoogleMapsStroke1Opacity.', - strokeWeight: '.$GoogleMapsStroke1Weight.', + strokeColor: "'.$GoogleMapsStrokeAColor.'", + strokeOpacity: '.$GoogleMapsStrokeAOpacity.', + strokeWeight: '.$GoogleMapsStrokeAWeight.', map: map - });'; - + }); + '; + } + $map .= 'var decodedPathR = google.maps.geometry.encoding.decodePath(document.getElementById("pathR").value); var decodedLevelsR = decodeLevels(""); var setRegionR = new google.maps.Polyline({ @@ -278,6 +300,17 @@ sub TRAFFIC_GetMap($@){ map: map });' if $returnDebugPoly && decode_base64($returnDebugPoly ); + $map .='var decodedPath = google.maps.geometry.encoding.decodePath(document.getElementById("path0").value); + var decodedLevels = decodeLevels(""); + var setRegion = new google.maps.Polyline({ + path: decodedPath, + levels: decodedLevels, + strokeColor: "'.$GoogleMapsStroke1Color.'", + strokeOpacity: '.$GoogleMapsStroke1Opacity.', + strokeWeight: '.$GoogleMapsStroke1Weight.', + map: map + }); + '; $map .= 'var trafficLayer = new google.maps.TrafficLayer(); trafficLayer.setMap(map);' if AttrVal($name, "GoogleMapsTrafficLayer", 0) eq 1; @@ -461,7 +494,7 @@ sub TRAFFIC_StartUpdate($){ if(defined(AttrVal($name, "start_address", undef )) && defined(AttrVal($name, "end_address", undef ))){ BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;normal',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash); - + if(defined(AttrVal($name, "includeReturn", undef )) && AttrVal($name, "includeReturn", undef ) eq 1){ BlockingCall("TRAFFIC_DoUpdate",$hash->{NAME}.';;;return',"TRAFFIC_FinishUpdate",60,"TRAFFIC_AbortUpdate",$hash); } @@ -525,16 +558,19 @@ sub TRAFFIC_DoUpdate(){ } } - my $origin = AttrVal($name, "start_address", 0 ); - my $destination = AttrVal($name, "end_address", 0 ); - my $travelMode = AttrVal($name, "travelMode", 'driving' ); + my $origin = AttrVal($name, "start_address", 0 ); + my $destination = AttrVal($name, "end_address", 0 ); + my $travelMode = AttrVal($name, "travelMode", 'driving' ); + my $alternatives = 'false'; + $alternatives = 'true' if (AttrVal($name, "alternatives", undef )); if($direction eq "return"){ - $origin = AttrVal($name, "end_address", 0 ); - $destination = AttrVal($name, "start_address", 0 ); + $origin = AttrVal($name, "end_address", 0 ); + $destination = AttrVal($name, "start_address", 0 ); + $alternatives = 'false'; } - my $url = 'https://maps.googleapis.com/maps/api/directions/json?origin='.$origin.'&destination='.$destination.'&mode='.$travelMode.$TRlanguage.'&departure_time=now'.$TRwaypoints.'&key='.$hash->{APIKEY}; + 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; Log3 $hash, 4, "TRAFFIC: ($name) using $url"; my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } ); @@ -543,6 +579,7 @@ sub TRAFFIC_DoUpdate(){ # test json decode and catch error nicely + eval { my $testJson = decode_json($body->decoded_content); 1; @@ -550,14 +587,14 @@ sub TRAFFIC_DoUpdate(){ if($@) { my $e = $@; Log3 $hash, 1, "TRAFFIC: ($name) decode_json on googles return failed, cant continue"; - my %errorreturn = ('status' => 'API error'); - return "$name;;;$direction;;;".encode_json(\%errorreturn); + Log3 $hash, 5, "TRAFFIC: ($name) received: ".Dumper($body->decoded_content); + my %errorReturn = ('status' => 'API error','action' => 'retry'); + #fixme: handle this and schedule a retry + return "$name;;;$direction;;;".encode_json(\%errorReturn); }; my $json = decode_json($body->decoded_content); - - my $duration_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'value'} ; my $duration_in_traffic_sec = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'value'}; @@ -567,10 +604,14 @@ sub TRAFFIC_DoUpdate(){ $returnJSON->{'READINGS'}->{'state'} = $json->{'status'}; $returnJSON->{'READINGS'}->{'status'} = $json->{'status'}; $returnJSON->{'READINGS'}->{'eta'} = FmtTime( gettimeofday() + $duration_in_traffic_sec ) if defined($duration_in_traffic_sec); + $returnJSON->{'READINGS'}->{'summary'} = $json->{'routes'}[0]->{'summary'}; - $returnJSON->{'HELPER'}->{'Poly'} = encode_base64 ($json->{'routes'}[0]->{overview_polyline}->{points}); - $returnJSON->{'HELPER'}->{'GoogleMapsCenter'} = $json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lat}.','.$json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lng}; + # handling alternatives + $returnJSON->{'READINGS'}->{'alternatives'} = join( ", ", map { $_->{summary}.' - '.$_->{'legs'}[0]->{'duration_in_traffic'}->{'text'} } @{$json->{'routes'}} ); + $returnJSON->{'HELPER'}->{'Poly'} = encode_base64 (join(',', map{ $_->{overview_polyline}->{points} } @{$json->{'routes'}} )); + $returnJSON->{'HELPER'}->{'GoogleMapsCenter'} = $json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lat}.','.$json->{'routes'}[0]->{'legs'}[0]->{start_location}->{lng}; + if($duration_in_traffic_sec && $duration_sec){ $returnJSON->{'READINGS'}->{'delay'} = prettySeconds($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/text/; Log3 $hash, 4, "TRAFFIC: ($name) delay in seconds = $duration_in_traffic_sec - $duration_sec"; @@ -628,9 +669,24 @@ sub TRAFFIC_FinishUpdate($){ my %sensors; my $dotrigger = 1; - Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC_FinishUpdate start"; my $json = decode_json($rawJson); + + # before we update anything, check if the status contains error, if yes -> retry + if(defined($json->{'status'}) && $json->{'status'} =~ m/error/){ + if ($json->{'action'} eq 'retry'){ + Log3 $hash, 1, "TRAFFIC: ($name) TRAFFIC_doUpdate returned an error \"".$json->{'status'}. "\" will schedule a retry in 5 seconds"; + RemoveInternalTimer ($hash); + my $nextTrigger = gettimeofday() + 5; + $hash->{TRIGGERTIME} = $nextTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); + InternalTimer($nextTrigger, "TRAFFIC_DoUpdate", $hash, 0); + }else{ + Log3 $hash, 1, "TRAFFIC: ($name) TRAFFIC_doUpdate returned an error: ".$json->{'status'}; + } + }else{ + + Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC_FinishUpdate start"; readingsBeginUpdate($hash); my $readings = $json->{'READINGS'}; @@ -671,6 +727,7 @@ sub TRAFFIC_FinishUpdate($){ readingsEndUpdate($hash, $dotrigger); Log3 $hash, 4, "TRAFFIC: ($name) TRAFFIC_FinishUpdate done"; Log3 $hash, 5, "TRAFFIC: ($name) Helper: ".Dumper($hash->{helper}); + }# not an error } sub TRAFFIC_weblink{ @@ -713,9 +770,22 @@ sub prettySeconds { }else{ return $time; } - } +sub minHex{ $_[0]<$_[1] ? $_[0] : $_[1] } + +sub degradeHex{ + my ($rgb, $degr) = (hex(shift), pop); + $rgb -= minHex( $rgb&(0xff<<$_), $degr<<$_ ) for (0,8,16); + return '%06x', $rgb; +} + +sub lightHex { + $_[0] =~ s/#//g; + return sprintf '%02x'x3, + map{ ($_ *= 1+$_[1]) > 0xff ? 0xff : $_ } + map hex, unpack 'A2'x3, $_[0]; +} 1; @@ -785,6 +855,7 @@ sub prettySeconds {
  • "start_address" - Street, zipcode City (mandatory)
  • "end_address" - Street, zipcode City (mandatory)
  • "raw_data" - 0:1
  • +
  • "alternatives" - 0:1, include alternative routes into readings and Map
  • "language" - de, en etc.
  • "waypoints" - Lat, Long coordinates, separated by |
  • "returnWaypoints" - Lat, Long coordinates, separated by |
  • @@ -807,16 +878,18 @@ sub prettySeconds { Readings: