Foscam F9821P V2 Control Script for Zoneminder

Foscam F9821P V2 Control Script for Zoneminder

A Zoneminder control script for the Foscam F9821P V2 Camera that is modifed from F9821W. Tested fulling working with Zoneminder version 1.34

Step 1: Save the following script as FoscamFI9821PV2.pm to your Zoneminder camera control folder. Reboot Zoneminder.

Step 2: Add a new control method and enter "FoscamFI9821PV2" in the protocol field and tick the settings as below:

Main tab:
Name - Foscam FI9821P V2
Type - Remote
Protocol - FoscamFI9821PV2
Can Wake - ticked (For IR control)
Can Sleep - ticked (For IR control)
Can Reset - ticked (For reboot function)

Move tab:
Can Move - ticked
Can Move Diagonally - ticked
Can Move Relative - ticked
Can Move Continuous - ticked

Pan tab:
Can Pan - ticked

Tilt tab:
Can Tilt - ticked

Presets tab:
Has Presets - ticked
Num Presets - 16
Has Home Preset - ticked
Can Set Presets - ticked

Monitor - Control tab
Controllable - ticked
Control type - Foscam FI9821P V2
Control device - usr=replace_with_username&pwd=replace_with_password
Control address - http://Camera IP address:88
Auto Stop Timeout - 1 (or longer if you want camera to move further with each button press)

💡
Note:
- Enter username and password in the "Control Device" field
E.g. usr=admin&pwd=password
- To set presets, you must enter a label name
- To use existing presets in camera, just pick any preset number in zoneminder and set a label name matching the one in camera. The number and order of preset does not matter.
# ==========================================================================
#
# ZoneMinder Foscam FI9821PV2 IP Control Protocol Module
# 
# Created from Dave Harris Foscam FI8918W control script - Feb 2011
# Modified by Henry Chang on 17/06/2020 to work with Foscam FI9821PV2
# 
# 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.
#
# ==========================================================================


package ZoneMinder::Control::FoscamFI9821PV2;
 
use 5.006;
use strict; 
use warnings;
 
require ZoneMinder::Base; 
require ZoneMinder::Control;
 
our @ISA = qw(ZoneMinder::Control);
 
# ==========================================================================
#
# Foscam FI9821P V2 IP Control Protocol
#
# ==========================================================================

use ZoneMinder::Logger qw(:all); 
use ZoneMinder::Config qw(:all);
use ZoneMinder::Database qw(zmDbConnect); 
use XML::LibXML;
use Time::HiRes qw( usleep );
 
sub new {
	my $class = shift;
	my $id = shift;
	my $self = ZoneMinder::Control->new( $id );
	my $logindetails = "";
	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();
 
	use LWP::UserAgent;
	$self->{ua} = LWP::UserAgent->new;
	$self->{ua}->agent( "ZoneMinder Control Agent/" );
 
	$self->{state} = 'open';	
	
	$self->loadPresets();
}

#Load presets from camera into Zoneminder
sub loadPresets {
	my $self = shift;	
	my $dom = XML::LibXML->load_xml(location => "http://".$self->{Monitor}->{ControlAddress}."/cgi-bin/CGIProxy.fcgi?cmd=getPTZPresetPointList&".$self->{Monitor}->{ControlDevice});
	my $counter = 1;
	my $dbh = zmDbConnect(1);
	my $sql_del = 'DELETE FROM ControlPresets where MonitorId= ?';
	my $sth_del = $dbh->prepare($sql_del);
    my $res_del = $sth_del->execute($self->{Monitor}->{Id});	
	
	my $sql = 'INSERT INTO ControlPresets (Label,MonitorId,Preset) VALUES (?,?,?)';
	my $sth = $dbh->prepare($sql);
	
	foreach my $cam_label ($dom->findnodes("//*[starts-with(local-name(),'point')]") ){					
		my $res = $sth->execute($cam_label->to_literal(),$self->{Monitor}->{Id},$counter);
		$counter++;
	}
}
 
sub close {
	my $self = shift;
	$self->{state} = 'closed';
}
 
sub printMsg {
	my $self = shift;
	my $msg = shift;
	my $msg_len = length($msg);
 
	Debug( $msg."[".$msg_len."]" );
}
 
sub sendCmd {
	my $self = shift;
	my $cmd = shift;
	my $result = undef;
	printMsg( $cmd, "Tx" );
 
	my $req = HTTP::Request->new( GET=>"http://".$self->{Monitor}->{ControlAddress}."/$cmd".$self->{Monitor}->{ControlDevice} );
	my $res = $self->{ua}->request($req);
 
	if ( $res->is_success )
	{
		$result = !undef;
	}
	else
	{
		Error( "Error check failed:'".$res->status_line()."'" );
	}
 
	return( $result );
}

sub autoStop
{
    my $self = shift;
    my $autostop = shift;
    if( $autostop )
    {
       Debug( "Auto Stop" );
       usleep( $autostop );
       my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzStopRun&";
       $self->sendCmd( $cmd );
    }
}

#Reboot camera
sub reset {
	my $self = shift;
	Debug( "Camera Reset" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=rebootSystem&";
	$self->sendCmd( $cmd );
}

#Up Arrow
sub moveConUp {
	my $self = shift;	
	Debug( "Move Up" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveUp&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Down Arrow
sub moveConDown {
	my $self = shift;	
	Debug( "Move Down" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveDown&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Left Arrow
sub moveConLeft {
	my $self = shift;	
	Debug( "Move Left" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveLeft&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Right Arrow
sub moveConRight {
	my $self = shift;	
	Debug( "Move Right" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveRight&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Diagonally Up Right Arrow
sub moveConUpRight {
	my $self = shift;	
	Debug( "Move Diagonally Up Right" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveTopRight&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Diagonally Down Right Arrow
sub moveConDownRight {
	my $self = shift;	
	Debug( "Move Diagonally Down Right" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveBottomRight&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Diagonally Up Left Arrow
sub moveConUpLeft {
	my $self = shift;	
	Debug( "Move Diagonally Up Left" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveTopLeft&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Diagonally Down Left Arrow
sub moveConDownLeft {
	my $self = shift;	
	Debug( "Move Diagonally Down Left" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzMoveBottomLeft&";
	$self->sendCmd( $cmd );
	$self->autoStop( $self->{Monitor}->{AutoStopTimeout} );
}
 
#Stop
sub moveStop {
	my $self = shift;
	Debug( "Move Stop" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzStopRun&";
	$self->sendCmd( $cmd );
}
 
#Move Camera to Home Position
sub presetHome {
	my $self = shift;
	Debug( "Home Preset" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzReset&";
	$self->sendCmd( $cmd );
}
 
#Set preset
sub presetSet {
    my $self = shift;
    my $params = shift;
	my $preset = $self->getParam($params, 'preset');
	
	#load preset name from camera
	my $old_preset = $preset-1;
	my $dom = XML::LibXML->load_xml(location => "http://".$self->{Monitor}->{ControlAddress}."/cgi-bin/CGIProxy.fcgi?cmd=getPTZPresetPointList&".$self->{Monitor}->{ControlDevice});
	my $old_label = $dom ->findvalue("/CGI_Result/point$old_preset");
	
	my $delete_old_label_cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzDeletePresetPoint&name=$old_label&";
    $self->sendCmd( $delete_old_label_cmd);
	
	my $dbh = zmDbConnect(1);
	my $sql = 'SELECT * FROM ControlPresets WHERE MonitorId = ? AND Preset = ?';
	my $sth = $dbh->prepare($sql);
    my $res = $sth->execute($self->{Monitor}->{Id}, $preset);
    my $ref = ($sth->fetchrow_hashref());
    my $label = $ref->{'Label'};   
    my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzAddPresetPoint&name=$label&";
    $self->sendCmd( $cmd );
	$self->loadPresets();
}
 
#Goto preset
sub presetGoto {
    my $self = shift;
    my $params = shift;
	my $preset = $self->getParam($params, 'preset');
	my $dbh = zmDbConnect(1);
	my $sql = 'SELECT * FROM ControlPresets WHERE MonitorId = ? AND Preset = ?';
	my $sth = $dbh->prepare($sql);
	my $res = $sth->execute($self->{Monitor}->{Id}, $preset);
    my $ref = ($sth->fetchrow_hashref());
    my $label = $ref->{'Label'};
    Debug( "Goto Preset $preset" );
    my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=ptzGotoPresetPoint&name=$label&";
    $self->sendCmd( $cmd );
}
 
#Turn IR on
sub wake {
	my $self = shift;
	Debug( "Wake - IR on" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=openInfraLed&";
	$self->sendCmd( $cmd );
}
 
#Turn IR off
sub sleep {
	my $self = shift;
	Debug( "Sleep - IR off" );
	my $cmd = "cgi-bin/CGIProxy.fcgi?cmd=closeInfraLed&";
	$self->sendCmd( $cmd );
}

1;
If you like my work, feel free to buy me a coffee: