Page 1 of 3

HikVision PTZ control

Posted: Wed Nov 02, 2016 1:16 pm
by Paranoid
I've written a PTZ control script that should work for HikVision IP cameras.

It has the following capabilities:
  • Move including diagonals
  • Zoom
  • Focus
  • Iris
  • Goto and Set presets
  • Home position
  • Camera reset (reboot)
Instructions

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"};'
This will tell you where the PelcoD.pm file is. Put the HikVision.pm in the same directory. You might need root permissions.
Execute the following to see if it is installed correctly:

Code: Select all

perl -E'use ZoneMinder::Control::HikVision; say "Looks OK";'
3. Add the HikVision capabilities to ZoneMinder
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;

Re: HikVision PTZ control

Posted: Wed Nov 02, 2016 3:13 pm
by knight-of-ni
Thank you for doing the work.

However, can I get you to add this to the wiki?
https://wiki.zoneminder.com/Hikvision

Note that you can use zmcamtool to export the PTZ Hikvision ptz configuration from your database, and thus save everyone else the work of manually having to add it to their own system.

The following should work:

Code: Select all

sudo zmcamtool.pl --export HikVision
Then just paste it to the wiki along with the control script.

Other users can then import it into their own system by saving your output as hikvision.sql and then issuing the following:

Code: Select all

sudo zmcamtool.pl --import hikvision.sql
Also, if I could look a gift horse in the mouth, it would be very helpful if you would turn this into a pull request on our github site so we could add it to zoneminder. You can use this pr as an example of what you need to do:
https://github.com/ZoneMinder/ZoneMinder/pull/1305

Re: HikVision PTZ control

Posted: Wed Nov 02, 2016 6:40 pm
by apbb2
This is great. Thank you.

I am having a hard time getting started though as I cannot find where PelcoD.pm resides. The perl script gives me nothing, so I am thinking it is not on my machine. whereis also comes up blank.

Where should it be?

I am currently running Ubuntu 16.04 with ZM 1.30 installed using PPA.

Once I get this installed I am sure it will work. Thanks Paranoid for getting this going!

Nevermind. I found it. It's in usr/share/perl5/ZoneMinder/Control/

Re: HikVision PTZ control

Posted: Wed Nov 02, 2016 7:15 pm
by apbb2
OK. Sorry for the PTZ noob questions, but i have one more.

I have setup the script & setup the control on ZM. When I try to move the camera, I am get an error:

Control response was status = undefined message = /usr/bin/zmcontrol.pl --panspeed=65 --autostop --command=moveConLeft --id=20=>

I set the Pan & Tilt speed min & max (1 & 100) & have no other values for the other min & max, range, step, etc.

Any insight appreciated.

Thanks.

Re: HikVision PTZ control

Posted: Wed Nov 02, 2016 8:51 pm
by Paranoid
apbb2 wrote:OK. Sorry for the PTZ noob questions, but i have one more.

I have setup the script & setup the control on ZM. When I try to move the camera, I am get an error:

Control response was status = undefined message = /usr/bin/zmcontrol.pl --panspeed=65 --autostop --command=moveConLeft --id=20=>

I set the Pan & Tilt speed min & max (1 & 100) & have no other values for the other min & max, range, step, etc.

Any insight appreciated.

Thanks.
What was the output when you ran the following?

Code: Select all

perl -E'use ZoneMinder::Control::HikVision; say "Looks OK";'
If it is anything other than "Looks OK" then copy the output here.

Can you also provide the output of the following:

Code: Select all

sudo zmcamtool.pl --export HikVision

Re: HikVision PTZ control

Posted: Thu Nov 03, 2016 5:36 pm
by apbb2
Running the perl gives me a 'Looks OK'

Here is the output of: sudo zmcamtool.pl --export HikVision

mysqldump: [Warning] Using a password on the command line interface can be insecure.
INSERT INTO `Controls` VALUES (NULL,'HikVision','Local','',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,0,1,1,0,0,0,0,1,1,100,0,0,1,0,0,0,0,1,1,100,1,0,0,0);

Re: HikVision PTZ control

Posted: Thu Nov 03, 2016 5:49 pm
by Paranoid
apbb2 wrote:Running the perl gives me a 'Looks OK'

Here is the output of: sudo zmcamtool.pl --export HikVision

mysqldump: [Warning] Using a password on the command line interface can be insecure.
INSERT INTO `Controls` VALUES (NULL,'HikVision','Local','',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,0,1,1,0,0,0,0,1,1,100,0,0,1,0,0,0,0,1,1,100,1,0,0,0);
Go into your camera setup.
Select the control tab.
Click on edit next to Control type.
A new window with pop up with the control capabilities.
Select "HikVision" and another window will pop up
In the "Protocol" entry put "HikVision"
Save and try again.

Re: HikVision PTZ control

Posted: Thu Nov 03, 2016 6:32 pm
by apbb2
Thanks. Almost.

We seem to have fixed the error, however now when you try to control, there is no response from the camera.

I'm stuck. Sorry to bug about this but i am hoping my stupidity helps someone else out down the line.

Re: HikVision PTZ control

Posted: Thu Nov 03, 2016 8:30 pm
by Paranoid
apbb2 wrote:Thanks. Almost.

We seem to have fixed the error, however now when you try to control, there is no response from the camera.

I'm stuck. Sorry to bug about this but i am hoping my stupidity helps someone else out down the line.
I've modified the code so as add some loging when authentication fails. Can you download it again?

The most likely problem is authentication.
In the control tab you must set the "Control Device" to the exact model of your camera and "Control Address" must have the username:password@cameraaddress for the camera.

After you have saved the new version try again. If you look in the zmcontrol log file there should be messages indicating what might have gone wrong.

If you dont know where the log files are then do the following.

1. Open the main options page on the consle.
2. Look under the Paths tab and the PATH_LOGS will tell you where the log files are located.
3. Select the "Logging" tab and make sure LOG_LEVEL_FILE is set to Info

You might have to stop/start zoneminder to make any changes take effect.

Re: HikVision PTZ control

Posted: Thu Nov 03, 2016 8:57 pm
by apbb2
Got it to work!

My issue was somewhat my stupidity & somewhat not.

I had a typo on my IP & when I took out port 88 I got it to work.

Logging definitely helped out for me.

Great Job!

Thank you so much for this.

Re: HikVision PTZ control

Posted: Sun Nov 06, 2016 2:11 pm
by knight-of-ni
FYI:
https://github.com/ZoneMinder/ZoneMinder/pull/1674

Note I did make the assumption Paranoid implicitly gave his permission to include his script in ZoneMinder. If this is not the case, then please let us know. It's your script, so tell us if this is ok.

Re: HikVision PTZ control

Posted: Fri May 26, 2017 6:01 pm
by vbo68
Thanks a LOT ! :D

Works Great with DS-2DE4220IW-DE

Now I will try to make the script work for Tracking, as the manual says :

"This will only work if your camera supports mapped movement modes where a point on an image can be mapped to a control command. This is generally most common on network cameras but can be replicated to some degree on other cameras that support relative movement modes. See the Camera Control section for more details"

Do you know if this HikVision camera supports "relative movement modes" ?

Thanks

Re: HikVision PTZ control

Posted: Fri May 26, 2017 6:56 pm
by vbo68
Just found the API Standards used by Hiksivion :

http://static.interlogix.com/library/In ... LISHED.pdf

Here are all the commands supported by the cameras, for good scripters (that I am not, but I will try)

Have fun !

Re: HikVision PTZ control

Posted: Fri May 26, 2017 7:02 pm
by Paranoid
You should be able to find all the information you need here: http://oversea-download.hikvision.com/u ... ervice.pdf

Re: HikVision PTZ control

Posted: Sat May 27, 2017 12:15 am
by vbo68
Thanks Paranoid ! I will have a look at it.

Meanwhile, I managed to add the mapping capability to your HikVision Script, It works, and now you can click on the picture to center it where you click, but the only issue is that is requires knowing the capture resolution of the camera stream in the monitor configuration, so for the moment I have hard coded it...

For the "relative" move functions, that will enable the motion tracking feature, I am still at the very begining because I think I have to calibrate the speed based on the value of the steps send by zoneminder. If someone that is really a develloper could have a look at it it would be great, here is where I stand :

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 $xResolution = 704;
my $yResolution = 576;
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");
}


sub moveRelVector {
    my $self = shift;
    my $panstep  = shift;
    my $tiltstep = shift;
    my $params = shift;
    my $command;                    # The ISAPI/PTZ command

    # Calculate autostop time
    my $duration = int(100);
    my $momentxml;
    $momentxml = "<Momentary><duration>$duration</duration></Momentary>";
    $command = "ISAPI/PTZCtrl/channels/$ChannelID/momentary";
    # Calculate movement speeds
    my $x = $panstep  * $self->getParam( $params, 'panspeed', 0 );
    my $y = $tiltstep * $self->getParam( $params, 'tiltspeed', 0 );
    # Create the XML
    my $xml = "<PTZData><pan>$x</pan><tilt>$y</tilt><zoom>0</zoom>$momentxml</PTZData>";
    # Send it to the camera
    $self->PutCmd($command,$xml);
}

sub moveRelUp
{
    my $self = shift;
    my $params = shift;
    my $step = $self->getParam( $params, 'tiltstep' );
    Debug( "Step Up $step" );
    $_[0]->moveRelVector(  0,  $step, 0, splice(@_,1));
}

sub moveRelDown
{
    my $self = shift;
    my $params = shift;
    my $step = $self->getParam( $params, 'tiltstep' );
    Debug( "Step Down $step" );
    $_[0]->moveRelVector(  0,  $step, 0, splice(@_,1));
}

sub moveRelLeft
{
    my $self = shift;
    my $params = shift;
    my $step = $self->getParam( $params, 'panstep' );
    Debug( "Step Left $step" );
    $_[0]->moveRelVector( $step ,  0, 0, splice(@_,1));
}

sub moveRelRight
{
    my $self = shift;
    my $params = shift;
    my $step = $self->getParam( $params, 'panstep' );
    Debug( "Step Right $step" );
    $_[0]->moveRelVector(  $step   0, 0, splice(@_,1));
}

sub moveRelUpRight
{
    my $self = shift;
    my $params = shift;
    my $panstep = $self->getParam( $params, 'panstep' );
    my $tiltstep = $self->getParam( $params, 'tiltstep' );
    Debug( "Step Up/Right $tiltstep/$panstep" );
    $_[0]->moveRelVector( $panstep , $tiltstep , 0, splice(@_,1));
}

sub moveRelUpLeft
{
    my $self = shift;
    my $params = shift;
    my $panstep = $self->getParam( $params, 'panstep' );
    my $tiltstep = $self->getParam( $params, 'tiltstep' );
    Debug( "Step Up/Left $tiltstep/$panstep" );
    $_[0]->moveRelVector(  $panstep , $tiltstep , 0, splice(@_,1));
}

sub moveRelDownRight
{
    my $self = shift;
    my $params = shift;
    my $panstep = $self->getParam( $params, 'panstep' );
    my $tiltstep = $self->getParam( $params, 'tiltstep' );
    Debug( "Step Down/Right $tiltstep/$panstep" );
    $_[0]->moveRelVector(   $panstep ,  $tiltstep , 0, splice(@_,1));
}

sub moveRelDownLeft
{
    my $self = shift;
    my $params = shift;
    my $panstep = $self->getParam( $params, 'panstep' );
    my $tiltstep = $self->getParam( $params, 'tiltstep' );
    Debug( "Step Down/Left $tiltstep/$panstep" );
    $_[0]->moveRelVector(   $panstep , $tiltstep , 0,  splice(@_,1));
}

sub moveMap {
    my $self = shift;
    my $params = shift;
    my $xcoord = $self->getParam( $params, 'xcoord' );
    my $ycoord = $self->getParam( $params, 'ycoord' );
    my $command;
    my $positionx = int((($yResolution - $ycoord)/$yResolution)*255);
    my $positiony = int((($xcoord)/$xResolution)*255);
    Debug( "Move Map to $xcoord,$ycoord" );
    $command = "ISAPI/PTZCtrl/channels/$ChannelID/relative";
    my $xml = "<PTZData><Relative><positionX> $positionx </positionX><positionY> $positiony </positionY><relativeZoom> 0</relativeZoom></Relative></PTZData>";
    $self->PutCmd($command,$xml);
}


sub moveAbs_todo {
    my $self = shift;
    my $params = shift;
    my $xcoord = $self->getParam( $params, 'xcoord' );
    my $ycoord = $self->getParam( $params, 'ycoord' );
    my $command;
    Debug( "Move  to absolute elevation and azimuth $xcoord,$ycoord" );
    $command = "ISAPI/PTZCtrl/channels/$ChannelID/absolute";
    my $xml = "<PTZData><AbsoluteHigh><elevation> 50 </elevation> <azimuth> 50 </azimuth><absoluteZoom> 0 </absoluteZoom></AbsoluteHigh></PTZData>";
    $self->PutCmd($command,$xml);
}
Any comment welcome, just don't be too hard on my coding, I am self teached develloper...
Anyway I will still work on it this weekend, and see if I can finalyse the Motion tracking capability.

The moveAbs {} , function is just at a very preliminary stage as I could not find exemples in the scripts of other cameras.

Good Night !