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