#!/usr/bin/perl

# By using internal and external temperature sensors, as well as two
# fans that are connected to X10 units (one set up as intake, taking
# air from the outside and bringing it in; one as exhaust, blowing
# the room's air outside), an ideal temperature can attempt to be met.

use strict;

##################### config stuff ###################################
# temp. stuff
my $inside_temp  = "ibutton:/var/tmp/temp,810008002E44C010";
my $outside_temp = "wmweather:/home/steve/.weather.txt";
#my $ideal_temp   = 'constant:23';
my $ideal_temp   = "schedule:default=22;2:00-7:00=20;-11:00=23;-18:00=22;-2:00=23";

# update interval
my $interval     = 3 * 60;

# ± middle is a lower power
my $middle       = 0.5; #°C

# X10 stuff
my $x10_command   = 'x10_rpc turn %UNITS% %ACTION%';
my $intake   = new Fan('e4', 'intake');
my $exhaust  = new Fan('e3', 'exhaust');

# temperature log
my $logfile = "/home/steve/temperature.log";
my $write_log = 1;

###################### end config ##########################

my %fan_state = ();

while( 1 ){

  my $inside = get_temp( $inside_temp );

  my $outside = get_temp( $outside_temp );

  my $ideal = get_temp( $ideal_temp );

  print "inside: \t$inside°C\n";
  print "outside:\t$outside°C\n";
  print "ideal:  \t$ideal°C\n";
  print '-' x 40, "\n";

  write_log( $inside, $outside ) if $write_log;

  my $exhaust_on = 0;
  my $intake_on  = 0;

  # inside is colder than ideal, and warmer outside
  if( $inside < $ideal && $outside > $inside ){
    $intake_on = 1;
  }

  # inside is warmer than ideal, and warmer outside
  if( $inside > $ideal && $outside > $inside ){
    $exhaust_on = 1;
  }

  # inside is warmer than ideal, and colder outside
  if( $inside > $ideal && $outside < $inside ){
    $intake_on  = 1;
    $exhaust_on = 1;
  }

  # if we're near the ideal temp, use less cooling
  if( ($inside - $ideal ) > 0 
      && ( $inside - $ideal ) < $middle 
      && $outside < $inside ){
      $intake_on = 0;
      $exhaust_on = 1;
  }


  if( $intake_on ){
      $intake->on;
  }else{
      $intake->off;
  }

  if( $exhaust_on ){
      $exhaust->on;
  }else{
      $exhaust->off;
  }

#   send_x10( $intake_units, ( $intake_on ? "on": "off" ) );
#   send_x10( $exhaust_units, ( $exhaust_on ? "on": "off" ) );

  sleep $interval;
}

sub write_log{
    my( $inside, $outside ) = @_;

    open LOG, ">>$logfile" or die "cannot open log file for writing: $!\n";
    
    print LOG time(), ", $inside, $outside\n";

    close LOG;
}

sub get_temp{

  my( $string ) = @_;

  my( $type, $param ) = $string =~ /^(\w+?):(.+)$/;

  my( @params ) = split /,/, $param;

  if( $type eq 'ibutton' ){
    return read_ibutton( @params );

  }elsif( $type eq 'wmweather' ){
    return read_wmweather( @params );

  }elsif( $type eq 'constant' ){
    return $param;

  }elsif( $type eq 'schedule' ){
    return process_temp_schedule(@params);

  }else{

    die "unsupported type, $type\n";
  }

}

sub read_ibutton{
  my( $pipe, $id ) = @_;

  open IB, "<$pipe" or die "cannot read from named pipe, '$pipe'\n";

  my %temps = ();
  while( my $line = <IB> ){
    my( $device, $temp ) = $line =~ /^(.+)\:\s*(.+)$/;
    $temps{$device} = $temp;
  }
  close IB;

  return $temps{$id};
}

sub read_wmweather{
  my( $file ) = @_;

  my $temp;
  open WM, "<$file" or die "cannot read from wmweather file, '$file'\n";

  while( my $line = <WM> ){
    if( $line =~ /^Temperature:\s*(\-?[\d\.]+)\s*F\s+\((\-?[\d\.]+)\s+C\)/ ){
      $temp = $2;
      last;
    }
  }

  close WM;

  return $temp;
}

sub process_temp_schedule{
  my( $schedule ) = @_;

  # get the current time
  #  0    1    2     3     4    5     6     7     8
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
    localtime(time);


  my $default;
  my $prev_time;
  my $found_temp;

  my $cur_time = $min + $hour * 60;

  foreach my $item (split /;/, $schedule ){
    my( $range, $temp ) = split /=/, $item;

    if( lc $range eq "default" ){
      $default = $temp;

    }elsif( $range =~ /(\d{1,2}:\d\d)?-(\d{1,2}:\d\d)/ ){
      my $start = $1;
      my $end   = $2;

      $start = hhmm_to_mins($start) if defined $start;
      $end   = hhmm_to_mins($end);

      $start = $prev_time unless defined $start;

      # if we still don't have a start time...
      unless( defined $start ){
	die "Cannot understand date range, \"$range\". Missing start time.\n";
      }

      # allow for the midnight wrap
      if( $end < $start ){
	if( $cur_time >= $start ){
	  $end = 1439; # 23 * 60 + 59

	}elsif( $cur_time < $start ){
	  $start = 0; # midnight
	}
      }elsif( $end == $start ){
	warn "start time and end time are the same\n";
      }

      $prev_time = $end;

      # a valid range
      if( $cur_time >= $start && $cur_time < $end ){
	$found_temp = $temp;
	last;
      }

    }

  }

  if( !defined $found_temp && defined $default ){
    $found_temp = $default;
  }

  return $found_temp;
}

# 24h time -> minutes
sub hhmm_to_mins{
  my( $hhmm ) = @_;

  my $mins;

  if( $hhmm =~ /(\d{1,2}):(\d\d)/ ){
    $mins = $1 * 60 + $2;
  }else{
    warn "invalid time, \"$hhmm\"\n";
  }

  return $mins;
}

######################################################################
package Fan;

sub new{

    my $proto = shift;
    my $class = ref($proto) || $proto;

    my($self) = {};

    bless( $self, $class );

    $self->{unit} = shift;
    $self->{name} = shift;
    $self->{state} = undef;

    return $self;
}

sub on{
    my $self = shift;
    
    $self->set_state('on');
}

sub off{
    my $self = shift;
    
    $self->set_state('off');
}

sub set_state{
    my $self = shift;
    my ($state) = @_;

#    if( !$self->{state} || $self->{state} != $state ){
	print "Turning ", $self->{name}, " ", $state, "\n";
	$self->send_x10( $state );
	$self->{state} = $state;
#    }
}

sub send_x10{
    my $self = shift;

    my( $action ) = @_;

    my $unit = $self->{unit};

    my $x10 = $x10_command;

    $x10 =~ s/\%UNITS\%/$unit/g;
    $x10 =~ s/\%ACTION\%/$action/g;

    system( $x10 );
}

1;
