############################################################################## ############################################################################## # # 48_BlinkCamera.pm # # This file is part of Fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Fhem. If not, see . # # $Id: 48_BlinkCamera.pm 24047 2021-03-21 20:57:48Z viegener $ # ############################################################################## # # BlinkCamera (c) Johannes Viegener / https://github.com/viegener/Telegram-fhem/tree/master/Blink # # This module interacts with Blink Home Cameras : https://blinkforhome.com/ # Some information is based on the work here: https://github.com/MattTW/BlinkMonitorProtocol # (although this was slightly outdated) # # Discussed in FHEM Forum: https://forum.fhem.de/index.php/topic,59719.0.html # # my $repositoryID = '$Id: 48_BlinkCamera.pm 24047 2021-03-21 20:57:48Z viegener $'; # ############################################################################## # ##### API change # Change to new login API - V4 (not containing networks) # getNetworks as new getter # set networks reading to INVALID on reading networks # ensure networks retrieved in docmd before other cmds # parse networks # 2.0 2020-05-13 CheckIn to SVN for regular FHEM update # # use DevIo according to commit hook # fix videodelete according to msg1053993 # fix videodelete filename (change path to _) # new attribute homeScreenV3 set to 1 to use new v3 api # support blink cameras on homescreen - with new homeScreenV3 # added documentation for homeScreenV3 and BLinkMini - readonly mode # added - for host name instaed of . after rest # new camtype dependant camEnable / camDisable # ...Cam...active state corrected (was always disabled) # getThumbnail also for Blink Mini # login using V4 for V3 attr # Digest networks from homescreen / eliminate networks call # FIX: cam type not send for old homescreen # FIX: remove also FUUID keyvalue on delete # FIX: FUUID in undef corrected # New default 1 for homescreenv3 - old apis deactivated # Disclaimer for API changes # Corrected handling of prefered network attribute # Adapt active handling to new format # Add new reading Enabled per camera with doc - showing enabled value # Correction for JSON values - true/false 1/0 # deprecation note for attr homescreenv3 (log and doc) # handle new video delete - media delete # get client id and verification information from login # add set option verifyPin for pin verification - not verified # add doc for verifyPin (experimental) #SVN 7.8.2020 # add version id as internal - sourceVersion # removed old homescreen functonality # doc attr maxRetries # attr maxRetries allow up to 9 # change retry sequence to 2** wait (instead fo 3**) - 2 4 8 16 ... # change retry for followup on cmd completion to 6 # change liveview for new API # camera...Thumbnail reading only set after file is received # alertupdate - reset after 10 cycles skipped # add more log for alertupdating and remove skipped value # login changed to V5 api and also new format of response #msg1141218 # reset unique id as additional reset option # change header for auth to new value token-auth # adapter also region from login to new tier # changed alert handing only when network is identified # Correct perl warning on setup call with empty event timestamp #msg1142674 # Add description for allert readings # 5.2.2023 # #msg1228374 - könnte man in der commandref mal aktualisieren, dass man den code auch per sms bekommen kann statt nur email # Added doorbells (lotus) cameras - basic function # Get thumbnail working for doorbells/lotus # Add alerts only for known cameras # video alert working for doorbells # camdisable/camenable not working for Lotus --> now with return message # 6.2.2023 # added type for syncmodule # added message for lveview being unsupported # camEnable/camDisable working now for doorbells/lotus # liveview cmd will also set liveCam reading to identify stream # getThumbnail for doorbells working # # # ############################################################################## ############################################################################## ############################################################################## ############################################################################## # TASKS # # # # # # # # subtype for syncmodule needed? # Button press? # schlummermodus # # # Set thumbnail Req reading only after thumbnail stored (from internal) # # # Analyze more information and settings # # FIX: getThumbnail url failing sometimes # FIX: imgOriginalFile not fully working # might need to check for transaction id on command post # store poll failures / digest? # allow thumbnailreset # ############################################################################## # Ideas # # ############################################################################## # #{"authtoken":{"authtoken":"sjkashajhdjkashd","message":"auth"},"networks":{"":{"name":"","onboarded":true}},"region":{"prde":"Europe"}} #{"message":"Unauthorized Access"} # # ############################################################################## package main; use strict; use warnings; #use HttpUtils; use utf8; use Encode; # JSON:XS is used here normally use JSON; use Data::Dumper; use URI::Escape; use Scalar::Util qw(reftype looks_like_number); use DevIo; ######################### # Forward declaration sub BlinkCamera_Define($$); sub BlinkCamera_Undef($$); sub BlinkCamera_Delete($$); sub BlinkCamera_Set($@); sub BlinkCamera_Get($@); sub BlinkCamera_Callback($$$); sub BlinkCamera_DoCmd($$;$$$); sub BlinkCamera_PollInfo($); sub BlinkCamera_GetCameraId( $;$ ); sub BlinkCamera_CameraDoCmd( $$$ ); sub BlinkCamera_CheckSetGet( $$$ ); sub BlinkCamera_ReplacePattern( $$;$ ); sub BlinkCamera_ParseStartAlerts($;$$$); sub BlinkCamera_AnalyzeAlertPage( $$$ ); sub BlinkCamera_GetCamType( $$ ); ######################### # Globals # OLD? my $BlinkCamera_host = "prod.immedia-semi.com"; #my $BlinkCamera_host = "rest.prir.immedia-semi.com"; #my $BlinkCamera_host = "rest.prde.immedia-semi.com"; my $BlinkCamera_hostpattern = "rest##sep####region##.immedia-semi.com"; my $BlinkCamera_header = "agent: TelegramBot/1.0\r\nUser-Agent: TelegramBot/1.0"; # my $BlinkCamera_header = "agent: TelegramBot/1.0\r\nUser-Agent: TelegramBot/1.0\r\nAccept-Charset: utf-8"; my $BlinkCamera_loginjson = "{ \"password\" : \"q_password_q\", \"client_specifier\" : \"FHEM blinkCameraModule 1 - q_name_q\", \"email\" : \"q_email_q\" }"; #my $BlinkCamera_loginjsonV4 = "{ \"app_version\": \"6.0.10 (8280) #881c8812\", \"client_name\": \"fhem q_name_q\", \"client_type\": \"ios\", \"device_identifier\": \"fhem #q_fuuid_q\", \"email\": \"q_email_q\", \"os_version\": \"13\", \"password\": \"q_password_q\", \"reauth\": q_reauth_q, \"unique_id\": \"q_uniqueid_q\" }"; my $BlinkCamera_loginjsonV5 = "{ \"app_version\": \"6.2.7 (10212) \", \"client_name\": \"fhem q_name_q\", \"client_type\": \"ios\", \"device_identifier\": \"fhem q_fuuid_q\", \"email\": \"q_email_q\", \"os_version\": \"14.4\", \"password\": \"q_password_q\", \"reauth\": q_reauth_q, \"unique_id\": \"q_uniqueid_q\" }"; my $BlinkCamera_verifyPinjson = "{ \"pin\" : \"q_pin_q\" }"; my $BlinkCamera_configCamAlertjson = "{ \"camera\" : \"q_id_q\", \"id\" : \"q_id_q\", \"network\" : \"q_network_q\", \"motion_alert\" : \"q_alert_q\" }"; my $BlinkCamera_configOwljson = "{ \"enabled\" : q_value_q }"; my $BlinkCamera_configLotusjson = "{ \"enabled\" : q_value_q }"; # OLD my $BlinkCamera_deleteVideojson = "{ \"video_list\" : [ q_id_q ] }"; my $BlinkCamera_deleteVideojson = "{ \"media_list\" : [ q_id_q ] }"; my $BlinkCamera_cameraThumbnailjson = "{ \"id\" : \"q_id_q\", \"network\" : \"q_network_q\" }"; my $BlinkCamera_liveviewjson = "{ \"intent\" : \"liveview\", \"motion_event_start_time\" : \"\" }"; my $BlinkCamera_imgTemplate="#URL#"; my $BlinkCamera_vidTemplate="Video Id:#ID#: #URL#"; # store list of videos in hash/intern - id -> deleted -> created_at, updated_at, camera_id, viewed, video my $BlinkCamera_alertEntry='^([^\|]*)\|([^\|]*)\|([^\|]*)\|([^\|]*)\|(.*)$'; # always include name for name of the device to find the right device hash in proxy my $BlinkCamera_camerathumbnail = "BlinkCamera/q_name_q/thumbnail/camera/q_id_q.jpg"; my $BlinkCamera_videofile = "BlinkCamera/q_name_q/video/q_id_q.mp4"; # special debug setting my $BlinkCamera_specialLog = 4; # NEw Header store for toekn auth # my $BlinkCamera_TokenHeader = "TOKEN_AUTH"; my $BlinkCamera_TokenHeader = "token-auth"; ############################################################################## ############################################################################## ## ## Module operation ## ############################################################################## ############################################################################## ##################################### # Initialize is called from fhem.pl after loading the module # define functions and attributed for the module and corresponding devices sub BlinkCamera_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "BlinkCamera_Define"; $hash->{UndefFn} = "BlinkCamera_Undef"; $hash->{DeleteFn} = "BlinkCamera_Delete"; $hash->{GetFn} = "BlinkCamera_Get"; $hash->{SetFn} = "BlinkCamera_Set"; $hash->{AttrFn} = "BlinkCamera_Attr"; $hash->{AttrList} = " maxRetries:0,1,2,3,4,5,6,7,8,9 ". "imgTemplate:textField ". "imgOriginalFile:0,1 ". "videoTemplate:textField ". "proxyDir:textField ". "webname:textField ". "network ". "pollingTimeout ". "homeScreenV3:0,1 ". $readingFnAttributes; } ###################################### # Define function is called for actually defining a device of the corresponding module # For BlinkCamera this is email address and password # data will be stored in the hash of the device as internals / password as setkeyvalue # sub BlinkCamera_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t]+", $def); my $name = $hash->{NAME}; Log3 $name, 3, "BlinkCamera_Define $name: called "; my $errmsg = ''; # Check parameter(s) if ( ( int(@a) != 4 ) && ( int(@a) != 3 ) ) { $errmsg = "syntax error: define BlinkCamera [ ] "; Log3 $name, 1, "BlinkCamera $name: " . $errmsg; return $errmsg; } if ( $a[2] =~ /^.+@.+$/ ) { $hash->{Email} = $a[2]; } else { $errmsg = "specify valid email address define BlinkCamera [ ] "; Log3 $name, 1, "BlinkCamera $name: " . $errmsg; return $errmsg; } if ( int(@a) == 3 ) { my ($err, $password) = getKeyValue("BlinkCamera_".$hash->{Email}); if ( defined($err) ){ $errmsg = "no password token found (Error:".$err.") specify password with define BlinkCamera [ ] "; Log3 $name, 1, "BlinkCamera $name: " . $errmsg; return $errmsg; } elsif ( ! defined($password) ) { $errmsg = "no password token found specify password with define BlinkCamera [ ] "; Log3 $name, 1, "BlinkCamera $name: " . $errmsg; return $errmsg; } } else { setKeyValue( "BlinkCamera_".$hash->{Email}, $a[3] ); # remove password from def $hash->{DEF} = $hash->{Email}; } my $ret; $hash->{TYPE} = "BlinkCamera"; $hash->{STATE} = "Undefined"; BlinkCamera_Setup( $hash ); return $ret; } ##################################### # Undef function is corresponding to the delete command the opposite to the define function # Cleanup the device specifically for external ressources like connections, open files, # external memory outside of hash, sub processes and timers sub BlinkCamera_Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "BlinkCamera_Undef $name: called "; HttpUtils_Close($hash->{HU_DO_PARAMS}); RemoveInternalTimer($hash); RemoveInternalTimer($hash->{HU_DO_PARAMS}); Log3 $name, 4, "BlinkCamera_Undef $name: done "; return undef; } ##################################### # Undef function is corresponding to the delete command the opposite to the define function # Cleanup the device specifically for external ressources like connections, open files, # external memory outside of hash, sub processes and timers sub BlinkCamera_Delete($$) { my ($hash, $name) = @_; my $fuuid = $hash->{FUUID}; Log3 $name, 3, "BlinkCamera_Delete $name: called "; setKeyValue( "BlinkCamera_".$hash->{Email}, undef ); setKeyValue( "BlinkCamera_BLINKUID_".$fuuid, undef ); Log3 $name, 4, "BlinkCamera_Delete $name: done "; return undef; } ############################################################################## ############################################################################## ## ## Instance operational methods ## ############################################################################## ############################################################################## #################################### # set function for executing set operations on device sub BlinkCamera_Set($@) { my ( $hash, $name, @args ) = @_; ### Check Args my $numberOfArgs = int(@args); return "BlinkCamera_Set: No cmd specified for set" if ( $numberOfArgs < 1 ); my $cmd = shift @args; my $addArg = ($args[0] ? join(" ", @args ) : undef); Log3 $name, 5, "BlinkCamera_Set $name: Processing BlinkCamera_Set( $cmd ) - args :".(defined($addArg)?$addArg:"").":" if ( $cmd ne "?" ); # check cmd / handle ? my $ret = BlinkCamera_CheckSetGet( $hash, $cmd, $hash->{setoptions} ); if ( $ret ) { # do nothing if error/ret is defined } elsif ($cmd eq 'login') { $ret = BlinkCamera_DoCmd( $hash, $cmd ); } elsif ($cmd eq 'verifyPin') { $ret = BlinkCamera_DoCmd( $hash, $cmd, $addArg ); } elsif( ($cmd eq 'camEnable') || ($cmd eq 'camDisable') ) { $ret = BlinkCamera_CameraDoCmd( $hash, $cmd, $addArg ) } elsif( ($cmd eq 'arm') || ($cmd eq 'disarm') ) { $ret = BlinkCamera_DoCmd( $hash, $cmd ); } elsif($cmd eq 'reset') { Log3 $name, 3, "BlinkCamera_Set $name: reset requested "; BlinkCamera_Setup( $hash ); } elsif($cmd eq 'resetUniqueID') { Log3 $name, 3, "BlinkCamera_Set $name: resetUniqueID requested "; my $fuuid = $hash->{FUUID}; setKeyValue( "BlinkCamera_BLINKUID_".$fuuid, undef ); BlinkCamera_Setup( $hash ); } elsif($cmd eq 'videoDelete') { $ret = BlinkCamera_DoCmd( $hash, "videoDelete", $addArg ); } elsif($cmd eq 'zDebug') { Log3 $name, 5, "BlinkCamera_Set $name: zDebug requested "; # $hash->{AuthToken} = "ABCDEF"; # Debug "-------------------------"; # Debug Dumper( $hash->{alertResults} ); # Debug "-------------------------"; # Debug Dumper( $hash->{videos} ); # Debug "-------------------------"; } Log3 $name, 5, "BlinkCamera_Set $name: $cmd ".((defined( $ret ))?"failed with :$ret: ":"done succesful ") if ( $cmd ne "?" ); return $ret } ##################################### # get function for gaining information from device sub BlinkCamera_Get($@) { my ( $hash, $name, @args ) = @_; ### Check Args my $numberOfArgs = int(@args); return "BlinkCamera_Get: No value specified for get" if ( $numberOfArgs < 1 ); my $cmd = $args[0]; my $arg = $args[1]; Log3 $name, 5, "BlinkCamera_Get $name: Processing BlinkCamera_Get( $cmd )" if ( $cmd ne "?" ); # check cmd / handle ? my $ret = BlinkCamera_CheckSetGet( $hash, $cmd, $hash->{getoptions} ); if ( $ret ) { # do nothing if error/ret is defined } elsif($cmd eq 'getInfo') { $ret = BlinkCamera_DoCmd( $hash, "homescreen" ); } elsif($cmd eq 'getNetworks') { $ret = BlinkCamera_DoCmd( $hash, "networks" ); } elsif ($cmd eq 'getInfoCamera') { return "BlinkCamera_Get: No value specified for get $cmd" if ( $numberOfArgs < 2 ) ; $ret = BlinkCamera_CameraDoCmd( $hash, "cameraConfig", $arg ); } elsif ($cmd eq 'getThumbnail') { return "BlinkCamera_Get: No value specified for get $cmd" if ( $numberOfArgs < 2 ) ; $ret = BlinkCamera_CameraDoCmd( $hash, "cameraThumbnail", $arg ); } elsif($cmd eq 'getVideoAlert') { $ret = BlinkCamera_DoCmd( $hash, "video", $arg ); } elsif($cmd eq 'liveview') { return "BlinkCamera_Get: No value specified for get $cmd" if ( $numberOfArgs < 2 ) ; $ret = BlinkCamera_CameraDoCmd( $hash, "liveview", $arg ); } elsif($cmd eq 'cameraList') { my $cList = BlinkCamera_GetCameraId( $hash ); if ( ! defined( $cList ) ) { $ret = "ERROR: No cameras found - need to run getInfo?"; } else { foreach my $cam ( @$cList ) { $ret = (defined($ret)?$ret."\n":"").$cam; } } $ret = "ERROR: cameralist is empty" if ( ! $ret ); return $ret } Log3 $name, 5, "BlinkCamera_Get $name: $cmd ".((defined( $ret ))?"failed with :$ret: ":"done succesful ") if ( $cmd ne "?" ); return $ret } ############################## # attr function for setting fhem attributes for the device sub BlinkCamera_Attr(@) { my ($cmd,$name,$aName,$aVal) = @_; my $hash = $defs{$name}; Log3 $name, 5, "BlinkCamera_Attr $name: called "; return "\"BlinkCamera_Attr: \" $name does not exist" if (!defined($hash)); if (defined($aVal)) { Log3 $name, 5, "BlinkCamera_Attr $name: $cmd on $aName to $aVal"; } else { Log3 $name, 5, "BlinkCamera_Attr $name: $cmd on $aName to "; } # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value if ($cmd eq "set") { if ( ($aName eq 'boolValue') ) { $aVal = ($aVal eq "1")? "1": "0"; } elsif ($aName eq 'pollingTimeout') { return "\"BlinkCamera_Attr: \" $aName needs to be given in digits only" if ( $aVal !~ /^[[:digit:]]+$/ ); # let all existing methods run into block RemoveInternalTimer($hash); $hash->{POLLING} = -1; # wait some time before next polling is starting BlinkCamera_ResetPollInfo( $hash ); } elsif ($aName eq 'pollingVerbose') { return "\"BlinkCamera_Attr: \" Incorrect value given for pollingVerbose" if ( $aVal !~ /^((1_Digest)|(2_Log)|(0_None))$/ ); } elsif ($aName eq 'homeScreenV3') { Log3 $name, 1, "BlinkCamera_Attr $name: Attribute homeScreenV3 is deprecated and can be removed - old API has been switched off"; } $_[3] = $aVal; } return undef; } ############################################################################## ############################################################################## ## ## Communication - Do command ## ############################################################################## ############################################################################## ##################################### # INTERNAL: Function to send a command to the blink server # cmd is login / arm / homescreen # par1/par2 are placeholder for addtl params sub BlinkCamera_DoCmd($$;$$$) { my ( $hash, @args) = @_; my ( $cmd, $par1, $par2, $retryCount) = @args; my $name = $hash->{NAME}; $retryCount = 0 if ( ! defined( $retryCount ) ); # increase retrycount for next try $args[3] = $retryCount+1; my $cmdString = $cmd.(defined($par1)?" par1:".$par1.":":"").(defined($par2)?" par2:".$par2.":":""); Log3 $name, 4, "BlinkCamera_DoCmd $name: called for cmd :$cmdString:"; # ensure cmdQueue exists $hash->{cmdQueue} = [] if ( ! defined( $hash->{cmdQueue} ) ); # Queue if not yet retried and currently waiting if ( ( defined( $hash->{doStatus} ) ) && ( $hash->{doStatus} =~ /^WAITING/ ) && ( $retryCount == 0 ) ){ # add to queue Log3 $name, $BlinkCamera_specialLog, "BlinkCamera_DoCmd $name: add send to queue cmd ".$cmdString; # command / alert will always be added to the beginning of the queue if ( ( $cmd eq "command" ) || ( $cmd eq "alerts" )) { unshift( @{ $hash->{cmdQueue} }, \@args ); } else { push( @{ $hash->{cmdQueue} }, \@args ); } return; } ####################### # check authentication otherwise queue the current cmd and do authenticate first if ( ($cmd ne "login") && ( ! defined( $hash->{AuthToken} ) ) ) { # add to queue Log3 $name, 4, "BlinkCamera_DoCmd $name: add send to queue cmd ".$cmdString; push( @{ $hash->{cmdQueue} }, \@args ); $cmd = "login"; $par1 = undef; $par2 = undef; # update cmdstring $cmdString = "cmd :$cmd: ".(defined($par1)?" par1:".$par1.":":"").(defined($par2)?" par2:".$par2.":":""); } ####################### # Check for invalid auth token and just remove cmds if ( ($cmd ne "login") && ( $hash->{AuthToken} eq "INVALID" ) ) { # add to queue Log3 $name, 2, "BlinkCamera_DoCmd $name: failed due to invalid auth token ".$cmdString; return; } ## get actual network and attr V3 my $net = BlinkCamera_GetNetwork( $hash ); # !! removed old homescreen - 2020-10-12 ####################### # check networks if not existing queue current cmd and get homescreen first if ( ($cmd ne "login") && ($cmd ne "homescreen") && ($cmd ne "verifyPin") && ( ! defined( $net ) ) ) { # add to queue Log3 $name, 4, "BlinkCamera_DoCmd $name: add send to queue cmd ".$cmdString; push( @{ $hash->{cmdQueue} }, \@args ); $cmd = "homescreen"; $par1 = undef; $par2 = undef; # update cmdstring $cmdString = "cmd :$cmd: ".(defined($par1)?" par1:".$par1.":":"").(defined($par2)?" par2:".$par2.":":""); } ####################### # Check for invalid auth token and just remove cmds if ( ($cmd ne "login") && ($cmd ne "homescreen") && ($cmd ne "verifyPin") && ( $net eq "INVALID" ) ) { # add to queue Log3 $name, 2, "BlinkCamera_DoCmd $name: failed due to invalid networks list (set attribute network) ".$cmdString; return; } my $ret; $hash->{doStatus} = "WAITING"; $hash->{doStatus} .= " retry $retryCount" if ( $retryCount > 0 ); $hash->{AuthToken} = "INVALID" if ($cmd eq "login"); # reset networks reading for reading networks readingsSingleUpdate($hash, "networks", "INVALID", 0 ) if ( ($cmd eq "networks") ); Log3 $name, 4, "BlinkCamera_DoCmd $name: try to send cmd ".$cmdString; if ( ( !defined( $par2 ) ) || ( ($par2 ne "POLLING" ) && ($par2 ne "HIDDEN" ) ) ) { $hash->{cmd} = $cmdString; $hash->{cmdJson} = ""; } # init param hash $hash->{HU_DO_PARAMS}->{hash} = $hash; delete( $hash->{HU_DO_PARAMS}->{args} ); delete( $hash->{HU_DO_PARAMS}->{boundary} ); delete( $hash->{HU_DO_PARAMS}->{compress} ); delete( $hash->{HU_DO_PARAMS}->{filename} ); $hash->{HU_DO_PARAMS}->{cmd} = $cmd; $hash->{HU_DO_PARAMS}->{par1} = $par1; $hash->{HU_DO_PARAMS}->{par2} = $par2; my $timeout = AttrVal($name,'cmdTimeout',30); $hash->{HU_DO_PARAMS}->{timeout} = $timeout; # only for test / debug $hash->{HU_DO_PARAMS}->{loglevel} = 4; $hash->{HU_DO_PARAMS}->{callback} = \&BlinkCamera_Callback; # handle data creation only if no error so far if ( ! defined( $ret ) ) { $hash->{HU_DO_PARAMS}->{method} = "POST"; my $dynhost = $BlinkCamera_hostpattern; my $region = ReadingsVal( $name, "region", "prde" ); if ($cmd eq "login") { $dynhost =~ s/##region##/prod/; # $dynhost =~ s/##sep##/-/; } else { $dynhost =~ s/##region##/$region/; # $dynhost =~ s/##sep##/./; } $dynhost =~ s/##sep##/-/; $hash->{URL} = "https://".$dynhost; $hash->{HU_DO_PARAMS}->{header} = $BlinkCamera_header. "\r\n"."Host: ".$dynhost; ####################### if ($cmd eq "login") { $hash->{HU_DO_PARAMS}->{header} .= "\r\n"."Content-Type: application/json"; my $email = $hash->{Email}; my ($err, $password) = getKeyValue("BlinkCamera_".$email); if(defined($err)) { $ret = "BlinkCamera_DoCmd $name: password retrieval failed with :$err:"; } elsif(! defined($password)) { $ret = "BlinkCamera_DoCmd $name: password is empty"; } else { my $isReauth = "true"; my $fuuid = $hash->{FUUID}; my ($err, $uid_key) = getKeyValue("BlinkCamera_BLINKUID_".$fuuid); if ( ( defined($err) ) || ( ! defined($uid_key) ) ) { $uid_key = join "", map { unpack "H*", chr(rand(256)) } 1..16; setKeyValue( "BlinkCamera_BLINKUID_".$fuuid, $uid_key ); $isReauth = "false"; } $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v5/account/login"; # $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_loginjsonV4; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_loginjsonV5; $hash->{HU_DO_PARAMS}->{data} =~ s/q_password_q/$password/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_email_q/$email/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_name_q/$name/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_uniqueid_q/$uid_key/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_reauth_q/$isReauth/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_fuuid_q/$fuuid/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: loginV5 data :".$hash->{HU_DO_PARAMS}->{data}.":"; } ####################### } elsif ( $cmd eq "verifyPin" ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}."\r\n"."Content-Type: application/json"; # /api/v4/account//client//pin/verify #ORG $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v4/account/".$hash->{account}."/client/".$hash->{clientid}."/pin/verify"; $hash->{HU_DO_PARAMS}->{url} = "https://rest-e004.immedia-semi.com"."/api/v4/account/".$hash->{account}."/client/".$hash->{clientid}."/pin/verify"; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_verifyPinjson; $hash->{HU_DO_PARAMS}->{data} =~ s/q_pin_q/$par1/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: verify pin : ".$par1.": - data :".$hash->{HU_DO_PARAMS}->{data}.":"; ####################### } elsif ( ($cmd eq "camEnable") || ($cmd eq "camDisable" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}."\r\n"."Content-Type: application/json"; my $ctype = "invalid"; if ( ! defined( $net ) ) { $ret = "BlinkCamera_DoCmd $name: no network identifier found for $cmd - set attribute"; } else { $ctype = BlinkCamera_GetCamType( $hash, $par1 ); } if ( ! $ret ) { my $alert = ($cmd eq "camEnable")?"true":"false"; if ( $ctype eq "camera" ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/network/".$net."/camera/".$par1."/update"; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_configCamAlertjson; $hash->{HU_DO_PARAMS}->{data} =~ s/q_id_q/$par1/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_network_q/$net/g; $hash->{HU_DO_PARAMS}->{data} =~ s/q_alert_q/$alert/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: cam type: ".$ctype.": - data :".$hash->{HU_DO_PARAMS}->{data}.":"; } elsif ( $ctype eq "owl" ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/networks/".$net."/owls/".$par1."/config"; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_configOwljson; $hash->{HU_DO_PARAMS}->{data} =~ s/q_value_q/$alert/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: cam type: ".$ctype.": - data :".$hash->{HU_DO_PARAMS}->{data}.":"; } elsif ( $ctype eq "lotus" ) { # $ret = "BlinkCamera_DoCmd $name: camera type (".$ctype.") unsupported !!"; if ($cmd eq "camEnable") { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}. "/networks/".$net."/doorbells/".$par1."/config"; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_configLotusjson; $hash->{HU_DO_PARAMS}->{data} =~ s/q_value_q/$alert/g; } else { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}. "/networks/".$net."/doorbells/".$par1."/disable"; $hash->{HU_DO_PARAMS}->{data} = ""; } Log3 $name, 4, "BlinkCamera_DoCmd $name: cam type: ".$ctype.": - data :".$hash->{HU_DO_PARAMS}->{data}.":"; } else { $ret = "BlinkCamera_DoCmd $name: camera type (".$ctype.") unknown !!"; } } ####################### } elsif ( ($cmd eq "arm") || ($cmd eq "disarm" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; if ( defined( $net ) ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/network/".$net."/".$cmd; } else { $ret = "BlinkCamera_DoCmd $name: no network identifier found for arm/disarm - set attribute"; } ####################### } elsif ($cmd eq "homescreen" ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET"; my $acc = $hash->{account}; if ( defined( $acc ) ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v3/accounts/".$acc."/".$cmd; } else { $ret = "BlinkCamera_DoCmd $name: no account id found for homescreen"; } ####################### } elsif ( ($cmd eq "networks" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET" ; $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/networks"; ####################### } elsif ( ($cmd eq "command" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET"; if ( defined( $net ) ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/network/".$net."/command/".$par1; } else { $ret = "BlinkCamera_DoCmd $name: no network identifier found for command - set attribute"; } ####################### } elsif ( ($cmd eq "alerts" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET"; # OLD V2 $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v2/videos/changed?page=".$par1."&since=".$hash->{alertUpdate}; # V1 seems still working here (v2 has been removed) # GET https://rest-prde.immedia-semi.com/api/v1/accounts//media/changed?since=2019-05-26T15%3A22%3A36Z&page=1 $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/media/changed?page=".$par1."&since=".$hash->{alertUpdate}; # my $net = BlinkCamera_GetNetwork( $hash ); # if ( defined( $net ) ) { # $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v2/videos?page=1"; # $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/events/network/".$net."/camera/2148?page=1"; # $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/events/network/".$net; # $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v2/videos/changed?page=2&since=2016-10-31T15:29:25Z"; # } ####################### } elsif ( ($cmd eq "cameraConfig" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET"; if ( defined( $net ) ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/network/".$net."/camera/".$par1."/config"; } else { $ret = "BlinkCamera_DoCmd $name: no network identifier found for $cmd - set attribute"; } ####################### } elsif ( ($cmd eq "cameraThumbnail" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "POST"; $hash->{HU_DO_PARAMS}->{data} = ""; if ( defined( $net ) ) { my $ctype = BlinkCamera_GetCamType( $hash, $par1 ); if ( $ctype eq "camera" ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/network/".$net."/camera/".$par1."/thumbnail"; # $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_cameraThumbnailjson; # $hash->{HU_DO_PARAMS}->{data} =~ s/q_network_q/$net/g; # $hash->{HU_DO_PARAMS}->{data} =~ s/q_id_q/$par1/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: $cmd cam type: ".$ctype.": "; } elsif ( $ctype eq "owl" ) { # https://rest-prde.immedia-semi.com/api/v1/accounts//networks//owls//thumbnail $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/networks/".$net."/owls/".$par1."/thumbnail"; Log3 $name, 4, "BlinkCamera_DoCmd $name: $cmd cam type: ".$ctype.": "; } elsif ( $ctype eq "lotus" ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/networks/".$net."/doorbells/".$par1."/thumbnail"; Log3 $name, 4, "BlinkCamera_DoCmd $name: $cmd cam type: ".$ctype.": "; } else { $ret = "BlinkCamera_DoCmd $name: $cmd camera type (".$ctype.") unknown !!"; } } else { $ret = "BlinkCamera_DoCmd $name: no network identifier found for $cmd - set attribute"; } ####################### } elsif ($cmd eq "thumbnail") { # camera id in par my $curl = $hash->{"thumbnail".$par1."Req"}; Log3 $name, 5, "BlinkCamera_DoCmd $name: par1 :".$par1.":"; Log3 $name, 5, "BlinkCamera_DoCmd $name: curl :".(defined($curl)?$curl:"").":"; $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET"; if ( defined( $curl ) ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}.$curl.".jpg"; if ( AttrVal( $name, "imgOriginalFile", 0 ) ) { if ( $curl =~ /^.*\/([^\/]+)/ ) { my $orgthumbfile = $1; $hash->{HU_DO_PARAMS}->{filename} = BlinkCamera_ReplacePattern( $BlinkCamera_camerathumbnail, $par1."_".$orgthumbfile, $name ); } else { $ret = "BlinkCamera_DoCmd $name: url did not contain filename " } } else { $hash->{HU_DO_PARAMS}->{filename} = BlinkCamera_ReplacePattern( $BlinkCamera_camerathumbnail, $par1, $name ); } } else { $ret = "BlinkCamera_DoCmd $name: no url found " } ####################### } elsif ($cmd eq "video") { # video id in par or take reading alertID my $vid = $par1; $vid = ReadingsVal( $name, "alertID", undef ) if ( !defined( $vid) ); my $vidUrl = BlinkCamera_GetAlertVideoURL( $hash, $vid ) if ( defined( $vid) ); # store back in par1 the actual video reques $par1 = $vid; $hash->{HU_DO_PARAMS}->{par1} = $par1; $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "GET"; if ( defined( $vidUrl ) ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}.$vidUrl; # --> /tmp/BlinkCamera__thumbnail__. $hash->{HU_DO_PARAMS}->{filename} = BlinkCamera_ReplacePattern( $BlinkCamera_videofile, $vid, $name ); } else { $ret = "BlinkCamera_DoCmd $name: no video id or url found (".(defined($vid)?$vid:"").")"; } ####################### } elsif ($cmd eq "videoDelete") { # video id in par or take reading alertID my $vid = $par1; $vid = ReadingsVal( $name, "alertID", undef ) if ( !defined( $vid) ); # first remove proxy file if existing my $vidUrl = BlinkCamera_GetAlertVideoURL( $hash, $vid ) if ( defined( $vid) ); if ( defined( $vidUrl ) ) { my $filename = BlinkCamera_ReplacePattern( $BlinkCamera_videofile, $vid, $name ); my $proxyDir = AttrVal($name,"proxyDir","/tmp/"); # normalize URL separator / into _ $filename =~ s/\//_/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: deleting $filename in $proxyDir "; eval { unlink $proxyDir."/".$filename; } if ( -e $proxyDir."/".$filename ); Log3 $name, 2, "BlinkCamera_DoCmd $name: video file $filename could not be deleted :$@: " if $@; } if ( defined( $vid ) ) { ## NEW API # https://rest-prde.immedia-semi.com/api/v1/accounts//media/delete # request # { # "media_list": [ # 742450103 # ] # } # response # { # "code": 711, # "message": "Successfully deleted videos" # } ## OLD # $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}."\r\n"."Content-Type: application/json"; # $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v3/videos/delete"; # $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_deleteVideojson; # $hash->{HU_DO_PARAMS}->{data} =~ s/q_id_q/$vid/g; $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}."\r\n"."Content-Type: application/json"; $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/media/delete"; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_deleteVideojson; $hash->{HU_DO_PARAMS}->{data} =~ s/q_id_q/$vid/g; Log3 $name, 4, "BlinkCamera_DoCmd $name: data :".$hash->{HU_DO_PARAMS}->{data}.":"; } else { $ret = "BlinkCamera_DoCmd $name: no video id for deletion found (".(defined($vid)?$vid:"").")"; } ####################### } elsif ( ($cmd eq "liveview" ) ) { $hash->{HU_DO_PARAMS}->{header} .= "\r\n".$BlinkCamera_TokenHeader.": ".$hash->{AuthToken}; $hash->{HU_DO_PARAMS}->{method} = "POST"; $hash->{HU_DO_PARAMS}->{data} = $BlinkCamera_liveviewjson; if ( defined( $net ) ) { my $ctype = BlinkCamera_GetCamType( $hash, $par1 ); if ( $ctype eq "camera" ) { # $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/network/".$net."/camera/".$par1."/liveview"; $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v5/accounts/".$hash->{account}."/networks/".$net."/cameras/".$par1."/liveview"; } elsif ( $ctype eq "owl" ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/networks/".$net."/owls/".$par1."/liveview"; } elsif ( $ctype eq "lotus" ) { $hash->{HU_DO_PARAMS}->{url} = $hash->{URL}."/api/v1/accounts/".$hash->{account}."/networks/".$net."/doorbells/".$par1."/liveview"; # $ret = "BlinkCamera_DoCmd $name: $cmd camera type (".$ctype.") unsupported !!"; } else { $ret = "BlinkCamera_DoCmd $name: $cmd camera type (".$ctype.") unknown !!"; } } else { $ret = "BlinkCamera_DoCmd $name: no network identifier found for $cmd - set attribute"; } } } ####################### ## JVI # Debug "send command :".$hash->{HU_DO_PARAMS}->{data}.":"; if ( defined( $ret ) ) { Log3 $name, 1, "BlinkCamera_DoCmd $name: Failed with :$ret:"; BlinkCamera_Callback( $hash->{HU_DO_PARAMS}, $ret, ""); } else { $hash->{HU_DO_PARAMS}->{args} = \@args; Log3 $name, 4, "BlinkCamera_DoCmd $name: call url :".$hash->{HU_DO_PARAMS}->{url}.": "; HttpUtils_NonblockingGet( $hash->{HU_DO_PARAMS} ); } return $ret; } ############################################################################## ############################################################################## ## ## callback ## ############################################################################## ############################################################################## ##################################### # INTERNAL: Called to retry a send operation after wait time # Gets the do params sub BlinkCamera_RetryDo($) { my ( $param ) = @_; my $hash= $param->{hash}; my $name = $hash->{NAME}; my $ref = $param->{args}; Log3 $name, 4, "BlinkCamera_RetryDo $name: call retry @$ref[3] cmd:@$ref[0]: par1:".(defined(@$ref[1])?@$ref[1]:"").": par2:".(defined(@$ref[2])?@$ref[2]:"").": "; BlinkCamera_DoCmd( $hash, @$ref[0], @$ref[1], @$ref[2], @$ref[3] ); } ##################################### # INTERNAL: Encode a deep structure # name sub BlinkCamera_Deepencode { my @result; my $name = shift( @_ ); # Debug "BlinkCamera_Deepencode with :".(@_).":"; for (@_) { my $reftype= ref $_; if( $reftype eq "ARRAY" ) { Log3 $name, 5, "BlinkCamera_Deepencode $name: found an ARRAY"; push @result, [ BlinkCamera_Deepencode($name, @$_) ]; } elsif( $reftype eq "HASH" ) { my %h; @h{keys %$_}= BlinkCamera_Deepencode($name, values %$_); Log3 $name, 5, "BlinkCamera_Deepencode $name: found a HASH"; push @result, \%h; } else { my $us = $_ ; if ( utf8::is_utf8($us) ) { $us = encode_utf8( $_ ); } Log3 $name, 5, "BlinkCamera_Deepencode $name: encoded a String from :".(defined($_)?$_:"").": to :".(defined($us)?$us:"").":"; push @result, $us; } } return @_ == 1 ? $result[0] : @result; } ##################################### # INTERNAL: Parse the login results sub BlinkCamera_ParseLogin($$$) { my ( $hash, $result, $readUpdates ) = @_; my $name = $hash->{NAME}; # !! removed old homescreen - 2020-10-12 my $ret; if ( defined( $result->{account} ) ) { my $acc = $result->{account}; if ( defined( $acc->{account_id} ) ) { $hash->{account} = $acc->{account_id}; } # V5 if ( defined( $acc->{client_id} ) ) { $hash->{clientid} = $acc->{client_id}; } if ( defined( $acc->{client_verification_required} ) ) { $hash->{clientverreq} = $acc->{client_verification_required}; } if ( defined( $acc->{phone_verification_required} ) ) { $hash->{phoneverreq} = $acc->{phone_verification_required}; } } # V4 # if ( defined( $result->{authtoken} ) ) { # my $at = $result->{authtoken}; # if ( defined( $at->{authtoken} ) ) { # $hash->{AuthToken} = $at->{authtoken}; # } # } # V5 if ( defined( $result->{auth} ) ) { my $au = $result->{auth}; if ( defined( $au->{token} ) ) { $hash->{AuthToken} = $au->{token}; } } # V4 # if ( defined( $result->{client} ) ) { # my $clt = $result->{client}; # if ( defined( $clt->{id} ) ) { # $hash->{clientid} = $clt->{id}; # $hash->{clientverreq} = $clt->{verification_required}; # } # } # V4 # my $resreg = $result->{region}; # if ( defined( $resreg ) ) { # $readUpdates->{region} = $resreg->{tier}; # $readUpdates->{regionName} = $resreg->{description}; # } else { # $readUpdates->{region} = undef; # $readUpdates->{regionName} = undef; # } # V5 my $resreg = $result->{account}; if ( defined( $resreg ) ) { $readUpdates->{region} = $resreg->{tier}; $readUpdates->{regionName} = $resreg->{region}; } else { $readUpdates->{region} = undef; $readUpdates->{regionName} = undef; } return $ret; } ##################################### # INTERNAL: Parse the networks results sub BlinkCamera_ParseNetworks($$$) { my ( $hash, $result, $readUpdates ) = @_; my $name = $hash->{NAME}; my $ret; # grab network list from summary my $resnet = $result->{summary}; my $netlist = ""; if ( defined( $resnet ) ) { Log3 $name, 4, "BlinkCamera_Callback $name: login number of networks ".scalar(keys %$resnet) ; foreach my $netkey ( keys %$resnet ) { Log3 $name, 4, "BlinkCamera_Callback $name: network ".$netkey ; my $net = $resnet->{$netkey}; my $ob = 0; $ob = 1 if ( ( defined( $net->{onboarded} ) ) && ( $net->{onboarded}) ); my $ns = $netkey.":".$net->{name}; # Log3 $name, 4, "BlinkCamera_Callback $name: onboarded :".$net->{onboarded}.":" ; if ( $ob ) { Log3 $name, 4, "BlinkCamera_Callback $name: found onboarded network ".$netkey ; $ns .= "\n" if ( length( $netlist) > 0 ); $netlist = $ns.$netlist; } else { $netlist .= "\n" if ( length( $netlist) > 0 ); $netlist .= $ns; } } } $readUpdates->{networks} = $netlist; return $ret; } ##################################### # INTERNAL: Parse the homescreen results --> valid for V3 sub BlinkCamera_ParseHomescreen($$$) { my ( $hash, $result, $readUpdates ) = @_; my $name = $hash->{NAME}; # !! removed old homescreen - 2020-10-12 my $ret; Log3 $name, 4, "BlinkCamera_ParseHomescreen $name: "; # grab network list from summary my $resnet = $result->{networks}; my $firstnetwork = undef; my $netlist = ""; my $prefnet = AttrVal($hash->{NAME},'network',undef); $readUpdates->{networkName} = ""; $readUpdates->{networkArmed} = ""; if ( defined( $resnet ) ) { Log3 $name, 4, "BlinkCamera_Callback $name: homescreen number of networks ".scalar(@$resnet) ; foreach my $anet ( @$resnet ) { my $netid = $anet->{id}; Log3 $name, 4, "BlinkCamera_Callback $name: network ".$netid ; my $ob = 0; # $ob = 1 if ( ( defined( $anet->{onboarded} ) ) && ( $anet->{onboarded}) ); my $ns = $netid.":".$anet->{name}; # Log3 $name, 4, "BlinkCamera_Callback $name: onboarded :".$anet->{onboarded}.":" ; # check with prefered network if ( defined( $prefnet ) ) { # only check against pref net if this is set to avoid using first network if ( $netid eq $prefnet) { $firstnetwork = $netid; $readUpdates->{networkName} = $anet->{name} if ( defined( $anet->{name} ) ); $readUpdates->{networkArmed} = ($anet->{armed}?"true":"false") if ( defined( $anet->{armed} ) ); } } elsif ( ! defined( $firstnetwork ) ) { $firstnetwork = $netid; $readUpdates->{networkName} = $anet->{name} if ( defined( $anet->{name} ) ); $readUpdates->{networkArmed} = ($anet->{armed}?"true":"false") if ( defined( $anet->{armed} ) ); } # $ob currently always 0 - check with onboarded networks - currently only influencing sequence if ( $ob ) { $ns .= "\n" if ( length( $netlist) > 0 ); $netlist = $ns.$netlist; } else { $netlist .= "\n" if ( length( $netlist) > 0 ); $netlist .= $ns; } } } $readUpdates->{networks} = $netlist; my $network = $firstnetwork; if ( ! defined( $network ) ) { Log3 $name, 2, "BlinkCamera_ParseHomescreen $name: Network ID not found - please set attribute to existing network"; } else { Log3 $name, 4, "BlinkCamera_ParseHomescreen $name: network ".$network ; # changed alerts after network identification only # Homescreen succesful so start a request for alerst/videos/notifications $hash->{alertSkipped} = 0 if ( ! defined ($hash->{alertSkipped} ) ); if ( defined ($hash->{alertUpdate} ) ) { $hash->{alertSkipped} += 1; if ( $hash->{alertSkipped} > 10 ) { delete( $hash->{alertUpdate} ); $hash->{alertSkipped} = 0; Log3 $name, 3, "BlinkCamera_Callback $name: alertUpdate reset - too many times skipped" ; } } else { BlinkCamera_ParseStartAlerts($hash) } } # sync module information my $syncList = $result->{sync_modules}; # loop through msync modules and get the requested module information $readUpdates->{networkSyncModule} = ""; $readUpdates->{networkSyncId} = ""; $readUpdates->{networkSyncName} = ""; $readUpdates->{networkSyncSerial} = ""; $readUpdates->{networkSyncFirmware} = ""; $readUpdates->{networkSyncWifi} = ""; $readUpdates->{networkSyncType} = ""; if ( defined( $syncList ) ) { foreach my $module ( @$syncList ) { if ( $module->{network_id} eq $network ) { $readUpdates->{networkSyncModule} = $module->{status} if ( defined( $module->{status} ) ); $readUpdates->{networkSyncId} = $module->{id} if ( defined( $module->{id} ) ); $readUpdates->{networkSyncName} = $module->{name} if ( defined( $module->{name} ) ); $readUpdates->{networkSyncSerial} = $module->{serial} if ( defined( $module->{serial} ) ); $readUpdates->{networkSyncFirmware} = $module->{fw_version} if ( defined( $module->{fw_version} ) ); $readUpdates->{networkSyncWifi} = $module->{wifi_strength} if ( defined( $module->{wifi_strength} ) ); $readUpdates->{networkSyncType} = $module->{type} if ( defined( $module->{type} ) ); Log3 $name, 4, "BlinkCamera_ParseHomescreen $name: found sync module info for network "; last; } } } # Cameras and Owls (blink mini) and lotus (doorbells) my $camList = $result->{cameras}; my $owlList = $result->{owls}; my $lotusList = $result->{doorbells}; return $ret if ( ( ! defined( $camList ) ) && ( ! defined( $owlList ) ) ); Log3 $name, 4, "BlinkCamera_Callback $name: handle cams" ; my $cameraGets = ""; my $cameras = ""; # loop through cameras and get the requested camera information if ( defined( $camList ) ) { foreach my $device ( @$camList ) { if ( $device->{network_id} eq $network ) { my $active = "disabled"; if ( defined( $device->{enabled} ) ) { $active = "armed" if ( $device->{enabled} == 1 ); $active = "armed" if ( $device->{enabled} eq "true" ); # $active = "armed" if ( $device->{enabled} ); } $readUpdates->{"networkCamera".$device->{id}} = $device->{name}.":".$active; $readUpdates->{"networkCamera".$device->{id}."Name"} = $device->{name}; $readUpdates->{"networkCamera".$device->{id}."Type"} = "camera"; $readUpdates->{"networkCamera".$device->{id}."Active"} = $active; $readUpdates->{"networkCamera".$device->{id}."Enabled"} = defined($device->{enabled})?$device->{enabled}:"undef"; $cameraGets .= $device->{name}.",".$device->{id}.","; $cameras .= $device->{id}.":".$device->{name}."\n"; if ( defined( $device->{thumbnail} ) ) { # Load Thumbnail only if not already there if ( ( ! defined( $hash->{"thumbnail".$device->{id}."Url"} ) || ( $hash->{"thumbnail".$device->{id}."Url"} ne $device->{thumbnail} ) ) ) { if ( ! defined( $hash->{"thumbnail".$device->{id}."Req"} ) ) { # Debug "retreive thumbnail from homescreen for ".$device->{id}; # Do thumbnail request only of not already there (e.g. by polling) $hash->{"thumbnail".$device->{id}."Req"} = $device->{thumbnail}; BlinkCamera_DoCmd( $hash, "thumbnail", $device->{id}, "HIDDEN" ); } else { Log3 $name, 4, "BlinkCamera_ParseHomescreen $name: thumbnail Req already defined for ".$device->{id}; } } else { # already there just update readings $readUpdates->{"networkCamera".$device->{id}."Url"} = BlinkCamera_getwebname( $hash ). BlinkCamera_ReplacePattern( $BlinkCamera_camerathumbnail, $device->{id}, $name ); } # do this only after storag of thumbnail being completed $readUpdates->{"networkCamera".$device->{id}."Thumbnail"} = $device->{thumbnail} ; } $readUpdates->{"networkCamera".$device->{id}."Batt"} = $device->{battery} if ( defined( $device->{battery} ) ); $readUpdates->{"networkCamera".$device->{id}."Firmware"} = $device->{fw_version} if ( defined( $device->{fw_version} ) ); $readUpdates->{"networkCamera".$device->{id}."Status"} = $device->{status} if ( defined( $device->{status} ) ); if ( defined( $device->{signals} ) ) { my $signal = $device->{signals}; $readUpdates->{"networkCamera".$device->{id}."Temp"} = $signal->{temp}; } } } } # loop through owls and get the requested camera information if ( defined( $owlList ) ) { foreach my $device ( @$owlList ) { if ( $device->{network_id} eq $network ) { my $active = "disabled"; if ( defined( $device->{enabled} ) ) { $active = "armed" if ( $device->{enabled} == 1 ); $active = "armed" if ( $device->{enabled} eq "true" ); # $active = "armed" if ( $device->{enabled} ); } $readUpdates->{"networkCamera".$device->{id}} = $device->{name}.":".$active; $readUpdates->{"networkCamera".$device->{id}."Name"} = $device->{name}; $readUpdates->{"networkCamera".$device->{id}."Type"} = "owl"; $readUpdates->{"networkCamera".$device->{id}."Active"} = $active; $readUpdates->{"networkCamera".$device->{id}."Enabled"} = defined($device->{enabled})?$device->{enabled}:"undef"; $cameraGets .= $device->{name}.",".$device->{id}.","; $cameras .= $device->{id}.":".$device->{name}."\n"; if ( defined( $device->{thumbnail} ) ) { # Load Thumbnail only if not already there if ( ( ! defined( $hash->{"thumbnail".$device->{id}."Url"} ) || ( $hash->{"thumbnail".$device->{id}."Url"} ne $device->{thumbnail} ) ) ) { if ( ! defined( $hash->{"thumbnail".$device->{id}."Req"} ) ) { # Debug "retreive thumbnail from homescreen for ".$device->{id}; # Do thumbnail request only of not already there (e.g. by polling) $hash->{"thumbnail".$device->{id}."Req"} = $device->{thumbnail}; BlinkCamera_DoCmd( $hash, "thumbnail", $device->{id}, "HIDDEN" ); } else { Log3 $name, 4, "BlinkCamera_ParseHomescreen $name: thumbnail Req already defined for ".$device->{id}; } } else { # already there just update readings $readUpdates->{"networkCamera".$device->{id}."Url"} = BlinkCamera_getwebname( $hash ). BlinkCamera_ReplacePattern( $BlinkCamera_camerathumbnail, $device->{id}, $name ); } # do this only after storag of thumbnail being completed $readUpdates->{"networkCamera".$device->{id}."Thumbnail"} = $device->{thumbnail} ; } $readUpdates->{"networkCamera".$device->{id}."Firmware"} = $device->{fw_version} if ( defined( $device->{fw_version} ) ); $readUpdates->{"networkCamera".$device->{id}."Status"} = $device->{status} if ( defined( $device->{status} ) ); if ( defined( $device->{signals} ) ) { my $signal = $device->{signals}; $readUpdates->{"networkCamera".$device->{id}."Temp"} = $signal->{temp}; } } } } # loop through doorbells (lotus) and get the requested camera information if ( defined( $lotusList ) ) { foreach my $device ( @$lotusList ) { if ( $device->{network_id} eq $network ) { my $active = "disabled"; if ( defined( $device->{enabled} ) ) { $active = "armed" if ( $device->{enabled} == 1 ); $active = "armed" if ( $device->{enabled} eq "true" ); # $active = "armed" if ( $device->{enabled} ); } $readUpdates->{"networkCamera".$device->{id}} = $device->{name}.":".$active; $readUpdates->{"networkCamera".$device->{id}."Name"} = $device->{name}; $readUpdates->{"networkCamera".$device->{id}."Type"} = "lotus"; $readUpdates->{"networkCamera".$device->{id}."Active"} = $active; $readUpdates->{"networkCamera".$device->{id}."Enabled"} = defined($device->{enabled})?$device->{enabled}:"undef"; $cameraGets .= $device->{name}.",".$device->{id}.","; $cameras .= $device->{id}.":".$device->{name}."\n"; if ( defined( $device->{thumbnail} ) ) { # Load Thumbnail only if not already there if ( ( ! defined( $hash->{"thumbnail".$device->{id}."Url"} ) || ( $hash->{"thumbnail".$device->{id}."Url"} ne $device->{thumbnail} ) ) ) { if ( ! defined( $hash->{"thumbnail".$device->{id}."Req"} ) ) { # Debug "retreive thumbnail from homescreen for ".$device->{id}; # Do thumbnail request only of not already there (e.g. by polling) $hash->{"thumbnail".$device->{id}."Req"} = $device->{thumbnail}; BlinkCamera_DoCmd( $hash, "thumbnail", $device->{id}, "HIDDEN" ); } else { Log3 $name, 4, "BlinkCamera_ParseHomescreen $name: thumbnail Req already defined for ".$device->{id}; } } else { # already there just update readings $readUpdates->{"networkCamera".$device->{id}."Url"} = BlinkCamera_getwebname( $hash ). BlinkCamera_ReplacePattern( $BlinkCamera_camerathumbnail, $device->{id}, $name ); } # do this only after storag of thumbnail being completed $readUpdates->{"networkCamera".$device->{id}."Thumbnail"} = $device->{thumbnail} ; } $readUpdates->{"networkCamera".$device->{id}."Batt"} = $device->{battery} if ( defined( $device->{battery} ) ); $readUpdates->{"networkCamera".$device->{id}."Firmware"} = $device->{fw_version} if ( defined( $device->{fw_version} ) ); $readUpdates->{"networkCamera".$device->{id}."Status"} = $device->{status} if ( defined( $device->{status} ) ); $readUpdates->{"networkCamera".$device->{id}."Mode"} = $device->{doorbell_mode} if ( defined( $device->{doorbell_mode} ) ); $readUpdates->{"networkCamera".$device->{id}."Snooze"} = $device->{snooze} if ( defined( $device->{snooze} ) ); if ( defined( $device->{signals} ) ) { my $signal = $device->{signals}; $readUpdates->{"networkCamera".$device->{id}."SignalLfr"} = $signal->{lfr}; $readUpdates->{"networkCamera".$device->{id}."SignalWifi"} = $signal->{wifi}; $readUpdates->{"networkCamera".$device->{id}."SignalBattery"} = $signal->{battery}; } } } } $cameraGets .= "all"; $hash->{getoptions}->{liveview} = $cameraGets; $hash->{getoptions}->{getThumbnail} = $cameraGets; $hash->{getoptions}->{getInfoCamera} = $cameraGets; $hash->{setoptions}->{camEnable} = $cameraGets; $hash->{setoptions}->{camDisable} = $cameraGets; $readUpdates->{networkCameras} = $cameras; return $ret; } ##################################### # INTERNAL: Parse the cameraConfig results sub BlinkCamera_ParseCameraConfig($$$$) { my ( $hash, $result, $cam, $readUpdates ) = @_; my $name = $hash->{NAME}; my $ret; my $camera = $result->{camera}; Log3 $name, 4, "BlinkCamera_ParseCameraConfig $name: for Camera $cam "; $readUpdates->{"cameraConfig".$cam} = Dumper($camera)."\n"; return $ret; } ##################################### # INTERNAL: Parse the results of alerts to manage videos sub BlinkCamera_ParseStartAlerts($;$$$) { my ( $hash, $result, $page, $readUpdates ) = @_; my $name = $hash->{NAME}; my $ret; my $isLast = 0; Log3 $name, 4, "BlinkCamera_ParseStartAlerts $name: for page :".(defined($page)?$page:"--").": "; if ( ! defined( $page ) ) { # prepare for getting alerts $page = 0; my $lastUpdate = $hash->{updateTimestamp}; $lastUpdate = "2016-01-01T14:33:02" if( ! defined( $lastUpdate ) ); # normalize timestamps for queries and update/create timestamps (2016-11-02T21:43:49+00:00) $lastUpdate =~ s/\+.+$/Z/; # store old eventTimestamp and lastUpdate $hash->{eventTimestamp} = ReadingsVal($name,"eventTimestamp",""); $hash->{alertUpdate} = $lastUpdate; my @a = (); $hash->{alertResults} = \@a; } else { # Store results my $v = $result->{media}; push( @{$hash->{alertResults}}, @$v ); $isLast = ( BlinkCamera_IsLastAlertPage( $hash, $result ) ); } if ( $isLast ) { Log3 $name, 4, "BlinkCamera_ParseStartAlerts $name: Analyze the results now "; $ret = BlinkCamera_AnalyzeAlertResults( $hash, $hash->{alertResults}, $readUpdates ); # remove internal values / specifically results delete( $hash->{alertUpdate} ); delete( $hash->{alertResults} ); delete( $hash->{eventTimestamp} ); } else { $ret = BlinkCamera_DoCmd( $hash, "alerts", $page+1, "HIDDEN" ) if ( ! $isLast ); } return $ret; } ##################################### # INTERNAL: Callback is the callback for any nonblocking call to the bot api (e.g. the long poll on update call) # 3 params are defined for callbacks # param-hash # err # data (returned from url call) # empty string used instead of undef for no return/err value sub BlinkCamera_Callback($$$) { my ( $param, $err, $data ) = @_; my $hash= $param->{hash}; my $name = $hash->{NAME}; my $ret; my $cmdId; my $result; my $ll = 5; my $maxRetries; my %readUpdates = (); my $filename = $param->{filename}; my $cmd = $param->{cmd}; my $par1 = $param->{par1}; my $par2 = $param->{par2}; my $polling = ( defined($par2) ) && ($par2 eq "POLLING" ); my $hidden = ( ( defined($par2) ) && ($par2 eq "HIDDEN" ) ) || $polling; my $fullurl; my $repfilename; Log3 $name, 4, "BlinkCamera_Callback $name: called from ".($polling?"Polling":($hidden?"Hidden":"DoCmd")); Log3 $name, 4, "BlinkCamera_Callback $name: ". (defined( $err )?"status err :".$err:""). (defined( $filename )? ": data length ".(( defined( $data ) )?length($data):"")." filename :".$filename.":" : ": data ".(( defined( $data ) )?$data:"")); # Check for timeout "read from $hash->{addr} timed out" if ( $err =~ /^read from.*timed out$/ ) { $ret = "NonBlockingGet timed out on read from ".($param->{hideurl}?"":$param->{url})." after ".$param->{timeout}."s"; } elsif ( $err ne "" ) { $ret = "NonBlockingGet: returned $err"; } elsif ( $data ne "" ) { # assuming empty data without err means timeout my $jo; if ( defined( $filename ) ) { # write file with media # check for message json return if ( $data =~ /^\s*{\s*\"message\":\"(.*)\"\s*}\s*$/ ) { Log3 $name, 4, "BlinkCamera_Callback $name: data on file returned :$data:"; $ret = "Callback returned error:".$1.":"; } if ( ! $ret ) { # allow changing proxy dir -> from devname my $proxyDir = AttrVal($name,"proxyDir","/tmp/"); # filename - "BlinkCamera/".$name."/thumbnail/camera/".$par1."_1.jpg" $repfilename = $filename; $repfilename =~ s/\//_/g; Log3 $name, 4, "BlinkCamera_Callback $name: binary write file :".$repfilename; $ret = BlinkCamera_BinaryFileWrite( $hash, $proxyDir.$repfilename, $data ); $fullurl = BlinkCamera_getwebname( $hash ).$filename; } } else { Log3 $name, 5, "BlinkCamera_Callback $name: data returned :$data:"; eval { my $json = JSON->new->allow_nonref; # $jo = $json->decode(Encode::encode_utf8($data)); $jo = $json->decode($data); # $jo = BlinkCamera_Deepencode( $name, $jo ); }; if ( $@ ) { $ret = "Callback returned no valid JSON: $@ "; } elsif ( ! defined( $jo ) ) { $ret = "Callback returned no valid JSON !"; } elsif ( ref( $jo ) ne "HASH" ) { $ret = "Callback returned no valid JSON (no hash: ".ref( $jo ).")!"; } elsif ( $jo->{message} ) { $ret = "Callback returned error:".$jo->{message}.":"; $ret = "SUCCESS" if ( $jo->{message} =~ /^Successfully / ); # special case for pin verification: Client has been successfully verified $ret = "SUCCESS" if ( $jo->{message} =~ /^Client has been successfully / ); # reset authtoken if {"message":"Unauthorized Access"} --> will be re checked on next call delete( $hash->{AuthToken} ) if ( $jo->{message} eq "Unauthorized Access" ); } else { $result = $jo; } Log3 $name, 4, "BlinkCamera_Callback $name: after decoding status ret:".(defined($ret)?$ret:" ").":"; } } $ll = 2 if ( $hidden ); $hash->{POLLING} = 0 if ( $polling ); ################################################## $hash->{HU_DO_PARAMS}->{data} = ""; $readUpdates{cmd} = $cmd if ( ! $hidden ); if ( ! defined( $ret ) ) { # SUCCESS - parse results $ll = $BlinkCamera_specialLog; # clean up param hash delete( $param->{buf} ); Log3 $name, 4, "BlinkCamera_Callback $name: analyze result for cmd:$cmd:"; # handle different commands if ( $cmd eq "login" ) { $ret = BlinkCamera_ParseLogin( $hash, $result, \%readUpdates ); } elsif ( ($cmd eq "networks") ) { $ret = BlinkCamera_ParseNetworks( $hash, $result, \%readUpdates ); } elsif ( ($cmd eq "arm") || ($cmd eq "disarm" ) || ($cmd eq "camEnable" ) || ($cmd eq "camDisable" ) ) { # Debug "result :".Dumper( $result ); $cmdId = $result->{id} if ( defined( $result->{id} ) ); Log3 $name, 4, "BlinkCamera_Callback $name: cmd :$cmd: sent resulting in id : ".(defined($cmdId)?$cmdId:""); } elsif ( ($cmd eq "cameraConfig") ) { $ret = BlinkCamera_ParseCameraConfig( $hash, $result, $par1, \%readUpdates ); } elsif ($cmd eq "homescreen" ) { $ret = BlinkCamera_ParseHomescreen( $hash, $result, \%readUpdates ); } elsif ( ($cmd eq "cameraThumbnail") ) { # store cmd id also for thumbnail to wait for result $cmdId = $result->{id} if ( defined( $result->{id} ) ); # if ( ! defined($cmdId) ) { # # no commandid means done already --> so get the full update # BlinkCamera_DoCmd( $hash, "homescreen", undef, "POLLING" ); # } } elsif ($cmd eq "command" ) { if ( defined( $result->{complete} ) ) { if ( $result->{complete} ) { BlinkCamera_DoCmd( $hash, "homescreen", undef, "POLLING" ); } else { $cmdId = $result->{id} if ( defined( $result->{id} ) ); $ret = "waiting for command to be finished"; $maxRetries = 6; } } } elsif ( ($cmd eq "video" ) || ($cmd eq "thumbnail" ) ) { my $readTemplate; my $readName; if ($cmd eq "video") { $readTemplate = AttrVal($name,"videoTemplate",$BlinkCamera_vidTemplate); $readName = "video"; $readUpdates{videoFilename} = $repfilename; $readUpdates{videoID} = $par1; } else { $readTemplate = AttrVal($name,"imgTemplate",$BlinkCamera_imgTemplate); $readName = "networkCamera".$param->{par1}."Img"; # Store which thumbnail file is loaded already $hash->{"thumbnail".$par1."Url"} = $hash->{"thumbnail".$par1."Req"}; # set the thumbnail reading after receiving the file $readUpdates{"networkCamera".$par1."Thumbnail"} = $hash->{"thumbnail".$par1."Req"} ; delete( $hash->{"thumbnail".$par1."Req"} ); # $readUpdates{"networkCamera".$par1."Url"} = BlinkCamera_getwebname( $hash ). # BlinkCamera_ReplacePattern( $BlinkCamera_camerathumbnail, $par1, $name ); $readUpdates{"networkCamera".$par1."Url"} = BlinkCamera_getwebname( $hash ).$filename; my $proxyDir = AttrVal($name,"proxyDir","/tmp/"); $readUpdates{"networkCamera".$par1."File"} = $proxyDir.$repfilename; } $readTemplate =~ s/#URL#/$fullurl/g; $readTemplate =~ s/#ID#/$par1/g; $readUpdates{$readName} = $readTemplate; } elsif ($cmd eq "alerts" ) { $ret = BlinkCamera_ParseStartAlerts( $hash, $result, $par1, \%readUpdates ); } elsif ($cmd eq "liveview" ) { $readUpdates{liveVideo} = $result->{server}; $readUpdates{liveCam} = $par1; } else { } } $ret = "SUCCESS" if ( ! defined( $ret ) ); Log3 $name, $ll, "BlinkCamera_Callback $name: for cmd :$cmd: retry :".(defined($param->{args}[3])?$param->{args}[3]:"----")." resulted in :$ret: cmdId :".(defined($cmdId)?$cmdId:"--")." from ".($polling?"Polling":($hidden?"Hidden":"DoCmd")); if ( ! $polling ) { # cmd result intern also set if retried / cmdjson only if verbose > 3 $hash->{cmdResult} = $ret; if ( AttrVal($name,"verbose",AttrVal("global","verbose",3)) > 3 ) { if ( defined( $filename ) ) { $hash->{cmdJson} = (defined($data)?"length :".length($data):""); } else { # Debug "Result :".$data.":"; $hash->{cmdJson} = (defined($data)?$data:""); } } # handle retry # ret defined / args defined in params if ( ( $ret ne "SUCCESS" ) && ( defined( $param->{args} ) ) ) { my $wait = $param->{args}[3]; $maxRetries = AttrVal($name,'maxRetries',0) if ( ! defined( $maxRetries ) ); if ( ( defined($wait) ) && ( $wait <= $maxRetries ) ) { # calculate wait time 10s / 100s / 1000s ~ 17min / 10000s ~ 3h / 100000s ~ 30h # $wait = 3**$wait; # new wait time calc - 2 4 8 16 my $waittime = 2**$wait; Log3 $name, 4, "BlinkCamera_Callback $name: do retry ".$param->{args}[3]." timer: $waittime (ret: $ret) for cmd ". $param->{args}[0]; # set timer InternalTimer(gettimeofday()+$waittime, "BlinkCamera_RetryDo", $param,0); # finish return; } Log3 $name, 3, "BlinkCamera_Callback $name: Reached max retries (ret: $ret) for cmd ".$cmd; } } else { $hash->{pollResult} = $cmd." : ".$ret; } $hash->{doStatus} = ""; ######################### # Also set and result in Readings readingsBeginUpdate($hash); if ( ! $polling ) { readingsBulkUpdate($hash, "cmdResult", $ret ); } if ( ( $ret eq "SUCCESS" ) ) { foreach my $readName ( keys %readUpdates ) { if ( defined( $readUpdates{$readName} ) ) { readingsBulkUpdate($hash, $readName, $readUpdates{$readName} ); } else { CommandDeleteReading(undef,$readName) } } } readingsEndUpdate($hash, 1); ######################### # Wait for command completion if cmd Id found if ( ( $ret eq "SUCCESS" ) ) { # cmd sent / waiting for completion (so add command check) / completion reached add homescreen if ( ( defined( $cmdId ) ) ) { Log3 $name, 4, "BlinkCamera_Callback $name: start polling for cmd result"; BlinkCamera_DoCmd( $hash, "command", $cmdId, "HIDDEN" ); return ; } } ######################### # start next command in queue if available if ( ( defined( $hash->{cmdQueue} ) ) && ( scalar( @{ $hash->{cmdQueue} } ) ) ) { my $ref = shift @{ $hash->{cmdQueue} }; Log3 $name, 4, "BlinkCamera_Callback $name: handle queued cmd with :@$ref[0]: "; BlinkCamera_DoCmd( $hash, @$ref[0], @$ref[1], @$ref[2], @$ref[3] ); } } ############################################################################## ############################################################################## ## ## Web proxy handling ## ############################################################################## ############################################################################## ######################################################################################## # # CGI handling for medai and thumbnails of camera # camera thumbnail URL # /BlinkCamera//camera/thumbnail__. # --> /tmp/BlinkCamera__thumbnail__. # sub BlinkCamera_WebCallback($) { my ($URL) = @_; Log3 undef, 4, "BlinkCamera_WebCallback: ".$URL; # Remove prefix $URL =~ s/^\/BlinkCamera//i; # handle camera thumbnail if ( ($URL =~ m/^\/([^\/]+)\/(thumbnail|video)\//i) ) { # filename - "BlinkCamera/".$name."/thumbnail/camera/".$par1."_1.jpg" or "BlinkCamera//video/.mp4" my $devname = $1; my $urlfile = "BlinkCamera".uri_unescape($URL); Log3 undef, 4, "BlinkCamera_WebCallback: devname :$devname: urlfile :$urlfile: "; # allow changing proxy dir -> from devname my $proxyDir = AttrVal($devname,"proxyDir","/tmp/"); # normalize URL separator / into _ $urlfile =~ s/\//_/g; # let fhemweb handle the rest my $fullfile = $proxyDir.$urlfile; if ( -e $fullfile ) { Log3 undef, 5, "Found file in proxydir ".$urlfile.' from ('.$URL.')'; $urlfile =~ m/^(.*)\.(.*)$/; FW_serveSpecial($1, $2, $proxyDir, 0); return(undef, undef); } else { Log3 undef, 2, "File not found in proxydir ".$urlfile.' from ('.$URL.')'; } } # Wenn wir hier ankommen, dann konnte nichts verarbeitet werden... return ("text/html; charset=UTF8", "BlinkCamera_WebCallback could not handle: ".$URL); } ######################################################################################## # # Defines an extension (CGI call) to get pictures / media sub BlinkCamera_DefineWebExt() { # CGI definition my $name = "BlinkCamera"; my $baseurl = "/".$name ; $data{FWEXT}{$baseurl}{FUNC} = "BlinkCamera_WebCallback"; $data{FWEXT}{$baseurl}{LINK} = $name; $data{FWEXT}{$baseurl}{NAME} = undef; } ############################################################################## ############################################################################## ## ## Polling / Setup ## ############################################################################## ############################################################################## ##################################### # INTERNAL: PollInfo is called to queue the next getInfo and/or set the next timer sub BlinkCamera_PollInfo($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "BlinkCamera_PollInfo $name: called "; # Get timeout from attribute my $timeout = AttrVal($name,'pollingTimeout',0); if ( $timeout == 0 ) { $hash->{STATE} = "Static"; Log3 $name, 4, "BlinkCamera_PollInfo $name: Polling timeout 0 - no polling "; return; } $hash->{STATE} = "Polling"; if ( $hash->{POLLING} ) { Log3 $name, 4, "BlinkCamera_PollInfo $name: polling still running "; } else { $hash->{POLLING} = 1; my $ret = BlinkCamera_DoCmd( $hash, "homescreen", undef, "POLLING" ); Log3 $name, 1, "BlinkCamera_PollInfo $name: Poll call resulted in ".$ret." " if ( defined($ret) ); } Log3 $name, 4, "BlinkCamera_PollInfo $name: initiate next polling homescreen ".$timeout."s"; InternalTimer(gettimeofday()+$timeout, "BlinkCamera_PollInfo", $hash,0); } ###################################### # make sure a reinitialization is triggered on next update # sub BlinkCamera_ResetPollInfo($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "BlinkCamera_ResetPollInfo $name: called "; RemoveInternalTimer($hash); HttpUtils_Close($hash->{HU_DO_PARAMS}); $hash->{FAILS} = 0; # let all existing methods first run into block $hash->{POLLING} = 0; # wait some time before next polling is starting if( $init_done ) { InternalTimer(gettimeofday()+5, "BlinkCamera_PollInfo", $hash, 0); } else { InternalTimer(gettimeofday()+60, "BlinkCamera_PollInfo", $hash, 0); } Log3 $name, 4, "BlinkCamera_ResetPollInfo $name: finished "; } ###################################### # make sure a reinitialization is triggered on next update # sub BlinkCamera_Setup($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "BlinkCamera_Setup $name: called "; $hash->{STATE} = "Undefined"; # remove all readings ebside eventTimestamp to avoid addtl notifications my $eventTime = ReadingsVal($name,"eventTimestamp",undef); CommandDeleteReading(undef, "$name .*"); if ( defined( $eventTime ) ) { Log3 $name, 4, "BlinkCamera_Setup $name: init eventtimestamp with ".$eventTime; readingsSingleUpdate($hash, "eventTimestamp", $eventTime, 0 ); } foreach my $aKey ( keys %{$hash} ) { # "thumbnail".$device->{device_id}."Req" delete( $hash->{$aKey} ) if ( $aKey =~ /^thumbnail/ ); } my %sets = ( "login" => undef, "verifyPin" => undef, "arm" => undef, "disarm" => undef, "camEnable" => undef, "camDisable" => undef, "reset" => undef, "resetUniqueID" => undef, "videoDelete" => undef, "zDebug" => undef ); my %gets = ( "getNetworks" => undef, "getInfo" => undef, "getInfoCamera" => undef, "getThumbnail" => undef, "getVideoAlert" => undef, "cameraList" => undef, "liveview" => undef, ); $hash->{getoptions} = \%gets; $hash->{setoptions} = \%sets; my %hu_do_params = ( url => "", timeout => 30, method => "GET", header => $BlinkCamera_header, hideurl => 1, callback => \&BlinkCamera_Callback ); $hash->{HU_DO_PARAMS} = \%hu_do_params; $hash->{POLLING} = -1; $hash->{sourceVersion} = $repositoryID; # Temp?? SNAME is required for allowed (normally set in TCPServerUtils) $hash->{SNAME} = $name; # Ensure queueing is not happening delete( $hash->{cmdQueue} ); delete( $hash->{doStatus} ); delete( $hash->{cmd} ); delete( $hash->{cmdResult} ); delete( $hash->{cmdJson} ); delete( $hash->{pollResult} ); delete( $hash->{AuthToken} ); delete( $hash->{videos} ); delete( $hash->{updateTimestamp} ); delete( $hash->{video} ); delete( $hash->{videoFilename} ); delete( $hash->{videoID} ); delete( $hash->{alertSkipped} ); delete( $hash->{alertUpdate} ); delete( $hash->{alertResults} ); delete( $hash->{clientid} ); delete( $hash->{clientverreq} ); delete( $hash->{phoneverreq} ); delete( $hash->{URL} ); # remove timer for retry RemoveInternalTimer($hash->{HU_DO_PARAMS}); # cleanup proxy dir only blink files for current name my $proxyDir = AttrVal($name,"proxyDir","/tmp/"); if(opendir(DH, $proxyDir)) { my $pattern = "^BlinkCamera_".$name."_"; while(my $f = readdir(DH)) { Log3 $name, 4, "BlinkCamera_Setup $name: found file :$f: "; next if($f !~ /$pattern/ ); eval { unlink $proxyDir."/".$f; }; Log3 $name, 1, "BlinkCamera_Setup $name: file $f could not be deleted :$@: " if $@; } closedir(DH); } $hash->{URL} = ""; $hash->{STATE} = "Defined"; BlinkCamera_ResetPollInfo($hash); BlinkCamera_DefineWebExt(); Log3 $name, 4, "BlinkCamera_Setup $name: ended "; } ############################################################################## ############################################################################## ## ## HELPER ## ############################################################################## ############################################################################## ##################################### # INTERNAL: get pattern replaced sub BlinkCamera_getwebname( $ ) { my ( $hash ) = @_; my $wn = AttrVal($hash->{NAME},'webname',"fhem"); # might add parsing for removing trailing / leadings slashes $wn =~ s/^\/+//; $wn =~ s/\/+$//; return "/".$wn."/"; } ##################################### # INTERNAL: get pattern replaced sub BlinkCamera_ReplacePattern( $$;$ ) { my ( $pattern, $id, $name ) = @_; $pattern =~ s/q_id_q/$id/g if ( defined($id) ); $pattern =~ s/q_name_q/$name/g if ( defined($name) ); return $pattern; } ##################################### # INTERNAL: Get Id for a camera or list of all cameras if no name or id was given or undef if not found sub BlinkCamera_CheckSetGet( $$$ ) { my ( $hash, $cmd, $options ) = @_; if (!exists($options->{$cmd})) { my @cList; foreach my $k (keys %$options) { my $opts = undef; $opts = $options->{$k}; if (defined($opts)) { push(@cList,$k . ':' . $opts); } else { push (@cList,$k); } } # end foreach return "BlinkCamera_CheckSetGet: Unknown argument $cmd, choose one of " . join(" ", @cList); } # error unknown cmd handling return undef; } ##################################### # INTERNAL: Get Id for a camera or list of all cameras if no name or id was given or undef if not found sub BlinkCamera_CameraDoCmd( $$$ ) { my ( $hash, $cmd, $arg ) = @_; my $ret; if ( $arg eq "all" ) { my $cList = BlinkCamera_GetCameraId( $hash ); if ( ! defined( $cList ) ) { $ret = "No cameras found - try GetInfo first"; } else { foreach my $cam ( @$cList ) { my $sret = BlinkCamera_DoCmd( $hash, $cmd, $cam ); if ( defined( $sret ) ) { $sret = "CameraConfig for $cam returned ".$sret; $ret = (defined($ret)?$ret:"").$sret; } } } } else { my $cam = BlinkCamera_GetCameraId( $hash, $arg ); if ( ! defined( $cam ) ) { $ret = "Camera :$arg: not found - try GetInfo first"; } else { $ret = BlinkCamera_DoCmd( $hash, $cmd, $cam ); } } return $ret; } ##################################### # INTERNAL: Get Id for a camera or list of all cameras if no name or id was given or undef if not found sub BlinkCamera_GetCameraId( $;$ ) { my ( $hash, $name ) = @_; my $cameras = ReadingsVal($hash->{NAME},'networkCameras',""); my $ret; my @cameradefs = split( "\n", $cameras); if ( defined( $name ) ) { foreach my $cameradef ( @cameradefs ) { $cameradef =~ /^([^:]+):(.*)$/; $ret = $1 if ( ( $2 eq $name ) || ( $1 eq $name ) ); } } else { my @retList; foreach my $cameradef ( @cameradefs ) { $cameradef =~ /^([^:]+):(.*)$/; push( @retList, $1 ) if ( defined( $1 ) ); } $ret = \@retList; } return $ret; } ##################################### # INTERNAL: Get name for a camera or undef if not found sub BlinkCamera_GetCameraName( $$ ) { my ( $hash, $id ) = @_; my $cameras = ReadingsVal($hash->{NAME},'networkCameras',""); my $ret; my @cameradefs = split( "\n", $cameras); foreach my $cameradef ( @cameradefs ) { $cameradef =~ /^([^:]+):(.*)$/; $ret = $2 if ( ( $2 eq $id ) || ( $1 eq $id ) ); } return $ret; } ##################################### # INTERNAL: Either read attribute, if not set use Reading networks first line sub BlinkCamera_GetNetwork( $ ) { my ( $hash ) = @_; my $net = AttrVal($hash->{NAME},'network',undef); if ( ! defined( $net ) ) { # grab reading my $nets = ReadingsVal($hash->{NAME},'networks',"INVALID"); if ( ( defined( $nets ) ) && ( $nets ne "INVALID" ) && ( $nets =~ /^([^:]+):/ ) ) { $net = $1; } } return $net; } ##################################### # INTERNAL: Either read attribute, if not set use Reading networks first line sub BlinkCamera_GetCamType( $$ ) { my ( $hash, $camid ) = @_; return ReadingsVal($hash->{NAME},"networkCamera".$camid."Type","INVALID"); } ###################################### # write binary file for (hest hash, filename and the data # sub BlinkCamera_BinaryFileWrite($$$) { my ($hash, $fileName, $data) = @_; return "BlinkCamera_BinaryFileWrite - could not write ".$fileName.": ".$! if ( ! open BINFILE, '>'.$fileName ); binmode BINFILE; print BINFILE $data; close BINFILE; return undef; } ############################################################################## ############################################################################## ## ## Alert / event handling # "/api/v2/videos/changed?page=2&since=2016-01-24T14:33:02Z"; # store list of videos in hash/intern - id -> deleted -> created_at, updated_at, camera_id, viewed, video ## ############################################################################## ############################################################################## ##################################### # INTERNAL: Get a single json video entry -> returns $id, $deleted, $updated, $entryString sub BlinkCamera_GetAlertEntry( $$ ) { my ( $hash, $jentry ) = @_; my $id; my $deleted; my $updated = ""; my $entrystring = ""; # store list of videos in hash/intern - id -> deleted -> created_at, updated_at, camera_id, viewed, video if ( defined($jentry->{id}) ) { $id = $jentry->{id}; $entrystring .= $jentry->{created_at} if ( defined( $jentry->{created_at} ) ); $entrystring .= "|"; $updated = $jentry->{updated_at} if ( defined( $jentry->{updated_at} ) ); $entrystring .= $updated; $entrystring .= "|"; $entrystring .= $jentry->{device_id} if ( defined( $jentry->{device_id} ) ); $entrystring .= "|"; $entrystring .= (( $jentry->{watched} eq "true" )?"1":"") if ( defined( $jentry->{watched} ) ); $entrystring .= "|"; $entrystring .= $jentry->{media} if ( defined( $jentry->{media} ) ); $deleted = 0; $deleted = (( $jentry->{deleted} eq "true" )?1:0) if ( defined( $jentry->{deleted} ) ); } return ($id, $deleted, $updated, $entrystring); } ##################################### # INTERNAL: get url for a video from internal alert list for given id sub BlinkCamera_GetAlertVideoURL( $$ ) { my ( $hash, $vid) = @_; my $name = $hash->{NAME}; my $vidUrl; if ( defined( $hash->{videos} ) ) { my $entry = $hash->{videos}->{$vid}; if ( ( defined( $entry ) ) && ( $entry =~ /$BlinkCamera_alertEntry/ ) ) { $vidUrl = $5; } } return $vidUrl; } ##################################### # INTERNAL: analyze an alert page to check if a followup page might be needed sub BlinkCamera_IsLastAlertPage( $$ ) { my ( $hash, $jpage ) = @_; my $name = $hash->{NAME}; my ( $limit, $entries ); return 1 if ( ( ! defined( $jpage->{limit} ) ) || ( ! defined( $jpage->{media} ) ) ); $limit = $jpage->{limit}; $entries = 0; my $v = $jpage->{media}; $entries = scalar( @$v ) if ( ( defined( $jpage->{media} ) ) && ( ref( $v ) eq "ARRAY" ) ); Log3 $name, 4, "BlinkCamera_IsLastAlertPage $name: limit :$limit: / entries :$entries: "; return ( $limit > $entries ); } ##################################### # INTERNAL: analyze an alert page sub BlinkCamera_HandleAlertEntry( $$$$ ) { my ( $hash, $id, $deleted, $entry ) = @_; my $name = $hash->{NAME}; $hash->{videos} = () if ( ! defined( $hash->{videos} ) ); my $videos = $hash->{videos}; if ( $deleted ) { delete( $videos->{$id} ); } elsif ( defined( $videos->{$id} ) ) { # existing means just update the entry $videos->{$id} = $entry; } else { # non existing means new entry events also needed $videos->{$id} = $entry; # store list of videos in hash/intern - id -> deleted -> created_at, updated_at, camera_id, viewed, video # alertTime - # alertCamera - # alertVideo - # alertID - if ( $entry =~ /$BlinkCamera_alertEntry/ ) { my $alertTime = $1; my $alertCamera = $3; my $alertViewed = $4; my $alertVideo = $5; my $lastUpdate = $hash->{eventTimestamp}; Log3 $name, 5, "BlinkCamera_HandleAlertEntry $name: id :$id: alert time :$alertTime: "; Log3 $name, 5, "BlinkCamera_HandleAlertEntry lastUpdate was :$lastUpdate: and viewed :$alertViewed:"; if ( ( $alertTime gt $lastUpdate ) && ( length($alertViewed) == 0 ) ) { Log3 $name, 5, "BlinkCamera_HandleAlertEntry $name: id :$id: is new alert "; my $cname = BlinkCamera_GetCameraName( $hash, $alertCamera ); if ( defined( $cname ) ) { readingsBeginUpdate($hash); readingsBulkUpdate($hash, "alertVideo", $alertVideo ); readingsBulkUpdate($hash, "alertCamera", $alertCamera ); readingsBulkUpdate($hash, "alertCameraName", $cname ); readingsBulkUpdate($hash, "alertTime", $alertTime ); readingsBulkUpdate($hash, "alertID", $id ); readingsEndUpdate($hash, 1); } else { # cameraname not found --> ignore } } # eval eventTimestamp to check for latest and update event timestamp my $newestTS = ReadingsVal($name,"eventTimestamp",""); readingsSingleUpdate($hash, "eventTimestamp", $alertTime, 1 ) if ( $alertTime gt $newestTS ); } else { Log3 $name, 1, "BlinkCamera_HandleAlertEntry $name: parsing of alertEntry failed :$entry: "; } } } ##################################### # INTERNAL: analyze an alert page sub BlinkCamera_AnalyzeAlertResults( $$$ ) { my ( $hash, $jvarray, $readUpdates ) = @_; my $name = $hash->{NAME}; my $ret; Log3 $name, 5, "BlinkCamera_AnalyzeAlertResults $name: parsing of alertEntry entries:".scalar(@$jvarray).": ref:".ref($jvarray).": "; return "BlinkCamera_AnalyzeAlertPage: No videos found" if ( ( ! defined( $jvarray ) ) || ( ref( $jvarray ) ne "ARRAY" ) ); # ensure the internal video hash is existing if ( ! defined( $hash->{videos} ) ) { my %h = (); $hash->{videos} = \%h; } my $lastUpdate = $hash->{updateTimestamp}; $lastUpdate = "" if ( ! defined( $lastUpdate ) ); foreach my $video ( reverse( @$jvarray ) ) { my ( $id, $deleted, $updated, $entry ) = BlinkCamera_GetAlertEntry( $hash, $video ); # set reading if time is later meaning > Log3 $name, 5, "BlinkCamera_AnalyzeAlertResults $name: id :$id: update :$updated: last :$lastUpdate: compare ".(( $updated gt $lastUpdate )?"newer":"-"); if ( $updated gt $lastUpdate ) { $hash->{updateTimestamp} = $updated; $lastUpdate = $updated; } # handle newly received entry BlinkCamera_HandleAlertEntry( $hash, $id, $deleted, $entry ) if ( defined( $id ) ); } return $ret; } ############################################################################## ############################################################################## ## ## Documentation ## ############################################################################## ############################################################################## 1; =pod =item summary interact with Blink Home (Security) cameras =item summary_DE steuere Blink Heim- / Sicherheits-kameras =begin html

BlinkCamera

    This module connects remotely to a system of Blink Home Cameras Blink Home Cameras are relatively inexpensive wire-free video home security & monitoring system

    Disclaimer: Since there are no official APIs for the blink cameras, there is no guarantee for this module to continue working. Several changes over the years have caused temporary outages due to incompatibe changes done by the provider of the Blink cameras.

    The blink device contains the possibility to regular poll for updates (i.e. specifically for notifications/alerts) MOst commands that change configurations are not synchronous, but the result will be returned after polling for status information. This is automatically handled in the device and the result of the cmd is marked in the reading cmdResult with the value "SUCCESS".
    Traditional Blink cameras and also the BlinkMini types should work
    The blink device also contains a proxy for retrieving videos and thumbnails throug an FHEMweb extension in the form of http://<fhem>:<fhemwebport>/fhem/BlinkCamera/<name of the blink device>/...

    Define
      define <name> BlinkCamera <email> [ <password> ]

      Defines a BlinkCamera device, which connects to the cloud servers with the given user name and password (as provided during registration / setup)

      The password will be removed from the define and stored separately (so needs to be given on the initial define)

      Example: define blink BlinkCamera ichbins@nicht.de abc123



    Set
      set <name> <what> [<value>]

      where <what> / <value> is one of

    • login
      Initiate a login to the blink servers. This is usually done automatically when needed or when the login is expired
    • verifyPin
      can be used to verify the pin send from blink via email/sms
    • arm or disarm
      All enabled cameras in the system will be armed (i.e. they will be set to a mode where alarms/videos are automatically created based on the current settings) / disarmed (set to inactive mode where no video is recorded.
    • camEnable <camera name or number or "all"> or camDisable <camera name or number>
      The specified camera will be enabled (i.e. so that it is included in the arm / disarm commands) / disabled (excluded from arm/disarm).
    • reset
      Reset the FHEM device (only used in case of something gets into an unknown or strange state)
    • resetUniqueID
      Reset the FHEM device (only used in case of something gets into an unknown or strange state). Additionally the uniqueID that is used in the device loing will be reset in this option.
    • videoDelete <video id>
      The video with the given id will be removed (both from the local filesystem and from the blink servers)


    Get
      get <name> <what> [<value>]

      where <what> / <value> is one of

    • getNetworks
      Retrieve the networks defined in the blink account. This is needed for further operations and information request to blink. For specifying a specific network (id), the attribute network can be also set
    • getInfo
      Get information about the system from the blink servers (including cameras and state) . This is usually done automatically based on the reular interval specified in attribute pollingTimeout
    • getInfoCamera <camera name or number or "all">
      Get the information about the specified camera from the blink system. Currently the information about the camera is just stored in raw json format in a single reading cameraConfig<camera id>
    • getThumbnail <camera name or number or "all">
      Request a new thumbnail being taken from the specified camera in the blink system. The thumbnail is not automatically retrieved, this can be done using getInfoCamera
    • getVideoAlert [ <video id> ]
      Retrieve the video for the corresponding id (or if ommitted as specified in the reading alertID) and store the video in a local file in the directory given in the attribute proxyDir
    • liveview <camera name or number or "all">
      Request a link to the live video stream. The live video stream access (URL) will be stored in the reading liveVideo (the associated cam id in liveCam). The link to the video is an rtsp - which can be shown in video players like VLC.
      Note: Live video streaming might have a substantially negative effect on battery life


    Attributes

    • maxRetries <0|1|2...|9>
      Defines the number of retries that are done in case of a failure of a command. Pauses between retries are done with an increasing delay between calls.
    • network <network id>
      This attribute is needed if your blink system contains more than one network. If not specified the first netowrk defined in the account is used
    • proxyDir <directory path>
      Specify the path where temporary files (videos, thumbnails) are stored to be access via the proxy server built into the device as an fhemweb extension
    • pollingTimeout <interval>
      Interval in which the system is checking for status updates from the blink servers (given in seconds - value 0 means no polling). This is the frequency in which new alerts can be received
    • imgTemplate <HTML template for reading>
      Give an HTML template for the image reading that shows the thumbnail of a camera. Default is a template which shows the image a link to the image and also the url as text. In the template the string #URL# will be replaced with the actual URL
    • imgOriginalFile <1 or 0>
      If set to 1 it will keep the original filename of the thumbnail when storing ti. With setting this new thumbnails will not overwrite existing ones.
      NOTE: No cleanup of thumbnails is done, so over time more and more thumbnails will be stored in the proxydir.
    • vidTemplate <HTML template for reading>
      Give an HTML template for the video reading that shows the video of a notification from the camera. Default is a template which shows the video a link to the video and also the url and id as text. In the template the string #URL# will be replaced with the actual URL of the video and #ID# will be replaced by the video ID.
    • webname <path to fhem web>
      can be set if fhem is not accessible through the standard url of FHEMWeb /fhem/... (Default value is fhem).
    • homeScreenV3 <1 or 0>
      If set to 1 (default) the new version 3 of the blink API will be used. Unfortunately this includes different readings and settings
      NOTE: This attribute is deprecated and not needed anymore, since the old API has been switched off by Blink (default is on = 1)


    Readings
    • cmd <internal name of the last executed command>
      Used to identify the cmd that was last executed and where the result is given in cmdResult
    • cmdResult <error message or SUCCESS>
      Used to identify success or failure of a command

    • networks <list of networks>
      Lists the defined networks for the account at blink in the form networkid:networkname
    • networkName <name>
      Name of the network that is currently used to fill the readings
    • networkArmed <status>
      Network arm status (true or false)
    • networkStatus <ok or failure>
      Basic status of the current network
    • networkCameras <number>
      Lists the defined cameras in the current network in the form cameraid:cameraname
    • networkSyncModule <id and status>
      Information about the syncmodule in the current network in the form syncid:syncmodulestatus

    • networkCamera...
      Set of readings specific for each camera (identified by the cameraID in the reading name). Providing status and name of the camera / most recent thumbnail / url for the thumbnail to the proxy
    • networkCameraEnabled
      Shows the enabled (active) status of the camera (known values: 0 / 1 / "undef" - last value will be given if this value is not contained in the camerainfo

    • alert...
      Set of readings specifying the last alert (movement alert) coming from one of the devices. Especially the ID is relevant here for accessing the corresponding video secifically. Additionally the camera, time and video url are specifed here.


=end html =cut