diff --git a/fhem/CHANGED b/fhem/CHANGED index 0b5af1d61..41ff0a5e6 100644 --- a/fhem/CHANGED +++ b/fhem/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. + - new: 49_Arlo: module for Arlo cameras - feature: 98_monitoring: - add "$listFuncAdded" function - bugfix: 22_HOMEMODE: - fix illegal division by zero in HOMEMODE_Luminance - renamed attribute name HomeYahooWeatherDevice to diff --git a/fhem/FHEM/49_Arlo.pm b/fhem/FHEM/49_Arlo.pm new file mode 100644 index 000000000..ec8e762e7 --- /dev/null +++ b/fhem/FHEM/49_Arlo.pm @@ -0,0 +1,1513 @@ +# $Id$ + +package main; + +use strict; +use warnings; +use IO::Socket; +use IO::Socket::INET; +use HTTP::Request; +use HTTP::Cookies; +use LWP::UserAgent; +use HttpUtils; +use JSON; + +my $MODULE='Arlo'; + +sub Arlo_Initialize($$) { + my ($hash) = @_; + $hash->{DefFn} = "Arlo_Define"; + $hash->{UndefFn} = "Arlo_Undef"; + $hash->{GetFn} = "Arlo_Get"; + $hash->{SetFn} = "Arlo_Set"; + $hash->{AttrList} = "disable:1 pingInterval updateInterval downloadDir downloadLink ssePollingInterval ".$readingFnAttributes; + $hash->{AttrFn} = "Arlo_Attr"; +} + +sub Arlo_Define($$) { + my ($hash, $def) = @_; + my $name = $hash->{NAME}; + my @a = split("[ \t][ \t]*", $def); + + my $subtype = $a[2]; + if ($subtype eq 'ACCOUNT' && @a == 5) { + my $user = Arlo_decrypt($a[3]); + my $passwd = Arlo_decrypt($a[4]); + $hash->{helper}{username} = $user; + $hash->{helper}{password} = $passwd; + $modules{$MODULE}{defptr}{"account"} = $hash; + + my $cryptUser = Arlo_encrypt($user); + my $cryptPasswd = Arlo_encrypt($passwd); + $hash->{DEF} = "ACCOUNT $cryptUser $cryptPasswd"; + InternalTimer(gettimeofday() + 3, "Arlo_Login", $hash); + + } elsif ($subtype eq 'BASESTATION' && @a == 5) { + my $serialNumber = $a[3]; + my $xCloudId = $a[4]; + my $d = $modules{$MODULE}{defptr}{"B$serialNumber"}; + return "basestation $serialNumber already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); + + Arlo_InitDevice($hash, 'B', $serialNumber, $xCloudId); + InternalTimer(gettimeofday() + 5, "Arlo_Subscribe", $hash); + + } elsif ($subtype eq 'BRIDGE' && @a == 5) { + my $serialNumber = $a[3]; + my $xCloudId = $a[4]; + my $d = $modules{$MODULE}{defptr}{"B$serialNumber"}; + return "bridge $serialNumber already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); + + Arlo_InitDevice($hash, 'B', $serialNumber, $xCloudId); + + } elsif ($subtype eq 'BABYCAM' && @a == 5) { # ArloQ = Kamera mit integrierter Basestation + my $serialNumber = $a[3]; + my $xCloudId = $a[4]; + my $d = $modules{$MODULE}{defptr}{"B$serialNumber"}; + return "BabyCam $serialNumber already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); + + Arlo_InitDevice($hash, 'B', $serialNumber, $xCloudId); + Arlo_InitDevice($hash, 'C', $serialNumber, $xCloudId, $serialNumber); + InternalTimer(gettimeofday() + 5, "Arlo_Subscribe", $hash); + + } elsif ($subtype eq 'CAMERA' && @a == 6) { + my $basestationSerialNumber = $a[3]; + my $serialNumber = $a[4]; + my $xCloudId = $a[5]; + my $d = $modules{$MODULE}{defptr}{"C$serialNumber"}; + return "camera $serialNumber already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); + + Arlo_InitDevice($hash, 'C', $serialNumber, $xCloudId, $basestationSerialNumber); + + } elsif ($subtype eq 'ARLOQ' && @a == 5) { # ArloQ = Kamera mit integrierter Basestation + my $serialNumber = $a[3]; + my $xCloudId = $a[4]; + my $d = $modules{$MODULE}{defptr}{"B$serialNumber"}; + return "ArloQ $serialNumber already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); + + Arlo_InitDevice($hash, 'B', $serialNumber, $xCloudId); + Arlo_InitDevice($hash, 'C', $serialNumber, $xCloudId, $serialNumber); + InternalTimer(gettimeofday() + 5, "Arlo_Subscribe", $hash); + + } elsif ($subtype eq 'LIGHT' && @a == 6) { + my $basestationSerialNumber = $a[3]; + my $serialNumber = $a[4]; + my $xCloudId = $a[5]; + my $d = $modules{$MODULE}{defptr}{"L$serialNumber"}; + return "Arlo light $serialNumber already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); + + Arlo_InitDevice($hash, 'L', $serialNumber, $xCloudId, $basestationSerialNumber); + + } else { + return "Usage: define Arlo ACCOUNT username password\ + define Arlo BASESTATION deviceName serialNumber xCloudId\ + define Arlo CAMERA basestationSerialNumber deviceName serialNumber xCloudId"; + } + + $hash->{NAME} = $name; + $hash->{SUBTYPE} = $subtype; + $hash->{STATE} = 'initialized'; + + $attr{$name}{room} = "Arlo"; + + return undef; +} + +sub Arlo_InitDevice($$$$;$) { + my ($hash, $prefix, $serialNumber, $xCloudId, $basestationSerialNumber) = @_; + if (defined($basestationSerialNumber)) { + my $basestation = $modules{$MODULE}{defptr}{"B$basestationSerialNumber"}; + $hash->{BASESTATION} = $basestation; + $hash->{basestationSerialNumber} = $basestationSerialNumber; + } + $hash->{serialNumber} = $serialNumber; + $hash->{xCloudId} = $xCloudId; + $modules{$MODULE}{defptr}{"$prefix$serialNumber"} = $hash; +} + +sub Arlo_Undef($$) { + my ($hash, $arg) = @_; + my $subtype = $hash->{SUBTYPE}; + 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}{"B$hash->{serialNumber}"}) if ($subtype eq 'BASESTATION' || $subtype eq 'BRIDGE' || $subtype eq 'ARLOQ' || $subtype eq 'BABYCAM'); + if ($subtype eq 'ACCOUNT') { + delete($modules{$MODULE}{defptr}{'account'}); + Arlo_Logout($hash); + } + RemoveInternalTimer($hash); + return undef; +} + +sub Arlo_Attr($$$) { + my ($cmd, $name, $attrName, $attrVal) = @_; + my $hash = $defs{$name}; + return undef if (!defined($hash)); + + if ($hash->{SUBTYPE} eq 'ACCOUNT') { + if ($attrName eq 'disable') { + RemoveInternalTimer($hash); + if ($cmd eq 'del') { + Arlo_Login($hash); + $hash->{STATE} = 'active'; + } else { + Arlo_Logout($hash); + $hash->{STATE} = 'disabled'; + } + } + } + + return undef; +} + +sub Arlo_Set($) { + my ($hash, @a) = @_; + return "\"set X\" needs at least an argument" if ( @a < 2 ); + my $name = shift @a; + my $opt = shift @a; + my $value = join(' ', @a); + my $subtype = $hash->{SUBTYPE}; + + if ($subtype eq 'ACCOUNT') { + if ($opt eq 'autocreate') { + Arlo_CreateDevices($hash); + } elsif ($opt eq 'reconnect') { + Arlo_Login($hash); + } elsif ($opt eq 'readModes') { + Arlo_ReadModes($hash); + } elsif ($opt eq 'updateReadings') { + Arlo_UpdateReadings($hash); + } else { + return "Unknown argument $opt, choose one of autocreate:noArg readModes:noArg reconnect:noArg updateReadings:noArg "; + } + } elsif ($subtype eq 'BASESTATION') { + 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"; + } + } elsif ($subtype eq 'BRIDGE') { + if (!Arlo_SetBasestationCmd($hash, $opt, $value)) { + return "Unknown argument $opt, choose one of arm:noArg disarm:noArg mode"; + } + } elsif ($subtype eq 'LIGHT') { + if ($opt eq 'on') { + Arlo_SetLightState($hash, 'on'); + } elsif ($opt eq 'off') { + Arlo_SetLightState($hash, 'off'); + } else { + return "Unknown argument $opt, choose one of on:noArg off:noArg"; + } + } elsif ($subtype eq 'BRIDGE') { + if (!Arlo_SetBasestationCmd($hash, $opt, $value)) { + return "Unknown argument $opt, choose one of arm:noArg disarm:noArg mode"; + } + } elsif ($subtype eq 'ARLOQ') { + if (!Arlo_SetBasestationCmd($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"; + } + } + } elsif ($subtype eq 'BABYCAM') { + if (!Arlo_SetBasestationCmd($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"; + } + } + } else { + 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 undef; +} + +sub Arlo_SetBasestationCmd($$$) { + my ($hash, $opt, $value) = @_; + if ($opt eq 'mode') { + Arlo_SetBasestationMode($hash, $value); + } elsif ($opt eq 'siren') { + Arlo_SetBasestationSiren($hash, $value); + } elsif ($opt eq 'arm') { + Arlo_BasestationArm($hash); + } elsif ($opt eq 'disarm') { + Arlo_BasestationDisarm($hash); + } elsif ($opt eq 'subscribe') { + Arlo_Subscribe($hash); + } elsif ($opt eq 'unsubscribe') { + Arlo_Unsubscribe($hash); + } else { + return undef; + } + return 1; +} + +sub Arlo_SetCameraCmd($$$) { + my ($hash, $opt, $value) = @_; + if ($opt eq 'on' || $opt eq 'off') { + Arlo_ToggleCamera($hash, $opt); + } elsif ($opt eq 'snapshot') { + Arlo_Snapshot($hash); + } elsif ($opt eq 'startRecording') { + Arlo_StartRecording($hash); + } elsif ($opt eq 'stopRecording') { + Arlo_CameraAction($hash, 'stopRecord'); + } elsif ($opt eq 'brightness') { + Arlo_SetBrightness($hash, $value); + } elsif ($opt eq 'nightlight') { + Arlo_SetNightLight($hash, $value); + } elsif ($opt eq 'nightlight-brightness') { + Arlo_SetNightLightBrightness($hash, $value); + } elsif ($opt eq 'nightlight-color') { + Arlo_SetNightLightColor($hash, $value); + } else { + return undef; + } + return 1; +} + +sub Arlo_Get($) { + my ($hash, @a) = @_; + my $subtype = $hash->{SUBTYPE}; + my $cmd = $a[1]; + return "Unknown argument $cmd, choose one of" if ($subtype ne 'CAMERA'); + + my $date = $a[2]; + 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 Arlo_GetRecordings($hash, $date); +} + +sub Arlo_Poll($) { + my ($hash) = @_; + return undef if (AttrVal($hash->{NAME}, 'disable', 0) eq '1' || $hash->{SUBTYPE} ne 'ACCOUNT'); + my $delay = AttrVal($hash->{NAME}, 'updateInterval', 3600); + eval { + Arlo_UpdateReadings($hash); + }; + if ($@) { + Log3 $hash->{NAME}, 2, "Error while update Arlo readings. Try again in $delay seconds."; + } else { + Log3 $hash->{NAME}, 3, "Updated Arlo readings. Next automatic update in $delay seconds."; + } + InternalTimer(gettimeofday() + $delay, 'Arlo_Poll', $hash); +} + +sub Arlo_GetBasestations($) { + my ($hash) = @_; + my @devices = (); + foreach my $key (keys %{$modules{$MODULE}{defptr}}) { + if (substr($key, 0, 1) eq 'B') { + my $serialNumber = substr($key, 1); + my $device = $modules{$MODULE}{defptr}{"B$serialNumber"}; + push @devices, $serialNumber; + } + } + return @devices; +} + +sub Arlo_Ping($) { + my ($hash) = @_; + my $delay = AttrVal($hash->{NAME}, 'pingInterval', 90); + Log3 $hash->{NAME}, 5, "Arlo Ping: $hash->{NAME} $hash->{SUBTYPE} $delay"; + return undef if (AttrVal($hash->{NAME}, 'disable', 0) eq '1' || $hash->{SUBTYPE} ne 'ACCOUNT'); + if ($hash->{SSE_STATUS} == 200) { + Arlo_SubscribeAll($hash); + } + InternalTimer(gettimeofday() + $delay, 'Arlo_Ping', $hash); +} + +sub Arlo_SetReading($$$$) { + my ($hash, $serialNumber, $name, $value) = @_; + if (defined($value)) { + my $device = $modules{$MODULE}{defptr}{"C$serialNumber"}; + $device = $modules{$MODULE}{defptr}{"B$serialNumber"} if (!defined($device)); + $device = $modules{$MODULE}{defptr}{"L$serialNumber"} if (!defined($device)); + if (defined($device)) { + readingsSingleUpdate($device, $name, $value, 1); + if (($name eq 'state' or $name eq 'activityState') and ReadingsVal($device->{NAME}, 'error', '') ne '') { + fhem("deletereading $device->{NAME} error"); + } + if ($name eq 'activityState' and $value eq 'idle' and defined($device->{streamURL})) { + delete $device->{streamURL}; + } + } + } +} + +sub Arlo_SetReadingAndDownload($$$$$$) { + my ($hash, $reading, $url, $cameraId, $fileSuffix, $recording) = @_; + my $name = $hash->{NAME}; + if (defined($url)) { + my $downloadDir = AttrVal($name, 'downloadDir', ''); + my $downloadLink = AttrVal($name, 'downloadLink', ''); + if ($downloadDir ne '') { + my $ua = LWP::UserAgent->new(); + my $fileName = $downloadDir.'/'.$cameraId.$fileSuffix; + if (!open(FH, ">$fileName")) { + Log3 $name, 1, "Arlo can't write file $fileName!"; + return; + } + my $response = $ua->get($url, ':content_cb' => \&Arlo_WriteResponseToFile, ':read_size_hint' => 65536); + close(FH); + } + if ($downloadLink ne '' && $downloadDir ne '') { + Arlo_SetReading($hash, $cameraId, $reading, $downloadLink.'/'.$cameraId.$fileSuffix); + } else { + Arlo_SetReading($hash, $cameraId, $reading, $url); + } + } +} + +sub Arlo_WriteResponseToFile($$) { + my($data, $response) = @_; + print FH $data; +} + +sub Arlo_Camera_Readings($$$$$$) { + my ($hash, $serialNumber, $state, $batteryLevel, $signalStrength, $brightness) = @_; + Log3 $hash->{NAME}, 5, "Update readings for Arlo Device $serialNumber: state=$state batteryLevel=$batteryLevel"; + my $cam = $modules{$MODULE}{defptr}{"C$serialNumber"}; + if (defined($cam)) { + readingsBeginUpdate($cam); + readingsBulkUpdate($cam, 'batteryLevel', $batteryLevel * 1); + readingsBulkUpdate($cam, 'brightness', $brightness); + readingsBulkUpdate($cam, 'signalStrength', $signalStrength * 1); + readingsBulkUpdate($cam, 'state', $state) if ($cam->{basestationSerialNumber} ne $cam->{serialNumber}); + readingsBulkUpdate($cam, 'error', '') if (ReadingsVal($cam->{NAME}, 'error', '') ne ''); + readingsEndUpdate($cam, 1); + Log3 $hash->{NAME}, 5, "Update readings for device $cam->{NAME} finished."; + } +} + +sub Arlo_Event($$) { + my ($hash, $args) = @_; + my @a = split("[ \t][ \t]*", $args); + my $serialNumber = shift @a; + my $key = shift @a; + my $value = shift @a; + my $cam = $modules{$MODULE}{defptr}{"C$serialNumber"}; + readingsSingleUpdate($cam, $key, $value, 1); +} + + +# +# Schnittstelle zur Arlo-Cloud - Aktionen +# + +sub Arlo_PrepareRequest($$;$$$$) { + my ($hash, $urlSuffix, $method, $body, $additionalHeader) = @_; + $method = "GET" if (!defined($method)); + + my $account = $modules{$MODULE}{defptr}{"account"}; + my $name = $account->{NAME}; + my $cookies = $account->{helper}{cookies}; + 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. + "\r\nschemaVersion: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0"; + $headers = $headers."\r\n".$additionalHeader if (defined($additionalHeader)); + Log3 $name, 5, "Header: $headers"; + + my $url = 'https://arlo.netgear.com/hmsweb'.$urlSuffix; + Log3 $name, 5, "URL: $url"; + + my $request = {url => $url, method => $method, header => $headers, host => 'arlo.netgear.com'}; + + if (defined($body)) { + my $bodyJson = encode_json $body; + Log3 $name, 5, "Body: $bodyJson"; + $request->{data} = $bodyJson; + } + + return $request; +} + +sub Arlo_Request($$;$$$$$) { + my ($hash, $urlSuffix, $method, $body, $additionalHeader, $callback) = @_; + my $request = Arlo_PrepareRequest($hash, $urlSuffix, $method, $body, $additionalHeader); + + if (defined($callback)) { + $request->{callback} = $callback; + } else { + $request->{callback} = \&Arlo_DefaultCallback; + } + + HttpUtils_NonblockingGet($request); +} + +sub Arlo_BlockingRequest($$;$$$$) { + my ($hash, $urlSuffix, $method, $body, $additionalHeader) = @_; + my $request = Arlo_PrepareRequest($hash, $urlSuffix, $method, $body, $additionalHeader); + return HttpUtils_BlockingGet($request); +} + +sub Arlo_DefaultCallback($$$) { + my ($hash, $err, $jsonData) = @_; + my $name = $modules{$MODULE}{defptr}{"account"}{NAME}; + if ($err) { + Log3 $name, 2, "Error occured when calling Arlo daemon: $err"; + return undef; + } elsif ($jsonData) { + my $response; + eval { + $response = decode_json $jsonData; + if ($response->{success}) { + Log3 $name, 5, "Response from Arlo: $jsonData"; + } else { + Log3 $name, 2, "Arlo call was not successful: $jsonData"; + $response = undef; + } + }; + if ($@) { + Log3 $hash->{NAME}, 3, 'Invalid Arlo callback response: '.$jsonData; + } + return $response; + } +} + + +sub Arlo_CreateDevices($) { + my ($hash) = @_; + Arlo_Request($hash, '/users/devices', 'GET', undef, undef, \&Arlo_CreateDevicesCallback); +} + +sub Arlo_CreateDevice($$$$$$;$) { + my ($hash, $deviceType, $prefix, $deviceName, $serialNumber, $xCloudId, $basestationSerialNumber) = @_; + my $d = $modules{$MODULE}{defptr}{"$prefix$serialNumber"}; + if (!defined($d)) { + if (defined($basestationSerialNumber)) { + CommandDefine(undef,"Arlo_$deviceName Arlo $deviceType $basestationSerialNumber $serialNumber $xCloudId"); + } else { + CommandDefine(undef,"Arlo_$deviceName Arlo $deviceType $serialNumber $xCloudId"); + } + } +} + +sub Arlo_GetNameWithoutUmlaut($) { + my ($string) = @_; + my %umlaute = ('ä' => 'ae', 'Ä' => 'Ae', 'ü' => 'ue', 'Ü' => 'Ue', 'ö' => 'oe', 'Ö' => 'Oe', 'ß' => 'ss', ' ' => '_' ); + my $umlautkeys = join ('|', keys(%umlaute)); + $string =~ s/($umlautkeys)/$umlaute{$1}/g; + return $string; +} + +sub Arlo_CreateDevicesCallback($$$) { + my ($hash, $err, $jsonData) = @_; + my $response = Arlo_DefaultCallback($hash, $err, $jsonData); + if (defined($response)) { + my @data = @{$response->{data}}; + foreach my $device (@data) { + my $serialNumber = $device->{deviceId}; + my $deviceName = $device->{deviceName}; + $deviceName = Arlo_GetNameWithoutUmlaut($deviceName); + my $deviceType = $device->{deviceType}; + my $xCloudId = $device->{xCloudId}; + Log3 $hash->{NAME}, 3, "Found device $deviceType with name $deviceName."; + if ($deviceType eq 'basestation') { + Arlo_CreateDevice($hash, 'BASESTATION', 'B', $deviceName, $serialNumber, $xCloudId); + } elsif ($deviceType eq 'arlobridge') { + Arlo_CreateDevice($hash, 'BRIDGE', 'B', $deviceName, $serialNumber, $xCloudId); + } elsif ($deviceType eq 'camera') { + my $parentId = $device->{parentId}; + if ($serialNumber ne $parentId) { + Arlo_CreateDevice($hash, 'CAMERA', 'C', $deviceName, $serialNumber, $xCloudId, $parentId); + } else { + Arlo_CreateDevice($hash, 'BABYCAM', 'B', $deviceName, $serialNumber, $xCloudId); + } + } elsif ($deviceType eq 'arloq') { + Arlo_CreateDevice($hash, 'ARLOQ', 'B', $deviceName, $serialNumber, $xCloudId); + } elsif ($deviceType eq 'lights') { + Arlo_CreateDevice($hash, 'LIGHT', 'L', $deviceName, $serialNumber, $xCloudId, $device->{parentId}); + } + } + } +} + + +sub Arlo_GenTransId() { + my $now = int(gettimeofday() * 1000); + my $random = unpack 'H*', pack 'd', rand(300000) * exp(32); + my $transId = 'web!'.$random."!".$now; + return $transId +} + + +sub Arlo_PreparePostRequest($$) { + my ($device, $body) = @_; + my $account = $modules{$device->{TYPE}}{defptr}{"account"}; + + my $userId = $account->{helper}{userId}; + my $deviceId = $device->{serialNumber}; + my $xCloudId = $device->{xCloudId}; + + my $transId = Arlo_GenTransId(); + + $body->{transId} = $transId; + $body->{from} = $userId.'_web'; + $body->{to} = $deviceId; + + return ($account, $deviceId, $xCloudId); +} + +sub Arlo_Notify($$;$) { + my ($hash, $body, $callback) = @_; + my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $body); + Log3 $account->{NAME}, 4, "Notify $deviceId, action: $body->{action} $body->{resource}"; + Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', $body, 'xcloudId: '.$xCloudId, $callback); +} + +sub Arlo_Subscribe($) { + my ($hash) = @_; + my $account = $modules{$MODULE}{defptr}{'account'}; + my $userId = $account->{helper}{userId}; + my $basestationId = $hash->{serialNumber}; + my @devices = ($basestationId); + my $props = {devices => \@devices}; + my $body = {action => 'set', resource => 'subscriptions/'.$userId.'_web', publishResponse => \0, properties => $props}; + if (!defined($account->{RESPONSE_TIMEOUT})) { + $account->{RESPONSE_TIMEOUT} = gettimeofday() + 30; + } + Arlo_Notify($hash, $body); +} + +sub Arlo_SubscribeAll($) { + my ($hash) = @_; + my @basestations = Arlo_GetBasestations($hash); + foreach my $serialNumber (@basestations) { + my $device = $modules{$MODULE}{defptr}{"B$serialNumber"}; + Arlo_Subscribe($device); + } +} + +sub Arlo_Unsubscribe($) { + my ($hash) = @_; + Arlo_Request($hash, '/client/unsubscribe'); +} + +sub Arlo_ReadModes($) { + my ($hash) = @_; + Arlo_Request($hash, '/users/automation/definitions?uniqueIds=all', 'GET', undef, undef, \&Arlo_ReadModesCallback); +}; + +sub Arlo_ReadModesCallback($$$) { + my ($hash, $err, $jsonData) = @_; + my $response = Arlo_DefaultCallback($hash, $err, $jsonData); + if (defined($response)) { + foreach my $key (keys %{$response->{data}}) { + if (defined($response->{data}{$key}{modes})) { + my $account = $modules{$MODULE}{defptr}{'account'}; + my @modes = @{$response->{data}{$key}{modes}}; + foreach my $mode (@modes) { + my $id = $mode->{id}; + my $bsMode = {name => $mode->{name}, type => $mode->{type}}; + $account->{MODES}{$id} = $bsMode; + } + } + } + } +} + +sub Arlo_ReadCamerasAndLights($) { + my ($hash) = @_; + my $cam = {action => 'get', resource => 'cameras', publishResponse => \0}; + my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $cam); + my $lights = {action => 'get', resource => 'lights', publishResponse => \0}; + Arlo_PreparePostRequest($hash, $lights); + my @body = ($cam, $lights); + if ($hash->{basestationSerialNumber} eq $hash->{serialNumber}) { + my $mode = {action => 'get', resource => 'modes', publishResponse => \0}; + my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $mode); + push @body, $mode; + } + Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', \@body, 'xcloudId: '.$xCloudId); +} + +sub Arlo_UpdateReadings($) { + my ($hash) = @_; + Arlo_Request($hash, '/users/devices/automation/active', 'GET', undef, undef, \&Arlo_UpdateReadingsCallback); + my @basestations = Arlo_GetBasestations($hash); + my $delay = 2; + foreach my $serialNumber (@basestations) { + my $device = $modules{$MODULE}{defptr}{"B$serialNumber"}; + InternalTimer(gettimeofday() + $delay, "Arlo_ReadCamerasAndLights", $device); + $delay += 2; + } +} + +sub Arlo_UpdateReadingsCallback($$$) { + my ($hash, $err, $jsonData) = @_; + my $response = Arlo_DefaultCallback($hash, $err, $jsonData); + if (defined($response) && defined($response->{data})) { + my @data = @{$response->{data}}; + foreach my $event (@data) { + if ($event->{type} eq 'activeAutomations') { + my $serialNumber = $event->{gatewayId}; + my @activeModes = @{$event->{activeModes}}; + if (@activeModes > 0) { + Arlo_SetModeReading($serialNumber, $activeModes[0]); + } + } + } + } +} + +sub Arlo_SetModeReading($$) { + my ($serialNumber, $mode) = @_; + my $basestation = $modules{$MODULE}{defptr}{"B$serialNumber"}; + if (defined($mode) && defined($basestation)) { + my $modeName; + my $account = $modules{$MODULE}{defptr}{"account"}; + if (defined($account->{MODES})) { + $modeName = $account->{MODES}{$mode}{name}; + $modeName = $account->{MODES}{$mode}{type} if (!defined($modeName) || $modeName eq ''); + } else { + if ($mode eq 'mode0') { + $modeName = 'disarmed'; + } elsif ($mode eq 'mode1') { + $modeName = 'armed'; + } else { + InternalTimer(gettimeofday() + 1, "Arlo_ReadModes", $basestation); + } + } + $modeName = $mode if (!defined($modeName) || $modeName eq ''); + readingsSingleUpdate($basestation, 'state', $modeName, 1); + } +} + +sub Arlo_BasestationArm($) { + my ($hash) = @_; + Arlo_DoSetBasestationMode($hash, 'mode1') + +} + +sub Arlo_BasestationDisarm($) { + my ($hash) = @_; + Arlo_DoSetBasestationMode($hash, 'mode0') +} + +sub Arlo_SetBasestationMode($$) { + my ($hash, $modeName) = @_; + my $account = $modules{$MODULE}{defptr}{"account"}; + if (defined($account->{MODES})) { + foreach my $id (keys %{$account->{MODES}}) { + my $nameOfId = $account->{MODES}{$id}{name}; + if ($modeName ne '' && $modeName eq $nameOfId) { + Log3 $hash->{NAME}, 3, "Set Arlo basestation mode to $id"; + Arlo_DoSetBasestationMode($hash, $id); + } + } + } else { + Log3 $hash->{NAME}, 2, "Could not set arlo mode $modeName, because modes for basestation have to be loaded. Please try a again in a few seconds."; + Arlo_ReadModes($hash); + } +} + +sub Arlo_DoSetBasestationMode($$) { + my ($hash, $mode) = @_; + if (defined($hash->{basestationSerialNumber})) { # Kamera mit integrierter Basestation + my $props = {active => $mode}; + my $body = {action => 'set', resource => 'modes', publishResponse => \1, properties => $props}; + Arlo_Notify($hash, $body); + } else { + my @modes = ($mode); + my @schedules = (); + my $now = int(gettimeofday() * 1000); + my $automation = {deviceId => $hash->{serialNumber}, timestamp => $now, activeModes => \@modes, activeSchedules => \@schedules}; + my @automations = ($automation); + my $body = { activeAutomations => \@automations }; + Arlo_Request($hash, '/users/devices/automation/active', 'POST', $body); + } +} + +sub Arlo_SetBasestationSiren($$) { + my ($hash, $state) = @_; + my $props = {sirenState => $state, duration => 300, volume => 8, pattern => 'alarm'}; + my $body = {action => 'set', resource => 'siren', publishResponse => \1, properties => $props}; + Arlo_Notify($hash, $body); +} + +sub Arlo_GetBasestationForCamera($) { + my ($hash) = @_; + my $serialNumber = $hash->{basestationSerialNumber}; + return $modules{$MODULE}{defptr}{"B$serialNumber"}; +} + +sub Arlo_ToggleCamera($$) { + my ($hash, $newState) = @_; + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $privacyActive = $newState eq 'off' ? \1 : \0; + my $props = {privacyActive => $privacyActive}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + Arlo_Notify($basestation, $body); +} + +sub Arlo_Snapshot($) { + my ($hash) = @_; + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $props = {activityState => 'fullFrameSnapshot'}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + my ($account, $basestationId, $xCloudId) = Arlo_PreparePostRequest($basestation, $body); + Log3 $account->{NAME}, 4, "Take snapshot for camera $cameraId."; + Arlo_Request($account, '/users/devices/fullFrameSnapshot', 'POST', $body, 'xcloudId: '.$xCloudId); +} + +sub Arlo_StartRecording($) { + my ($hash) = @_; + my $activityState = ReadingsVal($hash->{NAME}, 'activityState', 'idle') ; + if ($activityState eq 'userStreamActive' && defined($hash->{streamURL})) { + Arlo_Subscribe($hash); + return "Camera is still recording."; + } else { + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $props = {activityState => 'startUserStream', camera => $cameraId}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + my ($account, $basestationId, $xCloudId) = Arlo_PreparePostRequest($basestation, $body); + Log3 $account->{NAME}, 4, "Start streaming for camera $cameraId."; + $hash->{FOLLOW_CALL} = 'startRecord'; + Arlo_Request($account, '/users/devices/startStream', 'POST', $body, 'xcloudId: '.$xCloudId); + InternalTimer(gettimeofday() + 10, "Arlo_CheckStreamStart", $hash); + } + return undef; +} + +sub Arlo_StartRecordingStep2($) { + my ($hash) = @_; + if (defined($hash->{FOLLOW_CALL})) { + Arlo_CameraAction($hash, 'startRecord'); + delete($hash->{FOLLOW_CALL}); + } +} + +sub Arlo_CheckStreamStart($) { + my ($hash) = @_; + if (defined($hash->{FOLLOW_CALL})) { + readingsSingleUpdate($hash,'activityState', 'timeout', 1); + delete($hash->{FOLLOW_CALL}); + } +} + +sub Arlo_CameraAction($$) { + my ($hash, $action) = @_; + my $account = $modules{$MODULE}{defptr}{"account"}; + my $xCloudId = $hash->{xCloudId}; + my $cameraId = $hash->{serialNumber}; + my $basestationId = $hash->{basestationSerialNumber}; + my $body = {xcloudId => $xCloudId, parentId => $basestationId, deviceId => $cameraId, olsonTimeZone => 'Europe/Berlin'}; + Log3 $account->{NAME}, 4, "Action $action for camera $cameraId."; + Arlo_Request($account, '/users/devices/'.$action, 'POST', $body, 'xcloudId: '.$xCloudId); +} + +sub Arlo_SetBrightness($$) { + my ($hash, $brightness) = @_; + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $props = {brightness => ($brightness + 0)}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + Arlo_Notify($basestation, $body); +} + +sub Arlo_SetNightLight($$) { + my ($hash, $state) = @_; + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $enabled = \0; + $enabled = \1 if ($state eq 'on'); + my $nightLight = {enabled => $enabled}; + my $props = {nightLight => $nightLight}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + Arlo_Notify($basestation, $body); +} + +sub Arlo_SetNightLightBrightness($$) { + my ($hash, $brightness) = @_; + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $nightLight = {brightness => ($brightness + 0)}; + my $props = {nightLight => $nightLight}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + Arlo_Notify($basestation, $body); +} + +sub Arlo_GetValueBetweenBrackets($) { + my ($cmd) = @_; + my $pb = index($cmd, '('); + my $pe = index($cmd, ')'); + return substr($cmd, $pb + 1, $pe - $pb - 1); +} + +sub Arlo_SetNightLightColor($$) { + my ($hash, $color) = @_; + my $nightLight; + if (substr($color, 0, 5) eq 'white') { + $color = Arlo_GetValueBetweenBrackets($color); + $nightLight = { mode => 'temperature', temperature => ($color + 0)}; + } elsif (substr($color, 0, 3) eq 'rgb') { + $color = Arlo_GetValueBetweenBrackets($color); + my $p1 = index($color, ','); + my $p2 = index($color, ',', $p1 + 1); + return "Invalid format. Please use rgb(red,green,blue)." if ($p1 <= 0 || $p2 <= 0); + my $red = substr($color, 0, $p1) + 0; + my $green = substr($color, $p1 + 1, $p2 - $p1 - 1) + 0; + my $blue = substr($color, $p2 + 1) + 0; + $nightLight = { mode => 'rgb', rgb => { red => $red, green => $green, blue => $blue }}; + } else { + return "Invalid format. Please use white(temperature) or rgb(red,green,blue)."; + } + + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $props = {nightLight => $nightLight}; + my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; + Arlo_Notify($basestation, $body); +} + +sub Arlo_SetLightState($$) { + my ($hash, $state) = @_; + my $basestation = Arlo_GetBasestationForCamera($hash); + my $cameraId = $hash->{serialNumber}; + my $props = {lampState => $state}; + my $body = {action => 'set', resource => "lights/$cameraId", publishResponse => \1, properties => $props}; + Arlo_Notify($basestation, $body); +} + +sub Arlo_GetRecordings($$) { + my ($hash, $date) = @_; + my $body = {dateFrom => $date, dateTo => $date}; + my ($err, $jsonData) = Arlo_BlockingRequest($hash, '/users/library', 'POST', $body); + my $response = Arlo_DefaultCallback($hash, $err, $jsonData); + my @result = (); + my $deviceId = $hash->{serialNumber}; + if (defined($response)) { + my @data = @{$response->{data}}; + foreach my $r (@data) { + if ($deviceId eq $r->{deviceId}) { + my $rec = { time => $r->{localCreatedDate}, video => $r->{presignedContentUrl}, thumbnail => $r->{presignedThumbnailUrl} }; + push @result, $rec; + } + } + + } + return encode_json(\@result); +} + +# +# Login and Event-Polling +# + +sub Arlo_Login($) { + my ($hash) = @_; + RemoveInternalTimer($hash); + my $name = $hash->{NAME}; + + if (AttrVal($name, 'disable', 0) == 1) { + return; + } + + my $input = {email => $hash->{helper}{username}, password => $hash->{helper}{password}}; + my $postData = encode_json $input; + my $header = ['Content-Type' => 'application/json; charset=utf-8']; + + 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 $req = HTTP::Request->new('POST', 'https://arlo.netgear.com/hmsweb/login/v2', $header, $postData); + my $resp = $ua->request($req); + + if ($resp->is_success) { + eval { + my $respObj = decode_json $resp->decoded_content; + if ($respObj->{success}) { + my $data = $respObj->{data}; + $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->{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; + } else { + Log3 $name, 2, 'Arlo Login not successful: '.$resp->decoded_content; + } + }; + if ($@) { + Log3 $hash->{NAME}, 2, 'Invalid Arlo response for login request: '.$resp->decoded_content; + } + } else { + my $status_line = $resp->status_line; + if ($status_line =~ /401/) { + delete $hash->{RETRY}; + Log3 $name, 2, 'Arlo Login not successful, wrong username or password.'; + } else { + Log3 $name, 2, 'Arlo Login not successful, status $status_line'; + } + } + if (defined($hash->{RETRY})) { + Log3 $name, 3, 'Retry Arlo Login in 60 seconds.s'; + InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash); + } +} + +sub Arlo_Logout($) { + my ($hash) = @_; + Arlo_Request($hash, '/logout', 'PUT'); +} + +sub Arlo_GetCookies($) { + my ($cookie_jar) = @_; + my $result = ''; + $cookie_jar->scan(sub { + $result .= '; ' if ($result ne ''); + $result .= $_[1]."=".$_[2]; + }); + return $result; +} + +sub Arlo_EventQueue($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $cookies = $hash->{helper}{cookies}; + my $token = $hash->{helper}{token}; + delete $hash->{RESPONSE_TIMEOUT}; + + my $headers = "Authorization: ".$token."\r\nAccept: text/event-stream\r\nReferer: https://arlo.netgear.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; + my $con = {url => 'https://arlo.netgear.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'arlo.netgear.com', httpversion => '1.1'}; + my $err = HttpUtils_Connect($con); + if ($err) { + Log3 $name, 2, "Error in Arlo event queue: $err"; + if ($hash->{SSE_STATUS} == 0) { # aus EventPolling heraus gestartet + Log3 $name, 3, "Try to restart Arlo event listener in 60 seconds."; + InternalTimer(gettimeofday() + 60, "Arlo_EventQueue", $hash); + } + return; + } + + delete($con->{httpdatalen}); + delete($con->{httpheader}); + + $hash->{helper}{con} = $con; + Log3 $name, 2, "(Re)starting Arlo event listener."; + Arlo_EventPolling($hash); +} + +sub Arlo_EventPolling($) { + my ($hash) = @_; + my $con = $hash->{helper}{con}; + my $name = $hash->{NAME}; + + if (!defined($con->{conn})) { + Log3 $name, 3, "Arlo connection not defined, stop event polling."; + return; + } + + my ($rout, $rin) = ('', ''); + vec($rin, $con->{conn}->fileno(), 1) = 1; + Log3 $name, 5, "Checking for Arlo server response."; + my $nfound = select($rout=$rin, undef, undef, 0.1); + my $buf = ''; + while ($nfound > 0) { + my $len = sysread($con->{conn}, $buf, 65536); + if (!defined($len) || $len <= 0) { + HttpUtils_Close($con); + if ($hash->{SSE_STATUS} == 200) { + $hash->{SSE_STATUS} = 0; + Log3 $name, 3, "Arlo connection stopped, try to restart event listener."; + Arlo_EventQueue($hash); + } else { + Log3 $name, 2, "Stopping Arlo event listener."; + } + return; + } + my $lastChar = substr($buf, length($buf) - 1, 1); + if ($lastChar eq "\n" || $lastChar eq "\r") { + $nfound = 0; + } else { + $nfound = select($rout=$rin, undef, undef, 1); + } + } + Arlo_ProcessResponse($hash, $buf) if ($buf ne ''); + if ($hash->{SSE_STATUS} == 299) { + $hash->{RETRY} = 1; + InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash); + } else { + my $timeout = $hash->{RESPONSE_TIMEOUT}; + if (defined($timeout) && $timeout < gettimeofday()) { + $hash->{SSE_STATUS} = 0; + Log3 $name, 3, "Arlo connection timeout, try to restart event listener."; + HttpUtils_Close($con); + Arlo_EventQueue($hash); + } else { + my $ssePollingInterval = AttrVal($name, 'ssePollingInterval', 2); + InternalTimer(gettimeofday() + $ssePollingInterval, "Arlo_EventPolling", $hash); + } + } +} + + +sub Arlo_ProcessResponse($$) { + my ($hash, $response) = @_; + delete $hash->{RESPONSE_TIMEOUT}; + for my $line (split("\n", $response)) { + if (length($line) > 5) { + my $check = substr($line, 0, 5); + if (($check eq 'data:' && length($line) > 7) || $hash->{helper}{incompleteLine}) { + eval { + if ($hash->{helper}{incompleteLine}) { + $line = join('', $hash->{helper}{incompleteLine}, $line); + } + if (substr($line, 6, 1) eq '{' && substr($line, length($line)-1, 1) eq '}') { + delete $hash->{helper}{incompleteLine}; + my $event = decode_json substr($line, 5); + Log3 $hash->{NAME}, 4, $line; + Arlo_ProcessEvent($hash, $event); + } else { + $line =~ s/\\|\R//g; + $hash->{helper}{incompleteLine} = $line; + } + }; + if ($@) { + Log3 $hash->{NAME}, 2, 'Invalid Arlo JSON response: '.$line; + } + } elsif ($check eq 'HTTP/') { + my $status = substr($line, 9, 3); + $hash->{SSE_STATUS} = $status; + if ($status != 200) { + Log3 $hash->{NAME}, 2, "Arlo event queue error: wrong http status $status."; + } + } elsif ($check eq 'Conne' && substr($line, 0, 11) eq 'Connection:') { + if (index($line, 'keep-alive') < 0) { + Log3 $hash->{NAME}, 2, "Arlo event queue error: connection has no keep-alive header."; + $hash->{SSE_STATUS} = 299; + } + } elsif ($check eq 'Set-C' && substr($line, 0, 11) eq 'Set-Cookie:') { + if (index($line, 'JSESSIONID') > 0) { + Log3 $hash->{NAME}, 2, "Arlo event queue error: session lost."; + $hash->{SSE_STATUS} = 299; + } + } 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') { + Log3 $hash->{NAME}, 2, "Invalid Arlo event response: $line"; + } + } + } +} + +sub Arlo_ProcessEvent($$) { + my ($hash, $event) = @_; + my $name = $hash->{NAME}; + my $resource = $event->{resource}; + my $basestationId = $event->{from}; + if (defined($resource)) { + Log3 $name, 3, "Process Arlo event $resource for $basestationId" if (defined($basestationId)); + Log3 $name, 3, "Process Arlo event $resource" if (!defined($basestationId)); + if ($resource eq 'modes') { + my $props = $event->{properties}; + my $activeMode = $props->{activeMode}; + $activeMode = $props->{active} if (!defined($activeMode)); + Arlo_SetModeReading($basestationId, $activeMode); + } elsif ($resource eq 'cameras') { + my @props = @{$event->{properties}}; + for my $prop (@props) { + my $state = $prop->{privacyActive} ? 'off' : 'on'; + my $cameraId = $prop->{serialNumber}; + my $batteryLevel = $prop->{batteryLevel}; + my $signalStrength = $prop->{signalStrength}; + my $brightness = $prop->{brightness}; + Arlo_Camera_Readings($hash, $cameraId, $state, $batteryLevel, $signalStrength, $brightness); + } + } elsif (substr($resource,0,8) eq 'cameras/') { + my $props = $event->{properties}; + my $cameraId = substr($resource, 8); + my $action = $event->{action}; + if ($action eq 'fullFrameSnapshotAvailable') { + Arlo_SetReading($hash, $cameraId, 'activityState', 'idle'); + Arlo_SetReadingAndDownload($hash, 'snapshotUrl', $props->{presignedFullFrameSnapshotUrl}, $cameraId, '_snapshot.jpg', \0); + } elsif ($action eq 'is') { + Arlo_SetReading($hash, $cameraId, 'chargingState', $props->{chargingState}); + Arlo_SetReading($hash, $cameraId, 'batteryLevel', $props->{batteryLevel}); + Arlo_SetReading($hash, $cameraId, 'signalStrength', $props->{signalStrength}); + Arlo_SetReading($hash, $cameraId, 'brightness', $props->{brightness}); + Arlo_SetReading($hash, $cameraId, 'motionDetected', $props->{motionDetected}); + + my $privacyActive = $props->{privacyActive}; + if (defined($privacyActive)) { + my $state = $privacyActive ? 'off' : 'on'; + Arlo_SetReading($hash, $cameraId, 'state', $state); + } + + my $activityState = $props->{activityState}; + if ($activityState) { + Arlo_SetReading($hash, $cameraId, 'activityState', $activityState); + my $camera = $modules{$MODULE}{defptr}{"C$cameraId"}; + if ($activityState eq 'startUserStream') { + my $streamURL = $props->{streamURL}; + if (defined($streamURL)) { + $camera->{streamURL} = $streamURL =~ s/rtsp:/rtsps:/r; + } + } elsif ($activityState eq 'userStreamActive') { + InternalTimer(gettimeofday() + 0.5, "Arlo_StartRecordingStep2", $camera); + } + } + } + } elsif (substr($resource,0,7) eq 'lights/') { + my $props = $event->{properties}; + my $deviceId = substr($resource, 7); + my $action = $event->{action}; + if ($action eq 'is') { + Arlo_SetReading($hash, $deviceId, 'activityState', $props->{activityState}); + Arlo_SetReading($hash, $deviceId, 'batteryLevel', $props->{batteryLevel}); + Arlo_SetReading($hash, $deviceId, 'chargingState', $props->{chargingState}); + Arlo_SetReading($hash, $deviceId, 'motionDetected', $props->{motionDetected}); + Arlo_SetReading($hash, $deviceId, 'state', $props->{lampState}); + } + } elsif ($resource eq 'mediaUploadNotification') { + my $cameraId = $event->{deviceId}; + Arlo_SetReading($hash, $cameraId, 'activityState', 'idle'); + Arlo_SetReadingAndDownload($hash, 'lastVideoThumbnailUrl', $event->{presignedThumbnailUrl}, $cameraId, '_thumb.jpg', \0); + Arlo_SetReadingAndDownload($hash, 'lastVideoImageUrl', $event->{presignedLastImageUrl}, $cameraId, '.jpg', \0); + Arlo_SetReadingAndDownload($hash, 'lastVideoUrl', $event->{presignedContentUrl}, $cameraId, '.mp4', \1); + } + my $error = $event->{error}; + if ($error) { + Arlo_SetReading($hash, $event->{deviceId}, 'error', $event->{message}); + } + } else { + my $action = $event->{action}; + if (defined($action) && $action eq 'logout') { + Log3 $name, 3, "Received Arlo logout event."; + Arlo_SubscribeAll($hash); + } + } +} + +# +# Helper +# + +sub Arlo_encrypt($) { + my ($decoded) = @_; + my $key = getUniqueId(); + my $encoded; + + return $decoded if( $decoded =~ /crypt:/ ); + + for my $char (split //, $decoded) { + my $encode = chop($key); + $encoded .= sprintf("%.2x",ord($char)^ord($encode)); + $key = $encode.$key; + } + + return 'crypt:'.$encoded; +} + +sub Arlo_decrypt($) { + my ($encoded) = @_; + my $key = getUniqueId(); + my $decoded; + + return $encoded if( $encoded !~ /crypt:/ ); + + $encoded = $1 if( $encoded =~ /crypt:(.*)/ ); + + for my $char (map { pack('C', hex($_)) } ($encoded =~ /(..)/g)) { + my $decode = chop($key); + $decoded .= chr(ord($char)^ord($decode)); + $key = $decode.$key; + } + + return $decoded; +} +1; + +=pod +=item summary Communicates to Arlo cameras +=item summary_DE Kommuniziert mit Arlo Kameras +=begin html + + +

Arlo

+
    +

    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. + Events (like movement and state changes) are delivery by server-sent events (SSE).

    + +

    Define

    +
      + define Arlo_Cloud Arlo ACCOUNT <hans.mustermann@xyz.de> <myPassword> + +

      Please replace hans.mustermann@xyz.de by the e-mail address you have registered at Arlo and myPassword by the password used there.

      + +

      After you have successfully created the account definition, you can call set Arlo_Cloud autocreate. + Now the base station(s) and cameras which are assigned to the Arlo account will be created in FHEM. All new devices are created in the room Arlo.

      + +

      In the background there is a permanent SSE connection to the Arlo server. If you temporary don't use Arlo in FHEM, you can stop this SSE connection by setting + the attribute "disable" at the Arlo_Cloud device to 1.

      +
    + +

    Set

    +
      +
    • autocreate (subtype ACCOUNT)
      + Reads all devices which are assigned to the Arlo account and creates FHEM devices, if the devices don't exist in FHEM. +
    • +
    • reconnect (subtype ACCOUNT)
      + Connect or reconnect to the Arlo server. First FHEM logs in, then a SSE connection is established. This method is only used if the connection + to the Arlo server was interrupted. +
    • +
    • readModes (subtype ACCOUNT)
      + Reads the modes of the base stations (iincl. custom modes). Is called automatically, normally you don't have to call this manually. +
    • +
    • updateReadings (subtype ACCOUNT)
      + The data of all base stations and cameras are retrieved from the Arlo cloud. This is done every hour automatically, if you don't set the + attribute disable=1 at the Cloud device. The interval can be changed by setting the attribute updateInterval (in seconds, e.g. 600 for 10 minutes, 7200 for 2 hours). +
    • +
    • arm (subtype BASESTATION)
      + Activates the motion detection. +
    • +
    • disarm (subtype BASESTATION)
      + Deactivates the motion detection. +
    • +
    • mode (subtype BASESTATION)
      + Set a custom mode (parameter: name of the mode). +
    • +
    • siren (subtype BASESTATION)
      + Activates or deactivates the siren of the base station (attention: the siren is loud!). +
    • +
    • subscribe (subtype BASESTATION)
      + Subscribe base station for the SSE connection. Normally you don't have to do this manually, this is done automatically after login. +
    • +
    • unsubscribe (subtype BASESTATION)
      + Unsubscribe base station for the current SSE connection. +
    • +
    • brightness (subtype CAMERA)
      + Adjust brightness of the camera (possible values: -2 to +2). +
    • +
    • on (Subtype CAMERA)
      + Switch on camera (deactivate privacy mode). +
    • +
    • off (subtype CAMERA)
      + Switch off camera (activate privacy mode). +
    • +
    • snapshot (subtype CAMERA)
      + Take a snapshot. The snapshot url is written to the reading snapshotUrl. This command only works if the camera has the state on. +
    • +
    • startRecording (subtype CAMERA)
      + Start recording. This command only works if the camera has the state on. +
    • +
    • stopRecording (subtype CAMERA)
      + Stops an active recording. The recording url is stored in the reading lastVideoUrl, a frame of the recording in lastVideoImageUrl and a thumbnail of the recording in lastVideoThumbnailUrl. +
    • +
    + +

    Attributes

    + + +

    downloadDir

    +
      + If this attribute is set at the cloud device (subtype ACCOUNT), the files which are stored at the Arlo Cloud (videos / images) will also be downloaded to the given directory. + If you want to access these files via FHEM http you have to use a subdirectory of /opt/fhem/www. + Attention: the fhem user has to have write access to the directory. +
    + +

    downloadLink

    +
      + If the attribute downloadDir is set and the files will be downloaded from Arlo Cloud, you can set this attribute to create a correct URL to the last video, last snapshot and so on. + A correct value is like http://hostname:8083/fhem/subdirectory-of-www +
    + +

    disable

    +
      +

      Subtype ACCOUNT: Deactivates the SSE connection to Arlo Cloud.

      +

      Subtype BASESTATION: Deactivates the periodic update of the readings from Arlo Cloud.

      +
    + +

    pingInterval

    +
      +

      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. + Default is 90.

      +
    + +

    updateInterval

    +
      +

      Subtype ACCOUNT: Set the interval in seconds how often the readings of base statations and cameras will be updated. Default is 3600 = 1 hour.

      +
    + +

    ssePollingInterval

    +
      +

      Subtype ACCOUNT: Set the interval in seconds how often the SSE events are checked. Default is 2 seconds.

      +
    + +
+=end html + +=begin html_DE + + +

Arlo

+
    +

    Arlo Sicherheitskameras von NETGEAR 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.

    + +

    Define

    +
      + define Arlo_Cloud Arlo ACCOUNT <hans.mustermann@xyz.de> <meinPasswort> + +

      hans.mustermann@xyz.de durch die E-Mail-Adresse ersetzen, mit der man bei Arlo registriert ist, meinPasswort durch das Passwort dort.

      + +

      Nach der erfolgreichen Definition des Account kann auf dem neu erzeugten Device set Arlo_Cloud autocreate aufgerufen werden. + Dies legt die Basistation(en) und Kameras an, die zu dem Arlo Account zugeordnet sind. Die neuen Devices befinden sich initial im Raum Arlo.

      + +

      Aufgrund der SSE-Schnittstelle ist es notwendig, dass dauerhaft im Hintergrund eine Verbindung zum Arlo-Server gehalten wird. Falls dies nicht gewünscht ist, + da Arlo z.B. vorübergehend nicht genutzt wird, kann der Hintergrund-Job verhindert werden, indem das Attribut "disable" des Arlo_Cloud-Device auf 1 gesetzt wird.

      +
    + +

    Set

    +
      +
    • autocreate (Subtype ACCOUNT)
      + Liest alle dem Arlo-Account zugeordneten Geräte und legt dafür FHEM-Devices an, falls es diese nicht schon gibt. +
    • +
    • reconnect (Subtype ACCOUNT)
      + Neuaufbau der Verbindung zum Arlo-Server. Zunächst loggt sich FHEM neu bei Arlo ein, danach wird die SSE-Verbindung aufgebaut. Wird nur benötigt, + falls unerwartete Verbindungsabbrüche auftreten. +
    • +
    • readModes (Subtype ACCOUNT)
      + Liest die Modes der Basisstationen (inkl. Custom Modes). Wird automatisch aufgerufen, daher normalerweise kein manueller Aufruf notwendig. +
    • +
    • updateReadings (Subtype ACCOUNT)
      + Aktuelle Daten aller Basisstationen und Kameras aus der Cloud abrufen. Dies passiert einmal stündlich automatisch, falls dies nicht + durch Setzen des Attributes disabled=1 am Cloud-Device unterbunden wird. Den Abruf-Intervall kann man durch Setzen des Attributs updateInterval + anpassen (Angabe von Sekunden, also z.B. 600 für Abruf alle 10 Minuten oder 7200 für Abruf alle 2 Stunden). +
    • +
    • arm (Subtype BASESTATION)
      + Aktivieren der Bewegungserkennung. +
    • +
    • disarm (Subtype BASESTATION)
      + Deaktivieren der Bewegungserkennung. +
    • +
    • mode (Subtype BASESTATION)
      + Setzen eines benutzerdefinierten Modus (Parameter: Name des Modus). +
    • +
    • siren (Subtype BASESTATION)
      + Schaltet die Siren der Basisstation an oder aus (Achtung: laut!!). +
    • +
    • subscribe (Subtype BASESTATION)
      + Basisstation für die SSE-Schnittstelle registrieren. Muss normalerweise nie manuell aufgerufen werden, da dies beim Login automatisch passiert. +
    • +
    • unsubscribe (Subtype BASESTATION)
      + Registrierung einer Basisstation für die SSE-Schnittstelle rückgängig machen. +
    • +
    • brightness (Subtype CAMERA)
      + Helligkeit der Kamera anpassen (mögliche Werte: -2 bis +2). +
    • +
    • on (Subtype CAMERA)
      + Kamera einschalten. +
    • +
    • off (Subtype CAMERA)
      + Kamera ausschalten. +
    • +
    • snapshot (Subtype CAMERA)
      + Ein Standbild aufnehmen. Dieses kann danach über die URL aus dem Reading snapshotUrl aufgerufen werden. Damit der Befehl funktioniert, muss die Kamera den Status on haben. +
    • +
    • startRecording (Subtype CAMERA)
      + Aufnahme starten. Damit der Befehl funktioniert, muss die Kamera den Status on haben. +
    • +
    • stopRecording (Subtype CAMERA)
      + Aufnahme stoppen. Die Aufnahme kann danach über lastVideoUrl abgerufen werden, das Standbild dazu unter lastVideoImageUrl und lastVideoThumbnailUrl (klein). +
    • +
    + +

    Attribute

    + + +

    downloadDir

    +
      + Falls dieses Attribut am Cloud-Device (Subtype ACCOUNT) gesetzt ist, werden Dateien, die in der Arlo Cloud erzeugt werden (Videos / Bilder) in das hier angegebene Verzeichnis heruntergeladen. + Damit man auf die Dateien über http zugreifen kann, muss ein Verzeichnis unterhalb /opt/fhem/www angegeben werden (oder dieses selbst). + Wichtig: der fhem-User muss in in diesem Verzeichnis Schreibrechte haben. +
    + +

    downloadLink

    +
      + Falls über das Attribut downloadDir die Dateien ins lokale Verzeichnis heruntergeladen werden, kann über dieses Attribut am Cloud-Device angegeben werden, dass die in den Kameras gesetzten Links auf die lokale Kopie zeigen sollen. + Die Angabe des Links muss in der Form http://hostname:8083/fhem/unterverzeichnis-unter-www angegeben werden. +
    + +

    disable

    +
      +

      Subtype ACCOUNT: Deaktiviert die Verbindung zur Arlo-Cloud.

      +

      Subtype BASESTATION: Deaktiviert die regelmäßige Abfrage der Readings aus der Arlo Cloud.

      +
    + +

    pingInterval

    +
      +

      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 + es könnten keine Events mehr empfangen werden. Standard ist 90.

      +
    + +

    updateInterval

    +
      +

      Subtype ACCOUNT: Setzt das Intervall in Sekunden, wie häufig die Readings der Basisstationen und Kameras abgefragt werden. Standard ist 3600 = 1 Stunde.

      +
    + +

    ssePollingInterval

    +
      +

      Subtype ACCOUNT: Setzt das Intervall in Sekunden, wie häufig die SSE Events abgefragt werden sollen. Standard ist 2 Sekunden.

      +
    + +
+=end html_DE + +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 044af3bfa..c8852a0e2 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -256,6 +256,7 @@ FHEM/46_TRX_LIGHT.pm KernSani RFXTRX FHEM/46_TRX_SECURITY.pm KernSani RFXTRX FHEM/46_TRX_WEATHER.pm KernSani RFXTRX FHEM/47_OBIS.pm icinger Sonstige Systeme +FHEM/49_Arlo.pm maluk Sonstige Systeme FHEM/49_IPCAM.pm mfr69bs Sonstiges FHEM/49_SSCAM.pm DS_Starter Sonstiges FHEM/49_SSCamSTRM.pm DS_Starter Sonstiges