######################################################################### # $Id$ # fhem Modul which provides traffic details with Google Distance API # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## # Changelog: # # 2016-07-26 initial release # 2016-07-28 added eta, readings in minutes # 2016-08-01 changed JSON decoding/encofing, added stateReading attribute, added outputReadings attribute # 2016-08-02 added attribute includeReturn, round minutes & smart zero'ing, avoid negative values, added update burst # 2016-08-05 fixed 3 perl warnings # 2016-08-09 added auto-update if status returns UNKOWN_ERROR, added outputReading average # 2016-09-25 bugfix Blocking, improved errormessage # 2016-10-07 version 1.0, adding to SVN package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); use Data::Dumper; use LWP::Simple qw($ua get); use JSON; use POSIX; use Blocking; sub TRAFFIC_Initialize($); sub TRAFFIC_Define($$); sub TRAFFIC_Undef($$); sub TRAFFIC_Set($@); sub TRAFFIC_Attr(@); sub TRAFFIC_GetUpdate($); my %TRcmds = ( 'update' => 'noArg', ); my $TRVersion = '1.0'; sub TRAFFIC_Initialize($){ my ($hash) = @_; $hash->{DefFn} = "TRAFFIC_Define"; $hash->{UndefFn} = "TRAFFIC_Undef"; $hash->{SetFn} = "TRAFFIC_Set"; $hash->{AttrFn} = "TRAFFIC_Attr"; $hash->{AttrList} = "disable:0,1 start_address end_address raw_data:0,1 language waypoints stateReading outputReadings includeReturn:0,1 " . $readingFnAttributes; } sub TRAFFIC_Define($$){ my ($hash, $allDefs) = @_; my @deflines = split('\n',$allDefs); my @apiDefs = split('[ \t]+', shift @deflines); if(int(@apiDefs) < 3) { return "too few parameters: 'define TRAFFIC '"; } $hash->{NAME} = $apiDefs[0]; $hash->{APIKEY} = $apiDefs[2]; $hash->{VERSION} = $TRVersion; my $name = $hash->{NAME}; #clear all readings foreach my $clearReading ( keys %{$hash->{READINGS}}){ Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted"; delete($hash->{READINGS}{$clearReading}); } # basic update interval if(scalar(@apiDefs) > 3 && $apiDefs[3] =~ m/^\d+$/){ $hash->{Interval} = $apiDefs[3]; }else{ $hash->{Interval} = 3600; } Log3 $hash, 3, "TRAFFIC: ($name) defined ".$hash->{NAME}.' with interval set to '.$hash->{Interval}; # put in default verbose level $attr{$name}{"verbose"} = 1 if !$attr{$name}{"verbose"}; $attr{$name}{"outputReadings"} = "text" if !$attr{$name}{"outputReadings"}; readingsSingleUpdate( $hash, "state", "Initialized", 1 ); my $firstTrigger = gettimeofday() + 2; $hash->{TRIGGERTIME} = $firstTrigger; $hash->{TRIGGERTIME_FMT} = FmtDateTime($firstTrigger); RemoveInternalTimer($hash); InternalTimer($firstTrigger, "TRAFFIC_StartUpdate", $hash, 0); Log3 $hash, 5, "TRAFFIC: ($name) InternalTimer set to call GetUpdate in 2 seconds for the first time"; return undef; } sub TRAFFIC_Undef($$){ my ( $hash, $arg ) = @_; RemoveInternalTimer ($hash); return undef; } # # Attr command ######################################################################### sub TRAFFIC_Attr(@){ my ($cmd,$name,$attrName,$attrValue) = @_; # $cmd can be "del" or "set" # $name is device name my $hash = $defs{$name}; if ($cmd eq "set") { addToDevAttrList($name, $attrName); Log3 $hash, 3, "TRAFFIC: ($name) attrName $attrName set to attrValue $attrValue"; } if($attrName eq "disable" && $attrValue eq "1"){ readingsSingleUpdate( $hash, "state", "disabled", 1 ); } if($attrName eq "outputReadings" || $attrName eq "includeReturn"){ #clear all readings foreach my $clearReading ( keys %{$hash->{READINGS}}){ Log3 $hash, 5, "TRAFFIC: ($name) READING: $clearReading deleted"; delete($hash->{READINGS}{$clearReading}); } # start update InternalTimer(gettimeofday() + 1, "TRAFFIC_StartUpdate", $hash, 0); } return undef; } sub TRAFFIC_Set($@){ my ($hash, @param) = @_; return "\"set \" needs at least one argument: \n".join(" ",keys %TRcmds) if (int(@param) < 2); my $name = shift @param; my $set = shift @param; $hash->{VERSION} = $TRVersion if $hash->{VERSION} != $TRVersion; if(AttrVal($name, "disable", 0 ) == 1){ readingsSingleUpdate( $hash, "state", "disabled", 1 ); Log3 $hash, 3, "TRAFFIC: ($name) is disabled, $set not set!"; return undef; }else{ Log3 $hash, 5, "TRAFFIC: ($name) set $name $set"; } my $validCmds = join("|",keys %TRcmds); if($set !~ m/$validCmds/ ) { return join(' ', keys %TRcmds); }elsif($set =~ m/update/){ Log3 $hash, 5, "TRAFFIC: ($name) update command recieved"; # if update burst ist specified if( (my $burstCount = shift @param) && (my $burstInterval = shift @param)){ Log3 $hash, 5, "TRAFFIC: ($name) update burst is set to $burstCount $burstInterval"; $hash->{BURSTCOUNT} = $burstCount; $hash->{BURSTINTERVAL} = $burstInterval; }else{ Log3 $hash, 5, "TRAFFIC: ($name) no update burst set"; } # update internal timer and update NOW my $updateTrigger = gettimeofday() + 1; $hash->{TRIGGERTIME} = $updateTrigger; $hash->{TRIGGERTIME_FMT} = FmtDateTime($updateTrigger); RemoveInternalTimer($hash); # start update InternalTimer($updateTrigger, "TRAFFIC_StartUpdate", $hash, 0); return undef; } } sub TRAFFIC_StartUpdate($){ my ( $hash ) = @_; my $name = $hash->{NAME}; if(AttrVal($name, "disable", 0 ) == 1){ RemoveInternalTimer ($hash); Log3 $hash, 3, "TRAFFIC: ($name) is disabled"; return undef; } if ( $hash->{Interval}) { RemoveInternalTimer ($hash); my $nextTrigger = gettimeofday() + $hash->{Interval}; if(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} > 0){ $nextTrigger = gettimeofday() + $hash->{BURSTINTERVAL}; $hash->{BURSTCOUNT}--; }elsif(defined($hash->{BURSTCOUNT}) && $hash->{BURSTCOUNT} == 0){ delete($hash->{BURSTCOUNT}); delete($hash->{BURSTINTERVAL}); } $hash->{TRIGGERTIME} = $nextTrigger; $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); InternalTimer($nextTrigger, "TRAFFIC_StartUpdate", $hash, 0); Log3 $hash, 3, "TRAFFIC: ($name) internal interval timer set to call StartUpdate again in " . int($hash->{Interval}). " seconds"; } 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); } }else{ readingsSingleUpdate( $hash, "state", "incomplete configuration", 1 ); Log3 $hash, 1, "TRAFFIC: ($name) is not configured correctly, please add start_address and end_address"; } } sub TRAFFIC_AbortUpdate($){ } sub TRAFFIC_DoUpdate(){ my ($string) = @_; my ($hName, $direction) = split(";;;", $string); # direction is normal or return my $hash = $defs{$hName}; my $dotrigger = 1; my $name = $hash->{NAME}; my ($sec,$min,$hour,$dayn,$month,$year,$wday,$yday,$isdst) = localtime(time); Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_DoUpdate start"; if ( $hash->{Interval}) { RemoveInternalTimer ($hash); my $nextTrigger = gettimeofday() + $hash->{Interval}; $hash->{TRIGGERTIME} = $nextTrigger; $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); InternalTimer($nextTrigger, "TRAFFIC_DoUpdate", $hash, 0); Log3 $hash, 3, "TRAFFIC: ($name) internal interval timer set to call GetUpdate again in " . int($hash->{Interval}). " seconds"; } my $returnJSON; my $TRlanguage = ''; if(defined(AttrVal($name,"language",undef))){ $TRlanguage = '&language='.AttrVal($name,"language",""); }else{ Log3 $hash, 5, "TRAFFIC: ($name) no language specified"; } my $TRwaypoints = ''; if(defined(AttrVal($name,"waypoints",undef))){ $TRwaypoints = '&waypoints=via:' . join('|via:', split('\|', AttrVal($name,"waypoints",undef))); if($direction eq "return"){ $TRwaypoints = '&waypoints=via:' . join('|via:', reverse split('\|', AttrVal($name,"waypoints",undef))); Log3 $hash, 5, "TRAFFIC: ($name) reversing waypoints"; } }else{ Log3 $hash, 5, "TRAFFIC: ($name) no waypoints specified"; } my $origin = AttrVal($name, "start_address", 0 ); my $destination = AttrVal($name, "end_address", 0 ); if($direction eq "return"){ $origin = AttrVal($name, "end_address", 0 ); $destination = AttrVal($name, "start_address", 0 ); } my $url = 'https://maps.googleapis.com/maps/api/directions/json?origin='.$origin.'&destination='.$destination.'&mode=driving'.$TRlanguage.'&departure_time=now'.$TRwaypoints.'&key='.$hash->{APIKEY}; Log3 $hash, 2, "TRAFFIC: ($name) using $url"; my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } ); $ua->default_header("HTTP_REFERER" => "www.google.de"); my $body = $ua->get($url); 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'}; $returnJSON->{'duration'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/; $returnJSON->{'duration_in_traffic'} = $json->{'routes'}[0]->{'legs'}[0]->{'duration_in_traffic'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/; $returnJSON->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'text'} if AttrVal($name, "outputReadings", "" ) =~ m/text/; $returnJSON->{'state'} = $json->{'status'}; $returnJSON->{'status'} = $json->{'status'}; $returnJSON->{'eta'} = FmtTime( gettimeofday() + $duration_in_traffic_sec ); if($duration_in_traffic_sec && $duration_sec){ $returnJSON->{'delay'} = prettySeconds($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/text/; Log3 $hash, 3, "TRAFFIC: ($name) delay in seconds = $duration_in_traffic_sec - $duration_sec"; $returnJSON->{'delay_min'} = int($duration_in_traffic_sec - $duration_sec) if AttrVal($name, "outputReadings", "" ) =~ m/min/; if(defined($returnJSON->{'delay_min'})){ if( ( $returnJSON->{'delay_min'} && $returnJSON->{'delay_min'} =~ m/^-/ ) || $returnJSON->{'delay_min'} < 60){ Log3 $hash, 5, "TRAFFIC: ($name) delay_min was negative or less than 1min (".$returnJSON->{'delay_min'}."), set to 0"; $returnJSON->{'delay_min'} = 0; }else{ $returnJSON->{'delay_min'} = int($returnJSON->{'delay_min'} / 60 + 0.5); #divide 60 and round } } }else{ Log3 $hash, 1, "TRAFFIC: ($name) did not receive duration_in_traffic, not able to calculate delay"; } # condition based values $returnJSON->{'error_message'} = $json->{'error_message'} if $json->{'error_message'}; # output readings $returnJSON->{'duration_min'} = int($duration_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/; $returnJSON->{'duration_in_traffic_min'} = int($duration_in_traffic_sec / 60 + 0.5) if AttrVal($name, "outputReadings", "" ) =~ m/min/; $returnJSON->{'duration_sec'} = $duration_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/; $returnJSON->{'duration_in_traffic_sec'} = $duration_in_traffic_sec if AttrVal($name, "outputReadings", "" ) =~ m/sec/; # raw data (seconds) $returnJSON->{'distance'} = $json->{'routes'}[0]->{'legs'}[0]->{'distance'}->{'value'} if AttrVal($name, "raw_data", 0); # average readings if(AttrVal($name, "outputReadings", "" ) =~ m/average/){ # calc average $returnJSON->{'average_duration_min'} = int($hash->{READINGS}{'average_duration_min'}{VAL} + $returnJSON->{'duration_min'}) / 2 if $returnJSON->{'duration_min'}; $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'}; $returnJSON->{'average_delay_min'} = int($hash->{READINGS}{'average_delay_min'}{VAL} + $returnJSON->{'delay_min'}) / 2 if $returnJSON->{'delay_min'}; # override if this is the first average $returnJSON->{'average_duration_min'} = $returnJSON->{'duration_min'} if !$hash->{READINGS}{'average_duration_min'}{VAL}; $returnJSON->{'average_duration_in_traffic_min'} = $returnJSON->{'duration_in_traffic_min'} if !$hash->{READINGS}{'average_duration_in_traffic_min'}{VAL}; $returnJSON->{'average_delay_min'} = $returnJSON->{'delay_min'} if !$hash->{READINGS}{'average_delay_min'}{VAL}; } Log3 $hash, 5, "TRAFFIC: ($name) returning from TRAFFIC_DoUpdate: ".encode_json($returnJSON); Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_DoUpdate done"; return "$name;;;$direction;;;".encode_json($returnJSON); } sub TRAFFIC_FinishUpdate($){ my ($name,$direction,$rawJson) = split(/;;;/,shift); my $hash = $defs{$name}; my %sensors; my $dotrigger = 1; Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_FinishUpdate start"; my $json = decode_json($rawJson); readingsBeginUpdate($hash); foreach my $readingName (keys %{$json}){ Log3 $hash, 3, "TRAFFIC: ($name) ReadingsUpdate: $readingName - ".$json->{$readingName}; if($direction eq 'return'){ readingsBulkUpdate($hash,'return_'.$readingName,$json->{$readingName}); }else{ readingsBulkUpdate($hash,$readingName,$json->{$readingName}); } } 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. InternalTimer(gettimeofday() + 3, "TRAFFIC_StartUpdate", $hash, 0); } if(my $stateReading = AttrVal($name,"stateReading",undef)){ Log3 $hash, 5, "TRAFFIC: ($name) stateReading defined, override state"; if(!$json->{$stateReading}){ Log3 $hash, 1, "TRAFFIC: ($name) stateReading $stateReading not found"; }else{ readingsBulkUpdate($hash,'state',$json->{$stateReading}); } } readingsEndUpdate($hash, $dotrigger); Log3 $hash, 3, "TRAFFIC: ($name) TRAFFIC_FinishUpdate done"; } sub prettySeconds { my $time = shift; if($time =~ m/^-/){ return "0 min"; } my $days = int($time / 86400); $time -= ($days * 86400); my $hours = int($time / 3600); $time -= ($hours * 3600); my $minutes = int($time / 60); my $seconds = $time % 60; $days = $days < 1 ? '' : $days .' days '; $hours = $hours < 1 ? '' : $hours .' hours '; $minutes = $minutes < 1 ? '' : $minutes . ' min '; $time = $days . $hours . $minutes; if(!$time){ return "0 min"; }else{ return $time; } } 1; #====================================================================== #====================================================================== # # HTML Documentation for help and commandref # #====================================================================== #====================================================================== =pod =item device =item summary provide traffic details with Google Distance API =item summary_DE stellt Verkehrsdaten mittels Google Distance API bereit =begin html

TRAFFIC

    TRAFFIC - google maps directions module

    This FHEM module collects and displays data obtained via the google maps directions api
    requirements:
    perl JSON module
    perl LWP::SIMPLE module
    Google maps API key

    Features:
    • get distance between start and end location
    • get travel time for route
    • get travel time in traffic for route
    • define additional waypoints
    • calculate delay between travel-time and travel-time-in-traffic
    • choose default language
    • disable the device
    • 5 log levels
    • get outputs in seconds / meter (raw_data)
    • state of google maps returned in error reading (i.e. The provided API key is invalid)
    • customize update interval (default 3600 seconds)
    • calculate ETA with localtime and delay
    • configure the output readings with attribute outputReadings, text, min sec
    • configure the state-reading
    • optionally display the same route in return
    • one-time-burst, specify the amount and interval between updates


    Define:

      define <name> TRAFFIC <YOUR-API-KEY> [UPDATE-INTERVAL]

      example:
      define muc2berlin TRAFFIC ABCDEFGHIJKLMNOPQRSTVWYZ 600


    Attributes:
    • "start_address" - Street, zipcode City (mandatory)
    • "end_address" - Street, zipcode City (mandatory)
    • "raw_data" - 0:1
    • "language" - de, en etc.
    • "waypoints" - Lat, Long coordinates, separated by |
    • "disable" - 0:1
    • "stateReading" - name the reading which will be used in device state
    • "outputReadings" - define what kind of readings you want to get: text, min, sec, average
    • "includeReturn" - 0:1


    Readings:
    • delay
    • distance
    • duration
    • duration_in_traffic
    • state
    • eta
    • delay_min
    • duration_min
    • duration_in_traffic_min
    • error_message


    Set
    • update [burst-update-count] [burst-update-interval] - update readings manually


=end html =cut