package Reolink; =head1 NAME Reolink - perl module that implements an interface to Reolink IP cameras For more complete information, See official documentation at https://drive.google.com/drive/folders/19vQBJia0wKvzwscA-EpTDSFV5OfNYUL6 =head1 SYNOPSIS use Reolink; =head1 DESCRIPTION TODO =head1 AUTHOR Jon duSaint EFE =cut use strict; use warnings; use Carp; use Data::Dumper; use HTTP::Request; use IO::Socket::SSL; use JSON; use LWP::UserAgent; our $VERSION = '1.1'; # Reolink defaults my ($default_user, $default_pass, $default_proto) = ('admin', '', 'http'); sub make_url { my $this = shift; $this->{url} = "$this->{proto}://$this->{host}/cgi-bin/api.cgi"; } # Internal function to actually invoke the API sub api { my ($this, %args) = @_; unless ($this->{http}) { $this->{http} = LWP::UserAgent->new (agent => "Reolink/$VERSION"); $this->{http}->ssl_opts (SSL_verify_mode => SSL_VERIFY_NONE, verify_hostname => 0); } my $json = { cmd => $args{cmd} }; $json->{action} = $args{action} if $args{action}; $json->{param} = $args{param} // {}; #print Dumper $json; my $token = $this->{token} // 'null'; my $url = "$this->{url}?cmd=$args{cmd}&token=$token"; my $req = HTTP::Request->new ('POST', $url, ['Content-Type' => 'application/json', 'Accept' => 'application/json'], encode_json ([$json])); my $resp = $this->{http}->request ($req); if ($resp->is_redirect && $this->{proto} eq 'http') { $this->{proto} = 'https'; make_url ($this); return api ($this, %args); } if ($this->{debug}) { if (! $resp->is_success) { print Dumper $resp; } } my $ret = $resp->is_success ? $resp->decoded_content : '[{"code":-1}]'; my $retref = decode_json ($ret); carp "Request for command '$args{cmd} failed" unless $resp->is_success; return $retref->[0]; } =head1 METHODS =over 1 =item C<< new(host => 'hostname') >> =item C<< new(host => 'hostname', user|username => 'username', pass|password => 'password', proto => 'http|https') >> Create a new C object C defaults to C<'admin'>, C defaults to empty, and C defaults to C. =back =cut sub new { my ($class, %args) = @_; my $this = { host => $args{host} // croak ("Missing 'host' parameter"), user => $args{user} // $args{username} // $default_user, pass => $args{pass} // $args{password} // $default_pass, proto => $args{proto} // $default_proto, token => undef, errors => 0, image_quality_workaround => $args{image_quality_workaround} ? 1 : 0, debug => $args{debug} ? 1 : 0 }; make_url ($this); bless $this, $class; } ################################################################################ # # All the commands with identical calling conventions # # { "cmd" : "$cmd" } my @basic_commands = qw/ GetDevInfo GetHddInfo Reboot GetAutoUpgrade CheckFirmware UpgradeOnline UpgradeStatus GetChannelstatus Logout GetOnline ScanWifi GetIrLights GetPowerLed /; # { "cmd" : "$cmd", "action" : 1 } my @basic_action_commands = qw/ GetTime GetAutoMaint GetUser GetLocalLink GetDdns GetEmail GetFtp GetFtpV20 GetNtp GetNetPort GetUpnp GetWifi GetWifiSignal GetPush GetP2p GetNorm GetAudioAlarm /; # { "cmd" : "$cmd", "action" : 1, "param" : { "channel" : $channel } } my @channel_commands = qw/ GetDevName GetEmailV20 GetPushV20 GetImage GetOsd GetIsp GetMask GetEnc GetRec GetRecV20 GetPtzPreset GetPtzPatrol GetPtzSerial GetPtzTattern GetAutoFocus GetMdAlarm GetMdState GetAudioAlarmV20 GetBuzzerAlarmV20 GetWhiteLed /; # Generate method definitions for the above lists { no strict 'refs'; foreach my $pair (([\@basic_commands, sub { $_[0]->api (cmd => $_[1]) }], [\@basic_action_commands, sub { $_[0]->api (cmd => $_[1], action => 1) }], [\@channel_commands, sub { $_[0]->api (cmd => $_[1], action => 1, param => {channel => $_[2]}) }])) { foreach my $cmd (@{$pair->[0]}) { *{"Reolink::$cmd"} = sub { my $resp = $pair->[1] ($_[0], $cmd, @_[1..$#_]); maybe_handle_errors ($_[0], $cmd, $resp); $resp; }; } } } =over 2 =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =item C =back =cut ################################################################################ # # All the commands that require parameters # # # Wrap a call to the API, including the call to handle errors # # The subroutine should return a list of args to api (generally "param => { ... }"). # sub wrap_api(&@) { my $sub = shift; my $this = $_[0]; my $cmd = (split (/::/, (caller (1))[3]))[-1]; my $resp = $this->api (cmd => $cmd, &$sub (@_)); $this->maybe_handle_errors ($cmd, $resp); $resp; } =over 2 =item C Show the abilities of the specified user. =back =cut sub GetAbility { wrap_api { ( param => { User => {userName => $_[1] } } ) } @_; } =over 2 =item C Set the name of the camera. This name shows up in the on-screen display. =back =cut sub SetDevName { wrap_api { ( param => { DevName => { name => $_[1] } } ) } @_; } =over 2 =item C Set the time on the device. =item C Just enable daylight savings time. =back =cut sub SetTime { if (@_ == 1 || @_ == 7) { wrap_api { if (@_ == 1) { ( param => { Dst => { enable => $_[1] // 1 } } ) } else { ( param => { Time => { year => $_[1], mon => $_[2], day => $_[3], hour => $_[4], min => $_[5], sec => $_[6], timeZone => -28800, timeFmt => "DD/MM/YYYY", hourFmt => 0 } } ) } } @_; } else { +{ code => 0 }; } } sub unimplemented { return +{ cmd => $_[0], code => 1, error => { rspCode => 1, detail => "$_[0] is unimplemented in Reolink.pm" } }; } unimplemented 'SetAutoMaint'; unimplemented 'Format'; unimplemented 'Upgrade'; unimplemented 'Restore'; unimplemented 'UpgradePrepare'; unimplemented 'SetAutoUpgrade'; =over 2 =item C Log in to the device. This should be the first method invoked. =back =cut sub Login { my ($this) = @_; my $resp = $this->api (cmd => 'Login', param => { User => { userName => $this->{user}, password => $this->{pass} } }); if ($resp->{code} == 0) { $this->{token} = $resp->{value}->{Token}->{name}; return 1; } 0; } unimplemented 'AddUser'; unimplemented 'DelUser'; unimplemented 'ModifyUser'; unimplemented 'Disconnect'; unimplemented 'SetLocalLink'; unimplemented 'SetDdns'; unimplemented 'SetEmail'; unimplemented 'SetEmailV20'; unimplemented 'TestEmail'; unimplemented 'SetFtp'; unimplemented 'SetFtpV20'; unimplemented 'TestFtp'; =over 2 =item C Set NTP values on the device. C turns it on or off. C, C, and C are all optional, and set the corresponding attributes. =back =cut sub SetNtp { my ($this, $enable, $server, $port, $interval) = @_; my $req = {}; $req->{enable} = $enable if defined $enable; $req->{server} = $server if $server; $req->{port} = $port if $port; $req->{interval} = $interval if $interval; if (keys (%$req) == 0) { carp "No ntp params to set"; return 0; } my $resp = $this->api (cmd => 'SetNtp', param => {Ntp => $req}); $resp->{code}; } unimplemented 'SetNetPort'; unimplemented 'SetUpnp'; unimplemented 'SetWifi'; unimplemented 'TestWifi'; unimplemented 'SetPush'; unimplemented 'SetPushV20'; unimplemented 'SetP2p'; unimplemented 'SetNorm'; unimplemented 'SetImage'; =over 2 =item C Set on-screen display parameters. "Channel" name is the name of the camera as set here or with C. Both "Pos" parameters specify where to display that datum. Valid values are: C C C C C C Unconditionally disables the "Reolink" watermark. =back =cut sub SetOsd { wrap_api { ( param => { Osd => { channel => $_[1], watermark => 0, osdChannel => { enable => $_[2], name => $_[3], pos => $_[4] }, osdTime => { enable => $_[5], pos => $_[6] } } } ) } @_; } unimplemented 'SetIsp'; unimplemented 'SetMask'; unimplemented 'Preview'; unimplemented 'GetCrop'; unimplemented 'SetCrop'; unimplemented 'SetEnc'; unimplemented 'SetRec'; unimplemented 'SetRecV20'; unimplemented 'Search'; unimplemented 'Download'; =over 2 =item C Take a photo and save it to the file specified. This might take a few seconds to run. =back =cut use Image::Magick; sub Snap { my ($this, $filename) = @_; # Generate random 16 char token. my @valid = ("A".."Z", "a".."z", "0".."9", "-", "_"); my $tok = join "", map { $valid[ int (rand (256)) % @valid] } 1..16; my $url = "$this->{url}?cmd=Snap&channel=0&rs=$tok&token=$this->{token}"; my $resp = $this->{http}->post ($url); unless ($resp->is_success) { use Data::Dumper; carp "Failed to take snapshot"; print Dumper $resp; return 1; } if ($this->{image_quality_workaround}) { my $image = Image::Magick->new (); $image->BlobToImage ($resp->decoded_content); $image->Set (quality => 66); if ($image->Get ('type') ne 'TrueColor') { print "Switching type from " . $image->Get ('type') . " to TrueColor\n"; $image->Set (type => 'TrueColor'); } $image->Write (filename => $filename); } else { my $fh; unless (open ($fh, ">$filename")) { carp "Failed to open '$filename': $!"; return 1; } binmode $fh; print $fh $resp->decoded_content; close $fh; } 0; } unimplemented 'Playback'; unimplemented 'NvrDownload'; unimplemented 'SetPtzPreset'; unimplemented 'SetPtzPatrol'; unimplemented 'PtzCtrl'; unimplemented 'SetPtzSerial'; unimplemented 'SetPtzTattern'; unimplemented 'SetAutoFocus'; unimplemented 'GetAlarm'; unimplemented 'SetAlarm'; unimplemented 'SetMdAlarm'; unimplemented 'SetAudioAlarm'; unimplemented 'SetAudioAlarmV20'; unimplemented 'SetBuzzerAlarmV20'; unimplemented 'rtmp=start'; unimplemented 'rtmp=stop'; unimplemented 'rtmp=auth'; =over 2 =item C Set the IR LED state. Valid values for C are C, C, and C. =back =cut sub SetIrLights { wrap_api { ( param => { IrLights => { channel => $_[1], state => $_[2] } } ) } @_; } =over 2 =item C Set the state of the power LED. Valid values for C are C and C =back =cut sub SetPowerLed { wrap_api { ( param => { PowerLed => { channel => $_[0], state => $_[2] } } ) } @_; } =over 2 =item C { short => 'invalid parameters', long => 'method called with invalid parameters' }, 1 => { short => 'unimplemented', long => 'Method not implemented' }, 0 => { short => "success", long => "success" }, -1 => { short => "not exist", long => "Missing parameters" }, -2 => { short => "out of mem", long => "Used up memory" }, -3 => { short => "check err", long => "Check error" }, -4 => { short => "param error", long => "Parameters error" }, -5 => { short => "max session", long => "Reached the max session number." }, -6 => { short => "please login first", long => "Login required" }, -7 => { short => "login failed", long => "Login error" }, -8 => { short => "timeout", long => "Operation timeout" }, -9 => { short => "not support", long => "Not supported" }, -10 => { short => "protocol", long => "Protocol error" }, -11 => { short => "fcgi read failed", long => "Failed to read operation" }, -12 => { short => "get config failed", long => "Failed to get configuration." }, -13 => { short => "set config failed", long => "Failed to set configuration." }, -14 => { short => "malloc failed", long => "Failed to apply for memory" }, -15 => { short => "create socket failed", long => "Failed to created socket" }, -16 => { short => "send failed", long => "Failed to send data" }, -17 => { short => "rcv failed", long => "Failed to receiver data" }, -18 => { short => "open file failed", long => "Failed to open file" }, -19 => { short => "read file failed", long => "Failed to read file" }, -20 => { short => "write file failed", long => "Failed to write file" }, -21 => { short => "error token", long => "Token error" }, -22 => { short => "The length of the string exceeds the limit", long => "The length of the string exceeds the limitmation" }, -23 => { short => "missing param", long => "Missing parameters" }, -24 => { short => "error command", long => "Command error" }, -25 => { short => "internal error", long => "Internal error" }, -26 => { short => "ability error", long => "Ability error" }, -27 => { short => "invalid user", long => "Invalid user" }, -28 => { short => "user already exist", long => "User already exist" }, -29 => { short => "maximum number of users", long => "Reached the maximum number of users" }, -30 => { short => "same version", long => "The version is identical to the current one." }, -31 => { short => "busy", long => "Ensure only one user can upgrade" }, -32 => { short => "ip conflict", long => "Modify IP conflicted with used IP" }, -34 => { short => "need bing email", long => "Cloud login need bind email first" }, -35 => { short => "unbind", long => "Cloud login unbind camera" }, -36 => { short => "network timeout", long => "Cloud login get login information out of time" }, -37 => { short => "password err", long => "Cloud login password error" }, -38 => { short => "uid err", long => "Cloud bind camera uid error" }, -39 => { short => "user not exist", long => "Cloud login user doesn’t exist" }, -40 => { short => "unbind failed", long => "Cloud unbind camera failed" }, -41 => { short => "cloud not support", long => "The device doesn’t support cloud" }, -42 => { short => "login cloud server failed", long => "Cloud login server failed" }, -43 => { short => "bind failed", long => "Cloud bind camera failed" }, -44 => { short => "cloud unknown err", long => "Cloud unknown error" }, -45 => { short => "need verify code", long => "Cloud bind camera need verify code" }, -48 => { short => "Fetching a picture failed", long => "Snap a picture failed" }, -100 => { short => "test failed", long => "Test Email、Ftp、Wifi failed" }, -101 => { short => "check firmware failed", long => "Upgrade checking firmware failed" }, -102 => { short => "download online failed", long => "Upgrade download online failed" }, -103 => { short => "get upgrade status failed", long => "Upgrade get upgrade status failed" } ); =over 2 =item C Return a string that describes the error id. =item C Return a short string that describes the error id. =back =cut sub ErrorString { my $this = shift; my $id = shift; unless (defined $error_strings{$id}) { carp "Invalid error id '$id'"; return 'Invalid error id'; } if (defined $_[0] && ($_[0] eq 'short' || $_[0] == 0)) { $error_strings{$id}->{short}; } else { $error_strings{$id}->{long}; } } =over 2 =item C Enable or disable internal error reporting. When set, and when an API call results in an error, methods will C the error string. =back =cut sub Errors { my ($this, $enable) = @_; unless (defined $enable) { return $this->{errors}; } $this->{errors} = $enable; } # Internal helper function for error handling sub maybe_handle_errors { my ($this, $cmd, $resp) = @_; if ($this->{errors} && $resp->{code}) { carp "$cmd error: ".$this->ErrorString ($resp->{error}->{rspCode}); return 1; } return 0; } 1;