############################################## # $Id: 72_Spritpreis.pm 0 2017-01-10 12:00:00Z pjakobs $ # v0.0: inital testing # v0.1: basic functionality for pre-configured Tankerkoenig IDs package main; use strict; use warnings; use Time::HiRes; use Time::HiRes qw(usleep nanosleep); use Time::HiRes qw(time); use JSON::XS; use URI::URL; use Data::Dumper; require "HttpUtils.pm"; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; ##################################### # # fhem skeleton functions # ##################################### sub Spritpreis_Initialize(@) { my ($hash) = @_; $hash->{DefFn} = 'Spritpreis_Define'; $hash->{UndefFn} = 'Spritpreis_Undef'; $hash->{ShutdownFn} = 'Spritpreis_Undef'; $hash->{SetFn} = 'Spritpreis_Set'; $hash->{GetFn} = 'Spritpreis_Get'; $hash->{AttrFn} = 'Spritpreis_Attr'; $hash->{NotifyFn} = 'Spritpreis_Notify'; $hash->{ReadFn} = 'Spritpreis_Read'; $hash->{AttrList} = "lat lon rad IDs type sortby apikey interval address priceformat:2dezCut,2dezRound,3dez"." $readingFnAttributes"; #$hash->{AttrList} = "IDs type interval"." $readingFnAttributes"; return undef; } sub Spritpreis_Define($$) { ##################################### # # how to define this # # for Tankerkönig: # define Spritpreis Tankerkoenig # # for Spritpreisrechner.at # define Spritpreis Spritpreisrechner # ##################################### my $apiKey; my ($hash, $def)=@_; my @parts=split("[ \t][ \t]*", $def); my $name=$parts[0]; if(defined $parts[2]){ if($parts[2] eq "Tankerkoenig"){ ## if(defined $parts[3]){ $apiKey=$parts[3]; }else{ Log3 ($hash, 2, "$hash->{NAME} Module $parts[1] requires a valid apikey"); return undef; } my $result; my $url="https://creativecommons.tankerkoenig.de/json/prices.php?ids=12121212-1212-1212-1212-121212121212&apikey=".$apiKey; my $param= { url => $url, timeout => 1, method => "GET", header => "User-Agent: fhem\r\nAccept: application/json", }; my ($err, $data)=HttpUtils_BlockingGet($param); if ($err){ Log3($hash,2,"$hash->{NAME}: Error verifying APIKey: $err"); return undef; }else{ eval { $result = JSON->new->utf8(1)->decode($data); }; if ($@) { Log3 ($hash, 4, "$hash->{NAME}: error decoding response $@"); } else { if ($result->{ok} ne "true" && $result->{ok} != 1){ Log3 ($hash, 2, "$hash->{name}: error: $result->{message}"); return undef; } } $hash->{helper}->{apiKey}=$apiKey; $hash->{helper}->{service}="Tankerkoenig"; } if(AttrVal($hash->{NAME}, "IDs","")){ # # if IDs attribute is defined, populate list of stations at startup # my $ret=Spritpreis_Tankerkoenig_populateStationsFromAttr($hash); } # # start initial update # Spritpreis_Tankerkoenig_updateAll($hash); } elsif($parts[2] eq "Spritpreisrechner"){ $hash->{helper}->{service}="Spritpreisrechner"; } }else{ Log3($hash,2,"$hash->{NAME} Module $parts[1] requires a provider specification. Currently either \"Tankerkoenig\" (for de) or \"Spritpreisrechner\" (for at)"); } return undef; } sub Spritpreis_Undef(@){ my ($hash,$name)=@_; RemoveInternalTimer($hash); return undef; } sub Spritpreis_Set(@) { my ($hash , $name, $cmd, @args) = @_; return "Unknown command $cmd, choose one of update add delete" if ($cmd eq '?'); Log3($hash, 3,"$hash->{NAME}: get $hash->{NAME} $cmd $args[0]"); if ($cmd eq "update"){ if(defined $args[0]){ if($args[0] eq "all"){ # removing the timer so we don't get a flurry of requests RemoveInternalTimer($hash); Spritpreis_Tankerkoenig_updateAll($hash); }elsif($args[0] eq "id"){ if(defined $args[1]){ Spritpreis_Tankerkoenig_updatePricesForIDs($hash, $args[1]); }else{ my $r="update id requires an id parameter!"; Log3($hash, 2,"$hash->{NAME} $r"); return $r; } } }else{ # # default behaviour if no ID or "all" is given is to update all existing IDs # Spritpreis_Tankerkoenig_updateAll($hash); } }elsif($cmd eq "add"){ if(defined $args[0]){ Log3($hash, 4,"$hash->{NAME} add: args[0]=$args[0]"); if($args[0] eq "id"){ # # add station by providing a single Tankerkoenig ID # if(defined($args[1])){ Spritpreis_Tankerkoenig_GetDetailsForID($hash, $args[1]); }else{ my $ret="add by id requires a station id"; return $ret; } } }else{ my $ret="add requires id or (some other method here soon)"; return $ret; } }elsif($cmd eq "delete"){ # # not sure how to "remove" readings through the fhem api # } return undef; } sub Spritpreis_Get(@) { my ($hash, $name, $cmd, @args) = @_; return "Unknown command $cmd, choose one of search test" if ($cmd eq '?'); Log3($hash, 3,"$hash->{NAME}: get $hash->{NAME} $cmd $args[0]"); if ($cmd eq "search"){ my $str=''; my $i=0; while($args[$i++]){ $str=$str." ".$args[$i]; } Log3($hash,4,"$hash->{NAME}: search string: $str"); my @loc=Spritpreis_GetCoordinatesForAddress($hash, $str); my ($lat, $lng, $str)=@loc; if($lat==0 && $lng==0){ return $str; }else{ if($hash->{helper}->{service} eq "Tankerkoenig"){ my $ret=Spritpreis_Tankerkoenig_GetStationIDsForLocation($hash, @loc); return $ret; } } }elsif($cmd eq "test"){ my $ret=Spritpreis_Tankerkoenig_populateStationsFromAttr($hash); return $ret; }else{ return undef; } #Spritpreis_Tankerkoenig_GetPricesForLocation($hash); #Spritpreis_GetCoordinatesForAddress($hash,"Hamburg, Elbphilharmonie"); # add price trigger here return undef; } sub Spritpreis_Attr(@) { my ($cmd, $device, $attrName, $attrVal)=@_; my $hash = $defs{$device}; if ($cmd eq 'set' and $attrName eq 'interval'){ Spritpreis_updateAll($hash); } return undef; } sub Spritpreis_Notify(@) { return undef; } sub Spritpreis_Read(@) { return undef; } ##################################### # # generalized functions # these functions will call the # specific functions for the defined # provider. # ##################################### sub Spritpreis_GetDetailsForID(){ } sub Spritpreis_updateAll(@){ my ($hash)=@_; if($hash->{helper}->{service} eq "Tankerkoenig"){ Spritpreis_Tankerkoenig_updateAll(); }elsif($hash->{helper}->{service} eq "Spritpreisrechner"){ } } ##################################### # # functions to create requests # ##################################### sub Spritpreis_Tankerkoenig_GetIDsForLocation(@){ my ($hash) = @_; my $lat=AttrVal($hash->{'NAME'}, "lat",0); my $lng=AttrVal($hash->{'NAME'}, "lon",0); my $rad=AttrVal($hash->{'NAME'}, "rad",5); my $type=AttrVal($hash->{'NAME'}, "type","diesel"); my $apiKey=$hash->{helper}->{apiKey}; Log3($hash,4,"$hash->{'NAME'}: apiKey: $apiKey"); if($apiKey eq "") { Log3($hash,3,"$hash->{'NAME'}: please provide a valid apikey, you can get it from https://creativecommons.tankerkoenig.de/#register. This function can't work without it"); my $r="err no APIKEY"; return $r; } my $url="https://creativecommons.tankerkoenig.de/json/list.php?lat=$lat&lng=$lng&rad=$rad&type=$type&apikey=$apiKey"; my $param = { url => $url, timeout => 2, hash => $hash, method => "GET", header => "User-Agent: fhem\r\nAccept: application/json", parser => \&Spritpreis_ParseIDsForLocation, callback => \&Spritpreis_callback }; HttpUtils_NonblockingGet($param); return undef; } #-------------------------------------------------- # sub # Spritpreis_Tankerkoenig_GetIDs(@){ # my ($hash) = @_; # Log3($hash, 4, "$hash->{NAME} called Spritpreis_Tankerkoenig_updatePricesForIDs"); # my $IDstring=AttrVal($hash->{NAME}, "IDs",""); # Log3($hash,4,"$hash->{NAME}: got ID String $IDstring"); # my @IDs=split(",", $IDstring); # my $i=1; # my $j=1; # my $IDList; # do { # $IDList=$IDs[0]; # # # # todo hier stimmt was mit den Indizes nicht! # # # do { # $IDList=$IDList.",".$IDs[$i]; # }while($j++ < 9 && defined($IDs[$i++])); # Spritpreis_Tankerkoenig_updatePricesForIDs($hash, $IDList); # Log3($hash, 4,"$hash->{NAME}: Set ending at $i IDList=$IDList"); # $j=1; # }while(defined($IDs[$i])); # return undef; # } #-------------------------------------------------- sub Spritpreis_Tankerkoenig_populateStationsFromAttr(@){ # # This walks through the IDs Attribute and adds the stations listed there to the station readings list, # initially getting full details # my ($hash) =@_; Log3($hash,4, "$hash->{NAME}: called Spritpreis_Tankerkoenig_populateStationsFromAttr "); my $IDstring=AttrVal($hash->{NAME}, "IDs",""); Log3($hash,4,"$hash->{NAME}: got ID String $IDstring"); my @IDs=split(",", $IDstring); my $i; do{ Spritpreis_Tankerkoenig_GetDetailsForID($hash, $IDs[$i]); }while(defined($IDs[$i++])); } sub Spritpreis_Tankerkoenig_updateAll(@){ # # this walks through the list of ID Readings and updates the fuel prices for those stations # it does this in blocks of 10 as suggested by the Tankerkoenig API # my ($hash) = @_; Log3($hash,4, "$hash->{NAME}: called Spritpreis_Tankerkoenig_updateAll "); my $i=1; my $j=0; my $id; my $IDList; do { $IDList=ReadingsVal($hash->{NAME}, $j."_id", ""); while($j++<9 && ReadingsVal($hash->{NAME}, $i."_id", "") ne "" ){ Log3($hash, 5, "$hash->{NAME}: i: $i, j: $j, id: ".ReadingsVal($hash->{NAME}, $i."_id", "") ); $IDList=$IDList.",".ReadingsVal($hash->{NAME}, $i."_id", ""); $i++; } if($IDList ne ""){ Spritpreis_Tankerkoenig_updatePricesForIDs($hash, $IDList); Log3($hash, 4,"$hash->{NAME}(update all): Set ending at $i IDList=$IDList"); } $j=1; }while(ReadingsVal($hash->{NAME}, $i."_id", "") ne "" ); Log3($hash, 4, "$hash->{NAME}: updateAll set timer for ".(gettimeofday()+AttrVal($hash->{NAME},"interval",15)*60)." delay ".(AttrVal($hash->{NAME},"interval", 15)*60)); InternalTimer(gettimeofday()+AttrVal($hash->{NAME}, "interval",15)*60, "Spritpreis_Tankerkoenig_updateAll",$hash); return undef; } sub Spritpreis_Tankerkoenig_GetDetailsForID(@){ # # This queries the Tankerkoenig API for the details for a specific ID # It does not verify the provided ID # The parser function is responsible for handling the response # my ($hash, $id)=@_; my $apiKey=$hash->{helper}->{apiKey}; if($apiKey eq "") { Log3($hash,3,"$hash->{'NAME'}: please provide a valid apikey, you can get it from https://creativecommons.tankerkoenig.de/#register. This function can't work without it"); my $r="err no APIKEY"; return $r; } my $url="https://creativecommons.tankerkoenig.de/json/detail.php?id=".$id."&apikey=$apiKey"; Log3($hash, 4,"$hash->{NAME}: called $url"); my $param={ url => $url, hash => $hash, timeout => 10, method => "GET", header => "User-Agent: fhem\r\nAccept: application/json", parser => \&Spritpreis_Tankerkoenig_ParseDetailsForID, callback=> \&Spritpreis_callback }; HttpUtils_NonblockingGet($param); return undef; } sub Spritpreis_Tankerkoenig_updatePricesForIDs(@){ # # This queries the Tankerkoenig API for an update on all prices. It takes a list of up to 10 IDs. # It will not verify the validity of those IDs nor will it check that the number is 10 or less # The parser function is responsible for handling the response # my ($hash, $IDList) = @_; my $apiKey=$hash->{helper}->{apiKey}; my $url="https://creativecommons.tankerkoenig.de/json/prices.php?ids=".$IDList."&apikey=$apiKey"; Log3($hash, 4,"$hash->{NAME}: called $url"); my $param={ url => $url, hash => $hash, timeout => 10, method => "GET", header => "User-Agent: fhem\r\nAccept: application/json", parser => \&Spritpreis_Tankerkoenig_ParsePricesForIDs, callback=> \&Spritpreis_callback }; HttpUtils_NonblockingGet($param); return undef; } sub Spritpreis_Spritpreisrechner_updatePricesForLocation(@){ # # for the Austrian Spritpreisrechner, there's not concept of IDs. The only method # is to query for prices by location which will make it difficult to follow the # price trend at any specific station. # my ($hash)=@_; my $url="http://www.spritpreisrechner.at/espritmap-app/GasStationServlet"; my $lat=AttrVal($hash->{'NAME'}, "lat",0); my $lng=AttrVal($hash->{'NAME'}, "lon",0); my $param={ url => $url, timeout => 1, method => "POST", header => "User-Agent: fhem\r\nAccept: application/json", data => { "", "DIE", "15.409674251128", "47.051201316374", "15.489496791403", "47.074588294516" } }; my ($err,$data)=HttpUtils_BlockingGet($param); Log3($hash,5,"$hash->{'NAME'}: Dumper($data)"); return undef; } sub Spritpreis_Tankerkoenig_GetStationIDsForLocation(@){ # # This is currently not being used. The idea is to provide a lat/long location and a radius and have # the stations within this radius are presented as a list and, upon selecting them, will be added # to the readings list # my ($hash,@location) = @_; # my $lat=AttrVal($hash->{'NAME'}, "lat",0); # my $lng=AttrVal($hash->{'NAME'}, "lon",0); my $rad=AttrVal($hash->{'NAME'}, "rad",5); my $type=AttrVal($hash->{'NAME'}, "type","all"); # my $sort=AttrVal($hash->{'NAME'}, "sortby","price"); my $apiKey=$hash->{helper}->{apiKey}; my ($lat, $lng, $formattedAddress)=@location; my $result; if($apiKey eq "") { Log3($hash,3,"$hash->{'NAME'}: please provide a valid apikey, you can get it from https://creativecommons.tankerkoenig.de/#register. This function can't work without it"); my $r="err no APIKEY"; return $r; } my $url="https://creativecommons.tankerkoenig.de/json/list.php?lat=$lat&lng=$lng&rad=$rad&type=$type&apikey=$apiKey"; Log3($hash, 4,"$hash->{NAME}: sending request with url $url"); my $param= { url => $url, hash => $hash, timeout => 1, method => "GET", header => "User-Agent: fhem\r\nAccept: application/json", }; my ($err, $data) = HttpUtils_BlockingGet($param); if($err){ Log3($hash, 4, "$hash->{NAME}: error fetching nformation"); } elsif($data){ Log3($hash, 4, "$hash->{NAME}: got data"); Log3($hash, 5, "$hash->{NAME}: got data $data\n\n\n"); eval { $result = JSON->new->utf8(1)->decode($data); }; if ($@) { Log3 ($hash, 4, "$hash->{NAME}: error decoding response $@"); } else { my @headerHost = grep /Host/, @FW_httpheader; $headerHost[0] =~ s/Host: //g; my ($stations) = $result->{stations}; my $ret="

Stations for Address

$formattedAddress

"; foreach (@{$stations}){ (my $station)=$_; Log3($hash, 2, "Name: $station->{name}, id: $station->{id}"); $ret=$ret . ""; } $ret=$ret . "
NameOrtStraße
{NAME} . "+add+id+" . $station->{id} . ">"; $ret=$ret . $station->{name} . "" . $station->{place} . "" . $station->{street} . " " . $station->{houseNumber} . "
"; Log3($hash,2,"$hash->{NAME}: ############# ret: $ret"); return $ret; } }else { Log3 ($hash, 4, "$hash->{NAME}: something's very odd"); } return $data; # InternalTimer(gettimeofday()+AttrVal($hash->{NAME}, "interval",15)*60, "Spritpreis_Tankerkoenig_GetPricesForLocation",$hash); return undef; } ##################################### # # functions to handle responses # ##################################### sub Spritpreis_callback(@) { # # the generalized callback function. This should check all the general API errors and # handle them centrally, leaving the parser functions to handle response specific errors # my ($param, $err, $data) = @_; my ($hash) = $param->{hash}; # TODO generic error handling #Log3($hash, 5, "$hash->{NAME}: received callback with $data"); # do the result-parser callback if ($err){ Log3($hash, 4, "$hash->{NAME}: error fetching information: $err"); return undef; } my $parser = $param->{parser}; #Log3($hash, 4, "$hash->{NAME}: calling parser $parser with err $err and data $data"); &$parser($hash, $err, $data); if( $err || $err ne ""){ Log3 ($hash, 3, "$hash->{NAME} Readings NOT updated, received Error: ".$err); } return undef; } sub Spritpreis_ParseIDsForLocation(@){ return undef; } sub Spritpreis_Tankerkoenig_ParseDetailsForID(@){ # # this parses the response generated by the query Spritpreis_Tankerkoenig_GetDetailsForID # The response will contain the ID for a single station being, so no need to go through # multiple parts here. It will work whether or not that ID is currently already in the list # of readings. If it is, the details will be updated, if it is not, the new station will be # added at the end of the list # my ($hash, $err, $data)=@_; my $result; if($data){ Log3($hash, 4, "$hash->{NAME}: got StationDetail reply"); Log3($hash, 5, "$hash->{NAME}: got data $data\n\n\n"); eval { $result = JSON->new->utf8(1)->decode($data); }; if ($@) { Log3 ($hash, 4, "$hash->{NAME}: error decoding response $@"); } else { my $i=0; my $station = $result->{station}; while(ReadingsVal($hash->{NAME},$i."_id",$station->{id}) ne $station->{id}) { # # this loop iterates through the readings until either an id is equal to the current # response $station->{id} or, if no id is, it will come up with the default which is set # to $station->{id}, thus it will be added # $i++; } readingsBeginUpdate($hash); readingsBulkUpdate($hash,$i."_name",$station->{name}); my @types=("e5", "e10", "diesel"); foreach my $type (@types){ Log3($hash,4,"$hash->{NAME}: checking type $type"); if(defined($station->{$type})){ if(AttrVal($hash->{NAME}, "priceformat","") eq "2dezCut"){ chop($station->{$type}); }elsif(AttrVal($hash->{NAME}, "priceformat","") eq "2dezRound"){ $station->{$type}=sprintf("%.2f", $station->{$type}); } if(ReadingsVal($hash->{NAME}, $i."_".$type."_trend",0)!=0){ my $p=ReadingsVal($hash->{NAME}, $i."_".$type."_price",0); Log3($hash,4,"$hash->{NAME}:parseDetailsForID $type price old: $p"); if($p>$station->{$type}){ readingsBulkUpdate($hash,$i."_".$type."_trend","fällt"); Log3($hash,4,"$hash->{NAME}:parseDetailsForID trend: fällt"); }elsif($p < $station->{$type}){ readingsBulkUpdate($hash,$i."_".$type."_trend","steigt"); Log3($hash,4,"$hash->{NAME}:parseDetailsForID trend: konstant"); }else{ } readingsBulkUpdate($hash,$i."_".$type."_price",$station->{$type}) } } } readingsBulkUpdate($hash,$i."_place",$station->{place}); readingsBulkUpdate($hash,$i."_street",$station->{street}." ".$station->{houseNumber}); readingsBulkUpdate($hash,$i."_distance",$station->{dist}); readingsBulkUpdate($hash,$i."_brand",$station->{brand}); readingsBulkUpdate($hash,$i."_lat",$station->{lat}); readingsBulkUpdate($hash,$i."_lon",$station->{lng}); readingsBulkUpdate($hash,$i."_id",$station->{id}); readingsBulkUpdate($hash,$i."_isOpen",$station->{isOpen}); readingsEndUpdate($hash,1); } } } sub Spritpreis_Tankerkoenig_ParsePricesForIDs(@){ # # This parses the response to Spritpreis_Tankerkoenig_updatePricesForIDs # this response contains price updates for the requested stations listed by ID # since we don't keep a context between the API request and the response, # in order to update the correct readings, this routine has to go through the # readings list and make sure it does find matching IDs. It will not add new # stations to the list # my ($hash, $err, $data)=@_; my $result; if($err){ Log3($hash, 4, "$hash->{NAME}: error fetching information"); } elsif($data){ Log3($hash, 4, "$hash->{NAME}: got PricesForLocation reply"); Log3($hash, 5, "$hash->{NAME}: got data $data\n\n\n"); eval { $result = JSON->new->utf8(1)->decode($data); }; if ($@) { Log3 ($hash, 4, "$hash->{NAME}: error decoding response $@"); } else { my ($stations) = $result->{prices}; Log3($hash, 5, "$hash->{NAME}: stations:".Dumper($stations)); # # the return value is keyed by stations, therefore, I'll have # to fetch the stations from the existing readings and run # through it along those ids. # my $i=0; while(ReadingsVal($hash->{NAME}, $i."_id", "") ne "" ){ my $id=ReadingsVal($hash->{NAME}, $i."_id", ""); Log3($hash, 4, "$hash->{NAME}: checking ID $id"); if(defined($stations->{$id})){ Log3($hash, 4, "$hash->{NAME}: updating readings-set $i (ID $id)" ); Log3($hash, 5, "$hash->{NAME} Update set:\nprice: $stations->{$id}->{price}\ne5: $stations->{$id}->{e5}\ne10: $stations->{$id}->{e10}\ndiesel: $stations->{$id}->{diesel}\n"); readingsBeginUpdate($hash); my @types=("e5", "e10", "diesel"); foreach my $type (@types){ Log3($hash, 4, "$hash->{NAME} ParsePricesForIDs checking type $type"); if(defined($stations->{$id}->{$type})){ if(AttrVal($hash->{NAME}, "priceformat","") eq "2dezCut"){ chop($stations->{$id}->{$type}); }elsif(AttrVal($hash->{NAME}, "priceformat","") eq "2dezRound"){ $stations->{$id}->{$type}=sprintf("%.2f", $stations->{$id}->{$type}); } Log3($hash, 4, "$hash->{NAME} ParsePricesForIDs updating type $type"); #if(ReadingsVal($hash->{NAME}, $i."_".$type."_trend","") ne ""){ my $p=ReadingsVal($hash->{NAME}, $i."_".$type."_price",0); Log3($hash,4,"$hash->{NAME}:parseDetailsForID $type price old: $p"); if($p>$stations->{$id}->{$type}){ readingsBulkUpdate($hash,$i."_".$type."_trend","fällt"); }elsif($p < $stations->{$id}->{$type}){ readingsBulkUpdate($hash,$i."_".$type."_trend","steigt"); }else{ } #} readingsBulkUpdate($hash,$i."_".$type."_price",$stations->{$id}->{$type}) } } readingsBulkUpdate($hash,$i."_isOpen",$stations->{$id}->{status}); readingsEndUpdate($hash, 1); } $i++; } } } return undef; } sub Spritpreis_Tankerkoening_ParseStationIDsForLocation(@){ my ($hash, $err, $data)=@_; my $result; Log3($hash,5,"$hash->{NAME}: ParsePricesForLocation has been called with err $err and data $data"); if($err){ Log3($hash, 4, "$hash->{NAME}: error fetching information"); } elsif($data){ Log3($hash, 4, "$hash->{NAME}: got PricesForLocation reply"); Log3($hash, 5, "$hash->{NAME}: got data $data\n\n\n"); eval { $result = JSON->new->utf8(1)->decode($data); }; if ($@) { Log3 ($hash, 4, "$hash->{NAME}: error decoding response $@"); } else { my ($stations) = $result->{stations}; #Log3($hash, 5, "$hash->{NAME}: stations:".Dumper($stations)); my $ret="
{NAME}." station method='get'>