#!/usr/bin/perl
# -----------------------------------------------------------------------------
#  A kind of cam_patrol "proxy". Responsible for replicating
#  db config to flat files. Implements a part of cam_patrol
#  functionality dealing with creating device directories
#  (e.g. /opt/sarch/var/conf/1) when device is created, updating
#  device config files when its configuration is changed in db
#  and removing device files with all required cleanups when a
#  'deleted' flag is set for the corresponding object in db
# -----------------------------------------------------------------------------
#  Author: Alex Tsibulnik
#  Edited by: AT
#  QA by:
#  Copyright: videoNEXT LLC
# -----------------------------------------------------------------------------
use strict;
use warnings;
use POSIX "_exit";
use Data::Dumper;
use MIME::Base64;
use JSON;
use SKM::DB;
use NextCAM::Init 'GetAsrv';
use NextCAM::Conf qw(GetCfgs WriteCfg);
use Node::Conf qw(UNI NodeConf Set_Conf);
use SKM::Common;
use Master::Zmk;
use Log::Log4perl "get_logger";

require "$ENV{APL}/common/bin/logger.patrol";
my $log = get_logger('NEXTCAM::CAM::CAM_PATROL');
$log->logdie('Variables APL/CONFDIR must be set') if !$ENV{APL} || !$ENV{APL_CONF};

#----------------------------- PRESETS ----------------------------------------------
my $APL = $ENV{APL};
my $CONFDIR = $ENV{APL_CONF};
my $APL_VAR = $ENV{APL_VAR};
my $APL_USR = $ENV{APL_USR} || 'apl';
my $APL_HTTPD_GRP = $ENV{APL_HTTPD_GRP} || 'apache';
my $STAGING_AREA = "$CONFDIR/conf";
my $LICENSE_FILE = "$APL_VAR/license/license.key";
my $VCA_LIC_FILE = "$CONFDIR/vca.lic";
my $MSERVER = "$APL/mgears/bin/mediaServer";
my $UNI = UNI;
my $NodeOBJ;
my $FirstLoop = 1;
my $NodeIP;
my $Hangup = 0;
my $dbm;
my $dbh;
my %dbs;                  # prepared statemens;
my $last_stat_check=0;    # time when status was checked
my $last_wstat_check=0;   # time when wipe status was checked

my $DB_CONNECT_INTERVAL = 10; # Timeout between attempts
my $WSTAT_CHECK_INTERVAL= 30; # default seconds between storage space consumption checks
my $STAT_CHECK_INTERVAL = 6;  # default seconds between updating statistics
my $DB_MAX_CONNECTS = 6; # Maximum attempts to connect to apl db
my $AMQ_MAX_CONNECTS = 3;
my $AMQ_CONNECT_INTERVAL = 5;
my $MAIN_LOOP_SLEEP = 3;
my $IDENTITY_OBJID  = 53;
my $IDENTITY_CONF   = "$CONFDIR/identity.conf";
my $NODE_CONF       = "$CONFDIR/node/conf";
my $IS_MASTER = -f "$CONFDIR/master/s_master";
my %Stat;       # Actual camera statuses
my %Identity;
my %Conf;

# Provide AMQ notifications if Net::STOMP::Client package installed
my $AMQNotify = 0;
my $Stomp;
eval {
    require Net::STOMP::Client;
    Net::STOMP::Client->import;
};
if ($@) {
    $log->warn(
	"Net::STOMP::Client package is missing in the system. ".
	"Pushing update notifications to AMQ will be disabled"
    );
}
else {
    $AMQNotify = 1;
}

# Configuration attributes to be excluded from replication
my @EXCL_ATTR = ('OBJ','DELETED','STIME','RTIME','STATUS','OLD_DEVID','OTYPE','CMD_RESET');

#----------------------------- SIGNALS ----------------------------------------------
$SIG{INT}  = sub { die 'SIGINT' };
$SIG{USR1} = sub { $log->info("SIGUSR1 received");};
$SIG{TERM} = sub { die 'SIGTERM' };
$SIG{HUP}  = sub { $log->info("HUP received"); $Hangup = 1 };
$SIG{PIPE} = 'IGNORE'; # for AMQ socket write error handling

#---------------------------- Start main loop ---------------------------------------

main();

#----------------------------- ROUTINES ---------------------------------------------
#####################################################################################

sub load_avg     { # indicate a current CPU load
    if(open(AVG,"/proc/loadavg")) {
      my ($avg)=split(/\s/,<AVG>);
      close AVG;
      return $avg;
    } else {
      return 5;    # never return here, but some default just in case
    }
}

#----------------------------- amq_init ---------------------------------------------
# Establish TCP connection to ActiveMQ daemon
#
sub amq_init {
    my $init = shift;
    my $max_tries = $init ? $AMQ_MAX_CONNECTS : 1;
    #eval { $Stomp->disconnect } if $Stomp;
    for (my $i = 1; $i <= $AMQ_MAX_CONNECTS; $i++) {
	eval {
	    $Stomp = Net::STOMP::Client->new(
	        host => "127.0.0.1", 
	        port => 61613,
	        timeout => {
		    connect    => 3,
	            connected  => 3,
	            disconnect => 3,
	            receive    => 3,
	            send       => 1
	        });
	    $Stomp->connect;
	};
        if ($@) {
    	    chomp $@;
    	    $log->warn("AMQ connection failed (attempt $i): $@");
    	    last if $i >= $max_tries;
    	    sleep $AMQ_CONNECT_INTERVAL;
        } else {
    	    last;
        }
    }
}

#----------------------------- db_master --------------------------------------------
# Establish connection to master database
#
sub db_master {
    eval { $dbm->disconnect() if $dbm; $dbm=''; }; # disconnect if defined
    for (my $i = 1; $i <= $DB_MAX_CONNECTS; $i++) {
	eval {
    	    $dbm=DBMaster({PrintError=>1,RaiseError => 1});
    	    $dbm->{FetchHashKeyName} = 'NAME_uc';
	    $dbm->{ShowErrorStatement} = 1;
            $dbm->{AutoCommit}=1;           #auto-commit, some functions will disable/enable auto-commit
	};
	if($@) {
            $log->logdie("DB_MASTER: Attempt $i (final). Cannot connect to master: $@") if $i>=$DB_MAX_CONNECTS;
    	    $log->error("Attempt $i. Cannot connect to master: $@");
    	    $log->error('Sleep '. $i*$DB_CONNECT_INTERVAL . ' before next attempt');
    	    sleep($i*$DB_CONNECT_INTERVAL);
	} else {
	    last;
	}
     }
     eval {
       $dbs{DELETE_OBJ}=$dbm->prepare("UPDATE _objs SET deleted=1 WHERE obj=?");
       $dbs{GET_OBJS}=$dbm->prepare("SELECT obj, otype, stime, rtime, deleted FROM _objs
	    				  WHERE (rtime is NULL or rtime < stime)
	    				  AND otype in ('D','V') AND subtype not in ('L','N')
	    				  AND (node_id=? OR node_id IN
	    				  (SELECT obj FROM _objs WHERE otype='D' and subtype='E'))");
       $dbs{GET_OBJ_STIME}=$dbm->prepare("SELECT stime from _objs where obj=?");
       $dbs{GET_IDENTITY_RTIME}=$dbm->prepare("SELECT 1 from _objs where obj=? AND (rtime is NULL or rtime < stime)");
       $dbs{GET_IDENTITY_ATTR}=$dbm->prepare("SELECT attr,val FROM _obj_attr WHERE obj=?");
       $dbs{GET_LICENSE_KEY}=$dbm->prepare("SELECT val FROM _obj_attr WHERE obj=? and attr='LICENSE_KEY'");
       $dbs{UPDATE_RTIME}=$dbm->prepare("UPDATE _objs SET rtime=now() at time zone 'UTC' WHERE obj=?");
       $dbs{CLEAR_RTIME}=$dbm->prepare("UPDATE _objs SET rtime=null WHERE obj=?");
       $dbs{UPDATE_ATTR}=$dbm->prepare("update _obj_attr set val=? where attr=? and obj=?");
       $dbs{UPDATE_SPACE}=$dbm->prepare("update _obj_attr set val=coalesce((select case when round(space/1024)> 0 then round(space/1024) else 0 end from sm_space where nodeid=? and objid=?),0)  where attr='STAT_VA_SPACE' and obj=?");
       $dbs{INSERT_SPACE}=$dbm->prepare("insert into _obj_attr(obj,attr,val) select objid,'STAT_VA_SPACE',coalesce(case when round(space/1024)> 0 then round(space/1024) else 0 end,0) from sm_space where nodeid=? and objid=?");
       $dbs{INSERT_ATTR}=$dbm->prepare("insert into _obj_attr(obj,attr,val) values(?,?,?)");
       $dbs{AUDIO_OBJ}  =$dbm->prepare("SELECT obj FROM _objs"
                                         ." where otype='D' AND subtype ='A' AND obj=? AND node_ip=?");
       $dbs{NEW_OBJ}    =$dbm->prepare("SELECT nextval_seq_obj()");
       $dbs{INSERT_OBJ} =$dbm->prepare("INSERT INTO _objs (obj,node_id,node_ip,otype,subtype,name,description,location,rtime)"
                                           ."VALUES (?,?,?,'D','A',?,?,?,now() at time zone 'UTC')");
       $dbs{ALLAUDIO_INS}=$dbm->prepare("INSERT INTO _links (obj_res,obj_cons,protected) VALUES (?,3,1)");
       $dbs{DELETE_ATTR}=$dbm->prepare("DELETE FROM _obj_attr WHERE obj=? and attr not in ('MON','tourinfo','STATUS')");
       $dbs{GET_NODE_TS}=$dbm->prepare("SELECT obj,rtime,
    							CASE WHEN rtime < stime
    							     THEN 1
    							     ELSE 0
    							END AS updated
    					FROM _objs WHERE name=? AND otype='D' AND subtype='N' and deleted=0");
       $dbs{GET_NODE_ATTR}=$dbm->prepare("SELECT attr,val FROM _obj_attr WHERE obj=? and attr not like E'STAT\\\\_%'");
       $dbs{GET_NODE_IP}=$dbm->prepare("SELECT val FROM _objs o INNER JOIN _obj_attr oa ON o.obj=oa.obj
    					WHERE otype='D' and subtype='N' and name=? and attr='IP' AND deleted=0");
       $dbs{FIND_SUBSTITUTE_NODE}=$dbm->prepare("SELECT o.obj FROM _objs o INNER JOIN _obj_attr oa ON o.obj=oa.obj
    						WHERE otype='D' and subtype='N' and attr='IP' and val=? and deleted=0");
       $dbs{CMD_RESET_ACK}=$dbm->prepare("UPDATE _obj_attr SET val='0' WHERE obj=? AND attr='CMD_RESET'");
       # Get Node IP
       $dbs{GET_NODE_IP}->execute(UNI());
       my $arr_obj=$dbs{GET_NODE_IP}->fetchall_arrayref;
       $NodeIP =$arr_obj->[0][0];
       
       # Camera downtime related queries
       $dbs{FETCH_OPEN_PERIODS}=$dbm->prepare("SELECT d.objid,d.utc_from,d.status FROM camera_downtime d INNER JOIN _objs o
                                                ON o.obj=d.objid WHERE o.node_id=? AND d.utc_to IS NULL AND o.deleted=0");
       $dbs{SCHEDULED_CAM}=$dbm->prepare(
            "SELECT a2.val FROM _obj_attr a INNER JOIN _obj_attr a1 
            ON (CASE a.val WHEN '' THEN 0 ELSE a.val::int END)=a1.obj 
            INNER JOIN _obj_attr a2 
            ON (CASE a1.val WHEN '' THEN 0 ELSE a1.val::int END)=a2.obj
            WHERE a.obj=? AND a.attr='SCHEDULEID' AND a1.attr='DEFAULT_POSTUREID' AND a2.attr='PROPERTY_ARCHSTATE'"
       );
       $dbs{OPEN_PERIOD}=$dbm->prepare("INSERT INTO camera_downtime (objid, utc_from, utc_to, status) 
                                        VALUES (?, now() at time zone 'UTC', NULL, ?)");
       $dbs{CLOSE_PERIOD}=$dbm->prepare("UPDATE camera_downtime SET utc_to=now() at time zone 'UTC' 
                                        WHERE objid=? AND utc_to IS NULL");
       
       # Nodechange-related queries
       $dbs{VALIDATE_NODEID}=$dbm->prepare("SELECT name FROM _objs WHERE otype='D' AND subtype='N' AND deleted=0 AND obj=?");
       $dbs{UPDATE_NODEID}=$dbm->prepare("UPDATE _objs SET node_id=?, node_ip=? WHERE obj=?");
     };
     $log->logdie("SQLERR:$@") if $@;
}

sub get_mtime { #------------- GET THE AGE OF THE FILE ------------------------------
    my @ss = stat(shift);
    return $ss[9];
}
#----------------------------- stat2db   --------------------------------------------
#
# read stat and put into db (_obj_attr);
# ATTN: OBJ=DEV
#
sub stat2db {
    my ($dev)=@_; # for cameras: objid==dev
    my %stat;
    if (open(STAT,"$CONFDIR/$dev/stat")) {
        %stat=map{/(^\w+)=(.+)/} grep {/^\w+=.+/} <STAT>;
        close STAT;
        $Stat{$dev} = $stat{STATUS}; # Valid for cameras only
    }
    foreach (keys %stat) {
#      print "stat for $dev $_=$stat{$_}\n";
      eval {
        $dbs{UPDATE_ATTR}->execute($stat{$_},$_,$dev);
        my $rows=$DBI::rows;
        if($rows<1) {        # stat record is missing. Inserting ..
    	    $dbs{INSERT_ATTR}->execute($dev,$_,$stat{$_});
        }
        $dbs{UPDATE_SPACE}->execute($UNI,$dev,$dev);
        my $rows2=$DBI::rows;
        if($rows2<1) {        # stat record is missing. Inserting ..
            $dbs{INSERT_SPACE}->execute($UNI,$dev);
        }
      };
      $log->error("Cannot write status to DB for device ObjId=$dev: $@") if $@;
    }
    
    # Send STAT_SHOW for avatar cameras only
    if ($AMQNotify and $Stomp and defined $stat{STAT_SHOW}) {
	amq_notify($dev, {STAT_SHOW => $stat{STAT_SHOW}}, undef, 'obj_stat_change');
    }
}

sub anl_stat2db {
    my ($dev)=@_; # for cameras: objid==dev
    my $stat_anl="";
    if (open(STAT,"$CONFDIR/$dev/stat.vae")) {
	local $/;
	$stat_anl = <STAT>;
	close STAT;
	# rtrim
	$stat_anl =~ s/\s+$//g;
    }
    eval {
        $dbs{UPDATE_ATTR}->execute($stat_anl,"STAT_VAE",$dev);
        my $rows=$DBI::rows;
        if($rows<1) {        # stat record is missing. Inserting ..
    	    $dbs{INSERT_ATTR}->execute($dev,"STAT_VAE",$stat_anl);
        }
    };
    $log->error("Cannot write analytics status to DB for device ObjId=$dev: $@") if $@;
}

sub av_stat2db {
    my %default_stat=(
     STAT_STATUS=>'OFFLINE',STAT_REASON=>'',STAT_REASON_TIME=>'',STAT_UPTIME=>'',
     STAT_POSITION=>'',STAT_POWER=>'OK',STAT_POWER_TIME=>''
     # will be extended
    );
    return if not -d "$CONFDIR/av"; # avatar conf & stat
    opendir(DIR, "$CONFDIR/av") or die "Cannot open $CONFDIR/av: $!";
    my @devs=grep {/^\d+$/} readdir(DIR);
    close DIR;
    my $cur_time=time;
    foreach my $dev (@devs) {
      my $stat_name="$CONFDIR/av/$dev/stat";
 #     my $stat_name="$CONFDIR/av/stat";      # this is old incorrect location
      next if not -f $stat_name;
      next if get_mtime($stat_name)<$last_stat_check;
      my %stat;
      if (open(STAT,$stat_name)) {
        %stat=map{/(^\w+)=(.*)/} grep {/^\w+=.*/} <STAT>;
        close STAT;
      }
      %stat=(%default_stat,%stat); # enforce defaults
      foreach (keys %stat) {
        my $attr=$_;
        next if ! /^STAT/;
        $attr="STAT_".$_ if ! /^STAT/;
#       print "stat for $dev $attr=$stat{$_}\n";
        eval {
          $dbs{UPDATE_ATTR}->execute($stat{$_},$attr,$dev);
          my $rows=$DBI::rows;
          if($rows<1) {        # stat record is missing. Inserting ..
              $dbs{INSERT_ATTR}->execute($dev,$attr,$stat{$_});
          }
        };
        $log->error("Cannot write status to DB for device ObjId=$dev: $@") if $@;
      }
    }
}

#----------------------------- scan_stat --------------------------------------------
#
# Check any update stat file
my $delayed_last_time=0;

sub scan_stat {
    my $cur_time=time;
    my $loadavg=load_avg();
    return if $cur_time - $last_stat_check<$STAT_CHECK_INTERVAL;         # to early to check again
    my $factor=1;
    $factor=2  if $loadavg > 60;
    $factor=4  if $loadavg > 200;
    $factor=6  if $loadavg > 300;

    opendir(DIR, "$CONFDIR") or die "Cannot open $CONFDIR: $!";
    my @devs=grep {/^\d+$/} readdir(DIR);
    close DIR;
    # Remove deleted devices from camera status cache
    foreach my $obj (keys %Stat) {
        my $exists = 0;
        foreach my $dev (@devs) { 
            if ($dev eq $obj) {
                $exists = 1;
                last;
            }
        }
        delete $Stat{$obj} if not $exists;
        # Delete from global conf cache
        delete $Conf{$obj} if not $exists;
    }
    
    if($cur_time -  $last_stat_check > $STAT_CHECK_INTERVAL * $factor) { 
      $dbm->{AutoCommit} = 0;        # disable auto-commit (performance)
      foreach my $dev (@devs) {
        next if not -f "$CONFDIR/$dev/stat";
        next if get_mtime("$CONFDIR/$dev/stat")<$last_stat_check;
        stat2db($dev); 
      }
      foreach my $dev (@devs) {
        next if not -f "$CONFDIR/$dev/stat.vae";
        next if get_mtime("$CONFDIR/$dev/stat.vae")<$last_stat_check;
        anl_stat2db($dev);
      }
      $dbm->commit;
      $dbm->{AutoCommit} = 1;        # enable auto-commit
      av_stat2db;                    # avatar stat
      $last_stat_check=$cur_time;
      $delayed_last_time=0;                 # clean delay stat info
      
      manage_downtime();
      
    } else {
        my $delay=$STAT_CHECK_INTERVAL * $factor - ($cur_time - $last_stat_check);
        $delay=1 if $delay<1; # if fractions
        if($delay>$delayed_last_time) {     # only message if delay increased
           $log->info("load avg: $loadavg. Statistics delayed for $delay seconds") if $factor!=1;
           $delayed_last_time=$delay;
        }
    }
    
    scan_wstat(\@devs) if $cur_time - $last_wstat_check > $WSTAT_CHECK_INTERVAL;
}

#----------------------------- wstat2db   --------------------------------------------
#
# read storage stat and put into db (_obj_attr);
# ATTN: OBJ=DEV
#
sub wstat2db {
    my ($dev,$hist)=@_;
    my $hist_orig = $hist;

    my $cur_hour = (gmtime(time))[2];
    $hist = join(':,',0..23).":" unless $hist;
    my @hours = map {/^\d+:(.*)$/} split(',',$hist);

    # Read current value of VA_RATE
    my ($va_rate,$va_rate_hour,$va_rate_day) = (undef, 0, 0);
    if (open(STAT,"$CONFDIR/$dev/stat.wipe")) {
        ($va_rate)=map{/^VA_RATE=(\d+)/} grep {/^\w+=.+/} <STAT>;
        close STAT;
    }

    if (defined $va_rate) {
	# Convert to MB/hour
	$va_rate_hour = int($va_rate * 3600 / 1024);
        $hours[$cur_hour] = $va_rate_hour;
    }
    else {
	$log->error("stat.wipe contains invalid VA_RATE value for objid=$dev");
    }

    # Compute average values
    my @count_hours = grep $_,@hours;
    $va_rate_day += $_ foreach @count_hours;
    $va_rate_day = @count_hours ? int($va_rate_day/@count_hours) * 24 : 0;

    # Construct new STAT_VA_RATE_HOURS_PER_DAY value
    my $i = 0;
    do { $_ = "$i:$_"; $i++ } foreach @hours;
    $hist = join(',',@hours);

    # Update DB attrs
    eval {
	$dbs{UPDATE_ATTR}->execute($hist,'STAT_VA_RATE_HOURS_PER_DAY',$dev);
	my $rows=$DBI::rows;
        if($rows<1) {        # stat record is missing. Inserting ..
    	    $dbs{INSERT_ATTR}->execute($dev,'STAT_VA_RATE_HOURS_PER_DAY',$hist);
        }
	$dbs{UPDATE_ATTR}->execute($va_rate_hour,'STAT_VA_RATE_HOUR',$dev);
	$dbs{UPDATE_ATTR}->execute($va_rate_day,'STAT_VA_RATE_DAY',$dev);
    };
    $log->error("Cannot write storage statistics to DB for device ObjId=$dev: $@") if $@;
}

#----------------------------- scan_wstat --------------------------------------------
#
# compute and update camera average space consumption statistics

sub scan_wstat {
    my @devs = @{$_[0]};
    return unless @devs;
    # Query stats from DB
    my $sql = "SELECT obj,val FROM _obj_attr WHERE obj IN (".join(',',@devs). ")
		AND attr='STAT_VA_RATE_HOURS_PER_DAY'";
    my $rhStat;
    eval {
	$rhStat = $dbm->selectall_hashref($sql,'OBJ',{Slice=>{}});
    };
    if ($@) {
	$log->error("Cannot get STAT_VA_RATE_HOURS_PER_DAY attributes from DB: $@");
    }
    else {
        $dbm->{AutoCommit} = 0;        # disable auto-commit (performance)
	foreach my $dev (@devs) {
    	    next if not -f "$CONFDIR/$dev/stat.wipe";
	    next if get_mtime("$CONFDIR/$dev/stat.wipe")<$last_wstat_check;
	    wstat2db($dev,$rhStat->{$dev}{VAL});
	}
        $dbm->commit;
        $dbm->{AutoCommit} = 1;        # enable auto-commit
    }
    $last_wstat_check=time;
}

#----------------------------- audio support --------------------------------------
sub get_existen_audio_objid {
    my $dev=shift;
    my ($obj)=$dev=~/^a(\d+)$/;
    $dbs{AUDIO_OBJ}->execute($obj,$UNI);
    my $rows=$dbs{AUDIO_OBJ}->fetchall_arrayref or die "SQLERR:Can not find OBJ for DEV=$dev";
    return '' if  $dbs{AUDIO_OBJ}->rows<1;
    return $rows->[0][0];
}


sub new_obj {
    $dbs{NEW_OBJ}->execute() or die "SQLERR:Can not get new OBJID for audio device";
    my $rows = $dbs{NEW_OBJ}->fetchall_arrayref;
    return $rows->[0][0];
}


#----------------------------- audio_setup -----------------------------------------
#
# check if defice alredy have audio, then update
# if audio not present then create
#
sub audio_setup {
  my $video=shift;
  my ($associate,$audio_devid)=($video->{ASSOCIATE},$video->{AUDIO_DEVID}); # remember old values
  return if not defined $video->{AUDIO} or ! $video->{AUDIO};
  $log->info("setup audio for $video->{OBJID}");
  my  %audio;    #--------------------------------------- get default audio-conf
  eval {
    $dbm->{AutoCommit}=0;
    open(TEMPL, "$APL/conf/etc/audio.cfg") or die("template cannot be oppened $APL/conf/etc/audio.cfg");
    %audio=map {(split(/~/))[1,4]} grep{!/^\s*#/} <TEMPL>;
    close TEMPL;
    foreach(keys %audio) { #------------------------------ dublicate video attrs
    next if /^(OBJID|DEVID|NAME|MEDIA_FORMAT|DEVICETYPE)$/; # keep defaults
      $audio{$_}=$video->{$_} if exists $video->{$_};
    }
    $audio{DEVID}=$video->{AUDIO_DEVID};
    $audio{OBJID}=get_existen_audio_objid($audio{DEVID});
    $audio{NAME}="audio $video->{NAME}";
    $audio{ARCHSTATE}=($video->{AUDIO} eq 'on')?'on':'off';
    $audio{ARCHSTATE}='off' if $video->{ARCHSTATE} eq 'off';
    $audio{BELONGS2DEVID}=$video->{DEVID};
#    print Dumper \%audio;
    if(! $audio{OBJID}) {  #---------------------------- new audio
      $log->warn("Misconfigured audio for dev=$video->{DEVID}: $audio{DEVID}!")
        if $audio{DEVID} and -d "$CONFDIR/$audio{DEVID}";
      $audio{OBJID}=new_obj();
      $audio{DEVID}='a'.$audio{OBJID};  #keep the same for new audio but preffix a
      $dbs{INSERT_OBJ}->execute($audio{OBJID},$NodeOBJ,$UNI,$audio{NAME},$audio{NAME},$audio{LOCATION});
      $dbs{ALLAUDIO_INS}->execute($audio{OBJID});
      #$video->{ASSOCIATE}=$audio{OBJID}; # fine for Cirrus, TBD for SKM
      if($video->{ASSOCIATE}!~/\b$audio{OBJID}\b/) {  # if obj is not in the associate
        $video->{ASSOCIATE}=",$video->{ASSOCIATE}" if $video->{ASSOCIATE};
        $video->{ASSOCIATE}="$audio{OBJID}$video->{ASSOCIATE}";
      }

      $video->{AUDIO_DEVID}=$audio{DEVID};
      $dbs{UPDATE_ATTR}->execute($video->{ASSOCIATE},'ASSOCIATE',$video->{OBJ});
      $dbs{UPDATE_ATTR}->execute($video->{AUDIO_DEVID},'AUDIO_DEVID',$video->{OBJ});
    }else {                #--------------------------- existent audio
      $dbs{DELETE_ATTR}->execute($audio{OBJID});
    }
    foreach my $attr ( keys %audio ) {
      $dbs{INSERT_ATTR}->execute($audio{OBJID}, $attr, $audio{$attr});
    }
    $dbs{DELETE_OBJ}->execute($audio{OBJID}) if $video->{DELETED} ne '0';
    WriteCfg(\%audio);     #-------------------------- commit
    $dbm->commit;
  };
  if ($@) {                #-------------------------- rollback
    $log->error("cannot create audio for video OBJID: $video->{OBJ}: $@");
    $dbm->rollback;
    ($video->{ASSOCIATE},$video->{AUDIO_DEVID})=($associate,$audio_devid); # rollback changed values
  }
  $dbm->{AutoCommit}=1; # switch on auto-commit
}

# Handles special situation when device model is changed and we need to
# perform a bunch of special actions (e.g. cleanup old links)
sub on_model_change {
    my $video = shift;
    $log->info("handle model change for $video->{OBJ}");
    if (! $video->{AUDIO} and $video->{AUDIO_DEVID}) {
	# New model does not have audio but old one had
	# Remove audio device and cleanup links
	$log->info("Remove audio device cause new model shouldn't have it");
	my ($associate,$audio_devid)=($video->{ASSOCIATE},$video->{AUDIO_DEVID}); # remember old values
	$dbm->{AutoCommit} = 0;
	eval {
	    my $audio_objid = get_existen_audio_objid($audio_devid);
	    if ($audio_objid) {
		if($video->{ASSOCIATE}=~/\b$audio_objid\b/) {  # if obj is in the associate
    		    my $str = trim($video->{ASSOCIATE});
    		    my @objids = trim(split(/,/, $str));
    		    my @res = ();
    		    foreach my $objid (@objids) {
    			next if $objid == $audio_objid;
    			push @res, $objid;
    		    }
    		    $video->{ASSOCIATE} = join(', ',@res);
    		}
    	    }
    	    $video->{AUDIO_DEVID} = '';
    	    $dbs{UPDATE_ATTR}->execute($video->{ASSOCIATE},'ASSOCIATE',$video->{OBJ});
    	    $dbs{UPDATE_ATTR}->execute($video->{AUDIO_DEVID},'AUDIO_DEVID',$video->{OBJ});
    	    # Delete audio device
    	    $dbs{DELETE_OBJ}->execute($audio_objid) if $audio_objid;
    	    $dbs{CLEAR_RTIME}->execute($audio_objid) if $audio_objid;
    	    $dbm->commit;
	};
	if ($@) {                #-------------------------- rollback
	    $log->error("cannot delete audio for video OBJID: $video->{OBJ}: $@");
	    $dbm->rollback;
	    ($video->{ASSOCIATE},$video->{AUDIO_DEVID})=($associate,$audio_devid); # rollback changed values
	}
	$dbm->{AutoCommit} = 1;
    }
}

#----------------------------- restart_app ------------------------------------------
# Restart application
#
sub restart_app {
    sleep 5;
    $log->info("restarting the node");
    local $SIG{TERM} = 'IGNORE';              # protect from kill TERM till the rest of func
    system("sudo /opt/sarch/conf/bin/node_proc_ctl restart >/dev/null 2>&1 &");
    sleep 30;                                 # expect to be killed (-9) at this point
    exit 0;                                   # exit if survived
}

#----------------------------- get_node_ts ------------------------------------------
# Returns node objid, rtime and 'updated' flag
#
sub get_node_ts {
    my $arr_obj;
    eval {
	$dbs{GET_NODE_TS}->execute($UNI=UNI());	# Reread node UNI cause it might have been changed
        $arr_obj = $dbs{GET_NODE_TS}->fetchall_arrayref;
    };
    if ($@) {
	die "SQLERR: $@";
    }
    die "Master DB is in inconsistent state!" if @$arr_obj > 1;
    $NodeOBJ = $arr_obj->[0][0] if @$arr_obj == 1;
    return $arr_obj;
}

#----------------------------- replicate_vca_license --------------------------------
# Replicate VCA analytics key
sub replicate_vca_license {
    my $rh = shift;
    
    if (exists $rh->{SSVA_VCA_ACTKEY}) {
    	my $vca_key = $rh->{SSVA_VCA_ACTKEY};
    	if (!$vca_key) { # Key is empty
    	    $log->warn("Node attribute SSVA_VCA_ACTKEY is empty. Skip replication");
    	}
    	else {{ # Compare old and new values
    	    my $curval;
    	    $log->error("Unable to read VCA license file"),last
    	        if -f $VCA_LIC_FILE and not open(FH, $VCA_LIC_FILE);
    	    $curval .= $_ while <FH>;
    	    close FH;

    	    # Do nothing if license was not changed
    	    last if $vca_key eq $curval;

    	    # Backup previous license
    	    rename($VCA_LIC_FILE, $VCA_LIC_FILE.".bak") if -f $VCA_LIC_FILE;
    	    if (open(FH, ">$VCA_LIC_FILE")) {
    		print FH $vca_key;
    		close FH;
    		$log->info("VCA license replicated successfully");
    	    }
    	    else {
    		$log->error("Unable to replicate VCA license: $!");
    	    }
    	}}
    }
}

sub replicate_node_attr {
    my ($rh, $rhNode);
    eval {
        $dbs{GET_NODE_ATTR}->execute($NodeOBJ);
        $rh = $dbs{GET_NODE_ATTR}->fetchall_hashref('ATTR');
    };
    die "DB_MASTER: $@" if $@;
    
    # Comvert to simple hashmap
    $rhNode->{$_} = $rh->{$_}{VAL} foreach (keys %$rh);

    # Replicate vca license
    replicate_vca_license($rhNode);

    my $nodeconf = NodeConf; # Configuration from flat file
    $log->error("Cannot read node attributes from flat file: $!"),return if not $nodeconf;

    # Do not compare internal controlled by node attributes such as IP address
    my @diff;
    my @ignore = ('OBJID','UNI','FQDN','VERID','IP','RTSP_PORT','HOST','INSTALL_RESULT','HISTORY');
    my $diff_result = hash_diff($rhNode, $nodeconf, \@diff, \@ignore);

    return unless $diff_result;

    # Replicate to flat file
    # Got only one attribute to be replicated from DB to flat file: URI
    local $" = '|';
    foreach my $attr (grep {not /^(@ignore)$/} keys %$rhNode) {
        $nodeconf->{$attr} = $rhNode->{$attr};
    }
    if (not Set_Conf($nodeconf)) {
        $log->error("Cannot write node conf: $!");
    }
}

#----------------------------- check_cloud_storage  ---------------------------------
# Adjust current value of cloud storage provider
#
sub check_cloud_storage {
    my $b = Master::Zmk::VABundle();
    my $val = $b->{CLOUD_STORAGE};
    $val = 'none' if not $val or $val=~/DISABLED/i;
    $dbs{UPDATE_ATTR}->execute($val, "CLOUD_STORAGE", $IDENTITY_OBJID);
    $dbs{UPDATE_ATTR}->execute("no", "CLOUD_STORAGE_ENABLED", $IDENTITY_OBJID)
	if $val eq 'none';
    $dbs{CLEAR_RTIME}->execute($IDENTITY_OBJID);
}

#----------------------------- replicate_license ------------------------------------
# Replicate system license data from DB to flat file
#
sub replicate_license {
    my $has_license_file = -f $LICENSE_FILE;
    my $has_license_db = 0;
    my $license_changed = 0;
    my $license_data;

    # Fetch attributes from DB
    eval {
	$dbs{GET_LICENSE_KEY}->execute($IDENTITY_OBJID);
	my $ra = $dbs{GET_LICENSE_KEY}->fetchrow_arrayref;
	$license_data = decode_base64($ra->[0]) if $ra->[0];
	$has_license_db = defined($license_data) && length($license_data) > 0;
    };
    $log->error("Cannot fetch license key from DB: $@") if $@;

    return if not $has_license_db;

    # Read license from flat file and compare with value from DB
    if (open(FH, $LICENSE_FILE)) {
	local $/;
	my $cur_lic = <FH>;
	close FH;
	$license_changed = 1 if $license_data ne $cur_lic;
    }
    else {
	$log->error("Cannot read $LICENSE_FILE: $!");
    }

    # Store license data to a flat file
    if ( $has_license_db && ( $license_changed || ! $has_license_file ) )
    {
	$log->info("Replicate license data to a flat file");

	# If license file exists but isn't writable, recreate it
	unlink $LICENSE_FILE if -f $LICENSE_FILE and not -w $LICENSE_FILE;

        if (open(FH, ">$LICENSE_FILE")) {
    	    print FH $license_data;
    	    close FH;

    	    # Adjust permissions and ownership
    	    system("/bin/chmod 644 $LICENSE_FILE");
	    system("/bin/chown $APL_USR:$APL_HTTPD_GRP $LICENSE_FILE");
	    
	    # Execute some actions on license change
	    check_cloud_storage;
    	}
    	else {
    	    $log->error("Unable to write to $LICENSE_FILE: $!");
    	}
    }
}

#----------------------------- scan_node_obj ----------------------------------------
# Fetches node stime and rtime from DB. Restarts patrol If rtime is NULL. Stops
# patrol if node object missing in DB. Does replication of analytics keys to flat
# files if needed
#
sub scan_node_obj {
    my $raObj = get_node_ts;

    unless (@$raObj) {
	# IMPLEMENTED LOGIC:
	# If no object of type 'NODE' found in DB for current UNI (it is taken from file)
	# 1. Restart node if substitute node with the same IP found in DB. This situation
	#    takes place if UNI in DB has been just updated but flat file on the node
	#    still contains old UNI
	# 2. Special situation of bacup/restore: Node is Master and both UNI and IP are
	#    changed. No substitute is found in DB. In such case do nothing. On next
	#    iteration UNI in file should be properly updated and situation will be
	#    handled correctly
	# 3. Do not stop patrol in any case

	# Find out whether node UNI was changed
	$dbs{FIND_SUBSTITUTE_NODE}->execute($NodeIP);
        my $arr_obj = $dbs{FIND_SUBSTITUTE_NODE}->fetchall_arrayref;
	if($arr_obj->[0][0]) {
	    $log->warn("Node UNI change detected. Patrol will be restarted");
	    restart_app;
	}
    }
    elsif (not $raObj->[0][1]) { # RTIME is 'null'
	# IMPLEMENTED LOGIC:
	# If node rtime is 'null'
	# 1. If this ist the first loop, we support that 'conf_setup' engine isn't yet finished its work
	#    So, let him accomplish its job, wait a minute, and then restart patrol if rtime not changed
	# 2. Otherwise, restart patrol at once

	if ($FirstLoop) {
	    my $wait = 60;
	    my $tmt = 5;
	    my $ok = 0;
	    do {
		$log->info("Node RTIME is still null on the first launch. Wait $tmt sec...");
		sleep $tmt;
		$wait -= $tmt;
		$ok = @{ get_node_ts() } [0]->[1];
	    } while (not $ok and $wait > 0);

	    restart_app unless $ok;
	}
	else {
	    $log->info("Node RTIME is null. Patrol restart is scheduled");
	    restart_app;
	}

	$FirstLoop = 0;
    }
    elsif ($raObj->[0][2]) { # RTIME < STIME
	replicate_license;
	replicate_node_attr;
	update_rtime($NodeOBJ);
    } # FISLE
}

#----------------------------- scan_db_objs -----------------------------------------
# Look for recently modified devices in _objs table
#
sub scan_db_objs {
    $log->debug("Scanning db objects");
    my %conf;

    for(my $i = 1; $i < 4; $i++) {
	eval {
	    # Find matching devices
	    $dbs{GET_OBJS}->execute($NodeOBJ);
	    my $rh = $dbs{GET_OBJS}->fetchall_hashref('OBJ');
	    return unless %$rh;	# Nothing to do
	    $conf{$_} = $rh->{$_} foreach keys %$rh;

	    # Get device configuration
	    my $obj_list = join(',', keys %conf) or last;
	    my $ra = $dbm->selectall_arrayref("SELECT * from _obj_attr WHERE obj IN ($obj_list) and attr not like E'STAT\\\\_%'",
	        { Slice => {} });
	    $conf{ $_->{OBJ} }{ $_->{ATTR} } = $_->{VAL} foreach (@$ra);
	};
	last if not $@;
        die "DB_MASTER: $@" if $i>=3;
        $log->error("Attempt $i. Cannot get objects from master: $@");
        db_master;
    }

    # Process devices
    foreach my $obj (keys %conf) {
	next unless $obj;
	my $cfg = $conf{$obj};
	if (! $cfg->{DEVID} && $cfg->{OTYPE} eq 'D') { # Bad for devices, not for avatars
	    $log->error("ObjId $obj: Empty DEVID!");
	    next;
	}
	if($obj ne $cfg->{OBJID}) {
	    $log->error("Misconfigured device: obj=$obj, OBJID=$cfg->{OBJID}, DEVID=$cfg->{DEVID}");
	}
	if($cfg->{DELETED} == 1) {
	    $cfg->{LOCATION} = '@garbage'; # required for cam_patrol
	}
        audio_setup($cfg)
    	    if $cfg->{OTYPE} eq 'D' and
    	       $cfg->{DEVICETYPE} eq 'CAMERA' and
    	       $cfg->{AUDIO};
	delegate_to_patrol($cfg) if $cfg->{OTYPE} eq 'D'; # Devices
	handle_avatar($cfg) if $cfg->{OTYPE} eq 'V'; # Avatars
	if ($cfg->{OTYPE} eq 'D' and $cfg->{NODEID} and $cfg->{NODEID} ne $NodeOBJ) {
	    handle_node_change($obj, $cfg);
	}
	else {
	    update_rtime($obj, $cfg);
	}
    }
    
    # Add data to global conf cache
    foreach my $obj (keys %conf) {
    	next if $conf{$obj}{OTYPE} eq 'D' and $conf{$obj}{DEVICETYPE} ne 'CAMERA';
    	delete $Conf{$obj} if $Conf{$obj};
    	$Conf{$obj}{$_} = $conf{$obj}{$_} foreach keys %{$conf{$obj}};
    }
}

#----------------------------- scan_identity ----------------------------------------
# Check and replicate 'Identity' object (objid=53)
#
sub scan_identity {
    $log->debug("Scanning identity");
    my %attr;

    # Remove temproary file
    unlink "${IDENTITY_CONF}.tmp" if -f "${IDENTITY_CONF}.tmp";
    # Read identity from file if not initialized yet
    if (not %Identity and open FH,$IDENTITY_CONF) {
	%Identity = map {/^(\w+)=(.*)$/} grep {/^w+=/} <FH>;
	close FH;
    }
    
    # Fetch attributes from DB
    eval {
	$dbs{GET_IDENTITY_RTIME}->execute($IDENTITY_OBJID);
	my $ra = $dbs{GET_IDENTITY_RTIME}->fetchrow_arrayref;
	$dbs{GET_IDENTITY_ATTR}->execute($IDENTITY_OBJID);
	$ra = $dbs{GET_IDENTITY_ATTR}->fetchall_arrayref;
	$attr{$_->[0]} = $_->[1] foreach (@$ra);
    };
    $log->error("Cannot scan identity: $@") if $@;

    my @diff;
    my $diff_result = hash_diff(\%attr, \%Identity, \@diff);

    return unless $diff_result;

    # Handle all changes to system configuration.
    # Do not rely on cam_patrol and editparam.pl
    unless ($FirstLoop) {
	$log->info("Found IDENTITY diff: @diff");

	#--- ADVANTOR
        if ($IS_MASTER and grep /^(ACC_DATABITS|ACC_BAUDRATE|ACC_STOPBITS|ACC_PORT|ACC_PARITY|ACC_HANDSHAKE)$/, @diff) {
    	    $log->info('Essential configuration change. Have to send SIGHUP to advantord.pl');
    	    system("ps -au apl -o pid,cmd|grep advantord.pl|cut -c 1-6|xargs kill -1 2>/dev/null");
	}
	#--- PTZ_server
	if (grep /^PTZ_PROPAGATE2ELOG$/, @diff) {
	    $log->info('Essential configuration change. Have to send SIGHUP to PTZ_server');
	    system("ps -au apl -o pid,cmd|grep PTZ_server.pl|cut -c 1-6|xargs kill -1 2>/dev/null");
	}
	#--- MediaServer
	if (grep /^MAX_RTSP_SESSIONS$/, @diff) {
	    $log->info('Maximum concurrent session per node changed');
	    setup_max_rtsp_sessions($attr{MAX_RTSP_SESSIONS});
	}
    }
    else {
	# Process MAX_RTSP_SESSIONS on system start (in case of backup recovery)
	setup_max_rtsp_sessions($attr{MAX_RTSP_SESSIONS});
    }
    
    # Replicate for flat file
    if (open FH, ">${IDENTITY_CONF}.tmp") {
	print FH "$_=$attr{$_}\n" foreach sort keys %attr;
	close FH;
	rename "${IDENTITY_CONF}.tmp", $IDENTITY_CONF or $log->error("identity.conf.tmp rename failed");
    }
    else {
	$log->error("Cannot write identity.conf.tmp: $!");
    }
    
    # Finally, update Identity rtime in DB
    %Identity = %attr;
    update_rtime($IDENTITY_OBJID);
}

#----------------------------- setup_max_rtsp_sessions ------------------------------
# Compare given value with current limit
# If limit was changed, update value in local SQLite DB and restart mediaServer
#
sub setup_max_rtsp_sessions {
    my $max_rtsp_sessions = shift;

    $log->info("Setup MAX_RTSP_SESSIONS for node");
    my $ok = 1;
    eval {

	die "Value not a number\n" if not defined $max_rtsp_sessions or $max_rtsp_sessions !~ /^\d+$/;

	unless ($dbh) {
	    $dbh = DBNode({'PrintError'=>0,'RaiseError'=>1}) or die("Failed to connect to node DB: $DBI::errstr");
	}
	my $rows = $dbh->selectrow_arrayref(
	    "SELECT start_script FROM processes where procname='mediaServer'"
	);
	my $start_script = $rows->[0];
	my $curval;
	my @parts = split(/\s/,$start_script);
	my $bin = shift @parts;
	for (my $i = 0; $i < @parts; $i++) {
	    if ($parts[$i] eq '-s' and $#parts > $i) {
		$curval = $parts[$i+1];
		$parts[$i+1] = $max_rtsp_sessions;
		last;
	    }
	}
	unshift @parts, "-s", $max_rtsp_sessions unless defined $curval;
	unshift @parts, $bin;
	if (not defined $curval or $curval ne $max_rtsp_sessions) {
	    $log->info(
		"Maximum concurrent session per node changed: " .
		($curval ? $curval : "default") . " => " . $max_rtsp_sessions .
		". Have to restart mediaServer with new argument"
	    );
	    $start_script = join(" ", @parts);
	    $dbh->do(
		"UPDATE processes SET start_script=? WHERE procname='mediaServer'",
		undef, $start_script
	    );
	    sleep 2; # Wait until procctl updates its cache
	    # Find mediaServer processes and terminate them
	    system("/bin/ps ax | grep $MSERVER | grep -v grep | cut -c 1-6 | xargs kill -TERM &>/dev/null");
	}
    };
    if ($@) {
	chomp $@;
	$log->error("Error processing MAX_RTSP_SESSIONS parameter change: $@");
	$ok = 0;
    }

    return $ok;
}

#----------------------------- amq_notify -----------------------------------------
# Send notification of object attribtues changes to AMQ
#
sub amq_notify {
    my ($objid, $conf, $attr, $topic) = @_;
    
    my %msg;
    my %diff;
    my $body;
    $topic = "obj_attr_change" if not defined $topic;
    if ($attr and ref $attr eq 'ARRAY') {
	foreach my $attr (@$attr) {
    	    $diff{$attr} = $conf->{$attr};
	}
    }
    else {
	%diff = %$conf;
    }
    $msg{$objid} = \%diff;
    $body = encode_json(\%msg);
    eval {
        $Stomp->send(
	    destination => "/topic/$topic",
    	    body => $body,
    	    'content-length' => ""
	);
        $Stomp->send(
	    destination => "/topic/$topic.$objid",
    	    body => $body,
    	    'content-length' => ""
	);
    };
    if ($@) {
	$Stomp = undef;
	chomp $@;
	$log->warn("Error sending AMQ notification: $@");
    }
}

#----------------------------- delegate_to_patrol -----------------------------------
# 'Light' version of 'replicate_device' routine.
# Delegates device processing to cam_patrol, simply putting file named '$dev.conf'
# to the staging area
#
sub delegate_to_patrol {
    my $rhDev = shift;
    my $dev = $rhDev->{DEVID};
    my $ok;
    # workaround for DEMO cameras (validate function in GUI forces PROTO=HTTP)
    $rhDev->{PROTO}='FAKE' if $rhDev->{CAMERAMODEL} eq 'DEMO';

    # Read device conf from flat file and compare with the one from db
    my $is_new_device = -d "$CONFDIR/$dev" ? 0 : 1;
    #return if $rhDev->{DELETED} and $is_new_device; # Skip completely deleted devices
    my %devcfg = GetCfgs((DEVID=>$dev));
    my @diff;
    my $diff_result = hash_diff($rhDev, $devcfg{$dev}, \@diff);
    handle_cmd_reset($rhDev->{OBJ}) if $rhDev->{CMD_RESET} eq '1';
    return unless $diff_result;

    if(!$is_new_device) {
	if ($rhDev->{DELETED}) {
	    $log->info("ObjId $rhDev->{OBJ}: device was deleted");
	}
	else {
	    $log->info("ObjId $rhDev->{OBJ} => Found non-null diff: @diff");
	}
    } else {
	$log->info("ObjId $rhDev->{OBJ} => This is a new device");
    }    

    # Handle special situation: camera model change
    my $re = join('|',@diff);
    if (!$is_new_device and $rhDev->{DEVICETYPE} eq 'CAMERA' and 'CAMERAMODEL|MODELID' =~ /$re/) {
	on_model_change($rhDev);
    }

    # Put device configuration into staging area
    # and let cam_patrol decide how to deal with it
    unlink "$STAGING_AREA/$dev.conf.tmp" if -f  "$CONFDIR/$dev.conf.tmp";
    $ok = open(CF,">$STAGING_AREA/$dev.conf.tmp");
    unless($ok) {
	$log->error("Cannot open $STAGING_AREA/$dev.conf.tmp for writing\n");
	return;
    }
    local $" = '|';
    print CF "$_=$rhDev->{$_}\n" foreach(grep {!/^(@EXCL_ATTR)$/} sort keys %$rhDev);
    close CF;
    unlink "$STAGING_AREA/$dev.conf" if -f  "$STAGING_AREA/$dev.conf";
    rename "$STAGING_AREA/$dev.conf.tmp", "$STAGING_AREA/$dev.conf";
    
    # Send notification to ActiveMQ
    if ($AMQNotify and $Stomp) {
	amq_notify($rhDev->{OBJ}, $rhDev, \@diff);
    }
}

#----------------------------- handle_avatar ---- -----------------------------------
# Replicates avatar data to node configuration files
#
sub handle_avatar {
    my $rhDev = shift;
    my $objid = $rhDev->{OBJ};

    # create dedicated area for avatar devices if doesn't exist
    mkdir "$CONFDIR/av" if ! -d "$CONFDIR/av";

    my $avdir = "$CONFDIR/av/$objid";
    # If devices is moved to garbage, delete corresponding dir
    if ($rhDev->{DELETED}) {
        $rhDev->{LOCATION} = '@garbage';
    }

    # Read device conf from flat file and compare with the one from db
    my $is_new_device = -d $avdir ? 0 : 1;

    my %avcfg;
    if (! $is_new_device && open(CFG, "$avdir/conf")) {
    	%avcfg = map {/(\w+)=(.*)/} grep {/^\w+=.*$/} <CFG>;
	close(CFG);
    }

    my @diff;
    my $diff_result = hash_diff($rhDev, \%avcfg, \@diff);
    return unless $diff_result;

    if(!$is_new_device) {
	$log->info("AVATAR ObjId $rhDev->{OBJ} => Found non-null diff: @diff");
    } else {
	$log->info("AVATAR ObjId $rhDev->{OBJ} => This is a new device");
    }

    # Put avatar configuration into DEFAULT TMP CONF dir: $CONFDIR/conf
    unlink "$STAGING_AREA/$objid.conf.tmp" if -f "$STAGING_AREA/$objid.conf.tmp";
    unless(open(CF,">$STAGING_AREA/$objid.conf.tmp")) {
	$log->error("Cannot open $avdir/conf.tmp for writing\n");
	return;
    }
    local $" = '|';
    print CF "$_=$rhDev->{$_}\n" foreach(grep {!/^(@EXCL_ATTR)$/} sort keys %$rhDev);
    close CF;
    rename "$STAGING_AREA/$objid.conf.tmp", "$STAGING_AREA/$objid.conf";
    
    # Send notification to ActiveMQ
    if ($AMQNotify and $Stomp) {
	amq_notify($rhDev->{OBJ}, $rhDev, \@diff);
    }
}

# ---------------------------- handle_cmd_reset -------------------------------------
# Initiate camera reset command
# Create special trigger file that will be processed by cam_patrol
#
sub handle_cmd_reset {
    my $obj = shift;

    $log->info("Initiate reset request for camera $obj");
    my $reqfile = "$CONFDIR/$obj/cmd_reset";

    if (-f $reqfile) {
	$log->info("Request file already exists");
    }
    else {
	if (open(FH, ">$reqfile")) {
	    print FH "START\n";
	    close FH;
	}
	else {
	    $log->error("Cannot write to file $reqfile: $!");
	}
    }

    eval {
	$dbs{CMD_RESET_ACK}->execute($obj);
    };
    $log->error("Error when updating CMD_RESET attribute: $@") if $@;
}

# ---------------------------- handle_node_change -----------------------------------
# Handle special case when object migrates to another node
# update special fields in _objs table so that db2conf on that node recognizes and
# processes migrated object
#
sub handle_node_change {
    my $objid = shift;
    my $rhDev = shift;
    my $nodeid = $rhDev->{NODEID};
    eval {
        $dbm->{AutoCommit} = 0;
        $dbs{VALIDATE_NODEID}->execute($nodeid);
        my $uni;
        my $ra = $dbs{VALIDATE_NODEID}->fetchall_arrayref;
        if (not @$ra) {
            $log->error("Node with OBJID=$nodeid doesn't exist! Reset device's NODEID");
            $nodeid = $NodeOBJ;
            $uni = $UNI;
            $dbs{UPDATE_ATTR}->execute($nodeid, 'NODEID', $objid);
        } else {
            $uni = $ra->[0][0];
        }
        # update object's nodeid and node uni
        $dbs{UPDATE_NODEID}->execute($nodeid, $uni, $objid);
        # If camera has audio, migrate it also
        if ($rhDev->{AUDIO_DEVID}) {
            my $audio_obj = get_existen_audio_objid($rhDev->{AUDIO_DEVID});
            if ($audio_obj) {
                $dbs{UPDATE_NODEID}->execute($nodeid, $uni, $audio_obj);
            }
        }
        $dbm->commit;
        $dbm->{AutoCommit} = 1;
    };
    if ($@) {
        $dbm->{AutoCommit} = 1;
        $log->error("Node change failed for device $rhDev->{OBJID} failed: $@");
        eval { $dbm->rollback };
    }
}

# ---------------------------- hash_diff --------------------------------------------
# Calculates a difference between 2 hashes. Return value is 0 if hashes are equal and
# 1 otherwise. Resulting array of hash keys is put into 3rd argument which should
# be an array ref
#
sub hash_diff {
    my ($h1, $h2, $raDiff, $raExcl) = @_;
    $h1 ||= {}; $h2 ||= {};
    local $" = '|';
    @$raDiff = grep { not exists $h1->{$_} or $h1->{$_} ne $h2->{$_} }
	grep {not /^(@EXCL_ATTR)$/ and not /^STAT_/ } keys %$h2;
    if ($raExcl and @$raExcl) {
        @$raDiff = grep { not /^(@$raExcl)$/ } @$raDiff;
    }
    push @$raDiff, grep { not exists $h2->{$_} and not /^(@EXCL_ATTR)$/ and not /^STAT_/} keys %$h1;
    return @$raDiff ? 1 : 0;
}

sub trim {
    my @args = @_;
    s/^\s+//g foreach @args;
    s/\s+$//g foreach @args;
    return wantarray ? @args : $args[0];
}

sub update_rtime {
    my $obj = $_[0] + 0;
    my $cfg = $_[1];
    eval {{
	# Check if stime was changed since last db query
	if ($cfg and exists $cfg->{STIME}) {
	    $dbs{GET_OBJ_STIME}->execute($obj);
	    my $ra = $dbs{GET_OBJ_STIME}->fetchrow_arrayref;
	    my $new_stime = $ra->[0];
	    if ($new_stime ne $cfg->{STIME}) {
		# Concurrent change detected
		# Skip updating RTIME for this object
		# Will do this on next iteration
		$log->warn("Concurrent change detected for ObjId=$obj. Leave it for further processing");
		last;
	    }
	}
	$dbs{UPDATE_RTIME}->execute($obj);
	$log->debug("Sucessfully updated RTIME for device: OBJID=$obj");
    }};
    $log->error("Cannot update rtime for object $obj: $@") if $@;
}

sub manage_downtime {

    # TODO: If first start - close all open periods
    my %dt = ();
    
    eval {
        $dbm->{AutoCommit} = 0;
        
        # First close all open periods for cameras that are now OK or deleted
        $dbs{FETCH_OPEN_PERIODS}->execute($NodeOBJ);
        my $ra = $dbs{FETCH_OPEN_PERIODS}->fetchall_arrayref;
        foreach my $p (@$ra) {
            my ($objid, $utc_from, $status) = @$p;
            $dt{$objid} = $status;
            if (not exists $Stat{$objid} or $Stat{$objid} eq 'ON') {
                $dbs{CLOSE_PERIOD}->execute($objid);
                print "close period for $objid; stat=$Stat{$objid}\n";
            }
        }
        
        # Insert new record for camera if not streaming
        foreach my $objid (keys %Stat) {
            my $prev_dt_status = $dt{$objid};
            my $status = $Stat{$objid};
            next if $status eq 'ON';
            
            # Define downtime status for camera according to its actual status
            my $dt_status;
            if ($status eq 'OFF') {
                # If got camera configuration in cache, check if it is controlled by scheduler
                if ($Conf{$objid} and exists $Conf{$objid}{SCHEDULEID} and not $Conf{$objid}{SCHEDULEID}) {
                    $dt_status = 1;
                }
                else {
                    $dbs{SCHEDULED_CAM}->execute($objid);
                    my $ra = $dbs{SCHEDULED_CAM}->fetchrow_arrayref;
                    # If camera's ARCHSTATE is controlled by scheduler the result of query will be 'on' or 'off'
                    # Otherwise result will be empty
                    if ($ra and @$ra and $ra->[0] and $ra->[0] =~ /^(on|off)/) {
                        $dt_status = 2;
                    }
                    else {
                        $dt_status = 1;
                    }
                }
            }
            else {
                $dt_status = 3;
            }
            
            # If open period for camera doesn't exist, open new period
            # Else if period exists and status differs, first close old period and then open new one
            # Otherwise do nothing
            if (defined $prev_dt_status) {
                if ($prev_dt_status eq $dt_status) {
                    next;
                }
                else {
                    print "Close old period for $objid (prev_status=$prev_dt_status)\n";
                    $dbs{CLOSE_PERIOD}->execute($objid);
                }
            }
            $dbs{OPEN_PERIOD}->execute($objid, $dt_status);
            print "Open period for $objid; status=$dt_status\n";
        }
        
        $dbm->commit;
        $dbm->{AutoCommit} = 1;
    };
    if ($@) {
        my $err = $@;
        $log->error("Camera downtime  manage failed: $err");
        warn "Camera downtime manage failed: $err\n";
        eval { $dbm->rollback };
        die "DB_MASTER: $err";
    }
}

#----------------------------- MAIN -------------------------------------------------
sub main {

    warn("Concurrent run detected\n"),_exit(1) if CheckPid;
    WritePid;

    db_master;
    amq_init(1) if $AMQNotify;

    # First replicate license files for the case of backup recovery
    replicate_license;
    replicate_vca_license;
    check_cloud_storage;

    while(1) {
	eval {
	    db_master if !$dbm || $Hangup;
	    amq_init if $AMQNotify && !$Stomp;
	    scan_node_obj;
	    scan_db_objs;
	    scan_identity;
	    scan_node_obj;
	    scan_stat;                    # check device statistics if updated
	    sleep $MAIN_LOOP_SLEEP;
	};
	if($@) {
	    if($@ =~ /SIGINT|SIGTERM/) {
		$log->logdie("TERMINATED: $@");
	    } elsif ($@ =~ /SQLERR/) {
		$log->error("SQL Error: $@");
		eval { $dbm->disconnect; };
		undef $dbm;
	    } elsif($@ =~ /DB_MASTER/) {
		$log->logdie("Master database connection error: $@");
	    } else {
		$log->logdie("UNKNOWN ERROR: $@");
	    }
	}
        my $loadavg=load_avg();
        my $extrasleep=0;
        $extrasleep=4  if $loadavg>50;
        $extrasleep=7  if $loadavg>200;
        $extrasleep=10 if $loadavg>300;
        if($extrasleep) {
           $log->info("Extrasleep=$extrasleep since loadavg:$loadavg");
           sleep $extrasleep;
        }
	$FirstLoop = 0;
	$Hangup = 0;
    }
}

END {
    eval { $dbm->disconnect } if $dbm;
    eval { $dbh->disconnect } if $dbh;
    RemovePid;
}
