├── src ├── suncron.sh ├── suncron.conf ├── lib │ ├── ConfigFile.pm │ ├── CalcTime.pm │ └── Astro │ │ └── Sunrise.pm └── suncron.pl ├── Makefile ├── README.md └── LICENSE /src/suncron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Generate a cron file based on when sun rises or sets 4 | SUNCRON_PATH/bin/suncron 5 | 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INSTALL_DIR ?= /usr 2 | 3 | .PHONY: default install uninstall 4 | 5 | default: 6 | @echo "make install|uninstall" 7 | 8 | install: 9 | @echo "Installing..." 10 | @install -d ${INSTALL_DIR}/bin 11 | @install -d ${INSTALL_DIR}/share/suncron/lib/Astro 12 | @install -d /etc/default 13 | @install -m755 src/suncron.pl ${INSTALL_DIR}/bin/suncron 14 | @sed < src/suncron.sh >/tmp/suncron.sh -e "s#SUNCRON_PATH#${INSTALL_DIR}#" 15 | @install -m755 /tmp/suncron.sh /etc/cron.daily/suncron 16 | @rm -f /tmp/suncron.sh 17 | @install -m644 src/suncron.conf /etc/default/suncron 18 | @install -m644 README.md ${INSTALL_DIR}/share/suncron 19 | @install -m644 LICENSE ${INSTALL_DIR}/share/suncron 20 | @install -m755 src/lib/CalcTime.pm ${INSTALL_DIR}/share/suncron/lib 21 | @install -m755 src/lib/ConfigFile.pm ${INSTALL_DIR}/share/suncron/lib 22 | @install -m755 src/lib/Astro/Sunrise.pm ${INSTALL_DIR}/share/suncron/lib/Astro 23 | @echo "Done." 24 | @echo "A sample configuration file has been copied to '/etc/default/suncron'." 25 | @echo "Existing file has a '~' appended." 26 | @echo "You need to change the coordinates and time zone information in order to get" 27 | @echo "correct calculation for sunset and sunrise. The configuration file also contain" 28 | @echo "two sample entries showing the format of rules." 29 | @echo "A cron script has been added to /etc/cron.daily so that new entris are made" 30 | @echo "every day. Verify the daily cron jobs are run at about midnight." 31 | 32 | uninstall: 33 | @echo "Uninstalling..." 34 | @rm -f /etc/cron.daily/suncron 35 | @rm -f /etc/cron.d/suncron 36 | @rm -f ${INSTALL_DIR}/bin/suncron 37 | @rm -f /etc/default/suncron 38 | @rm -Rf ${INSTALL_DIR}/share/suncron 39 | @echo "Done" 40 | 41 | -------------------------------------------------------------------------------- /src/suncron.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Sample configuration file for SunCron 3 | # 4 | # This file is expected to be located in /etc/default unless you use the 5 | # command line option to override this. 6 | # 7 | 8 | # 9 | # Location and time zone informatino is needed to calculate a correct sunrise 10 | # and sunset. 11 | # The coordinates should be decimal degrees. 12 | # Valid timezones are found in /usr/share/zoneinfo 13 | # 14 | [Location] 15 | Longitude=17.3 16 | Latitude=59.05 17 | Timezone=Europe/Stockholm 18 | 19 | 20 | # The following section lists the rules that are transformed into cron entries. 21 | # 22 | # Each rules has the following format: 23 | # condition;true;false;cron part 24 | # - condition is a formula like 'sunrise - 01:00 < 07:00' 25 | # - true is the time if condition was true 26 | # - false is the time if condition was false 27 | # - cron part is a crontab line not including minute and hour field (the first 28 | # two fields). 29 | # 30 | # True and false can be empty. 31 | # True and false may be a simple calculation (like 'sunrise - 01:00') 32 | # Keep formulas simple in both conditon and true/false 33 | # 34 | [Rules] 35 | # Below are two samples, showing the rules and the resulting entry 36 | # 37 | # conditon: sun rises after 07:00 38 | # if true: execute at 06:15 39 | # if false: do nothing 40 | # cron: every day, run 'echo TEST>/dev/null' as user 'root' 41 | # This is what the resulting entry would look like: 42 | sunrise > 07:00 ; 06:15 ; ; * * * root echo TEST>/dev/null 43 | 44 | # 45 | # condition sun sets before 18:00 46 | # if true: execute at 17:00, 47 | # if false: execute an hour before sunset 48 | # cron: every saturday and sunday, run 'ls >/dev/null /root' as user root 49 | # This is what the resulting entry would look like: 50 | sunset < 18:00 ; 17:00 ; sunset - 01:00 ; * * 6-7 root ls >/dev/null /root 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SunCron 2 | Generate cron entries from rules based on sunset and/or sunrise 3 | 4 | SunCron is a perl program that will generate cron rules based on the time when sun sets and/or rises. I made it to control the lighting in my apartment together with the nexa remote control I made. You can however run any command that doesn’t require user input. 5 | 6 | ## Requirements: 7 | - Perl 8 | - Astro::Sunrise module (included in this package) 9 | - DateTime module (available from CPAN) 10 | 11 | ## Installation: 12 | - Run 'make install' as root. 13 | - Edit /etc/default/suncron 14 | - Verify that daily cron jobs execute sometime between 00:00 and 01:00 or 15 | you'll risk missing the sunset. In this case you'll be a day late, that 16 | is, the sunrise and sunset times will be for previous day. 17 | 18 | ## Usage: 19 | The idea behind this project is to execute programs at sunset or sunrise. 20 | Instead of running an entire program to wait for sunset or sunrise, this 21 | program is supposed to execute at midnight and calculate todays sunrise and 22 | sunset times. It will then parse a configuration file with a set of rules 23 | and update the cron file with the new values. 24 | 25 | The configuration file is found in '/etc/default/suncron.conf'. 26 | It has a Location section and a rule section. Each rule consists of four 27 | parts: 28 | - a condition
29 | The condition is a clear text formula describing the condition, 30 | eg. 'sunset < 17:00' to test if sun sets before 17:00. 31 | 32 | - a "true" and a "false" statement
33 | these statements is the time to use if condition is true/false, 34 | eg. 'sunrise + 03:30' for three and a half hour after sunrise. 35 | Or you can leave it empty to ignore it. 36 | 37 | - a cron part
38 | the cron part is a cron line without the minute and hour field, 39 | eg. '* * [1-4] root /path/to/command arg' 40 | 41 | The resulting cron data is written to /etc/cron.d/suncron (unless you 42 | redirect it with the command line switch, see 'suncron --help'). 43 | Conditions that evaluate to a true/false state that is empty will NOT 44 | be written to the file. 45 | 46 | ## Known issues: 47 | The program makes no attempt at parsing the cron rule, so rules that 48 | have a cron part that is to execute at, say, every thursday will be 49 | written every day. This does not mean it will be executed every day, 50 | since cron will handle this correctly. It's just that suncron makes 51 | an unnecessary line in the cron file. I'm lazy, so you'll have to 52 | accept this ;-) 53 | -------------------------------------------------------------------------------- /src/lib/ConfigFile.pm: -------------------------------------------------------------------------------- 1 | package ConfigFile; 2 | 3 | # This file is part of SunCron. 4 | # 5 | # SunCron is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU Gerenral Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # SunCron is distributed in the hope that it will be usefull, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with SunCron; if not, write to the Free Software 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | # 19 | # Copyright: (C) 2009-2010 Johan Stenarson 20 | 21 | use strict; 22 | use warnings; 23 | 24 | sub new { 25 | my ($package, $file) = @_; 26 | my ($latitude, $longitude, $timezone); 27 | my $section = "none"; 28 | my @rules = (); 29 | 30 | die('Error: Need a filename to configuration file') if not defined($file); 31 | 32 | my $conf; 33 | my $result = open($conf, "<", $file); 34 | 35 | if($result) { 36 | while(<$conf>) { 37 | if(!/^\s*#/ && !/^\s*$/) { 38 | if(/^\s*\[(\w*)\]\s*/) { 39 | $section = lc $1; 40 | } else { 41 | if($section eq "location") { 42 | (my $key, my $value) = split(/=/, $_, 2); 43 | $value =~ s/^\s*//; 44 | $value =~ s/\s*$//; 45 | $key =~ s/^\s*//; 46 | $key =~ s/\s*$//; 47 | if(lc $key eq "longitude") { 48 | $longitude = $value; 49 | } elsif(lc $key eq "latitude") { 50 | $latitude = $value; 51 | } elsif(lc $key eq "timezone") { 52 | $timezone = $value; 53 | } 54 | } elsif (lc $section eq "rules") { 55 | # do the magix 56 | s/^\s*//; 57 | s/\s*$//; 58 | push @rules, ($_); 59 | } else { 60 | die("Unknown section: $section\n"); 61 | } 62 | } 63 | } 64 | } 65 | } else { 66 | die("Couldn't open configuration file: '$file'"); 67 | } 68 | 69 | my $self = { _file => $file, _latitude => $latitude, _longitude => $longitude, _rules => \@rules, _timezone => $timezone }; 70 | 71 | bless($self, $package); 72 | 73 | return $self; 74 | } 75 | 76 | sub latitude { 77 | my $self = shift; 78 | 79 | return $self->{_latitude}; 80 | } 81 | 82 | sub longitude { 83 | my $self = shift; 84 | 85 | return $self->{_longitude}; 86 | } 87 | 88 | sub timezone { 89 | my $self = shift; 90 | 91 | return $self->{_timezone}; 92 | } 93 | 94 | sub get_rules { 95 | my $self = shift; 96 | 97 | return $self->{_rules}; 98 | } 99 | 100 | 1; 101 | __END__ 102 | -------------------------------------------------------------------------------- /src/suncron.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | # This file is part of SunCron. 4 | # 5 | # SunCron is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Gerenral Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # SunCron is distributed in the hope that it will be usefull, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with SunCron; if not, write to the Free Software 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | # 19 | # Copyright: (C) 2009-2010 Johan Stenarson 20 | 21 | # 22 | # Are we installed? 23 | # 24 | use lib (-e '/usr/share/suncron/' ? '/usr/share/suncron/lib' : $ENV{PWD}.'/lib'); 25 | 26 | use strict; 27 | use warnings; 28 | 29 | use Data::Dumper; 30 | use Getopt::Long; 31 | use Pod::Usage; 32 | 33 | # Internal modules 34 | use ConfigFile; 35 | use CalcTime; 36 | 37 | my $sun_never; 38 | local $SIG{__WARN__} = sub { 39 | # Catch warning from Astro if sun never sets or rises 40 | if($_[0] =~ /Sun never (\w*)!*/) { 41 | $sun_never = $1; 42 | } else { 43 | warn $_[0]; 44 | } 45 | }; 46 | 47 | my $NAME = 'SunCron'; 48 | my $VERSION = '0.1'; 49 | 50 | my $config_file = '/etc/default/suncron'; 51 | my $cron_file = '/etc/cron.d/suncron'; 52 | 53 | # Parse command line arguments 54 | my ($version, $help, $man, $init, $verbose); 55 | my $result = GetOptions( 56 | 'conf=s' => \$config_file, 57 | 'cron=s' => \$cron_file, 58 | 'help' => \$help, 59 | 'man' => \$man, 60 | 'verbose' => \$verbose, 61 | 'version' => \$version, 62 | ); 63 | 64 | pod2usage(1) if $help; 65 | pod2usage(-exitstatus => 0, -verbose => 2) if $man; 66 | if($version || $verbose) { 67 | print <new($config_file); 78 | 79 | print "Creating time expression parser...\n" if $verbose; 80 | my $parser = CalcTime->new( longitude => $config->longitude, latitude => $config->latitude, timezone => $config->timezone ); 81 | 82 | open(my $cron, ">", $cron_file); 83 | 84 | print $cron "#\n# WARNING! This file is automatically generated, changes will be lost\n#\n"; 85 | 86 | my @rules = @{$config->get_rules()} or die("No rules defined in configuration\n"); 87 | for my $rule (@rules) { 88 | print "Parsing rule: $rule\n" if $verbose; 89 | 90 | my @parts = split(/;/, $rule); 91 | my $time; 92 | 93 | # Parse condition 94 | my $result = $parser->evaluate($parts[0]); 95 | 96 | last if defined $sun_never; 97 | 98 | if($result) { 99 | $time = $parser->evaluate($parts[1]); 100 | } else { 101 | $time = $parser->evaluate($parts[2]); 102 | } 103 | if(defined $time) { 104 | print $cron $time->minute()." ".$time->hour()." ".$parts[3]."\n"; 105 | } 106 | } 107 | 108 | print $cron "# File is empty because sun never $sun_never\n" if defined $sun_never; 109 | 110 | close $cron; 111 | 112 | print "Done!\n" if $verbose; 113 | 114 | exit; 115 | 116 | =head1 NAME 117 | 118 | SunCron - Tell cron to run commands at times based on sunset and sunrise 119 | 120 | =head1 SYNOPSIS 121 | 122 | suncron [options...] 123 | 124 | Options: 125 | --conf=FILE filename of configuration file 126 | --cron=FILE filename of cron file 127 | --help show brief help text then exit 128 | --man show full documentation then exit 129 | --verbose show verbose information 130 | --version output version information and exit 131 | 132 | =head1 OPTIONS 133 | 134 | =over 135 | 136 | =item B<--conf> 137 | 138 | Full path to configuration file. Default is F. 139 | 140 | =item B<--cron> 141 | 142 | Where to write cron file. Default is F. 143 | 144 | =item B<--help> 145 | 146 | Show a brief help text and exit. 147 | 148 | =item B<--man> 149 | 150 | Show documentation and exit. 151 | 152 | =item B<--verbose> 153 | 154 | Show information about what program is doing. 155 | 156 | =item B<--version> 157 | 158 | Show version number and exit. 159 | 160 | =back 161 | 162 | =head1 DESCRIPTION 163 | 164 | This program is expected to be run daily at midnight and will then calculate 165 | the sunrise and sunset. This information is used when evaluating rules in the 166 | configuration file and the result will be used to generate cron entries in a 167 | cron file. In other words, with this program you'll be able to tell cron to 168 | run commands at sunrise or sunset. 169 | 170 | The configuration file is found in '/etc/default/suncron.conf'. 171 | It has a B section and a B section. Each rule consists of four 172 | parts: 173 | 174 | =over 175 | 176 | =item B 177 | 178 | The condition is a clear text formula describing the condition, eg. 'sunset < 17:00' to test if sun sets before 17:00. 179 | 180 | =item B and B 181 | 182 | These statements is the time to use if condition is true/false, eg. 'sunrise + 03:30' for three and a half hour after sunrise. 183 | 184 | =item B 185 | 186 | The cron part is a cron line without the minute and hour field, eg. '* * [1-4] root /path/to/command arg'. The resulting cron data is written to /etc/cron.d/suncron (unless you redirect it with the --cron switch). 187 | 188 | =head1 REQUIREMENTS 189 | 190 | This project depend on the following software: 191 | 192 | =over 193 | 194 | =item B 195 | 196 | Perl extension for computing the sunrise/sunset on a given day 197 | 198 | =item B 199 | 200 | A date and time object 201 | 202 | =back 203 | 204 | =head1 AUTHOR 205 | 206 | Johan Stenarson Ejohan.stenarson@gmail.comE 207 | 208 | =head1 BUGS 209 | 210 | Probably, but none that I know of. 211 | 212 | =head1 COPYRIGHT 213 | 214 | Copyright (C) 2009-2010 Johan Stenarson. All rights reserved. 215 | This program is free software; you can redistribute it and/or modify it. 216 | 217 | The full text of the license can be found in the copyright file included with this package. 218 | 219 | =cut 220 | 221 | -------------------------------------------------------------------------------- /src/lib/CalcTime.pm: -------------------------------------------------------------------------------- 1 | package CalcTime; 2 | 3 | # This file is part of SunCron. 4 | # 5 | # SunCron is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU Gerenral Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # SunCron is distributed in the hope that it will be usefull, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with SunCron; if not, write to the Free Software 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | # 19 | # Copyright: (C) 2009-2010 Johan Stenarson 20 | 21 | use warnings; 22 | use strict; 23 | 24 | use DateTime; 25 | use Astro::Sunrise; 26 | 27 | our $VERSION = '0.1'; 28 | 29 | # 30 | # The following variables are allowed in an expression 31 | # 32 | my %params = ( 33 | 'time' => 1, # Replaced with current time 34 | 'sunset' => 1, # Replaced with time of sunset 35 | 'sunrise' => 1 # Replaced with time of sunrise 36 | ); 37 | 38 | # 39 | # The following operators are allowed in an expression 40 | # The higher value, the later the expression is evaluated. 41 | # 42 | my %operators = ( 43 | '-' => 1, # Substract two operands 44 | '+' => 2, # Add two operands 45 | '<' => 3, # Test left side is less than right side 46 | '>' => 4, # Test left side is less than right side 47 | 'or' => 5, # One or both must be true 48 | 'and' => 6, # Both peft and right must be true 49 | ); 50 | 51 | 52 | 53 | =head1 USAGE 54 | 55 | =over 56 | 57 | =item B 58 | 59 | =over 60 | 61 | =item Cnew();> 62 | =item Cnew( time =E DateTime-Enow(), longitude =E 12.345, latitude =E 34.567 );> 63 | 64 | =back 65 | 66 | Create a new CalcTime object. The time parameter will override the default value, which is 67 | to use the current time at object creation. Longitude and latitude values need to be set 68 | if sunrise or sunset are used in the expressions. 69 | 70 | =cut 71 | 72 | sub new { 73 | my ($class, %args) = @_; 74 | 75 | my $timezone = $args{'timezone'} || 'UTC'; 76 | my $time = $args{'time'} || DateTime->now(time_zone => $timezone); 77 | my $latitude = $args{'latitude'} || 0.0; 78 | my $longitude = $args{'longitude'} || 0.0; 79 | 80 | my $self = { _time => $time, _latitude => $latitude, _longitude => $longitude }; 81 | 82 | return bless($self, $class); 83 | } 84 | 85 | =item B 86 | 87 | =over 88 | 89 | =item C<$parser-Eset_datetime(DateTime-Enow());> 90 | 91 | =back 92 | 93 | This will set the time to use instead of current time, which is default if no 94 | time is given. 95 | 96 | =cut 97 | 98 | sub set_datetime ($) { 99 | my ($self, $time) = @_; 100 | 101 | $self->{'_time'} = $time; 102 | } 103 | 104 | =item B 105 | 106 | =over 107 | 108 | =item C<$parser-Eset_coordinate( 12.345, 23.456 );> 109 | 110 | =back 111 | 112 | This will set the coordinates used to calculate sunset and sunrise. 113 | First parameter is longitude and second parameter is latitude. 114 | Both are in decimal degrees 115 | 116 | =cut 117 | 118 | sub set_coordinate ($$) { 119 | my ($self, $longitude, $latitude) = @_; 120 | 121 | $self->{'_longitude'} = $longitude; 122 | $self->{'_latitude'} = $latitude; 123 | } 124 | 125 | =item B 126 | 127 | =over 128 | 129 | =item C<$parser-Eevaluate("time E 07:00 and time E sunrise");> 130 | 131 | =back 132 | 133 | Test if time expression is true and if so, return the final DateTime 134 | object. 135 | 136 | =cut 137 | 138 | sub evaluate { 139 | my ($self, $expression) = @_; 140 | my $result = 1; 141 | 142 | my @parts = $self->_prepare($expression); 143 | 144 | $result = $self->_calculate(@parts); 145 | 146 | if(ref($result) eq 'DateTime::Duration') { 147 | my $tmp = $self->{_time}->clone(); 148 | $tmp->set_hour($result->hours); 149 | $tmp->set_minute($result->minutes); 150 | $result = $tmp; 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | =item B 157 | 158 | =over 159 | 160 | =item C<$parser-Ecrosses_horizon();> 161 | 162 | =back 163 | 164 | Test if sun goes up and down (not midnight sun or polar night) 165 | 166 | =cut 167 | 168 | sub crosses_horizon { 169 | my $self = shift; 170 | 171 | if(not defined $self->{_sunset}) { 172 | $self->_calc_sun(); 173 | } 174 | 175 | return (($self->{_sunset}->hours != $self->{_sunrise}->hours) and ($self->{_sunset}->minutes != $self->{_sunrise}->minutes)); 176 | } 177 | 178 | # 179 | # Convert string format to an array of elements. The order will be a postfix 180 | # order for easier calculation. 181 | # Each element will be tested if it is an allowed operation or variable. 182 | # 183 | sub _prepare { 184 | my ($self, $rule) = @_; 185 | my @result = (); 186 | my @stack = (); 187 | 188 | # Insert spaces for easier split 189 | $rule =~ s// \> /g; 191 | $rule =~ s/\+/ + /g; 192 | $rule =~ s/-/ - /g; 193 | $rule =~ s/and/ and /g; 194 | $rule =~ s/or/ or /g; 195 | $rule =~ s/ [ ]+/ /g; 196 | $rule =~ s/^ *//; 197 | $rule =~ s/ *$//; 198 | 199 | my @parts = split(/ /, $rule); 200 | 201 | foreach my $item (@parts) { 202 | if( ($item =~ m/[\d]{1,2}:[\d]{1,2}/) or $params{$item} ) { 203 | push( @result, $item ); 204 | } else { 205 | OPERATOR: while(@stack) { 206 | die("Syntax error: Unknown expression '$item'\n") if not defined($operators{$item}); 207 | if($operators{$item} > $operators{$stack[0]}) { 208 | push(@result, pop(@stack)); 209 | } else { 210 | last OPERATOR; 211 | } 212 | } 213 | push(@stack, $item); 214 | } 215 | } 216 | 217 | while(@stack) { 218 | push(@result, pop(@stack)); 219 | } 220 | 221 | return @result; 222 | } 223 | 224 | # 225 | # Parse an element. 226 | # 227 | # Elements can be a variable (time, sunrise or sunset) or a time (HH:MM). 228 | # 229 | sub _parse_parameter { 230 | my ($self, $parameter) = @_; 231 | my $result; 232 | 233 | if($parameter eq "time") { 234 | $result = $self->{_time}->clone(); 235 | } elsif($parameter eq "sunset") { 236 | if(not defined $self->{_sunset}) { 237 | $self->_calc_sun(); 238 | } 239 | $result = $self->{_sunset}->clone(); 240 | } elsif($parameter eq "sunrise") { 241 | if(not defined $self->{_sunrise}) { 242 | $self->_calc_sun(); 243 | } 244 | $result = $self->{_sunrise}->clone(); 245 | } elsif($parameter =~ m/[\d]{1,2}:[\d]{1,2}/) { 246 | # Try parsing it as a HH:MM value 247 | my @items = split(/:/, $parameter); 248 | $result = DateTime::Duration->new( hours => $items[0], minutes => $items[1] ); 249 | } 250 | 251 | return $result; 252 | } 253 | 254 | sub _calc_sun { 255 | my $self = shift; 256 | 257 | # 258 | # NOTE! Daylight Saving parameter is set to 0, since offset is adjusted 259 | # accordingly from DateTime object, that is DST is included in offset. 260 | # 261 | my ($sunrise, $sunset) = sunrise($self->{_time}->year, $self->{_time}->month, $self->{_time}->day, 262 | $self->{_longitude}, $self->{_latitude}, $self->{_time}->offset / 3600, 0); 263 | 264 | $self->{_sunrise} = $self->_parse_parameter($sunrise); 265 | $self->{_sunset} = $self->_parse_parameter($sunset); 266 | } 267 | 268 | # 269 | # Calculate the postfix expression 270 | # 271 | sub _calculate { 272 | my ($self, @expr) = @_; 273 | my @stack = (); 274 | 275 | foreach my $item (@expr) { 276 | if( ($item =~ m/[\d]{1,2}:[\d]{1,2}/) or $params{$item} ) { 277 | push(@stack, $self->_parse_parameter($item)); 278 | } else { 279 | my $rhs = pop(@stack); 280 | my $lhs = pop(@stack); 281 | push(@stack, $self->_do_operator($item, $lhs, $rhs)); 282 | } 283 | } 284 | 285 | return pop(@stack); 286 | } 287 | 288 | sub _do_operator { 289 | my ($self, $operator, $lhs, $rhs) = @_; 290 | my $result; 291 | 292 | if($operator eq "+") { 293 | $lhs->add_duration( $rhs ); 294 | $result = $lhs; 295 | } elsif( $operator eq "-") { 296 | $lhs->subtract_duration( $rhs ); 297 | $result = $lhs; 298 | } elsif( $operator eq ">") { 299 | # We can't compare a real time with a duration. Treat duration as a 300 | # real time. 301 | if($lhs->isa('DateTime::Duration')) { 302 | my $tmp = $self->{_time}->clone()->set( hour => $lhs->hours, minute => $lhs->minutes ); 303 | $lhs = $tmp; 304 | } 305 | if($rhs->isa('DateTime::Duration')) { 306 | my $tmp = $self->{_time}->clone()->set( hour => $rhs->hours, minute => $rhs->minutes ); 307 | $rhs = $tmp; 308 | } 309 | $result = DateTime->compare( $lhs, $rhs ) > 0; 310 | } elsif( $operator eq "<") { 311 | if($lhs->isa('DateTime::Duration')) { 312 | my $tmp = $self->{_time}->clone()->set( hour => $lhs->hours, minute => $lhs->minutes ); 313 | $lhs = $tmp; 314 | } 315 | if($rhs->isa('DateTime::Duration')) { 316 | my $tmp = $self->{_time}->clone()->set( hour => $rhs->hours, minute => $rhs->minutes ); 317 | $rhs = $tmp; 318 | } 319 | $result = DateTime->compare( $lhs, $rhs ) < 0; 320 | } elsif( $operator eq "or") { 321 | $result = ( $lhs or $rhs ); 322 | } elsif( $operator eq "and") { 323 | $result = ( $lhs and $rhs ); 324 | } else { 325 | die("Syntax error: Unknown operator '$operator'\n"); 326 | } 327 | 328 | #$result = 0 if not defined $result; 329 | 330 | #print _debug_item($lhs) . " $operator " . _debug_item($rhs) . " = " . _debug_item($result) . "\n"; 331 | 332 | return $result; 333 | } 334 | 335 | sub _debug_item { 336 | my $item = shift; 337 | 338 | if(ref($item) eq 'DateTime::Duration') { 339 | return $item->hours . ":" . $item->minutes; 340 | } 341 | 342 | return $item; 343 | } 344 | 345 | __END__ 346 | =head1 NAME 347 | 348 | CalcTime - An object that compute simple time expressions 349 | 350 | =head1 SYNOPSIS 351 | 352 | use CalcTime; 353 | 354 | $parser = CalcTime->new(); 355 | $parser = CalcTime->new( 356 | time => DateTime->new( month => 1, day => 1 ), 357 | longitude => 17.345, 358 | latitude => 53.123 359 | ); 360 | 361 | $parser->set_datetime( DateTime->new( year => 1971 ); 362 | $parser->set_coordinate( 12.345, 45.678 ); 363 | 364 | $result = $parser->evaluate("time > 06:15 and sunrise > time"); 365 | 366 | =head1 DESCRIPTION 367 | 368 | This module will parse a clear text time expression and evaluate if it is 369 | true or not. 370 | 371 | Expressions are written in clear text and may contain operators, variables and 372 | time expressions. 373 | 374 | =head2 Operators 375 | 376 | Valid operators are: 377 | 378 | =over 379 | 380 | =item B<+> 381 | 382 | Add two times 383 | 384 | =item B<-> 385 | 386 | Subtracts a time from another 387 | 388 | =item B> 389 | 390 | Verify left time expression is less than right expression. 391 | 392 | =item B> 393 | 394 | Verify left time expression is greater than right expression. 395 | 396 | =item B 397 | 398 | Left and right expression must be true 399 | 400 | =item B 401 | 402 | Left, right or both expressions must be true. 403 | 404 | =back 405 | 406 | =head2 Variables 407 | 408 | Valid variables are: 409 | 410 | =over 411 | 412 | =item B