# $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 netatmo device\ define netatmo userid publickey\ define netatmo PUBLIC device latitude longitude [radius]\ define 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

netatmo

    A Fhem module for netatmo weatherstations.

    Notes:
    • JSON has to be installed on the FHEM host.

    Define
      define <name> netatmo <device>
      define <name> netatmo [ACCOUNT] <username> <password> <client_id> <client_secret>

      Defines a netatmo device.

      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).
      Examples:
        define netatmo netatmo ACCOUNT abc@test.com myPassword 2134123412399119d4123134 AkqcOIHqrasfdaLKcYgZasd987123asd
        define netatmo netatmo 2f:13:2b:93:12:31
        define netatmo netatmo MODULE 2f:13:2b:93:12:31 f1:32:b9:31:23:11

    Readings

    Set
    • autocreate
      Create fhem devices for all netatmo devices.

    Get
    • devices
      list the netatmo devices for this account
    • update
      trigger an update
    • public [<station>] [<address>] <args>
      no arguments -> get all public stations in a radius of 0.025° around global fhem longitude/latitude
      <rad> -> get all public stations in a radius of <rad>° around global fhem longitude/latitude
      <lon> <lat> [<rad>] -> get all public stations in a radius of 0.025° or <rad>° around <lon>/<lat>
      <lon_ne> <lat_ne> <lon_sw> <lat_sw> -> get all public stations in the area of <lon_ne> <lat_ne> <lon_sw> <lat_sw>
      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 +.
      if <station> is given then print fhem define for this station

    Attributes
    • interval
      the interval in seconds used to check for new values.
    • disable
      1 -> stop polling
    • nossl
      1 -> don't use ssl.

=end html =cut