#! /usr/bin/perl
###############################################################################
#
# License
# =======
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
# .
#
# Copyright (C) 2012 Jörg Sommrey
###############################################################################
use strict;
use warnings;
use DBI;
use Getopt::Long;
use Pod::Usage;
use DateTime;
use DateTime::Format::Strptime;
# wview mysql account data
use constant WVIEW_DB => "wview";
use constant WVIEW_HOST => "localhost";
use constant WVIEW_USER => "nobody";
use constant WVIEW_PASSWD => undef;
# names of fields that are available via web interface
my @allfields =
qw{barometer dewpoint groundHumidity groundTemp topWindchill
topTemp heatindex inHumidity inTemp outHumidity outTemp
outTempBatteryStatus rain rainBatteryStatus rainRate
groundBatteryStatus sun windBatteryStatus windDir windGust
windGustDir windSpeed windchill};
my %aliases =
qw{groundHumidity extraHumid1
groundTemp extraTemp1
topWindchill extraTemp2
topTemp extraTemp3
sun UV
groundBatteryStatus txBatteryStatus};
use constant DATEFMT => "%F";
use constant DATETIMEFMT => "%F %T";
use constant TIMEZONE => "local";
my $strpdt = new DateTime::Format::Strptime(pattern => DATETIMEFMT,
time_zone => TIMEZONE);
my $strpd = new DateTime::Format::Strptime(pattern => DATEFMT,
time_zone => TIMEZONE);
my ($dbh, $from, $to, $min, $max, $smooth, $list, $term, $output,
$aggregate, $aggregate_type);
my @axes = qw{x1y2 x1y1};
sub fieldname {
my $field = shift;
my ($alias, $dummy) = grep /^$field$/i, keys %aliases;
return $aliases{$alias} if $alias;
return $field;
}
sub plottype {
$_ = shift;
/dir$/i and return "points pt 14";
/^rain$/i and return "steps";
/^rainrate$/i and return "boxes";
my $type = "lines";
$type .= " smooth bezier" if $smooth;
return $type;
}
sub aggrtype {
my $field = shift;
$aggregate eq "no" && return $field;
$field =~ /^rain$/i && return "sum($field)";
return "$aggregate_type($field)";
}
sub aggregate {
$aggregate eq "no" && return "datetime";
$aggregate eq "hour" &&
return "DATE_FORMAT(datetime, '%Y-%m-%d %H:00:00')";
$aggregate eq "day" &&
return "DATE_FORMAT(datetime, '%Y-%m-%d 00:00:00')";
}
sub groupby {
$aggregate eq "no" && return "";
return "GROUP BY 1";
}
sub unit {
$_ = shift;
/status/i and return "";
/(barometer|pressure)$/i and return 'mbar';
/(temp|dewpoint|windchill|heatindex)/i and return 'C';
/humid/i and return '%';
/(speed|gust)$/i and return 'km/h';
/dir$/i and return 'deg';
/^rain$/i and return 'mm';
/^rainrate/i and return 'mm/h';
/^uv$/i and return 'K';
return "";
}
sub datetime {
my $time = shift;
my $dt = $strpdt->parse_datetime($time);
$dt = $strpd->parse_datetime($time) unless $dt;
$dt->set_formatter($strpdt) if $dt;
return $dt;
}
sub plot {
my $field = shift;
my $cumulate = 1 if $field =~ /^rain$/i;
my $sql = "SELECT ". aggregate() . ", " . aggrtype($field) .
" FROM archive_m " .
"WHERE datetime BETWEEN ? AND ? " . groupby();
my $sth = $dbh->prepare($sql) or die "prepare failed: " . DBI->errstr;
$sth->execute($min, $max) or die "execute failed: " . DBI->errstr;
my $accu = .0;
while (my $row = $sth->fetchrow_arrayref) {
unless ($cumulate) {
print GP "$row->[0]\t$row->[1]\n" if defined $row->[1];
print GP "\n" unless defined $row->[1];
} else {
if (defined $row->[1]) {
$accu += $row->[1];
print GP "$row->[0]\t$accu\n";
} else {
print GP "\n";
}
}
}
print GP "e\n";
}
my ($help, $man);
my @ifield;
my $days = 1;
my $aggr = "auto";
my $aggrtype = "AVG";
Getopt::Long::Configure('no_ignore_case');
GetOptions("from=s" => \$from,
"to=s" => \$to,
"days=i" => \$days,
"smooth!" => \$smooth,
"aggr=s" => \$aggr,
"list!" => \$list,
"Aggrtype=s" => \$aggrtype,
"Field=s" => \@ifield,
"Terminal=s" => \$term,
"output=s" => \$output,
"help" => \$help,
"man" => \$man) or pod2usage(2);
pod2usage(-verbose => 1) if $help;
pod2usage(-verbose => 2) if $man;
@ifield = split(/,/, join(',', @ifield));
push @ifield, qw{heatIndex windChill outTemp dewPoint} unless @ifield;
my %unit;
foreach my $alias (@ifield) {
my $field = fieldname($alias);
my $unit = unit($field);
$unit{$unit} ||= [];
push @{$unit{$unit}}, $alias;
}
print(join("\n", @allfields) . "\n") && exit if $list;
my @field;
my %alias;
my %axes;
my @unit = ();
foreach my $unit (sort {$#{$unit{$b}} <=> $#{$unit{$a}}} keys %unit) {
my $axes = shift @axes or last;
push @unit, $unit;
foreach my $alias (@{$unit{$unit}}) {
my $field = fieldname($alias);
push @field, $field;
$alias{$field} = $alias;
$axes{$field} = $axes;
}
}
$to = datetime($to) if $to;
$from = datetime($from) if $from;
foreach (qw{no auto hour day}) {
/^$aggr/ and $aggregate = $_ and last;
}
foreach (qw{AVG MIN MAX}) {
/^$aggrtype/i and $aggregate_type = $_ and last;
}
$to = DateTime->now(time_zone => TIMEZONE, formatter => $strpdt)
if !$to && !$from;
$to = $from->clone()->add(days => $days) if !$to && $from;
$from = $to->clone()->add(days => -$days) if !$from;
my $dburi = "DBI:mysql:database=" . WVIEW_DB . ";host=" . WVIEW_HOST;
$dbh = DBI->connect($dburi, WVIEW_USER, WVIEW_PASSWD, {RaiseError => 1})
or die "could not connect to database: " . DBI->errstr;
my $stmt = "SELECT min(datetime), max(datetime) FROM archive_m ";
$stmt .= "WHERE datetime BETWEEN ? AND ? AND (";
$stmt .= join(" OR ", (map "$_ IS NOT NULL", @field));
$stmt .= ");";
($min, $max) = $dbh->selectrow_array($stmt, undef, $from,
$to);
$min = datetime($min);
$max = datetime($max);
my ($ddays, $dmonths) = $max->subtract_datetime($min)->in_units('days',
'months');
if ($aggregate eq "auto") {
{
$dmonths > 0 and $aggregate = "day" and last;
($ddays > 2 or $dmonths > 0) and $aggregate = "hour" and last;
$aggregate = "no";
}
}
my $format;
if ($ddays < 2 && $dmonths == 0) {
$format = '%H:%M';
} elsif ($ddays < 6 && $dmonths == 0) {
$format = '%d-%H';
} elsif ($dmonths < 4 || $dmonths < 5 && $ddays < 16) {
$format = '%d.%m';
} else {
$format = '%y/%m';
}
open GP, "|gnuplot -persist" or die "cannot pipe to gnuplot";
#open GP, ">-";
print GP 'set timefmt "%Y-%m-%d %H:%M:%S"' . "\n";
print GP "set xdata time\n";
print GP 'set format x "' . $format . '"' . "\n";
print GP 'set autoscale y2' . "\n";
print GP 'set ytics nomirror' . "\n";
print GP 'set y2tics' . "\n";
print GP "set terminal $term\n" if $term;
print GP "set output \"$output\"\n" if $output;
print GP "set key left\n";
print GP "set y2label \"$unit[0]\"\n" if $unit[0];
if ($unit[0] && $unit[0] eq "deg") {
print GP "set y2range [0:360]\n";
print GP "set y2tics 0,45\n";
}
if ($unit[1] && $unit[1] eq "deg") {
print GP "set yrange [0:360]\n";
print GP "set ytics 0,45\n";
}
print GP "set ylabel \"$unit[1]\"\n" if $unit[1];
print GP "set yrange [0:360]\n" if $unit[1] && $unit[1] eq "deg";
print GP "unset ytics\n" unless $unit[1];
print GP 'plot ';
print GP join(', ', (map '"-" using 1:3 axes ' . $axes{$_} .
' title "' . $alias{$_} . '" with ' . plottype($_), @field));
print GP "\n";
plot($_) foreach (@field);
close GP;
__END__
=head1 NAME
weatherplot - plot weather data
=head1 SYNOPSIS
B [B<-from> from-time] [B<-to> to-time] [B<-days> timespan]
[B<-smooth>]
[B<-aggr> {B|B|B|B}
[B<-Aggrtype> {B|B|B}]]
[B<-list>] [B<-Field> field] [B<-Terminal> term [B<-output> outfile]] ...
=head1 OPTIONS
=over 8
=item B<-from> from-time
Start time of plot. Defaults to I - I.
Format is I or I.
=item B<-to> to-time
End time of plot. Defaults to current time or I +
I, if I is specified.
Format is I or I.
=item B<-days> timespan
Lenght of plotting interval in days. Defaults to 1.
=item B<-smooth>
smooth graphs using bezier curves.
=item B<-aggr> {B|B|B|B}
select data aggregation.
If set to B, no aggregation will occur if the plotting interval is
up to 3 days, hourly aggregation for intervals longer than 3 days up to
one month and daily aggregation for longer intervals. By selecting B,
B or B you may force the given aggregation. Default: B
=item B<-Aggrtype> {B|B|B}
select type of data aggregation. Defaults to B. For I plots, the
aggregation type is always SUM, which is not available for other fields.
=item B<-list>
lists the names of available fields and exits. This list is only used
in the web interface.
With this program you can plot any field from the database.
=item B<-Field> field
Name of field to plot. Multiple B<-Field>-Options may be given or
multiple field names may be separated by comma.
=item B<-Terminal> term
Terminal type for B.
=item B<-output> outfile
output file name for B.
=item B<-help>
prints a short help message and exits.
=item B<-man>
prints the full documentation and exits.
=back
=head1 DESCRIPTION
Plots the requested data fields in the given timespan via I.
The plot has a left scale and a right scale.
Fields having common units share the same scale.
If the selected fields have more than two units, only the fields
belonging to the two most used units are plotted.
=cut
# vi:ts=4: