#! /usr/bin/perl -CS

use strict;
use utf8;
use Time::HiRes ('gettimeofday');
use IO::Socket::INET;
use IO::File;
use DBI;
use POSIX qw(strftime);
use IPC::Shareable;
use Sys::Syslog qw(:standard :macros);

my $RELOAD_REQUEST = 0;

# shared memeory
my $glue = 'zpwr';
my %options = (
    create    => 'yes',
    exclusive => 0,
    mode      => 0666,
    destroy   => 'yes',
);

my %cur_pzem_data;
tie %cur_pzem_data, 'IPC::Shareable', $glue, { %options } or
    die "powerlogger: tie failed\n";

my( $socket, $client_socket);
openlog('[pwrloggerd]', 'ndelay', 'local0');


$SIG{__WARN__}  = sub{ syslog('warning', shift)};
$SIG{__DIE__}   = sub{ syslog('err', shift) };
$SIG{HUP}		= sub{$RELOAD_REQUEST++};
$SIG{ALRM}      = sub { die("sigalrm!\n"); };
$SIG{TERM}      = sub { flush_db_buffer(); die("terminated\n"); };

open(STDIN, '</dev/null');
open(STDOUT, '>/dev/null');
open(STDERR, '>&STDOUT');

my $PIDFILE 	= '/tmp/pwrloggerd.pid';
chdir '/myscripts';

our $TIME_DIFF_THRESHOLD_MS = 300; 			# maximum allowed difference between local time and time from nodeMCU
our $TIME_DIFF_THRESHOLD_CRITRICAL = 500;	# should be corrected immediately
our $TIME_DIFF_THRESHOLD_PASSED_N = 0;		# how many times difference threshold was passed consecutevely 
our $TIME_DIFF_MAX_CNT = 100;				# how many time threshold should be passed to issue command for time correction

our @REC_BUFFER = ();
our $MAX_BUFFER_SZ = 60; # hold 300 records so 5 - 8 min if we get one record in 1-1.5sec



my $pidfh = IO::File->new($PIDFILE, O_WRONLY|O_CREAT|O_EXCL, 0644);
if($pidfh){
	print $pidfh $$;
	close $pidfh;
}else{
	alert_sysadmin("first open of the pidfile failed $!");
	open my $pidfile, '<', $PIDFILE or die "unable to open existing pid file for reading";
	my $pid = <$pidfile>;
	if( $pid =~ /^\d+$/){
		if(kill(0, $pid)){# process exists
			alert_sysadmin("daemon process already exists"), die;
		}else{# process doesn't exists
			create_pid_file();
		} 
	}else{# file corrupted
		create_pid_file();
	}
}

my $dbh = DBI->connect('DBI:mysql:powerlog:localhost', 'powerd', 'password', { RaiseError => 1, AutoCommit => 0, mysql_enable_utf8 =>1 });
die("unable to connect to DB powerlog\n") unless $dbh;



# creating a listening socket for nodemcu reports
 $socket = new IO::Socket::INET (
    LocalHost   => '192.168.1.10',
    LocalPort   => '40000',
    Proto       => 'tcp',
    Type     	=> SOCK_STREAM,
    Listen      => 5,
    Reuse       => 1,
    Blocking    => 1,
    Autoflush   => 1
) or die "cannot create nodemcu socket $!\n";

# get the latest virtual power counter from database

my($virtual_wh_cntr, $previous_internal_cntr) = $dbh->selectrow_array("SELECT pwr_energy, pwr_energy_orig FROM powerdata ORDER by pwr_timestamp DESC LIMIT 1");


alert_sysadmin("daemon started with energy counter = $virtual_wh_cntr. server waiting for client connection on port 40000");
syslog('debug', "daemon started with energy counter = $virtual_wh_cntr. server waiting for client connection on port 40000");


while($client_socket = $socket->accept()){
	
	my $data;
	while(1){ # reading multiple lines during one connection
	
		if($RELOAD_REQUEST){
			syslog('warning', 'reload request received');
			shutdown($client_socket, 1);
			close($socket);
			closelog();
			do_reload();
		};

		my ($err, $errmsg) = (0, "");
		alarm 5;
		
        eval{
            #$err = 1, $errmsg = "eof on client socket"         if eof($client_socket);
            $err = 1, $errmsg = "client socket in undefined"   if !$client_socket;
            if( !$err ){
             	$data = readline($client_socket);
            	$err = 1, $errmsg = "undefined data from socket"   if !$@ and !defined($data); # not alarm, but response is undefined
            }
        	alarm 0;
        };
  
        if($@ or $err){
			alarm 0;
            #alert_sysadmin("sock read error: system: $@; local: $errmsg");
            syslog('err', "sock read error: system: $@; local: $errmsg");
			flush_db_buffer();
            last;
        }
		chop $data;

		if( $data =~ /^DEBUG/ ){
			# print debug data somewhere
			syslog('debug', $data);
		}elsif( $data =~ /^POWER/ ){
			my($sec, $usec) = gettimeofday();
			my $epoch_ms 	= int($sec*1000 + $usec/1000);

			my($epoch_sec_part, $epoch_ms_part, $voltage, $power, $energy, $uptime, $pkgs_rcvd) = $data =~ m/^POWER\s*(\d+)\s*(\d+)\s*(\d+\.\d+)\s*(\d+)\s(\d+)\s*(\d+)\s*(\d+)\s*$/;
			

			if(	$epoch_sec_part !~ /\d+/ or
			    $epoch_ms_part !~ /\d+/  or
			    $voltage !~ /\d+\.\d+/   or
			    $power !~ /\d+/          or
			    $energy !~ /\d+/
			){
			  #my $msg = "epoch_sec:$epoch_sec_part; epoch_ms:$epoch_ms_part; voltage: $voltage; power: $power; energy: $energy; uptime: ".  parse_duration($uptime) . "  pkgs_rcvd: $pkgs_rcvd";
			  #alert_sysadmin("incorrect PZEM data: $data");
				syslog('err', "incorrect PZEM data: $data");
				next;
			}
			
			# because of unstable power supply conditions PZEM sometimes writes wrong data into it's memeory
			# so, unfortunately, internal energy counter of PZEM is unreliable.
			# moreover - it can't be corrected, so instead of using internal counter we are forced to use some virtual emergy counter
			# this counter is increased when internal is increased
			# in this case we can easily correct DB in case of internal counter failure and go ahead
			
			my $delta_e = $energy - $previous_internal_cntr;
			$virtual_wh_cntr += abs( $delta_e ); 
			$previous_internal_cntr = $energy;
			
			if( $delta_e < 0 ){
				alert_sysadmin("internal energy counter decreased! $delta_e");
			}

			# dealing with time difference between nodeMCU and server
			my $time_diff = ($epoch_sec_part*1000 + $epoch_ms_part) -  $epoch_ms;
			
			
			if( (abs($time_diff) > $TIME_DIFF_THRESHOLD_MS) and (abs($time_diff) < $TIME_DIFF_THRESHOLD_CRITRICAL)){
				$TIME_DIFF_THRESHOLD_PASSED_N++;
				if( $TIME_DIFF_THRESHOLD_PASSED_N >= $TIME_DIFF_MAX_CNT ){
					my($sec, $usec) = gettimeofday();
					my $epoch_ms 	= int($sec*1000+$usec/1000);
					print $client_socket "ADJUST_TIME $epoch_ms $epoch_ms\n";
					#print $client_socket pack("QQQQ", $epoch_ms, $epoch_ms, $epoch_ms, $epoch_ms);
					#alert_sysadmin("ADJUST_TIME ordinary issued $time_diff\n");
					syslog('warning', "ADJUST_TIME ordinary issued $time_diff");
					$TIME_DIFF_THRESHOLD_PASSED_N = 0;
				}
			}elsif( abs($time_diff) > $TIME_DIFF_THRESHOLD_CRITRICAL ){
				my($sec, $usec) = gettimeofday();
				my $epoch_ms 	= int($sec*1000+$usec/1000);
				print $client_socket "ADJUST_TIME $epoch_ms $epoch_ms\n";
				#print $client_socket pack("QQQQ", $epoch_ms, $epoch_ms, $epoch_ms, $epoch_ms);
				#alert_sysadmin("ADJUST_TIME IMMEDIATE issued $time_diff\n");
				syslog('warning', "ADJUST_TIME IMMEDIATE issued $time_diff");
				$TIME_DIFF_THRESHOLD_PASSED_N = 0;
				next; # we shouldn't save such data
			}else{
				$TIME_DIFF_THRESHOLD_PASSED_N = 0;
			}
			
			                
			%cur_pzem_data = (
				uptime		=>	parse_duration($uptime),
				timediff	=>  $time_diff,
				timestamp   =>  strftime( '%Y-%m-%d %H:%M:%S', localtime($epoch_sec_part)),
				epoch		=>	$epoch_sec_part * 1000 + $epoch_ms_part,
				voltage     =>  $voltage,
				power       =>  $power,
				energy      =>  $virtual_wh_cntr,  #$energy,
				energy_orig	=>	$energy,
			);
			
			if( @REC_BUFFER >= $MAX_BUFFER_SZ ){
				flush_db_buffer();
			}

			# push last data to buffer
			push @REC_BUFFER, {
					"CUR_TIMESTAMP"     => $cur_pzem_data{timestamp},
					"CUR_VOLTAGE"       => $cur_pzem_data{voltage},
					"CUR_POWER"         => $cur_pzem_data{power},
					"CUR_ENERGY"        => $cur_pzem_data{energy},
					"CUR_ENERGY_ORIG"	=> $cur_pzem_data{energy_orig},
			};
												
			#print "\rvoltage: $voltage; power: $power; energy: $energy; uptime: ", parse_duration($uptime) . " timeshift: $time_diff; pkgs_rcvd: $pkgs_rcvd;                   ";
		}else{ # unknown message
		
		}
	}
    alarm 0;

	# somehow we got out of loop. we must reinitialize socket
	shutdown($client_socket, 1);
	#alert_sysadmin('main loop died. server waiting for client connection on port 40000');
	syslog('err', 'main loop died. server waiting for client connection on port 40000' );
}

sub flush_db_buffer{
	my $statement = "INSERT INTO powerdata SET pwr_timestamp = ?, pwr_voltage = ?, pwr_power = ?, pwr_energy = ?, pwr_energy_orig=?";
	my $sth = $dbh->prepare($statement);
	$sth->execute(@{shift(@REC_BUFFER)}{"CUR_TIMESTAMP", "CUR_VOLTAGE", "CUR_POWER", "CUR_ENERGY", "CUR_ENERGY_ORIG"}) while @REC_BUFFER;
	$dbh->commit();
	syslog('debug','BUFFER HAS BEEN FLUSGED');
}

sub do_reload{
	alert_sysadmin("pwrlogger daemon reload requested");
	flush_db_buffer();
	unlink $PIDFILE;
    exec './pwrloggerd.pl';
}

sub alert_sysadmin{
	my $message = shift;
  	
	# you should provide your own method of urgent notification here.
	# Me personally use jabber
	
}

sub create_pid_file{
	unlink $PIDFILE or die "unable to unlink old pidfile";
	my $pidfh = IO::File->new($PIDFILE, O_WRONLY|O_CREAT|O_EXCL, 0644);
	die "unable to create pidfile exlusively" unless $pidfh;
	print $pidfh $$;
	close $pidfh;
	alert_sysadmin("pidfile recreated");
}

sub parse_duration {
    my $seconds = shift;
  
    my $hours = int( $seconds / (60*60) );
    my $days = int($hours/24);
    $hours -= $days * 24; 
    my $mins = ( $seconds / 60 ) % 60;
    my $secs = $seconds % 60;
    return sprintf("%dd:%02d:%02d:%02d", $days,$hours,$mins,$secs);
}

