mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-03-26 09:42:42 -04:00
830 lines
18 KiB
Perl
830 lines
18 KiB
Perl
# ==========================================================================
|
|
#
|
|
# ZoneMinder Reolink HTTP API Control Protocol Module
|
|
# Copyright (C) 2026 ZoneMinder Community
|
|
#
|
|
# This program 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.
|
|
#
|
|
# This program 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 this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
# ==========================================================================
|
|
#
|
|
# This module contains the implementation of the Reolink HTTP API control
|
|
# protocol using api.cgi endpoint
|
|
#
|
|
package ZoneMinder::Control::Reolink_HTTP;
|
|
|
|
use 5.006;
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Time::HiRes qw( usleep );
|
|
|
|
require ZoneMinder::Base;
|
|
require ZoneMinder::Control;
|
|
require LWP::UserAgent;
|
|
use URI;
|
|
use HTTP::Request;
|
|
|
|
our @ISA = qw(ZoneMinder::Control);
|
|
|
|
our %CamParams = ();
|
|
|
|
# ==========================================================================
|
|
#
|
|
# Reolink HTTP API Control Protocol using api.cgi
|
|
#
|
|
# On Control Address use the format:
|
|
# USERNAME:PASSWORD@ADDRESS:PORT
|
|
# eg: admin:password@10.1.2.1:80
|
|
# admin:mypass@192.168.1.100:80
|
|
#
|
|
# Control Device can be left empty or set to camera channel (usually 0)
|
|
#
|
|
# This module uses the Reolink HTTP API (api.cgi) rather than ONVIF
|
|
# See: https://support.reolink.com/hc/en-us/articles/360007010473-CGI-API-User-Guide
|
|
# https://github.com/verheesj/reolink-api
|
|
#
|
|
# ==========================================================================
|
|
|
|
use ZoneMinder::Logger qw(:all);
|
|
use ZoneMinder::Config qw(:all);
|
|
use JSON;
|
|
|
|
sub new {
|
|
my $class = shift;
|
|
my $id = shift;
|
|
my $self = ZoneMinder::Control->new($id);
|
|
bless($self, $class);
|
|
return $self;
|
|
}
|
|
|
|
sub open {
|
|
my $self = shift;
|
|
|
|
$self->loadMonitor();
|
|
|
|
# Initialize user agent
|
|
$self->{ua} = LWP::UserAgent->new;
|
|
$self->{ua}->agent('ZoneMinder Control Agent/'. ZoneMinder::Base::ZM_VERSION);
|
|
$self->{ua}->timeout(10);
|
|
|
|
# Use parent class guess_credentials to parse ControlAddress
|
|
$self->guess_credentials();
|
|
|
|
# Set defaults if not set by guess_credentials
|
|
$$self{username} = 'admin' unless $$self{username};
|
|
$$self{password} = '' unless $$self{password};
|
|
$$self{port} = 80 unless $$self{port};
|
|
|
|
Debug("Reolink HTTP: Using credentials for $$self{host}:$$self{port}, user: $$self{username}");
|
|
|
|
# Get channel from ControlDevice or default to 0
|
|
$$self{channel} = $self->{Monitor}->{ControlDevice} || 0;
|
|
|
|
# Initialize token variables
|
|
$$self{token} = 'null';
|
|
$$self{token_expiry} = 0;
|
|
$$self{token_lease_time} = 3600; # Default 1 hour
|
|
|
|
# Perform login to get token
|
|
if ($self->login()) {
|
|
$self->{state} = 'open';
|
|
Info('Reolink HTTP API connection established with token');
|
|
return ! undef;
|
|
}
|
|
|
|
Warning('Reolink HTTP API login failed');
|
|
$self->{state} = 'closed';
|
|
return undef;
|
|
}
|
|
|
|
sub close {
|
|
my $self = shift;
|
|
$self->logout() if $$self{token} && $$self{token} ne 'null';
|
|
$self->{state} = 'closed';
|
|
}
|
|
|
|
sub login {
|
|
my $self = shift;
|
|
|
|
Debug('Reolink: Attempting login');
|
|
|
|
# Build login request
|
|
my $login_cmd = {
|
|
cmd => 'Login',
|
|
action => 0,
|
|
param => {
|
|
User => {
|
|
userName => $$self{username},
|
|
password => $$self{password}
|
|
}
|
|
}
|
|
};
|
|
|
|
my $json = encode_json([$login_cmd]);
|
|
my $url = 'http://'.$$self{host}.':'. $$self{port}.'/cgi-bin/api.cgi?cmd=Login&token=null';
|
|
|
|
$self->printMsg($json, 'Tx');
|
|
|
|
my $req = HTTP::Request->new(POST => $url);
|
|
$req->content_type('application/json');
|
|
$req->content($json);
|
|
|
|
my $res = $self->{ua}->request($req);
|
|
|
|
if ($res->is_success) {
|
|
my $content = $res->decoded_content;
|
|
Debug("Login response: $content");
|
|
|
|
my $response;
|
|
eval {
|
|
$response = decode_json($content);
|
|
};
|
|
if ($@) {
|
|
Error("Failed to parse login response: $@");
|
|
}
|
|
if (ref($response) eq 'ARRAY' && @$response > 0) {
|
|
my $result = $response->[0];
|
|
|
|
if ($result->{code} == 0 && $result->{value} && $result->{value}->{Token}) {
|
|
$$self{token} = $result->{value}->{Token}->{name};
|
|
$$self{token_lease_time} = $result->{value}->{Token}->{leaseTime} || 3600;
|
|
$$self{token_expiry} = time() + $$self{token_lease_time};
|
|
|
|
Debug("Reolink login successful, token: $$self{token}, expires in: $$self{token_lease_time}s");
|
|
return !undef;
|
|
} else {
|
|
my $code = $result->{code} || 'unknown';
|
|
my $detail = $result->{error}->{detail} || 'unknown error';
|
|
Error("Reolink login failed with code $code: $detail");
|
|
}
|
|
}
|
|
} else {
|
|
Error('Reolink login HTTP request failed: '.$res->status_line);
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub logout {
|
|
my $self = shift;
|
|
|
|
return unless $$self{token} && $$self{token} ne 'null';
|
|
|
|
Debug('Reolink: Logging out');
|
|
|
|
my $logout_cmd = {
|
|
cmd => 'Logout',
|
|
action => 0,
|
|
param => {}
|
|
};
|
|
|
|
$self->sendApiCmd($logout_cmd);
|
|
|
|
$$self{token} = 'null';
|
|
$$self{token_expiry} = 0;
|
|
}
|
|
|
|
sub ensureToken {
|
|
my $self = shift;
|
|
|
|
# Check if token needs refresh (within 60 seconds of expiry)
|
|
if (!$$self{token} || $$self{token} eq 'null' || time() >= ($$self{token_expiry} - 60)) {
|
|
Debug('Token expired or expiring soon, refreshing...');
|
|
return $self->login();
|
|
}
|
|
|
|
return ! undef;
|
|
}
|
|
|
|
sub sendApiCmd {
|
|
my $self = shift;
|
|
my $cmd = shift;
|
|
my $result = undef;
|
|
|
|
# Ensure we have a valid token
|
|
return undef unless $self->ensureToken();
|
|
|
|
# Build the POST request
|
|
my @cmds = ref($cmd) eq 'ARRAY' ? @$cmd : ($cmd);
|
|
my $json = encode_json(\@cmds);
|
|
|
|
$self->printMsg($json, 'Tx');
|
|
|
|
# Construct URL with token
|
|
my $url = 'http://'.$$self{host}.':'.$$self{port}.'/cgi-bin/api.cgi?cmd='.$cmds[0]->{cmd}.'&token='.$$self{token};
|
|
|
|
my $req = HTTP::Request->new(POST => $url);
|
|
$req->content_type('application/json');
|
|
$req->content($json);
|
|
|
|
my $res = $self->{ua}->request($req);
|
|
|
|
if ($res->is_success) {
|
|
$result = $res->decoded_content;
|
|
Info('Camera control success: '.$res->status_line().' for '.$cmds[0]->{cmd});
|
|
|
|
# Try to decode JSON response
|
|
eval {
|
|
my $response = decode_json($result);
|
|
# Check for error in response
|
|
if (ref($response) eq 'ARRAY' && @$response > 0) {
|
|
if (defined($response->[0]->{code}) && $response->[0]->{code} != 0) {
|
|
my $code = $response->[0]->{code};
|
|
my $detail = $response->[0]->{error}->{detail} || 'unknown error';
|
|
Error("API returned error code $code: $detail");
|
|
}
|
|
}
|
|
};
|
|
} else {
|
|
Error('Camera control command FAILED: '.$res->status_line().' for '.$cmds[0]->{cmd});
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
# Reboot camera
|
|
sub reboot {
|
|
my $self = shift;
|
|
|
|
Info('Rebooting camera');
|
|
|
|
my $cmd = {
|
|
cmd => 'Reboot',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel}
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
# PTZ Control Commands
|
|
|
|
sub moveConUp {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'tiltspeed') || 32;
|
|
|
|
Debug('Move Up');
|
|
$$self{Monitor}->suspendMotionDetection() if ! $self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'Up',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConDown {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'tiltspeed') || 32;
|
|
|
|
Debug('Move Down');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'Down',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConLeft {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'panspeed') || 32;
|
|
|
|
Debug('Move Left');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'Left',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConRight {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'panspeed') || 32;
|
|
|
|
Debug('Move Right');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'Right',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConUpLeft {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'panspeed') || 32;
|
|
|
|
Debug('Move Up Left');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'LeftUp',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConUpRight {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'panspeed') || 32;
|
|
|
|
Debug('Move Up Right');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'RightUp',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConDownLeft {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'panspeed') || 32;
|
|
|
|
Debug('Move Down Left');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'LeftDown',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveConDownRight {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'panspeed') || 32;
|
|
|
|
Debug('Move Down Right');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'RightDown',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub moveStop {
|
|
my $self = shift;
|
|
|
|
Debug('Move Stop');
|
|
$$self{Monitor}->resumeMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'Stop',
|
|
speed => 0
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
# Zoom Commands
|
|
|
|
sub zoomConTele {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'speed') || 32;
|
|
|
|
Debug('Zoom Tele');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'ZoomInc',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub zoomConWide {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'speed') || 32;
|
|
|
|
Debug('Zoom Wide');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'ZoomDec',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub zoomStop {
|
|
my $self = shift;
|
|
|
|
Debug('Zoom Stop');
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'Stop',
|
|
speed => 0
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
# Focus Commands
|
|
|
|
sub focusConNear {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'speed') || 32;
|
|
|
|
Debug('Focus Near');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'FocusDec',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub focusConFar {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $speed = $self->getParam($params, 'speed') || 32;
|
|
|
|
Debug('Focus Far');
|
|
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'FocusInc',
|
|
speed => $speed
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub focusAuto {
|
|
my $self = shift;
|
|
|
|
Debug('Focus Auto');
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'AutoFocus'
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
# Preset Commands
|
|
|
|
sub presetGoto {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $preset = $self->getParam($params, 'preset');
|
|
|
|
Debug("Goto Preset $preset");
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'ToPos',
|
|
id => int($preset)
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub presetSet {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
my $preset = $self->getParam($params, 'preset');
|
|
|
|
Debug("Set Preset $preset");
|
|
|
|
my $cmd = {
|
|
cmd => 'SetPtzPreset',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
enable => 1,
|
|
id => int($preset),
|
|
name => "Preset$preset"
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub presetHome {
|
|
my $self = shift;
|
|
|
|
Debug('Home Preset');
|
|
|
|
# Home is typically preset 0 or 1 on Reolink
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'ToPos',
|
|
id => 0
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
# Guard/Patrol Commands
|
|
|
|
sub autoScan {
|
|
my $self = shift;
|
|
|
|
Debug('Auto Scan/Patrol');
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'StartPatrol',
|
|
id => 0
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
sub autoStop {
|
|
my $self = shift;
|
|
|
|
Debug('Stop Patrol');
|
|
|
|
my $cmd = {
|
|
cmd => 'PtzCtrl',
|
|
action => 0,
|
|
param => {
|
|
channel => $$self{channel},
|
|
op => 'StopPatrol'
|
|
}
|
|
};
|
|
|
|
$self->sendApiCmd($cmd);
|
|
}
|
|
|
|
# Image Settings (using iris/white balance controls as available in ZM)
|
|
|
|
sub irisAbsOpen {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
|
|
# Use this to increase brightness
|
|
Debug('Increase Brightness');
|
|
# This would require GetIsp and SetIsp commands
|
|
# Leaving as placeholder for future implementation
|
|
}
|
|
|
|
sub irisAbsClose {
|
|
my $self = shift;
|
|
my $params = shift;
|
|
|
|
# Use this to decrease brightness
|
|
Debug('Decrease Brightness');
|
|
# This would require GetIsp and SetIsp commands
|
|
# Leaving as placeholder for future implementation
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=pod
|
|
|
|
=head1 NAME
|
|
|
|
ZoneMinder::Control::Reolink_HTTP - Reolink HTTP API camera control
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module contains the implementation of the Reolink HTTP API control protocol
|
|
using the api.cgi endpoint. This is an alternative to the ONVIF-based Reolink. pm
|
|
module and uses the native Reolink HTTP API with proper token-based authentication.
|
|
|
|
This module has been tested with various Reolink PTZ camera models including
|
|
RLC-423, RLC-420, RLC-511WA, and E1 Pro.
|
|
|
|
Based on the JavaScript implementation at: https://github.com/verheesj/reolink-api
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
On the Control Address field, use the format:
|
|
|
|
USERNAME:PASSWORD@ADDRESS:PORT
|
|
|
|
Examples:
|
|
admin:password@192.168.1.100:80
|
|
admin:mypass123@10.0.0.50:80
|
|
|
|
The Control Device field can be set to the camera channel (usually 0 for single
|
|
cameras, or 0-15 for NVRs). If left empty, channel 0 is assumed.
|
|
|
|
=head1 AUTHENTICATION
|
|
|
|
This module implements the Reolink token-based authentication system:
|
|
|
|
=over 4
|
|
|
|
=item * Performs Login to obtain a session token
|
|
|
|
=item * Token is included in all subsequent API calls
|
|
|
|
=item * Automatically refreshes token before expiration (default 1 hour lease)
|
|
|
|
=item * Properly logs out on close to prevent "max session" errors
|
|
|
|
=back
|
|
|
|
=head1 FEATURES
|
|
|
|
This module supports:
|
|
|
|
=over 4
|
|
|
|
=item * Continuous Pan/Tilt in all 8 directions
|
|
|
|
=item * Variable speed control for pan/tilt
|
|
|
|
=item * Zoom In/Out (for cameras with zoom)
|
|
|
|
=item * Focus Near/Far and Auto Focus
|
|
|
|
=item * Preset positions (save, recall)
|
|
|
|
=item * Home position
|
|
|
|
=item * Auto Patrol/Guard modes
|
|
|
|
=item * Camera reboot command
|
|
|
|
=item * Stop commands for all movements
|
|
|
|
=item * Automatic token refresh
|
|
|
|
=back
|
|
|
|
=head1 API REFERENCE
|
|
|
|
Based on Reolink CGI API documentation:
|
|
https://support.reolink.com/hc/en-us/articles/360007010473-CGI-API-User-Guide
|
|
|
|
=head1 NOTES
|
|
|
|
This module transmits the username and password during the initial login to obtain
|
|
a token. The token is then used for all subsequent requests. While the camera
|
|
supports HTTPS, ensure you're using it on a trusted/isolated network or configure
|
|
your Reolink camera to use HTTPS.
|
|
|
|
Token sessions have a maximum limit (typically 20 concurrent sessions). The module
|
|
properly logs out on close to prevent "max session" errors.
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<ZoneMinder::Control>, L<ZoneMinder::Control::Reolink>
|
|
|
|
=head1 AUTHOR
|
|
|
|
ZoneMinder Community
|
|
|
|
Based on templates from Instar720p, Amcrest_HTTP, and the verheesj/reolink-api
|
|
JavaScript implementation.
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
Copyright (C) 2026 ZoneMinder Community
|
|
|
|
This program 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.
|
|
|
|
This program 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 this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
=cut
|