1# ==========================================================================
2#
3# ZoneMinder HikVision Control Protocol Module
4# Copyright (C) 2016 Terry Sanders
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU General Public License
8# as published by the Free Software Foundation; either version 2
9# of the License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19#
20# ==========================================================================
21#
22# This module contains an implementation of the HikVision ISAPI camera control
23# protocol
24#
25package ZoneMinder::Control::HikVision;
26
27use 5.006;
28use strict;
29use warnings;
30
31require ZoneMinder::Base;
32require ZoneMinder::Control;
33
34our @ISA = qw(ZoneMinder::Control);
35
36# ==========================================================================
37#
38# HiKVision ISAPI Control Protocol
39#
40# Set the following:
41# ControlAddress: username:password@camera_webaddress:port
42# ControlDevice: IP Camera Model
43#
44# ==========================================================================
45
46use ZoneMinder::Logger qw(:all);
47
48use Time::HiRes qw( usleep );
49
50use LWP::UserAgent;
51use HTTP::Cookies;
52
53my $ChannelID = 1;              # Usually...
54my $DefaultFocusSpeed = 50;     # Should be between 1 and 100
55my $DefaultIrisSpeed = 50;      # Should be between 1 and 100
56my ($user,$pass,$host,$port);
57
58sub open {
59  my $self = shift;
60  $self->loadMonitor();
61  #
62  # Create a UserAgent for the requests
63  #
64  $self->{UA} = LWP::UserAgent->new();
65  $self->{UA}->cookie_jar( {} );
66  #
67  # Extract the username/password host/port from ControlAddress
68  #
69  if ( $self->{Monitor}{ControlAddress} =~ /^([^:]+):([^@]+)@(.+)/ ) { # user:pass@host...
70    $user = $1;
71    $pass = $2;
72    $host = $3;
73  } elsif ( $self->{Monitor}{ControlAddress} =~ /^([^@]+)@(.+)/ ) { # user@host...
74    $user = $1;
75    $host = $2;
76  } else { # Just a host
77    $host = $self->{Monitor}{ControlAddress};
78  }
79  # Check if it is a host and port or just a host
80  if ( $host =~ /([^:]+):(.+)/ ) {
81    $host = $1;
82    $port = $2;
83  } else {
84    $port = 80;
85  }
86  # Save the credentials
87  if ( defined($user) ) {
88    $self->{UA}->credentials("$host:$port", $self->{Monitor}{ControlDevice}, $user, $pass);
89  }
90  # Save the base url
91  $self->{BaseURL} = "http://$host:$port";
92}
93
94sub PutCmd {
95  my $self = shift;
96  my $cmd = shift;
97  my $content = shift;
98  my $req = HTTP::Request->new(PUT => $self->{BaseURL}.'/'.$cmd);
99  if ( defined($content) ) {
100    $req->content_type('application/x-www-form-urlencoded; charset=UTF-8');
101    $req->content('<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $content);
102  }
103  my $res = $self->{UA}->request($req);
104  unless( $res->is_success ) {
105    #
106    # The camera timeouts connections at short intervals. When this
107    # happens the user agent connects again and uses the same auth tokens.
108    # The camera rejects this and asks for another token but the UserAgent
109    # just gives up. Because of this I try the request again and it should
110    # succeed the second time if the credentials are correct.
111    #
112    if ( $res->code == 401 ) {
113      $res = $self->{UA}->request($req);
114      unless( $res->is_success ) {
115        #
116        # It has failed authentication. The odds are
117        # that the user has set some parameter incorrectly
118        # so check the realm against the ControlDevice
119        # entry and send a message if different
120        #
121        my $auth = $res->headers->www_authenticate;
122        foreach (split(/\s*,\s*/,$auth)) {
123          if ( $_ =~ /^realm\s*=\s*"([^"]+)"/i ) {
124            if ( $self->{Monitor}{ControlDevice} ne $1 ) {
125              Warning("Control Device appears to be incorrect.
126                Control Device should be set to \"$1\".
127                Control Device currently set to \"$self->{Monitor}{ControlDevice}\".");
128              $self->{Monitor}{ControlDevice} = $1;
129              $self->{UA}->credentials("$host:$port", $self->{Monitor}{ControlDevice}, $user, $pass);
130              return PutCmd($self,$cmd,$content);
131            }
132          }
133        }
134        #
135        # Check for username/password
136        #
137        if ( $self->{Monitor}{ControlAddress} =~ /.+:(.+)@.+/ ) {
138          Info('Check username/password is correct');
139        } elsif ( $self->{Monitor}{ControlAddress} =~ /^[^:]+@.+/ ) {
140          Info('No password in Control Address. Should there be one?');
141        } elsif ( $self->{Monitor}{ControlAddress} =~ /^:.+@.+/ ) {
142          Info('Password but no username in Control Address.');
143        } else {
144          Info('Missing username and password in Control Address.');
145        }
146        Error($res->status_line);
147      }
148    } else {
149      Error($res->status_line);
150    }
151  } # end unless res->is_success
152} # end sub putCmd
153#
154# The move continuous functions all call moveVector
155# with the direction to move in. This includes zoom
156#
157sub moveVector {
158  my $self = shift;
159  my $pandirection  = shift;
160  my $tiltdirection = shift;
161  my $zoomdirection = shift;
162  my $params = shift;
163  my $command;                    # The ISAPI/PTZ command
164
165  # Calculate autostop time
166  my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
167  # Change from microseconds to milliseconds
168  $duration = int($duration/1000);
169  my $momentxml;
170  if( $duration ) {
171    $momentxml = "<Momentary><duration>$duration</duration></Momentary>";
172    $command = "ISAPI/PTZCtrl/channels/$ChannelID/momentary";
173  }
174  else {
175    $momentxml = "";
176    $command = "ISAPI/PTZCtrl/channels/$ChannelID/continuous";
177  }
178  # Calculate movement speeds
179  my $x = $pandirection  * $self->getParam( $params, 'panspeed', 0 );
180  my $y = $tiltdirection * $self->getParam( $params, 'tiltspeed', 0 );
181  my $z = $zoomdirection * $self->getParam( $params, 'speed', 0 );
182  # Create the XML
183  my $xml = "<PTZData><pan>$x</pan><tilt>$y</tilt><zoom>$z</zoom>$momentxml</PTZData>";
184  # Send it to the camera
185  $self->PutCmd($command,$xml);
186}
187sub zoomStop         { $_[0]->moveVector(  0,  0, 0, splice(@_,1)); }
188sub moveStop         { $_[0]->moveVector(  0,  0, 0, splice(@_,1)); }
189sub moveConUp        { $_[0]->moveVector(  0,  1, 0, splice(@_,1)); }
190sub moveConUpRight   { $_[0]->moveVector(  1,  1, 0, splice(@_,1)); }
191sub moveConRight     { $_[0]->moveVector(  1,  0, 0, splice(@_,1)); }
192sub moveConDownRight { $_[0]->moveVector(  1, -1, 0, splice(@_,1)); }
193sub moveConDown      { $_[0]->moveVector(  0, -1, 0, splice(@_,1)); }
194sub moveConDownLeft  { $_[0]->moveVector( -1, -1, 0, splice(@_,1)); }
195sub moveConLeft      { $_[0]->moveVector( -1,  0, 0, splice(@_,1)); }
196sub moveConUpLeft    { $_[0]->moveVector( -1,  1, 0, splice(@_,1)); }
197sub zoomConTele      { $_[0]->moveVector(  0,  0, 1, splice(@_,1)); }
198sub zoomConWide      { $_[0]->moveVector(  0,  0,-1, splice(@_,1)); }
199#
200# Presets including Home set and clear
201#
202sub presetGoto {
203  my $self = shift;
204  my $params = shift;
205  my $preset = $self->getParam($params,'preset');
206  $self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/presets/$preset/goto");
207}
208sub presetSet {
209  my $self = shift;
210  my $params = shift;
211  my $preset = $self->getParam($params,'preset');
212  my $xml = "<PTZPreset><id>$preset</id></PTZPreset>";
213  $self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/presets/$preset",$xml);
214}
215sub presetHome {
216  my $self = shift;
217  my $params = shift;
218  $self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/homeposition/goto");
219}
220#
221# Focus controls all call Focus with a +/- speed
222#
223sub Focus {
224  my $self = shift;
225  my $speed = shift;
226  my $xml = "<FocusData><focus>$speed</focus></FocusData>";
227  $self->PutCmd("ISAPI/System/Video/inputs/channels/$ChannelID/focus",$xml);
228}
229sub focusConNear {
230  my $self = shift;
231  my $params = shift;
232
233  # Calculate autostop time
234  my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
235  # Get the focus speed
236  my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
237  $self->Focus(-$speed);
238  if($duration) {
239    usleep($duration);
240    $self->moveStop($params);
241  }
242}
243sub Near {
244  my $self = shift;
245  my $params = shift;
246  $self->Focus(-$DefaultFocusSpeed);
247}
248sub focusAbsNear {
249  my $self = shift;
250  my $params = shift;
251
252  # Get the focus speed
253  my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
254  $self->Focus(-$speed);
255}
256sub focusRelNear {
257  my $self = shift;
258  my $params = shift;
259  # Get the focus speed
260  my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
261  $self->Focus(-$speed);
262}
263sub focusConFar {
264  my $self = shift;
265  my $params = shift;
266
267  # Calculate autostop time
268  my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
269  # Get the focus speed
270  my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
271  $self->Focus($speed);
272  if($duration) {
273    usleep($duration);
274    $self->moveStop($params);
275  }
276}
277sub Far {
278  my $self = shift;
279  my $params = shift;
280  $self->Focus($DefaultFocusSpeed);
281}
282sub focusAbsFar {
283  my $self = shift;
284  my $params = shift;
285
286  # Get the focus speed
287  my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
288  $self->Focus($speed);
289}
290sub focusRelFar {
291  my $self = shift;
292  my $params = shift;
293
294  # Get the focus speed
295  my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
296  $self->Focus($speed);
297}
298#
299# Iris controls all call Iris with a +/- speed
300#
301sub Iris {
302  my $self = shift;
303  my $speed = shift;
304
305  my $xml = "<IrisData><iris>$speed</iris></IrisData>";
306  $self->PutCmd("ISAPI/System/Video/inputs/channels/$ChannelID/iris",$xml);
307}
308sub irisConClose {
309  my $self = shift;
310  my $params = shift;
311
312  # Calculate autostop time
313  my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
314  # Get the iris speed
315  my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
316  $self->Iris(-$speed);
317  if($duration) {
318    usleep($duration);
319    $self->moveStop($params);
320  }
321}
322sub Close {
323  my $self = shift;
324  my $params = shift;
325
326  $self->Iris(-$DefaultIrisSpeed);
327}
328sub irisAbsClose {
329  my $self = shift;
330  my $params = shift;
331
332  # Get the iris speed
333  my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
334  $self->Iris(-$speed);
335}
336sub irisRelClose {
337  my $self = shift;
338  my $params = shift;
339
340  # Get the iris speed
341  my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
342  $self->Iris(-$speed);
343}
344sub irisConOpen {
345  my $self = shift;
346  my $params = shift;
347
348  # Calculate autostop time
349  my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
350  # Get the iris speed
351  my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
352  $self->Iris($speed);
353  if($duration) {
354    usleep($duration);
355    $self->moveStop($params);
356  }
357}
358sub Open {
359  my $self = shift;
360  my $params = shift;
361
362  $self->Iris($DefaultIrisSpeed);
363}
364sub irisAbsOpen {
365  my $self = shift;
366  my $params = shift;
367
368  # Get the iris speed
369  my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
370  $self->Iris($speed);
371}
372sub irisRelOpen {
373  my $self = shift;
374  my $params = shift;
375
376  # Get the iris speed
377  my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
378  $self->Iris($speed);
379}
380#
381# reset (reboot) the device
382#
383sub reset {
384  my $self = shift;
385
386  $self->PutCmd('ISAPI/System/reboot');
387}
388
3891;
390__END__
391