Sven Velt 11 年之前
父节点
当前提交
0f6965cffd
共有 15 个文件被更改,包括 838 次插入22 次删除
  1. 1 1
      Makefile
  2. 7 1
      README
  3. 75 19
      arexxd.c
  4. 49 0
      example-graphs
  5. 1 1
      init-arexxd
  6. 二进制
      teplomery.gnumeric
  7. 517 0
      web/UCW/CGI.pm
  8. 46 0
      web/index.cgi
  9. 15 0
      web/rh-12h.cgi
  10. 15 0
      web/rh-48h.cgi
  11. 16 0
      web/rh-month.cgi
  12. 27 0
      web/temp-12h.cgi
  13. 27 0
      web/temp-48h.cgi
  14. 23 0
      web/temp-month.cgi
  15. 19 0
      web/temp-quick.cgi

+ 1 - 1
Makefile

@@ -1,4 +1,4 @@
-VERSION=1.1
+VERSION=1.2
 ARCHIVE=arexxd-$(VERSION).tar.gz
 
 CC=gcc

+ 7 - 1
README

@@ -2,7 +2,7 @@
 
 			    Arexx Data Logger Daemon
 
-		       (c) 2011 Martin Mares <mj@ucw.cz>
+		    (c) 2011--2012 Martin Mares <mj@ucw.cz>
 
        You can use and distribute this program under the terms of GPLv2.
 
@@ -28,3 +28,9 @@ source to process the data in any way you like.
 This program is not supported or endorsed by AREXX in any way. Also, there is
 no warranty on what it does or does not. However, if you have any bug reports
 or suggestions, I will be glad to hear them.
+
+To build the program, you need development packages for libusb-1.0 and librrd
+(they are probably called <something>-dev in your system). You might want
+to tweak init-arexxd script, which expects that an account called "arexxd"
+exists in /etc/passwd. Also, you can customize the correct_point() function
+in arexxd.c.

+ 75 - 19
arexxd.c

@@ -1,7 +1,7 @@
 /*
  *	Linux Interfece for Arexx Data Loggers
  *
- *	(c) 2011 Martin Mares <mj@ucw.cz>
+ *	(c) 2011-2012 Martin Mares <mj@ucw.cz>
  */
 
 #include <stdio.h>
@@ -19,7 +19,7 @@
 #include <libusb-1.0/libusb.h>
 #include <rrd.h>
 
-#define LOG_PATH "/var/log/arexxd"
+#define DEFAULT_LOG_DIR "/var/log/arexxd"
 
 typedef unsigned char byte;
 static libusb_context *usb_ctxt;
@@ -29,6 +29,7 @@ static int use_syslog;
 static int debug_mode;
 static int debug_packets;
 static int debug_raw_data;
+static char *log_dir = DEFAULT_LOG_DIR;
 
 static void die(char *fmt, ...)
 {
@@ -82,7 +83,6 @@ static void log_pkt(char *fmt, ...)
 
 /*** RRD interface ***/
 
-#define SLOT_SIZE 10					// 10 seconds per averaging slot
 #define MAX_ARGS 20
 #define MAX_ARG_SIZE 1024
 
@@ -112,10 +112,10 @@ static void arg_push(const char *fmt, ...)
 	arg_pos += len;
 }
 
-static void rrd_point(time_t t, int id, double val, char *unit)
+static void rrd_point(time_t t, const char *name, double val, char *unit)
 {
 	char rr_name[256];
-	snprintf(rr_name, sizeof(rr_name), "sensor-%d.rrd", id);
+	snprintf(rr_name, sizeof(rr_name), "sensor-%s.rrd", name);
 
 	struct stat st;
 	if (stat(rr_name, &st) < 0 || !st.st_size) {
@@ -156,20 +156,53 @@ static void rrd_point(time_t t, int id, double val, char *unit)
 
 #define TIME_OFFSET 946681200		// Timestamp of 2000-01-01 00:00:00
 
-static int data_point_counter;
+static int data_point_counter;		// Since last log message
+
+static double correct_point(int id, double val, const char **name)
+{
+	/*
+	 *  Manually calculated corrections and renames for my sensors.
+	 *  Replace with your formulae.
+	 */
+	switch (id) {
+		case 10415:
+			*name = "ursarium";
+			return val - 0.93;
+		case 10707:
+			*name = "balcony";
+			return val - 0.71;
+		case 19246:
+			*name = "catarium";
+			return val + 0.49;
+		case 19247:
+			*name = "catarium-rh";
+			return val;
+		case 12133:
+			*name = "outside";
+			return val + 0.44;
+		default:
+			return val;
+	}
+}
 
 static void cooked_point(time_t t, int id, double val, char *unit, int q)
 {
+	char namebuf[16];
+	snprintf(namebuf, sizeof(namebuf), "%d", id);
+	const char *name = namebuf;
+
+	double val2 = correct_point(id, val, &name);
+
 	if (debug_raw_data) {
 		struct tm tm;
 		localtime_r(&t, &tm);
 		char tbuf[64];
 		strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tm);
-		printf("== %s id=%d val=%.3f unit=%s q=%d\n", tbuf, id, val, unit, q);
+		printf("== %s id=%d name=%s val=%.3f val2=%.3f unit=%s q=%d\n", tbuf, id, name, val, val2, unit, q);
 	}
 
 	data_point_counter++;
-	rrd_point(t, id, val, unit);
+	rrd_point(t, name, val2, unit);
 }
 
 static void raw_point(int t, int id, int raw, int q)
@@ -453,6 +486,7 @@ static void set_clock(void)
 
 /*** Main ***/
 
+static sigset_t term_sigs;
 static volatile sig_atomic_t want_shutdown;
 
 static void sigterm_handler(int sig __attribute__((unused)))
@@ -460,28 +494,51 @@ static void sigterm_handler(int sig __attribute__((unused)))
 	want_shutdown = 1;
 }
 
+static void interruptible_msleep(int ms)
+{
+	sigprocmask(SIG_UNBLOCK, &term_sigs, NULL);
+	struct timespec ts = { .tv_sec = ms/1000, .tv_nsec = (ms%1000) * 1000000 };
+	nanosleep(&ts, NULL);
+	sigprocmask(SIG_BLOCK, &term_sigs, NULL);
+}
+
 static const struct option long_options[] = {
 	{ "debug",		0, NULL, 'd' },
-	{ "log-packets",	0, NULL, 'p' },
+	{ "log-dir",		1, NULL, 'l' },
+	{ "debug-packets",	0, NULL, 'p' },
+	{ "debug-raw",		0, NULL, 'r' },
 	{ NULL,			0, NULL, 0 },
 };
 
 static void usage(void)
 {
-	fprintf(stderr, "Usage: arexxd [--debug] [--log-packets]\n");
+	fprintf(stderr, "\n\
+Usage: arexxd <options>\n\
+\n\
+Options:\n\
+-d, --debug		Debug mode (no chdir, no fork, no syslog)\n\
+-l, --log-dir=<dir>	Directory where all received data should be stored\n\
+-p, --debug-packets	Log all packets sent and received\n\
+-r, --debug-raw		Log conversion from raw values\n\
+");
 	exit(1);
 }
 
 int main(int argc, char **argv)
 {
 	int opt;
-	while ((opt = getopt_long(argc, argv, "dp", long_options, NULL)) >= 0)
+	while ((opt = getopt_long(argc, argv, "dl:pr", long_options, NULL)) >= 0)
 		switch (opt) {
 			case 'd':
 				debug_mode++;
 				break;
+			case 'l':
+				log_dir = optarg;
+				break;
 			case 'p':
 				debug_packets++;
+				break;
+			case 'r':
 				debug_raw_data++;
 				break;
 			default:
@@ -496,8 +553,8 @@ int main(int argc, char **argv)
 	// libusb_set_debug(usb_ctxt, 3);
 
 	if (!debug_mode) {
-		if (chdir(LOG_PATH) < 0)
-			die("Cannot change directory to %s: %m", LOG_PATH);
+		if (chdir(log_dir) < 0)
+			die("Cannot change directory to %s: %m", log_dir);
 		if (debug_packets || debug_raw_data) {
 			close(1);
 			if (open("debug", O_WRONLY | O_CREAT | O_APPEND, 0666) < 0)
@@ -518,7 +575,6 @@ int main(int argc, char **argv)
 	sigaction(SIGTERM, &sa, NULL);
 	sigaction(SIGINT, &sa, NULL);
 
-	sigset_t term_sigs;
 	sigemptyset(&term_sigs);
 	sigaddset(&term_sigs, SIGTERM);
 	sigaddset(&term_sigs, SIGINT);
@@ -531,7 +587,7 @@ int main(int argc, char **argv)
 				inited = 1;
 				log_error("Data logger not connected, waiting until it appears");
 			}
-			sleep(30);
+			interruptible_msleep(30000);
 			continue;
 		}
 		log_info("Listening");
@@ -563,17 +619,17 @@ int main(int argc, char **argv)
 			want_sleep = 1;
 			if (err > 0 && parse_packet(reply))
 				want_sleep = 0;
-			sigprocmask(SIG_UNBLOCK, &term_sigs, NULL);
 			if (want_sleep) {
-				sleep(4);
+				interruptible_msleep(4000);
 				want_stats = 1;
-			}
-			sigprocmask(SIG_BLOCK, &term_sigs, NULL);
+			} else
+				interruptible_msleep(5);
 		}
 
 		log_info("Disconnecting data logger");
 		release_device();
 		inited = 0;
+		interruptible_msleep(10000);
 	}
 
 	log_info("Terminated");

+ 49 - 0
example-graphs

@@ -0,0 +1,49 @@
+#!/bin/sh
+# An example graphing script for arexxd
+
+D=/var/log/arexxd
+
+rrdtool graph graph-12h-temp.png \
+	--start 'now-12h' \
+	--end 'now' \
+	--title "Temperature" \
+	-w 720 -h 600 \
+	-x MINUTE:10:HOUR:1:HOUR:2:0:%H:%M \
+	-y 5:1 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--units-exponent 0 --lower-limit -20 --upper-limit 40 --rigid \
+	--legend-position east \
+	DEF:a=$D/sensor-10415.rrd:temp:AVERAGE 'LINE1:a#0000cc:Ursarium\n' \
+	DEF:b=$D/sensor-12133.rrd:temp:AVERAGE 'LINE1:b#00cc00:Balcony\n' \
+	DEF:c=$D/sensor-19246.rrd:temp:AVERAGE 'LINE1:c#cc0000:Catarium'
+
+rrdtool graph graph-12h-humidity.png \
+	--start 'now-12h' \
+	--end 'now' \
+	--title "Relative Humidity" \
+	-w 720 -h 600 \
+	-x MINUTE:10:HOUR:1:HOUR:2:0:%H:%M \
+	-y 5:2 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--legend-position east \
+	--units-exponent 0 --lower-limit 0 --upper-limit 100 --rigid \
+	DEF:a=$D/sensor-19247.rrd:rh:AVERAGE 'LINE1:a#cc0000:Catarium'
+
+rrdtool graph graph-monthly-temp.png \
+	--start 'now-30d' \
+	--end 'now' \
+	--title "Temperature: MIN and MAX" \
+	-w 720 -h 600 \
+	-x DAY:1:DAY:5:DAY:5:0:%d:%m \
+	-y 5:1 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--units-exponent 0 --lower-limit -20 --upper-limit 40 --rigid \
+	--legend-position east \
+	DEF:alo=$D/sensor-10415.rrd:temp:MIN 'LINE1:alo#0000cc:Ursarium\n' \
+	DEF:ahi=$D/sensor-10415.rrd:temp:MAX 'LINE1:ahi#0000cc' \
+	DEF:blo=$D/sensor-12133.rrd:temp:MIN 'LINE1:blo#00cc00:Balcony\n' \
+	DEF:bhi=$D/sensor-12133.rrd:temp:MAX 'LINE1:bhi#00cc00' \
+	DEF:clo=$D/sensor-19246.rrd:temp:MIN 'LINE1:clo#cc0000:Catarium' \
+	DEF:chi=$D/sensor-19246.rrd:temp:MAX 'LINE1:chi#cc0000'
+
+gq

+ 1 - 1
init-arexxd

@@ -13,7 +13,7 @@ PATH=/sbin:/bin:/usr/sbin:/usr/bin
 DAEMON=/usr/local/sbin/arexxd
 NAME=arexxd
 DESC="Arexx data logger daemon"
-OPTIONS="--log-packets"
+OPTIONS="--debug-packets"
 
 . /lib/lsb/init-functions
 

二进制
teplomery.gnumeric


+ 517 - 0
web/UCW/CGI.pm

@@ -0,0 +1,517 @@
+#	Poor Man's CGI Module for Perl
+#
+#	(c) 2002--2011 Martin Mares <mj@ucw.cz>
+#	Slightly modified by Tomas Valla <tom@ucw.cz>
+#
+#	This software may be freely distributed and used according to the terms
+#	of the GNU Lesser General Public License.
+
+package UCW::CGI;
+
+# First of all, set up error handling, so that even errors during parsing
+# will be reported properly.
+
+# Variables to be set by the calling module:
+#	$UCW::CGI::error_mail		mail address of the script admin (optional)
+#					(this one has to be set in the BEGIN block!)
+#	$UCW::CGI::error_hook		function to be called for reporting errors
+
+my $error_reported;
+my $exit_code;
+my $debug = 0;
+
+sub report_bug($)
+{
+	if (!defined $error_reported) {
+		$error_reported = 1;
+		print STDERR $_[0];
+		if (defined($UCW::CGI::error_hook)) {
+			&$UCW::CGI::error_hook($_[0]);
+		} else {
+			print "Content-Type: text/plain\n\n";
+			print "Internal bug:\n";
+			print $_[0], "\n";
+			print "Please notify $UCW::CGI::error_mail\n" if defined $UCW::CGI::error_mail;
+		}
+	}
+	die;
+}
+
+BEGIN {
+	$SIG{__DIE__} = sub { report_bug($_[0]); };
+	$SIG{__WARN__} = sub { report_bug("WARNING: " . $_[0]); };
+	$exit_code = 0;
+}
+
+END {
+	$? = $exit_code;
+}
+
+use strict;
+use warnings;
+
+require Exporter;
+our $VERSION = 1.0;
+our @ISA = qw(Exporter);
+our @EXPORT = qw(&html_escape &url_escape &url_deescape &url_param_escape &url_param_deescape &self_ref &self_form &http_get);
+our @EXPORT_OK = qw();
+
+our $utf8_mode = 0;
+
+sub http_error($;@) {
+	my $err = shift @_;
+	print join("\n", "Status: $err", "Content-Type: text/plain", @_, "", $err, "");
+	exit;
+}
+
+### Escaping ###
+
+sub url_escape($) {
+	my $x = shift @_;
+	utf8::encode($x) if $utf8_mode;
+	$x =~ s/([^-\$_.!*'(),0-9A-Za-z\x80-\xff])/"%".unpack('H2',$1)/ge;
+	utf8::decode($x) if $utf8_mode;
+	return $x;
+}
+
+sub url_deescape($) {
+	my $x = shift @_;
+	utf8::encode($x) if $utf8_mode;
+	$x =~ s/%(..)/pack("H2",$1)/ge;
+	utf8::decode($x) if $utf8_mode;
+	return $x;
+}
+
+sub url_param_escape($) {
+	my $x = shift @_;
+	$x = url_escape($x);
+	$x =~ s/%20/+/g;
+	return $x;
+}
+
+sub url_param_deescape($) {
+	my $x = shift @_;
+	$x =~ s/\+/ /g;
+	return url_deescape($x);
+}
+
+sub html_escape($) {
+	my $x = shift @_;
+	$x =~ s/&/&amp;/g;
+	$x =~ s/</&lt;/g;
+	$x =~ s/>/&gt;/g;
+	$x =~ s/"/&quot;/g;
+	$x =~ s/'/&#39;/g;
+	return $x;
+}
+
+### Analysing RFC 822 Style Headers ###
+
+sub rfc822_prepare($) {
+	my $x = shift @_;
+	# Convert all %'s and backslash escapes to %xx escapes
+	$x =~ s/%/%25/g;
+	$x =~ s/\\(.)/"%".unpack("H2",$1)/ge;
+	# Remove all comments, beware, they can be nested (unterminated comments are closed at EOL automatically)
+	while ($x =~ s/^(("[^"]*"|[^"(])*(\([^)]*)*)(\([^()]*(\)|$))/$1 /) { }
+	# Remove quotes and escape dangerous characters inside (again closing at the end automatically)
+	$x =~ s{"([^"]*)("|$)}{my $z=$1; $z =~ s/([^0-9a-zA-Z%_-])/"%".unpack("H2",$1)/ge; $z;}ge;
+	# All control characters are properly escaped, tokens are clearly visible.
+	# Finally remove all unnecessary spaces.
+	$x =~ s/\s+/ /g;
+	$x =~ s/(^ | $)//g;
+	$x =~ s{\s*([()<>@,;:\\"/\[\]?=])\s*}{$1}g;
+	return $x;
+}
+
+sub rfc822_deescape($) {
+	my $x = shift @_;
+	return url_deescape($x);
+}
+
+### Reading of HTTP headers ###
+
+sub http_get($) {
+	my $h = shift @_;
+	$h =~ tr/a-z-/A-Z_/;
+	return $ENV{"HTTP_$h"} // $ENV{"$h"};
+}
+
+### Parsing of Arguments ###
+
+my $main_arg_table;
+my %raw_args;
+
+sub parse_raw_args_ll($$) {
+	my ($arg, $s) = @_;
+	$s =~ s/\r\n/\n/g;
+	$s =~ s/\r/\n/g;
+	utf8::decode($s) if $utf8_mode;
+	push @{$raw_args{$arg}}, $s;
+}
+
+sub parse_raw_args($) {
+	my ($s) = @_;
+	$s =~ s/\s+//;
+	for $_ (split /[&:]/, $s) {
+		(/^([^=]+)=(.*)$/) or next;
+		my $arg = $1;
+		$_ = $2;
+		s/\+/ /g;
+		s/%(..)/pack("H2",$1)/eg;
+		parse_raw_args_ll($arg, $_);
+	}
+}
+
+sub parse_multipart_form_data();
+
+sub init_args() {
+	if (!defined $ENV{"GATEWAY_INTERFACE"}) {
+		print STDERR "Must be called as a CGI script.\n";
+		$exit_code = 1;
+		exit;
+	}
+
+	my $method = $ENV{"REQUEST_METHOD"};
+	if (my $qs = $ENV{"QUERY_STRING"}) {
+		parse_raw_args($qs);
+	}
+	if ($method eq "GET" || $method eq "HEAD") {
+	} elsif ($method eq "POST") {
+		my $content_type = $ENV{"CONTENT_TYPE"} // "";
+		if ($content_type =~ /^application\/x-www-form-urlencoded\b/i) {
+			while (<STDIN>) {
+				chomp;
+				parse_raw_args($_);
+			}
+		} elsif ($content_type =~ /^multipart\/form-data\b/i) {
+			parse_multipart_form_data();
+		} else {
+			http_error "415 Unsupported Media Type";
+			exit;
+		}
+	} else {
+		http_error "405 Method Not Allowed", "Allow: GET, HEAD, PUT";
+	}
+}
+
+sub parse_args($) {			# CAVEAT: attached files must be defined in the main arg table
+	my $args = shift @_;
+	if (!$main_arg_table) {
+		$main_arg_table = $args;
+		init_args();
+	}
+
+	for my $a (values %$args) {
+		my $r = ref($a->{'var'});
+		defined($a->{'default'}) or $a->{'default'}="";
+		if ($r eq 'SCALAR') {
+			${$a->{'var'}} = $a->{'default'};
+		} elsif ($r eq 'ARRAY') {
+			@{$a->{'var'}} = ();
+		}
+	}
+
+	for my $arg (keys %$args) {
+		my $a = $args->{$arg};
+		defined($raw_args{$arg}) or next;
+		for (@{$raw_args{$arg}}) {
+			$a->{'multiline'} or s/(\n|\t)/ /g;
+			s/^\s+//;
+			s/\s+$//;
+			if (my $rx = $a->{'check'}) {
+				if (!/^$rx$/) { $_ = $a->{'default'}; }
+			}
+
+			my $v = $a->{'var'};
+			my $r = ref($v);
+			if ($r eq 'SCALAR') {
+				$$v = $_;
+			} elsif ($r eq 'ARRAY') {
+				push @$v, $_;
+			}
+		}
+	}
+}
+
+### Parsing Multipart Form Data ###
+
+my $boundary;
+my $boundary_len;
+my $mp_buffer;
+my $mp_buffer_i;
+my $mp_buffer_boundary;
+my $mp_eof;
+
+sub refill_mp_data($) {
+	my ($more) = @_;
+	if ($mp_buffer_boundary >= $mp_buffer_i) {
+		return $mp_buffer_boundary - $mp_buffer_i;
+	} elsif ($mp_buffer_i + $more <= length($mp_buffer) - $boundary_len) {
+		return $more;
+	} else {
+		if ($mp_buffer_i) {
+			$mp_buffer = substr($mp_buffer, $mp_buffer_i);
+			$mp_buffer_i = 0;
+		}
+		while ($mp_buffer_i + $more > length($mp_buffer) - $boundary_len) {
+			last if $mp_eof;
+			my $data;
+			my $n = read(STDIN, $data, 2048);
+			if ($n > 0) {
+				$mp_buffer .= $data;
+			} else {
+				$mp_eof = 1;
+			}
+		}
+		$mp_buffer_boundary = index($mp_buffer, $boundary, $mp_buffer_i);
+		if ($mp_buffer_boundary >= 0) {
+			return $mp_buffer_boundary;
+		} elsif ($mp_eof) {
+			return length($mp_buffer);
+		} else {
+			return length($mp_buffer) - $boundary_len;
+		}
+	}
+}
+
+sub get_mp_line($) {
+	my ($allow_empty) = @_;
+	my $n = refill_mp_data(1024);
+	my $i = index($mp_buffer, "\r\n", $mp_buffer_i);
+	if ($i >= $mp_buffer_i && $i < $mp_buffer_i + $n - 1) {
+		my $s = substr($mp_buffer, $mp_buffer_i, $i - $mp_buffer_i);
+		$mp_buffer_i = $i + 2;
+		return $s;
+	} elsif ($allow_empty) {
+		if ($n) {							# An incomplete line
+			my $s = substr($mp_buffer, $mp_buffer_i, $n);
+			$mp_buffer_i += $n;
+			return $s;
+		} else {							# No more lines
+			return undef;
+		}
+	} else {
+		http_error "400 Bad Request: Premature end of multipart POST data";
+	}
+}
+
+sub skip_mp_boundary() {
+	if ($mp_buffer_boundary != $mp_buffer_i) {
+		http_error "400 Bad Request: Premature end of multipart POST data";
+	}
+	$mp_buffer_boundary = -1;
+	$mp_buffer_i += 2;
+	my $b = get_mp_line(0);
+	print STDERR "SEP $b\n" if $debug;
+	$mp_buffer_boundary = index($mp_buffer, $boundary, $mp_buffer_i);
+	if (substr("\r\n$b", 0, $boundary_len) eq "$boundary--") {
+		return 0;
+	} else {
+		return 1;
+	}
+}
+
+sub parse_mp_header() {
+	my $h = {};
+	my $last;
+	while ((my $l = get_mp_line(0)) ne "") {
+		print STDERR "HH $l\n" if $debug;
+		if (my ($name, $value) = ($l =~ /([A-Za-z0-9-]+)\s*:\s*(.*)/)) {
+			$name =~ tr/A-Z/a-z/;
+			$h->{$name} = $value;
+			$last = $name;
+		} elsif ($l =~ /^\s+/ && $last) {
+			$h->{$last} .= $l;
+		} else {
+			$last = undef;
+		}
+	}
+	foreach my $n (keys %$h) {
+		$h->{$n} = rfc822_prepare($h->{$n});
+		print STDERR "H $n: $h->{$n}\n" if $debug;
+	}
+	return (keys %$h) ? $h : undef;
+}
+
+sub parse_multipart_form_data() {
+	# First of all, find the boundary string
+	my $ct = rfc822_prepare($ENV{"CONTENT_TYPE"});
+	if (!(($boundary) = ($ct =~ /^.*;\s*boundary=([^; ]+)/))) {
+		http_error "400 Bad Request: Multipart content with no boundary string received";
+	}
+	$boundary = rfc822_deescape($boundary);
+	print STDERR "BOUNDARY IS $boundary\n" if $debug;
+
+	# BUG: IE 3.01 on Macintosh forgets to add the "--" at the start of the boundary string
+	# as the MIME specs preach. Workaround borrowed from CGI.pm in Perl distribution.
+	my $agent = http_get("User-Agent") // "";
+	$boundary = "--$boundary" unless $agent =~ /MSIE\s+3\.0[12];\s*Mac/;
+	$boundary = "\r\n$boundary";
+	$boundary_len = length($boundary) + 2;
+
+	# Check upload size in advance
+	if (my $size = http_get("Content-Length")) {
+		my $max_allowed = 0;
+		foreach my $a (values %$main_arg_table) {
+			$max_allowed += $a->{"maxsize"} || 65536;
+		}
+		if ($size > $max_allowed) {
+			http_error "413 Request Entity Too Large";
+		}
+	}
+
+	# Initialize our buffering mechanism and part splitter
+	$mp_buffer = "\r\n";
+	$mp_buffer_i = 0;
+	$mp_buffer_boundary = -1;
+	$mp_eof = 0;
+
+	# Skip garbage before the 1st part
+	while (my $i = refill_mp_data(256)) { $mp_buffer_i += $i; }
+	skip_mp_boundary() || return;
+
+	# Process individual parts
+	do { PART: {
+		print STDERR "NEXT PART\n" if $debug;
+		my $h = parse_mp_header();
+		my ($field, $cdisp, $a);
+		if ($h &&
+		    ($cdisp = $h->{"content-disposition"}) &&
+		    $cdisp =~ /^form-data/ &&
+		    (($field) = ($cdisp =~ /;name=([^;]+)/)) &&
+		    ($a = $main_arg_table->{"$field"})) {
+			print STDERR "FIELD $field\n" if $debug;
+			if (defined $h->{"content-transfer-encoding"}) {
+				http_error "400 Bad Request: Unexpected Content-Transfer-Encoding";
+			}
+			if (defined $a->{"var"}) {
+				while (defined (my $l = get_mp_line(1))) {
+					print STDERR "VALUE $l\n" if $debug;
+					parse_raw_args_ll($field, $l);
+				}
+				next PART;
+			} elsif (defined $a->{"file"}) {
+				require File::Temp;
+				require IO::Handle;
+				my $max_size = $a->{"maxsize"} || 1048576;
+				my @tmpargs = (undef, UNLINK => 1);
+				push @tmpargs, DIR => $a->{"tmpdir"} if defined $a->{"tmpdir"};
+				my ($fh, $fn) = File::Temp::tempfile(@tmpargs);
+				print STDERR "FILE UPLOAD to $fn\n" if $debug;
+				${$a->{"file"}} = $fn;
+				${$a->{"fh"}} = $fh if defined $a->{"fh"};
+				my $total_size = 0;
+				while (my $i = refill_mp_data(4096)) {
+					print $fh substr($mp_buffer, $mp_buffer_i, $i);
+					$mp_buffer_i += $i;
+					$total_size += $i;
+					if ($total_size > $max_size) { http_error "413 Request Entity Too Large"; }
+				}
+				$fh->flush();	# Don't close the handle, the file would disappear otherwise
+				next PART;
+			}
+		}
+		print STDERR "SKIPPING\n" if $debug;
+		while (my $i = refill_mp_data(256)) { $mp_buffer_i += $i; }
+	} } while (skip_mp_boundary());
+}
+
+### Generating Self-ref URL's ###
+
+sub make_out_args(@) {		# Usage: make_out_args([arg_table, ...] name => value, ...)
+	my @arg_tables = ( $main_arg_table );
+	while (@_ && ref($_[0]) eq 'HASH') {
+		push @arg_tables, shift @_;
+	}
+	my %overrides = @_;
+	my $out = {};
+	for my $table (@arg_tables) {
+		for my $name (keys %$table) {
+			my $arg = $table->{$name};
+			defined($arg->{'var'}) || next;
+			defined($arg->{'pass'}) && !$arg->{'pass'} && !exists $overrides{$name} && next;
+			defined $arg->{'default'} or $arg->{'default'} = "";
+			my $value;
+			if (!defined($value = $overrides{$name})) {
+				if (exists $overrides{$name}) {
+					$value = $arg->{'default'};
+				} else {
+					$value = ${$arg->{'var'}};
+					defined $value or $value = $arg->{'default'};
+				}
+			}
+			if ($value ne $arg->{'default'}) {
+				$out->{$name} = $value;
+			}
+		}
+	}
+	return $out;
+}
+
+sub self_ref(@) {
+	my $out = make_out_args(@_);
+	return "?" . join(':', map { "$_=" . url_param_escape($out->{$_}) } sort keys %$out);
+}
+
+sub self_form(@) {
+	my $out = make_out_args(@_);
+	return join('', map { "<input type=hidden name=$_ value='" . html_escape($out->{$_}) . "'>\n" } sort keys %$out);
+}
+
+### Cookies
+
+sub set_cookie($$@) {
+	#
+	# Unfortunately, the support for the new cookie standard (RFC 2965) among
+	# web browsers is still very scarce, so we are still using the old Netscape
+	# specification.
+	#
+	# Usage: set_cookie(name, value, option => value...), where options are:
+	#
+	#	max-age		maximal age in seconds
+	#	domain		domain name scope
+	#	path		path name scope
+	#	secure		if present, cookie applies only to SSL connections
+	#			(in this case, the value should be undefined)
+	#	discard		if present with any value, the cookie is discarded
+	#
+
+	my $key = shift @_;
+	my $value = shift @_;
+	my %other = @_;
+	if (exists $other{'discard'}) {
+		delete $other{'discard'};
+		$other{'max-age'} = 0;
+	}
+	if (defined(my $age = $other{'max-age'})) {
+		delete $other{'max-age'};
+		my $exp = ($age ? (time + $age) : 0);
+		# Avoid problems with locales
+		my ($S,$M,$H,$d,$m,$y,$wd) = gmtime $exp;
+		my @wdays = ( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
+		my @mons = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' );
+		$other{'expires'} = sprintf("%s, %02d-%s-%d %02d:%02d:%02d GMT",
+			$wdays[$wd], $d, $mons[$m], $y+1900, $H, $M, $S);
+	}
+
+	print "Set-Cookie: $key=", url_escape($value);
+	foreach my $k (keys %other) {
+		print "; $k";
+		print "=", $other{$k} if defined $other{$k};
+	}
+	print "\n";
+}
+
+sub parse_cookies() {
+	my $h = http_get("Cookie") or return ();
+	my @cook = ();
+	foreach my $x (split /;\s*/, $h) {
+		my ($k,$v) = split /=/, $x;
+		$v = url_deescape($v) if defined $v;
+		push @cook, $k => $v;
+	}
+	return @cook;
+}
+
+1;  # OK

+ 46 - 0
web/index.cgi

@@ -0,0 +1,46 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use lib '.';
+use UCW::CGI;
+
+my $graph;
+
+UCW::CGI::parse_args({
+	'g' => { 'var' => \$graph, 'default' => 'temp-12h' },
+});
+
+print <<AMEN ;
+Content-type: text/html
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html><head>
+<title>Weather in the Burrow</title>
+<body>
+<h1>Weather in the Burrow</h1>
+AMEN
+
+sub links(@) {
+	my $prefix = shift @_;
+	my $out = "";
+	for my $x (@_) {
+		my $y = $prefix . $x;
+		if ($graph eq $y) {
+			$out .= " <b>$x</b>";
+		} else {
+			$out .= " <a href='?g=$y'>$x</a>";
+		}
+	}
+	return $out;
+}
+
+print "<p>Temperature:", links("temp-", "quick", "12h", "48h", "month"), "\n";
+print "<p>Humidity:", links("rh-", "12h", "48h", "month"), "\n";
+print "<p>Power:", links("power-", "2h", "2h-detail", "day", "day-detail", "month"), "\n";
+
+if ($graph =~ /^power-/) {
+	$graph = "http://micac.burrow.ucw.cz/cgi-bin/$graph";
+}
+print "<p><img src='$graph.cgi'>\n";

+ 15 - 0
web/rh-12h.cgi

@@ -0,0 +1,15 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-12h' \
+	--end 'now' \
+	--title "Relative Humidity" \
+	-w 720 -h 600 \
+	-x MINUTE:10:HOUR:1:HOUR:2:0:%H:%M \
+	-y 5:2 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--legend-position east \
+	--units-exponent 0 --lower-limit 0 --upper-limit 100 --rigid \
+	DEF:a=$D/sensor-catarium-rh.rrd:rh:AVERAGE 'LINE1:a#cc0000:Catarium'

+ 15 - 0
web/rh-48h.cgi

@@ -0,0 +1,15 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-48h' \
+	--end 'now' \
+	--title "Relative Humidity: Last 2 days" \
+	-w 720 -h 600 \
+	-x HOUR:1:HOUR:8:HOUR:8:0:%H:%M \
+	-y 5:2 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--legend-position east \
+	--units-exponent 0 --lower-limit 0 --upper-limit 100 --rigid \
+	DEF:a=$D/sensor-catarium-rh.rrd:rh:AVERAGE 'LINE1:a#cc0000:Catarium'

+ 16 - 0
web/rh-month.cgi

@@ -0,0 +1,16 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-30d' \
+	--end 'now' \
+	--title "Relative Humidity: MIN and MAX" \
+	-w 720 -h 600 \
+	-x DAY:1:DAY:5:DAY:5:0:%d:%m \
+	-y 5:2 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--legend-position east \
+	--units-exponent 0 --lower-limit 0 --upper-limit 100 --rigid \
+	DEF:alo=$D/sensor-catarium-rh.rrd:rh:MIN 'LINE1:alo#cc0000:Catarium' \
+	DEF:ahi=$D/sensor-catarium-rh.rrd:rh:MAX 'LINE1:ahi#cc0000'

+ 27 - 0
web/temp-12h.cgi

@@ -0,0 +1,27 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-12h' \
+	--end 'now' \
+	--title "Temperature" \
+	-w 720 -h 600 \
+	-x MINUTE:10:HOUR:1:HOUR:2:0:%H:%M \
+	-y 5:1 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--units-exponent 0 --lower-limit -20 --upper-limit 40 --rigid \
+	--legend-position east \
+	DEF:c=$D/sensor-catarium.rrd:temp:AVERAGE \
+	DEF:u=$D/sensor-ursarium.rrd:temp:AVERAGE \
+	DEF:b=$D/sensor-balcony.rrd:temp:AVERAGE \
+	DEF:o=$D/sensor-outside.rrd:temp:AVERAGE \
+	'AREA:c#cc7777' \
+	'AREA:u#7777cc' \
+	'AREA:b#cc77cc' \
+	'AREA:o#77cc77' \
+	'LINE2:c#cc0000:Catarium\n' \
+	'LINE2:u#0000cc:Ursarium\n' \
+	'LINE2:b#cc00cc:Balcony\n' \
+	'LINE2:o#00cc00:Outside' \
+	'HRULE:0#0000ff'

+ 27 - 0
web/temp-48h.cgi

@@ -0,0 +1,27 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-48h' \
+	--end 'now' \
+	--title "Temperature: Last 2 days" \
+	-w 720 -h 600 \
+	-x HOUR:1:HOUR:8:HOUR:8:0:%H:%M \
+	-y 5:1 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--units-exponent 0 --lower-limit -20 --upper-limit 40 --rigid \
+	--legend-position east \
+	DEF:c=$D/sensor-catarium.rrd:temp:AVERAGE \
+	DEF:u=$D/sensor-ursarium.rrd:temp:AVERAGE \
+	DEF:b=$D/sensor-balcony.rrd:temp:AVERAGE \
+	DEF:o=$D/sensor-outside.rrd:temp:AVERAGE \
+	'AREA:c#cc7777' \
+	'AREA:u#7777cc' \
+	'AREA:b#cc77cc' \
+	'AREA:o#77cc77' \
+	'LINE2:c#cc0000:Catarium\n' \
+	'LINE2:u#0000cc:Ursarium\n' \
+	'LINE2:b#cc00cc:Balcony\n' \
+	'LINE2:o#00cc00:Outside' \
+	'HRULE:0#0000ff'

+ 23 - 0
web/temp-month.cgi

@@ -0,0 +1,23 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-30d' \
+	--end 'now' \
+	--title "Temperature: MIN and MAX" \
+	-w 720 -h 600 \
+	-x DAY:1:DAY:5:DAY:5:0:%d:%m \
+	-y 5:1 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--units-exponent 0 --lower-limit -20 --upper-limit 40 --rigid \
+	--legend-position east \
+	DEF:alo=$D/sensor-ursarium.rrd:temp:MIN 'LINE2:alo#0000cc:Ursarium\n' \
+	DEF:ahi=$D/sensor-ursarium.rrd:temp:MAX 'LINE2:ahi#0000cc' \
+	DEF:dlo=$D/sensor-catarium.rrd:temp:MIN 'LINE2:dlo#cc0000:Catarium\n' \
+	DEF:dhi=$D/sensor-catarium.rrd:temp:MAX 'LINE2:dhi#cc0000' \
+	DEF:clo=$D/sensor-balcony.rrd:temp:MIN 'LINE2:clo#cc00cc:Balcony\n' \
+	DEF:chi=$D/sensor-balcony.rrd:temp:MAX 'LINE2:chi#cc00cc' \
+	DEF:blo=$D/sensor-outside.rrd:temp:MIN 'LINE2:blo#00cc00:Outside' \
+	DEF:bhi=$D/sensor-outside.rrd:temp:MAX 'LINE2:bhi#00cc00' \
+	'HRULE:0#0000ff'

+ 19 - 0
web/temp-quick.cgi

@@ -0,0 +1,19 @@
+#!/bin/sh
+echo "Content-type: image/png"
+echo
+D=/var/log/arexxd
+exec rrdtool graph - \
+	--start 'now-2h' \
+	--end 'now' \
+	--title "Temperature" \
+	-w 360 -h 200 \
+	-x MINUTE:10:MINUTE:30:MINUTE:30:0:%H:%M \
+	-y 5:1 \
+	--right-axis 1:0 --right-axis-format "%3.0lf" \
+	--units-exponent 0 --lower-limit -20 --upper-limit 40 --rigid \
+	--legend-position east \
+	DEF:a=$D/sensor-ursarium.rrd:temp:AVERAGE 'LINE2:a#0000cc:Ursarium\n' \
+	DEF:d=$D/sensor-catarium.rrd:temp:AVERAGE 'LINE2:d#cc0000:Catarium\n' \
+	DEF:c=$D/sensor-balcony.rrd:temp:AVERAGE 'LINE2:c#cc00cc:Balcony\n' \
+	DEF:b=$D/sensor-outside.rrd:temp:AVERAGE 'LINE2:b#00cc00:Outside' \
+	'HRULE:0#0000ff'