mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-02-08 05:15:10 +00:00
1152 lines
32 KiB
Perl
1152 lines
32 KiB
Perl
|
|
# $Id$
|
|
|
|
package main;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Encode qw(encode_utf8);
|
|
use JSON;
|
|
|
|
use HttpUtils;
|
|
|
|
my $netatmo_isFritzBox = undef;
|
|
sub
|
|
netatmo_isFritzBox()
|
|
{
|
|
$netatmo_isFritzBox = int( qx( [ -f /usr/bin/ctlmgr_ctl ] && echo 1 || echo 0 ) ) if( !defined( $netatmo_isFritzBox) );
|
|
|
|
return $netatmo_isFritzBox;
|
|
}
|
|
|
|
sub
|
|
netatmo_Initialize($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
$hash->{DefFn} = "netatmo_Define";
|
|
$hash->{NOTIFYDEV} = "global";
|
|
$hash->{NotifyFn} = "netatmo_Notify";
|
|
$hash->{UndefFn} = "netatmo_Undefine";
|
|
$hash->{SetFn} = "netatmo_Set";
|
|
$hash->{GetFn} = "netatmo_Get";
|
|
$hash->{AttrFn} = "netatmo_Attr";
|
|
$hash->{AttrList} = "IODev ".
|
|
"disable:1 ".
|
|
"interval ".
|
|
"nossl:1 ";
|
|
$hash->{AttrList} .= $readingFnAttributes;
|
|
}
|
|
|
|
#####################################
|
|
|
|
sub
|
|
netatmo_Define($$)
|
|
{
|
|
my ($hash, $def) = @_;
|
|
|
|
my @a = split("[ \t][ \t]*", $def);
|
|
|
|
my $name = $a[0];
|
|
|
|
my $subtype;
|
|
if( @a == 3 ) {
|
|
$subtype = "DEVICE";
|
|
|
|
my $device = $a[2];
|
|
|
|
$hash->{Device} = $device;
|
|
|
|
$hash->{INTERVAL} = 60*5 if( !$hash->{INTERVAL} );
|
|
|
|
my $d = $modules{$hash->{TYPE}}{defptr}{"D$device"};
|
|
return "device $device already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name );
|
|
|
|
$modules{$hash->{TYPE}}{defptr}{"D$device"} = $hash;
|
|
|
|
} elsif( ($a[2] eq "PUBLIC" && @a > 3 ) ) {
|
|
my $device = $a[3];
|
|
$hash->{Device} = $device;
|
|
|
|
if( $a[4] && $a[4] =~ m/[\da-f]{2}(:[\da-f]{2}){5}/ ) {
|
|
$subtype = "MODULE";
|
|
|
|
my $module = "";
|
|
my $readings = "";
|
|
|
|
my @a = splice( @a, 4 );
|
|
while( @a ) {
|
|
$module .= " " if( $module );
|
|
$module .= shift( @a );
|
|
|
|
$readings .= " " if( $readings );
|
|
$readings .= shift( @a );
|
|
}
|
|
|
|
$hash->{Module} = $module;
|
|
$hash->{dataTypes} = $readings;
|
|
|
|
my $d = $modules{$hash->{TYPE}}{defptr}{"M$module"};
|
|
return "module $module already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name );
|
|
|
|
$modules{$hash->{TYPE}}{defptr}{"M$module"} = $hash;
|
|
|
|
my $state_format;
|
|
if( $readings =~ m/temperature/ ) {
|
|
$state_format .= " " if( $state_format );
|
|
$state_format .= "T: temperature";
|
|
}
|
|
if( $readings =~ m/humidity/ ) {
|
|
$state_format .= " " if( $state_format );
|
|
$state_format .= "H: humidity";
|
|
}
|
|
$attr{$name}{stateFormat} = $state_format if( !defined($attr{$name}{stateFormat}) && defined($state_format) );
|
|
|
|
} else {
|
|
my $lon = $a[4];
|
|
my $lat = $a[5];
|
|
my $rad = $a[6];
|
|
$rad = 0.02 if( !$rad );
|
|
|
|
$hash->{Lat} = $lat;
|
|
$hash->{Lon} = $lon;
|
|
$hash->{Rad} = $rad;
|
|
|
|
delete( $hash->{LAST_POLL} );
|
|
|
|
$subtype = "DEVICE";
|
|
|
|
my $d = $modules{$hash->{TYPE}}{defptr}{"D$device"};
|
|
return "device $device already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name );
|
|
|
|
$modules{$hash->{TYPE}}{defptr}{"D$device"} = $hash;
|
|
}
|
|
|
|
$hash->{INTERVAL} = 60*15 if( !$hash->{INTERVAL} );
|
|
|
|
} elsif( ($a[2] eq "MODULE" && @a == 5 ) ) {
|
|
$subtype = "MODULE";
|
|
|
|
my $device = $a[@a-2];
|
|
my $module = $a[@a-1];
|
|
|
|
$hash->{Device} = $device;
|
|
$hash->{Module} = $module;
|
|
|
|
$hash->{INTERVAL} = 60*5 if( !$hash->{INTERVAL} );
|
|
|
|
my $d = $modules{$hash->{TYPE}}{defptr}{"M$module"};
|
|
return "module $module already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name );
|
|
|
|
$modules{$hash->{TYPE}}{defptr}{"M$module"} = $hash;
|
|
|
|
} elsif( @a == 6 || ($a[2] eq "ACCOUNT" && @a == 7 ) ) {
|
|
$subtype = "ACCOUNT";
|
|
|
|
my $username = $a[@a-4];
|
|
my $password = $a[@a-3];
|
|
my $client_id = $a[@a-2];
|
|
my $client_secret = $a[@a-1];
|
|
|
|
$hash->{Clients} = ":netatmo:";
|
|
|
|
$hash->{username} = $username;
|
|
$hash->{password} = $password;
|
|
$hash->{client_id} = $client_id;
|
|
$hash->{client_secret} = $client_secret;
|
|
|
|
$attr{$name}{nossl} = 1 if( !$init_done && netatmo_isFritzBox() );
|
|
|
|
} else {
|
|
return "Usage: define <name> netatmo device\
|
|
define <name> netatmo userid publickey\
|
|
define <name> netatmo PUBLIC device latitude longitude [radius]\
|
|
define <name> netatmo [ACCOUNT] username password" if(@a < 3 || @a > 5);
|
|
}
|
|
|
|
$hash->{NAME} = $name;
|
|
$hash->{SUBTYPE} = $subtype;
|
|
|
|
$hash->{STATE} = "Initialized";
|
|
|
|
if( $init_done ) {
|
|
netatmo_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" );
|
|
netatmo_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" );
|
|
netatmo_initDevice($hash) if( $hash->{SUBTYPE} eq "MODULE" );
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub
|
|
netatmo_Notify($$)
|
|
{
|
|
my ($hash,$dev) = @_;
|
|
|
|
return if($dev->{NAME} ne "global");
|
|
return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
|
|
|
|
netatmo_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" );
|
|
netatmo_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" );
|
|
netatmo_initDevice($hash) if( $hash->{SUBTYPE} eq "MODULE" );
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub
|
|
netatmo_Undefine($$)
|
|
{
|
|
my ($hash, $arg) = @_;
|
|
|
|
delete( $modules{$hash->{TYPE}}{defptr}{"D$hash->{Device}"} ) if( $hash->{SUBTYPE} eq "DEVICE" );
|
|
delete( $modules{$hash->{TYPE}}{defptr}{"M$hash->{Module}"} ) if( $hash->{SUBTYPE} eq "MODULE" );
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub
|
|
netatmo_Set($$@)
|
|
{
|
|
my ($hash, $name, $cmd) = @_;
|
|
|
|
my $list = "autocreate:noArg";
|
|
|
|
if( $cmd eq "autocreate" ) {
|
|
return netatmo_autocreate($hash, 1 );
|
|
return undef;
|
|
}
|
|
|
|
return "Unknown argument $cmd, choose one of $list";
|
|
}
|
|
|
|
sub
|
|
netatmo_getToken($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
$hash->{https} = "https";
|
|
$hash->{https} = "http" if( AttrVal($hash->{NAME}, "nossl", 0) );
|
|
|
|
my($err,$data) = HttpUtils_BlockingGet({
|
|
url => "$hash->{https}://api.netatmo.net/oauth2/token",
|
|
timeout => 10,
|
|
noshutdown => 1,
|
|
data => {grant_type => 'password', client_id => $hash->{client_id}, client_secret=> $hash->{client_secret}, username => $hash->{username}, password => $hash->{password}},
|
|
});
|
|
|
|
netatmo_dispatch( {hash=>$hash,type=>'token'},$err,$data );
|
|
}
|
|
|
|
sub
|
|
netatmo_refreshToken($;$)
|
|
{
|
|
my ($hash,$nonblocking) = @_;
|
|
|
|
if( !$hash->{access_token} ) {
|
|
netatmo_getToken($hash);
|
|
return undef;
|
|
} elsif( !$nonblocking && defined($hash->{expires_at}) ) {
|
|
my ($seconds) = gettimeofday();
|
|
return undef if( $seconds < $hash->{expires_at} - 300 );
|
|
}
|
|
|
|
if( $nonblocking ) {
|
|
HttpUtils_NonblockingGet({
|
|
url => "$hash->{https}://api.netatmo.net/oauth2/token",
|
|
timeout => 10,
|
|
noshutdown => 1,
|
|
data => {grant_type => 'refresh_token', client_id => $hash->{client_id}, client_secret=> $hash->{client_secret}, refresh_token => $hash->{refresh_token}},
|
|
hash => $hash,
|
|
type => 'token',
|
|
callback => \&netatmo_dispatch,
|
|
});
|
|
} else {
|
|
my($err,$data) = HttpUtils_BlockingGet({
|
|
url => "$hash->{https}://api.netatmo.net/oauth2/token",
|
|
timeout => 10,
|
|
noshutdown => 1,
|
|
data => {grant_type => 'refresh_token', client_id => $hash->{client_id}, client_secret=> $hash->{client_secret}, refresh_token => $hash->{refresh_token}},
|
|
});
|
|
|
|
netatmo_dispatch( {hash=>$hash,type=>'token'},$err,$data );
|
|
}
|
|
}
|
|
sub
|
|
netatmo_refreshTokenTimer($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
Log3 $name, 4, "$name: refreshing token";
|
|
|
|
netatmo_refreshToken($hash, 1);
|
|
}
|
|
|
|
sub
|
|
netatmo_connect($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
netatmo_getToken($hash);
|
|
}
|
|
sub
|
|
netatmo_initDevice($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
AssignIoPort($hash);
|
|
if(defined($hash->{IODev}->{NAME})) {
|
|
Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME};
|
|
} else {
|
|
Log3 $name, 1, "$name: no I/O device";
|
|
}
|
|
|
|
my $device;
|
|
if( $hash->{Module} ) {
|
|
$device = netatmo_getDeviceDetail( $hash, $hash->{Module} );
|
|
} else {
|
|
$device = netatmo_getDeviceDetail( $hash, $hash->{Device} );
|
|
}
|
|
|
|
$hash->{stationName} = $device->{station_name} if( $device->{station_name} );
|
|
|
|
$hash->{model} = $device->{type};
|
|
$hash->{firmware} = $device->{firmware};
|
|
if( $device->{place} ) {
|
|
$hash->{country} = $device->{place}{country};
|
|
$hash->{bssid} = $device->{place}{bssid};
|
|
$hash->{altitude} = $device->{place}{altitude};
|
|
$hash->{city} = $device->{place}{geoip_city};
|
|
$hash->{location} = $device->{place}{location}[0] .",". $device->{place}{location}[1];
|
|
}
|
|
|
|
my $state_format;
|
|
if( $device->{data_type} ) {
|
|
delete($hash->{dataTypes});
|
|
|
|
my @reading_names = ();
|
|
foreach my $type (@{$device->{data_type}}) {
|
|
$hash->{dataTypes} = "" if ( !defined($hash->{dataTypes}) );
|
|
$hash->{dataTypes} .= "," if ( $hash->{dataTypes} );
|
|
$hash->{dataTypes} .= $type;
|
|
|
|
push @reading_names, lc($type);
|
|
|
|
if( $type eq "Temperature" ) {
|
|
$state_format .= " " if( $state_format );
|
|
$state_format .= "T: temperature";
|
|
} elsif( $type eq "Humidity" ) {
|
|
$state_format .= " " if( $state_format );
|
|
$state_format .= "H: humidity";
|
|
}
|
|
}
|
|
|
|
$hash->{helper}{readingNames} = \@reading_names;
|
|
}
|
|
|
|
$attr{$name}{stateFormat} = $state_format if( !defined($attr{$name}{stateFormat}) && defined($state_format) );
|
|
|
|
netatmo_poll($hash);
|
|
}
|
|
|
|
sub
|
|
netatmo_getDevices($;$)
|
|
{
|
|
my ($hash,$blocking) = @_;
|
|
|
|
netatmo_refreshToken($hash);
|
|
|
|
if( $blocking ) {
|
|
my($err,$data) = HttpUtils_BlockingGet({
|
|
url => "$hash->{https}://api.netatmo.net/api/devicelist",
|
|
noshutdown => 1,
|
|
data => { access_token => $hash->{access_token}, scope => 'read_station' },
|
|
});
|
|
|
|
netatmo_dispatch( {hash=>$hash,type=>'devicelist'},$err,$data );
|
|
|
|
return $hash->{helper}{devices};
|
|
} else {
|
|
HttpUtils_NonblockingGet({
|
|
url => "$hash->{https}://api.netatmo.net/api/devicelist",
|
|
noshutdown => 1,
|
|
data => { access_token => $hash->{access_token}, scope => 'read_station', },
|
|
hash => $hash,
|
|
type => 'devicelist',
|
|
callback => \&netatmo_dispatch,
|
|
});
|
|
}
|
|
}
|
|
sub
|
|
netatmo_getPublicDevices($$;$$$$)
|
|
{
|
|
my ($hash,$blocking,$lon_ne,$lat_ne,$lon_sw,$lat_sw) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $iohash = $hash->{IODev};
|
|
$iohash = $hash if( !defined($iohash) );
|
|
|
|
if( !defined($lat_ne) ) {
|
|
my $s = $lon_ne;
|
|
$s = 0.025 if ( !defined($s) );
|
|
my $lat = AttrVal("global","latitude", 50.112);
|
|
my $lon = AttrVal("global","longitude", 8.686);
|
|
|
|
$lon_ne = $lon + $s;
|
|
$lat_ne = $lat + $s;
|
|
$lon_sw = $lon - $s;
|
|
$lat_sw = $lat - $s;
|
|
} elsif( !defined($lat_sw) ) {
|
|
my $lat = $lat_ne;
|
|
my $lon = $lon_ne;
|
|
my $s = $lon_sw;
|
|
$s = 0.025 if ( !defined($s) );
|
|
|
|
$lon_ne = $lon + $s;
|
|
$lat_ne = $lat + $s;
|
|
$lon_sw = $lon - $s;
|
|
$lat_sw = $lat - $s;
|
|
}
|
|
Log3 $name, 4, "$name getpublicdata: $lon_ne, $lon_sw, $lat_ne, $lat_sw";
|
|
|
|
netatmo_refreshToken($iohash);
|
|
|
|
if( $blocking ) {
|
|
my($err,$data) = HttpUtils_BlockingGet({
|
|
url => "$iohash->{https}://api.netatmo.net/api/getpublicdata",
|
|
noshutdown => 1,
|
|
data => { access_token => $iohash->{access_token}, lat_ne => $lat_ne, lon_ne => $lon_ne, lat_sw => $lat_sw, lon_sw => $lon_sw },
|
|
});
|
|
|
|
return netatmo_dispatch( {hash=>$hash,type=>'publicdata'},$err,$data );
|
|
} else {
|
|
HttpUtils_NonblockingGet({
|
|
url => "$iohash->{https}://api.netatmo.net/api/getpublicdata",
|
|
noshutdown => 1,
|
|
data => { access_token => $iohash->{access_token}, lat_ne => $lat_ne, lon_ne => $lon_ne, lat_sw => $lat_sw, lon_sw => $lon_sw },
|
|
hash => $hash,
|
|
type => 'publicdata',
|
|
callback => \&netatmo_dispatch,
|
|
});
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_getAddress($$$$)
|
|
{
|
|
my ($hash,$blocking,$lon,$lat) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $iohash = $hash->{IODev};
|
|
$iohash = $hash if( !defined($iohash) );
|
|
|
|
if( $blocking ) {
|
|
my($err,$data) = HttpUtils_BlockingGet({
|
|
url => "$iohash->{https}://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lon",
|
|
noshutdown => 1,
|
|
});
|
|
|
|
return netatmo_dispatch( {hash=>$hash,type=>'address'},$err,$data );
|
|
} else {
|
|
HttpUtils_NonblockingGet({
|
|
url => "$iohash->{https}://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lon",
|
|
noshutdown => 1,
|
|
hash => $hash,
|
|
type => 'address',
|
|
callback => \&netatmo_dispatch,
|
|
});
|
|
}
|
|
}
|
|
sub
|
|
netatmo_getLatLong($$$)
|
|
{
|
|
my ($hash,$blocking,$addr) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $iohash = $hash->{IODev};
|
|
$iohash = $hash if( !defined($iohash) );
|
|
|
|
if( $blocking ) {
|
|
my($err,$data) = HttpUtils_BlockingGet({
|
|
url => "$iohash->{https}://maps.googleapis.com/maps/api/geocode/json?address=germany+$addr",
|
|
noshutdown => 1,
|
|
});
|
|
|
|
return netatmo_dispatch( {hash=>$hash,type=>'latlng'},$err,$data );
|
|
} else {
|
|
HttpUtils_NonblockingGet({
|
|
url => "$iohash->{https}://maps.googleapis.com/maps/api/geocode/json?address=germany+$addr",
|
|
noshutdown => 1,
|
|
hash => $hash,
|
|
type => 'latlng',
|
|
callback => \&netatmo_dispatch,
|
|
});
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_getDeviceDetail($$)
|
|
{
|
|
my ($hash,$id) = @_;
|
|
|
|
$hash = $hash->{IODev} if( defined($hash->{IODev}) );
|
|
|
|
netatmo_getDevices($hash,1) if( !$hash->{helper}{devices} );
|
|
|
|
foreach my $device (@{$hash->{helper}{devices}}) {
|
|
return $device if( $device->{_id} eq $id );
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
sub
|
|
netatmo_requestDeviceReadings($@)
|
|
{
|
|
my ($hash,$id,$type,$module) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
return undef if( !defined($hash->{IODev}) );
|
|
|
|
my $iohash = $hash->{IODev};
|
|
$type = $hash->{dataTypes} if( !$type );
|
|
$type = "Temperature,Co2,Humidity,Noise,Pressure" if( !$type );
|
|
|
|
netatmo_refreshToken( $iohash );
|
|
|
|
my %data = (access_token => $iohash->{access_token}, device_id => $id, scale => "max", type => $type);
|
|
$data{"module_id"} = $module if( $module );
|
|
|
|
my $lastupdate = ReadingsVal( $name, ".lastupdate", undef );
|
|
$data{"date_begin"} = $lastupdate if( defined($lastupdate) );
|
|
|
|
HttpUtils_NonblockingGet({
|
|
url => "$iohash->{https}://api.netatmo.net/api/getmeasure",
|
|
timeout => 10,
|
|
noshutdown => 1,
|
|
data => \%data,
|
|
hash => $hash,
|
|
type => 'getmeasure',
|
|
requested => $type,
|
|
callback => \&netatmo_dispatch,
|
|
});
|
|
}
|
|
|
|
sub
|
|
netatmo_poll($)
|
|
{
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
RemoveInternalTimer($hash);
|
|
|
|
if( $hash->{SUBTYPE} eq "DEVICE" ) {
|
|
netatmo_pollDevice($hash);
|
|
} elsif( $hash->{SUBTYPE} eq "MODULE" ) {
|
|
netatmo_pollDevice($hash);
|
|
}
|
|
|
|
if( defined($hash->{helper}{update_count}) && $hash->{helper}{update_count} > 1024 ) {
|
|
InternalTimer(gettimeofday()+2, "netatmo_poll", $hash, 0);
|
|
} else {
|
|
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "netatmo_poll", $hash, 0);
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_dispatch($$$)
|
|
{
|
|
my ($param, $err, $data) = @_;
|
|
my $hash = $param->{hash};
|
|
my $name = $hash->{NAME};
|
|
|
|
$hash->{openRequests} -= 1 if( $param->{type} eq 'getmeasure' );
|
|
|
|
if( $err ) {
|
|
Log3 $name, 2, "$name: http request failed: $err";
|
|
} elsif( $data ) {
|
|
Log3 $name, 4, "$name: $data";
|
|
|
|
$data =~ s/\n//g;
|
|
if( $data !~ m/^{.*}$/m ) {
|
|
Log3 $name, 2, "$name: invalid json detected: >>$data<<";
|
|
return undef;
|
|
}
|
|
|
|
my $json;
|
|
if( netatmo_isFritzBox() ) {
|
|
$json = decode_json($data);
|
|
} else {
|
|
$json = JSON->new->utf8(0)->decode($data);
|
|
}
|
|
|
|
if( $json->{error} ) {
|
|
#$hash->{lastError} = $json->{error}{message};
|
|
}
|
|
|
|
if( $param->{type} eq 'token' ) {
|
|
netatmo_parseToken($hash,$json);
|
|
} elsif( $param->{type} eq 'devicelist' ) {
|
|
netatmo_parseDeviceList($hash,$json);
|
|
} elsif( $param->{type} eq 'getmeasure' ) {
|
|
netatmo_parseReadings($hash,$json,$param->{requested});
|
|
} elsif( $param->{type} eq 'publicdata' ) {
|
|
return netatmo_parsePublic($hash,$json);
|
|
} elsif( $param->{type} eq 'address' ) {
|
|
return netatmo_parseAddress($hash,$json);
|
|
} elsif( $param->{type} eq 'latlng' ) {
|
|
return netatmo_parseLatLng($hash,$json);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_autocreate($;$)
|
|
{
|
|
my($hash,$force) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
if( !$hash->{helper}{devices} ) {
|
|
netatmo_getDevices($hash);
|
|
return undef if( !$force );
|
|
}
|
|
|
|
if( !$force ) {
|
|
foreach my $d (keys %defs) {
|
|
next if($defs{$d}{TYPE} ne "autocreate");
|
|
return undef if(AttrVal($defs{$d}{NAME},"disable",undef));
|
|
}
|
|
}
|
|
|
|
my $autocreated = 0;
|
|
|
|
my $devices = $hash->{helper}{devices};
|
|
foreach my $device (@{$devices}) {
|
|
if( defined($modules{$hash->{TYPE}}{defptr}{"D$device->{_id}"}) ) {
|
|
Log3 $name, 4, "$name: device '$device->{_id}' already defined";
|
|
next;
|
|
}
|
|
if( defined($modules{$hash->{TYPE}}{defptr}{"M$device->{_id}"}) ) {
|
|
Log3 $name, 4, "$name: module '$device->{_id}' already defined";
|
|
next;
|
|
}
|
|
|
|
my $id = $device->{_id};
|
|
my $devname = "netatmo_D". $id;
|
|
$devname =~ s/:/_/g;
|
|
my $define= "$devname netatmo $id";
|
|
if( $device->{main_device} ) {
|
|
$devname = "netatmo_M". $id;
|
|
$devname =~ s/:/_/g;
|
|
$define= "$devname netatmo MODULE $device->{main_device} $id";
|
|
}
|
|
|
|
Log3 $name, 3, "$name: create new device '$devname' for device '$id'";
|
|
my $cmdret= CommandDefine(undef,$define);
|
|
if($cmdret) {
|
|
Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret";
|
|
} else {
|
|
$cmdret= CommandAttr(undef,"$devname alias ".$device->{module_name}) if( defined($device->{module_name}) );
|
|
$cmdret= CommandAttr(undef,"$devname room netatmo");
|
|
$cmdret= CommandAttr(undef,"$devname IODev $name");
|
|
|
|
$autocreated++;
|
|
}
|
|
}
|
|
|
|
CommandSave(undef,undef) if( $autocreated && AttrVal( "autocreate", "autosave", 1 ) );
|
|
|
|
return "created $autocreated devices";
|
|
}
|
|
|
|
sub
|
|
netatmo_parseToken($$)
|
|
{
|
|
my($hash, $json) = @_;
|
|
|
|
RemoveInternalTimer($hash);
|
|
|
|
my $had_token = $hash->{access_token};
|
|
|
|
$hash->{access_token} = $json->{access_token};
|
|
$hash->{refresh_token} = $json->{refresh_token};
|
|
|
|
if( $hash->{access_token} ) {
|
|
$hash->{STATE} = "Connected";
|
|
|
|
($hash->{expires_at}) = gettimeofday();
|
|
$hash->{expires_at} += $json->{expires_in};
|
|
|
|
netatmo_getDevices($hash) if( !$had_token );
|
|
|
|
InternalTimer(gettimeofday()+$json->{expires_in}*3/4, "netatmo_refreshTokenTimer", $hash, 0);
|
|
} else {
|
|
$hash->{STATE} = "Error" if( !$hash->{access_token} );
|
|
InternalTimer(gettimeofday()+60, "netatmo_refreshTokenTimer", $hash, 0);
|
|
}
|
|
}
|
|
sub
|
|
netatmo_parseDeviceList($$)
|
|
{
|
|
my($hash, $json) = @_;
|
|
|
|
my $do_autocreate = 1;
|
|
$do_autocreate = 0 if( !defined($hash->{helper}{devices}) ); #autocreate
|
|
|
|
my @devices = ();
|
|
foreach my $device (@{$json->{body}{devices}}) {
|
|
push( @devices, $device );
|
|
}
|
|
foreach my $module (@{$json->{body}{modules}}) {
|
|
push( @devices, $module );
|
|
}
|
|
|
|
$hash->{helper}{devices} = \@devices;
|
|
|
|
netatmo_autocreate($hash) if( $do_autocreate );
|
|
}
|
|
|
|
sub
|
|
netatmo_updateReadings($$)
|
|
{
|
|
my($hash, $readings) = @_;
|
|
|
|
my ($seconds) = gettimeofday();
|
|
|
|
my $latest = 0;
|
|
if( $readings && @{$readings} ) {
|
|
readingsBeginUpdate($hash);
|
|
my $i = 0;
|
|
foreach my $reading (sort { $a->[0] <=> $b->[0] } @{$readings}) {
|
|
$hash->{".updateTimestamp"} = FmtDateTime($reading->[0]);
|
|
$hash->{CHANGETIME}[$i++] = FmtDateTime($reading->[0]);
|
|
readingsBulkUpdate( $hash, $reading->[1], $reading->[2] );
|
|
$latest = $reading->[0] if( $reading->[0] > $latest );
|
|
}
|
|
#$hash->{helper}{update_count} = int(@{$readings});
|
|
|
|
#$seconds = $latest + 1 if( $latest );
|
|
readingsBulkUpdate( $hash, ".lastupdate", $seconds, 0 );
|
|
|
|
readingsEndUpdate($hash,1);
|
|
|
|
delete $hash->{CHANGETIME};
|
|
}
|
|
|
|
return ($seconds,$latest);
|
|
}
|
|
sub
|
|
netatmo_parseReadings($$;$)
|
|
{
|
|
my($hash, $json, $requested) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $reading_names = $hash->{helper}{readingNames};
|
|
if( $requested ) {
|
|
my @readings = split( ',', $requested );
|
|
$reading_names = \@readings;
|
|
}
|
|
|
|
if( $json ) {
|
|
$hash->{status} = $json->{status};
|
|
$hash->{status} = $json->{error}{message} if( $json->{error} );
|
|
my $lastupdate = ReadingsVal( $name, ".lastupdate", 0 );
|
|
my @r = ();
|
|
my $readings = \@r;
|
|
$readings = $hash->{readings} if( defined($hash->{readings}) );
|
|
if( $hash->{status} eq "ok" ) {
|
|
foreach my $values ( @{$json->{body}}) {
|
|
my $time = $values->{beg_time};
|
|
my $step_time = $values->{step_time};
|
|
|
|
foreach my $value (@{$values->{value}}) {
|
|
my $i = 0;
|
|
foreach my $reading (@{$value}) {
|
|
next if( !defined($reading) );
|
|
|
|
#my $name = $hash->{helper}{readingNames}[$i];
|
|
my $name = lc($reading_names->[$i++]);
|
|
|
|
push(@{$readings}, [$time, $name, $reading]);
|
|
}
|
|
|
|
$time += $step_time if( $step_time );
|
|
}
|
|
}
|
|
|
|
if( $hash->{openRequests} ) {
|
|
$hash->{readings} = $readings;
|
|
} else {
|
|
my ($seconds,undef) = netatmo_updateReadings( $hash, $readings );
|
|
$hash->{LAST_POLL} = FmtDateTime( $seconds );
|
|
delete $hash->{readings};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_parsePublic($$)
|
|
{
|
|
my($hash, $json) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
if( $json ) {
|
|
$hash->{status} = $json->{status};
|
|
$hash->{status} = $json->{error}{message} if( $json->{error} );
|
|
if( $hash->{status} eq "ok" ) {
|
|
if( $hash->{Lat} ) {
|
|
my $found = 0;
|
|
my @readings = ();
|
|
my $devices = $json->{body};
|
|
if( ref($devices) eq "ARRAY" ) {
|
|
foreach my $device (@{$devices}) {
|
|
next if( $device->{_id} ne $hash->{Device} );
|
|
next if( ref($device->{measures}) ne "HASH" );
|
|
Log3 $name, 4, "$name found: $device->{_id}: $device->{place}->{location}->[0] $device->{place}->{location}->[1]";
|
|
foreach my $module ( keys %{$device->{measures}}) {
|
|
next if( ref($device->{measures}->{$module}->{res}) ne "HASH" );
|
|
foreach my $timestamp ( keys %{$device->{measures}->{$module}->{res}} ) {
|
|
next if( $hash->{LAST_POLL} && $timestamp <= $hash->{LAST_POLL} );
|
|
my $i = 0;
|
|
foreach my $value ( @{$device->{measures}->{$module}->{res}->{$timestamp}} ) {
|
|
my $type = $device->{measures}->{$module}->{type}[$i];
|
|
|
|
push(@readings, [$timestamp, lc($type), $value]);
|
|
|
|
++$i;
|
|
}
|
|
}
|
|
}
|
|
|
|
$found = 1;
|
|
last;
|
|
}
|
|
}
|
|
|
|
my (undef,$latest) = netatmo_updateReadings( $hash, \@readings );
|
|
$hash->{LAST_POLL} = $latest if( @readings );
|
|
|
|
$hash->{STATE} = "Error: device not found" if( !$found );
|
|
} else {
|
|
return $json->{body};
|
|
}
|
|
} else {
|
|
return $hash->{status};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_parseAddress($$)
|
|
{
|
|
my($hash, $json) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
if( $json ) {
|
|
$hash->{status} = $json->{status};
|
|
$hash->{status} = $json->{error}{message} if( $json->{error} );
|
|
if( $hash->{status} eq "OK" ) {
|
|
if( $json->{results} ) {
|
|
return $json->{results}->[0]->{formatted_address};
|
|
}
|
|
} else {
|
|
return $hash->{status};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_parseLatLng($$)
|
|
{
|
|
my($hash, $json) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
if( $json ) {
|
|
$hash->{status} = $json->{status};
|
|
$hash->{status} = $json->{error}{message} if( $json->{error} );
|
|
if( $hash->{status} eq "OK" ) {
|
|
if( $json->{results} ) {
|
|
return $json->{results}->[0]->{geometry}->{bounds};
|
|
}
|
|
} else {
|
|
return $hash->{status};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_pollDevice($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
$hash->{openRequests} = 0 if ( !defined( $hash->{openRequests}) );
|
|
|
|
if( $hash->{Module} ) {
|
|
my @types = split( ' ', $hash->{dataTypes} );
|
|
my $lastupdate = ReadingsVal( $hash->{NAME}, ".lastupdate", undef );
|
|
#$lastupdate = "2014-07-18 14:06:54";
|
|
$hash->{openRequests} += int(@types);
|
|
foreach my $module (split( ' ', $hash->{Module} ) ) {
|
|
my $type;
|
|
$type = shift(@types) if( $hash->{Module} =~ m/ / );
|
|
readingsSingleUpdate($hash, ".lastupdate", $lastupdate, 0);
|
|
netatmo_requestDeviceReadings( $hash, $hash->{Device}, $type, $module ne $hash->{Device}?$module:undef );
|
|
}
|
|
} elsif( defined($hash->{Lat}) ) {
|
|
$hash->{openRequests} += 1;
|
|
netatmo_getPublicDevices($hash, 0, $hash->{Lon}, $hash->{Lat}, $hash->{Rad} );
|
|
} else {
|
|
$hash->{openRequests} += 1;
|
|
netatmo_requestDeviceReadings( $hash, $hash->{Device} );
|
|
}
|
|
}
|
|
|
|
sub
|
|
netatmo_Get($$@)
|
|
{
|
|
my ($hash, $name, $cmd, @args) = @_;
|
|
|
|
my $list;
|
|
if( $hash->{SUBTYPE} eq "DEVICE"
|
|
|| $hash->{SUBTYPE} eq "MODULE" ) {
|
|
$list = "update:noArg updateAll:noArg";
|
|
|
|
if( $cmd eq "updateAll" ) {
|
|
$cmd = "update";
|
|
CommandDeleteReading( undef, "$name .*" );
|
|
}
|
|
|
|
if( $cmd eq "update" ) {
|
|
netatmo_poll($hash);
|
|
return undef;
|
|
}
|
|
} elsif( $hash->{SUBTYPE} eq "ACCOUNT" ) {
|
|
$list = "devices:noArg public";
|
|
|
|
if( $cmd eq "devices" ) {
|
|
my $devices = netatmo_getDevices($hash,1);
|
|
my $ret;
|
|
foreach my $device (@{$devices}) {
|
|
$ret .= "\n" if( $ret );
|
|
$ret .= "$device->{_id}\t$device->{module_name}\t$device->{hw_version}\t$device->{firmware}";
|
|
}
|
|
|
|
$ret = "id\t\t\tname\t\thw\tfw\n" . $ret if( $ret );
|
|
$ret = "no devices found" if( !$ret );
|
|
return $ret;
|
|
} elsif( $cmd eq "public" ) {
|
|
my $station;
|
|
$station = shift @args if( $args[0] && $args[0] =~ m/[\da-f]{2}(:[\da-f]{2}){5}/ );
|
|
|
|
if( $args[0] && ( $args[0] =~ m/^\d{5}$/
|
|
|| $args[0] =~ m/^a:/ ) ) {
|
|
my $addr = shift @args;
|
|
$addr = substr($addr,2) if( $addr =~ m/^a:/ );
|
|
|
|
my $bounds = netatmo_getLatLong( $hash,1,$addr );
|
|
$args[0] = $bounds->{northeast}->{lng};
|
|
$args[1] = $bounds->{northeast}->{lat};
|
|
$args[2] = $bounds->{southwest}->{lng};
|
|
$args[3] = $bounds->{southwest}->{lat};
|
|
}
|
|
|
|
my $devices = netatmo_getPublicDevices($hash, 1, $args[0], $args[1], $args[2], $args[3] );
|
|
my $ret;
|
|
|
|
if( ref($devices) eq "ARRAY" ) {
|
|
foreach my $device (@{$devices}) {
|
|
next if( $station && $station ne $device->{_id} );
|
|
$ret .= "\n" if( $ret );
|
|
$ret .= sprintf( "%s\t%.8f\t%.8f\t%i", $device->{_id},
|
|
$device->{place}->{location}->[0], $device->{place}->{location}->[1],
|
|
$device->{place}->{altitude} );
|
|
$ret .= "\t";
|
|
|
|
my $addr .= netatmo_getAddress( $hash, 1, $device->{place}->{location}->[0], $device->{place}->{location}->[1] );
|
|
next if( ref($device->{measures}) ne "HASH" );
|
|
|
|
my $ext;
|
|
foreach my $module ( sort keys %{$device->{measures}}) {
|
|
next if( ref($device->{measures}->{$module}->{res}) ne "HASH" );
|
|
|
|
$ext .= "$module ";
|
|
$ext .= join(',', @{$device->{measures}->{$module}->{type}});
|
|
$ext .= " ";
|
|
|
|
foreach my $timestamp ( keys %{$device->{measures}->{$module}->{res}} ) {
|
|
my $i = 0;
|
|
foreach my $value ( @{$device->{measures}->{$module}->{res}->{$timestamp}} ) {
|
|
my $type = $device->{measures}->{$module}->{type}[$i];
|
|
|
|
if( $type eq "temperature" ) {
|
|
$ret .= sprintf( "\t%.1f \xc2\xb0C", $value );
|
|
} elsif( $type eq "humidity" ) {
|
|
$ret .= sprintf( "\t%i %%", $value );
|
|
} elsif( $type eq "pressure" ) {
|
|
$ret .= sprintf( "\t%i hPa", $value );
|
|
} elsif( $type eq "rain" ) {
|
|
$ret .= sprintf( "\t%i mm", $value );
|
|
}
|
|
|
|
++$i;
|
|
}
|
|
last;
|
|
}
|
|
}
|
|
my $got_rain = 0;
|
|
foreach my $module ( keys %{$device->{measures}}) {
|
|
my $value = $device->{measures}->{$module}->{rain_60min};
|
|
if( defined($value) ) {
|
|
$got_rain = 1;
|
|
|
|
$ext .= "$module ";
|
|
$ext .= join(',', "rain");
|
|
$ext .= " ";
|
|
|
|
$ret .= sprintf( "\t%i mm", $value ) if( defined($value) );
|
|
}
|
|
}
|
|
$ret .= "\t" if( !$got_rain );
|
|
|
|
$ret .= "\t$addr";
|
|
|
|
$ret .= "\n\tdefine netatmo_P$device->{_id} netatmo PUBLIC $device->{_id} $ext" if( $station );
|
|
}
|
|
} else {
|
|
$ret = $devices if( !ref($devices) );
|
|
}
|
|
|
|
$ret = "id\t\t\tlongitude\tlatitude\taltitude\n" . $ret if( $ret );
|
|
$ret = "no devices found" if( !$ret );
|
|
return $ret;
|
|
}
|
|
}
|
|
|
|
return "Unknown argument $cmd, choose one of $list";
|
|
}
|
|
|
|
sub
|
|
netatmo_Attr($$$)
|
|
{
|
|
my ($cmd, $name, $attrName, $attrVal) = @_;
|
|
|
|
my $orig = $attrVal;
|
|
$attrVal = int($attrVal) if($attrName eq "interval");
|
|
$attrVal = 60*5 if($attrName eq "interval" && $attrVal < 60*5 && $attrVal != 0);
|
|
|
|
if( $attrName eq "interval" ) {
|
|
my $hash = $defs{$name};
|
|
$hash->{INTERVAL} = $attrVal;
|
|
$hash->{INTERVAL} = 60*5 if( !$hash->{INTERVAL} );
|
|
} elsif( $attrName eq "disable" ) {
|
|
my $hash = $defs{$name};
|
|
RemoveInternalTimer($hash);
|
|
if( $cmd eq "set" && $attrVal ne "0" ) {
|
|
} else {
|
|
$attr{$name}{$attrName} = 0;
|
|
netatmo_poll($hash);
|
|
}
|
|
} elsif( $attrName eq "nossl" ) {
|
|
my $hash = $defs{$name};
|
|
if( $hash->{SUBTYPE} eq "ACCOUNT" ) {
|
|
if( $cmd eq "set" && $attrVal ne "0" ) {
|
|
$hash->{https} = "http";
|
|
} else {
|
|
$hash->{https} = "https";
|
|
}
|
|
} else {
|
|
return $attrName ." not allowed for netatmo $hash->{SUBTYPE}";
|
|
}
|
|
}
|
|
|
|
if( $cmd eq "set" ) {
|
|
if( $orig ne $attrVal ) {
|
|
$attr{$name}{$attrName} = $attrVal;
|
|
return $attrName ." set to ". $attrVal;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
1;
|
|
|
|
=pod
|
|
=begin html
|
|
|
|
<a name="netatmo"></a>
|
|
<h3>netatmo</h3>
|
|
<ul>
|
|
A Fhem module for netatmo weatherstations.<br><br>
|
|
|
|
Notes:
|
|
<ul>
|
|
<li>JSON has to be installed on the FHEM host.</li>
|
|
</ul><br>
|
|
|
|
<a name="netatmo_Define"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<code>define <name> netatmo <device></code><br>
|
|
<code>define <name> netatmo [ACCOUNT] <username> <password> <client_id> <client_secret></code><br>
|
|
<br>
|
|
|
|
Defines a netatmo device.<br><br>
|
|
If a netatmo device of the account type is created all fhem devices for the netatmo devices are automaticaly created
|
|
(if autocreate is not disabled).
|
|
<br>
|
|
|
|
Examples:
|
|
<ul>
|
|
<code>define netatmo netatmo ACCOUNT abc@test.com myPassword 2134123412399119d4123134 AkqcOIHqrasfdaLKcYgZasd987123asd</code><br>
|
|
<code>define netatmo netatmo 2f:13:2b:93:12:31</code><br>
|
|
<code>define netatmo netatmo MODULE 2f:13:2b:93:12:31 f1:32:b9:31:23:11</code><br>
|
|
</ul>
|
|
</ul><br>
|
|
|
|
<a name="netatmo_Readings"></a>
|
|
<b>Readings</b>
|
|
<ul>
|
|
</ul><br>
|
|
|
|
<a name="netatmo_Set"></a>
|
|
<b>Set</b>
|
|
<ul>
|
|
<li>autocreate<br>
|
|
Create fhem devices for all netatmo devices.</li>
|
|
</ul><br>
|
|
|
|
<a name="netatmo_Get"></a>
|
|
<b>Get</b>
|
|
<ul>
|
|
<li>devices<br>
|
|
list the netatmo devices for this account</li>
|
|
<li>update<br>
|
|
trigger an update</li>
|
|
<li>public [<station>] [<address>] <args><br>
|
|
no arguments -> get all public stations in a radius of 0.025° around global fhem longitude/latitude<br>
|
|
<rad> -> get all public stations in a radius of <rad>° around global fhem longitude/latitude<br>
|
|
<lon> <lat> [<rad>] -> get all public stations in a radius of 0.025° or <rad>° around <lon>/<lat><br>
|
|
<lon_ne> <lat_ne> <lon_sw> <lat_sw> -> get all public stations in the area of <lon_ne> <lat_ne> <lon_sw> <lat_sw><br>
|
|
if <address> is given then list stations in the area of this address. can be given as 5 digit german postal code or a: followed by a textual address. all spaces have to be replaced by a +.<br>
|
|
if <station> is given then print fhem define for this station<br></li>
|
|
</ul><br>
|
|
|
|
<a name="netatmo_Attr"></a>
|
|
<b>Attributes</b>
|
|
<ul>
|
|
<li>interval<br>
|
|
the interval in seconds used to check for new values.</li>
|
|
<li>disable<br>
|
|
1 -> stop polling</li>
|
|
<li>nossl<br>
|
|
1 -> don't use ssl.</li><br>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html
|
|
=cut
|