##############################################
# $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
";
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="