From 68ecaf6fb5b3cf281d6986c56b8bedd65f5e3b3b Mon Sep 17 00:00:00 2001 From: jm1ke Date: Fri, 7 Oct 2016 19:34:28 +0000 Subject: [PATCH] 98_TRAFFIC: adding new module, version 1.0 git-svn-id: https://svn.fhem.de/fhem/trunk@12293 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/98_TRAFFIC.pm | 531 ++++++++++++++++++++++++++++++++++++++++ fhem/HISTORY | 3 + fhem/MAINTAINER.txt | 1 + 4 files changed, 536 insertions(+) create mode 100644 fhem/FHEM/98_TRAFFIC.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 838e74796..6c93ff254 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,6 +1,7 @@ # 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. + - new: 98_TRAFFIC: provide traffic details with Google Distance API - feature: 98_Hyperion: set configFile only available if at least two files are found while get configFiles minor code improvements diff --git a/fhem/FHEM/98_TRAFFIC.pm b/fhem/FHEM/98_TRAFFIC.pm new file mode 100644 index 000000000..de8980938 --- /dev/null +++ b/fhem/FHEM/98_TRAFFIC.pm @@ -0,0 +1,531 @@ +######################################################################### +# $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 + diff --git a/fhem/HISTORY b/fhem/HISTORY index 7a3257c2f..f8f5e54ea 100644 --- a/fhem/HISTORY +++ b/fhem/HISTORY @@ -694,3 +694,6 @@ - Sat Oct 1 2016 (Christian.Kühnel) - added new module 99_Venetian + +- Fri Oct 7 2016 (jmike) + - adding new module 98_TRAFFIC diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 34abd4044..cdb8582ad 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -347,6 +347,7 @@ FHEM/98_PID20.pm John http://forum.fhem.de Automatis FHEM/98_RandomTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste/Kalendermodule FHEM/98_SVG.pm rudolfkoenig http://forum.fhem.de Frontends/SVG/Plots/logProxy FHEM/98_THRESHOLD.pm damian-s http://forum.fhem.de Automatisierung +FHEM/98_TRAFFIC.pm jmike http://forum.fhem.de Unterstuetzende Dienste FHEM/98_UbiquitiPM.pm Wzut http://forum.fhem.de Sonstige Systeme FHEM/98_UbiquitiOut.pm Wzut http://forum.fhem.de Sonstige Systeme FHEM/98_WeekdayTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste