diff --git a/CHANGED b/CHANGED index 516bfb067..08cdcee54 100644 --- a/CHANGED +++ b/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - bugfix: 38_netatmo: fixed home settings for recording - feature: 93_DbLog: variable $DEVICE is available in attr DbLogValueFn to have readonly access to the source device name - feature: 77_SMAEM: use OBIS metrics (Thx to RiG), change Readings Lx_THD diff --git a/FHEM/38_netatmo.pm b/FHEM/38_netatmo.pm index 7bda94d39..6dfa4e23b 100644 --- a/FHEM/38_netatmo.pm +++ b/FHEM/38_netatmo.pm @@ -39,6 +39,21 @@ my %health_index = ( 0 => "healthy", 4 => "unhealthy", 5 => "unknown", ); +my %sd_status = ( 0 => "unknown", + 1 => "missing card", + 2 => "card inserted", + 3 => "card formatted", + 4 => "working card", + 5 => "defective card", + 6 => "incompatible speed", + 7 => "insufficient space", + 'on' => "on", ); + +my %alim_status = ( 0 => "unknown", + 1 => "incorrect power adapter", + 2 => "correct power adapter", + 'on' => "on", ); + sub netatmo_Initialize($) { @@ -60,6 +75,9 @@ netatmo_Initialize($) "webhookURL webhookPoll:0,1 ". #"serverAPI ". "addresslimit "; + $hash->{AttrList} .= "webhookURL webhookPoll:0,1 ";# if($hash->{model} eq "WEBHOOK"); + $hash->{AttrList} .= "graphReadings:0,1 locale:en-US,en-GB,de-DE,es-ES,fr-FR,it-IT,nl-NL,ru-RU,ja-JP,zh-CN,zh-TW ";# if($hash->{model} eq "FORECAST"); + $hash->{AttrList} .= "setpoint_duration ";# if($hash->{model} eq "THERMOSTAT"); $hash->{AttrList} .= $readingFnAttributes; } @@ -578,7 +596,6 @@ netatmo_Set($$@) $hash->{SUBTYPE} = "unknown" if(!defined($hash->{SUBTYPE})); my $list = ""; $list = "autocreate:noArg autocreate_homes:noArg autocreate_thermostats:noArg autocreate_homecoachs:noArg" if( $hash->{SUBTYPE} eq "ACCOUNT" ); - #$list .= " unban:noArg" if( $hash->{SUBTYPE} eq "ACCOUNT" ); $list = "home:noArg away:noArg" if ($hash->{SUBTYPE} eq "PERSON"); $list = "empty:noArg notify_movements:never,empty,always notify_unknowns:empty,always notify_animals:true,false record_animals:true,false record_movements:never,empty,always record_alarms:never,empty,always presence_record_humans:ignore,record,record_and_notify presence_record_vehicles:ignore,record,record_and_notify presence_record_animals:ignore,record,record_and_notify presence_record_movements:ignore,record,record_and_notify presence_record_alarms:ignore,record,record_and_notify gone_after presence_enable_notify_from_to:empty,always presence_notify_from presence_notify_to smart_notifs:on,off" if ($hash->{SUBTYPE} eq "HOME"); $list = "enable disable irmode:auto,always,never led_on_live:on,off mirror:off,on audio:on,off" if ($hash->{SUBTYPE} eq "CAMERA"); @@ -590,6 +607,7 @@ netatmo_Set($$@) $list = "setpoint_mode:off,hg,away,program,manual,max program:".$hash->{schedulenames}." setpoint_temp:5.0,5.5,6.0,6.5,7.0,7.5,8.0,8.5,9.0,9.5,10.0,10.5,11.0,11.5,12.0,12.5,13.0,13.5,14.0,14.5,15.0,15.5,16.0,16.5,17.0,17.5,18.0,18.5,19.0,19.5,20.0,20.5,21.0,21.5,22.0,22.5,23.0,23.5,24.0,24.5,25.0,25.5,26.0,26.5,27.0,27.5,28.0,28.5,29.0,29.5,30.0" if(defined($hash->{schedulenames})); } $list = "clear:noArg webhook:add,drop" if ($hash->{SUBTYPE} eq "WEBHOOK"); + #$list .= " checkBan:noArg" if( $hash->{SUBTYPE} eq "WEBHOOK" ); return undef if( $list eq "" ); $cmd = "(undefined)" if(!defined($cmd)); @@ -705,9 +723,9 @@ netatmo_Set($$@) } return undef; } - if( $cmd eq 'unban' )# unban:noArg + if( $cmd eq 'checkBan' )# checkBan:noArg { - return netatmo_Unban($hash); + return netatmo_checkDev($hash); } @@ -997,117 +1015,115 @@ netatmo_connect($) } sub -netatmo_Unban($) +netatmo_checkDev($) { my ($hash) = @_; + my $iohash = $hash->{IODev}; my $name = $hash->{NAME}; + RemoveInternalTimer($hash,"netatmo_checkDev"); + InternalTimer(gettimeofday()+AttrVal($name,"interval",3600), "netatmo_checkDev", $hash) if(AttrVal($name,"interval",0)>0); + HttpUtils_NonblockingGet({ - url => "https://dev.netatmo.com/", - timeout => 20, - noshutdown => 1, + url => "https://app.netatmo.net/api/createdapplist", + timeout => 30, hash => $hash, - type => 'unban', - callback => \&netatmo_parseUnban, + type => 'checkdev', + method => "POST", + #data => $json, + header => "Referer: https://dev.netatmo.com/dev/myaccount\r\nAuthorization: Bearer ".$iohash->{access_token_app}."\r\nContent-Type: application/json;charset=utf-8",#\r\nCookie: netatmocomci_csrf_cookie_na=".$csrf_token."; netatmocomlocale=en-US; netatmocomacces_token=".$accesstoken, + callback => \&netatmo_parseDev, }); + Log3 $name, 4, "$name: checking for dev apps"; + return undef; - } sub -netatmo_parseUnban($$$) +netatmo_parseDev($$$) { my ($param,$err,$data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; - #Log3 $name, 1, "$name unban\n".Dumper($param->{httpheader}); - - $data =~ /csrf_value: "(.*)"/; - my $csrf_token = $1; - - # https://auth.netatmo.com/en-US/access/login?next_url=https://dev.netatmo.com/dev/myaccount - Log3 $name, 1, "$name unban ".$csrf_token; - - HttpUtils_NonblockingGet({ - url => "https://auth.netatmo.com/en-US/access/login?next_url=https://dev.netatmo.com/dev/myaccount", - timeout => 30, - hash => $hash, - ignoreredirects => 1, - type => 'unban', - header => "Cookie: netatmocomci_csrf_cookie_na=".$csrf_token."; netatmocomlocale=en-US", - data => {ci_csrf_netatmo => $csrf_token, mail => netatmo_decrypt($hash->{helper}{username}), pass => netatmo_decrypt($hash->{helper}{password}), log_submit => 'Log+in', stay_logged => 'accept'}, - callback => \&netatmo_parseUnban2, - }); + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if($@) + { + Log3 $name, 2, "$name: invalid json evaluation on dev apps check ".$@; + return undef; + } + my $found = 0; + foreach my $devappid ( keys %{$json->{body}}) { + my $devapp = $json->{body}{$devappid}; + if(defined($devapp->{webhook_uri}) && $devapp->{webhook_uri} eq AttrVal($name,"webhookURL","-")){ + #if(defined($devapp->{activated}) && $devapp->{activated}){ + netatmo_CheckApp($hash,$devapp->{_id}); + $found = 1; + #} + } + } + Log3 $name, 2, "$name: no active dev apps matching this webhook were found" if($found == 0); return undef; } sub -netatmo_parseUnban2($$$) +netatmo_CheckApp($$) { - my ($param,$err,$data) = @_; - my $hash = $param->{hash}; + my ($hash,$appid) = @_; + my $iohash = $hash->{IODev}; my $name = $hash->{NAME}; - Log3 $name, 1, "$name header\n".Dumper($param->{httpheader}); - my $header1 = $param->{httpheader}; - my $header2 = $param->{httpheader}; - my $header3 = $param->{httpheader}; - - $header1 =~ s/=deleted/x=deleted/g; - $header2 =~ s/=deleted/x=deleted/g; - $header3 =~ s/=deleted/x=deleted/g; - - $header1 =~ /Set-Cookie: netatmocomci_csrf_cookie_na=(.*); expires/; - my $csrf_token = $1; - $hash->{helper}{csrf_token} = $csrf_token; - - $header2 =~ /Set-Cookie: netatmocomaccess_token=(.*); path/; - my $accesstoken = $1; - $accesstoken =~ s/%7C/|/g; - $hash->{helper}{access_token} = $accesstoken; - - $header3 =~ /Set-Cookie: netatmocomrefresh_token=(.*); expires/; - my $refreshtoken = $1; - $hash->{helper}{refresh_token} = $refreshtoken; - - Log3 $name, 1, "$name csrftoken ".$csrf_token; - Log3 $name, 1, "$name accesstoken ".$accesstoken; - Log3 $name, 1, "$name refreshtoken ".$refreshtoken; - - my $json = '{"application_id":"'.$hash->{helper}{client_id}.'"}'; + my $json = '{"application_id":"'.$appid.'"}'; HttpUtils_NonblockingGet({ - url => "https://dev.netatmo.com/api/unbanapp", + url => "https://app.netatmo.net/api/getapp", timeout => 30, hash => $hash, - type => 'unban', - header => "Referer: https://dev.netatmo.com/dev/myaccount\r\nAuthorization: Bearer ".$accesstoken."\r\nContent-Type: application/json;charset=utf-8\r\nCookie: netatmocomci_csrf_cookie_na=".$csrf_token."; netatmocomlocale=en-US; netatmocomacces_token=".$accesstoken, + type => 'checkdev', + method => "POST", + header => "Referer: https://dev.netatmo.com/dev/myaccount/.".$appid."\r\nAuthorization: Bearer ".$iohash->{access_token_app}."\r\nContent-Type: application/json;charset=utf-8",#\r\nCookie: netatmocomci_csrf_cookie_na=".$csrf_token."; netatmocomlocale=en-US; netatmocomacces_token=".$accesstoken, data => $json, - callback => \&netatmo_parseUnban3, + callback => \&netatmo_parseApp, }); - + Log3 $name, 4, "$name: checking dev app ".$appid; return undef; } sub -netatmo_parseUnban3($$$) +netatmo_parseApp($$$) { my ($param,$err,$data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; - Log3 $name, 1, "$name header\n".Dumper($param->{httpheader}); - Log3 $name, 1, "$name data\n".Dumper($data); - Log3 $name, 1, "$name err\n".Dumper($err); + Log3 $name, 3, "$name got dev app data to check ban state"; + + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if($@) + { + Log3 $name, 2, "$name: invalid json evaluation on dev app check ".$@; + return undef; +} + + if(defined($json->{body}) && defined($json->{body}{temporary_ban})) { + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, "banned", (($json->{body}{temporary_ban})?"yes":"no") ); + readingsBulkUpdate( $hash, "activated", (($json->{body}{activated})?"yes":"no") ); + readingsBulkUpdate( $hash, "usage_1", $json->{body}{usage_1} ); + readingsBulkUpdate( $hash, "usage_2", $json->{body}{usage_2} ); + readingsEndUpdate($hash,1); + } + Log3 $name, 5, "$name app data:\n".Dumper($json); return undef; } + + sub netatmo_initDevice($) { @@ -2172,7 +2188,7 @@ netatmo_setNotifications($$$) if( !defined($iohash->{csrf_token}) ) { my($err0,$data0) = HttpUtils_BlockingGet({ - url => "https://dev.netatmo.com/en-US", + url => "https://auth.netatmo.com/en-us/access/login", timeout => 10, noshutdown => 1, }); @@ -2181,7 +2197,8 @@ netatmo_setNotifications($$$) Log3 $name, 1, "$name: csrf call failed! ".$err0; return undef; } - $data0 =~ /csrf_value: "(.*)",/; + #Log3 $name, 1, "$name: CSRF\n".$data0; + $data0 =~ /csrf-token" content="(.*)"/; my $tmptoken = $1; $iohash->{csrf_token} = $tmptoken; if(!defined($iohash->{csrf_token})) { @@ -3858,7 +3875,7 @@ netatmo_parseForecast($$) $forecasttime = $json->{body}{time_current_symbol}; } - return undef if($datatime <= $lastupdate); + return undef if($datatime <= $lastupdate && (!AttrVal($name,"graphReadings",0) || defined(ReadingsVal($name,"graph01_rain",undef)))); readingsSingleUpdate($hash, ".lastupdate", $datatime, 0); if($json->{body}{airqdata}) @@ -4037,6 +4054,78 @@ netatmo_parseForecast($$) }#foreach forecast }#defined forecastdays + + if(AttrVal($name,"graphReadings",0)==1 && defined($json->{body}{forecastGraphs})) + { + my $i = 0; + foreach my $raingraph ( @{$json->{body}{forecastGraphs}{rain}}) + { + + next if(ref($raingraph) ne "ARRAY"); + + if(defined(@{$raingraph}[1])) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime(int(@{$raingraph}[0])); + readingsBulkUpdate( $hash, "graph".sprintf("%02d", $i)."_rain", int(@{$raingraph}[1]*1000)/1000, 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime(int(@{$raingraph}[0])); + readingsEndUpdate($hash,0); + $i++; + } + }#foreach rain + + $i = 0; + foreach my $popgraph ( @{$json->{body}{forecastGraphs}{rain_proba}}) + { + + next if(ref($popgraph) ne "ARRAY"); + + if(defined(@{$popgraph}[1])) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime(int(@{$popgraph}[0])); + readingsBulkUpdate( $hash, "graph".sprintf("%02d", $i)."_pop", @{$popgraph}[1], 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime(int(@{$popgraph}[0])); + readingsEndUpdate($hash,0); + $i++; + } + }#foreach pop + + $i = 0; + foreach my $tempgraph ( @{$json->{body}{forecastGraphs}{temperature}}) + { + + next if(ref($tempgraph) ne "ARRAY"); + + if(defined(@{$tempgraph}[1])) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime(int(@{$tempgraph}[0])); + readingsBulkUpdate( $hash, "graph".sprintf("%02d", $i)."_temperature", @{$tempgraph}[1], 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime(int(@{$tempgraph}[0])); + readingsEndUpdate($hash,0); + $i++; + } + }#foreach temp + + $i = 0; + foreach my $humgraph ( @{$json->{body}{forecastGraphs}{humidity}}) + { + + next if(ref($humgraph) ne "ARRAY"); + + if(defined(@{$humgraph}[1])) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime(int(@{$humgraph}[0])); + readingsBulkUpdate( $hash, "graph".sprintf("%02d", $i)."_humidity", @{$humgraph}[1], 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime(int(@{$humgraph}[0])); + readingsEndUpdate($hash,0); + $i++; + } + }#foreach hum + }#defined forecastgraphs + }#ok }#json else @@ -4138,8 +4227,9 @@ netatmo_parseHomeReadings($$;$) readingsSingleUpdate($camera, "name", encode_utf8($cameradata->{name}), 1) if(defined($cameradata->{name})); readingsSingleUpdate($camera, "status", $cameradata->{status}, 1) if(defined($cameradata->{status})); #$camera->{STATE} = ($cameradata->{status} eq "on") ? "online" : "offline"; - readingsSingleUpdate($camera, "sd_status", $cameradata->{sd_status}, 0) if(defined($cameradata->{sd_status})); - readingsSingleUpdate($camera, "alim_status", $cameradata->{alim_status}, 0) if(defined($cameradata->{alim_status})); + readingsSingleUpdate($camera, "sd_status", $sd_status{$cameradata->{sd_status}}, 0) if(defined($cameradata->{sd_status})); + readingsSingleUpdate($camera, "alim_status", $alim_status{$cameradata->{alim_status}}, 0) if(defined($cameradata->{alim_status})); + readingsSingleUpdate($camera, "homekit_status", $cameradata->{homekit_status}, 0) if(defined($cameradata->{homekit_status})); readingsSingleUpdate($camera, "is_local", $cameradata->{is_local}, 1) if(defined($cameradata->{is_local})); readingsSingleUpdate($camera, "vpn_url", $cameradata->{vpn_url}, 1) if(defined($cameradata->{vpn_url})); CommandDeleteReading( undef, "$camera->{NAME} vpn_url" ) if(!defined($cameradata->{vpn_url})); @@ -5415,8 +5505,8 @@ netatmo_parsePublic($$) $avgtime_rain = sprintf( "%i", $avgtime_rain ); $avgtime_wind = sprintf( "%i", $avgtime_wind ); $avg_altitude = sprintf( "%.2f", $avg_altitude ); - $avg_latitude = sprintf( "%.8f", $avg_latitude ); - $avg_longitude = sprintf( "%.8f", $avg_longitude ); + $avg_latitude = sprintf( "%.5f", $avg_latitude ); + $avg_longitude = sprintf( "%.5f", $avg_longitude ); if(scalar(@readings_temperature) > 0) { @@ -5689,11 +5779,12 @@ netatmo_pollForecast($) Log3 $name, 4, "$name: pollForecast (forecastdata)"; + my $locale = AttrVal($name, "locale", "en-US"); HttpUtils_NonblockingGet({ url => "https://app.netatmo.net/api/simplifiedfuturemeasure", timeout => 60, noshutdown => 1, - data => { device_id => $hash->{Station}, }, + data => { device_id => $hash->{Station}, locale => $locale}, header => "Authorization: Bearer ".$iohash->{access_token_app}, hash => $hash, type => 'forecastdata', @@ -6132,6 +6223,8 @@ netatmo_registerWebhook($) my $webhookurl = AttrVal($name,"webhookURL",undef); return undef if(!defined($webhookurl)); + InternalTimer(gettimeofday()+AttrVal($name,"interval",3600), "netatmo_checkDev", $hash) if(AttrVal($name,"interval",0)>=0); + HttpUtils_NonblockingGet({ url => "https://".$iohash->{helper}{apiserver}."/api/addwebhook", timeout => 30, @@ -6150,6 +6243,8 @@ netatmo_dropWebhook($) my ($hash) = @_; my $name = $hash->{NAME}; + RemoveInternalTimer($hash, "netatmo_checkDev"); + return undef if( !defined($hash->{IODev}) ); my $iohash = $hash->{IODev}; netatmo_refreshToken($iohash, defined($iohash->{access_token})); @@ -6214,6 +6309,13 @@ sub netatmo_Webhook() { "NO" ); } + if(ReadingsVal($name,"banned","-") eq "yes" && ReadingsAge($name,"banned",0) >= AttrVal($name,"interval",0)){ + netatmo_checkDev($hash); + } else { + RemoveInternalTimer($hash,"netatmo_checkDev"); + InternalTimer(gettimeofday()+AttrVal($name,"interval",3600), "netatmo_checkDev", $hash) if(AttrVal($name,"interval",0)>=0); + } + Log3 $name, 5, "Netatmo webhook JSON:\n".$data; my $json = eval { JSON->new->utf8(0)->decode($data) }; @@ -6290,6 +6392,12 @@ sub netatmo_Attr($$$) $attr{$name}{$attrName} = 0; netatmo_poll($hash); } + } elsif( $attrName eq "graphReadings" ) { + if( $cmd eq "set" && $attrVal ne "0" ) { + } else { + my $hash = $defs{$name}; + CommandDeleteReading( undef, "$name graph.*" ) if($hash->{SUBTYPE} eq "FORECAST"); + } } if( $cmd eq "set" ) { @@ -6500,7 +6608,8 @@ sub netatmo_weatherIcon() Webhook

@@ -6599,7 +6708,7 @@ sub netatmo_weatherIcon()
  • videoquality
    video quality for playlists (HOME - default: medium)
  • webhookURL
    - webhook URL - can include basic auth and port (80 or 443 only!): http://user:pass@your.url:80/fhem/netatmo (WEBHOOK)
  • + webhook URL - can include basic auth and port (80 or 443 only, no self-signed certificates!): http://user:pass@your.url:80/fhem/netatmo (WEBHOOK)
  • webhookPoll
    poll home after event from webhook (WEBHOOK - default: 0)
  • ignored_device_ids
    diff --git a/FHEM/HttpUtils.pm b/FHEM/HttpUtils.pm index 899f62d8b..49f545fb5 100644 --- a/FHEM/HttpUtils.pm +++ b/FHEM/HttpUtils.pm @@ -1,4 +1,4 @@ -############################################## +############################################## # $Id$ package main; @@ -650,7 +650,7 @@ HttpUtils_Connect2($) HttpUtils_Close($hash); return $hash->{callback}($hash, "write error: $err", undef) } - $data = substr($data,$ret); + $data = ($ret<=length($data))?substr($data,$ret):""; if(length($data) == 0) { shutdown($hash->{conn}, 1) if($s); delete($hash->{directWriteFn});