#!/usr/bin/perl -w
# Licence: WTFPLv2
# Copyright 2015: Julien Moutinho
use strict;
use warnings;
use DateTime::Duration;
use DateTime;
use Device::SerialPort qw( 0.07 );
use File::Basename;
use File::Path;
use File::Spec;
use Log::Log4perl qw(:easy);
use RRDTool::OO;
#use Data::Dumper qw(Dumper);
#use IO::Handle;
my $step = 30; # NOTE: sampling interval in seconds
sub rrd_init () {
#Log::Log4perl->easy_init(
# { level => $INFO
# , category => 'rrdtool'
# , layout => '%m%n'
# });
my $now
= DateTime->now
( time_zone => 'local'
#, locale => $config{locale}
); # ->set_time_zone('floating');
my $dir = File::Spec->catdir(dirname($0), 'rrd', (sprintf '%0d', $now->year()));
my $file = File::Spec->catfile($dir, (sprintf '%02d.rrd', $now->month()));
File::Path::make_path($dir);
my $rrd = RRDTool::OO->new(file => $file);
if (not (-e $file)) {
my $one_month = DateTime::Duration->new(months => 1, end_of_month => 'limit');
my $month_begin = $now->clone->truncate(to => 'month');
my $month_end = $month_begin->clone->add_duration($one_month);
my $month_seconds = $month_end->epoch() - $month_begin->epoch();
$rrd->create
( step => $step
, data_source =>
{ name => "temperature"
, type => "GAUGE"
, min => -15
, max => 49
, heartbeat => 2 * $step # seconds
}
, data_source =>
{ name => "humidity"
, type => "GAUGE"
, min => 0
, max => 100
, heartbeat => 2 * $step # seconds
}
, data_source =>
{ name => "air"
, type => "GAUGE"
, min => 0
, max => 1000
, heartbeat => 2 * $step # seconds
}
, data_source =>
{ name => "dust"
, type => "GAUGE"
, min => 0
, max => 100000
, heartbeat => 2 * $step # seconds
}
, data_source =>
{ name => "co_ch4_lpg"
, type => "GAUGE"
, min => 0
, max => 5
, heartbeat => 2 * $step # seconds
}
, archive =>
{ cpoints => 1
, cfunc => 'AVERAGE'
, rows => $month_seconds / $step
, xff => 0.99 # ignore unknowns values (U)
}
);
}
return $rrd;
}
sub dev_init ($) {
my ($file) = @_;
my $dev = Device::SerialPort->new($file);
$dev->baudrate(9600); # MUST: match *uino Serial.begin()
#$dev->buffers(4096, 4096); # NOTE: no-op on POSIX
$dev->databits(8);
$dev->dtr_active(1); # NOTE: reset the *uino on serial connection
$dev->handshake('none');
$dev->parity("none");
$dev->read_char_time(0); # NOTE: don't wait for each character
$dev->read_const_time(1000); # NOTE: 1 second per unfulfilled "read" call
$dev->stopbits(1);
$dev->write_settings;
return $dev;
}
sub main () {
my $rrd = rrd_init();
my $dev = dev_init($ARGV[0]);
#autoflush STDOUT 1;
my $read_timeout = 60;
my $timeout = $read_timeout;
my $buffer = "";
my $last_time = 0;
my @collect = ();
while ($timeout>0) {
my $curr_day = (localtime)[3];
if ($curr_day == 1) {
# NOTE: month changed, change RRD
# FIXME: avoid to check that every second...
$rrd = rrd_init();
}
my ($count, $saw) = $dev->read(1);
# NOTE: this could have read up to 255 chars (max for portability)
# but reading only 1 char at a time
# enables to add a more accurate timestamp.
if (defined $count and $count > 0) {
$buffer .= $saw;
#print STDERR "saw: $buffer\n";
my @lines = split /\r?\n/, $buffer;
if (@lines > 1) {
my $time = time;
# NOTE: process only ended lines
$buffer = pop @lines;
#print STDERR "buffer: $buffer\n";
foreach (@lines) {
#print STDERR "line: $_\n";
my @fields
= $_ =~ m/^loop=[0-9]+;up=[0-9]+;temp=([0-9]+.[0-9]+)?;humi=([0-9]+.[0-9]+)?;air=([0-9]+)?;dust=([0-9]+.[0-9]+)?;fire=([0-9]+.[0-9]+)?$/;
my $i;
for ($i = 0; $i < @fields; $i++) {
if (defined $fields[$i] && $fields[$i] ne '') {
$collect[$i] = $fields[$i];
}
elsif (not (exists $collect[$i])) {
$collect[$i] = undef;
}
}
#print STDERR "collect: ".Dumper(\@collect)."\n";
if ($time >= $last_time + $step && @collect == 5) {
$rrd->update
( time => $time
, values => [join (':', map { defined $_ ? $_ : 'U' } @collect)]
);
@collect = ();
$last_time = $time;
}
#else {
# print STDOUT ($time, ";", $_, "\n");
# }
}
}
$timeout = $read_timeout;
}
else {
$timeout--;
}
}
if ($timeout==0) {
die "Connection timeout (after ${read_timeout}s)\n";
}
undef $dev;
}
main;
1;