2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-01-31 06:39:11 +00:00

49_Arlo.pm: support new Arlo API, video download fix

git-svn-id: https://svn.fhem.de/fhem/trunk@20175 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
maluk 2019-09-16 19:31:02 +00:00
parent e50ffccad1
commit ff99dcedeb
2 changed files with 149 additions and 58 deletions

View File

@ -1,5 +1,6 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # 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. # Do not insert empty lines here, update check depends on it.
- change: 49_Arlo: support new Arlo API, video download fix
- change: 93_DbRep: release unnecessary allocated memory, fix warnings - change: 93_DbRep: release unnecessary allocated memory, fix warnings
- feature: 59_Weather: check newLocation value - feature: 59_Weather: check newLocation value
- change: 49_SSCam: release unnecessary allocated memory - change: 49_SSCam: release unnecessary allocated memory

View File

@ -9,6 +9,7 @@ use IO::Socket::INET;
use HTTP::Request; use HTTP::Request;
use HTTP::Cookies; use HTTP::Cookies;
use LWP::UserAgent; use LWP::UserAgent;
use MIME::Base64;
use HttpUtils; use HttpUtils;
use JSON; use JSON;
@ -20,7 +21,7 @@ sub Arlo_Initialize($$) {
$hash->{UndefFn} = "Arlo_Undef"; $hash->{UndefFn} = "Arlo_Undef";
$hash->{GetFn} = "Arlo_Get"; $hash->{GetFn} = "Arlo_Get";
$hash->{SetFn} = "Arlo_Set"; $hash->{SetFn} = "Arlo_Set";
$hash->{AttrList} = "disable:1 pingInterval updateInterval downloadDir downloadLink ssePollingInterval ".$readingFnAttributes; $hash->{AttrList} = "disable:1 pingInterval updateInterval downloadDir downloadLink ssePollingInterval videoDownloadFix:0,1 ".$readingFnAttributes;
$hash->{AttrFn} = "Arlo_Attr"; $hash->{AttrFn} = "Arlo_Attr";
} }
@ -42,7 +43,7 @@ sub Arlo_Define($$) {
$hash->{DEF} = "ACCOUNT $cryptUser $cryptPasswd"; $hash->{DEF} = "ACCOUNT $cryptUser $cryptPasswd";
InternalTimer(gettimeofday() + 3, "Arlo_Login", $hash); InternalTimer(gettimeofday() + 3, "Arlo_Login", $hash);
} elsif ($subtype eq 'BASESTATION' && @a == 5) { } elsif (($subtype eq 'BASESTATION' || $subtype eq 'ROUTER') && @a == 5) {
my $serialNumber = $a[3]; my $serialNumber = $a[3];
my $xCloudId = $a[4]; my $xCloudId = $a[4];
my $d = $modules{$MODULE}{defptr}{"B$serialNumber"}; my $d = $modules{$MODULE}{defptr}{"B$serialNumber"};
@ -129,7 +130,7 @@ sub Arlo_Undef($$) {
my $subtype = $hash->{SUBTYPE}; my $subtype = $hash->{SUBTYPE};
delete($modules{$MODULE}{defptr}{"L$hash->{serialNumber}"}) if ($subtype eq 'LIGHT'); delete($modules{$MODULE}{defptr}{"L$hash->{serialNumber}"}) if ($subtype eq 'LIGHT');
delete($modules{$MODULE}{defptr}{"C$hash->{serialNumber}"}) if ($subtype eq 'CAMERA' || $subtype eq 'ARLOQ' || $subtype eq 'BABYCAM'); delete($modules{$MODULE}{defptr}{"C$hash->{serialNumber}"}) if ($subtype eq 'CAMERA' || $subtype eq 'ARLOQ' || $subtype eq 'BABYCAM');
delete($modules{$MODULE}{defptr}{"B$hash->{serialNumber}"}) if ($subtype eq 'BASESTATION' || $subtype eq 'BRIDGE' || $subtype eq 'ARLOQ' || $subtype eq 'BABYCAM'); delete($modules{$MODULE}{defptr}{"B$hash->{serialNumber}"}) if ($subtype eq 'BASESTATION' || $subtype eq 'BRIDGE' || $subtype eq 'ROUTER' || $subtype eq 'ARLOQ' || $subtype eq 'BABYCAM');
if ($subtype eq 'ACCOUNT') { if ($subtype eq 'ACCOUNT') {
delete($modules{$MODULE}{defptr}{'account'}); delete($modules{$MODULE}{defptr}{'account'});
Arlo_Logout($hash); Arlo_Logout($hash);
@ -179,7 +180,7 @@ sub Arlo_Set($) {
} else { } else {
return "Unknown argument $opt, choose one of autocreate:noArg readModes:noArg reconnect:noArg updateReadings:noArg "; return "Unknown argument $opt, choose one of autocreate:noArg readModes:noArg reconnect:noArg updateReadings:noArg ";
} }
} elsif ($subtype eq 'BASESTATION') { } elsif ($subtype eq 'BASESTATION' || $subtype eq 'ROUTER') {
if (!Arlo_SetBasestationCmd($hash, $opt, $value)) { if (!Arlo_SetBasestationCmd($hash, $opt, $value)) {
return "Unknown argument $opt, choose one of arm:noArg disarm:noArg mode subscribe:noArg unsubscribe:noArg siren:on,off"; return "Unknown argument $opt, choose one of arm:noArg disarm:noArg mode subscribe:noArg unsubscribe:noArg siren:on,off";
} }
@ -198,18 +199,18 @@ sub Arlo_Set($) {
} elsif ($subtype eq 'ARLOQ') { } elsif ($subtype eq 'ARLOQ') {
if (!Arlo_SetBasestationCmd($hash, $opt, $value)) { if (!Arlo_SetBasestationCmd($hash, $opt, $value)) {
if (!Arlo_SetCameraCmd($hash, $opt, $value)) { if (!Arlo_SetCameraCmd($hash, $opt, $value)) {
return "Unknown argument $opt, choose one of arm:noArg disarm:noArg subscribe:noArg unsubscribe:noArg snapshot:noArg startRecording:noArg stopRecording:noArg brightness:-2,-1,0,1,2"; return "Unknown argument $opt, choose one of arm:noArg disarm:noArg subscribe:noArg unsubscribe:noArg snapshot:noArg startRecording:noArg stopRecording:noArg brightness:-2,-1,0,1,2";
} }
} }
} elsif ($subtype eq 'BABYCAM') { } elsif ($subtype eq 'BABYCAM') {
if (!Arlo_SetBasestationCmd($hash, $opt, $value)) { if (!Arlo_SetBasestationCmd($hash, $opt, $value)) {
if (!Arlo_SetCameraCmd($hash, $opt, $value)) { if (!Arlo_SetCameraCmd($hash, $opt, $value)) {
return "Unknown argument $opt, choose one of arm:noArg disarm:noArg subscribe:noArg unsubscribe:noArg nightlight:on,off nightlight-brightness nightlight-color snapshot:noArg startRecording:noArg stopRecording:noArg brightness:-2,-1,0,1,2"; return "Unknown argument $opt, choose one of arm:noArg disarm:noArg subscribe:noArg unsubscribe:noArg nightlight:on,off nightlight-brightness nightlight-color snapshot:noArg startRecording:noArg stopRecording:noArg brightness:-2,-1,0,1,2";
} }
} }
} else { } else {
if (!Arlo_SetCameraCmd($hash, $opt, $value)) { if (!Arlo_SetCameraCmd($hash, $opt, $value)) {
return "Unknown argument $opt, choose one of on:noArg off:noArg snapshot:noArg startRecording:noArg stopRecording:noArg brightness:-2,-1,0,1,2"; return "Unknown argument $opt, choose one of on:noArg off:noArg snapshot:noArg startRecording:noArg stopRecording:noArg brightness:-2,-1,0,1,2 downloadLastVideo:noArg";
} }
} }
@ -254,6 +255,8 @@ sub Arlo_SetCameraCmd($$$) {
Arlo_SetNightLightBrightness($hash, $value); Arlo_SetNightLightBrightness($hash, $value);
} elsif ($opt eq 'nightlight-color') { } elsif ($opt eq 'nightlight-color') {
Arlo_SetNightLightColor($hash, $value); Arlo_SetNightLightColor($hash, $value);
} elsif ($opt eq 'downloadLastVideo') {
Arlo_DownloadLastVideo($hash);
} else { } else {
return undef; return undef;
} }
@ -269,7 +272,8 @@ sub Arlo_Get($) {
my $date = $a[2]; my $date = $a[2];
return "Unknown argument $cmd, choose one of recordings" if ($cmd ne "recordings"); return "Unknown argument $cmd, choose one of recordings" if ($cmd ne "recordings");
return "Paramter date (format YYYYMMDD) needed." if (!defined($date) || length($date) != 8); return "Paramter date (format YYYYMMDD) needed." if (!defined($date) || length($date) != 8);
return Arlo_GetRecordings($hash, $date); my @result = Arlo_GetRecordings($hash, $date);
return encode_json(\@result);
} }
sub Arlo_Poll($) { sub Arlo_Poll($) {
@ -397,15 +401,15 @@ sub Arlo_PrepareRequest($$;$$$$) {
my $name = $account->{NAME}; my $name = $account->{NAME};
my $cookies = $account->{helper}{cookies}; my $cookies = $account->{helper}{cookies};
my $token = $account->{helper}{token}; my $token = $account->{helper}{token};
my $headers = "Authorization: ".$token."\r\nReferer: https://arlo.netgear.com\r\nContent-Type: application/json; charset=utf-8\r\nCookie: ".$cookies. my $headers = "Authorization: ".$token."\r\nReferer: https://my.arlo.com\r\nContent-Type: application/json; charset=utf-8\r\nCookie: ".$cookies.
"\r\nschemaVersion: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0"; "\r\nschemaVersion: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0";
$headers = $headers."\r\n".$additionalHeader if (defined($additionalHeader)); $headers = $headers."\r\n".$additionalHeader if (defined($additionalHeader));
Log3 $name, 5, "Header: $headers"; Log3 $name, 5, "Header: $headers";
my $url = 'https://arlo.netgear.com/hmsweb'.$urlSuffix; my $url = 'https://my.arlo.com/hmsweb'.$urlSuffix;
Log3 $name, 5, "URL: $url"; Log3 $name, 5, "URL: $url";
my $request = {url => $url, method => $method, header => $headers, host => 'arlo.netgear.com'}; my $request = {url => $url, method => $method, header => $headers, host => 'my.arlo.com'};
if (defined($body)) { if (defined($body)) {
my $bodyJson = encode_json $body; my $bodyJson = encode_json $body;
@ -416,8 +420,8 @@ sub Arlo_PrepareRequest($$;$$$$) {
return $request; return $request;
} }
sub Arlo_Request($$;$$$$$) { sub Arlo_Request($$;$$$$$$) {
my ($hash, $urlSuffix, $method, $body, $additionalHeader, $callback) = @_; my ($hash, $urlSuffix, $method, $body, $additionalHeader, $callback, $origin) = @_;
my $request = Arlo_PrepareRequest($hash, $urlSuffix, $method, $body, $additionalHeader); my $request = Arlo_PrepareRequest($hash, $urlSuffix, $method, $body, $additionalHeader);
if (defined($callback)) { if (defined($callback)) {
@ -426,6 +430,10 @@ sub Arlo_Request($$;$$$$$) {
$request->{callback} = \&Arlo_DefaultCallback; $request->{callback} = \&Arlo_DefaultCallback;
} }
if (defined($origin)) {
$request->{origin} = $origin;
}
HttpUtils_NonblockingGet($request); HttpUtils_NonblockingGet($request);
} }
@ -448,7 +456,16 @@ sub Arlo_DefaultCallback($$$) {
if ($response->{success}) { if ($response->{success}) {
Log3 $name, 5, "Response from Arlo: $jsonData"; Log3 $name, 5, "Response from Arlo: $jsonData";
} else { } else {
Log3 $name, 2, "Arlo call was not successful: $jsonData"; my $logLevel = 2;
if ($response->{data}) {
my $data = $response->{data};
my $origin = $hash->{origin};
if ($origin && $data->{error} eq '2059' && $data->{reason} eq 'Device is offline.') {
readingsSingleUpdate($origin, 'state', 'offline', 1) if (ReadingsVal($origin->{NAME}, 'state', '') ne 'offline');
$logLevel = 5;
}
}
Log3 $name, $logLevel, "Arlo call was not successful: $jsonData";
$response = undef; $response = undef;
} }
}; };
@ -502,6 +519,8 @@ sub Arlo_CreateDevicesCallback($$$) {
Log3 $hash->{NAME}, 3, "Found device $deviceType with name $deviceName."; Log3 $hash->{NAME}, 3, "Found device $deviceType with name $deviceName.";
if ($deviceType eq 'basestation') { if ($deviceType eq 'basestation') {
Arlo_CreateDevice($hash, 'BASESTATION', 'B', $deviceName, $serialNumber, $xCloudId, $model); Arlo_CreateDevice($hash, 'BASESTATION', 'B', $deviceName, $serialNumber, $xCloudId, $model);
} elsif ($deviceType eq 'routerM1') {
Arlo_CreateDevice($hash, 'ROUTER', 'B', $deviceName, $serialNumber, $xCloudId, $model);
} elsif ($deviceType eq 'arlobridge') { } elsif ($deviceType eq 'arlobridge') {
Arlo_CreateDevice($hash, 'BRIDGE', 'B', $deviceName, $serialNumber, $xCloudId, $model); Arlo_CreateDevice($hash, 'BRIDGE', 'B', $deviceName, $serialNumber, $xCloudId, $model);
} elsif ($deviceType eq 'camera') { } elsif ($deviceType eq 'camera') {
@ -550,7 +569,7 @@ sub Arlo_Notify($$;$) {
my ($hash, $body, $callback) = @_; my ($hash, $body, $callback) = @_;
my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $body); my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $body);
Log3 $account->{NAME}, 4, "Notify $deviceId, action: $body->{action} $body->{resource}"; Log3 $account->{NAME}, 4, "Notify $deviceId, action: $body->{action} $body->{resource}";
Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', $body, 'xcloudId: '.$xCloudId, $callback); Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', $body, 'xcloudId: '.$xCloudId, $callback, $hash);
} }
sub Arlo_Subscribe($) { sub Arlo_Subscribe($) {
@ -564,7 +583,7 @@ sub Arlo_Subscribe($) {
if (!defined($account->{RESPONSE_TIMEOUT})) { if (!defined($account->{RESPONSE_TIMEOUT})) {
$account->{RESPONSE_TIMEOUT} = gettimeofday() + 30; $account->{RESPONSE_TIMEOUT} = gettimeofday() + 30;
} }
Arlo_Notify($hash, $body); Arlo_Notify($hash, $body, \&Arlo_SubscribeCallback);
} }
sub Arlo_SubscribeAll($) { sub Arlo_SubscribeAll($) {
@ -576,6 +595,16 @@ sub Arlo_SubscribeAll($) {
} }
} }
sub Arlo_SubscribeCallback($$$) {
my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultCallback($hash, $err, $jsonData);
my $origin = $hash->{origin};
if (defined($response) && $origin && ReadingsVal($origin->{NAME}, 'state', '') eq 'offline') {
readingsSingleUpdate($hash, 'state', 'online', 1);
InternalTimer(gettimeofday() + 1, 'Arlo_UpdateBasestationReadings', $hash);
}
}
sub Arlo_Unsubscribe($) { sub Arlo_Unsubscribe($) {
my ($hash) = @_; my ($hash) = @_;
Arlo_Request($hash, '/client/unsubscribe'); Arlo_Request($hash, '/client/unsubscribe');
@ -613,7 +642,7 @@ sub Arlo_ReadCamerasAndLights($) {
my @body = ($cam, $lights); my @body = ($cam, $lights);
if (defined($hash->{basestationSerialNumber}) && $hash->{basestationSerialNumber} eq $hash->{serialNumber}) { if (defined($hash->{basestationSerialNumber}) && $hash->{basestationSerialNumber} eq $hash->{serialNumber}) {
my $mode = {action => 'get', resource => 'modes', publishResponse => \0}; my $mode = {action => 'get', resource => 'modes', publishResponse => \0};
my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $mode); Arlo_PreparePostRequest($hash, $mode);
push @body, $mode; push @body, $mode;
} }
Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', \@body, 'xcloudId: '.$xCloudId); Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', \@body, 'xcloudId: '.$xCloudId);
@ -621,7 +650,7 @@ sub Arlo_ReadCamerasAndLights($) {
sub Arlo_UpdateReadings($) { sub Arlo_UpdateReadings($) {
my ($hash) = @_; my ($hash) = @_;
Arlo_Request($hash, '/users/devices/automation/active', 'GET', undef, undef, \&Arlo_UpdateReadingsCallback); Arlo_UpdateBasestationReadings($hash);
my @basestations = Arlo_GetBasestations($hash); my @basestations = Arlo_GetBasestations($hash);
my $delay = 2; my $delay = 2;
foreach my $serialNumber (@basestations) { foreach my $serialNumber (@basestations) {
@ -631,6 +660,11 @@ sub Arlo_UpdateReadings($) {
} }
} }
sub Arlo_UpdateBasestationReadings($) {
my ($hash) = @_;
Arlo_Request($hash, '/users/devices/automation/active', 'GET', undef, undef, \&Arlo_UpdateReadingsCallback);
}
sub Arlo_UpdateReadingsCallback($$$) { sub Arlo_UpdateReadingsCallback($$$) {
my ($hash, $err, $jsonData) = @_; my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultCallback($hash, $err, $jsonData); my $response = Arlo_DefaultCallback($hash, $err, $jsonData);
@ -701,7 +735,7 @@ sub Arlo_SetBasestationMode($$) {
sub Arlo_DoSetBasestationMode($$) { sub Arlo_DoSetBasestationMode($$) {
my ($hash, $mode) = @_; my ($hash, $mode) = @_;
if (defined($hash->{basestationSerialNumber})) { # Kamera mit integrierter Basestation if (defined($hash->{basestationSerialNumber}) || $hash->{SUBTYPE} eq 'ROUTER') { # Kamera mit integrierter Basestation oder Router M1
my $props = {active => $mode}; my $props = {active => $mode};
my $body = {action => 'set', resource => 'modes', publishResponse => \1, properties => $props}; my $body = {action => 'set', resource => 'modes', publishResponse => \1, properties => $props};
Arlo_Notify($hash, $body); Arlo_Notify($hash, $body);
@ -752,7 +786,7 @@ sub Arlo_Snapshot($) {
sub Arlo_StartRecording($) { sub Arlo_StartRecording($) {
my ($hash) = @_; my ($hash) = @_;
my $activityState = ReadingsVal($hash->{NAME}, 'activityState', 'idle') ; my $activityState = ReadingsVal($hash->{NAME}, 'activityState', 'idle');
if ($activityState eq 'userStreamActive' && defined($hash->{streamURL})) { if ($activityState eq 'userStreamActive' && defined($hash->{streamURL})) {
Arlo_Subscribe($hash); Arlo_Subscribe($hash);
return "Camera is still recording."; return "Camera is still recording.";
@ -760,7 +794,7 @@ sub Arlo_StartRecording($) {
my $basestation = Arlo_GetBasestationForCamera($hash); my $basestation = Arlo_GetBasestationForCamera($hash);
my $cameraId = $hash->{serialNumber}; my $cameraId = $hash->{serialNumber};
my $props = {activityState => 'startUserStream', camera => $cameraId}; my $props = {activityState => 'startUserStream', camera => $cameraId};
my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props};
my ($account, $basestationId, $xCloudId) = Arlo_PreparePostRequest($basestation, $body); my ($account, $basestationId, $xCloudId) = Arlo_PreparePostRequest($basestation, $body);
Log3 $account->{NAME}, 4, "Start streaming for camera $cameraId."; Log3 $account->{NAME}, 4, "Start streaming for camera $cameraId.";
$hash->{FOLLOW_CALL} = 'startRecord'; $hash->{FOLLOW_CALL} = 'startRecord';
@ -887,7 +921,29 @@ sub Arlo_GetRecordings($$) {
} }
} }
return encode_json(\@result); return @result;
}
sub Arlo_DownloadLastVideo($) {
my ($hash) = @_;
my $cameraId = $hash->{serialNumber};
my $date = strftime '%Y%m%d', localtime;
my @recordings = Arlo_GetRecordings($hash, $date);
my $length = @recordings;
if ($length > 0) {
my $rec = $recordings[0];
my $lastVideoTime = $hash->{lastVideoTime};
my $newVideoTime = int($rec->{time});
my $account = $modules{$MODULE}{defptr}{"account"};
if (!defined($lastVideoTime) || $newVideoTime > $lastVideoTime) {
$hash->{lastVideoTime} = $newVideoTime;
Log3 $account->{NAME}, 4, "Download new recording $newVideoTime.";
Arlo_SetReadingAndDownload($account, 'lastVideoThumbnailUrl', $rec->{thumbnail}, $cameraId, '_thumb.jpg', \0);
Arlo_SetReadingAndDownload($account, 'lastVideoUrl', $rec->{video}, $cameraId, '.mp4', \1);
} else {
Log3 $account->{NAME}, 4, "Don't download recording because there is now new recording. Last Video: $lastVideoTime New Video: $newVideoTime";
}
}
} }
# #
@ -903,36 +959,56 @@ sub Arlo_Login($) {
return; return;
} }
my $input = {email => $hash->{helper}{username}, password => $hash->{helper}{password}}; my $password = encode_base64($hash->{helper}{password}, '');
my $input = {email => $hash->{helper}{username}, password => $password, EnvSource => 'prod', language => 'de'};
my $postData = encode_json $input; my $postData = encode_json $input;
my $header = ['Content-Type' => 'application/json; charset=utf-8']; my $header = ['Content-Type' => 'application/json; charset=utf-8', 'Auth-Version' => 2];
my $cookie_jar = HTTP::Cookies->new; my $cookie_jar = HTTP::Cookies->new;
my $ua = LWP::UserAgent->new(cookie_jar => $cookie_jar, agent => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0'); my $ua = LWP::UserAgent->new(cookie_jar => $cookie_jar, agent => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0');
my $req = HTTP::Request->new('POST', 'https://arlo.netgear.com/hmsweb/login/v2', $header, $postData); my $req = HTTP::Request->new('POST', 'https://ocapi-app.arlo.com/api/auth', $header, $postData);
my $resp = $ua->request($req); my $resp = $ua->request($req);
if ($resp->is_success) { if ($resp->is_success) {
eval { eval {
my $respObj = decode_json $resp->decoded_content; my $respObj = decode_json $resp->decoded_content;
if ($respObj->{success}) { if ($respObj->{meta}{code} == 200) {
my $data = $respObj->{data}; my $data = $respObj->{data};
$hash->{helper}{token} = $data->{token}; $hash->{helper}{token} = $data->{token};
$cookie_jar->extract_cookies($resp);
$hash->{helper}{cookies} = Arlo_GetCookies($cookie_jar);
Log3 $name, 5, $hash->{helper}{cookies};
$hash->{helper}{userId} = $data->{userId}; $hash->{helper}{userId} = $data->{userId};
$hash->{SSE_STATUS} = 200; my $validateData = $data->{authenticated};
delete $hash->{RETRY}; my $authorization = encode_base64($data->{token}, '');
$hash->{STATE} = 'active'; $header = ['Content-Type' => 'application/json; charset=utf-8', 'Auth-Version' => 2, 'Authorization' => $authorization];
Arlo_Request($hash, '/users/devices'); $req = HTTP::Request->new('GET', 'https://ocapi-app.arlo.com/api/validateAccessToken?data='.$validateData, $header);
Arlo_EventQueue($hash); $resp = $ua->request($req);
Arlo_Ping($hash); if ($resp->is_success) {
if (!defined($hash->{MODES})) { $respObj = decode_json $resp->decoded_content;
InternalTimer(gettimeofday() + 5, "Arlo_ReadModes", $hash); if ($respObj->{meta}{code} == 200) {
} $header = ['Content-Type' => 'application/json; charset=utf-8', 'Auth-Version' => 2, 'Authorization' => $data->{token}];
InternalTimer(gettimeofday() + 30, "Arlo_Poll", $hash); $req = HTTP::Request->new('GET', 'https://my.arlo.com/hmsweb/users/session/v2', $header);
return; $resp = $ua->request($req);
if ($resp->is_success) {
$respObj = decode_json $resp->decoded_content;
if ($respObj->{success}) {
$cookie_jar->extract_cookies($resp);
$hash->{helper}{cookies} = Arlo_GetCookies($cookie_jar);
Log3 $name, 5, $hash->{helper}{cookies};
$hash->{SSE_STATUS} = 200;
delete $hash->{RETRY};
$hash->{STATE} = 'active';
Arlo_Request($hash, '/users/devices');
Arlo_EventQueue($hash);
Arlo_Ping($hash);
if (!defined($hash->{MODES})) {
InternalTimer(gettimeofday() + 5, "Arlo_ReadModes", $hash);
}
InternalTimer(gettimeofday() + 30, "Arlo_Poll", $hash);
return;
}
}
}
}
Log3 $name, 2, 'Arlo ValidateAccessToken not successful: '.$resp->decoded_content;
} else { } else {
Log3 $name, 2, 'Arlo Login not successful: '.$resp->decoded_content; Log3 $name, 2, 'Arlo Login not successful: '.$resp->decoded_content;
} }
@ -977,9 +1053,9 @@ sub Arlo_EventQueue($) {
my $token = $hash->{helper}{token}; my $token = $hash->{helper}{token};
delete $hash->{RESPONSE_TIMEOUT}; delete $hash->{RESPONSE_TIMEOUT};
my $headers = "Authorization: ".$token."\r\nAccept: text/event-stream\r\nReferer: https://arlo.netgear.com\r\n". my $headers = "Authorization: ".$token."\r\nAccept: text/event-stream\r\nReferer: https://my.arlo.com\r\n".
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0\r\nCookie: ".$cookies; "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0\r\nCookie: ".$cookies;
my $con = {url => 'https://arlo.netgear.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'arlo.netgear.com', httpversion => '1.1'}; my $con = {url => 'https://my.arlo.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'my.arlo.com', httpversion => '1.1'};
my $err = HttpUtils_Connect($con); my $err = HttpUtils_Connect($con);
if ($err) { if ($err) {
Log3 $name, 2, "Error in Arlo event queue: $err"; Log3 $name, 2, "Error in Arlo event queue: $err";
@ -1012,8 +1088,9 @@ sub Arlo_EventPolling($) {
vec($rin, $con->{conn}->fileno(), 1) = 1; vec($rin, $con->{conn}->fileno(), 1) = 1;
Log3 $name, 5, "Checking for Arlo server response."; Log3 $name, 5, "Checking for Arlo server response.";
my $nfound = select($rout=$rin, undef, undef, 0.1); my $nfound = select($rout=$rin, undef, undef, 0.1);
my $buf = ''; my $content = '';
while ($nfound > 0) { while ($nfound > 0) {
my $buf = '';
my $len = sysread($con->{conn}, $buf, 65536); my $len = sysread($con->{conn}, $buf, 65536);
if (!defined($len) || $len <= 0) { if (!defined($len) || $len <= 0) {
HttpUtils_Close($con); HttpUtils_Close($con);
@ -1026,14 +1103,10 @@ sub Arlo_EventPolling($) {
} }
return; return;
} }
my $lastChar = substr($buf, length($buf) - 1, 1); $content = $content . $buf;
if ($lastChar eq "\n" || $lastChar eq "\r") { $nfound = select($rout=$rin, undef, undef, 0.1);
$nfound = 0;
} else {
$nfound = select($rout=$rin, undef, undef, 1);
}
} }
Arlo_ProcessResponse($hash, $buf) if ($buf ne ''); Arlo_ProcessResponse($hash, $content) if ($content ne '');
if ($hash->{SSE_STATUS} == 299) { if ($hash->{SSE_STATUS} == 299) {
$hash->{RETRY} = 1; $hash->{RETRY} = 1;
InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash); InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash);
@ -1093,7 +1166,8 @@ sub Arlo_ProcessResponse($$) {
$hash->{SSE_STATUS} = 299; $hash->{SSE_STATUS} = 299;
} }
} elsif ($check ne 'event' && $check ne 'Cache' && $check ne 'Conte' && $check ne 'Date:' && $check ne 'Pragm' && $check ne 'Server' } elsif ($check ne 'event' && $check ne 'Cache' && $check ne 'Conte' && $check ne 'Date:' && $check ne 'Pragm' && $check ne 'Server'
&& substr($check, 0, 2) ne 'X-' && $check ne 'trans' && $check ne 'Serve' && $check ne 'Expir') { && substr($check, 0, 2) ne 'X-' && $check ne 'trans' && $check ne 'Serve' && $check ne 'Expir' && $check ne 'Stric' && $check ne 'Trans'
&& $check ne 'Expec' && $check ne 'CF-RA') {
Log3 $hash->{NAME}, 2, "Invalid Arlo event response: $line"; Log3 $hash->{NAME}, 2, "Invalid Arlo event response: $line";
} }
} }
@ -1145,8 +1219,9 @@ sub Arlo_ProcessEvent($$) {
my $activityState = $props->{activityState}; my $activityState = $props->{activityState};
if ($activityState) { if ($activityState) {
Arlo_SetReading($hash, $cameraId, 'activityState', $activityState);
my $camera = $modules{$MODULE}{defptr}{"C$cameraId"}; my $camera = $modules{$MODULE}{defptr}{"C$cameraId"};
my $oldActivityState = ReadingsVal($camera->{NAME}, 'activityState', '');
Arlo_SetReading($hash, $cameraId, 'activityState', $activityState);
if ($activityState eq 'startUserStream') { if ($activityState eq 'startUserStream') {
my $streamURL = $props->{streamURL}; my $streamURL = $props->{streamURL};
if (defined($streamURL)) { if (defined($streamURL)) {
@ -1154,6 +1229,9 @@ sub Arlo_ProcessEvent($$) {
} }
} elsif ($activityState eq 'userStreamActive') { } elsif ($activityState eq 'userStreamActive') {
InternalTimer(gettimeofday() + 0.5, "Arlo_StartRecordingStep2", $camera); InternalTimer(gettimeofday() + 0.5, "Arlo_StartRecordingStep2", $camera);
} elsif ($activityState eq 'idle' && ($oldActivityState eq 'alertStreamActive' || $oldActivityState eq 'userStreamActive') && AttrVal($name, 'videoDownloadFix', 0) == 1) {
Log3 $name, 4, "Download latest video for $camera->{NAME} in 2 seconds";
InternalTimer(gettimeofday() + 2, "Arlo_DownloadLastVideo", $camera);
} }
} }
} }
@ -1168,7 +1246,7 @@ sub Arlo_ProcessEvent($$) {
Arlo_SetReading($hash, $deviceId, 'motionDetected', $props->{motionDetected}); Arlo_SetReading($hash, $deviceId, 'motionDetected', $props->{motionDetected});
Arlo_SetReading($hash, $deviceId, 'state', $props->{lampState}); Arlo_SetReading($hash, $deviceId, 'state', $props->{lampState});
} }
} elsif ($resource eq 'mediaUploadNotification') { } elsif ($resource eq 'mediaUploadNotification' && AttrVal($name, 'videoDownloadFix', 0) == 0) {
my $cameraId = $event->{deviceId}; my $cameraId = $event->{deviceId};
Arlo_SetReading($hash, $cameraId, 'activityState', 'idle'); Arlo_SetReading($hash, $cameraId, 'activityState', 'idle');
Arlo_SetReadingAndDownload($hash, 'lastVideoThumbnailUrl', $event->{presignedThumbnailUrl}, $cameraId, '_thumb.jpg', \0); Arlo_SetReadingAndDownload($hash, 'lastVideoThumbnailUrl', $event->{presignedThumbnailUrl}, $cameraId, '_thumb.jpg', \0);
@ -1235,7 +1313,7 @@ sub Arlo_decrypt($) {
<a name="Arlo"></a> <a name="Arlo"></a>
<h3>Arlo</h3> <h3>Arlo</h3>
<ul> <ul>
<p>Arlo security cams from NETGEAR are connected to the Arlo Cloud via base stations. The base stations and cameras can be controlled with a REST API. <p>Arlo security cams are connected to the Arlo Cloud via base stations. The base stations and cameras can be controlled with a REST API.
Events (like movement and state changes) are delivery by server-sent events (SSE).</p> Events (like movement and state changes) are delivery by server-sent events (SSE).</p>
<p><a name="ArloDefine"></a> <b>Define</b></p> <p><a name="ArloDefine"></a> <b>Define</b></p>
@ -1359,6 +1437,12 @@ sub Arlo_decrypt($) {
<p>Subtype BASESTATION: Deactivates the periodic update of the readings from Arlo Cloud.</p> <p>Subtype BASESTATION: Deactivates the periodic update of the readings from Arlo Cloud.</p>
</ul> </ul>
<p><a name="ArloPingVideoDownloadFix"></a> <b>videoDownloadFix</b></p>
<ul>
<p>Subtype ACCOUNT: Set this attribute to 1 if videos are not downloaded automatically. Normally the server sents a notification when there is a new video available but sometimes
this doesn't work. Default is 0.</p>
</ul>
<p><a name="ArloPingInterval"></a> <b>pingInterval</b></p> <p><a name="ArloPingInterval"></a> <b>pingInterval</b></p>
<ul> <ul>
<p>Subtype ACCOUNT: Set the interval in seconds for the heartbeat-ping. Without a heartbeat-ping the session in Arlo Cloud would expire and FHEM wouldn't receive any more events. <p>Subtype ACCOUNT: Set the interval in seconds for the heartbeat-ping. Without a heartbeat-ping the session in Arlo Cloud would expire and FHEM wouldn't receive any more events.
@ -1383,7 +1467,7 @@ sub Arlo_decrypt($) {
<a name="Arlo"></a> <a name="Arlo"></a>
<h3>Arlo</h3> <h3>Arlo</h3>
<ul> <ul>
<p>Arlo Sicherheitskameras von NETGEAR werden über eine Basisstation an die Arlo Cloud angebunden. Diese kann über eine REST-API angesprochen werden und liefert <p>Arlo Sicherheitskameras werden über eine Basisstation an die Arlo Cloud angebunden. Diese kann über eine REST-API angesprochen werden und liefert
Ereignisse (wie z.B. erkannte Bewegungen oder sonstige Statusänderungen) über Server-Sent Events (SSE) zurück.</p> Ereignisse (wie z.B. erkannte Bewegungen oder sonstige Statusänderungen) über Server-Sent Events (SSE) zurück.</p>
<p><a name="ArloDefine"></a> <b>Define</b></p> <p><a name="ArloDefine"></a> <b>Define</b></p>
@ -1508,6 +1592,12 @@ sub Arlo_decrypt($) {
<p>Subtype BASESTATION: Deaktiviert die regelmäßige Abfrage der Readings aus der Arlo Cloud.</p> <p>Subtype BASESTATION: Deaktiviert die regelmäßige Abfrage der Readings aus der Arlo Cloud.</p>
</ul> </ul>
<p><a name="ArloPingVideoDownloadFix"></a> <b>videoDownloadFix</b></p>
<ul>
<p>Subtype ACCOUNT: Dieser Wert muss auf 1 gesetzt werden, falls Videos nach der Aufnahme nicht automatisch heruntergeladen werden. Normalerweise werden Events vom Server gesendet,
sobald eine neue Aufnahme vorhanden ist, aber manchmal funktioniert das nicht. Standard ist 0 (ausgeschaltet).</p>
</ul>
<p><a name="ArloPingInterval"></a> <b>pingInterval</b></p> <p><a name="ArloPingInterval"></a> <b>pingInterval</b></p>
<ul> <ul>
<p>Subtype ACCOUNT: Setzt das Intervall in Sekunden, wie häfuig ein Heartbeat-Ping gesendet wird. Ohne den Heartbeat-Ping würde die Session ablaufen und <p>Subtype ACCOUNT: Setzt das Intervall in Sekunden, wie häfuig ein Heartbeat-Ping gesendet wird. Ohne den Heartbeat-Ping würde die Session ablaufen und