#!/usr/bin/perl

use POSIX;
use DateTime;
use DateTime::Event::Sunrise;
use Time::Local;
use strict;
use sigtrap 'handler', \&daemon_exit, 'normal-signals';

# The following variables should be adapted to your geographical location and system setup
my $LATITUDE = "65.603";    # 65.603N 22.18W -> Luleå, Sweden
my $LONGITUDE = "22.18";
my $TIMEZONE = "Europe/Stockholm";
my $RFCMD = "/usr/local/bin/rfcmd";
my $TELLSTICK_DEVICE = "/dev/tellstick";
my $CONFIG_FILE = "/etc/tellstickd.conf";
my $LOG_FILE = "/var/log/tellstickd";
my $pidFile = '/var/run/tellstickd.pid';


# You should not need to go beyond this point (unless you have found a bug or need to improve the functionality).
my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
my @weekDays = qw(Sun Mon Tue Wed Thu Fri Sat Sun);
my $NO = 0;
my $YES = 1;
my @device_cfg;
my $conf_file = "";
my $make_daemon = $NO;
my $simulate = $NO;
my $n_rfcmd_calls;
my $current_time;
my $number_of_rules;
my $Day;
my $Month;
my $Year;
my $Minute;
my $Hour;
my $Second;
my $WeekDay;
my $DayOfYear;
my $IsDST;
my $daylightSavings;
my $dayOfYear;
my $dayOfWeek;
my $dayOfMonth;

my $PROGRAM_NAME = "tellstickd";
my $VERSION = "0.6.3";

# Structure of the configurations to be read;
# $device_cfg[$i][0];   # Protocol, only NEXA, WAVEMAN and SARTANO supported so far by rfcmd
# $device_cfg[$i][1];   # Housecode A-P
# $device_cfg[$i][2];   # Channel 1-3
# $device_cfg[$i][3];   # On time
# $device_cfg[$i][4];   # Off time
# $device_cfg[$i][5];   # Off when bright, 0 = no, 1 = yes
# $device_cfg[$i][6];   # Off when bright delay
# $device_cfg[$i][7];   # Time in advance when getting dark
# $device_cfg[$i][8];   # On time random interval
# $device_cfg[$i][9];   # Off time random interval
# $device_cfg[$i][10];  # Rule active these days [11234567], 1=monday, ... 7=sunday.
# $device_cfg[$i][20];  # Sunrise off time, calculated in this program and stored here
# $device_cfg[$i][21];  # Sunset on time, calculated in this program and stored here
# $device_cfg[$i][22];  # Switch state, 0 = off, 1 = on
# $device_cfg[$i][23];  # Original on time
# $device_cfg[$i][24];  # Original off time

sub daemonize {
    chdir '/'                 or die "$PROGRAM_NAME: Can't chdir to /: $!";
    open STDIN, '/dev/null'   or die "$PROGRAM_NAME: Can't read /dev/null: $!";
#open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
#open STDERR, '>/dev/null' or die "$PROGRAM_NAME: Can't write to /dev/null: $!";
    defined(my $pid = fork)   or die "$PROGRAM_NAME: Can't fork: $!";
    exit if $pid;
    setsid                    or die "$PROGRAM_NAME: Can't start a new session: $!";
    umask 0;
}
sub daemon_exit() {
    # Remove pidfile
    unlink $pidFile;
    # Since tellstickd most probably is stuck sleeping just kill ourself
    kill('TERM', $$);
    die;
}
sub get_sunrise_time
{
    my $sunrise_time;

    ($Second, $Minute, $Hour, $Day, $Month, $Year, $WeekDay, $DayOfYear, $IsDST) = localtime(time);
    $Year += 1900;

    # Note, localtime returns months 0..11, Datetime->new expects months 1..12.
    my $dt = DateTime->new( year => $Year, month => $Month+1, day => $Day, time_zone => $TIMEZONE, );
    my $sunrise = DateTime::Event::Sunrise ->new( longitude => $LONGITUDE, latitude => $LATITUDE, altitude => '-0.833', iteration => '1');

    my $dt1 = $sunrise->sunrise_datetime($dt);

    ($sunrise_time) = $dt1->datetime =~ /T(.*)\:/;

    return $sunrise_time
}

sub get_sunset_time
{
    my $sunset_time;

    ($Second, $Minute, $Hour, $Day, $Month, $Year, $WeekDay, $DayOfYear, $IsDST) = localtime(time);
    $Year += 1900;

    # Note, localtime returns months 0..11, Datetime->new expects months 1..12.
    my $dt = DateTime->new( year => $Year, month => $Month+1, day => $Day, time_zone => $TIMEZONE, );
    my $sunrise = DateTime::Event::Sunrise ->new( longitude => $LONGITUDE, latitude => $LATITUDE, altitude => '-0.833', iteration => '1');

    my $dt2 = $sunrise->sunset_datetime($dt);

    ($sunset_time) = $dt2->datetime =~ /T(.*)\:/;

    return $sunset_time;
}

# Checks if $current_time is inbetween argument 1 ($time1) and argument 2 ($time2)
sub is_inbetween_times
{
    my $time1 = $_[0];
    my $time2 = $_[1];
    my $curr_time = $_[2];

    $curr_time =~ s/://g;
    $time1 =~ s/://g;
    $time2 =~ s/://g;
    $curr_time =~ s/^0*//;
    $time1 =~ s/^0*//;
    $time2 =~ s/^0*//;

    # If we pass midnight, the following has to be checked/done
    if ($time2 <= $time1) {
	if ($curr_time >= $time1 && $curr_time < 2400 || $curr_time >= 0000 && $curr_time < $time2) {
	    return $YES;
	}
    }
    # The following applies to the most common rules within a day.
    else {
	if ($curr_time >= $time1 && $curr_time < $time2) {
	    return $YES;
	}
    }

    return $NO;
}



sub rfcmd_exec {
    my $device_id = $_[0];
    my $action = $_[1];
    my $RFCMD_OPTIONS;

    if ($device_cfg[$device_id][0] eq "SARTANO") {
	$RFCMD_OPTIONS = "$TELLSTICK_DEVICE $device_cfg[$device_id][0] $device_cfg[$device_id][1]";
    }
    elsif ($device_cfg[$device_id][0] eq "NEXA" || $device_cfg[$device_id][0] eq "WAVEMAN") {
	$RFCMD_OPTIONS = "$TELLSTICK_DEVICE $device_cfg[$device_id][0] $device_cfg[$device_id][1] $device_cfg[$device_id][2]";
    }
    else {
	die("$PROGRAM_NAME: Device $device_id has an unknown protocol. Only NEXA, WAVEMAN and SARTANO allowed.\n");
    }


    # Action = 1 means turn device on, 0 turn device off
    if ($action == 1) {
	# Only turn the device on if it is not already turned on to avoid flashing dimmers.
	# and check that it is not disabled by on/off time set to -1 in the config file
	if ($device_cfg[$device_id][22] == 0  && $device_cfg[$device_id][23] != -1) {
	    printf("$PROGRAM_NAME: Time is $current_time. Switching on $device_cfg[$device_id][0] device $device_cfg[$device_id][1]$device_cfg[$device_id][2].\n");
	    if ($simulate == $NO) {
		`$RFCMD $RFCMD_OPTIONS 1`;
	    }
	    $device_cfg[$device_id][22] = 1;

	    return;
	}
	else {
	    return;
	}
    }
    elsif ($action == 0) {
	if ($device_cfg[$device_id][22] == 1  && $device_cfg[$device_id][24] != -1) {
	    printf("$PROGRAM_NAME: Time is $current_time. Switching off $device_cfg[$device_id][0] device $device_cfg[$device_id][1]$device_cfg[$device_id][2].\n");
	    if ($simulate == $NO) {
		`$RFCMD $RFCMD_OPTIONS 0`;
	    }
	    $device_cfg[$device_id][22] = 0;

	    return;
	}
	else {
	    return;
	}
    }

    return;
}


# Time format xx:yy for input parameters argument1 has to be positive, argument2 can be positive or negative
sub add_time
{
    my $time1 = $_[0];
    my $time2 = $_[1];
    my $multiplier = 1;
    my $hour;
    my $min;
    my $time1_hour;
    my $time1_minute;
    my $time2_hour;
    my $time2_minute;
    my $time1_epoch_seconds;
    my $time2_offset_seconds;

    ($Second, $Minute, $Hour, $Day, $Month, $Year, $WeekDay, $DayOfYear, $IsDST) = localtime(time);
    $Year += 1900;

    (my $sign) =  $time2 =~ /^(.*?)[0-9]/;
    ($time1_hour, $time1_minute) = $time1 =~ /(.*)\:(.*)/;
    # Remove any leading signs
    $time2 =~ s/^-?//g;
    ($time2_hour, $time2_minute) = $time2 =~ /(.*)\:(.*)/;

    if ($sign eq "-") {
	$multiplier = -1;
    }

    $time1_epoch_seconds = timelocal(0,$time1_minute,$time1_hour, $Day, $Month, $Year);
    $time2_offset_seconds = 3600*$time2_hour + 60*$time2_minute;

    my $result_seconds = $time1_epoch_seconds + $multiplier*$time2_offset_seconds;
  
    ($min,$hour)= (localtime($result_seconds))[1,2];

    return sprintf("%02d:%02d", $hour, $min);
}

# Time format xx:yy for input parameters
sub subtract_time
{
    my $time1 = $_[0];
    my $time2 = $_[1];
    my $multiplier = 1;
    my $hour;
    my $min;
    my $time1_hour;
    my $time1_minute;
    my $time2_hour;
    my $time2_minute;
    my $time1_epoch_seconds;
    my $time2_offset_seconds;

    ($Second, $Minute, $Hour, $Day, $Month, $Year, $WeekDay, $DayOfYear, $IsDST) = localtime(time);
    $Year += 1900;

    (my $sign) =  $time2 =~ /^(.*?)[0-9]/;
    ($time1_hour, $time1_minute) = $time1 =~ /(.*)\:(.*)/;
    # Remove any leading signs
    $time2 =~ s/^-?//g;
    ($time2_hour, $time2_minute) = $time2 =~ /(.*)\:(.*)/;

    if ($sign eq "-") {
	$multiplier = -1;
    }

    $time1_epoch_seconds = timelocal(0,$time1_minute,$time1_hour, $Day, $Month, $Year);
    $time2_offset_seconds = 3600*$time2_hour + 60*$time2_minute;

    my $result_seconds = $time1_epoch_seconds - $multiplier*$time2_offset_seconds;
  
    ($min,$hour)= (localtime($result_seconds))[1,2];

    return sprintf("%02d:%02d", $hour, $min);
}

sub randomize_on
{
    my $device_id = $_[0];

    # If the on- and off times are the same, just return
    if ($device_cfg[$device_id][23] eq $device_cfg[$device_id][24]) {
	return;
    }

    (my $random_hour, my $random_minute) = $device_cfg[$device_id][8] =~ /(.*)\:(.*)/;

    my $rand_res = int(rand(60 * $random_hour + $random_minute));

    my $rand_res_hour = floor($rand_res / 60);
    my $rand_res_minute = $rand_res - 60*$rand_res_hour;

    # Here we have the random offset in format xx:yy
    my $random_time = sprintf("%02d:%02d", $rand_res_hour, $rand_res_minute);

    $device_cfg[$device_id][3] = add_time($device_cfg[$device_id][23], $random_time); 

    if (is_inbetween_times($device_cfg[$device_id][23], $device_cfg[$device_id][24], $device_cfg[$device_id][3]) == $NO) {
	randomize_on($device_id);
    } 

    return;
}

sub randomize_off
{
    my $device_id = $_[0];

    # If the on- and off times are the same, just return
    if ($device_cfg[$device_id][23] eq $device_cfg[$device_id][24]) {
	return;
    }

    (my $random_hour, my $random_minute) = $device_cfg[$device_id][9] =~ /(.*)\:(.*)/;

    my $rand_res = int(rand(60 * $random_hour + $random_minute));

    my $rand_res_hour = floor($rand_res / 60);
    my $rand_res_minute = $rand_res - 60*$rand_res_hour;

    # Here we have the random offset in format xx:yy
    my $random_time = sprintf("%02d:%02d", $rand_res_hour, $rand_res_minute);

    $device_cfg[$device_id][4] = add_time($device_cfg[$device_id][24], $random_time); 

    if (is_inbetween_times($device_cfg[$device_id][23], $device_cfg[$device_id][24], $device_cfg[$device_id][4]) == $YES) {
	randomize_off($device_id);
    } 

    return;
}


# Time format sanity check routine
sub is_time_format_correct
{
     my $time = $_[0];
     (my $hour, my $minute) = $time =~ /^-?(\d\d)\:(\d\d)$/;

     if ($hour && $minute) {
         if ($hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59) {
	     return $YES;
	 }   
     }

     return $NO;
}


# Time format sanity check routine
sub is_rule_valid_today
{
    my $device_id = $_[0];
    my $now = $_[1];
    my $rule = $device_cfg[$device_id][10];

    (my $week_rule) = $rule =~/^([e|o])/;
    if ($week_rule) {
	my $is_odd_week = ($now->week_number % 2);
	if (($is_odd_week && ($week_rule eq "e"))  ||
	    (!$is_odd_week && ($week_rule eq "o"))) {
	    return $NO;
	}
    }
    my $day_of_week = $now->day_of_week;
    if ($rule =~ /$day_of_week/) {
	return $YES;
    }
    
    return $NO;
}

# Ugly hack, but we have to get the log_file option from the config file
# before parsing the config file
sub get_config_log_file_param
{
    my $input_file = $_[0];
    open(CFG_FILE, "<$input_file") or die "$PROGRAM_NAME: Could not access config file: $conf_file\n";
    
    while ($_ = <CFG_FILE>) {
	chomp($_);
	(my $value) = $_ =~ /^log_file\s*=\s*([a-zA-Z0-9_\/\.]*)/;
	if ($value) {
	    close(CFG_FILE);
	    return $value;
	}
    }
    close(CFG_FILE);
    return $NO;
}

sub read_config
{
    my $input_file = $_[0];
    my @inrad;
    my $on_interval_end;
    my $off_interval_end;

    printf("$PROGRAM_NAME: Reading configuration file $conf_file...\n");

    open(CFG_FILE, "<$input_file") or die "$PROGRAM_NAME: Could not access config file: $conf_file\n";

    my $i = 0;
    my $conf_row = 0;
    while ($_ = <CFG_FILE>) {
	$conf_row++;
	if ($_ =~ /^[\#|\s|\n]/) {
	    # skip lines beginning with # or space or just a new line
	    next;
	}


	if ($_ =~ /^[NEXA|WAVEMAN|SARTANO]/ ) {
	    chomp($_);

	    @inrad = split /\s+/, $_;
	    $device_cfg[$i][0] = $inrad[0];            # Protocol
	    $device_cfg[$i][1] = $inrad[1];            # Housecode A and channel for NEXA, WAVEMAN, channel for SARTANO.
	    $device_cfg[$i][10] = $inrad[2];           # Rule valid days [[oe]1234567]
	    $device_cfg[$i][22] = 1;                   # Initial state set to on (1) so that they will be switched off at startup


	    if ($device_cfg[$i][0] eq "SARTANO") {
		$device_cfg[$i][2] = "";
	    }
	    elsif ($device_cfg[$i][0] eq "NEXA" || $device_cfg[$i][0] eq "WAVEMAN") {
		# Put the housecode and channel in the respective fields.
		(my $house_code,my $channel) = $device_cfg[$i][1] =~ /^([A-P])(\d{1,2})$/;
		if (!$house_code || !$channel || ($channel < 1) || ($channel > 16)) {
		    die("$PROGRAM_NAME: Housecode/channel out of range error on row $conf_row in configuration file.\n");
		}
		$device_cfg[$i][1] = $house_code;
		$device_cfg[$i][2] = $channel;
	    }
	    else {
		die("$PROGRAM_NAME: Unsupported protcol: $device_cfg[$i][0]. Only NEXA, WAVEMAN or SARTANO allowed.\n");
	    }




	    # Lets find the on interval and put it into the proper fields, $device_cfg[$i][3]=On time, $device_cfg[$i][8] = On time random interval
	    
	    if ($inrad[3] =~ /-/) {
		($device_cfg[$i][3], $on_interval_end) = $inrad[3] =~ /^(\d?\d:\d\d)-(\d?\d:\d\d)$/; 
	    }
	    else {
		($device_cfg[$i][3]) = $inrad[3] =~ /^(\d?\d:\d\d)$/; 
		$on_interval_end = $device_cfg[$i][3];
	    }

	    $device_cfg[$i][8] = subtract_time($on_interval_end, $device_cfg[$i][3]);



	    # Off time = $device_cfg[$i][4], Off time random interval = $device_cfg[$i][9]
	    if ($inrad[4] =~ /-/) {
		($device_cfg[$i][4], $off_interval_end) = $inrad[4] =~ /^(\d?\d:\d\d)-(\d?\d:\d\d)$/;
	    }
	    else {
		($device_cfg[$i][4]) = $inrad[4] =~ /^(\d?\d:\d\d)$/; 
		$off_interval_end = $device_cfg[$i][4];
	    }

	    $device_cfg[$i][9] = subtract_time($off_interval_end, $device_cfg[$i][4]);



	    # Save the original on and off
	    $device_cfg[$i][23] = $device_cfg[$i][3];  # Original on time is stored here pos 3 will be used for random on times
	    $device_cfg[$i][24] = $device_cfg[$i][4];  # Original off time is stored here pos 4 will be used for random off times





	    # $inrad[5] is the off post sunrise value including sign, $inrad[6] is the on pre sunrise value sign, 
	    if (length($inrad[5]) > 0 && length($inrad[6]) > 0) {
		# Found a parameter after off interval, assuming daytime switch off is sought.
		$device_cfg[$i][5] = 1;


		# First the off post sunrise
		(my $sign, my $minutes) = $inrad[5] =~ /^([-\+]?)(.*?)$/;
		
		if ($sign eq "+") {
		    $sign = "";
		}

		(my $hours) = floor($minutes / 60);
		$minutes = $minutes - $hours * 60;

		$device_cfg[$i][6] = sprintf("%s%02d:%02d", $sign, $hours, $minutes);

		# Then figure on pre sunset out
		(my $sign, my $minutes) = $inrad[6] =~ /^([-\+]?)(.*?)$/;
		
		if ($sign eq "+") {
		    $sign = "";
		}

		(my $hours) = floor($minutes / 60);
		$minutes = $minutes - $hours * 60;

		$device_cfg[$i][7] = sprintf("%s%02d:%02d", $sign, $hours, $minutes);
	    }
	    else {
		# If no parameters given after off interval it is assumed that the light will be on daytime
		$device_cfg[$i][5] = 0;
		$device_cfg[$i][6] = "00:00";
		$device_cfg[$i][7] = "00:00";
	    }


	    
	    ##############################################################################
	    # Some sanity checks
	    # If the turn on time is not to be used, this is marked with -1
	    if ($device_cfg[$i][3] != -1) { 
		if (is_time_format_correct($device_cfg[$i][3]) == $NO) {
		    die("$PROGRAM_NAME: Format of turn on time for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] not correct: $device_cfg[$i][3]\n");
		}
	    }
	    else {
		printf("$PROGRAM_NAME: Noted configuration for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] with no turn on time.\n");
	    }

	    # If the turn off time is not to be used, this is marked with -1
	    if ($device_cfg[$i][4] != -1) { 
		if (is_time_format_correct($device_cfg[$i][4]) == $NO) {
		    die("$PROGRAM_NAME: Format of turn off time for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] not correct: $device_cfg[$i][4]\n");
		}
	    }
	    else {
		printf("$PROGRAM_NAME: Noted configuration for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] with no turn off time.\n");
	    }

	    if (is_time_format_correct($device_cfg[$i][6]) == $NO) {
		die("$PROGRAM_NAME: Format of turn off after sunrise for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] not correct: $device_cfg[$i][6]\n");
	    }

	    if (is_time_format_correct($device_cfg[$i][7]) == $NO) {
		die("$PROGRAM_NAME: Format of turn on before sunset for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] not correct. Exiting.\n");
	    }

	    if (is_time_format_correct($device_cfg[$i][8]) == $NO) {
		die("$PROGRAM_NAME: Format of on interval for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] not correct: $device_cfg[$i][8]\n");
	    }

	    if (is_time_format_correct($device_cfg[$i][9]) == $NO) {
		die("$PROGRAM_NAME: Format of off interval for device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] not correct: $device_cfg[$i][9]\n");
	    }

	    if ($device_cfg[$i][10] =~ /^[o|e][^1-7]/) {
		die("$PROGRAM_NAME: Device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2]: Specified rule validity day out of range: $device_cfg[$i][10]\n");
	    }



	    printf("$PROGRAM_NAME: $device_cfg[$i][0] device $device_cfg[$i][1]$device_cfg[$i][2] config:\n");

	    printf("            This rule is valid:        ");
	    for (my $j = 1 ; $j <= 7 ; $j++) {
		if ($device_cfg[$i][10] =~ /$j/) {
		    printf(" $weekDays[$j]");
		}
	    }
	    if ($device_cfg[$i][10] =~ /o/) {
		printf(" (odd weeks)");
	    }
	    if ($device_cfg[$i][10] =~ /e/) {
		printf(" (even weeks)");
	    }
	    printf("\n");

	    if ($device_cfg[$i][3] == -1) {
		printf("            On time:                    N/A\n");
	    }
	    else {
		printf("            On time:                    $device_cfg[$i][3]\n");
	    }

	    if ($device_cfg[$i][4] == -1) {
		printf("            Off time:                   N/A\n");
	    }
	    else {
		printf("            Off time:                   $device_cfg[$i][4]\n");
	    }

	    printf("            On time random interval:    $device_cfg[$i][8]\n");
	    printf("            Off time random interval:   $device_cfg[$i][9]\n");

	    if ($device_cfg[$i][5] == $YES) {
		printf("            Turned off daytime:         yes\n");
		printf("            Off post sunrise:           $device_cfg[$i][6]\n");
		printf("            On pre sunset:              $device_cfg[$i][7]\n");
	    }
	    else {
		printf("            Turned off daytime:         no\n");
	    }

	    $i++;
	    next;
	}
	# Try to get config parameter name and value
	(my $param, my $value) = $_ =~ /\s*(\w+)\s*=\s*(-?[a-zA-Z0-9_\/\.]*)/;
        if ($param && $value) {
	    printf("$PROGRAM_NAME: $param = $value\n");
	    if ($param eq 'latitude') {
		if ($value !~ /^-?[0-9\.]*$/) {
		    die ("$PROGRAM_NAME: Latitude must be a valid number\n");
		}
		if ($value < -90 || $value > 90) {
		    die ("$PROGRAM_NAME: Latitude must be between -90 and 90 degrees\n");
		}
		$LATITUDE = $value;
		next;
	    }
	    if ($param eq 'longitude') {
		if ($value !~ /^-?[0-9\.]*$/) {
		    die ("$PROGRAM_NAME: Longitude must be a valid number\n");
		}
		if ($value < -90 || $value > 90) {
		    die ("$PROGRAM_NAME: Longitude must be between -180 and 180 degrees\n");
		}
		$LONGITUDE = $value;
		next;
	    }
	    if ($param eq 'timezone') {
		if (!DateTime::TimeZone->is_valid_name($value)) {
		    die ("$PROGRAM_NAME: Invalid timezone\n");
		}
		$TIMEZONE = $value;
		next;
	    }
	    if ($param eq 'rfcmd') {
		$RFCMD = $value;
		next;
	    }
	    if ($param eq 'tellstick_device') {
		$TELLSTICK_DEVICE = $value;
		next;
	    }
	    if ($param eq 'log_file') {
		$LOG_FILE = $value;
		next;
	    }
	    if ($param eq 'pid_file') {
		$pidFile = $value;
		next;
	    }
	    die ("$PROGRAM_NAME: Unknown config parameter $param = $value\n");
	}
	else {
	    die("$PROGRAM_NAME: Unknown config file line $_\n");
	}
    }
    close(CFG_FILE);
    return $i;
}

sub usageprompt
{
    printf("This program is intended for control of Tellstick remote devices.\n");
    printf("Version: %s\n", $VERSION);

    printf("\nUsage: %s [options]\n", $PROGRAM_NAME);
    printf("\nOptions:\n");
    printf("-c, --config filename   Searchpath to configuration file, if not given $CONFIG_FILE is assumed.\n");
    printf("-d, --daemon            Run the program as a daemon and write a logfile.\n");
    printf("-h, --help              Show this message and exit.\n");
    printf("--simulate              Run in foreground, log to console, no actual device operations performed.\n");
}


# ############## Main section ######################
my $i = -1;

while ($i < $#ARGV) {
    if ($i+1 <= $#ARGV) {
	if ($ARGV[$i+1] =~ /^--config$/ || $ARGV[$i+1] =~ /^-c$/) {
	    $i++;
	    $conf_file = $ARGV[$i+1];
	}
	elsif ($ARGV[$i+1] =~ /^-d$/ || $ARGV[$i+1] =~ /^--daemon$/) {
	    $make_daemon = $YES;
	}
	elsif ($ARGV[$i+1] =~ /^-h$/ || $ARGV[$i+1] =~ /^--help$/) {
	    usageprompt();
	    exit;
	}
	elsif ($ARGV[$i+1] =~ /--simulate/) {
	    $simulate = $YES;
	}
	else {
	    die("$PROGRAM_NAME: Unknown input argument: $ARGV[$i+1]\n");
	}
    }

    $i++;
}

if ($#ARGV < 0) {
    die "Try \"$PROGRAM_NAME --help\" for more information.\n";
}


if (length($conf_file) < 1) {
    $conf_file = $CONFIG_FILE;
}
$LOG_FILE = get_config_log_file_param($conf_file);

if ($LOG_FILE && $make_daemon && !$simulate) {
    open STDOUT, "> $LOG_FILE" or die "$PROGRAM_NAME: Can't write to $LOG_FILE: $!";
    open STDERR, ">> $LOG_FILE" or die "$PROGRAM_NAME: Can't write to $LOG_FILE: $!";
}
if (!$LOG_FILE && $make_daemon) {
# start a daemon without a log file
    open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
    open STDERR, '>/dev/null' or die "$PROGRAM_NAME: Can't write to /dev/null: $!";
}
# Read the configuration file
$number_of_rules = read_config($conf_file);

if ($number_of_rules < 1) {
    die("$PROGRAM_NAME: Configuration file has no devices to control, exiting.\n");
}
else {
    printf("\n$PROGRAM_NAME: $number_of_rules rule(s) has been read.\n\n");
}

if ($make_daemon == $YES && $simulate == $NO) {
    &daemonize();
    open PIDFILE, ">$pidFile" or die "$PROGRAM_NAME: Can't open $pidFile: $!\n";
    print PIDFILE $$ . "\n";
    close PIDFILE;
}


printf("$PROGRAM_NAME: Starting $PROGRAM_NAME version $VERSION\n");


###################################################################################################
# Now to the eternal loop
my $first_loop = $YES;
my $now;
while (1) {
    $n_rfcmd_calls = 0;

    # What is the current time
    ($Second, $Minute, $Hour, $Day, $Month, $Year, $WeekDay, $DayOfYear, $IsDST) = localtime();
    $Year += 1900;
    $current_time = sprintf("%02d:%02d", $Hour, $Minute);
    $now =  DateTime->now( time_zone => $TIMEZONE);

    ###################################################################################################

    # First, lets set the sunrise and sunset times for the devices that it applies to, once a day, at 00:00.
    if ($first_loop == $YES || $current_time eq "00:00") {
	my $sunrise_time = get_sunrise_time();
	printf("$PROGRAM_NAME: Recalculating sunrise/sunset. Current date: %d-%02d-%02d\n", $Year, $Month + 1, $Day);
	printf("$PROGRAM_NAME: Time is $current_time. Sunrise today is expected at $sunrise_time\n");

	my $sunset_time = get_sunset_time();
	printf("$PROGRAM_NAME: Time is $current_time. Sunset today is expected at $sunset_time\n\n");

	for (my $i = 0; $i < $number_of_rules; $i++) {
	    # Initially all devices are switched off to get a known state
	    if ($first_loop == $YES) {
		rfcmd_exec($i, 0);
	    }

	    # Lets set the actual on- and offtimes depending on random interval settings.
	    randomize_on($i);
	    if (is_rule_valid_today($i, $now) == $YES) {
		printf("$PROGRAM_NAME: Today device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] will be turned on $device_cfg[$i][3]\n");
	    }
	    randomize_off($i);
	    if (is_rule_valid_today($i, $now) == $YES) {
		printf("$PROGRAM_NAME: Today device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] will be turned off $device_cfg[$i][4]\n");
	    }

	    # If the device is supposed to be off daytime, the following applies
	    # Sunrise time + requested offset
	    if ($device_cfg[$i][5] == $YES) {
		$device_cfg[$i][20] = add_time($sunrise_time, $device_cfg[$i][6]);
		if (is_rule_valid_today($i, $now) == $YES) {
		    printf("$PROGRAM_NAME: Device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] sunrise off time set to $device_cfg[$i][20].\n");
		}
		# Sunset time - requested offset
		$device_cfg[$i][21] = subtract_time($sunset_time, $device_cfg[$i][7]);
		if (is_rule_valid_today($i, $now) == $YES) {
		    printf("$PROGRAM_NAME: Device $device_cfg[$i][0] $device_cfg[$i][1]$device_cfg[$i][2] sunset on time set to $device_cfg[$i][21].\n");
		}
	    }
	}
    }


    ###################################################################################################

    # Below the required actions are performed for each device
    for ($i = 0; $i < $number_of_rules; $i++) {


	# Lets check if the rule is to be applied today
	if (is_rule_valid_today($i, $now) == $YES) {



	    # First if we consider daytime off
	    if ($device_cfg[$i][5] == $YES) {
		if (is_inbetween_times($device_cfg[$i][3], $device_cfg[$i][4], $current_time) == $YES && is_inbetween_times($device_cfg[$i][20], $device_cfg[$i][21], $current_time) == $NO) {
		    rfcmd_exec($i, 1);
		    $n_rfcmd_calls++;
		}
		else {
		    rfcmd_exec($i, 0);
		    $n_rfcmd_calls++;
		}
	    }


	    # Then if daytime off is not considered
	    else {
		if (is_inbetween_times($device_cfg[$i][3], $device_cfg[$i][4], $current_time) == $YES) {
		    rfcmd_exec($i, 1);
		    $n_rfcmd_calls++;
		}
		else {
		    rfcmd_exec($i, 0);
		    $n_rfcmd_calls++;
		}
	    }
    }



    }

    $first_loop = $NO;

    if ($n_rfcmd_calls < 30) {
	sleep(60 - 2*$n_rfcmd_calls);   # Wait a while until next round [seconds]. If rfcmd has been called, reduce this figure by 2 seconds per call.
    }
}
