It has the following capabilities:
- Move including diagonals
- Zoom
- Focus
- Iris
- Goto and Set presets
- Home position
- Camera reset (reboot)
1. Save the code to a file named "HikVision.pm"
2. Find out where the control scripts are stored on your system and put the file there.
If you don’t know where they are located then execute the following:
Code: Select all
perl -E'use ZoneMinder::Control::PelcoD; say $INC{"ZoneMinder/Controli/PelcoD.pm"};'
Execute the following to see if it is installed correctly:
Code: Select all
perl -E'use ZoneMinder::Control::HikVision; say "Looks OK";'
Go to the Control tab on your camera settings and where it says "Control Type" click on edit.
This will bring up the Control Capabilities window. Click on "Add New Control".
Give it name, set the type to Remote and Protocol to "HikVision". If you want to be able to reboot your camera from zoneminder then you should also check "Can Reset"
Under the "Move" tab check "Can Move", "Can Move Diagonally" and "Can Move Continuous"
For the Pan/Tilt/Zoom/Focus/Iris tabs check the "Can Pan/Tilt/..." and the "Has Pan/Tilt/... Speed". The speed range is 1(slowest) to 100(fastest). If you prefer a constant speed then set the min and max values to whatever speed you prefer.
It can not "White balance"
In the preset tab check the "Has Presets", "Has Home Preset" and "Can Set Presets" option. Enter the number of presets you have (Note: even though my camera has over 200 presets I only put 20 here).
4. When you are sure all is correct then hit the save button.
5. Close any open zoneminder configuration windows including the the one for configuring your camera.
6. Open the camera configuration window and go to the control tab.
7. Under "Control Type" select the one you have just created
8. Enter your camera model in "Control Device"
This has to be the exact model. If you get it wrong then this will not work
If your IP camera has a web interface you can find it under Configuration->System Settings->Basic Information.
9. In "Control Address" put the username, password, camera address and port as shown below:
username:password@192.168.1.20:88
If it has a DNS entry set up you can use its hostname rather than ip address.
10. Save
If you have more than one HikVision camera then you need only do steps 6 - 10 for each additional camera
EDIT: I developed this on a camera that hasn't been installed yet. It is currently sitting upside down on a table. As a result I got the tilt direction wrong. This has now been corrected.
EDIT: Added some logging for when authentication fails
Code: Select all
# ==========================================================================
#
# ZoneMinder HikVision Control Protocol Module
# Copyright (C) 2016 Terry Sanders
#
# 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ==========================================================================
#
# This module contains an implementation of the HikVision ISAPI camera control
# protocol
#
package ZoneMinder::Control::HikVision;
use 5.006;
use strict;
use warnings;
require ZoneMinder::Base;
require ZoneMinder::Control;
our @ISA = qw(ZoneMinder::Control);
# ==========================================================================
#
# HiKVision ISAPI Control Protocol
#
# Set the following:
# ControlAddress: username:password@camera_webaddress:port
# ControlDevice: IP Camera Model
#
# ==========================================================================
use ZoneMinder::Logger qw(:all);
use Time::HiRes qw( usleep );
use LWP::UserAgent;
use HTTP::Cookies;
my $ChannelID = 1; # Usually...
my $DefaultFocusSpeed = 50; # Should be between 1 and 100
my $DefaultIrisSpeed = 50; # Should be between 1 and 100
sub new {
my $class = shift;
my $id = shift;
my $self = ZoneMinder::Control->new( $id );
bless( $self, $class );
srand( time() );
return $self;
}
our $AUTOLOAD;
sub AUTOLOAD {
my $self = shift;
my $class = ref($self) || croak( "$self not object" );
my $name = $AUTOLOAD;
$name =~ s/.*://;
if ( exists($self->{$name}) )
{
return( $self->{$name} );
}
Fatal( "Can't access $name member of object of class $class" );
}
sub open {
my $self = shift;
$self->loadMonitor();
#
# Create a UserAgent for the requests
#
$self->{UA} = LWP::UserAgent->new();
$self->{UA}->cookie_jar( {} );
#
# Extract the username/password host/port from ControlAddress
#
my ($user,$pass,$host,$port);
if( $self->{Monitor}{ControlAddress} =~ /^([^:]+):([^@]+)@(.+)/ ) { # user:pass@host...
$user = $1;
$pass = $2;
$host = $3;
}
elsif( $self->{Monitor}{ControlAddress} =~ /^([^@]+)@(.+)/ ) { # user@host...
$user = $1;
$host = $2;
}
else { # Just a host
$host = $self->{Monitor}{ControlAddress};
}
# Check if it is a host and port or just a host
if( $host =~ /([^:]+):(.+)/ ) {
$host = $1;
$port = $2;
}
else {
$port = 80;
}
# Save the credentials
if( defined($user) ) {
$self->{UA}->credentials( "$host:$port", $self->{Monitor}{ControlDevice}, $user, $pass );
}
# Save the base url
$self->{BaseURL} = "http://$host:$port";
}
sub PutCmd {
my $self = shift;
my $cmd = shift;
my $content = shift;
my $req = HTTP::Request->new(PUT => "$self->{BaseURL}/$cmd");
if(defined($content)) {
$req->content_type("application/x-www-form-urlencoded; charset=UTF-8");
$req->content('<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $content);
}
my $res = $self->{UA}->request($req);
unless( $res->is_success ) {
#
# The camera timeouts connections at short intervals. When this
# happens the user agent connects again and uses the same auth tokens.
# The camera rejects this and asks for another token but the UserAgent
# just gives up. Because of this I try the request again and it should
# succeed the second time if the credentials are correct.
#
if($res->code == 401) {
$res = $self->{UA}->request($req);
unless( $res->is_success ) {
#
# It has failed authentication. The odds are
# that the user has set some paramater incorrectly
# so check the realm against the ControlDevice
# entry and send a message if different
#
my $auth = $res->headers->www_authenticate;
foreach (split(/\s*,\s*/,$auth)) {
if( $_ =~ /^realm\s*=\s*"([^"]+)"/i ) {
if( $self->{Monitor}{ControlDevice} ne $1 ) {
Info "Control Device appears to be incorrect.";
Info "Control Device should be set to \"$1\".";
Info "Control Device currently set to \"$self->{Monitor}{ControlDevice}\".";
}
}
}
#
# Check for username/password
#
if( $self->{Monitor}{ControlAddress} =~ /.+:(.+)@.+/ ) {
Info "Check username/password is correct";
} elsif ( $self->{Monitor}{ControlAddress} =~ /^[^:]+@.+/ ) {
Info "No password in Control Address. Should there be one?";
} elsif ( $self->{Monitor}{ControlAddress} =~ /^:.+@.+/ ) {
Info "Password but no username in Control Address.";
} else {
Info "Missing username and password in Control Address.";
}
Fatal $res->status_line;
}
}
else {
Fatal $res->status_line;
}
}
}
#
# The move continuous functions all call moveVector
# with the direction to move in. This includes zoom
#
sub moveVector {
my $self = shift;
my $pandirection = shift;
my $tiltdirection = shift;
my $zoomdirection = shift;
my $params = shift;
my $command; # The ISAPI/PTZ command
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Change from microseconds to milliseconds
$duration = int($duration/1000);
my $momentxml;
if( $duration ) {
$momentxml = "<Momentary><duration>$duration</duration></Momentary>";
$command = "ISAPI/PTZCtrl/channels/$ChannelID/momentary";
}
else {
$momentxml = "";
$command = "ISAPI/PTZCtrl/channels/$ChannelID/continuous";
}
# Calculate movement speeds
my $x = $pandirection * $self->getParam( $params, 'panspeed', 0 );
my $y = $tiltdirection * $self->getParam( $params, 'tiltspeed', 0 );
my $z = $zoomdirection * $self->getParam( $params, 'speed', 0 );
# Create the XML
my $xml = "<PTZData><pan>$x</pan><tilt>$y</tilt><zoom>$z</zoom>$momentxml</PTZData>";
# Send it to the camera
$self->PutCmd($command,$xml);
}
sub moveStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); }
sub moveConUp { $_[0]->moveVector( 0, 1, 0, splice(@_,1)); }
sub moveConUpRight { $_[0]->moveVector( 1, 1, 0, splice(@_,1)); }
sub moveConRight { $_[0]->moveVector( 1, 0, 0, splice(@_,1)); }
sub moveConDownRight { $_[0]->moveVector( 1, -1, 0, splice(@_,1)); }
sub moveConDown { $_[0]->moveVector( 0, -1, 0, splice(@_,1)); }
sub moveConDownLeft { $_[0]->moveVector( -1, -1, 0, splice(@_,1)); }
sub moveConLeft { $_[0]->moveVector( -1, 0, 0, splice(@_,1)); }
sub moveConUpLeft { $_[0]->moveVector( -1, 1, 0, splice(@_,1)); }
sub zoomConTele { $_[0]->moveVector( 0, 0, 1, splice(@_,1)); }
sub zoomConWide { $_[0]->moveVector( 0, 0,-1, splice(@_,1)); }
#
# Presets including Home set and clear
#
sub presetGoto {
my $self = shift;
my $params = shift;
my $preset = $self->getParam($params,'preset');
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/presets/$preset/goto");
}
sub presetSet {
my $self = shift;
my $params = shift;
my $preset = $self->getParam($params,'preset');
my $xml = "<PTZPreset><id>$preset</id></PTZPreset>";
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/presets/$preset",$xml);
}
sub presetHome {
my $self = shift;
my $params = shift;
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/homeposition/goto");
}
#
# Focus controls all call Focus with a +/- speed
#
sub Focus {
my $self = shift;
my $speed = shift;
my $xml = "<FocusData><focus>$speed</focus></FocusData>";
$self->PutCmd("ISAPI/System/Video/inputs/channels/$ChannelID/focus",$xml);
}
sub focusConNear {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Near {
my $self = shift;
my $params = shift;
$self->Focus(-$DefaultFocusSpeed);
}
sub focusAbsNear {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed);
}
sub focusRelNear {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed);
}
sub focusConFar {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Far {
my $self = shift;
my $params = shift;
$self->Focus($DefaultFocusSpeed);
}
sub focusAbsFar {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed);
}
sub focusRelFar {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed);
}
#
# Iris controls all call Iris with a +/- speed
#
sub Iris {
my $self = shift;
my $speed = shift;
my $xml = "<IrisData><iris>$speed</iris></IrisData>";
$self->PutCmd("ISAPI/System/Video/inputs/channels/$ChannelID/iris",$xml);
}
sub irisConClose {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Close {
my $self = shift;
my $params = shift;
$self->Iris(-$DefaultIrisSpeed);
}
sub irisAbsClose {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed);
}
sub irisRelClose {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed);
}
sub irisConOpen {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Open {
my $self = shift;
my $params = shift;
$self->Iris($DefaultIrisSpeed);
}
sub irisAbsOpen {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed);
}
sub irisRelOpen {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed);
}
#
# reset (reboot) the device
#
sub reset {
my $self = shift;
$self->PutCmd("ISAPI/System/reboot");
}
1;