#!/usr/bin/perl -T # Usage: ./solar.pl profile.csv $i $j < forecast_file.csv # where each data row from the forecast file has the following format: # # PvEstimate,PvEstimate10,PvEstimate90,PeriodEnd,Period # 0.4645,0.3248,0.5801,2022-11-03T11:00:00Z,PT30M # # and each data row from the profile file has the following format: # # Date,Percentage of Daily Consumption # 2022-11-02 01:00,3.12040112867697 # # The $i parameter is the daily average power usage of the house in kWh # # The $j parameter is the usable battery capacity in kWh use strict; use warnings; use DateTime; use Scalar::Util qw(looks_like_number); my @csv_forecast; my @csv_profile; # Check command line switches are fine. die "Wrong command line" unless ( @ARGV == 3 ); die "Wrong second argument" unless looks_like_number($ARGV[1]); if ( ($ARGV[1]) < 0 ) { die "Wrong second argument"; } die "Wrong third argument" unless looks_like_number($ARGV[2]); if ( ($ARGV[2] < 0 )) { die "Wrong second argument"; } my $profile_path = $ARGV[0]; my $daily_power = $ARGV[1]; my $max_battery = $ARGV[2]; # Arbitrarily set the battery to mid-charge in the begining my $battery = $max_battery / "2"; my $line_number = "0"; #open (my $fforecast, '<', $ARGV[0]) or die "Failed to open $ARGV[0]"; while ( my $line = <STDIN> ) { $line =~ s/\R/\012/; chomp ($line); my ($power,$timestamp,$period) = (split "," , $line)[0,3,4]; if ($line_number == "0") { if ( ( $power ne 'PvEstimate' ) or ( $timestamp ne 'PeriodEnd')) { die "Forecast CSV file looks malformed."; } } else { $power =~ /^\d(\.\d+)?$/ or die "First column at row ++$line_number of the source file is malformed"; $timestamp =~ /^(\d{4}\-\d{2}\-\d{2})(T\d{2}\:\d{2}\:\d{2}Z)?$/ or die "The date column at row ++$line_number has an unexpected format"; $period =~ /^(PT30M)$/ or die "Fifth column at row ++$line_number of the source file has an unexpected value." } ${csv_forecast[$line_number]}{"power"} = $power; ${csv_forecast[$line_number]}{"timestamp"} = $timestamp; ++$line_number; } #close ($fforecast); $line_number = 0; open (my $fprofile, '<', $ARGV[0]) or die "Failed to open $ARGV[0]"; while ( my $line = <$fprofile>) { chomp ($line); my ($timestamp,$percentage) = (split "," , $line); if ($line_number == "0") { if ( ($timestamp ne 'Date') or ($percentage ne 'Percentage of Daily Consumption')) { die "Profile file looks malformed" } } else { $percentage =~ /^[\d]*\.[\d]*$/ or die "Profile looks malformed"; $timestamp =~ /^([\d]{4})\-([\d]{2})\-([\d]{2}) ([\d]{2})\:([\d]{2})$/ or die "Profile looks malformed."; ${csv_profile[$line_number]}{"timestamp"} = $timestamp; ${csv_profile[$line_number]}{"percentage"} = $percentage; } ++$line_number; } close ($fprofile); # For each 30 minutes period, calculate whether we are going to produce # more power than we are going to consume (and therefore waste it), # or whether we are going to consume more power than we produce (and # therefore have deficit). my $waste = "0"; my $deficit = "0"; my $produced = "0"; my ( $y,$m,$d,$h,$mt,$sec ); my ( $i,$j ); for ( $i = 1 ; $i < @csv_forecast ; ++$i ) { ($y,$m,$d) = $csv_forecast[$i]{"timestamp"} =~ /^([\d]{4})\-([\d]{2})\-([\d]{2})/; ($h,$mt,$sec) = $csv_forecast[$i]{"timestamp"} =~ /T([\d]{2})\:([\d]{2})\:([\d]{2})Z$/; unless ( defined ($h) ) { ($h,$mt,$sec) = ( '0', '0', '0') }; my $forecast_time = DateTime->new( year => $y, month => $m, day => $d, hour => $h, minute => $mt, second => $sec, time_zone => 'UTC' ); $forecast_time->set_time_zone('local'); # Check if the forecast entries are placed in proper order if ( $i > "1" ) { my ( $oy,$om,$od,$oh,$omt,$osec ); ($oy,$om,$od) = $csv_forecast[$i-1]{"timestamp"} =~ /^([\d]{4})\-([\d]{2})\-([\d]{2})/; ($oh,$omt,$osec) = $csv_forecast[$i-1]{"timestamp"} =~ /T([\d]{2})\:([\d]{2})\:([\d]{2})Z$/; unless ( defined ($oh) ) { ($oh,$omt,$osec) = ( '0', '0', '0') }; my $oforecast_time = DateTime->new( year => $oy, month => $om, day => $od, hour => $oh, minute => $omt, second => $osec, time_zone => 'UTC' ); $oforecast_time->set_time_zone('local'); # Compare if old forecast timestamp is older than the one we are about to evaluate. die "The forecast file uses wrong ordering" if ( DateTime->compare($forecast_time,$oforecast_time) != 1 ); # Check that the time difference is only 30 minutes die "The forecast file has a messed up timestamp" if ($forecast_time->subtract_datetime($oforecast_time)->minutes() != 30 ); } my $consumption; my ( $hour,$minute ); for ( $j = 1 ; $j < @csv_profile ; ++$j ) { ($hour,$minute) = $csv_profile[$j]{"timestamp"} =~ /([\d]{2})\:([\d]{2})$/; if ( $hour == $forecast_time->hour() and $minute == $forecast_time->minute() ) { $consumption = $csv_profile[$j]{"percentage"} * $daily_power * 0.01; last; } } die "No consumption information in the profile file for timestamp " . $forecast_time->hour() . ":" . $forecast_time->minute() unless defined($consumption); if ( $consumption > ( $csv_forecast[$i]{"power"} * 0.5 )) { my $excess = $consumption - ($csv_forecast[$i]{"power"} * 0.5 ); if ( ($battery - $excess) < "0" ) { $deficit += $excess - $battery; $battery = "0"; } else { $battery -= $excess; } } else { my $excess = ($csv_forecast[$i]{"power"} * 0.5 ) - $consumption; if ( ($battery + $excess) > $max_battery) { $waste += $excess - ($max_battery - $battery); $battery = $max_battery; } else { $battery += $excess; } } $produced += $csv_forecast[$i]{"power"} * 0.5; # If the day has changed, print information for the day. if ( $forecast_time->hour() == "0" and $forecast_time->minute() == "0" ) { $forecast_time->subtract( days=> "1"); print "INFORMATION FOR DATE ". $forecast_time->date() . "\n"; print "Total power produced will be $produced kWh\n"; print "About $deficit kWh will be taken from the grid (deficit)\n"; print "About $waste kWh will be produced but not used (wasted)\n"; print "The charge of the battery by midnight will be " . ( $battery / $max_battery ) * 100 . "% \n\n"; $deficit = 0; $waste = 0; $produced = 0; } } exit;