#!/usr/bin/perl
#
# Copyright 2001-2009 Daniel Klein, dan@klein.com.  All Rights Reserved.
# You may make unlimited copies of this code provided:
#    1) That this and other entire copyright notices are retained.
#    2) That neither the code nor any derivative code is sold for any reason
#	or amount.  If you want to sell it or a derivative work, email me and
#	we'll work out a royalty fee structure.
# 
# Please let me know that you're using it (you don't *have* to, but I'm curious
# about how it is being used!)
#
# COMPLETE DOCUMENTATION IS AT THE END OF THIS FILE, IN POD FORMAT
#
use constant VERSION =>
    '$Id: thermd,v 2.85 2014/08/24 15:21:31 root Exp root $';

require 5.7.3;				# To get safe signals
use strict;
use locale;
use Carp;
use Encode;
use POSIX qw(:termios_h :errno_h :locale_h :sys_wait_h strftime INT_MAX INT_MIN ceil floor);
use Fcntl qw(:DEFAULT :flock);
use FileHandle;
unless ($^O eq "MSWin32") {
    eval qq{ use Sys::Syslog; };
    }
use Sys::Hostname;
use Search::Dict;
use FindBin ();
use File::Basename ();
use File::Spec;
use Socket;
use Locale::Maketext;
Thermd::I18N->import();			# Until v3.x when we use Thermd::I18N
use MIME::Base64;
use HTTP::Date;				# Must download from CPAN
use Config::General qw(ParseConfig);	# Must download from CPAN
#
# Other modules are used inside of eval strings, so that they are not compiled
# at every invocation unless they are actually needed.  The modules used are:
# CGI, CGI::Carp, and Getopt::Long.
#
# See the POD documentation for a full list of optional modules, depending on
# features used.
#

#+
# Everything that you tune is in the config file - there are no longer any
# configuration variables here.
#+
#
# Set up initialization stuff, first
#
our $lh = Thermd::I18N->get_handle() or die "Cannot determine language...";
#$lh->fail_with('failure_handler_auto');
$Carp::Verbose = 1;
setlocale(LC_ALL, $lh->locale());	# Will get overridden in daemon

my $script = File::Basename::basename($0);
$SIG{HUP} = $SIG{INT} = $SIG{TERM} = \&get_kicked;

our ($opt_all_sensors, $opt_annotate,
     $opt_barometer,
     $opt_center, $opt_checkconfig, $opt_config, $opt_csv, $opt_current,
     $opt_daemon,
     $opt_email, $opt_epochtime,
     $opt_format, $opt_from,
     $opt_graph, $opt_help, $opt_height, $opt_hilo,
     $opt_i18n,
     $opt_list,
     $opt_nofork, $opt_nosmooth, $opt_nowarn,
     $opt_outfile,
     $opt_rainfall, $opt_raw, $opt_report,
     $opt_sesame, $opt_span,
     $opt_temperature, $opt_to,
     $opt_units,
     $opt_verbose, $opt_view,
     $opt_width, $opt_windspeed,
     %config, $is_cgi, $is_child, $date_from, $date_to, $email_regex,
     $ofd, $workbook, $worksheet,
     @pollers, @actions,
     %kids, %logfile);

END {
    if (!$is_child && keys %kids) {
	msg("err", "END Infanticide from PID $$ - $0 killing: @{[keys %kids]}");
	$SIG{CHLD} = "IGNORE";
	kill POSIX::SIGTERM(), keys %kids;
	}
    }

our %options = (
    all_sensors	=> \$opt_all_sensors,
    annotate    => \$opt_annotate,
    "barometer=s"=>\$opt_barometer,
    "center=s"	=> \$opt_center,
    checkconfig	=> \$opt_checkconfig,
    "config=s"	=> \$opt_config,
    csv		=> \$opt_csv,
    current	=> \$opt_current,
    daemon	=> \$opt_daemon,
    epochtime	=> \$opt_epochtime,
    email	=> \$opt_email,
    "format=s"	=> \$opt_format,
    "from=s"	=> \$opt_from,
    graph	=> \$opt_graph,
    "height=i"	=> \$opt_height,
    hilo	=> \$opt_hilo,
    help	=> \$opt_help,
    "i18n:s"	=> \$opt_i18n,
    list	=> \$opt_list,
    nofork	=> \$opt_nofork,
    nosmooth	=> \$opt_nosmooth,
    nowarn	=> \$opt_nowarn,
    "outfile=s"	=> \$opt_outfile,
    "span=s"	=> \$opt_span,
    "rainfall=s"=>\$opt_rainfall,
    raw		=> \$opt_raw,
    report	=> \$opt_report,
    "temperature=s"=>\$opt_temperature,
    "to=s"	=> \$opt_to,
    "units=s"	=> \$opt_units,
    verbose	=> \$opt_verbose,
    "view=s"	=> \$opt_view,
    "width=i"	=> \$opt_width,
    "windspeed=s"=>\$opt_windspeed,
    );

#
# This lists what switches are legal with which mode, used in check_opts
#
my @common = qw(config nowarn verbose);
my @unitspec = qw(barometer rainfall temperature units windspeed);
my @daterange = qw(center from span to);
my @daemon = qw(force nofork verbose);
my @report = qw(current raw epochtime format outfile view);
my @graph = qw(height width hilo nosmooth outfile view);
my @annotate = qw(current outfile view);
my @checkconfig = qw(email list);

#
# The @color_wheel is what is used when we don't explicitly specify a
# GraphColor attribute for a sensor.  The %color_map correlates a color
# name with an #RRGGBB value.  There are more colors in the %color_map
# than there are in the @color_wheel
#
my @color_wheel = qw(red lime blue purple orange teal fuschia olive aqua
		green pink yellow navy maroon black);

my %color_map =
	( red => "#FF0000",     teal => "#008080",   blue => "#0000FF",
	  fuschia => "#FF00FF", olive => "#808000",  aqua => "#00FFFF",
	  green => "#008000",   black => "#000000",  orange => "#FFA500",
	  purple => "#800080",  silver => "#C0C0C0", white => "#FFFFFF",
	  pink => "#FFC0CB",    yellow => "#FFFF00", lime => "#00FF00",
	  gray => "#808080",    navy => "#000080",   maroon => "#800000");

my %compass =	# Used to lookup TEMP08 values and convert them to numbers
	( N => 0,   NNE => 22.5,  NE => 45,  ENE => 67.5,
	  E => 90,  ESE => 112.5, SE => 135, SSE => 157.5,
	  S => 180, SSW => 202.5, SW => 225, WSW => 247.5,
	  W => 270, WNW => 292.5, NW => 315, NNW => 337.5 );

my @compass = map { $lh->maketext($_) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE);

#
# For an explanation, see http://sheepdogsoftware.co.uk/sc3wmw.htm or
# http://www.midondesign.com/Documents/Calculating%20Wind%20Speed%20and%20Direction.pdf
#
my %compass_lookup = (
    "2220" => 0,   "2200" => 22.5,  "2202" => 45,  "2002" => 67.5,
    "2022" => 90,  "0022" => 112.5, "0222" => 135, "0221" => 157.5,
    "2221" => 180, "2211" => 202.5, "2212" => 225, "2112" => 247.5,
    "2122" => 270, "1122" => 292.5, "1222" => 315, "1220" => 337.5,
    );

my %linetype_map = (solid => 1, dashed => 2, dotted => 3, dotdashed => 4);

my %units = (English => {
		# inHg is hardwired for English
		barometer => "inHg", temperature => "F",
		rainfall => "Inches", windspeed => "MPH",
		},
	     Metric => {
		# ...but look up scale for Metric
		barometer => $lh->baroscale, temperature => "C",
		rainfall => "mm", windspeed => "KPH",
		},
	    );

my %l_month = ($lh->maketext("jan") => 1,  $lh->maketext("feb") => 2,
               $lh->maketext("mar") => 3,  $lh->maketext("apr") => 4,
               $lh->maketext("may") => 5,  $lh->maketext("jun") => 6,
               $lh->maketext("jul") => 7,  $lh->maketext("aug") => 8,
               $lh->maketext("sep") => 9,  $lh->maketext("oct") => 10,
               $lh->maketext("nov") => 11, $lh->maketext("dec") => 12);

my %e_month = ("jan" => 1,  "feb" => 2,  "mar" => 3,  "apr" => 4,
	       "may" => 5,  "jun" => 6,  "jul" => 7,  "aug" => 8,
	       "sep" => 9,  "oct" => 10, "nov" => 11, "dec" => 12);

#
# The list of legal keys for Weather Underground
#
my %wunder_types = map { ($_ => 1)} qw(
		humidity tempf winddir windspeedmph windgustmph
		dailyrainin baromin soiltempf soilmoisture
		indoortempf indoorhumidity
		);

#
# Thermocouple ranges, from http://srdata.nist.gov/its90/main/
#
my %min_max = (
    B => [    0, 1820 ],
    E => [ -200, 1000 ],
    J => [ -210, 1200 ],
    K => [ -270, 1372 ],
    N => [ -270, 1300 ],
    R => [  -50, 1768 ],
    S => [  -50, 1768 ],
    T => [ -270,  400 ],
    );

#
# Constants for the Veris H8030 and H8031 (via private communication from
# h8030_h8031_i0c2_Z205220-0C.pdf, document not found on the web), and for
# the H8035 and H8036, http://www.veris.com/modbus/8035-6MB_implementation.pdf
#
my %veris = (
    40001 => {
	type        => "KWH",
	_scale	    => "KWh",
	_label	    => "KiloWatt Hours",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.01562, 300  => 0.0625,
		},
	    "H8035/H8036" => {
		100  => 7.8125e-3, 300  => 0.03125, 400  => 0.03125,
		800  => 0.0625,    1600 => .125,    2400 => 0.25,
		},
	    },
	description => "Energy Consumption, LSW",
	},
    40002 => {
	type        => "KWH",
	_scale	    => "KWh",
	_label	    => "KiloWatt Hours",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 1024,  300  => 4096,
		},
	    "H8035/H8036" => {
		100  => 512,  300  => 2048, 400  => 2048,
		800  => 4096, 1600 => 8192, 2400 => 16384,
		},
	    },
	description => "Energy Consumption, MSW",
	},
    40003 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.008, 300  => 0.032,
		},
	    "H8035/H8036" => {
		100  => 0.004, 300  => 0.016, 400  => 0.016,
		800  => 0.032, 1600 => 0.064, 2400 => 0.128,
		},
	    },
	description => "Demand (power)",
	},
    40004 => {
	type        => "VAR",
	_scale	    => "VAR",
	_label	    => "Volt Amps R",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.008, 300  => 0.032,
		},
	    "H8035/H8036" => {
		100  => 0.004, 300  => 0.016, 400  => 0.016,
		800  => 0.032, 1600 => 0.064, 2400 => 0.128,
		},
	    },
	description => "Reactive Power",
	},
    40005 => {
	type        => "VA",
	_scale	    => "VA",
	_label	    => "Volt Amps",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.008, 300  => 0.032,
		},
	    "H8035/H8036" => {
		100  => 0.004, 300  => 0.016, 400  => 0.016,
		800  => 0.032, 1600 => 0.064, 2400 => 0.128,
		},
	    },
	description => "Apparent Power",
	},
    40006 => {
	type        => "Power-Factor",
	_scale	    => "PF",
	_label	    => "Power Factor",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.0518e-5, 300  => 3.0518e-5,
		},
	    "H8035/H8036" => {
		100  => 3.0518e-5, 300  => 3.0518e-5, 400  => 3.0518e-5,
		800  => 3.0518e-5, 1600 => 3.0518e-5, 2400 => 3.0518e-5,
		},
	    },
	description => "Power Factor",
	},
    40007 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.03125, 300  => 0.03125,
		},
	    "H8035/H8036" => {
		100  => 0.03125, 300  => 0.03125, 400  => 0.03125,
		800  => 0.03125, 1600 => 0.03125, 2400 => 0.03125,
		},
	    },
	description => "Voltage, line to line",
	},
    40008 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.015625, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 0.015625, 300  => 0.015625, 400  => 0.015625,
		800  => 0.015625, 1600 => 0.015625, 2400 => 0.015625,
		},
	    },
	description => "Voltage, line to neutral",
	},
    40009 => {
	type        => "Amps",
	_scale	    => "A",
	_label	    => "Amps",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.9063e-3, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 3.9063e-3, 300  => 0.015625, 400  => 0.015625,
		800  => 0.03125,   1600 => 0.0625,   2400 => 0.125,
		},
	    },
	description => "Current",
	},
    40010 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.001, 300  => 0.004,
		},
	    "H8035/H8036" => {
		100  => 0.001, 300  => 0.004, 400  => 0.004,
		800  => 0.008, 1600 => 0.016, 2400 => 0.032,
		},
	    },
	description => "Demand (power), phase A",
	},
    40011 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.001, 300  => 0.004,
		},
	    "H8035/H8036" => {
		100  => 0.001, 300  => 0.004, 400  => 0.004,
		800  => 0.008, 1600 => 0.016, 2400 => 0.032,
		},
	    },
	description => "Demand (power), phase B",
	},
    40012 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.001, 300  => 0.004,
		},
	    "H8035/H8036" => {
		100  => 0.001, 300  => 0.004, 400  => 0.004,
		800  => 0.008, 1600 => 0.016, 2400 => 0.032,
		},
	    },
	description => "Demand (power), phase C",
	},
    40013 => {
	type        => "Power-Factor",
	_scale	    => "PF",
	_label	    => "Power Factor",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.0518e-5, 300  => 3.0518e-5,
		},
	    "H8035/H8036" => {
		100  => 3.0518e-5, 300  => 3.0518e-5, 400  => 3.0518e-5,
		800  => 3.0518e-5, 1600 => 3.0518e-5, 2400 => 3.0518e-5,
		},
	    },
	description => "Power Factor, phase A",
	},
    40014 => {
	type        => "Power-Factor",
	_scale	    => "PF",
	_label	    => "Power Factor",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.0518e-5, 300  => 3.0518e-5,
		},
	    "H8035/H8036" => {
		100  => 3.0518e-5, 300  => 3.0518e-5, 400  => 3.0518e-5,
		800  => 3.0518e-5, 1600 => 3.0518e-5, 2400 => 3.0518e-5,
		},
	    },
	description => "Power Factor, phase B",
	},
    40015 => {
	type        => "Power-Factor",
	_scale	    => "PF",
	_label	    => "Power Factor",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.0518e-5, 300  => 3.0518e-5,
		},
	    "H8035/H8036" => {
		100  => 3.0518e-5, 300  => 3.0518e-5, 400  => 3.0518e-5,
		800  => 3.0518e-5, 1600 => 3.0518e-5, 2400 => 3.0518e-5,
		},
	    },
	description => "Power Factor, phase C",
	},
    40016 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.03125, 300  => 0.03125,
		},
	    "H8035/H8036" => {
		100  => 0.03125, 300  => 0.03125, 400  => 0.03125,
		800  => 0.03125, 1600 => 0.03125, 2400 => 0.03125,
		},
	    },
	description => "Voltage, phase A-B",
	},
    40017 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.03125, 300  => 0.03125,
		},
	    "H8035/H8036" => {
		100  => 0.03125, 300  => 0.03125, 400  => 0.03125,
		800  => 0.03125, 1600 => 0.03125, 2400 => 0.03125,
		},
	    },
	description => "Voltage, phase B-C",
	},
    40018 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.03125, 300  => 0.03125,
		},
	    "H8035/H8036" => {
		100  => 0.03125, 300  => 0.03125, 400  => 0.03125,
		800  => 0.03125, 1600 => 0.03125, 2400 => 0.03125,
		},
	    },
	description => "Voltage, phase A-C",
	},
    40019 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.015625, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 0.015625, 300  => 0.015625, 400  => 0.015625,
		800  => 0.015625, 1600 => 0.015625, 2400 => 0.015625,
		},
	    },
	description => "Voltage, phase A-N",
	},
    40020 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.015625, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 0.015625, 300  => 0.015625, 400  => 0.015625,
		800  => 0.015625, 1600 => 0.015625, 2400 => 0.015625,
		},
	    },
	description => "Voltage, phase B-N",
	},
    40021 => {
	type        => "Volts",
	_scale	    => "V",
	_label	    => "Volts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.015625, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 0.015625, 300  => 0.015625, 400  => 0.015625,
		800  => 0.015625, 1600 => 0.015625, 2400 => 0.015625,
		},
	    },
	description => "Voltage, phase C-N",
	},
    40022 => {
	type        => "Amps",
	_scale	    => "A",
	_label	    => "Amps",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.9063e-3, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 3.9063e-3, 300  => 0.015625, 400  => 0.015625,
		800  => 0.03125,   1600 => 0.0625,   2400 => 0.125,
		},
	    },
	description => "Current, phase A",
	},
    40023 => {
	type        => "Amps",
	_scale	    => "A",
	_label	    => "Amps",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.9063e-3, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 3.9063e-3, 300  => 0.015625, 400  => 0.015625,
		800  => 0.03125,   1600 => 0.0625,   2400 => 0.125,
		},
	    },
	description => "Current, phase B",
	},
    40024 => {
	type        => "Amps",
	_scale	    => "A",
	_label	    => "Amps",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 3.9063e-3, 300  => 0.015625,
		},
	    "H8035/H8036" => {
		100  => 3.9063e-3, 300  => 0.015625, 400  => 0.015625,
		800  => 0.03125,   1600 => 0.0625,   2400 => 0.125,
		},
	    },
	description => "Current, phase C",
	},
    40025 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.008, 300  => 0.032,
		},
	    "H8035/H8036" => {
		100  => 0.004, 300  => 0.016, 400  => 0.016,
		800  => 0.032, 1600 => 0.064, 2400 => 0.128,
		},
	    },
	description => "Average Demand",
	},
    40026 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.008, 300  => 0.032,
		},
	    "H8035/H8036" => {
		100  => 0.004, 300  => 0.016, 400  => 0.016,
		800  => 0.032, 1600 => 0.064, 2400 => 0.128,
		},
	    },
	description => "Minimum Demand",
	},
    40027 => {
	type        => "KWatts",
	_scale	    => "KW",
	_label	    => "KiloWatts",
	multiplier  => {
	    "H8030/H8031" => {
		100  => 0.008, 300  => 0.032,
		},
	    "H8035/H8036" => {
		100  => 0.004, 300  => 0.016, 400  => 0.016,
		800  => 0.032, 1600 => 0.064, 2400 => 0.128,
		},
	    },
	description => "Maximum Demand",
	},
    );

# Types and commands for the Newport/Omega iServer family
my %newport = (
    ibthx => {
	regex  => qr/^SR(TC|Hi|H2|DC2)$/,
	errmsg => "SRTC, SRHi, SRH2, or SRDC2",
	type   => {
	    SRTC  => "temperature",
	    SRHi  => "barometer",
	    SRH2  => "humidity",
	    SRDC2 => "dewpoint",
	    },
	},
    ibtx => {
	regex  => qr/^SR(TC|Hi|H2|DC2)$/,
	errmsg => "SRTC, SRHi, SRH2, or SRDC2",
	type   => {
	    SRTC  => "temperature",
	    SRHi  => "barometer",
	    SRH2  => "humidity",
	    SRDC2 => "dewpoint",
	    },
	},
    "ibtx-m" => {
	regex  => qr/^SR(TC|Hi)$/,
	errmsg => "SRTC or SRHi",
	type   => {
	    SRTC  => "temperature",
	    SRHi  => "barometer",
	    },
	},
    "iptx-d" => {
	regex  => qr/^SR(TC|Hb)$/,
	errmsg => "SRTC or SRHb",
	type   => {
	    SRTC   => "temperature",
	    SRHb => "pressure",
	    },
	},
    "iptx-w" => {
	regex  => qr/^SR(TC|Hb)$/,
	errmsg => "SRTC or SRHb",
	type   => {
	    SRTC   => "temperature",
	    SRHb => "pressure",
	    },
	},
    itcx => {
	regex  => qr/^SR[THD]C$/,
	errmsg => "SRTC, SRHC, or SRDC",
	type   => {
	    SRTC => "temperature",
	    SRHC => "temperature",
	    SRDC => "temperature",
	    },
	},
    "ithx-m" => {
	regex  => qr/^SR[THD]$/,
	errmsg => "SRT, SRH, or SRD",
	type   => {
	    SRT  => "temperature",
	    SRH  => "humidity",
	    SRD  => "dewpoint",
	    },
	},
    "ithx-2" => {
	regex  => qr/^SR(TC|H|DC)2?$/,
	errmsg => "SRTC, SRTC2, SRH, SRH2, SRDC, or SRDC2",
	type   => {
	    SRTC  => "temperature",
	    SRTC2 => "temperature",
	    SRH  => "humidity",
	    SRH2 => "humidity",
	    SRDC  => "dewpoint",
	    SRDC2 => "dewpoint",
	    },
	},
    "ithx-w" => {
	regex  => qr/^SR(TC|H|DC)2?$/,
	errmsg => "SRTC, SRTC2, SRH, SRH2, SRDC, or SRDC2",
	type   => {
	    SRTC  => "temperature",
	    SRTC2 => "temperature",
	    SRH  => "humidity",
	    SRH2 => "humidity",
	    SRDC  => "dewpoint",
	    SRDC2 => "dewpoint",
	    },
	},
    );

use constant PROGRAM_START => time;

my $number     = qr/[+-]?(?:\d+(?:[.,]\d*)?|[.,]\d+)/;
my $numeric    = qr/^$number$/;
my $optnumeric = qr/^(?:$numeric)?$/;

my ($dbh, $sth);	# Only used if we have LogFormat SQL

sub usage {
    warn @_ if @_;
    die <<'==END==';
Usage - can run either as a logging daemon, reporting script, image annotator,
	graph generator, or CGI script.  You must pick one :-)

ALL FLAGS MAY BE ABBREVIATED!

Daemon: thermd -daemon [-config file] [-nofork [-verbose]]
	-config file	Use this as a config file (default=/etc/thermd.conf)
	-force		Kill the existing daemon, if one is found
        -nofork         Don't fork to background (usually only for debugging)
        -verbose        Verbose output (definitely for debugging :-)

Report: thermd -report [report args] [range-value args]
        -format type    One of CSV (comma separated values), TSV (tab
			separated values), XML, CER (Eddy Common Event
			Record) or Excel.
			Default format is TSV
        -epochtime      When printing time, print Unix time(2), instead of
			the default human-readable time
	-current	Print current values only
	-raw		Print all current values, in raw form

Annotate: thermd -annotate [range-value args] 

Graph: thermd -graph [graph args] [range-value args]
	-width pixels	Override the width of the graph.  If not specified
			here, default value comes from the config file, and
			that default value is 750.
	-height pixels	Override the height of the graph.  If not specified
			here, default value comes from the config file, and
			that default value is 300.
	-hilo		Graph 24-hour extremes instead of individual datapoints
        -nosmooth	When graphs get dense (typ. > 1 month of data), graph
			data is automatically smoothed.  This switch defeats
			that function.

    Common range-value args
	-config file	Use this as a config file (default=/etc/thermd.conf)
        -outfile file   Output graph/table to this file (instead of STDOUT)
        -view name      Show a named view (defined in config file, default=all)
	-all_sensors	Ignore/override any -view, and show *all* sensors
	-nowarn		Do not print configuration warnings
	-units {English|Metric}
			Use English/Metric units (override config file)
        -temperature {C|F}
			Show temperature values as (override config and -units)
        -windspeed {mph|kph|mps|knots|knot}
			Show windspeed values as (override config and -units)
        -rainfall {inches|mm}
			Show rainfall values as (override config and -units)
			(also affects MaxBotix "range" values)
        -barometer {inHg|mmHg|hPa|kPa|millibar|mBar}
			Show barometer values as (override config and -units)
        -from daytime   From date/time *
        -to daytime     To date/time *
        -center daytime Center chart on date/time *
        -span reltime	Span for centered chart **

        * Date/time values may be specified as:
            absolute (ex: "20030228" or "20030228T1535" - actually, any
                format allowed by HTTP::Date)
            relative (ex: 3d, -2.5h, 3w, +5m3d1h, etc., suffixes are y=>year,
		m=>month, w=>week, d=>day, h=>hour)  Note: sign is highly
		significant, and changes what it is relative to...
            named (i.e., "now", "today", "yesterday"), considered absolute
		times as far as discussion below is considered

            If -from and -to are both missing, we assume: -from -1d -to now
            If -from is specified but -to is missing, we assume: -to now
            If -from is missing but -to is specified, we assume that -from
		is 1d before -from
            If -from is relative and -to is absolute, we assume that -from
                is relative to now and comes before -to (which means that
                sign is ignored!), so that if today is 20030327:
		    -from -1m -to 20030228	 means the same as:
		    -from 20030227 -to 20030228
                Since sign is ignored here, you could also say:
		    -from 1m -to 20030228	or:
		    -from 1m -to yesterday	or:
		    -from +1m -to 20030228	(counterituitive, but legal)
            If -from is absolute and -to is relative, we assume that -to
		is relative to -from and comes after -from, so that:
		    -from 20030228 -to 1d	means the same as:
		    -from 20030228 -to +1d	means the same as:
		    -from 20030228 -to 20030301
		However, there is one special case: if -to is negative, it
		is interpreted as relative to "now", so:
		    -from 20030228 -to -1d	(from 20030228 to yesterday)
            If both -from and -to are relative, then both are considered to
		be relative to "now" (with one exception), so that:
		    -from -5d -to -4d		means the same as:
		    -from +5d -to 4d		ignore sign on -from, same as:
		    -from 5d -to 4d		but:
		    -from -4d -to -5d		illegal reverse range!
		There is one special case, if -to is positive (i.e., +time),
		then it is relative to -from, so:
		    -from -5d -to +1d		means the same as:
		    -from -5d -to -4d

        ** Span values may only be relative, see above
            If -center is specified, but -span is missing, then -span
            defaults to 1d (i.e., from 12h before to 12h after -center)

Test: thermd -checkconfig [-config file]
	-checkconfig	Check the validity of the config file, and exit
	-config file	Use this as a config file (default=/etc/thermd.conf)
	-verbose	Output a full config file, with all defaults expressed
	-email		Also sends an email to all alarm addresses - useful
			for checking your email configuration
	-list		Prints a list of logfiles used by thermd (overrides
			-verbose, and only prints the list)

Debug: thermd -i18n
	-i18n [lang]	Report on internationalization database

CGI: put it in your CGI area, and the rest should happen automatically :-)
==END==
    }

##############################################################################
# Initialization: If we are called as a CGI script, load CGI.pm and
# either output a form or output a graph.  If we are called from the
# shell, the act on the command-line switches.  NOTE: When we output
# a graph from a CGI script, we output a MIME header and then fake
# things up to look like we were called from the shell.
##############################################################################
if ($ENV{REQUEST_METHOD}) {
    eval qq{
	use CGI qw(-nosticky :standard :netscape );
	use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
	};
    die $@ if $@;
    $is_cgi = 1;
    $opt_config = param('config') || "/etc/thermd.conf";
    read_config();
    $opt_units = param('units') || $config{displayin};
    $opt_barometer = $units{$opt_units}{barometer};
    $opt_rainfall = $units{$opt_units}{rainfall};
    $opt_temperature = $units{$opt_units}{temperature};
    $opt_windspeed = $units{$opt_units}{windspeed};
    $opt_from = param('from') || "-1d";
    $opt_to = param('to') || "now";
    $opt_view = param('view');
    $opt_hilo = param('hilo');
    check_opts("cgi", @common, @daterange, @unitspec, @annotate, @report, @graph);
    parse_and_assign_dates();
    do_cgi();
    }
else {
    eval qq{ use Getopt::Long; };
    die $@ if $@;
    $Getopt::Long::autoabbrev = 1;

    GetOptions(%options);
    die "Use -help for a full list of options\n"	if $Getopt::Long::error;
    if ($opt_verbose || $opt_checkconfig) {
	print "$script V@{[(split(/\s+/, VERSION))[2,3]]}\n\n";
	}
    usage()	if $opt_help;
    die "You must choose exactly one of -daemon, -report, -annotate or -graph\nUse -help for a full list of options\n"
	unless $opt_daemon + $opt_report + $opt_annotate + $opt_graph + $opt_checkconfig + defined($opt_i18n) == 1;
    die "Please use '-format csv' instead of '-csv'"	if $opt_csv;

    $| = $opt_verbose;
    $opt_config ||= "/etc/thermd.conf";
    #
    # If we are going to be the daemon, reset the locale to "C", so that
    # all logging and interprocess communication is in "C", and not the
    # LC_NUMERIC format.  Use of Thermd::I18N is unchanged, though.
    #
    setlocale(LC_ALL, "C")	if $opt_daemon;
    read_config()		unless defined $opt_i18n;

    if ($opt_daemon) {
	check_opts("daemon", @common, @daemon);
	#
	# After the config file is fully verified, do the main daemon job
	#
	do_daemon();
	}
    elsif ($opt_report) {
	check_opts("report", @common, @daterange, @unitspec, @report);

	$opt_format = lc($opt_format || "tsv");
	die "Unrecognized -format $opt_format.  Use tsv, csv, excel, xml, or cer"
	    unless $opt_format =~ /^(tsv|csv|xml|excel|cer|timeplot)$/;

	if ($opt_format eq "excel") {
	    eval qq{ use Spreadsheet::WriteExcel; };
	    die $@ if $@;
	    }
	elsif ($opt_format eq "timeplot") {
	    $opt_epochtime = 1;
	    }
	elsif ($opt_format eq "xml") {
	    eval qq{ use XML::Simple; };
	    die $@ if $@;
	    }
	elsif ($opt_format eq "cer") {
	    die "You must use '-current' when using '-format cer'\n"
		unless $opt_current;
	    die "Format 'cer' is under development, and is not operational yet";
	    eval qq{ use XML::Simple; };
	    die $@ if $@;
	    }

	if ($opt_raw) {
	    my ($time, @lines) = read_current_values();
	    if (defined $time) {
		for my $line (@lines) {
		    my ($val, $units, $name) = split /\t/, $line, 3;
		    $val = sprintf "%.3f", $val;	# Internationalize
		    $line = join("\t", $val, $units, $name);
		    my ($n, $k) = split '@', $name;
		    if (exists $config{collector}{$k}{sensor}) {
			$line .= "\t$config{collector}{$k}{sensor}{$n}{name}";
			}
		    else {
			$line .= "\t$config{collector}{$k}{actuator}{$n}{name}";
		    }
		    }
		print join("\n", strftime("%c",localtime($time)), @lines), "\n";
		}
	    else {
		print $lh->maketext("The logging daemon does not seem to be running\n");
		}
	    }
	elsif ($opt_current) {
	    $date_from = $date_to = PROGRAM_START();
	    do_report(collect_data());
	    }
	else {
	    parse_and_assign_dates();
	    do_report(collect_data());
	    }
	}
    elsif ($opt_annotate) {
        check_opts("annotate", @common, @unitspec, @annotate);
        parse_and_assign_dates();
	if ($config{view}{$opt_view}{type} eq "graph") {
	    die "View '$opt_view' is an graph: use -graph, not -annotate\n";
	    }
        do_annotate();
        }
    elsif ($opt_graph) {
	check_opts("graph", @common, @daterange, @unitspec, @graph);
	parse_and_assign_dates();
	if ($config{view}{$opt_view}{type} eq "image") {
	    die "View '$opt_view' is an image: use -annotate, not -graph\n";
	    }
	do_graph(collect_data($config{view}{$opt_view}{cliplo},
	    $config{view}{$opt_view}{cliphi}));
	}
    elsif ($opt_checkconfig) {
	check_opts("checkconfig", @common, @checkconfig);
	if ($opt_list) {
	    my @list;
	    print "CONFIG\t$opt_config\n";
	    if ($config{logformat} eq "text") {
		print "LOGDIR\t$config{logwrite}\n";
		}
	    elsif ($config{logformat} eq "sql") {
		print "LOGSQL\t$config{_sql_type} $config{_database} $config{_db_auth}\n";
		}
	    else {
		die "Unknown LogFormat $config{logformat}";
		}
	    for my $k (keys %{ $config{collector} }) {
		for my $n (keys %{ $config{collector}{$k}{sensor} }) {
		    push @list, "$config{collector}{$k}{sensor}{$n}{logfile}\n";
		    }
		for my $n (keys %{ $config{collector}{$k}{actuator} }) {
		    push @list, "$config{collector}{$k}{actuator}{$n}{logfile}\n";
		    }
		}
	    print "LOGFILE\t$_"	for sort @list;
	    }
	else {
	    dump_config();
	    send_test_emails()	if $opt_email;
	    }
	}
    elsif (defined $opt_i18n) {
	check_i18n();
	}
    else {
	die "Shouldn't get here!";
	}
    }
exit 0;


##############################################################################
#                             Daemon
##############################################################################

my (@my_select_buffer, $next_log_time, $rescan_file_descriptors);

sub do_daemon {
    my ($rin, $rout, %rfd, @rfd, $now, $fileno, $k, $ak, $n, $val, $fd, $count,
	$collector, $sensor, $extra, $alter_ego);

    #
    # Fork and detach...  We use POSIX::_exit so that we don't go through
    # Perl's exit handlers and shutdown routines.
    #
    unless ($opt_nofork) {
	print "Forking and backgrounding...\n"	if $opt_verbose;
	my $pid = fork;
	if (!defined $pid) {
	    die "Couldn't fork - $!\n";
	    }
	elsif ($pid) {
	    close STDIN;
	    close STDOUT;
	    close STDERR;
	    POSIX::_exit(0);		# Parent exits, child lives on
	    }
	else {
	    print "Daemon child PID == $$\n"	if $opt_verbose;
	    POSIX::setsid()		or die "Couldn't start new session\n";
	    msg("notice", "Backgrounded...");
	    }
	}
    #
    # Store the current PID (child, or -nofork) into the PID file
    #
    unless ($config{pidfile} eq File::Spec->devnull()) {
	$fd = $config{_pidfile_fd};
	truncate $fd, 0;
	print $fd "$$\n";
	# ...and keep the file open to stay locked...
	}
    #
    # Fork off all the sub-processes and handle their possible demise
    #
    $SIG{CHLD} = \&child_died;
    for my $poller (@pollers) {
	$poller->();
	}
    #
    # Then do any queued actions (like opening and closing switches)
    #
    for my $action (@actions) {
	$action->();
	}

    #
    # And finally get ready for the main loop
    #
    # Before looping, go to sleep for 5 seconds before collecting any data (to
    # give collectors time to read), then calculate the next time to log data.
    #
    print "Running...\n"				if $opt_nofork;
    print "Waiting 5 seconds before polling...\n"	if $opt_verbose;
    sleep 5;
    $next_log_time = find_next_log_time();
    #
    # Main daemon loop.  The use of "select" precludes the use of buffered
    # I/O, so we can't use getline - so beware the funky code below...  The
    # call to my_select is non-blocking - it will return any FDs that have a
    # line of data.  We shuffle through those, see if it is at or past time to 
    # to log (logging if necessary), and then lather, rinse, repeat.
    #
    $rescan_file_descriptors = 1;
    FOREVER: while (1) {
	#
	# Start by building bit vectors of all the collectors, so we can read
	# from them as data becomes available.  Also make an association between
	# the fileno and the collector name (so we know what we are reading when
	# we read it).  We need to do this every time, in case a child dies and
	# winds up with a new file descriptor.
	#
	if ($rescan_file_descriptors) {
	    $rin = '';
	    %rfd = ();
	    for my $k (keys %{ $config{collector} }) {
		$collector = $config{collector}{$k};
		#
		# Read-only collectors are (obviously) not polled.  Individual
		# underground collectors handled by a single wunderground_group
		# collector.  Derived sensors are also not polled.
		#
		next if $collector->{readonly};
		next if $collector->{type} eq "wunderground";
		next if $collector->{type} eq "derived";
		vec($rin, $collector->{_fd}->fileno, 1) = 1;
		$rfd{$collector->{_fd}->fileno} = $k;
		}
	    @rfd = keys %rfd;
	    $rescan_file_descriptors = 0;
	    my_select($rout=$rin, \%rfd, 1);
	    }
	else {
	    my_select($rout=$rin, \%rfd, 0);
	    }

	$now = time;
	$count = 0;
	COLLECTOR: while ($fileno = shift @rfd) {
	    #
	    # Rotate though collectors, in case of timeout, but make sure that
	    # when we finish the list, we break and start at the right place
	    #
	    if (++$count > scalar keys %rfd) {
		unshift @rfd, $fileno;
		last COLLECTOR;
		}
	    else {
		push @rfd, $fileno;
		}
	    $k = $rfd{$fileno};
	    $collector = $config{collector}{$k};
	    next COLLECTOR unless vec($rout, $fileno, 1);
	    printf "   %s: %d line%s\n", $k,
		scalar @{$my_select_buffer[$fileno]},
		@{$my_select_buffer[$fileno]} == 1 ? "" : "s"
		    if $opt_verbose;
	    LINE: while (@{$my_select_buffer[$fileno]}) {
		$_ = shift @{$my_select_buffer[$fileno]};
#nv#		print "$now $k: $_\n"	if $opt_verbose;
		#
		# All polled collectors are generic, because the poller can
		# reformat the data.  All streaming collectors are parsed
		# here, since there is no poller to reformat their data
		#
		if ($collector->{_datatype} eq "generic") {
		    # Lines look like "NN 19.625" (where NN is sensor number)
		    ($n, $val) = split;
		    }
		elsif ($collector->{_datatype} eq "wunderground_group") {
		    # Lines look like "NN 19.625 KK" (KK is real collector name)
		    # See below for more details on $alter_ego
		    ($n, $val, $ak) = split /\s+/, $_, 3;
		    $alter_ego = $collector;
		    $collector = $config{collector}{$ak};
		    }
		elsif ($collector->{_datatype} eq "qk145") {
		    next LINE unless /^\d/;		# Skip non-temp lines
		    # Lines look like "1 0081.95"
		    ($n, $val) = split;
		    # This seems redundant, but Heison has shown it's needed
		    next unless $n =~ /^[1-4]$/ && $val =~ /\d+\.\d+/;
		    }
		elsif ($collector->{_datatype} eq "vk011") {
		    next LINE unless /^Sensor/;		# Skip non-temp lines
		    # Lines look like "Sensor 1 +81.95 DegC" (and more?)
		    ($n, $val) = /Sensor\s+(\d)\s+([+-]?[\d.]+)/;
		    }
		elsif ($collector->{_datatype} eq "maxbotix") {
		    # Lines look like "R086"
		    next unless /R(\d\d\d)/;
		    $n = "Range";
		    $val = $1;
		    }
		elsif ($collector->{_datatype} eq "temp08") {
		    # Lines are complicated...
		    ($n, $val, $extra) = ();
		    if (/^Temp .*\[([\dA-F]{16})\]=(-?[\d.]+)C/) {
			($n, $val) = ($1, $2);
			$n = hunt_for_sensor($collector, $n, "temperature");
			next LINE unless defined($n);
			}
		    elsif (/^Humidity .*\[([\dA-F]{16})\]=(\d+)%/) {
			($n, $val) = ($1, $2);
			$n = hunt_for_sensor($collector, $n, "humidity");
			next LINE unless defined($n);
			}
		    elsif (/^Barometer .*\[([\dA-F]{16})\]=(\d+) inHg/) {
			($n, $val) = ($1, $2);
			$n = hunt_for_sensor($collector, $n, "barometer");
			next LINE unless defined($n);
			}
		    elsif (/^Rain .*\[([\dA-F]{16})\]=([\d.]+) Inch/) {
			($n, $val) = ($1, $2);
			$n = hunt_for_sensor($collector, $n, "rain");
			next LINE unless defined($n);
			}
		    elsif (/^Lightning .*\[([\dA-F]{16})\]=([\d.]+)/) {
			($n, $val) = ($1, $2);
			$n = hunt_for_sensor($collector, $n, "lightning");
			next LINE unless defined($n);
			}
		    elsif (/^Wind Dirn\[([\dA-F]{16})\]=(\w+)/) {
			($n, $val) = ($1, $compass{$2});
			$n = hunt_for_sensor($collector, $n, "direction");
			next LINE unless defined($n);
			}
		    elsif (/^Wind Speed\[([\dA-F]{16})\]=(\d+).*=\s+(\d+)/) {
			($n, $val, $extra) = ($1, $2, $3);
			$n = hunt_for_sensor($collector, $n, "speed");
			next LINE unless defined($n);
			#
			# NOTE!  Nothing in life (or programming) is simple.
			# Every sensor (except one!) gives one reading on one
			# line.  For the wind speed and gust sensor, we extract
			# both values, and record the speed.  We then construct
			# a fake gust line, push it into the input stream, and
			# reexecute the loop - where the data is picked up on
			# a later loop on the "fabricated data" line below.
			#
			if (defined $extra) {
			    my $N = $n;
			    $N =~ s/\.\w$//;	# remove HUNTed subindex
			    push @{$my_select_buffer[$fileno]}, "Wind Gust[$N]=$extra";
			    }
			}
		    #
		    # This is fabricated data - see above for details
		    #
		    elsif (/^Wind Gust\[([\dA-F]{16})\]=(\d+)/) {
			($n, $val) = ($1, $2);
			$n = hunt_for_sensor($collector, $n, "gust");
			next LINE unless defined($n);
			}
		    }
		elsif ($collector->{_datatype} eq "snmp_trap") {
		    # Data looks like "IP_NUM OID = val\tOID = val\t..."
		    #
		    # All SNMP traps come from a single snmp_trap collector,
		    # but they reflect events on potentially multiple other
		    # collectors.  When we get a trap, split the payload
		    # until we find a match in the snmp_trap collector's
		    # trap_lookup hash, then switch to that collector and
		    # sensor number (we'll switch back to our true identity
		    # at the end of this loop).
		    #
		    my ($addr, $oid, $v);
		    ($addr) = /([\d.]+)/;
		    s/[\d.]+\s+//;
		    PAYLOAD: for my $pair (split /\t/) {
			($oid, $v) = split / = /, $pair;
			if (exists $collector->{_trap_lookup}{"$addr:$oid"}) {
			    my $trap = $collector->{_trap_lookup}{"$addr:$oid"};
			    $alter_ego = $collector;
			    $collector = $trap->{collector};
			    $n = $trap->{n};
			    $val =
				$v == $collector->{sensor}{$n}{snmponvalue}
				    ? 1 : 0;
			    last PAYLOAD;
			    }
			}
		    #
		    # Sometimes we might get a spurious or misdirected trap
		    #
		    next LINE unless defined $alter_ego;
		    }
		else {
		    die "Unexpected collector $k datatype: $collector->{_datatype}"
		    }

		unless (defined $val) {
		    print "\t?? Unable to parse data\n"	if $opt_verbose;
		    next LINE;		# There weren't two fields (?)
		    }
		#
		# Throw out bogus values (and log them) and ignore ReadOnly
		# sensors.  Then tweak the values if necessary using AdjustBy
		# or OnValue/OffValue
		#
		$sensor = $collector->{sensor}{$n};
		next LINE if $sensor->{readonly};
		if ($sensor->{_scale} eq "F" && $val >= $sensor->{_max_f}) {
		    msg("err", "OverTemp $sensor->{_nk}: $val > $sensor->{_max_f}");
		    next LINE;
		    }
		elsif ($sensor->{_scale} eq "C" && $val >= $sensor->{_max_c}) {
		    msg("err", "OverTemp $sensor->{_nk}: $val > $sensor->{_max_c}");
		    next LINE;
		    }
		elsif ($sensor->{_scale} eq "F" && $val <= $sensor->{_min_f}) {
		    msg("err", "UnderTemp $sensor->{_nk}: $val < $sensor->{_min_f}");
		    next LINE;
		    }
		elsif ($sensor->{_scale} eq "C" && $val <= $sensor->{_min_c}) {
		    msg("err", "UnderTemp $sensor->{_nk}: $val < $sensor->{_min_c}");
		    next LINE;
		    }
		elsif (($sensor->{_scale} eq "MPH" ||
			$sensor->{_scale} eq "%") && $val >= 100) {
		    msg("err", "Overdriven $sensor->{_nk}: $val >= 100 MPH");
		    next LINE;
		    }
		elsif (($sensor->{_scale} eq "MPH" ||
			$sensor->{_scale} eq "%") && $val < 0) {
		    msg("err", "Underdriven $sensor->{_nk}: $val < 0 MPH");
		    next LINE;
		    }
		elsif ($sensor->{_scale} eq "inHg" && $val >= 32) {
		    msg("err", "Overdriven $sensor->{_nk}: $val >= 32 inHg");
		    next LINE;
		    }
		elsif ($sensor->{_scale} eq "inHg" && $val <= 29) {
		    msg("err", "Underdriven $sensor->{_nk}: $val <= 29 inHg");
		    next LINE;
		    }

		#
		# Turn on/off values into logged values
		#
		if ($sensor->{type} eq "onoff") {
		    if ($val) {		# Non-zero == On
			$val = $sensor->{onvalue};
			}
		    else {
			$val = $sensor->{offvalue};
			}
		    }
		#
		# Adjust the sensor - unless it is a wind direction sensor.
		# The reason is that this will throw off the consensus
		# averaging algorithm - so do it after we average (and also
		# just below for the "current" value, a special case)
		#
		# NOTE: for some sensors, AdjustBy is disallowed in the
		# config, so we'll simply be adjusting by 0 here...  Likewise
		# if multiplyby is missing, we multiply by 1.  Only one of
		# the two will actually be in effect.
		#
		unless ($sensor->{type} eq "direction") {
		    $val += $sensor->{adjustby};
		    $val *= $sensor->{multiplyby} || 1;
		    }
		#
		# Handle wind gusts and MaxRat especially - we only care about
		# the maximum value, not the average.  Values are reset to 0
		# when we log every LogInterval minutes.  Trick out _last5 to
		# only have whatever the maximum value is.
		#
		if ($sensor->{type} eq "gust" ||
			$sensor->{type} eq "counter" && $sensor->{subtype} eq "maxrate") {
		    if ($val >= $sensor->{_sum}) {
			$sensor->{_sum} = $val;		# Maximum
			$sensor->{_last5} = [ $val ];
			}
		    $sensor->{_count} = 1;
		    }
		#
		# MinRate (not sure who'd need this, but why not?) is the
		# opposite of MaxRate (except undef is merely the initial
		# value, not a minimum)
		#
		elsif ($sensor->{type} eq "counter" && $sensor->{subtype} eq "minrate") {
		    if ($val <= $sensor->{_sum} || !defined($sensor->{_sum})) {
			$sensor->{_sum} = $val;		# Minimum
			$sensor->{_last5} = [ $val ];
			}
		    $sensor->{_count} = 1;
		    }
		#
		# Likewise, handle rainfall (and for the TEMP08, lightning)
		# specially.  For rain, the thermd counter continuously
		# increases until it is reset after a period of inactivity.
		# For the TEMP08, the data value always increases until reset
		# (since it is summed in the sensor), so we care when it goes
		# up.  For everything, the collector code reports on the
		# delta value, so we only care if the value is > 0.
		# Total values are potentially reset to 0 when we log every
		# LogInterval minutes (via InactivityReset and _lastchange).
		#
		elsif ($sensor->{type} eq "rain" ||
			$sensor->{type} eq "counter" && $sensor->{subtype} eq "total") {
		    #
		    # Temp08 rain sensor already reports total, not delta
		    #
		    if ($collector->{type} eq "temp08") {
			if ($val > $sensor->{_last5}[0]) {
			    $sensor->{_sum} = $val;	# Overwrite
			    $sensor->{_lastchange} = $now;
			    }
			}
		    #
		    # All others compute delta in the poller.  Since counters
		    # can have a multiplyby of negative (e.g., for SNMP in/out
		    # counters), we check absolute value
		    #
		    elsif (abs($val) > 0) {
			$sensor->{_sum} += $val;	# Increment
			$sensor->{_lastchange} = $now;
			}
		    $sensor->{_count} = 1;
		    $sensor->{_last5} = [ $sensor->{_sum} ];
		    }
		elsif ($sensor->{type} eq "lightning" && $collector->{type} eq "temp08" ||
			$sensor->{type} eq "counter" && $sensor->{subtype} =~ /^(count|raw)$/) {
		    if ($val > $sensor->{_last5}[0]) {
			$sensor->{_sum} = $val;		# Overwrite
			$sensor->{_lastchange} = $now;
			}
		    $sensor->{_count} = 1;
		    $sensor->{_last5} = [ $sensor->{_sum} ];
		    }
		#
		# WattHours (from the enersure, smartnet, etc) is a measure
		# of usage since the last reading - so we need to accumulate
		# the values in between log periods, instead of averaging
		# them.
		#
		elsif ($sensor->{type} eq "wh") {
		    # Don't count the very first reading, which may be huge
		    # after a long lapse in readings
		    if (defined($sensor->{_lastchange})) {
			$sensor->{_sum} += $val;		# Increment
			}
		    else {
			$sensor->{_sum} = 0;
			}
		    $sensor->{_count} = 1;
		    $sensor->{_last5} = [ $sensor->{_sum} ];
		    $sensor->{_lastchange} = $now;
		    }
		#
		# Wind direction is also special.  If you simply average the
		# direction values, then NNW (348.75 degrees) averaged with
		# NNE (11.5 degrees) will yield S (180 degrees).  The solution
		# is to do a consensus average (described later) and to do
		# that we have to keep track of all direction data collected
		# for this measurement period in {_weight}.  We still use the
		# single _last5 value for "current" readings.  Note that we
		# adjust *after* we store the value in the weighted average
		# holder, so that "current" values print correctly, but we
		# still average on the raw data (and adjust the average after
		# we compute it).
		#
		elsif ($sensor->{type} eq "direction") {
		    $sensor->{_weight}{$val}++;
		    $val += $sensor->{adjustby};
		    $val -= 360	if $val >= 360;
		    $val += 360	if $val <    0;
		    $sensor->{_sum} = $val;
		    $sensor->{_count} = 1;
		    $sensor->{_last5} = [ $val ];
		    }
		#
		# For everything else, collect the current value as well as
		# pre-compute the long-term average.  Keep the last 5 readings
		# for a short-term average.  Ensure that the short-term average
		# _last5 only has 5 entries
		#
		else {
		    append_to_average($sensor, $val);
		    }
		#
		# The _lastdata field is used to detect sensor failures and
		# for derived collectors
		#
		$sensor->{_lastdata} = $now;
		#
		# It is conceivable that due to system load, we would be
		# so slow in the main program that we would not finish
		# reading from all the collectors and could miss writing to
		# current or miss a loginterval.  So check using REAL time...
		#
		last COLLECTOR if time >= $next_log_time;
		}
	    continue {
		#
		# If we were an snmp_trap handler, we changed the collector
		# identity above, so now we need to reset it.
		#
		$collector = $alter_ego		if defined $alter_ego;
		undef $alter_ego;
		}
	    }
	#
	# Update the current file every time through (determined by the
	# minimum PollInterval value).  If $next_log_time has arrived, also
	# do the logging and RSS.  Finally, figure out how long to sleep
	# before doing the next readings.  We want to sleep for _minpoll/2
	# seconds, or until $next_log_time-1, whichever is less (if _minpoll
	# is 0, which happens if all the collectors are streaming, then
	# use 10 seconds as a "safe" value).
	#
	if ($config{logformat} eq "sql") {
	    next FOREVER unless connect_to_database(0);
	    }
	percolate_actuator_values();
	compute_derived_sensors();
	update_current();
	if (time >= $next_log_time) {	# Compare REAL time, not saved time
	    do_log_and_rss($next_log_time);
	    $next_log_time = find_next_log_time();
	    }
	if ($config{logformat} eq "sql") {
	    $sth->finish;
	    $dbh->disconnect;
	    }
	}
    continue {
	$now = time;
	sleep ((($next_log_time - $now > $config{_minpoll}) ||
		($next_log_time - $now <= 0)) ?
	    int($config{_minpoll}/2) : (($next_log_time - $now) - 1));
	}
    #
    # We should never get here
    #
    msg("alert", "Abnormal termination - restarting");
    if ($config{logformat} eq "sql") {
	$sth->finish;
	$dbh->disconnect;
	}
    get_kicked("HUP");
    }

sub child_died {
    my ($kidpid, $collector);
    # If a second child dies while in the signal handler caused by the
    # first death, we won't get another signal. So must loop here else
    # we will leave the unreaped child as a zombie. And the next time
    # two children die we get another zombie. And so on.
    while (($kidpid = waitpid(-1,WNOHANG)) > 0) {
       $collector = $kids{$kidpid};
       next unless $collector;	# I.e., don't restart the RSS logging processes
       msg("err", "Child PID $kidpid ($collector->{_name}) died - restarting");
       delete $kids{$kidpid};
       close $collector->{_fd};
       create_child_process($collector);
       msg("err", "New PID is $collector->{_kidpid}");
       }
    $SIG{CHLD} = \&child_died;  # SysV sucks, because it unsets handlers...
    $rescan_file_descriptors = 1;
    }

sub create_child_process {
    my $collector = shift;

    if ($collector->{_subr}) {
	$collector->{_kidpid} = $collector->{_subr}($collector);
	$kids{ $collector->{_kidpid} } = $collector;
	}
    else {
	die "Poller for $collector->{_name} has no poller subroutine!"
	}
    }

#
# The extra parameters are used for multivalue sensors like the D2P and counters
# If the type is undef (as, for HA7Net counters), we'll match any type.
#
sub hunt_for_sensor ($$$;$$) {
    my ($collector, $n, $type, $extra, $match) = @_;
    my $n_re = qr/^$n/;
    my @n;
    for my $nx (grep { /^$n_re/ } keys %{ $collector->{sensor} }) {
	next unless exists $collector->{sensor}{$nx};
	if ($collector->{sensor}{$nx}{type} eq $type || !defined $type) {
	    if ($extra) {
		next unless $collector->{sensor}{$nx}{$extra} eq $match;
		push @n, $nx;
		}
	    else {
		push @n, $nx;
		}
	    }
	}
    if (wantarray) {
	return @n;
	}
    else {
	return shift @n;
	}
    }

sub get_kicked {
    my $kicked = shift;

    msg("notice", "Received $kicked signal");
    print "\nPID $$ Received $kicked signal\n"	if $opt_verbose;
    #
    # We get here under two conditions - the first is we got a HUP, INT or
    # TERM signal (if we get a HUP, we restart).  The second is because the
    # main daemon loop terminated for some unknown reason.  Either way,
    # close down file handles and kill our kids, and optionally restart.
    #
    if ($config{logformat} eq "text") {
	for my $k (sort keys %{ $config{collector} }) {
	    if ($config{collector}{$k}{type} =~ /qk145|vk011|temp08|maxbotix/) {
		for my $n (sort keys %{ $config{collector}{$k}{sensor} }) {
		    close $config{collector}{$k}{sensor}{$n}{_fd};
		    }
		}
	    }
	}
    elsif ($config{logformat} eq "sql") {
	$sth->finish		if defined $sth;
	$dbh->disconnect	if defined $dbh;
	}
    else {
	die "Unknown LogFormat";
	}

    if (!$is_child && keys %kids) {
	msg("err", "Infanticide from PID $$ - $0 killing: @{[keys %kids]}");
	$SIG{CHLD} = "IGNORE";
	kill POSIX::SIGTERM(), keys %kids;
	}

    if ($kicked eq "HUP") {
	my $SELF = File::Spec->catfile($FindBin::Bin, $script);
	my @cmd =  ("$SELF", "-daemon", "-config", $opt_config);
	push @cmd, "-verbose"	if $opt_verbose;
	push @cmd, "-nofork"	if $opt_nofork;
	msg("notice", "Kicked & restarting: @cmd");
	exec @cmd;
	msg("err", "Cannot restart @cmd - $!");
	}

    msg("notice", "Shutting down");
    exit 0;
    }

sub append_to_average {
    my ($sensor_actuator, $val) = @_;

    $sensor_actuator->{_sum} += $val;		# Long-term averaging
    $sensor_actuator->{_count}++;
    unshift @{ $sensor_actuator->{_last5} }, $val;
    if (@{ $sensor_actuator->{_last5} } > 5) {
	splice @{$sensor_actuator->{_last5}}, 5;	# Removes tail
	}
    }

sub compute_average {
    my $sensor = shift;
    my ($sum, $cnt);
    #
    # Average the as many elements as there are available (may be 0)
    #
    for my $t (@{ $sensor->{_last5} }) {
	$sum += $t;
	$cnt++;
	}
    return $cnt ? $sum / $cnt : undef;
    }

sub update_current {
    my ($sensor, $collector, $actuator);
    my $now = time;
    my ($min, $hr, $day, $month) = (localtime($now))[1..4];
    my $nowstr = sprintf "%02d%02d %02d%02d", $hr, $min, ++$month, $day;
    #
    # Update the "current" file.  We don't care what kind of collector we
    # have - the data has already been collected in a device-dependent way
    # and sanitized.  Also, check for any alarm conditions that may have been
    # tripped (or reset), and for failed sensors.
    #
    print "UPDATING 'current' $nowstr\n"	if $opt_verbose;
    if ($config{logformat} eq "text") {
	print "Opening $config{logwrite}/current.tmp\n"	if $opt_verbose;
	open CURRENT, ">", "$config{logwrite}/current.tmp"
	    or warn "Can't open $config{logwrite}/current.tmp - $!\n";
	CURRENT->autoflush(1);
	print CURRENT strftime("%c", localtime $now), "\t$now\n";
	}
    elsif ($config{logformat} eq "sql") {
	$dbh->begin_work();	# Start of grouped transaction
	$sth = $dbh->prepare("DELETE FROM current");
	$sth->execute();
	}
    else {
	die "Unknown LogFormat";
	}
    COLLECTOR: for my $k (keys %{ $config{collector} }) {
	$collector = $config{collector}{$k};
	next COLLECTOR if $collector->{readonly};
	SENSOR: for my $n (keys %{ $collector->{sensor} }) {
	    my $avg;
	    $sensor = $collector->{sensor}{$n};
	    next SENSOR if $sensor->{readonly};
	    #
	    # If we haven't seen data for a bit over 5*PollInterval, the sensor
	    # is flagged as dead (or alternatively, revived if we _do_ get data)
	    #
	    if ($now - $sensor->{_lastdata} > 5*$collector->{pollinterval}+10) {
		unless ($sensor->{_failed}++) {
		    msg("crit", "\u$sensor->{type} sensor $sensor->{_nk} failed");
		    $sensor->{_last5} = [ ];
		    }
		}
	    else {
		if ($sensor->{_failed}) {
		    msg("crit", "\u$sensor->{type} sensor $sensor->{_nk} returned");
		    }
		$sensor->{_failed} = 0;
		}
	    $avg = compute_average($sensor);
	    if (defined $avg) {
		if ($config{logformat} eq "text") {
		    printf CURRENT "%.3f\t%s\t%s@%s\n",
			$avg, $sensor->{_scale}, $n, $k;
		    }
		elsif ($config{logformat} eq "sql") {
		    $sth = $dbh->prepare(
			"INSERT INTO current " .
			"(logtime, value, units, log_id, log_name) VALUES " .
			"($now, $avg, '$sensor->{_scale}', $sensor->{_id}, '$n\@$k')");
		    $sth->execute();
		    }
		else {
		    die "Unknown LogFormat";
		    }
		printf "%.3f\t%s\t%s@%s\n",
		    $avg, $sensor->{_scale}, $n, $k if $opt_verbose;
		check_alarm($k, $n, $avg, $nowstr);
		}
	    }
	ACTUATOR: for my $n (keys %{ $collector->{actuator} }) {
	    my $avg;
	    $actuator = $collector->{actuator}{$n};
	    next ACTUATOR if $actuator->{readonly};
	    $avg = compute_average($actuator);
	    if (defined $avg) {
		if ($config{logformat} eq "text") {
		    printf CURRENT "%.3f\t%s\t%s@%s\n",
			$avg, $actuator->{_scale}, $n, $k;
		    }
		elsif ($config{logformat} eq "sql") {
		    $sth = $dbh->prepare(
			"INSERT INTO current " .
			"(logtime, value, units, log_id, log_name) VALUES " .
			"($now, $avg, '$actuator->{_scale}', $actuator->{_id}, '$n\@$k')");
		    $sth->execute();
		    }
		else {
		    die "Unknown LogFormat";
		    }
		printf "%.3f\t%s\t%s@%s\n",
		    $avg, $actuator->{_scale}, $n, $k if $opt_verbose;
		}
	    }
	}
    if ($config{logformat} eq "text") {
	print "Closing $config{logwrite}/current.tmp\n"	if $opt_verbose;
	close CURRENT;
	print "Renaming current.tmp => current\n"	if $opt_verbose;
	rename "$config{logwrite}/current.tmp", "$config{logwrite}/current"
	    or msg("err", "Cannot rename current.tmp => current - $!");
	}
    elsif ($config{logformat} eq "sql") {
	$dbh->commit();		# End of grouped transaction
	}
    else {
	die "Unknown LogFormat";
	}
    }

sub ztrim {
    my $in = shift;
    my $out = sprintf "%.3f", $in;
    $out =~ s/0+$//;	# Trim trailing 0's
    $out =~ s/\.$//;	# In case the number was N.000
    return $out;
    }

sub reset_temp08_counter {
    my ($collector, $sensor, $n) = @_;
    my ($hex, $id);

    #
    # Look inside the results of the DIS command to find the sensor number of
    # the gauge so we can send it an RST command.
    #
    ($hex = $n) =~ s/\.\w$//;	# Shouldn't be there anyway
    ($id) = $collector->{_DIS_str} =~ /^(\d\d)\s+$hex/mi;
    msg("notice", "Resetting TEMP08 $sensor->{type} gauge $id $hex");
    syswrite($collector->{_fd}, "RST$id");
    sleep 1;
    syswrite($collector->{_fd}, "y");
    }

sub do_log_and_rss {
    my $now = shift;
    our $every;			# Persistent data, locally scoped
    #
    # Only write per-file log data once every LogInterval minutes.  If
    # we are here, then it is time!  Once we write, reset the averaging
    # counts (so we calculate LogInterval-minute averages and not
    # averages since boot-time!)
    #
    if ($opt_verbose) {
	printf "UPDATING LOGFILES @ %s\n", strftime("%c", localtime);
	printf "    SCHEDULED FOR %s\n", strftime("%c", localtime($now));
	}
    COLLECTOR: for my $k (sort keys %{ $config{collector} }) {
	my $collector = $config{collector}{$k};
	next COLLECTOR if $collector->{readonly};
	SENSOR: for my $n (sort keys %{ $collector->{sensor} }) {
	    my $sensor = $collector->{sensor}{$n};
	    next SENSOR if $sensor->{readonly};
	    if ($sensor->{_count}) {
		my ($avg, $fd, $str);
		#
		# For wind direction, calculate the consensus average.
		# For every other sensor type, a simple average is good
		# enough. We adapt the consensus average algorithm found
		# at http://www.beals5.com/wx/faqs.htm
		#
		if ($sensor->{type} eq "direction") {
		    my (%weight_sum, $max_sum, $max_idx);
		    for (my $i = 0; $i < 90; $i += 22.5) {
			$sensor->{_weight}{$i+360} = $sensor->{_weight}{$i};
			}
		    for (my $i = 0; $i < 360; $i += 22.5) {
			$weight_sum{$i} = $sensor->{_weight}{$i} +
			    $sensor->{_weight}{$i+22.5} +
			    $sensor->{_weight}{$i+45} +
			    $sensor->{_weight}{$i+67.5} +
			    $sensor->{_weight}{$i+90};
			if ($weight_sum{$i} > $max_sum) {
			    $max_sum = $weight_sum{$i};
			    $max_idx = $i;
			    }
			}
		    if ($weight_sum{$max_idx}) {
			$avg = $max_idx +
			    (($sensor->{_weight}{$max_idx+22.5} +
				(2 * $sensor->{_weight}{$max_idx+45}) +
				(3 * $sensor->{_weight}{$max_idx+67.5}) +
				(4 * $sensor->{_weight}{$max_idx+90}))
			    * 22.5 / $weight_sum{$max_idx});
			}
		    else {
			# Only happens if there is no wind direction data
			$avg = 0;
			}
		    #
		    # After we have done the average on the raw values, we
		    # apply the adjust_by value.  We MUST average on the
		    # raw values, else the constants built into the algorithm
		    # above don't reference values correctly
		    #
		    $avg -= 360	if $avg > 360;
		    $avg += $sensor->{adjustby};
		    $avg -= 360	if $avg >= 360;
		    $avg += 360	if $avg <    0;
		    $sensor->{_weight} = {};
		    }
		else {
		    $avg = $sensor->{_sum} / $sensor->{_count};
		    }
		#
		# Write to logfile
		#
		if ($config{logformat} eq "text") {
		    $str = sprintf "%010d\t%.3f", $now, $avg;
		    $fd = $sensor->{_fd};
		    seek ($fd, 0, 2);	# Append to file
		    print $fd "$str\n";
		    }
		elsif ($config{logformat} eq "sql") {
		    $str = sprintf "(%010d,%d,%.3f)", $now, $sensor->{_id}, $avg;
		    $sth = $dbh->prepare(
			"INSERT INTO readings " .
			"(logtime, log_id, value) VALUES " .
			$str);
		    $sth->execute();
		    }
		else {
		    die "Unknown LogFormat";
		    }
		print "\t$str\t$sensor->{name}\n" if $opt_verbose;
		$sensor->{_last} = $avg;
		#
		# Reset the sum and count to 0 after logging.  However, 
		# counters with a defined InactivityReset (which includes the
		# rain gauges) never go down except irregularly, so do not
		# reset their values (except as below)
		#
		unless ($sensor->{inactivityreset}) {
		    $sensor->{_sum} = undef;	# Not 0, because of MinRate
		    $sensor->{_count} = 0;
		    }
		}
	    else {
		print "\tNO DATA\t$sensor->{name}\n" if $opt_verbose;
		$sensor->{_last} = undef;
		$sensor->{_sum} = undef;	# Not 0, because of MinRate
		$sensor->{_count} = 0;
		}
	    #
	    # Here is where we reset counters with a InactivityReset (this
	    # includes all rain gauges) if there has been been no change in
	    # N hours/minutes.  For the TEMP08, we also have to reset the
	    # collector.  For the other collectors, we only reset the locally
	    # stored numbers.
	    #
	    if ($sensor->{inactivityreset} &&
			$sensor->{_last5}[0] > 0 &&
			defined $sensor->{_lastchange} &&
			$now - $sensor->{_lastchange} >
			    $sensor->{inactivityreset}) {
		#
		# Reset the TEMP08 counter
		#
		if ($collector->{type} eq "temp08") {
		    reset_temp08_counter($collector, $sensor, $n);
		    }
		#
		# Always reset the internal counters
		#
		$sensor->{_last5} = [ 0 ];
		$sensor->{_last} = 0;
		$sensor->{_sum} = 0;
		$sensor->{_count} = 0;
		}
	    #
	    # For TEMP08 lightning sensors, also reset the TEMP08 counter
	    #
	    if ($sensor->{type} eq "lightning" &&
		    $collector->{type} eq "temp08") {
		reset_temp08_counter($collector, $sensor, $n);
		}
	    }
	ACTUATOR: for my $n (sort keys %{ $collector->{actuator} }) {
	    my $actuator = $collector->{actuator}{$n};
	    next ACTUATOR if $actuator->{readonly};
	    if ($actuator->{_count}) {
		my ($fd, $str);
		my $avg = $actuator->{_sum} / $actuator->{_count};
		#
		# Write to logfile
		#
		if ($config{logformat} eq "text") {
		    $str = sprintf "%010d\t%.3f", $now, $avg;
		    $fd = $actuator->{_fd};
		    seek ($fd, 0, 2);	# Append to file
		    print $fd "$str\n";
		    }
		elsif ($config{logformat} eq "sql") {
		    $str = sprintf "(%010d,%d,%.3f)", $now, $actuator->{_id}, $avg;
		    $sth = $dbh->prepare(
			"INSERT INTO readings " .
			"(logtime, log_id, value) VALUES " .
			$str);
		    $sth->execute();
		    }
		else {
		    die "Unknown LogFormat";
		    }
		print "\t$str\t$actuator->{name}\n" if $opt_verbose;
		#
		# Because actuators are probably rarely changed, preserve the
		# last seen value as the assumed current value.
		#
		$actuator->{_last} = $avg;
		$actuator->{_sum} = $actuator->{_last5}[0];
		$actuator->{_count} = 1;
		}
	    }
	}
    #
    # If there are any Wunderground views, send the data to wunderground
    #
    for my $view (keys %{ $config{_view} }) {
	if ($config{view}{$view}{type} eq "wunderground") {
	    send_to_wunderground($view);
	    }
	}
    #
    # And only generate RSS data every Every iterations of log writes
    #
    if ($config{rss} && ++$every == $config{rss}{every}) {
	generate_rss();
	$every = 0;
	}
    #
    # For sanity's sake, restart twice a year (*after* writing out data)
    #
    if ($now - $^T > 86400*180) {
	msg("notice", "Periodic restart of daemon");
	get_kicked("HUP");
	# NOTREACHED
	}
    }

#
# The next time we want to log is the next round value of LogInterval seconds
#
sub find_next_log_time {
    my $retval;
    my $now = time;
    $retval = int(($now + $config{loginterval}) / $config{loginterval}) *
	$config{loginterval};
    print "Next log at $retval, in ", $retval-$now, " secs\n" if $opt_verbose;
    return $retval;
    }

sub owfs_readval {
    my ($collector, $nx, $sub) = @_;
    my ($val, $str, $fn);

    if ($collector->{type} eq "owfs") {
	($fn = "$collector->{mountpoint}/$nx/$sub") =~ s#//#/#g;
	open S, "<", $fn	or msg("err", "Cannot open OWFS file $fn - $!");
	chomp($val = <S>);
	close S;
	}
    elsif ($collector->{type} eq "owhttpd") {
	$fn = "$collector->{_baseurl}/$nx/$sub";
	$str = $collector->{_ua}->get($fn)->content;
	($val) = $str =~ m#<TD><B>$sub</B></TD><TD>\s*((-\s*)?[\d.]+|yes|no)#i;
	}
    elsif ($collector->{type} eq "owshell") {
	$val = `owread -s $collector->{_baseurl} $nx/$sub`;
	}
    else {
	die "Unknown collector type $collector->{type}";
	}

    if ($val =~ /yes/i) {
	return 1;
	}
    elsif ($val =~ /no/i) {
	return 0;
	}
    elsif ($val =~ /\d/) {	# Any digit means we treat it like a number
	return $val;
	}
    else {
	next SENSOR;	# Clever!  Makes fork_owfs_poller skip sensor
			# WARNING: uses up-level addressing.
	}
    }

#
# This returns C/min for AvgRate, MinRate and MaxRate (the min/maximizing is
# done in the logging daemon); delta-C for Count and Total (again, all work
# done in logging daemon); and C for Raw
#
sub evaluate_counter {
    my ($sensor, $count, $now) = @_;
    my ($deltaT, $deltaC, $retval);

    $now ||= time();	# For collectors which don't support internal time

    if (!defined $sensor->{_tare}) {
	$deltaC = 0;
	}
    elsif ($count >= $sensor->{_tare}) {
	$deltaC = $count - $sensor->{_tare};
	}
    else {
	$deltaC = $count + (0xFFFFFFFF - $sensor->{_tare});
	}
    #
    # Reset tare every time - let the daemon do the summing or maxima.
    #
    $sensor->{_tare} = $count;

    #
    # Compute and return the appropriate value from the counter value
    #
    if ($sensor->{type} =~ /^(speed|gust)$/
	    || $sensor->{subtype} =~ /^(avgrate|maxrate|minrate)$/) {
	if ($sensor->{_then}) {
	    $deltaT = $now - $sensor->{_then};
	    # Report count/second - all min/max/rate is done in the daemon
	    $retval = $deltaC / $deltaT;
	    }
	else {
	    $retval = undef;
	    }
	$sensor->{_then} = $now;
	}
    elsif ($sensor->{type} =~ /^(rain|lightning)$/
	    || $sensor->{subtype} =~ /^(count|total)$/) {
	$retval = $deltaC;
	}
    elsif ($sensor->{subtype} eq "raw") {
	$retval = $count;
	}
    else {
	msg("err", "Impossible sensor subtype '$sensor->{subtype}' on $sensor->{name}");
	die "Impossible sensor subtype '$sensor->{subtype}' on $sensor->{name}";
	}

    return $retval;
    }

sub fork_owfs_poller {		# Supports owfs, owhttpd, and owshell
    my $collector = shift;
    my $name = $collector->{_name};
    my ($v, $fn, $n, $nx, $now, $start, $addr, $h, $t, %latch, %sense, %save);
    my $fd = new FileHandle;

    die "Unknown collector" unless $collector->{type} =~ /^(owfs|owhttpd|owshell)$/;
    print "Forking $collector->{type} poller for <Collector $name>\n"
	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for OWFS $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $_\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ @{[$collector->{ipaddress} || $collector->{mountpoint}]}";
    #
    # Continually rotate through the collection of sensors, requesting them
    # in turn from owfs, owhttpd, or owshell
    #
    while (1) {
	$start = time;
	#
	# WARNING!  The "SENSOR:" label is up-level addressed in owfs_readval()
	#
	SENSOR: for my $n (sort keys %{ $collector->{sensor} }) {
	    my $sensor = $collector->{sensor}{$n};
	    ($nx = $n) =~ s/\.\w$//;		# Remove subindex
	    if ($nx =~ /^(10|22|26|28|30)/) {
		warn "Missing owfs filename for $n!" unless $sensor->{_owfs};
		#
		# Handle temperature, humidity, etc first
		#
		$v = owfs_readval($collector, $nx, $sensor->{_owfs});
		if ($sensor->{type} eq "barometer") {
		    my $pressure = $v * $sensor->{slope} + $sensor->{intercept};
		    print "$n $pressure\n";
		    }
		else {
		    print "$n $v\n";
		    }
		}
	    elsif ($nx =~ /^12/) {
		#
		# Deal with the DS2406's.
		#
		if ($sensor->{type} eq "barometer") {
		    die "Missing owfs filename!" unless $sensor->{_owfs};
		    # The sensor reads in millibar, we want inHg (and convert
		    # to display units elsewhere)
		    $v = owfs_readval($collector, $nx, $sensor->{_owfs}) / 33.863788;
		    print "$n $v\n";
		    }
		elsif ($sensor->{type} eq "temperature") {
		    die "Missing owfs filename!" unless $sensor->{_owfs};
		    # Reads in Celsius (which is what we expect)
		    $v = owfs_readval($collector, $nx, $sensor->{_owfs});
		    print "$n $v\n";
		    }
		elsif ($sensor->{type} eq "onoff") {
		    my $p = uc $sensor->{pio};
		    $latch{sensor}->{pio} = owfs_readval($collector, $nx, "latch.$p");
		    $sense{sensor}->{pio} = owfs_readval($collector, $nx, "PIO.$p");
		    #
		    # Send the current value of the sensors.  But since the sensor
		    # knows what transitions were made, if the switch toggled
		    # on-off-on (or off-on-off), send TWO measurements (and
		    # always send the current state last).
		    #
		    if (defined $save{$addr}{$p} &&
			    $sense{$p} == $save{$addr}{$p} && $latch{$p}) {
			print "$n ", 1 - $sense{$p}, "\n";
			}
		    print "$n $sense{$p}\n";
		    #
		    # Save the values for comparison next time around
		    #
		    $save{$addr}{a} = $sense{a};
		    $save{$addr}{b} = $sense{b};
		    #
		    # Finally, reset the latches
		    #
		    msg("err", "I have no idea if the latches for $collector->{mountpoint}/$nx are reset this way...");
		    open S, ">", "$collector->{mountpoint}/$nx/latch.$p"	or next SENSOR;
		    print S "0\n";
		    close S;
		    }
		else {
		    die "Sanity error - please contact dan\@klein.com";
		    }
		}
	    elsif ($nx =~ /^1D/) {
		die "Missing owfs filename!" unless $sensor->{_owfs};
		#
		# Now deal with the DS2423's.  For speed or gust only print
		# one value (since we are walking the real sensors)
		#
		$v = owfs_readval($collector, $nx, $sensor->{_owfs});
		$v = evaluate_counter($sensor, $v, time());
		print "$n $v\n"		if defined $v;
		}
	    elsif ($nx =~ /^20/) {
		my ($A, $B, $C, $D, $dir);
		#
		# Now deal with the tricky case - the multi-value 2450 sensors.
		# We have to aquire a lock, and read a few values, and assemble
		# the direction data based on a lookup table.
		#
		# Extract the 4 values and scale them to high/medium/low (the
		# actual values are roughly 0, 2.5, and 5 volts).
		#
		$A = owfs_readval($collector, $nx, "volt.A");
		$B = owfs_readval($collector, $nx, "volt.B");
		$C = owfs_readval($collector, $nx, "volt.C");
		$D = owfs_readval($collector, $nx, "volt.D");

		for ($A, $B, $C, $D) {
		    if ($_ > 4) {
			$_ = 2;
			}
		    elsif ($_ > 1) {
			$_ = 1; 
			}
		    else {
			$_ = 0;
			}
		    }

		if ($sensor->{inverted}) {
		    $dir = $compass_lookup{"$D$C$B$A"};
		    }
		else {
		    $dir = $compass_lookup{"$A$B$C$D"};
		    }
		next SENSOR unless defined $dir;
		print "$n $dir\n";
		}
	    else {
		die "Unknown sensor type for $n in owfs_poller";
		}
	    }
	}
    continue {
	#
	# After we cycle through the sensor list, figure out how long our
	# readings took, and pause before restarting.  Since readings can
	# take a while, we want to sleep enough so that we can restart at the
	# start of the next PollInterval.  But we have to be careful - if there
	# were enough sensors for our reading to take more than PollInterval
	# seconds, we don't want to sleep for a negative time (which is really
	# a *very* big positive time).  Since we can't predict how long a
	# reading will take (we can only see how long it took to finish last
	# time), this will probably slowly skew.  It doesn't matter - at worst,
	# we'll miss one set of readings in the main daemon loop.
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the OWFS poller exit?");
    }

sub fork_newport_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $ok, $val, $n, $t);
    my $fd = new FileHandle;

    print "Forking Newport/Omega poller\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Wunderground $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}}";

    #
    # Continually rotate through the collection of sensors.  Unlike most
    # collectors, we open/close the connection each time we collect from
    # all the sensors (the collectors eventually time out otherwise).
    #
    while (1) {
	$ok = $ua->open (
	    host 			=> $collector->{ipaddress},
	    port			=> $collector->{port} || 2000,
	    );
	unless ($ok) {
	    msg("warn", "Could not create Telnet connection for Newport/Omega $collector->{_subtype}");
	    return;
	    }
	$start = time;
	while (my ($n, $sensor) = each %{ $collector->{sensor} }) {
	    $ok = $ua->print("*$n");
	    next unless $ok;
	    (undef,$val) = $ua->waitfor('/-?\d+\.\d+/');
	    print "$n $val\n";
	    }
	}
    continue {
	$ua->close;
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;    # Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the Newport/Omega poller exit?");
    }

sub fork_ha7net_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($url, $id, $lockid, $ua, $response, @response, $v, $count,
	$now, $start, $addr, $h, $t, %latch, %sense, %save);
    my $fd = new FileHandle;

    eval qq{ use Digest::CRC 'crc16'; };
    die $@ if $@;

    our @crc8 = (
	0,94,188,226,97,63,221,131,194,156,126,32,163,253,31,65,
	157,195,33,127,252,162,64,30,95,1,227,189,62,96,130,220,
	35,125,159,193,66,28,254,160,225,191,93,3,128,222,60,98,
	190,224,2,92,223,129,99,61,124,34,192,158,29,67,161,255,
	70,24,250,164,39,121,155,197,132,218,56,102,229,187,89,7,
	219,133,103,57,186,228,6,88,25,71,165,251,120,38,196,154,
	101,59,217,135,4,90,184,230,167,249,27,69,198,152,122,36,
	248,166,68,26,153,199,37,123,58,100,134,216,91,5,231,185,
	140,210,48,110,237,179,81,15,78,16,242,172,47,113,147,205,
	17,79,173,243,112,46,204,146,211,141,111,49,178,236,14,80,
	175,241,19,77,206,144,114,44,109,51,209,143,12,82,176,238,
	50,108,142,208,83,13,239,177,240,174,76,18,145,207,45,115,
	202,148,118,40,171,245,23,73,8,86,180,234,105,55,213,139,
	87,9,235,181,54,104,138,212,149,203,41,119,244,170,72,22,
	233,183,85,11,136,214,52,106,43,117,151,201,74,20,246,168,
	116,42,200,150,21,75,169,247,182,232,10,84,215,137,107,53);

    sub crc8 {
	my $str = shift;
	my $crc = 0;
	my $byte;

	for (my $i = 0; $i < length($str); $i++) {
	    $byte = ord(substr($str, $i, 1));
	    $crc = $crc8[$crc ^ $byte];
	    }
	return $crc;
    }

    #
    # Thermocouple polynomial coefficients taken
    # from http://srdata.nist.gov/its90/main/
    #
    our %thermocouple = (
	B => [
	    { min => 0.291, max => 2.431, coeff => [
		 9.8423321E+01,  6.9971500E+02, -8.4765304E+02,  1.0052644E+03,
		-8.3345952E+02,  4.5508542E+02, -1.5523037E+02,  2.9886750E+01,
		-2.4742860E+00
		] },
	    { min => 2.431, max => 13.820, coeff => [
		 2.1315071E+02,  2.8510504E+02, -5.2742887E+01,  9.9160804E+00,
		-1.2965303E+00,  1.1195870E-01, -6.0625199E-03,  1.8661696E-04,
		-2.4878585E-06
		] },
	    ],
	E => [
	    { min => -8.825, max => 0.000, coeff => [
		 0.0000000E+00,  1.6977288E+01, -4.3514970E-01, -1.5859697E-01,
		-9.2502871E-02, -2.6084314E-02, -4.1360199E-03, -3.4034030E-04,
		-1.1564890E-05
		] },
	    { min => 0.000, max => 76.373, coeff => [
		 0.0000000E+00,  1.7057035E+01, -2.3301759E-01,  6.5435585E-03,
		-7.3562749E-05, -1.7896001E-06,  8.4036165E-08, -1.3735879E-09,
		 1.0629823E-11, -3.2447087E-14
		] },
	    ],
	J => [
	    { min=> -8.095, max => 0.000, coeff => [
		 0.0000000E+00,  1.9528268E+01, -1.2286185E+00, -1.0752178E+00,
		-5.9086933E-01, -1.7256713E-01, -2.8131513E-02, -2.3963370E-03,
		-8.3823321E-05
		] },
	    { min => 0.000, max => 42.919, coeff => [
		 0.000000E+00,  1.978425E+01, -2.001204E-01,  1.036969E-02,
		-2.549687E-04,  3.585153E-06, -5.344285E-08,  5.099890E-10
		] },
	    { min => 42.919, max => 69.553, coeff => [
		-3.11358187E+03,  3.00543684E+02, -9.94773230E+00,
		 1.70276630E-01, -1.43033468E-03,  4.73886084E-06
		] },
	    ],
 	K => [
	    { min => -5.891, max => 0.000, coeff => [
		 0.0000000E+00,  2.5173462E+01, -1.1662878E+00, -1.0833638E+00,
		-8.9773540E-01, -3.7342377E-01, -8.6632643E-02, -1.0450598E-02,
		-5.1920577E-04
		] },
	    { min => 0.000, max => 20.644, coeff => [
		 0.000000E+00,  2.508355E+01,  7.860106E-02, -2.503131E-01,
		 8.315270E-02, -1.228034E-02,  9.804036E-04, -4.413030E-05,
		 1.057734E-06, -1.052755E-08
		] },
	    { min => 20.644, max => 54.886, coeff => [
		-1.318058E+02,  4.830222E+01, -1.646031E+00,  5.464731E-02,
		-9.650715E-04,  8.802193E-06, -3.110810E-08
		] },
	    ],
	N => [
	    { min => -3.990, max => 0.000, coeff => [
		 0.0000000E+00,  3.8436847E+01,  1.1010485E+00,  5.2229312E+00,
		 7.2060525E+00,  5.8488586E+00,  2.7754916E+00,  7.7075166E-01,
		 1.1582665E-01,  7.3138868E-03
		] },
	    { min => 0.000, max => 20.613, coeff => [
		 0.00000E+00,  3.86896E+01, -1.08267E+00,  4.70205E-02,
		-2.12169E-06, -1.17272E-04,  5.39280E-06, -7.98156E-08
		] },
	    { min => 20.613, max => 47.513, coeff => [
		 1.972485E+01,  3.300943E+01, -3.915159E-01,  9.855391E-03,
		-1.274371E-04,  7.767022E-07
		] },
	    ],
 	R => [
	    { min => -0.226, max => 1.923, coeff => [
		 0.0000000E+00,  1.8891380E+02, -9.3835290E+01,  1.3068619E+02,
		-2.2703580E+02,  3.5145659E+02, -3.8953900E+02,  2.8239471E+02,
		-1.2607281E+02,  3.1353611E+01, -3.3187769E+00
		] },
	    { min => 1.923, max => 13.228, coeff => [
		 1.334584505E+01,  1.472644573E+02, -1.844024844E+01,
		 4.031129726E+00, -6.249428360E-01,  6.468412046E-02,
		-4.458750426E-03,  1.994710149E-04, -5.313401790E-06,
		 6.481976217E-08
		] },
	    { min => 11.361, max => 19.739, coeff => [
		-8.199599416E+01,  1.553962042E+02, -8.342197663E+00,
		 4.279433549E-01, -1.191577910E-02,  1.492290091E-04
		] },
	    { min => 19.739, max => 21.103, coeff => [
		 3.406177836E+04, -7.023729171E+03,  5.582903813E+02,
		-1.952394635E+01,  2.560740231E-01
		] },
	    ],
	S => [
	    { min => -0.235, max => 1.874, coeff => [
		 0.00000000E+00,  1.84949460E+02, -8.00504062E+01,
		 1.02237430E+02, -1.52248592E+02,  1.88821343E+02,
		-1.59085941E+02,  8.23027880E+01, -2.34181944E+01,
		 2.79786260E+00
		] },
	
	    { min => 1.874, max => 11.950, coeff => [
		 1.291507177E+01,  1.466298863E+02, -1.534713402E+01,
		 3.145945973E+00, -4.163257839E-01,  3.187963771E-02,
		-1.291637500E-03,  2.183475087E-05, -1.447379511E-07,
		 8.211272125E-09
		] },
	
	    { min => 10.332, max => 17.536, coeff => [
		-8.087801117E+01,  1.621573104E+02, -8.536869453E+00,
		 4.719686976E-01, -1.441693666E-02,  2.081618890E-04
		] },
	
	    { min => 17.536, max => 18.693, coeff => [
		 5.333875126E+04, -1.235892298E+04,  1.092657613E+03,
		-4.265693686E+01,  6.247205420E-01
		] },
	    ],
 	T => [
	    { min => -5.603, max => 0.000, coeff => [
		 0.0000000E+00,  2.5949192E+01, -2.1316967E-01,  7.9018692E-01,
		 4.2527777E-01,  1.3304473E-01,  2.0241446E-02,  1.2668171E-03,
		] },
	    { min => 0.000, max => 20.872, coeff => [
		 0.000000E+00,  2.592800E+01, -7.602961E-01,  4.637791E-02,
		-2.165394E-03,  6.048144E-05, -7.293422E-07
		] },
	    ],
    );

    sub calculate_polynomial {
	my ($uv, $type) = @_;
	my $mv = $uv / 1000;
	my $coeff;
	#
	# Find the set of coefficients that match.  If it is less than the
	# max, use it (which means that for type B, for example, which says
	# it ranges from 0-1820C, but for whom the reverse polynomial tables
	# are only listed from 250-1820C, we possibly use an imperfect table)
	# Similar issues exist for other types, too.
	#
	for my $h (@{ $thermocouple{$type} }) {
	    if ($mv < $h->{max}) {
		$coeff = $h->{coeff};
		last;
		}
	    }
	#
	# If the mV range is beyond that of the tables, use the last element,
	# again possibly giving erroneous readings...
	#
	$coeff ||= $thermocouple{$type}[-1]{coeff};

	my $retval = 0;

	for my $i (0..$#$coeff) {
	    $retval += ($mv ** $i) * $coeff->[$i];
	    }

	return $retval;
	}

    print "Forking HA7Net poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for HA7Net $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Continually rotate through the collection of URLs, requesting them
    # in turn from the HA7Net
    #
    POLL: while (1) {
	$start = time;
	$count = 0;
	while ($count++ < @{$collector->{_urls}}) {
	    $url = shift @{$collector->{_urls}};
	    push @{$collector->{_urls}}, $url;
	    $response = $ua->get($url)->content;
	    if ($response =~ /Read (DS18B20 Result|Temperature Reply)/) {
		#
		# Note - an empty temperature can match (and can happen on
		# device problems
		#
		@response = $response =~
		    /ID="Address.*?VALUE="([\da-fA-F]{16})"
			     .*?
		     ID="Temperature.*?VALUE="(-?[\d.]*)"/gx;
		while (($addr, $t) = splice(@response, 0, 2)) {
		    next unless $t =~ /$numeric/;
		    for my $n (hunt_for_sensor($collector, $addr, "temperature")) {
			print "$n $t\n";
			}
		    }
		}
	    elsif ($response =~ /Read Humidity Reply/) {
		#
		# Note - an empty humidity/temperature can match (and can
		# happen on device problems
		#
		@response = $response =~
		    /ID="Address.*?VALUE="([\da-fA-F]{16})"
			     .*?
		     ID="Humidity.*?VALUE="(-?[\d.]*)"
			     .*?
		     ID="Temperature.*?VALUE="(-?[\d.]*)"/gx;
		while (($addr, $h, $t) = splice(@response, 0, 3)) {
		    next unless $h =~ /$numeric/ && $t =~ /$numeric/;
		    for my $n (hunt_for_sensor($collector, $addr, "humidity")) {
			print "$n $h\n";
			}
		    for my $n (hunt_for_sensor($collector, $addr, "temperature")) {
			print "$n $t\n";
			}
		    }
		}
	    elsif ($response =~ /Read Analog Probe Reply/) {
		#
		# Note - an empty probe/temperature can match (and can happen
		# on device problems
		#
		@response = $response =~
		    /ID="Probe_Address.*?VALUE="([\da-fA-F]{16})"
			     .*?
		     ID="Probe_Value.*?VALUE="(-?[\d.]*)"
			(?:
				 .*?
			 ID="Temperature_Address.*?VALUE="([\da-fA-F]{16})"
				 .*?
			 ID="Temperature.*?VALUE="(-?[\d.]*)"
			)?/gx;
		while (($addr, $v) = splice(@response, 0, 2)) {
		    next unless $v =~ /$numeric/;
		    for my $n (hunt_for_sensor($collector, $addr, "humidity")) {
			print "$n $v\n";
			}
		    for my $n (hunt_for_sensor($collector, $addr, "temperature")) {
			print "$n $v\n";
			}
		    }
		}
	    else {
		msg("err", "Unknown response from HA7Net $name $response");
		}
	    }
	#
	# Next deal with the DS2406's.
	#
	DS2406: for my $addr (@{$collector->{_2406}}) {
	    my ($val, $crc_in, $crc_out);
	    $response = $ua->get("$collector->{_baseurl}/1Wire/WriteBlock.html?Data=F54DFFFFFFFFFF&Address=$addr")->content;
	    if ($response =~ /Write Block Reply/) {
		($val, $crc_in) = $response =~
		    /NAME="ResultData_0.*?VALUE="([\da-fA-F]{10})([\da-fA-F]{4})"/;
		#
		# The DalSemi CRC16 is output as 1s complement, byte swapped
		#
		($crc_out = sprintf "%04X", ~crc16(pack "H*", $val) & 0xffff) =~
		    s/(..)(..)/$2$1/;
		if ($crc_in ne $crc_out) {
		    msg("warning", "CRC Error on onoff sensor $addr\@$name");
		    next DS2406;
		    }
		$v = hex(substr($1, 6, 2));
		$latch{b} = ($v & 0x20) >> 5 || 0;
		$latch{a} = ($v & 0x10) >> 4 || 0;
		$sense{b} = !($v & 0x08) || 0;	# So closed/shorted ==> 1
		$sense{a} = !($v & 0x04) || 0;	# So closed/shorted ==> 1
		#
		# Send the current value of the sensors.  But since the sensor
		# knows what transitions were made, if the switch toggled
		# on-off-on (or off-on-off), send TWO measurements (and
		# always send the current state last).
		#
		for my $p ('a'..'b') {
		    for my $n (hunt_for_sensor($collector, $addr, "onoff", "pio", $p)) {
			if (defined $save{$addr}{$p} &&
				$sense{$p} == $save{$addr}{$p} && $latch{$p}) {
			    print "$n ", 1 - $sense{$p}, "\n";
			    }
			print "$n $sense{$p}\n";
			}
		    }
		#
		# Save the values for comparison next time around
		#
		$save{$addr}{a} = $sense{a};
		$save{$addr}{b} = $sense{b};
		#
		# Finally, reset the latches
		#
		$ua->get("$collector->{_baseurl}/1Wire/WriteBlock.html?Data=F5CCFFFF&Address=$addr");
		}
	    else {
		msg("err", "Unknown answer for 2406 from HA7Net $name");
		}
	    }

	#
	# Now deal with the DS2423 counters.
	#
	my ($addr, $channels);
	DS2423: while (($addr, $channels) = each %{ $collector->{_2423} }) {
	    for my $channel (keys %$channels) {
		my ($data, $crc_in, $crc_out, $now);
		my $request = $channel eq 'A'
		    ? 'A5DF01FFFFFFFFFFFFFFFFFFFFFF'
		    : 'A5FF01FFFFFFFFFFFFFFFFFFFFFF';
		$response = $ua->get("$collector->{_baseurl}/1Wire/WriteBlock.html?Data=$request&Address=$addr")->content;
		if ($response =~ /Write Block Reply/) {
		    ($data, $crc_in) = $response =~
			/NAME="ResultData_0.*?VALUE="([\da-fA-F]{24})([\da-fA-F]{4})"/;
		    #
		    # The DalSemi CRC16 is output as 1s complement, byte swapped
		    #
		    ($crc_out = sprintf "%04X", ~crc16(pack "H*", $data) & 0xffff)
			=~ s/(..)(..)/$2$1/;
		    if ($crc_in ne $crc_out) {
			msg("warning", "CRC Error on counter sensor $addr\@$name");
			next DS2423;
			}
		    $v = hex(substr($data, 14, 2) . substr($data, 12, 2) .
			     substr($data, 10, 2) .  substr($data, 8, 2));
		    ($now) = $response =~ /NAME="Completed_0" VALUE="(\d+)"/;
		    #
		    # Elsewhere (in the logging daemon) we compute averages, min
		    # and max values, etc.  We just report raw values here, and
		    # search for undef because we don't care about the sensor type
		    # However, the return values from hunt_for_sensor are the real
		    # thermd addresses, and we need to use THAT to find the sensor
		    #
		    for my $n (hunt_for_sensor($collector, $addr, undef, "channel", $channel)) {
			my $vc = evaluate_counter($collector->{sensor}{$n}, $v, $now);
			next unless defined $vc;
			print "$n $vc\n";
#warn "$collector->{sensor}{$n}{name} $n $vc\n";
			}
		    }
		else {
		    msg("err", "Unknown answer for 2423 from HA7Net $name $response");
		    }
		}
	    }
        ####################################################################
	# NOTE: All 1-wire transactions beyond this point are guarded by an
	# HA7Net transaction lock because they require multiple HTTP requests
        ####################################################################
	$response = $ua->get("$collector->{_baseurl}/1Wire/GetLock.html")->content;
	if ($response =~ /Get Lock Result/) {
	    ($lockid) = $response =~ /NAME="LockID_0.*?VALUE="(\d+)"/;
	    }
	else {
	    msg("err", "Unknown lock reply for HA7Net $name");
	    next POLL;
	    }
	#
	# Now deal with the tricky case - the multi-value 2450 sensors.  We
	# have to read a few values, and assemble the direction data based
	# on a lookup table.
	#
	$count = 0;
	DS2450: while ($count++ < @{$collector->{_2450}}) {
	    my ($n, $val, $crc_in, $crc_out, $ChanA, $ChanB, $ChanC, $ChanD, $A, $B, $C, $D);
	    $id = shift @{$collector->{_2450}};
	    push @{$collector->{_2450}}, $id;

	    $url = "$collector->{_baseurl}/1Wire/WriteBlock.html?Address=$id&LockID=$lockid";
	    #
	    # Consult the DS2450 data sheet for the specific bit values used
	    # below and for alternate settings.
	    #
	    # Setup each of the four channels for A to D conversion.  We do
	    # this a four separate url calls.  This makes it a bit easier to
	    # understand and avoids a length restriction on WriteBlock strings
	    #
	    # We select 8-bits of resolution and set IR to 1 for a 0-5.12 volt
	    # range.  Therefore, we want to write the two byte (08 and 01) to
	    # locations 8 and 9 on memory page 1 (Control/Status) 
	    #
	    # This is a tricky url to construct since each written byte has
	    # it's own CRC and is ACKed byte by byte during the write cycle.
	    #
	    # Write Memory (55) starting at (0800) byte 08 byte 08 followed
	    # by 16 read cycles (FFFF) for the CRC and 8 read cycles (FF) for
	    # the echo of the written data.  Now write 01 int byte 9.  The
	    # address auto increments so we only need to include reads for
	    # CRC (FFFF) and echo'd data (FF).
	    #
	    ##$response = $ua->get("$url&Data=55080008FFFFFF01FFFFFF")->content;
	    ##$response =~ /NAME="ResultData_0.*?VALUE="([\da-fA-F]+)"/;
	    #
	    # And again for channel B, C and D of the AtoD.  I have optimized
	    # this below into two get's instead of four
	    #
	    ##$response = $ua->get("$url&Data=550A0008FFFFFF01FFFFFF")->content;
	    ##$response = $ua->get("$url&Data=550C0008FFFFFF01FFFFFF")->content;
	    ##$response = $ua->get("$url&Data=550E0008FFFFFF01FFFFFF")->content;
	    #
	    $ua->get("$url&Data=55080008FFFFFF01FFFFFF08FFFFFF01FFFFFF");
	    $ua->get("$url&Data=550C0008FFFFFF01FFFFFF08FFFFFF01FFFFFF");
	    #
	    #  Now we do an AtoD conversion (3c) of all 4 channels (0F00).
	    #  Don't forget the read time slots for the CRC (FFFF)
	    #
	    $response = $ua->get("$url&Data=3C0F00FFFF")->content;
	    #
	    # Read the results from memory page 0.  Since we only asked for an
	    # 8-bit conversion only use the most signficant byte of the result.
	    #
	    # Build the url with read command (AA) from Page 0 (0000), read
	    # cycle for 8 bytes of return data (16 FFs) and a 2 byte CRC (FFFF)
	    #
	    $response = $ua->get("$url&Data=AA0000FFFFFFFFFFFFFFFFFFFF")->content;
	    ($val, $crc_in) = $response =~
		/NAME="ResultData_0.*?VALUE="([\da-fA-F]{22})([\da-fA-F]{4})"/;
	    #
	    # The DalSemi CRC16 is output as 1s complement, byte swapped
	    #
	    ($crc_out = sprintf "%04X", ~crc16(pack "H*", $val) & 0xffff) =~
		 s/(..)(..)/$2$1/;
	    if ($crc_in ne $crc_out) {
		msg("warning", "CRC Error on direction sensor $id\@$name");
		next DS2450;
		}
	    #
	    # Extract the 4 values and scale them to high/medium/low
	    #
	    $A = int(hex(substr($val, 8, 2)) / (256/3));
	    $B = int(hex(substr($val, 12, 2)) / (256/3));
	    $C = int(hex(substr($val, 16, 2)) / (256/3));
	    $D = int(hex(substr($val, 20, 2)) / (256/3));

	    for my $n (hunt_for_sensor($collector, $id, "direction")) {
		my $sensor = $collector->{sensor}{$n};
		if ($sensor->{inverted}) {
		    $val = $compass_lookup{"$D$C$B$A"};
		    }
		else {
		    $val = $compass_lookup{"$A$B$C$D"};
		    }
		next DS2450 unless defined $val;
		print "$n $val\n";
		}
	    }
	#
	# Now deal with the thermocouples on the HA7Net
	#
	$count = 0;
	DS2760: while ($count++ < @{$collector->{_2760}}) {
	    my ($data, $bits, $cold_junction, $uv, $temp, $type, $pair);
	    $pair = shift @{$collector->{_2760}};
	    push @{$collector->{_2760}}, $pair;
	    ($id, $type) = @$pair;
	    $url = "$collector->{_baseurl}/1Wire/WriteBlock.html?Address=$id&LockID=$lockid";
	    #
	    # Read the cold junction temperature
	    #
	    $response = $ua->get("$url&Data=6918FFFF")->content;
	    if ($response =~ /Write Block Reply/) {
		#
		# Unlike the 2438 (which is LSB/MSB), the 2760 is MSB/LSB,
		# but units are 0.125 degreesC
		#
		($data) = $response =~
		    /NAME="ResultData_0.*?VALUE="([\da-fA-F]{8})"/;
		$bits = hex(substr($data, 4, 4));
		if ($bits & 0x8000) {
		    $cold_junction = - (((~$bits + 1) & 0x7FFF) >> 5) * 0.125;
		    }
		else {
		    $cold_junction = ($bits >> 5) * 0.125;
		    }
		}
	    else {
		msg("err", "Unknown temp answer for 2760 from HA7Net $name");
		next DS2760;
		}
	    #
	    # Read the thermocouple microvolts
	    #
	    $response = $ua->get("$url&Data=690EFFFF")->content;
	    if ($response =~ /Write Block Reply/) {
		#
		# Microvolts bits as also MSB/LSB, in 15.625uv units
		#
		($data) = $response =~
		    /NAME="ResultData_0.*?VALUE="([\da-fA-F]{8})"/;
		$bits = hex(substr($data, 4, 4));
		if ($bits & 0x8000) {
		    $uv = - (((~$bits + 1) & 0x7FFF) >> 3) * 15.625;
		    }
		else {
		    $uv = ($bits >> 3) * 15.625;
		    }
		}
	    else {
		msg("err", "Unknown uv answer for 2760 from HA7Net $name");
		next DS2760;
		}
	    $temp = $cold_junction + calculate_polynomial($uv, $type);
	    for my $n (hunt_for_sensor($collector, $id, "temperature")) {
		print "$n $temp\n";
		}
	    }
	#
	# Now deal with the hard cases - the multi-value 2438 sensors.  We
	# have to read a few values, and assemble the temperature and humidity
	# (or barometer or current) values based on a formula.  Since the
	# sensor number has no suffix (but the logging is based on possibly
	# two Sensor blocks with different suffixed IDs), we have to hunt for
	# the blocks after we get the readings.  The RH algorithm is from
	# http://www.sensorsmag.com/sensors/article/articleDetail.jsp?id=361379
	#
	$count = 0;
	DS2438: while ($count++ < @{$collector->{_2438}}) {
	    my ($n, $data, $bits, $vdd, $vad, $temp, $i, $crc_in, $crc_out);
	    # Rotate sensors...
	    $id = shift @{$collector->{_2438}};
	    push @{$collector->{_2438}}, $id;

	    $url = "$collector->{_baseurl}/1Wire/WriteBlock.html?Address=$id&LockID=$lockid";

	    # Write scratchpad page 0 with 0x09 (set A/D to battery input (VDD)
	    # and read current register)
	    $ua->get("$url&Data=4e0009");
	    $ua->get("$url&Data=4800");	# Copy scratchpad 0 to memory page 0
	    $ua->get("$url&Data=44");	# Do a Convert T
	    $ua->get("$url&Data=B4");	# Do a Convert V
	    $ua->get("$url&Data=B800");	# Recall Memory page 0 to scratchpad 0
	    # Read Scratchpad 0 (8 bytes of page 0)
	    $response = $ua->get("$url&Data=BE00FFFFFFFFFFFFFFFFFF")->content;
	    # Assume it returned BE0008E00DA901000000E9:
	    #  BE 00  command and page
	    # Breakdown of page 0 of DS2438
	    #  08	status byte
	    #  E0 0D  temp    LSB/MSB 	0DE0 = 13.875
	    #  A9 01  volt    LSB/MSB	01A9 =  4.25
	    #  00 00  current LSB/MSB
	    #  E9     crc8
	    if ($response =~ /Write Block Reply/) {
		($data, $crc_in) = $response =~
		    /NAME="ResultData_0.*?VALUE="([\da-fA-F]{20})([\da-fA-F]{2})"/;
		#
		# The DalSemi CRC8 only counts the memory data
		#
		$crc_out = sprintf "%02X", crc8(pack "H*", substr($data, 4));
		if ($crc_in ne $crc_out) {
		    msg("warning", "CRC Error(1) on sensor $id\@$name");
		    next DS2438;
		    }
		$bits = hex(substr($data, 8, 2) . substr($data, 6, 2));
		if ($bits & 0x8000) {
		    $temp = - (((~$bits + 1) & 0x7FFF) >> 3) / 32;
		    }
		else {
		    $temp = ($bits >> 3) / 32;
		    }
		$vdd =  hex(substr($data, 12, 2) . substr($data, 10, 2)) / 100;
		$bits = hex(substr($data, 16, 2) . substr($data, 14, 2));
		if ($bits & 0x8000) {
		    $i = - (~$bits + 1) & 0x03FF;
		    }
		else {
		    $i = $bits & 0x3FF;
		    }
		}
	    else {
		msg("err", "Unknown response for 2438 from HA7Net $name");
		next DS2438;
		}

	    #
	    # Temperature and sunlight can be reported on now
	    #
	    for my $n (hunt_for_sensor($collector, $id, "temperature")) {
		print "$n $temp\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "sunlight")) {
		print "$n $i\n";
		}

	    #
	    # The barometer and humidity sensors need Vad, so if they are
	    # being reported, read Vad (otherwise, just go to the next sensor
	    # and save some time :-)
	    #
	    unless (defined(hunt_for_sensor($collector, $id, "humidity")) ||
	    	defined(hunt_for_sensor($collector, $id, "barometer"))) {
		next DS2438;
		}

	    # Write scratchpad page 0 with 0x00 (set A/D to gp input (Vad)
	    # without reading current register
	    $ua->get("$url&Data=4e0000");
	    $ua->get("$url&Data=4800");	# Copy scratchpad 0 to memory page 0
	    $ua->get("$url&Data=B4");	# Do a Convert V (don't bother with T)
	    $ua->get("$url&Data=B800");	# Recall Memory page 0 to scratchpad 0
	    # Read Scratchpad 0 (8 bytes of page 0)
	    $response = $ua->get("$url&Data=BE00FFFFFFFFFFFFFFFFFF")->content;

	    if ($response =~ /Write Block Reply/) {
		($data, $crc_in) = $response =~
		    /NAME="ResultData_0.*?VALUE="([\da-fA-F]{20})([\da-fA-F]{2})"/;
		#
		# The DalSemi CRC8 only counts the memory data
		#
		$crc_out = sprintf "%02X", crc8(pack "H*", substr($data, 4));
		if ($crc_in ne $crc_out) {
		    msg("warning", "CRC Error(2) on sensor $id\@$name");
		    next DS2438;
		    }
		$vad =  hex(substr($data, 12, 2) . substr($data, 10, 2)) / 100;
		}
	    else {
		msg("err", "Unknown answer for 2438 from HA7Net $name");
		next DS2438;
		}

	    for my $n (hunt_for_sensor($collector, $id, "humidity")) {
		my $sensorRH = (eval { ($vad / $vdd) } - 0.16) / 0.0062;
		my $trueRH = $sensorRH / (1.0546 - 0.00216 * $temp);
		print "$n $trueRH\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "barometer")) {
		my $sensor = $collector->{sensor}{$n};
		my $pressure = $vad * $sensor->{slope} + $sensor->{intercept};
		print "$n $pressure\n";
		}
	    }
	}
    continue {
        ####################################################################
	# Release the HA7Net transaction lock
        ####################################################################
	if ($lockid) {
	    # Release the lock
	    $ua->get("$collector->{_baseurl}/1Wire/ReleaseLock.html?LockID=$lockid");
	    undef $lockid;
	    }
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the HA7Net poller exit?");
    }

sub fork_wunderground_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $response, $xml, $device, $devices, $n, $t,
	$delta_t, $next_poll_time);
    my $fd = new FileHandle;

    print "Forking Wunderground group poller\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Wunderground $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    eval qq{ use XML::Simple; };
    die $@ if $@;

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, <@{[ scalar @{$collector->{_c}} ]} PWS> @ api.wunderground.com";
    #
    # Continually read the data URL and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	$next_poll_time = $start + 5*60;	# Arbitrary +5 minutes from now
	SUB: for my $sub (@{ $collector->{_c} }) {
	    if ($start >= $sub->{_next_poll_time}) {
		$sub->{_next_poll_time} = $start + $sub->{pollinterval};
		$next_poll_time = min($sub->{_next_poll_time}, $next_poll_time);
		}
	    else {
		$next_poll_time = min($sub->{_next_poll_time}, $next_poll_time);
		next SUB;
		}
	    $response = $ua->get($sub->{_baseurl});
	    if ($response->is_error) {
		msg("err", "Cannot contact Wunderground at $sub->{_baseurl}, " .
		    $response->status_line);
		next LOOP;
		}
	    $xml = XMLin($response->content, SuppressEmpty => 1);
	    # Convert (broken) RFC822 time to HTTP Time (which str2time can
	    # parse).  Use "now" just in case the parse fails
	    $t = $xml->{observation_time_rfc822};
	    $t =~ s/^[^,]+,\s+//;	# Eliminate day of week
	    $t =~ s/(\s\w{3})\w*/$1/;	# Reduce to 3-letter month
	    $t =~ s/(\d+):(\d+):(\d+)/sprintf "%02d:%02d:%02d",$1,$2,$3/e;
	    $delta_t = time - (str2time($t) || time);
	    if ($delta_t > $sub->{staleafter}) {	# Stale data
		msg("notice", "Data for Weather Station $sub->{stationid} is stale ($delta_t sec > StaleAfter $sub->{staleafter} sec)");
		next SUB;
		}
	    if (exists $sub->{sensor}{B} && defined $xml->{pressure_in}) {
		print "B $xml->{pressure_in} $sub->{_name}\n"
		    unless $xml->{pressure_in} == -999;
		}
	    if (exists $sub->{sensor}{D} && defined $xml->{wind_degrees}) {
		#
		# Wind needs to be rounded to the nearest 22.5 degrees, so our
		# consensus algorithm works.
		#
		print "D @{[int(($xml->{wind_degrees}/22.5 + .5)) * 22.5]} $sub->{_name}\n"
		    unless $xml->{wind_degrees} == -999;
		}
	    if (exists $sub->{sensor}{G} && defined $xml->{wind_gust_mph}) {
		print "G $xml->{wind_gust_mph} $sub->{_name}\n"
		    unless $xml->{wind_gust_mph} == -999;
		}
	    if (exists $sub->{sensor}{H} && defined $xml->{relative_humidity}) {
		print "H $xml->{relative_humidity} $sub->{_name}\n"
		    unless $xml->{relative_humidity} == -999;
		}
	    if (exists $sub->{sensor}{S} && defined $xml->{wind_mph}) {
		print "S $xml->{wind_mph} $sub->{_name}\n"
		    unless $xml->{wind_mph} == -999;
		}
	    if (exists $sub->{sensor}{T} && defined $xml->{temp_f}) {
		print "T $xml->{temp_f} $sub->{_name}\n"
		    unless $xml->{temp_f} == -999;
		}
	    if (exists $sub->{sensor}{P} && defined $xml->{dewpoint_f}) {
		print "P $xml->{dewpoint_f} $sub->{_name}\n"
		    unless $xml->{dewpoint_f} == -999;
		}
	    }
	}
    continue {
	#
	# Handled slightly differently than the rest of the pollers.  We
	# compute the next time to poll for each of the sub-collectors in
	# the body of the loop.  Just sleep until that time is to come
	# (unless it has already passed, in which case don't sleep at all).
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now > $next_poll_time ?  0 : $next_poll_time - $now);
	}
    msg("err", "How did the Wunderground poller exit?");
    }

sub fork_weathergoose_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $response, $xml, $device, $devices, $n);
    my $fd = new FileHandle;

    print "Forking WeatherGoose-style poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for WeatherGoose $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    eval qq{ use XML::Simple; };
    die $@ if $@;

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Continually read the data URL and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	$response = $ua->get("$collector->{_baseurl}/data.xml");
	if ($response->is_error) {
	    msg("err", "Cannot contact $name at $collector->{_baseurl}, " .
		$response->status_line);
	    next LOOP;
	    }
	$xml = XMLin($response->content);
	#
	# Distinguish between just a Goose and a Goose with external sensors.
	# The latter is a hash of sensors, so just build one if we only have
	# an unextended Goose.
	#
	if ($xml->{devices}->{device}->{id} =~ /^[0-9A-F]{16}$/) {
	    $devices = { $xml->{devices}->{device}->{name} =>
		$xml->{devices}->{device} };
	    }
	else {
	    $devices = $xml->{devices}->{device};
	    }
	DEVICE: while (($name, $device) = each %$devices) {
	    my $id = $device->{id};
	    unless ($device->{type} =~ m#^(WxGoos3?|PowerStrip|(Temp|AirFlow)Sensor$)#) {
		warn "Unknown sensor type $device->{type} $name in WeatherGoose $0\n";
		next DEVICE;
		}
	    next DEVICE unless $device->{available};
	    for my $n (hunt_for_sensor($collector, $id, "temperature")) {
		print "$n $device->{field}->{TempC}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "humidity")) {
		print "$n $device->{field}->{Humidity}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "airflow")) {
		print "$n $device->{field}->{Airflow}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "sound")) {
		print "$n $device->{field}->{Sound}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "light")) {
		print "$n $device->{field}->{Light}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "io1")) {
		print "$n $device->{field}->{IO1}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "io2")) {
		print "$n $device->{field}->{IO2}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "io3")) {
		print "$n $device->{field}->{IO3}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "volts")) {
		print "$n $device->{field}->{Volts}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "volts-min")) {
		print "$n $device->{field}->{'Volt-Min'}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "volts-max")) {
		print "$n $device->{field}->{'Volt-Max'}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "volts-peak")) {
		print "$n $device->{field}->{'Volt-Pk'}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "amps")) {
		print "$n $device->{field}->{Amps}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "amps-peak")) {
		print "$n $device->{field}->{'Amps-Pk'}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "kwh")) {
		print "$n $device->{field}->{'KWatt-hrs'}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "real-power")) {
		print "$n $device->{field}->{RealPower}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "apparent-power")) {
		print "$n $device->{field}->{ApPower}->{value}\n";
		}
	    for my $n (hunt_for_sensor($collector, $id, "power-factor")) {
		print "$n $device->{field}->{'Pwr-Factor%'}->{value}\n";
		}
	    }
	}
    continue {
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the Weathergoose poller exit?");
    }

sub fork_smartnet_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $response);
    my $fd = new FileHandle;

    print "Forking SmartNet poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Proliphix $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Continually read the data and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	my (@hold, @query, $when, $err);
	if ($collector->{_smartwatt}) {
	    @hold = ();
	    while (@query = splice(@{ $collector->{_smartwatt} }, 0, 10)) {
		$response = $collector->{_ua}->simple_request('SmartWatt.Read', @query);
		$when = shift @{$response};
		if ($err = shift @{$response}) {
		    msg("err", "Error code $err on SmartNet $name");
		    }
		for my $r (@{$response}) {
		    for my $n (hunt_for_sensor($collector, $r->{ROMID}, "watts")) {
			my $sensor = $collector->{sensor}{$n};
			if ($sensor->{_prev_time}) {
			    my $v = ($r->{WattHr} - $sensor->{_prev_Wh}) /
				($when - $sensor->{_prev_time});
			    print "$n\t$v\n";
			    }
			$sensor->{_prev_time} = $when;
			$sensor->{_prev_Wh} = $r->{WattHr};
			}
		    for my $n (hunt_for_sensor($collector, $r->{ROMID}, "wh")) {
			print "$n\t$r->{WattHr}\n";
			}
		    }
		push @hold, @query;
		}
	    $collector->{_smartwatt} = [ @hold ];
	    }
	if ($collector->{_smartsense}) {
	    @hold = ();
	    while (@query = splice(@{ $collector->{_smartsense} }, 0, 10)) {
		$response = $collector->{_ua}->simple_request('SmartSenseTH.Read', @query);
		$when = shift @{$response};
		if ($err = shift @{$response}) {
		    msg("err", "Error code $err on SmartNet $name");
		    }
		for my $r (@{$response}) {
		    for my $n (hunt_for_sensor($collector, $r->{ROMID}, "temperature")) {
			print "$n\t$r->{Temperature}\n";
			}
		    for my $n (hunt_for_sensor($collector, $r->{ROMID}, "humidity")) {
			print "$n\t$r->{TRH}\n";
			}
		    for my $n (hunt_for_sensor($collector, $r->{ROMID}, "dewpoint")) {
			print "$n\t$r->{DewPoint}\n";
			}
		    }
		push @hold, @query;
		}
	    $collector->{_smartsense} = [ @hold ];
	    }
	}
    continue {
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the smartnet poller exit?");
    }

sub fork_proliphix_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, %request, %check, $response, $v);
    my $fd = new FileHandle;
    my %oid = (
		TA => "OID4.1.13",
		T1 => "OID4.3.2.1",
		T2 => "OID4.3.2.2",
		T3 => "OID4.3.2.3",
		H1 => "OID4.1.14",
		S  => "OID4.1.2",
		);
    my %sensor = reverse %oid;		# Unique reverse mapping

    print "Forking Proliphix poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Proliphix $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Build the list of OIDs to poll
    #
    %request = map { ($oid{uc($_)} => "")} keys %{ $collector->{sensor} };
    #
    # Continually read the data URL and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	$response = $ua->post("$collector->{_baseurl}/get", \%request);
	if ($response->is_error) {
	    msg("err", "Cannot contact $name at $collector->{_baseurl}, " .
		$response->status_line);
	    next LOOP;
	    }
	%check = %request;
	for my $kv (split /&/, $response->content) {
	    my ($k, $v) = split /=/, $kv;
	    if (exists $sensor{$k}) {
		$v /= 10	if $sensor{$k} =~ /^T/;	# Reads deci-degrees F
		print "$sensor{$k}\t$v\n";
		delete $check{$k};
		}
	    else {
		msg("err", "Unexpected OID $k returned from Proliphix $name");
		}
	    }
	if (keys %check) {
	    msg("err", "@{[keys %check]} not returned for Proliphix $name");
	    }
	}
    continue {
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the Proliphix poller exit?");
    }

sub fork_hwg_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $response, $values, $v);
    my $fd = new FileHandle;

    print "Forking HWg poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for HWg $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";

    eval qq{ use XML::Simple; };
    die $@ if $@;

    #
    # Continually read the data URL and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	$response = $ua->get("$collector->{_baseurl}/values.xml");
	if ($response->is_error) {
	    msg("err", "Cannot contact $name at $collector->{_baseurl}, " .
		$response->status_line);
	    next LOOP;
	    }
	$values = XMLin($response->content);
	#
	# Output the values we just read
	#
	for my $h (@{ $values->{BinaryInSet}{Entry} }) {
	    printf "B%d %d\n", $h->{ID}, $h->{Value};
	    }
	for my $h (@{ $values->{BinaryOutSet}{Entry} }) {
	    printf "A%d %d\n", $h->{ID} - 150, $h->{Value};
	    }
	for my $h (@{ $values->{SenSet}{Entry} }) {
	    printf "%d %.3f\n", $h->{ID}, $h->{Value};
	    }
	}
    continue {
	#
	# The HWg constantly reads the sensors (so when we poll, we get an
	# instant answer), but we still need to honor PollInterval.
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the HWg poller exit?");
    }

sub fork_em1_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $response, @values, $v);
    my $fd = new FileHandle;
    my @sensors = qw(T1 H1 W1 T2 H2 W2 T3 H3 W3 T4 H4 W4);

    print "Forking EM1 poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for EM1 $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Continually read the data URL and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	$response = $ua->get("$collector->{_baseurl}/data");
	if ($response->is_error) {
	    msg("err", "Cannot contact $name at $collector->{_baseurl}, " .
		$response->status_line);
	    next LOOP;
	    }
	@values = split /\|/, $response->content;
	if (@values > 10) {
	    shift @values;	# Skip 0th field (temperature scale)
	    }
	else {
	    msg("err", "Unknown response from EM1 $name");
	    }
	#
	# Output the values we just read, skipping missing sensors
	#
	for my $k (@sensors) {
	    $v = shift @values;
	    next if $v == -999.9;
	    print "$k $v\n";
	    }
	}
    continue {
	#
	# The EM1 constantly reads the sensors (so when we poll, we get an
	# instant answer), but we still need to honor PollInterval.
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the EM1 poller exit?");
    }

sub fork_roomalert_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $response, $content, $v, $count, $out);
    my $fd = new FileHandle;
    use constant UNK_FIRMWARE => 0;
    use constant OLD_FIRMWARE => 1;
    use constant NEW_FIRMWARE => 2;
    my $firmware = UNK_FIRMWARE;

    print "Forking Room Alert poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Room Alert $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Continually read the data URL and split up the answer
    #
    LOOP: while (1) {
	$start = time;
	if ($firmware == UNK_FIRMWARE) {
	    $response = $ua->get("$collector->{_baseurl}/getData.htm");
	    if ($response->code == 400) {
		$firmware = OLD_FIRMWARE;
		redo LOOP;
		}
	    else {
		$firmware = NEW_FIRMWARE;
		redo LOOP;
		}
	    }
	elsif ($firmware == OLD_FIRMWARE) {
	    $response = $ua->get("$collector->{_baseurl}/getData.cgi");
	    if ($response->code == 400) {
		$firmware =  UNK_FIRMWARE;
		redo LOOP;
		}
	    }
	elsif ($firmware == NEW_FIRMWARE) {
	    $response = $ua->get("$collector->{_baseurl}/getData.htm");
	    if ($response->code == 400) {
		$firmware =  OLD_FIRMWARE;
		redo LOOP;
		}
	    }
	else {
	    die "Roomalert firmware inconsistency error";
	    }

	if ($response->is_error) {
	    msg("err", "Cannot contact $name at $collector->{_baseurl}, " .
		$response->status_line);
	    next LOOP;
	    }
	$content = $response->content;
	#
	# Basically, we convert the RoomAlert JSON output (an almost
	# Perl-like data structure) into Perl, and eval it, after doing some
	# simple data protection and encoding/decoding
	#
	if ($content =~ /^{name:/) {           # comment: to balance curly's }
	    $content =~ s/"([^"]*)"/'"' . encode_base64($1, "") . '"'/ge;
	    $content =~ s/:/ => /g;
	    $content =~ s/"([^"]*)"/'"' . decode_base64($1) . '"'/ge;
	    eval "\$out = $content";
	    }
	else {
	    msg("err", "Unknown response from Room Alert $name\n$content");
	    $out = {};
	    }
	#
	# Output the values we just read, skipping missing sensors
	#
	$count = 0;
	for my $th (@{ $out->{internal_sen} }) {
	    print "T0 $th->{tempc}\n"		if exists $th->{tempc};
	    print "H0 $th->{humid}\n"		if exists $th->{humid};
	    print "Power $th->{status}\n"	if $th->{type} eq "power";
	    print "Flood $th->{flood_status}\n"	if $th->{type} eq "flood";
	    }
	$count = 1;
	for my $th (@{ $out->{sensor} }) {
	    print "T$count $th->{tempc}\n";
	    print "H$count $th->{humid}\n"	if exists $th->{humid};
	    $count++;
	    }
	$count = 1;
	for my $th (@{ $out->{switch_sen} }) {
	    print "S$count $th->{status}\n";
	    $count++;
	    }
	for my $th (@{ $out->{wireless_sen} }) {
	    next if $th->{serial} eq "000000000000";
	    print "$th->{serial}T0 $th->{tempc}\n";
	    $count = 1;
	    for my $swit (@{ $th->{swit_sen} }) {
		print "$th->{serial}S$count $swit->{status}\n";
		$count++;
		}
	    $count = 1;
	    for my $digi (@{ $th->{digi_sen} }) {
		print "$th->{serial}T$count $digi->{tempc}\n" if exists $digi->{tempc};
		print "$th->{serial}H$count $digi->{humid}\n" if exists $digi->{humid};
		$count++;
		}
	    }
	}
    continue {
	#
	# The unit constantly reads the sensors (so when we poll, we get an
	# instant answer), but we still need to honor PollInterval.
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the Roomalert poller exit?");
    }

sub fork_commandline_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($now, $start);
    my $fd = new FileHandle;

    print "Forking CommandLine poller for <Collector $name>\n" if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0     && last;	# Child - the real work is below
	$pid == -1    && die "Cannot fork collector for CommandLine $name";
	   
	print "  Filenumber ", $fd->fileno(), "\n"  if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"    if $opt_verbose;
	return $pid;		# Parent returns PID
	}
						    
    $is_child++;
    close_logfiles();
    close $fd;		# We only use STDOUT in child
    $| = 1;         	# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    undef %config;	# Cleans up a lot of extraneous crap...
    $0 = "$script commandline poller, $name";

    #
    # Continually rotate through the collection of sensors
    #
    while (1) {
	$start = time;
	while (my ($n, $sensor) = each %{ $collector->{sensor} }) {
	    my $val = `$sensor->{command}`;
	    $val =~ s/^\s*//;
	    $val =~ s/\n.*$//s;
	    $val =~ s/\s*$//;
	    print "$n $val\n";
	    }
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;    # Must read AFTER all the get()'s
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the CommandLine poller exit?");
    }


sub fork_veris_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($now, $start, $result, $veris, $sensor, @addrs);
    my ($A, $M);
    my $errcnt = 0;
    my $fd = new FileHandle;

    print "Forking Veris poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Veris $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $veris = $collector->{_modbus_device};
    $A = $collector->{amperage};
    $M = $collector->{_multiplier_key};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name";
    $SIG{ALRM} = sub { print STDERR "Timeout!\n"; die "Timeout!" };
    LOOP: while (1) {
	$start = time;
	eval { $result = $veris->read(40001..$collector->{_last_modbus}); };
	if ($@) {
	    msg("err", $@);
	    msg("err", "Modbus error count $errcnt");
	    # If the Veris stops replying and we are not hardwired on a
	    # serial line, the Ether-to-serial device may have crashed.  Try
	    # to re-establish a connection with it, and retry the read...
	    if (++$errcnt > 3 && ! $collector->{device}) {
		msg("err", "reopening modbus device");
		close $collector->{_fd};
		sleep 1;
		$collector->{_fd} = unit_open($collector, $collector->{baudrate});
		my $modbus = new Modbus::Client $collector->{_fd};
		$veris = $collector->{_modbus_device} = $modbus->device($collector->{modbusaddress});
		$errcnt = 0;
		}
	    next LOOP;
	    }
	$errcnt = 0;
	printf "40001\t%.3f\n", 
	     $result->{40001} * $veris{40001}{multiplier}{$M}{$A} +
	     $result->{40002} * $veris{40002}{multiplier}{$M}{$A};
	for my $n (40003..$collector->{_last_modbus}) {
	    printf "%d\t%.3f\n",
		$n, $result->{$n} * $veris{$n}{multiplier}{$M}{$A};
	    }
	}
    continue {
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER the read from modbus
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the Veris poller exit?");
    }

sub fork_enersure_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($now, $start, $result, $enersure, $sensor, @addrs);
    my ($v, $i, $p, $w, $k, $ks);
    my $errcnt = 0;
    my $fd = new FileHandle;

    print "Forking Enersure poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for Enersure $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $enersure = $collector->{_modbus_device};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name";
    #
    # Build the list of sensors to poll.  It is MUCH faster to read all the
    # registers in a bank (and thence all registers in multiple banks) if we
    # are reading more than one register per bank.  So just take the simple
    # approach and say "if we read one, we read them all".
    #
    for my $n (@{ $collector->{_bank} }) {
	push @addrs, (30001+$n*10..30010+$n*10);
	}
    $ks = eval { $enersure->read_one(30010) } || 1;
    $SIG{ALRM} = sub { print STDERR "Timeout!\n"; die "Timeout!" };
    LOOP: while (1) {
	$start = time;
	eval { $result = $enersure->read(@addrs); };
	if ($@) {
	    msg("err", $@);
	    msg("err", "Modbus error count $errcnt");
	    # If the Enersure stops replying and we are not hardwired on a
	    # serial line, the Ether-to-serial device may have crashed.  Try
	    # exiting, which will signal that the child has died, and we'll
	    # try to re-establish a connection with it, and retry the read...
	    if (++$errcnt > 3 && ! $collector->{device}) {
		msg("err", "modbus device may have failed - exiting to retry");
		exit 0;
		}
	    next LOOP;
	    }
	$errcnt = 0;
	for my $n (@{ $collector->{_bank} }) {
	    $v = 30001 + 0 + ($n * 10); printf "V$n %.1f\n", $result->{$v}/10;
	    $i = 30001 + 2 + ($n * 10); printf "I$n %.3f\n", $result->{$i}/100;
	    $p = 30001 + 4 + ($n * 10); printf "P$n %.3f\n", $result->{$p}/1000;
	    $w = 30001 + 5 + ($n * 10); printf "W$n %.3f\n", $result->{$w};
	    $k = 30001 + 7 + ($n * 10); printf "K$n %.3f\n", $result->{$k}/$ks;
	    }
	}
    continue {
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER the read from modbus
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the Enersure poller exit?");
    }

sub fork_snmp_poller {
    my $collector = shift;
    my $name = $collector->{_name};
    my ($ua, $now, $start, $result, $sensor, $oid, @oids, %oids, $v);
    my $fd = new FileHandle;

    print "Forking SNMP poller for <Collector $name>\n"	if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork collector for SNMP $name";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    $is_child++;
    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $| = 1;			# And it gets flushed at every write!
    $SIG{HUP} = $SIG{INT} = $SIG{TERM} = "DEFAULT";
    $ua = $collector->{_ua};
    undef %config;		# Cleans up a lot of extraneous crap...
    $0 = "$script poller, $name @ $collector->{ipaddress}";
    #
    # Build the list of OIDs to poll (we only need to poll any OID once,
    # even if there are duplicates in the list)
    #
    for my $n (sort keys %{ $collector->{sensor} }) {
	$sensor = $collector->{sensor}{$n};
	$oids{ $sensor->{_oid} }++;
	}
    @oids = sort keys %oids;
    #
    # Continually read the OIDs
    #
    while (1) {
	$start = time;
	$result = $ua->get_request(-varbindlist => \@oids);
	SENSOR: for my $n (sort keys %{ $collector->{sensor} }) {
	    $sensor = $collector->{sensor}{$n};
	    $oid = $sensor->{_oid};
	    next unless defined($v = $result->{$oid});
	    #
	    # OnOff's are handled the same, regardless of collector
	    #
	    if ($sensor->{type} eq "onoff") {
		if ($v == $sensor->{snmponvalue}) {
		    print "$n 1\n";
		    }
		elsif ($v == $sensor->{snmpoffvalue}) {
		    print "$n 0\n";
		    }
		else {
		    msg("err", "Unexpected SNMP value of $v for Sensor $sensor->{name}.  Do you need to define SNMPOnValue or SNMPOffValue?");
		    print "$n $v\n";
		    }
		next SENSOR;
		}
	    #
	    # Some sensors are special, depending on collector
	    #
	    if ($collector->{type} eq "hwg") {
		if ($sensor->{type} =~ /^(temperature|humidity|volts|milliamps)$/) {
		    printf "%d %.1f\n", $n, $v/10;
		    }
		else {
		    print "$n $v\n";
		    }
		}
	    elsif ($collector->{type} eq "snmp") {
		if ($sensor->{type} eq "counter") {
		    $v = evaluate_counter($sensor, $v);
		    print "$n $v\n";
		    }
		elsif ($sensor->{type} eq "gauge") {
		    print "$n $v\n";
		    }
		else {
		    die "Unpossible SNMP sensor type $sensor->{type}";
		    }
		}
	    else {
		msg("err", "Unknown collector type in fork_snmp_poller!");
		print "$n $v\n";
		}
	    }
	}
    continue {
	#
	# See comments at end of fork_owfs_poller
	#
	$now = time;	# Must read AFTER the get_request
	sleep ($now - $start > $collector->{pollinterval} ?
	    0 : $collector->{pollinterval} - ($now - $start));
	}
    msg("err", "How did the SNMP poller exit?");
    }

sub fork_snmp_traphandler {
    my $collector = shift;
    my $fd = new FileHandle;

    print "Forking SNMP trap handler\n"		if $opt_verbose;
    for my $pid (open $fd, "-|") {
	$pid == 0	&& last;	# Child - the real work is below
	$pid == -1	&& die "Cannot fork SNMP trap handler";

	print "  Filenumber ", $fd->fileno(), "\n"	if $opt_verbose;
	$collector->{_fd} = $fd;
	print "  Poller PID is $pid\n"	if $opt_verbose;
	return $pid;		# Parent returns PID
	}

    close_logfiles();
    close $fd;			# We only use STDOUT in child
    $0 = "$script snmptrapd poller";
    while (1) {
	exec (qw(snmptrapd -f -OnQ -C --disableAuthorization=true -Lo -F), '%a %v\n', "-f", "UDP:$config{snmptrapport}");
	msg("err", "snmptrapd poller failed - retrying in 15 seconds");
	sleep 15;
	}
    }

sub unstick {
    alarm(0);			# In case we weren't called by an alarm
    msg("info", "Unsticking stuck poller\n");
    die "Unsticking - too much time\n";
    }

#
# The subroutine my_select is needed because select should not be used with
# buffered I/O, and because the QK145 uses \n\r as a line terminator, and
# the MaxBotix sonar devices use \r.  So we read ONE character per FD for
# every successful select call, and when we have read a line from any sensor,
# we return that full line, pre-trimmed
#
sub my_select {
    my ($rin, $rfd, $rescan) = @_;
    my ($rout, $rret, $ri, $ro, $fileno, $k, $char, $start, $count);
    my @my_select_tmp;		# No persistent data across invocations, that
				# is, throw out half-formed lines (which should
				# only occur if we hit our timeouts)
    our @rfd;			# Persistent descriptor list, though

    @rfd = keys %$rfd	if $rescan;	# Initialize (mostly) once
    #
    # For every FD that could posssibly have data, do a non-blocking select
    # (which means if there is no data waiting, we timeout immediately and
    # return no lines of data).  But if there are any lines that have data,
    # then (again, non-blocking) suck as much data as you can from FD while
    # preserving "lines" of data.
    #
    print "In my_select\n"	if $opt_verbose;
    if (select($rout=$rin, undef, undef, 0)) {
	$count = 0;
	FD: while ($fileno = shift @rfd) {
	    #
	    # Rotate though collectors, in case of timeout, but make sure that
	    # when we finish the list, we break and start at the right place
	    #
	    if (++$count > scalar keys %$rfd) {
		unshift @rfd, $fileno;
		last FD;
		}
	    else {
		push @rfd, $fileno;
		}
	    next FD unless vec($rout, $fileno, 1);
	    $k = $rfd->{$fileno};
	    print "  Draining $k"	if $opt_verbose;
	    #
	    # Completely drain any data waiting on _this_ $fileno.  Note that
	    # on FreeBSD 5.3, there is a bug somewhere in the OS, Perl (or
	    # possibly even my code :-), wherein the polling call to select
	    # returns "true" even if there is nothing to read (and then the
	    # sysread call blocks waiting for a character).  This does not
	    # happen reliably, but when it does, it appears to stay "stuck"
	    # in this mode.  My solution is this - since we want to drain
	    # waiting characters (and not wait for new ones), if 2 seconds
	    # elapses while reading from one file descriptor, assume that
	    # we're stuck and go on to the next descriptor.  Even on a
	    # heavily loaded system, we can read thousands of characters in
	    # 2 seconds, so this ought to do the trick.
	    #
	    # This clever code fails on Linux due to a different signal
	    # handling behavior, so the solution is simply to not set the
	    # alarm!  Since Linux doesn't appear have the select bug, we're
	    # probably cool with that.
	    #
	    # Correction - Some Linux DO have the bug :-(  I previously used
	    # an alarm timeout, I have now changed the code to do calls to
	    # time() - but only once per line, to reduce overhead.  And also
	    # set an alarrm for 3 seconds (which is longer than the in-line
	    # timeout) in case the select gets truly wedged.
	    #
	    vec($ri, $fileno, 1) = 1;
	    eval {
		local $SIG{ALRM} = \&unstick;
		alarm(3);
		$start = time;
		while (select($ro = $ri, undef, undef, 0)) {
		    sysread($config{collector}{$k}{_fd}, $char, 1);
		    #
		    # If we find a line terminator, trim leading and trailing
		    # whitespace, and make it available it only if there is
		    # something left (so, eliminate blank lines here)
		    #
		    if ($char eq "\r" || $char eq "\n") {
			$my_select_tmp[$fileno] =~ s/^\s+//;
			$my_select_tmp[$fileno] =~ s/\s+$//;
			next unless length($my_select_tmp[$fileno]);
			print "+"	if $opt_verbose;
			push @{$my_select_buffer[$fileno]}, $my_select_tmp[$fileno];
			$my_select_tmp[$fileno] = "";
			vec($rret, $fileno, 1) = 1;
			}
		    else {
#			print "Got '$char' from $fileno\n"	if $opt_verbose;
			$my_select_tmp[$fileno] .= $char;
			}
		    }
		continue {
		    unstick() if time - $start > 1;
		    }
		alarm(0);
		};	# end of eval with possible timeout
	    msg("err","Unstick seems to have been called while in $k") if $&;
	    print "Done\n"	if $opt_verbose;
	    }
	}
    #
    # "Return" the vector of FDs that have complete lines of data
    #
    $_[0] = $rret;			# Return by REFERENCE!
    print "Done with my_select\n"	if $opt_verbose;
    }

############################################################################
#                                Graphing                                  #
############################################################################

sub wind_direction_str {
    for (shift) {
	$_ <= 11.25  && return $lh->maketext("N");
	$_ <= 33.75  && return $lh->maketext("NNE");
	$_ <= 56.25  && return $lh->maketext("NE");
	$_ <= 78.75  && return $lh->maketext("ENE");
	$_ <= 101.25 && return $lh->maketext("E");
	$_ <= 123.75 && return $lh->maketext("ESE");
	$_ <= 146.25 && return $lh->maketext("SE");
	$_ <= 168.75 && return $lh->maketext("SSE");
	$_ <= 191.25 && return $lh->maketext("S");
	$_ <= 213.75 && return $lh->maketext("SSW");
	$_ <= 236.25 && return $lh->maketext("SW");
	$_ <= 258.75 && return $lh->maketext("WSW");
	$_ <= 281.25 && return $lh->maketext("W");
	$_ <= 303.75 && return $lh->maketext("WNW");
	$_ <= 326.25 && return $lh->maketext("NW");
	$_ <= 348.75 && return $lh->maketext("NNW");
	return $lh->maketext("N");
	}
    }

sub wind_speed_clr {
    for (shift) {
	$_ == 0  && return "#ffffff";
	$_ <= 5  && return "#e0ffe0";
	$_ <= 10 && return "#c0ffc0";
	$_ <= 15 && return "#90ff90";
	$_ <= 20 && return "#60ff60";
	$_ <= 25 && return "#60ff20";
	$_ <= 30 && return "#90c060";
	$_ <= 35 && return "#c09090";
	$_ <= 40 && return "#e09090";
	$_ <= 45 && return "#ff9090";
	$_ <= 50 && return "#ff6060";
	$_ <= 55 && return "#ff3030";
	return "#ff0000";
	}
    }

sub do_graph {
    my ($num_datapoints, $axes, $times, $data) = @_;

    my ($graph, $title, $from_str, $to_str,
	$overall_max, $overall_min, @legend, @points, %axis,
	$plot_wind_only, $plot_wind_compass, $direction_data,
	$plot_weighted_direction, $values_fmt, $y_number_format);

    #
    # Quick question - is there any data to graph?
    #
    unless ($num_datapoints) {
	#
        # The No Data banner is generated in-line.  Note that the
        # image is a fixed size, which is fine since it will be scaled 
        # by the client browser (whereas the graph size is a configurable
        # option above).
        # 
        require GD;
        my $im = new GD::Image(130,60);
        my $white = $im->colorAllocate(255,255,255);
        my $blue = $im->colorAllocate(0,0,255);
        $im->transparent($white);
        $im->string(GD::Font->Large, 0, 5, "No data exists", $blue);
        $im->string(GD::Font->Large, 0, 35, " in date range", $blue);
        print $ofd $im->png;
	return;
	}
    #
    # Change the graph axis from units to scale.
    #
    for my $a (@$axes) {
	if ($a->{_scale} =~ /^[CF]$/) {
	    $a->{_scale} = $lh->maketext("Temperature [_1]", "\N{U+00b0}$opt_temperature");
	    }
	elsif ($a->{_scale} eq "Deg") {
	    $a->{_scale} = $lh->maketext("Direction");		# Not "wind"
	    }
	else {
	    $a->{_scale} = (adjust_for_scale(0, $a->{_scale}, 0))[1]; # scale only
	    }
	}
    #
    # Set up the graph title
    #
    if ((localtime($date_from))[5] == (localtime($date_to))[5]) {
	$title = strftime $lh->maketext("%a %b %e"), localtime($date_from);
	}
    else {
	$title = strftime $lh->maketext("%a %b %e %Y"), localtime($date_from);
	}
    $title .= " - ";
    $title .= strftime $lh->maketext("%a %b %e %Y"), localtime($date_to);
    #
    # Figure out how many axis types there are - used much later
    #
    for my $a (@$axes) {
	$axis{ $a->{_scale} }++;
	}
    #
    # If we are asked to plot a Hi/Lo, suborn the minmax array for the low
    # and use the data array for the high
    #
    if ($opt_hilo) {
	my $delta = $times->[1] - $times->[0];
	my ($lead_in, $npoints, $cnt, $ti, $hi, $lo, $min, $max);
	#
	# Start by figuring how many elements are in the first day of data,
	# and by shrinking @$times
	#
	for (@$times) {
	    last if $_ % 86400 == 0;
	    $lead_in++
	    }
	$cnt = 0;
	for (@$times) {
	    if ($_ % 86400 == 0 || $cnt == $#$times) {
		push @$ti, $times->[$cnt];
		}
	    $cnt++;
	    }
	$times = $ti;
	#
	# Then for each array in @$data, collect hi/lo values for the 24-hour
	# periods (the first one will only be $lead_in long, and the last one
	# may be short, too.
	#
	for my $sensor (@$data) {
	    $hi = [];
	    $lo = [];
	    $min = INT_MAX;
	    $max = INT_MIN;
	    $cnt = 0;
	    $npoints = $lead_in;
	    for my $v (@{$sensor->{_data}}) {
		if (defined $v) {
		    $min = min($v, $min);
		    $max = max($v, $max);
		    }
		if (--$npoints == 0 || $cnt == $#{$sensor->{_data}}) {
		    push @$hi, ($max == INT_MIN) ? undef : $max;
		    push @$lo, ($min == INT_MAX) ? undef : $min;
		    $npoints = int(86400 / $delta);
		    $min = INT_MAX;
		    $max = INT_MIN;
		    }
		$cnt++;
		}
	    $sensor->{_data} = $hi;
	    $sensor->{_minmax} = $lo;
	    }
	}
    #
    # Otherwise, unless we are asked to make a messy plot, average the data
    # so that we don't try to plot more than 2x the number of points as there
    # are pixels on the graph.
    #
    elsif ($opt_nosmooth == 0) {
	my ($out, $runlen);
	if ($#$times > 2.5*$opt_width) {
	    $runlen = int($#$times / (2.5*$opt_width));
	    #
	    # Trim down the times array
	    #
	    $out = [];
	    for (my $i = 0; $i <= $#$times; ) {
		my ($sum, $dcnt);
		for (my $cnt = 0; $cnt < $runlen; $cnt++, $i++) {
		    $sum += $times->[$i];
		    $dcnt += defined($times->[$i]);
		    }
		push @{$out}, $dcnt ? $sum/$dcnt : undef;
		}
	    $times = $out;
	    #
	    # Handle the min/max/current arrays
	    #
	    for my $sensor (@$data) {
		$out = [];
		for (my $i = 0; $i <= $#{$sensor->{_minmax}}; ) {
		    my (@hold);
		    for (my $cnt = 0; $cnt < $runlen; $cnt++, $i++) {
			if (defined($sensor->{_minmax}[$i])) {
			    push @hold, $sensor->{_minmax}[$i];
			    }
			}
		    if (@hold == 0) {
			push @{$out}, undef;
			}
		    elsif (@hold == 1) {
			push @{$out}, $hold[0];
			}
		    elsif (@hold > 1) {
			if (@{$out}) {
			    pop @{$out};
			    push @{$out}, $hold[0];
			    }
			push @{$out}, $hold[1];
			}
		    }
		$sensor->{_minmax} = $out;
		}
	    #
	    # Now average the data arrays
	    #
	    for my $sensor (@$data) {
		$out = [];
		for (my $i = 0; $i <= $#{$sensor->{_data}}; ) {
		    my ($sum, $dcnt);
		    for (my $cnt = 0; $cnt < $runlen; $cnt++, $i++) {
			$sum += $sensor->{_data}[$i];
			$dcnt += defined($sensor->{_data}[$i]);
			}
		    push @{$out}, $dcnt ? $sum/$dcnt : undef;
		    }
		$sensor->{_data} = $out;
		}
	    }
	}
    #
    # Where do the ticks go?  What do they look like?  Depends on range...
    # The first element of @points are labels, everything else is data
    #
    if ($date_to - $date_from <= 28*3600) {	# 06:35
	@points = [ map { strftime "%H:%M", localtime($_) } @$times ];
	}
    elsif ($date_to - $date_from <= 7*86400) {	# Sun 6:35
	@points = [ map { strftime "%a %H:%M", localtime($_) } @$times ];
	}
    elsif ($date_to - $date_from <= 365*86400) {# Sun 8/27
	my $fmt = $lh->maketext("%a %m/%d");
	@points = [ map { strftime $fmt, localtime($_) } @$times ];
	}
    else {					# Aug 27
	my $fmt = $lh->maketext("%b %d");
	@points = [ map { strftime $fmt, localtime($_) } @$times ];
	}
    #
    # A special cases - if we are only graphing wind speed and direction,
    # and the user asks for "radar" plot the average and maximum wind speeds
    # for the period around a compass rose.  Also calculate (later) a weighted
    # average as a third plot...
    #
    if (keys %axis == 2 && $axis{$lh->maketext("Direction")} &&
	    ($axis{MPH} || $axis{KPH} || $axis{MPS} || $axis{KNOTS}) &&
	    $config{view}{$opt_view}{graphtype} eq "radar") {
	my ($max_speed, $max_cnt);
	$plot_wind_compass = 1;
	for (my $i = 0; $i <= $#$axes; $i++) {
	    if ($axes->[$i]{_scale} eq $lh->maketext("Direction")) {
		splice (@$axes, $i, 1);
		$direction_data = splice (@$data, $i, 1);
		last;
		}
	    }
	#
	# Consolidate data points around the compass rose.  We average the
	# speed and figure the maximum gust speed.
	#
	for (my $i = 0; $i <= $#{ $direction_data->{_data} }; $i++) {
	    my $nsew = wind_direction_str($direction_data->{_data}[$i]);
	    for my $sensor (@$data) {
		next unless defined $sensor->{_data}[$i];
		if ($sensor->{type} eq "speed") {
		    $sensor->{_compass}{$nsew}{sum} += $sensor->{_data}[$i];
		    $sensor->{_compass}{$nsew}{cnt}++;
		    }
		elsif ($sensor->{type} eq "gust") {
		    $sensor->{_compass}{$nsew}{sum} = $sensor->{_data}[$i]
			if $sensor->{_data}[$i] > $sensor->{_compass}{$nsew}{sum};
		    $max_speed = $sensor->{_data}[$i]
			if $sensor->{_data}[$i] > $max_speed;
		    $sensor->{_compass}{$nsew}{cnt} = 1;
		    }
		else {
		    die "I shouldn't be seeing a $sensor->{type} sensor here!";
		    }
		}
	    }
	#
	# Calculate the average/maximum speed (using the compass rose)
	# Overwrite @points, because the summary fields are only valid
	# for linear plots, not radial ones
	#
	@points = ( \@compass );
	for my $nsew (@compass) {
	    for my $sensor (@$data) {
		$sensor->{_compass}{$nsew}{avg} = 
		    $sensor->{_compass}{$nsew}{sum} == 0 ? 0 :
			int($sensor->{_compass}{$nsew}{sum} /
			    $sensor->{_compass}{$nsew}{cnt});
		push @{ $sensor->{_compass}{rose} }, $sensor->{_compass}{$nsew}{avg};
		}
	    }
	push @points, map { $_->{_compass}{rose} } @$data;
	#
	# If there is a speed sensor, we'll use that to create an extra plot
	#
	for my $sensor (@$data) {
	    if ($sensor->{type} eq "speed") {
		$plot_weighted_direction = 1;
		push @points, [];
		for my $nsew (@compass) {
		    $max_cnt = $sensor->{_compass}{$nsew}{cnt}
			if $sensor->{_compass}{$nsew}{cnt} > $max_cnt;
		    }
		for my $nsew (@compass) {
		    push @{$points[-1]},
			$sensor->{_compass}{$nsew}{cnt}/$max_cnt * $max_speed;
		    }
		}
	    }
	}
    #
    # Another special case - if we are only graphing wind speed and direction,
    # and the user requests a "natural" graph, then map the direction to a
    # set of characters in wind_direction_str, and plot those glyphs as the
    # "value" of the speed.  Which means that we have to delete the direction
    # list from the data being plotted.  The "map" statement unclutters the
    # graph by not marking every point.
    #
    elsif (keys %axis == 2 && $axis{$lh->maketext("Direction")} &&
	    ($axis{MPH} || $axis{KPH} || $axis{MPS} || $axis{KNOTS}) &&
	    $config{view}{$opt_view}{graphtype} eq "natural") {
	my ($j, $skip);
	$plot_wind_only = 1;
	for (my $i = 0; $i <= $#$axes; $i++) {
	    if ($axes->[$i]{_scale} eq $lh->maketext("Direction")) {
		splice (@$axes, $i, 1);
		$direction_data = splice (@$data, $i, 1);
		last;
		}
	    }
	$skip = int(@{ $direction_data->{_data} } / 12);
	map { undef $_ unless $j++ % $skip == 0 } @{ $direction_data->{_data} };
	push @points, map { $_->{_data} } @$data;
	}
    #
    # The actual graph points (added after the X axis values from above)
    # We plot minmax first, so that the diamonds stay solid (because GD
    # blanks parts of the diamonds otherwise)
    #
    else {
	push @points, map { $_->{_minmax} } @$data;
	push @points, map { $_->{_data} } @$data;
	}
    #
    # Calculate the top and bottom of the graph
    #
    $overall_min = INT_MAX; $overall_max = INT_MIN;
    for my $sensor (@$data) {
	if (defined($sensor->{_min}) && $sensor->{_min} < $overall_min) {
	    $overall_min = $sensor->{_min};
	    }
	if (defined($sensor->{_max}) && $sensor->{_max} > $overall_max) {
	    $overall_max = $sensor->{_max};
	    }
	}
    #
    # Round the min/max values nicely.  If no overall min or max, use 0-100
    # Rain is special - scale 0..1 if we have close values, else by 1 Inch/mm
    #
    if (keys %axis == 1 && $axis{Inch}) {
	$overall_min = 0;
	$overall_max = 0	if $overall_max == INT_MIN;
	$overall_max = int($overall_max) + 1;
	$values_fmt = "%.2f";
	}
    elsif (keys %axis == 1 && $axis{inHg}) {
	$overall_min = 30.25	if $overall_min == INT_MAX;
	$overall_max = 30.5	if $overall_max == INT_MIN;
	$overall_min = int($overall_min * 10) / 10;
	$overall_max = int($overall_max * 10 + .99) / 10;
	$values_fmt = "%.2f";
	}
    else {
	my ($nearest, $limit);
	$overall_min = 0	if $overall_min == INT_MAX;
	$overall_max = 100	if $overall_max == INT_MIN;
	if ($overall_max - $overall_min > 5000) {
	    $nearest = 10**int(log($overall_max - $overall_min) / log(10));
	    $values_fmt = "%d";
	    $y_number_format = "%.2e";
	    }
	elsif ($overall_max - $overall_min > 500) {
	    $nearest = 100;
	    $values_fmt = "%d";
	    }
	elsif ($overall_max - $overall_min > 5) {
	    $nearest = 10;
	    $values_fmt = "%.1f";
	    }
	elsif ($overall_max - $overall_min > 2) {
	    $nearest = 5;
	    $values_fmt = "%.1f";
	    }
	elsif ($overall_max - $overall_min > 1) {
	    $nearest = 2;
	    $values_fmt = "%.1f";
	    }
	else {
	    $nearest = 1;
	    $values_fmt = "%.2f";
	    }
	# Adjust limit down to the $nearest
	$limit = floor($overall_min);
	$overall_min = $limit - 
		($limit % $nearest ? $limit % $nearest
				   : 0);
	# Adjust limit up to the $nearest
	$limit = ceil($overall_max);
	$overall_max = $limit + 
		($limit % $nearest ? $nearest - ($limit % $nearest)
				   : 0);
	}
    #
    # Man is this weird (minmax, maxmin) but handy :-)
    #
    if (defined $config{view}{$opt_view}{maxmin} &&
	    $overall_min > $config{view}{$opt_view}{maxmin}) {
	$overall_min = $config{view}{$opt_view}{maxmin};
	}
    if (defined $config{view}{$opt_view}{minmax} &&
	    $overall_max < $config{view}{$opt_view}{minmax}) {
	$overall_max = $config{view}{$opt_view}{minmax};
	}
    #
    # Line types and colors (the lines, and the min/max/current markers),
    # as well as general graph data
    #
    if ($plot_wind_compass) {
	#+ Comment/Uncomment when GD::Chart::Radial is not defined inline
	get_radial_code();
	# eval qq{ use GD::Chart::Radial };
	#-
	$graph = new GD::Chart::Radial($opt_width, $opt_height);
	$graph->set( 
	    dclrs	=> [ map { $_->{graphcolor} } @$data ],
	    start_angle	=> 90,
	    style	=> "circle",
	    );
	#
	# We put this back late to get the proper label/color on the
	# direction, without having the direction data involved in any
	# of the overall_min/overall_max calculations.
	#
	push @$data, $direction_data;
	}
    elsif ($plot_wind_only) {
	require GD::Graph::mixed;
	$graph = new GD::Graph::mixed($opt_width, $opt_height);
	$graph->set_values_font(GD::Font->Small);
	$graph->set( 
	    x_label_skip=> (@{$points[0]} / $opt_width) *
				    length($points[0]->[0]) * 15,
	    dclrs	=> [ (map { $_->{graphcolor} } @$data) x 2 ],
	    types 	=> [ ("lines") x @$data ],
	    show_values => [ [ undef ], $direction_data->{_data} ],
	    line_types	=> [ map { $_->{linetype} } @$data ],
	    values_format=> \&wind_direction_str,
	    valuesclr	=> "#b00000",
	    );
	}
    elsif ($opt_hilo) {
	require GD::Graph::mixed;
	$graph = new GD::Graph::mixed($opt_width, $opt_height);
	$graph->set( 
	    x_label_skip=> (@{$points[0]} / $opt_width) *
				    length($points[0]->[0]) * 15,
	    dclrs	=> [ (map { $_->{graphcolor} } @$data) x 2 ],
	    types 	=> [ ("lines") x @$data, ("lines") x @$data ],
	    line_types	=> [ (map { $_->{linetype} } @$data) x 2 ],
	    );
	}
    else {
	require GD::Graph::mixed;
	$graph = new GD::Graph::mixed($opt_width, $opt_height);
	$graph->set( 
	    x_label_skip=> (@{$points[0]} / $opt_width) *
				    length($points[0]->[0]) * 15,
	    dclrs	=> [ (map { $_->{graphcolor} } @$data) x 2 ],
	    types 	=> [ ("points") x @$data, ("lines") x @$data ],
	    show_values => [ [ undef ], map { $_->{_minmax} } @$data ],
	    values_format=> $values_fmt,
	    line_types	=> [ map { $_->{linetype} } @$data ],
	    markers	=> [ 5 ],	# Filled diamonds for hi/lo only
	    marker_size	=> 3,
	    );
	}
    $graph->set( 
	default_type	=> "lines",
	line_type_scale	=> 4,
	labelclr	=> "black",
	y_min_value	=> $overall_min,
	y_max_value	=> $overall_max,
	y_long_ticks	=> 1,
	y_tick_number	=> (($overall_max - $overall_min) <= 20 ||
				($overall_max - $overall_min) >= 200 ?
				10 : ($overall_max - $overall_min) / 10),
	y_label		=> join(" / ", keys %axis),
	y_number_format => $y_number_format,

	title		=> ($config{location} && "$config{location} - " ) .
				$title,
	r_margin	=> 20,		# Leaves room for last values text
	skip_undef	=> 1,
	);
    $graph->set_legend(map { $_->{name} } @$data);

    print $ofd $graph->plot(\@points)->png;
    }

############################################################################
#                                Reporting                                 #
############################################################################

sub do_report {
    my ($num_datapoints, $axes, $times, $data) = @_;
    my (@points, @head, @row, $date_format, $row);

    unless ($num_datapoints) {
	print $ofd $lh->maketext("No data available in selected date range\n");
	return;
	}

    unshift @$axes, "";	# Because time is not an axis, this aligns axes & data
    if ($opt_epochtime) {
	@points = ( $times );
	}
    else {
	@points = [ map { strftime "%c", localtime($_) } @$times ];
	}
    push @points, map { $_->{_data} } @$data;
    #
    # Format specific initialization
    #
    if ($opt_format eq "excel") {
	# Nothing special
	binmode($ofd);
	$workbook = Spreadsheet::WriteExcel->new($ofd)
	    or die "Can't create spreadsheet - $!";
	$date_format = $workbook->add_format(
	    num_format => 'mmm d yyyy hh:mm:ss');
	$worksheet = $workbook->add_worksheet($opt_view);
	}
    #
    # Print the header
    #
    @head = ($lh->maketext("Time"));
    for (map { $_->{name} } @$data) {
	if ($opt_format eq "csv") {
	    s/"/""/g;		# Make titles safe for CSV...
	    }
	elsif ($opt_format eq "tsv") {
	    s/\t/ /g;		# ...or TSV
	    }
	elsif ($opt_format eq "xml" || $opt_format eq "cer") {
	    s/&/&amp;/g;	# ...or XML
	    s/</&lt;/g;
	    s/>/&gt;/g;
	    }
	elsif ($opt_format eq "excel" || $opt_format eq "timeplot") {
	    # Nothing special
	    }
	else {
	    die "Sanity check - impossible report format"
	    }
	push @head, $_;
	}
    if ($opt_format eq "csv") {
	print $ofd qq("), join ('","', @head), qq("\n);
	}
    elsif ($opt_format eq "tsv") {
	print $ofd join ("\t", @head), "\n";
	}
    elsif ($opt_format eq "excel") {
	if ($#{ $points[0] } >= 65535) {
	    my $msg = "You requested a spreadsheet with @{[ scalar @{$points[0]} ]} rows, and Excel can only handle 65535 rows.";
	    $worksheet->write_string(0, 0, $msg);
	    die "$msg\n";
	    }
	$row = 0;
	for (my $col = 0; $col < @head; $col++) {
	    $worksheet->write_string($row, $col, $head[$col]);
	    }
	$worksheet->freeze_panes(1, 0);
	$worksheet->set_column(0, 0, 18);
	$row++;
	}
    elsif ($opt_format eq "timeplot") {
	# No header for timeplot
	}
    elsif ($opt_format eq "xml") {
	# No header for XML
	}
    else {
	die "Format: $opt_format not yet implemented";
	}
    #
    # And now the data - walk the array of arrays (the first of them contains
    # the time, followed by the readings for the selected sensors), shifting
    # the first value off of each
    #
    while (@{$points[0]}) {
	@row = ();
	for my $ary (@points) {
	    push @row, shift @$ary;
	    }

	if ($opt_format eq "csv") {
	    print $ofd qq("), join ('","', @row), qq("\n);
	    }
	elsif ($opt_format eq "tsv") {
	    print $ofd join ("\t", , @row), "\n";
	    }
	elsif ($opt_format eq "timeplot") {
	    my @when = reverse( (gmtime($row[0]))[0..5]);
	    $when[0] += 1900;
	    $when[1]++;
	    $row[0] = sprintf("%4d-%02d-%02dT%02d:%02d:%02dZ", @when);
	    for (@row[1..$#row]) {
		$_ = sprintf "%.1f", $_		if defined $_;
		}
	    print $ofd join ("\t", , @row), "\n";
	    }
	elsif ($opt_format eq "excel") {
	    my @when = reverse( (localtime($row[0]))[0..5]);
	    $when[0] += 1900;
	    $when[1]++;
	    $worksheet->write_date_time($row, 0,
		sprintf("%4d-%02d-%02dT%02d:%02d:%02d", @when), $date_format);
	    for (my $col = 1; $col < @row; $col++) {
		$worksheet->write($row, $col, $row[$col]);
		}
	    $row++;
	    }
	elsif ($opt_format eq "xml") {
	    my $row = { time => $row[0] };
	    for (my $col = 1; $col < @row; $col++) {
		push @{ $row->{sensor} }, {
		    id => $axes->[$col]{_nk},
		    name => $head[$col],
		    value => $row[$col],
		    units => $axes->[$col]{_scale},
		    };
		}
	    push @{ $worksheet->{reading} }, $row;
	    }
	}
    #
    # Format specific shutdown
    #
    if ($opt_format eq "excel") {
	$workbook->close();
	}
    elsif ($opt_format eq "xml") {
	my $xs = XML::Simple->new;
	print $ofd $xs->XMLout($worksheet, RootName => "thermd");
	}
    }

sub do_annotate {
    my $scale_str;
    eval qq{ use Image::Magick 6.3.0; };
    my $image = Image::Magick->new; 
    my $view = $config{view}{$opt_view};
    my $x = $image->Read($view->{image});
    msg("err", $x) if $x;
    my ($time, @lines) = read_current_values();
    if (! defined $time) {
	die $lh->maketext("The logging daemon does not seem to be running\n");
	}
    #
    # Room for improvements:
    #  1) Should enable the user to specify date and time formats.
    #
    if ($view->{date}) {
	$image->Annotate(
	    font	=> $view->{date}{font},
	    pointsize	=> $view->{date}{fontsize},
	    x		=> $view->{date}{x},
	    y		=> $view->{date}{y},
	    text	=> strftime($lh->maketext("%a %b %e %Y"), localtime),
	    align	=> $view->{date}{textalign},
	    fill	=> $view->{date}{textcolor},
	    rotate	=> $view->{date}{textangle},
	    ); 
	}
    if ($view->{time}) {
	$image->Annotate(
	    font	=> $view->{time}{font},
	    pointsize	=> $view->{time}{fontsize},
	    x		=> $view->{time}{x},
	    y		=> $view->{time}{y},
	    text	=> strftime("%H:%M:%S %Z", localtime),
	    align 	=> $view->{time}{textalign},
	    fill	=> $view->{time}{textcolor},
	    rotate	=> $view->{time}{textangle},
	    );
	}
    for my $line (@lines) {
        my ($val, $units, $id) = split /\t/, $line, 3;
	my $_view = $config{_view}{$opt_view}{$id};
	next unless $_view;
        my $sensor = $_view->{_sensor};

	if ($sensor->{_scale} eq 'Deg') {
	    $val = $lh->maketext("[_1] wind", wind_direction_str($val));
	    }
	else {
	    $val = adjust_for_scale($val, $sensor->{_scale}, $_view->{precision});
	    }

        $image->Annotate(
	    font	=> $_view->{font},
	    pointsize	=> $_view->{fontsize},
	    x		=> $_view->{x},
	    y		=> $_view->{y},
	    text	=> $val,
	    align	=> $_view->{textalign},
	    fill	=> $_view->{textcolor},
	    rotate	=> $_view->{textangle},
	    );
        } 
    $image->Write(file => $ofd)
    }

sub determine_sensor_order {
    our @kn;
    return @kn if @kn;		# Cheap memoizing :-)
    #
    # Scan the sensors and eliminate the ones we don't want
    #
    for my $k (keys %{ $config{collector} }) {
	SENSOR: for my $n (keys %{ $config{collector}{$k}{sensor} }) {
	    my $nk = "$n\@$k";
	    #
	    # If the specified sensor is not in the currently selected view,
	    # skip it!  In fact, delete it so we don't look at it again later.
	    # Otherwise, record the axis value for later.  We need to convert
	    # all scales to different units-based strings EXCEPT direction.
	    #
	    next SENSOR unless $config{_view}{$opt_view}{$nk} ||
			       $opt_all_sensors;
	    #
	    # Put the collector/sensor k/n into this list so we can sort them
	    # later alphabetically by sensor name, and use the same sort below
	    #
	    push @kn, {
		kn     => $nk,
		k      => $k,
		n      => $n,
		sensor => $config{collector}{$k}{sensor}{$n},
		};
	    }
	ACTUATOR: for my $n (keys %{ $config{collector}{$k}{actuator} }) {
	    my $nk = "$n\@$k";
	    next ACTUATOR unless $config{_view}{$opt_view}{$nk} ||
				 $opt_all_sensors;
	    push @kn, {
		kn     => $nk,
		k      => $k,
		n      => $n,
		sensor => $config{collector}{$k}{actuator}{$n},
		};
	    }
	}
    #
    # Sort the sensors (independent of collector!)
    #
    if ($config{sensororder} eq "name") {
	@kn = sort { $a->{sensor}{name} cmp $b->{sensor}{name} } @kn;
	}
    elsif ($config{sensororder} eq "popup") {
	@kn = sort {
	    $a->{sensor}{popup} cmp $b->{sensor}{popup} ||
		$a->{sensor}{name} cmp $b->{sensor}{name}
	    } @kn;
	}
    elsif ($config{sensororder} eq "id") {
	@kn = sort {
	    ($a->{n} =~ /^\d+$/ && $b->{n} =~ /^\d+$/ ?
		$a->{n} <=> $b->{n} : $a->{n} cmp $b->{n})
		    || $a->{k} cmp $b->{k}
	    } @kn;
	}
    elsif ($config{sensororder} eq "subid") {
	@kn = sort {
	    $a->{k} cmp $b->{k} ||
		($a->{n} =~ /^\d+$/ && $b->{n} =~ /^\d+$/ ?
		    $a->{n} <=> $b->{n} : $a->{n} cmp $b->{n})
	    } @kn;
	}
    elsif ($config{sensororder} eq "subname") {
	@kn = sort { $a->{k} cmp $b->{k} ||
	    $a->{sensor}{name} cmp $b->{sensor}{name} } @kn;
	}

    return @kn;
    }

sub read_current_values {
    my (@lines, @kn, %kn, $idx, $last_time);
    #
    # Heavy-handedly force all data parsing and conversion to be international
    # Note that Thermd::I18N is still in effect
    #
    if ($config{logformat} eq "text") {
	open FD, "<", "$config{logread}/current"	or
	    msg("err", $lh->maketext("Cannot open [_1] - [_2]\n",
		"$config{logwrite}/current", $!));
	chomp(@lines = <FD>);
	close FD;
	$last_time = (split /\t/, shift @lines)[1];	# The epoch-time part
	}
    elsif ($config{logformat} eq "sql") {
	my ($log_name, $value, $units);
	$sth = $dbh->prepare(qq{
	    SELECT logtime, value, units, log_name FROM current
	    });
	$sth->execute();
	while(my $row = $sth->fetchrow_hashref()) {
	    $last_time ||= $row->{logtime};
	    push @lines, "$row->{value}\t$row->{units}\t$row->{log_name}";
	    }
	}
    else {
	die "Unknown LogFormat";
	}
    #
    # If the current values are more than 10 minutes old, we think that the
    # daemon is stalled/hung/stopped.  Report this as an undef time
    #
    if (time - $last_time > 60*10) {
	$last_time = undef;
	}
    $idx = 0;
    @kn = determine_sensor_order();
    for my $kn (@kn) {
	$kn{$kn->{kn}} = $idx++;
	}
    @lines = sort {
	my $name_a = (split /\t/, $a, 3)[2];
	my $name_b = (split /\t/, $b, 3)[2];
	$kn{$name_a} <=> $kn{$name_b}
	    } @lines;
    return ($last_time, @lines);
    }

sub collect_data {
    my ($cliplo, $cliphi) = @_;
    my ($time, $val, $prev, $last_val, $min, $max, $min_idx, $max_idx,
	$line, $sensor, $scale, @data, @time, %time, $last_tick, @axes,
	$cur_time, @lines, @values, $interval, $num_datapoints, @kn);

    #
    # Heavy-handedly force all data parsing and conversion to be English
    # Note that Thermd::I18N is still in effect
    #
    setlocale(LC_ALL, "C");
    #
    # Start by filling up a time array with the expected time values.  We use
    # a hash table because we have large indices, and may need to fill in
    # more values later in between values (we assume that LogInterval now
    # is the same as it always was, but it might have been more frequent
    # at some time in the past).  Start from the first date that is aligned
    # on LogInterval seconds, rounded down.
    #
    # Don't do this if we were called with -current (we only want one time)
    #
    unless ($opt_current) {
	$date_from = int(($date_from / $config{loginterval}) - 0.1) *
	    $config{loginterval};
	}
    for (my $t = $date_from; $t <= $date_to; $t += $config{loginterval}) {
	$time{$t}++;
	}
    $time{PROGRAM_START()}++	if $date_to == PROGRAM_START;
    $interval = $config{loginterval};
    ($cur_time, @lines) = read_current_values();
    @kn = determine_sensor_order();
    #
    # Now scan the sensors in order and collect the data we want.
    #
    SENSOR: for my $kn (@kn) {
	push @axes, ($sensor = $kn->{sensor});
	#
	# Overwrite the sensor elements with the values from the hash, if
	# they exist.
	#
	my $_view = $config{_view}{$opt_view}{"$kn->{n}\@$kn->{k}"};
	if (exists $_view->{name}) {
	    $sensor->{name} = $_view->{name};
	    }
	if (exists $_view->{linetype}) {
	    $sensor->{linetype} = $_view->{linetype};
	    }
	if (exists $_view->{graphcolor}) {
	    $sensor->{graphcolor} = $_view->{graphcolor};
	    }
	if (exists $_view->{popup}) {
	    $sensor->{popup} = $_view->{popup};
	    }
	#
	# Before we get any readings (because of the logic below), we
	# get the most recent one.  Try to append the "current" value
	# if we are reporting/graphing up to "now" (and the "now" values
	# are recent enough).
	#
	if ($date_to == PROGRAM_START && defined($cur_time)) {
	    LINE: for my $line (@lines) {
		my ($val, undef, $id) = split /\t/, $line, 3;
		next LINE unless $id eq "$kn->{n}\@$kn->{k}";
		($val, undef) = adjust_for_scale($val, $sensor->{_scale}, 3);
		$sensor->{_data}->{PROGRAM_START()} = $val;
		last LINE;
		}
	    }
	#
	# If we were called with -current, only use the current values
	#
	if ($opt_current) {
	    next SENSOR;
	    }

	if ($config{logformat} eq "text") {
	    #
	    # Dict::Search::look sets the file pointer to the first line
	    # that is greater than or equal to the value we want.  It will
	    # fail under three conditions
	    #	1) The file starts after the date we want (returns -1)
	    #	2) The file ends before the date we want (we are at eof)
	    #	3) The file is empty (it could happen! :-) (returns -1)
	    # We need to assess the success and two failure modes below
	    #
	    if (look($sensor->{_fd}, $date_from,
		    {comp => sub { $_[0] <=> $_[1] }}) == -1) {
		next SENSOR;
		}
	    chomp($line = $sensor->{_fd}->getline);
	    #
	    # If we can't read anything, then we're at the end of the file
	    # (conditions 2 or 3, above).  Skip this file - we have no time
	    # references to use.  We'll fill it in at the very end.
	    #
	    ($time, $val) = split /\t/, $line;
	    }
	elsif ($config{logformat} eq "sql") {
	    $sth = $dbh->prepare(qq{
		SELECT logtime, value FROM readings
		    WHERE log_id = $sensor->{_id}
		    AND   logtime >= ?
		    AND   logtime <= ?
		ORDER BY logtime});
	    $sth->execute($date_from, $date_to);
	    ($time, $val) = $sth->fetchrow_array();
	    }
	else {
	    die "Unknown LogFormat";
	    }
	next SENSOR unless $time;
	keys %{ $sensor->{_data} } = keys %time;	# Preallocate hash
	#
	# Now suck in as many entries as we need until $date_to or EOF
	# If file is empty or the dataset returned NULL, skip this part...
	#
	while ($time <= $date_to) {
	    ($val, undef) = adjust_for_scale($val, $sensor->{_scale}, 5);
	    $sensor->{_data}->{$time} = $val;
	    $prev = $time;
	    if ($config{logformat} eq "text") {
		chomp($line = $sensor->{_fd}->getline);
		($time, $val) = split /\t/, $line;
		}
	    elsif ($config{logformat} eq "sql") {
		($time, $val) = $sth->fetchrow_array();
		}
	    else {
		die "Unknown LogFormat";
		}
	    last unless $time;
	    next unless $val;
	    $interval = min($time - $prev, $interval)	if $time - $prev > 0;
	    }
	}
    #
    # If back in the measured time we had a different value for LogInterval,
    # we have to adjust the values in %time to compensate;
    #
    if (!$opt_current && $interval < $config{loginterval}) {
	for (my $t = $date_from; $t <= $date_to; $t += $interval) {
	    $time{$t}++;
	    }
	}
    #
    # Okay, we really want a collection of arrays, not hashes, so convert
    # the hashes to arrays.  Use a time array, too.  While we copy from hash
    # to array, calculate the min/max values and the indices of them in the
    # arrays.  Same for the last value measured (which may not be the last
    # value in the array, since we intentionally copy undefs!)
    #
    @time = sort keys %time;
    for my $kn (@kn) {
	my @values;		# Reallocated each time through the loop
	($max_idx, $min_idx, $last_tick, @values) = (undef);
	$min = INT_MAX;
	$max = INT_MIN;
	$sensor = $kn->{sensor};
	for my $t (@time) {
	    $val = $sensor->{_data}->{$t};
	    $val = $cliplo	if defined($cliplo) && $val < $cliplo;
	    $val = $cliphi	if defined($cliphi) && $val > $cliphi;
	    push @values, $val;
	    next unless defined $val;
	    $num_datapoints++;
	    if ($val < $min) {
		$min = $val;
		$min_idx = $#values;
		}
	    if ($val > $max) {
		$max = $val;
		$max_idx = $#values;
		}
	    $last_val = $val;
	    $last_tick = $#values;
	    }
	$sensor->{_data} = \@values;
	#
	# Now find the min and max values for the markers, and mark them
	# If the min_idx or max_idx are undefined, then we found no data
	# at all for that sensor in that time range.
	#
	if (defined($min_idx) && defined($max_idx)) {
	    $sensor->{_min} = $min;
	    $sensor->{_minmax}[$min_idx] = $min;
	    $sensor->{_max} = $max;
	    $sensor->{_minmax}[$max_idx] = $max;
	    $sensor->{_minmax}[$last_tick] = $last_val;
	    push @data, $sensor;
	    }
	#
	# No min/max, so kludge in empty arrays
	#
	else {
	    push @data, {_minmax => [], _data => []};
	    }
	}
    setlocale(LC_ALL, $lh->locale());	# Set the locale back
    return $num_datapoints, \@axes, \@time, \@data;
    }

############################################################################
#                               CGI Script                                 #
############################################################################

sub do_cgi {
    my $is_timeplot = (param('type') eq "timeplot" &&
	$config{view}{$opt_view}{graphtype} ne "radar" &&
	$config{view}{$opt_view}{type} ne "image");
    #
    # We need to do this first: if we are asked to show a view of type "radar"
    # then set the width of the graph equal to the height so we get a circular
    # (and not an elliptical) graph
    #
    if ($config{view}{$opt_view}{graphtype} eq "radar") {
	$opt_width = $opt_height;
	}
    if (param('graph')) {
	#
	# If we are called with param('graph'), we are creating a plot/picture
	# of the data.  The catch is, if we use timeplot, the plot is just
	# textual data that is picked up by the javascript.  So, do the right
	# thing for the type requested
	#
	if ($is_timeplot) {
	    print header(-content_type => "text/plain", -expires => "-10d");
	    $opt_epochtime = 1;
	    $opt_format = "timeplot";
	    do_report(collect_data($config{view}{$opt_view}{cliplo},
		$config{view}{$opt_view}{cliphi}));
	    }
	elsif ($config{view}{$opt_view}{type} eq "graph") {
	    print header(-content_type => "image/png", -expires => "-10d");
	    do_graph(collect_data($config{view}{$opt_view}{cliplo},
		$config{view}{$opt_view}{cliphi}));
	    }
	elsif ($config{view}{$opt_view}{type} eq "image") {
	    print header(-content_type => "image/png", -expires => "-10d");
	    do_annotate();
	    }
	}
    elsif (param('type') =~ /[ct]sv[ru]|excel|xml/) {
	if (param('type') =~ /([ct]sv)([ru])/) {
	    $opt_format = $1;
	    if ($opt_format eq "csv") {
		print header(-content_type => "text/csv; charset=@{[$lh->charset]}",
			    -expires => "-10d",
			    -content_disposition => "attachment;filename=thermd.csv");
		}
	    elsif ($opt_format eq "tsv") {
		print header(-content_type => "text/plain; charset=@{[$lh->charset]}", -expires => "-10d");
		}
	    $opt_epochtime = $2 eq 'u';
	    do_report(collect_data());
	    }
	elsif (param('type') eq "excel") {
	    eval qq{ use Spreadsheet::WriteExcel; };
	    die $@ if $@;

	    $opt_format = "excel";
	    print header(-content_type => "application/vnd.ms-excel; charset=@{[$lh->charset]}",
			-expires => "-10d",
			-content_disposition => "attachment;filename=thermd.xls");
	    $opt_epochtime = 1;
	    do_report(collect_data());
	    }
	elsif (param('type') eq "xml") {
	    eval qq{ use XML::Simple; };
	    die $@ if $@;

	    $opt_format = "xml";
	    print header(-content_type => "text/xml; charset=@{[$lh->charset]}",
			-expires => "-10d");
	    $opt_epochtime = 1;
	    $opt_current = 1;
	    $opt_all_sensors = 1;
	    do_report(collect_data());
	    }
	}
    elsif (param('pleasewait')) {
	#
	# The Please Wait banner is generated in-line.  Note that the
	# image is a fixed size, which is fine since it will be scaled
	# by the client browser (whereas the graph size is a configurable
	# option above).
	#
	print header(-content_type => "image/png", -expires => "now");
	require GD;
	my $im = new GD::Image(130,60);
	my $white = $im->colorAllocate(255,255,255);
	my $blue = $im->colorAllocate(0,0,255);
	$im->transparent($white);
	$im->string(GD::Font->Large,0,5,"Generating Graph",$blue);
	$im->string(GD::Font->Large,0,35,"Please Wait...",$blue);
	print $im->png;
	}
    elsif (param('docs')) {
	print header(), `pod2html $ENV{SCRIPT_FILENAME}`;
	}
    else {	# The HTML text wrapper around everything else
	local $, = " ";
	my (@current, @current_rows, $time, @lines);
	my $me = self_url();
	my %type_labels = (		# a hash for radio_group()
		    graph    => $lh->maketext("Graphical"),
		    timeplot => $lh->maketext("TimePlot"),
		    csvr     => $lh->maketext("CSV Human Time"),
		    csvu     => $lh->maketext("CSV UNIX Epoch Time"),
		    tsvr     => $lh->maketext("TSV Human"),
		    tsvu     => $lh->maketext("TSV Epoch"),
		    excel    => $lh->maketext("Excel"),
		    xml      => $lh->maketext("XML"),
		    );

	$me .= '?'	unless $me =~ /\?/;
	#
	# Create a list of views, make sure "all" (or the DefaultView) is first
	# Don't show the wunderground views - they are "push" only
	#
	my %view_labels = %{ $config{_view} };
	delete $view_labels{all};
	delete $view_labels{ $config{defaultview} };
	my @view_labels = ($config{defaultview}, sort {
	    $config{view}{$a}{buttonorder} <=> $config{view}{$b}{buttonorder} ||
		$a cmp $b
		}
	    grep { $config{view}{$_}{type} ne "wunderground"}
		keys %view_labels);
	#
	# Build the current data view (with colors)
	#
	($time, @lines) = read_current_values();
	if (defined $time) {
	    @current = ( $lh->maketext("Latest Readings: [_1]",
		decode_utf8(strftime("%c", localtime($time)))) );
	    LINE: for my $line (@lines) {
		chomp $line;
		my ($v, $sensor_units, $id) = split /\t/, $line, 3;
		my ($color, $popup, $adj, $old);
		my $_view = $config{_view}{$opt_view}{$id};
		#
		# If the specified sensor is not in the currently selected
		# view, skip it!  Otherwise, get its color by name match.
		# The element in the _view hash will be a pointer to a hash.
		# Overwrite the sensor GraphColor with the value from the
		# hash (either TextColor or GraphColor), if it exists.
		#
		next LINE unless $_view;
		my $sensor = $_view->{_sensor};
		if (exists $_view->{name}) {
		    $sensor->{name} = $_view->{name};
		    }
		$color = $_view->{textcolor} || $_view->{graphcolor} ||
		    $sensor->{graphcolor};
		$color = "black" if lc($color) eq "white";
		if ($popup = $_view->{popup} || $sensor->{popup}) {
		    $popup = escapeHTML($popup);
		    $popup =~ s#\n#<br />#g;
		    }
		if ($sensor_units eq 'Deg') {
		    $line = $lh->maketext("[_1] wind", wind_direction_str($v));
		    }
		elsif ($sensor_units eq 'Count') {
			$line = sprintf "%s %s", ztrim($v), $sensor->{name};
		    }
		else {
		    ($adj, undef) = adjust_for_scale($v, $sensor_units, 2);
		    $line = sprintf "%s %s", $adj, $sensor->{name};
		    }
		$old = autoEscape(0);
		push @current, font({color => $color,
				     $popup ? (onmouseover => "return escape('$popup')") : (),
				    }, "&nbsp;$line&nbsp;");
		autoEscape($old);
		}
	    }
	else {
	    @current = ($lh->maketext("The logging daemon does not seem to be running\n"));
	    }
	while (my @c = splice @current, 1, 5) {
	    push @current_rows, td([ @c ]);
	    }
	#
	# Here's an annoying one - from and to can be relative values, and
	# we allow things like "+1d".  But in HTTP requests, '+' is the same
	# as ' ', so we need to turn the '+' in to '%2b' in dates.
	#
	$opt_from =~ s/\+/%2b/g;
	$opt_to =~ s/\+/%2b/g;
	$, = "\n";
	print header(-content_type => "text/html; charset=@{[$lh->charset]}"),
	    start_html(-bgcolor => "white",
		-title => $config{location} ?
		    $lh->maketext("Temperature and Environment in [_1]",
			$config{location}) :
		    $lh->maketext("Temperature and Environment"),
		-head => meta({
		    -http_equiv => 'Refresh',
		    -content => "$config{refreshrate}; url=$me"}),
		-script => [
		    {
		    -language => "javascript",
		    -code => javascript_timer(),
		    },
		    #
		    # If we're asking for a timeplot, we need to call upon the
		    # main body of MIT code, as well as the second call to
		    # thermd which creates the data to be plotted
		    #
		    $is_timeplot ? (
			{
			-type => "text/javascript",
			-src => "http://static.simile.mit.edu/timeplot/api/1.0/timeplot-api.js",
			},
			{
			-language => "javascript",
			-code => javascript_timeplot(),
			},
			) : (),
		    ],
		#
		# Also, the timeplot code requires attributes in the <body> tag
		#
		$is_timeplot ? (
		    -onLoad => "onLoad();",
		    -onResize => "onResize();",
		    ) : (),
		),
	    table({width => "85%"}, TR(
		td({valign=>"top"},
		    h1($config{location} ?
			$lh->maketext("The environment in and around [_1]",
			    $config{location}) : ""),
		    ($config{gpscoordinates} ?
			h2(a({href => $config{mapurl}},
			    $config{gpscoordinates})) : "")),
		td({valign=>"top", align=>"right", nowrap=>1},
		    small(b(decode_utf8(strftime("%c", localtime)))),
		    br(),
		    small({-id=>'refresh_counter'}, "")))),
	    ($config{blurb} ? p($config{blurb}) : ""),
	    table({width => "85%"},
		# Sigh - If the charset is UTF-8, then $current[0] may have
		# UTF-8 in it, and @current_rows may have Unicode (which will
		# give garbage when concatenated).  Decode one of them if
		# necessary, so we can print them both in one string.
		# Honestly, the code makes no sense - make sure that DA, RU,
		# and EN work, and that'll do her.
		TR(th({colspan=>3},
		    ($lh->charset eq "UTF-8" && ! utf8::is_utf8($current[0]))
			? decode_utf8($current[0])
			: $current[0])),
		TR([@current_rows])),
	    '<script type="text/javascript">',
	    'ViewerStart = new Date(); ViewerRefresh = '. $config{refreshrate} .';',
	    'setTimeout("ViewerCountdown(\'refresh_counter\')", 1000);',
	    '</script>',
	    p(),
	    start_form({-method => "GET"});
	#
	# The timeplot code is simply a div that the javascript targets
	# while the self-generated images are an image that results from
	# another invocation of thermd
	#
	if ($is_timeplot) {
	    my @hdr;
	    for my $kn (determine_sensor_order()) {
		push @hdr, span(
		    {-style => "color: $kn->{sensor}{graphcolor};"},
		    $kn->{sensor}{name});
		}
	    print div({-style => "font-size: 10px; font-family: 'verdana', 'helvetica', sans serif;"},
		    join(' | ', @hdr)),
		div({-id => "thermd-tp", -style => "height: ${opt_height}px;"},
		    "&nbsp;");
	    }
	else {
	    print img({
		$config{view}{$opt_view}{type} eq "image"
		    ? ()
		    : (-height => $opt_height, -width => $opt_width),
		-lowsrc => "$me;pleasewait=1", -src => "$me;graph=1"});
	    }
	print hidden("config"),
	    br(),
	    table($opt_width == $opt_height ? {} : {width => $opt_width},
	      TR(
		td({align => "right"}, b($lh->maketext("Scale:"))),
		td(radio_group(-name => "units",
		    -onClick => "submit()",
		    -values => [qw(Metric English)],
		    -labels => {
				Metric => $lh->maketext("Metric"),
				English => $lh->maketext("English"),
				},
		    -default => $opt_units)),
		td({align => "middle"},
		    b($lh->maketext("Hi/Lo graph:")),
		    checkbox(-name => "hilo",
			-onClick => "submit()",
			-label => "")),
		td({align => "middle"}, 
		    b($lh->maketext("From:")),
		    textfield(-name => "from", -default => "-1d", -size => 6),
		    b($lh->maketext("To:")),
		    textfield(-name => "to", -default => "now", -size => 6),
		    submit(-label => $lh->maketext("Redraw")))),
	    @view_labels > 1 ?
		TR(td({align => "right"}, b($lh->maketext("View:"))),
		 td({colspan => @view_labels > 7 ? 4 : 3, align => "middle"},
		    radio_group(-name => "view",
			-onClick => "submit()",
			-values => [ @view_labels ],
			@view_labels > 7 ? (-columns => 7) : (),
			-default => $config{defaultview} || "all"))) : "",
	      TR(
		td({align => "right"}, b($lh->maketext("Type:"))),
		td({colspan => 3, align => "middle"}, table(
		    TR(td([
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(graph) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(csvr) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(tsvr) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(excel) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			])),
		    TR(td([
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(timeplot) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(csvu) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(tsvu) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			radio_group(-name => "type",
			    -onClick => "submit()",
			    -values => [ qw(xml) ],
			    -labels => \%type_labels,
			    -default => "graph"),
			]))
		    ))
		)),
	    hidden("endtime"),
	    end_form(),
	    javascript_popup(),
	    ($config{blurb2} ? p($config{blurb2}) : "");
	#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	# NOTE: If you delete these next few lines, you will be in violation
	# of my copyright, and that will make me mad (and litigious)...
	#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	print font({color => "black", size => 2}, address("Thermometer Daemon",
	    a({href => 'http://www.klein.com/thermd',
		target => "_blank"}, "source code"),
	    "and",
	    a({href => "$me;docs=1", target => "_blank"}, "documentation."),
	    "(V@{[(split(/\s+/, VERSION))[2,3]]}) Copyright &copy; 2001-2009",
	    a({href => 'mailto:dan@klein.com'}, "Daniel V. Klein")));
	#--------------------------------------------------------------------
	# End of serious copyright law stuff
	#--------------------------------------------------------------------
	if ($config{rss}) {
	    print address(
		a({href => "$config{rss}{url}/index.xml"}, "RSS-2.0 Feed"));
	    }
	print end_html();
	}
    }

sub javascript_timeplot {
    my $idx = 1;
    my $me = url(-absolute => 1, -query => 1);	# Without http://ip.com:port
    my $js = <<"==FIRST-PLOT-JS==";
var timeplot;
var eventSource = new Timeplot.DefaultEventSource();

var timeGeometry = new Timeplot.DefaultTimeGeometry({
    gridColor: new Timeplot.Color("#000000"),
    axisLabelsPlacement: "top",
    });

var valueGeometry = new Timeplot.DefaultValueGeometry({
    gridColor: "#000000",
    // min: 0,
    // max: 100,
    axisLabelsPlacement: "left",
    });

function onLoad() {
    var plotInfo = [
==FIRST-PLOT-JS==

    for my $kn (determine_sensor_order()) {
	$js .= <<"==MIDDLE-PLOT-JS==";
	Timeplot.createPlotInfo({
	    id: "plot$idx",
	    dataSource: new Timeplot.ColumnSource(eventSource, $idx),
	    timeGeometry: timeGeometry,
	    valueGeometry: valueGeometry,
	    lineColor: "$kn->{sensor}{graphcolor}",
	    showValues: true,
	    roundValues: false,
	    }),
==MIDDLE-PLOT-JS==
	$idx++;
	}

    $js .= <<"==LAST-PLOT-JS==";
	];
            
    timeplot = Timeplot.create(document.getElementById("thermd-tp"), plotInfo);
    timeplot.loadText("$me;graph=1", "\\t", eventSource);
    }

var resizeTimerID = null;
function onResize() {
    if (resizeTimerID == null) {
        resizeTimerID = window.setTimeout(function() {
            resizeTimerID = null;
            timeplot.repaint();
	    }, 100);
	}
    }
==LAST-PLOT-JS==

    return $js;
    }

sub javascript_timer {
    my $refreshing = $lh->maketext("Refreshing...");
    my $refreshing_in = $lh->maketext("Refreshing in");
    my $refresh_failed = $lh->maketext("Refresh failed, hit Reload");

    return <<"==TIMER-JS==";
/*
 * This timer script was adapted from a script in the source code to drraw,
 * http://web.taranis.org/drraw, which is released under a BSD-style licence
 */
var ViewerStart;
var ViewerRefresh;
function ViewerCountdown(target) {
  if (!ViewerStart || !ViewerRefresh)
      return;
  var targetElement;
  targetElement = document.getElementById(target);
  if (!targetElement)
      return;
  now = new Date();
  elapsed = ( now - ViewerStart ) / 1000;
  if (elapsed > ViewerRefresh + 30) {
      if ((elapsed - ViewerRefresh) % 2 < 1) {
          targetElement.innerHTML = "<b class=simplyred>$refresh_failed</b>";
      } else {
          targetElement.innerHTML = "<b>$refresh_failed</b>";
      }
      setTimeout("ViewerCountdown(thetarget)", 1000);
  } else if (elapsed > ViewerRefresh-1) {
      targetElement.innerHTML = "<b>$refreshing</b>";
      setTimeout("ViewerCountdown(thetarget)", 10000);
  } else {
      with (Math) {
          min = floor((ViewerRefresh - elapsed) / 60);
          sec = floor((ViewerRefresh - elapsed) % 60);
          if (min < 1) {
              if (sec < 30 && sec % 2 == 0)
                  targetElement.innerHTML = "<b><font color='green'>$refreshing_in "+ sec +"s</font></b>";
              else
                  targetElement.innerHTML = "<font color='blue'>$refreshing_in "+ sec +"s</font>";
          } else
              targetElement.innerHTML = "<font color='blue'>$refreshing_in "+ min +"m "+ sec +"s</font>";
      }
      thetarget=target;
      setTimeout("ViewerCountdown(thetarget)", 1000);
  }
}
==TIMER-JS==
    }

sub javascript_popup {
    return $config{_has_popups} ? <<'==POPUP-JS==' : "";
<script language="JavaScript">
/* This notice must be untouched at all times.

wz_tooltip.js    v. 3.45

The latest version is available at
http://www.walterzorn.com
or http://www.devira.com
or http://www.walterzorn.de

Copyright (c) 2002-2005 Walter Zorn. All rights reserved.
Created 1. 12. 2002 by Walter Zorn (Web: http://www.walterzorn.com )
Last modified: 17. 2. 2007

Cross-browser tooltips working even in Opera 5 and 6,
as well as in NN 4, Gecko-Browsers, IE4+, Opera 7+ and Konqueror.
No onmouseouts required.
Appearance of tooltips can be individually configured
via commands within the onmouseovers.

LICENSE: LGPL

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License (LGPL) as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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.

For more details on the GNU Lesser General Public License,
see http://www.gnu.org/copyleft/lesser.html
*/



////////////////  GLOBAL TOOPTIP CONFIGURATION  /////////////////////
var ttAbove       = false;        // tooltip above mousepointer? Alternative: true
var ttBgColor     = "#e6ecff";
var ttBgImg       = "";           // path to background image;
var ttBorderColor = "#003399";
var ttBorderWidth = 1;
var ttClickClose  = false;
var ttDelay       = 50;           // time span until tooltip shows up [milliseconds]
var ttFontColor   = "#000066";
var ttFontFace    = "arial,helvetica,sans-serif";
var ttFontSize    = "11px";
var ttFontWeight  = "normal";     // alternative: "bold";
var ttLeft        = false;        // tooltip on the left of the mouse? Alternative: true
var ttOffsetX     = 12;           // horizontal offset of left-top corner from mousepointer
var ttOffsetY     = 15;           // vertical offset                   "
var ttOpacity     = 90;          // opacity of tooltip in percent (must be integer between 0 and 100)
var ttPadding     = 3;            // spacing between border and content
var ttShadowColor = "";
var ttShadowWidth = 0;
var ttStatic      = false;        // tooltip NOT move with the mouse? Alternative: true
var ttSticky      = false;        // do NOT hide tooltip on mouseout? Alternative: true
var ttTemp        = 0;            // time span after which the tooltip disappears; 0 (zero) means "infinite timespan"
var ttTextAlign   = "left";
var ttTitleColor  = "#ffffff";    // color of caption text
var ttWidth       = 300;
////////////////////  END OF TOOLTIP CONFIG  ////////////////////////



//////////////  TAGS WITH TOOLTIP FUNCTIONALITY  ////////////////////
// List may be extended or shortened:
var tt_tags = new Array("a","area","b","big","caption","center","code","dd","div","dl","dt","em","font","h1","h2","h3","h4","h5","h6","i","img","input","li","map","ol","p","pre","s", "select", "small","span","strike","strong","sub","sup","table","td","textarea","th","tr","tt","u","var","ul","layer");
/////////////////////////////////////////////////////////////////////



///////// DON'T CHANGE ANYTHING BELOW THIS LINE /////////////////////
var tt_obj = null,         // current tooltip
tt_ifrm = null,            // iframe to cover windowed controls in IE
tt_objW = 0, tt_objH = 0,  // width and height of tt_obj
tt_objX = 0, tt_objY = 0,
tt_offX = 0, tt_offY = 0,
xlim = 0, ylim = 0,        // right and bottom borders of visible client area
tt_sup = false,            // true if T_ABOVE cmd
tt_sticky = false,         // tt_obj sticky?
tt_wait = false,
tt_act = false,            // tooltip visibility flag
tt_sub = false,            // true while tooltip below mousepointer
tt_u = "undefined",
tt_mf = null,              // stores previous mousemove evthandler
// Opera: disable href when hovering <a>
tt_tag = null;             // stores hovered dom node, href and previous statusbar txt


var tt_db = (document.compatMode && document.compatMode != "BackCompat")? document.documentElement : document.body? document.body : null,
tt_n = navigator.userAgent.toLowerCase(),
tt_nv = navigator.appVersion;
// Browser flags
var tt_op = !!(window.opera && document.getElementById),
tt_op6 = tt_op && !document.defaultView,
tt_op7 = tt_op && !tt_op6,
tt_ie = tt_n.indexOf("msie") != -1 && document.all && tt_db && !tt_op,
tt_ie7 = tt_ie && typeof document.body.style.maxHeight != tt_u,
tt_ie6 = tt_ie && !tt_ie7 && parseFloat(tt_nv.substring(tt_nv.indexOf("MSIE")+5)) >= 5.5,
tt_n4 = (document.layers && typeof document.classes != tt_u),
tt_n6 = (!tt_op && document.defaultView && typeof document.defaultView.getComputedStyle != tt_u),
tt_w3c = !tt_ie && !tt_n6 && !tt_op && document.getElementById,
tt_ce = document.captureEvents && !tt_n6;

function tt_Int(t_x)
{
    var t_y;
    return isNaN(t_y = parseInt(t_x))? 0 : t_y;
}
function wzReplace(t_x, t_y)
{
    var t_ret = "",
    t_str = this,
    t_xI;
    while((t_xI = t_str.indexOf(t_x)) != -1)
    {
	t_ret += t_str.substring(0, t_xI) + t_y;
	t_str = t_str.substring(t_xI + t_x.length);
    }
    return t_ret+t_str;
}
String.prototype.wzReplace = wzReplace;
function tt_N4Tags(tagtyp, t_d, t_y)
{
    t_d = t_d || document;
    t_y = t_y || new Array();
    var t_x = (tagtyp=="a")? t_d.links : t_d.layers;
    for(var z = t_x.length; z--;) t_y[t_y.length] = t_x[z];
    for(z = t_d.layers.length; z--;) t_y = tt_N4Tags(tagtyp, t_d.layers[z].document, t_y);
    return t_y;
}
function tt_Htm(tt, t_id, txt)
{
    var t_bgc = (typeof tt.T_BGCOLOR != tt_u)? tt.T_BGCOLOR : ttBgColor,
    t_bgimg   = (typeof tt.T_BGIMG != tt_u)? tt.T_BGIMG : ttBgImg,
    t_bc      = (typeof tt.T_BORDERCOLOR != tt_u)? tt.T_BORDERCOLOR : ttBorderColor,
    t_bw      = (typeof tt.T_BORDERWIDTH != tt_u)? tt.T_BORDERWIDTH : ttBorderWidth,
    t_ff      = (typeof tt.T_FONTFACE != tt_u)? tt.T_FONTFACE : ttFontFace,
    t_fc      = (typeof tt.T_FONTCOLOR != tt_u)? tt.T_FONTCOLOR : ttFontColor,
    t_fsz     = (typeof tt.T_FONTSIZE != tt_u)? tt.T_FONTSIZE : ttFontSize,
    t_fwght   = (typeof tt.T_FONTWEIGHT != tt_u)? tt.T_FONTWEIGHT : ttFontWeight,
    t_opa     = (typeof tt.T_OPACITY != tt_u)? tt.T_OPACITY : ttOpacity,
    t_padd    = (typeof tt.T_PADDING != tt_u)? tt.T_PADDING : ttPadding,
    t_shc     = (typeof tt.T_SHADOWCOLOR != tt_u)? tt.T_SHADOWCOLOR : (ttShadowColor || 0),
    t_shw     = (typeof tt.T_SHADOWWIDTH != tt_u)? tt.T_SHADOWWIDTH : (ttShadowWidth || 0),
    t_algn    = (typeof tt.T_TEXTALIGN != tt_u)? tt.T_TEXTALIGN : ttTextAlign,
    t_tit     = (typeof tt.T_TITLE != tt_u)? tt.T_TITLE : "",
    t_titc    = (typeof tt.T_TITLECOLOR != tt_u)? tt.T_TITLECOLOR : ttTitleColor,
    t_w       = (typeof tt.T_WIDTH != tt_u)? tt.T_WIDTH  : ttWidth;
    if(t_shc || t_shw)
    {
	t_shc = t_shc || "#c0c0c0";
	t_shw = t_shw || 5;
    }
    if(tt_n4 && (t_fsz == "10px" || t_fsz == "11px")) t_fsz = "12px";

    var t_optx = (tt_n4? '' : tt_n6? ('-moz-opacity:'+(t_opa/100.0)) : tt_ie? ('filter:Alpha(opacity='+t_opa+')') : ('opacity:'+(t_opa/100.0))) + ';';
    var t_y = '<div id="'+t_id+'" style="position:absolute;z-index:1010;';
    t_y += 'left:0px;top:0px;width:'+(t_w+t_shw)+'px;visibility:'+(tt_n4? 'hide' : 'hidden')+';'+t_optx+'">' +
	'<table border="0" cellpadding="0" cellspacing="0"'+(t_bc? (' bgcolor="'+t_bc+'" style="background:'+t_bc+';"') : '')+' width="'+t_w+'">';
    if(t_tit)
    {
	t_y += '<tr><td style="padding-left:3px;padding-right:3px;" align="'+t_algn+'"><font color="'+t_titc+'" face="'+t_ff+'" ' +
	    'style="color:'+t_titc+';font-family:'+t_ff+';font-size:'+t_fsz+';"><b>' +
	    (tt_n4? '&nbsp;' : '')+t_tit+'</b></font></td></tr>';
    }
    t_y += '<tr><td><table border="0" cellpadding="'+t_padd+'" cellspacing="'+t_bw+'" width="100%">' +
	'<tr><td'+(t_bgc? (' bgcolor="'+t_bgc+'"') : '')+(t_bgimg? ' background="'+t_bgimg+'"' : '')+' style="text-align:'+t_algn+';';
    if(tt_n6) t_y += 'padding:'+t_padd+'px;';
    t_y += '" align="'+t_algn+'"><font color="'+t_fc+'" face="'+t_ff+'"' +
	' style="color:'+t_fc+';font-family:'+t_ff+';font-size:'+t_fsz+';font-weight:'+t_fwght+';">';
    if(t_fwght == 'bold') t_y += '<b>';
    t_y += txt;
    if(t_fwght == 'bold') t_y += '</b>';
    t_y += '</font></td></tr></table></td></tr></table>';
    if(t_shw)
    {
	var t_spct = Math.round(t_shw*1.3);
	if(tt_n4)
	{
	    t_y += '<layer bgcolor="'+t_shc+'" left="'+t_w+'" top="'+t_spct+'" width="'+t_shw+'" height="0"></layer>' +
		'<layer bgcolor="'+t_shc+'" left="'+t_spct+'" align="bottom" width="'+(t_w-t_spct)+'" height="'+t_shw+'"></layer>';
	}
	else
	{
	    t_optx = tt_n6? '-moz-opacity:0.85;' : tt_ie? 'filter:Alpha(opacity=85);' : 'opacity:0.85;';
	    t_y += '<div id="'+t_id+'R" style="position:absolute;background:'+t_shc+';left:'+t_w+'px;top:'+t_spct+'px;width:'+t_shw+'px;height:1px;overflow:hidden;'+t_optx+'"></div>' +
		'<div style="position:relative;background:'+t_shc+';left:'+t_spct+'px;top:0px;width:'+(t_w-t_spct)+'px;height:'+t_shw+'px;overflow:hidden;'+t_optx+'"></div>';
	}
    }
    return(t_y+'</div>');
}
function tt_EvX(t_e)
{
    var t_y = tt_Int(t_e.pageX || t_e.clientX || 0) +
	tt_Int(tt_ie? tt_db.scrollLeft : 0) +
	tt_offX;
    if(t_y > xlim) t_y = xlim;
    var t_scr = tt_Int(window.pageXOffset || (tt_db? tt_db.scrollLeft : 0) || 0);
    if(t_y < t_scr) t_y = t_scr;
    return t_y;
}
function tt_EvY(t_e)
{
    var t_y2;

    var t_y = tt_Int(t_e.pageY || t_e.clientY || 0) +
	tt_Int(tt_ie? tt_db.scrollTop : 0);
    if(tt_sup && (t_y2 = t_y - (tt_objH + tt_offY - 15)) >= tt_Int(window.pageYOffset || (tt_db? tt_db.scrollTop : 0) || 0))
	t_y -= (tt_objH + tt_offY - 15);
    else if(t_y > ylim || !tt_sub && t_y > ylim-24)
    {
	t_y -= (tt_objH + 5);
	tt_sub = false;
    }
    else
    {
	t_y += tt_offY;
	tt_sub = true;
    }
    return t_y;
}
function tt_ReleasMov()
{
    if(document.onmousemove == tt_Move)
    {
	if(!tt_mf && tt_ce) document.releaseEvents(Event.MOUSEMOVE);
	document.onmousemove = tt_mf;
    }
}
function tt_ShowIfrm(t_x)
{
    if(!tt_obj || !tt_ifrm) return;
    if(t_x)
    {
	tt_ifrm.style.width = tt_objW+'px';
	tt_ifrm.style.height = tt_objH+'px';
	tt_ifrm.style.display = "block";
    }
    else tt_ifrm.style.display = "none";
}
function tt_GetDiv(t_id)
{
    return(
	tt_n4? (document.layers[t_id] || null)
	: tt_ie? (document.all[t_id] || null)
	: (document.getElementById(t_id) || null)
    );
}
function tt_GetDivW()
{
    return tt_Int(
	tt_n4? tt_obj.clip.width
	: (tt_obj.offsetWidth || tt_obj.style.pixelWidth)
    );
}
function tt_GetDivH()
{
    return tt_Int(
	tt_n4? tt_obj.clip.height
	: (tt_obj.offsetHeight || tt_obj.style.pixelHeight)
    );
}

// Compat with DragDrop Lib: Ensure that z-index of tooltip is lifted beyond toplevel dragdrop element
function tt_SetDivZ()
{
    var t_i = tt_obj.style || tt_obj;
    if(t_i)
    {
	if(window.dd && dd.z)
	    t_i.zIndex = Math.max(dd.z+1, t_i.zIndex);
	if(tt_ifrm) tt_ifrm.style.zIndex = t_i.zIndex-1;
    }
}
function tt_SetDivPos(t_x, t_y)
{
    var t_i = tt_obj.style || tt_obj;
    var t_px = (tt_op6 || tt_n4)? '' : 'px';
    t_i.left = (tt_objX = t_x) + t_px;
    t_i.top = (tt_objY = t_y) + t_px;
    //  window... to circumvent the FireFox Alzheimer Bug
    if(window.tt_ifrm)
    {
	tt_ifrm.style.left = t_i.left;
	tt_ifrm.style.top = t_i.top;
    }
}
function tt_ShowDiv(t_x)
{
    tt_ShowIfrm(t_x);
    if(tt_n4) tt_obj.visibility = t_x? 'show' : 'hide';
    else tt_obj.style.visibility = t_x? 'visible' : 'hidden';
    tt_act = t_x;
}
function tt_DeAlt(t_tag)
{
    if(t_tag)
    {
	if(t_tag.alt) t_tag.alt = "";
	if(t_tag.title) t_tag.title = "";
	var t_c = t_tag.children || t_tag.childNodes || null;
	if(t_c)
	{
	    for(var t_i = t_c.length; t_i; )
		tt_DeAlt(t_c[--t_i]);
	}
    }
}
function tt_OpDeHref(t_e)
{
    var t_tag;
    if(t_e)
    {
	t_tag = t_e.target;
	while(t_tag)
	{
	    if(t_tag.hasAttribute("href"))
	    {
		tt_tag = t_tag
		tt_tag.t_href = tt_tag.getAttribute("href");
		tt_tag.removeAttribute("href");
		tt_tag.style.cursor = "hand";
		tt_tag.onmousedown = tt_OpReHref;
		tt_tag.stats = window.status;
		window.status = tt_tag.t_href;
		break;
	    }
	    t_tag = t_tag.parentElement;
	}
    }
}
function tt_OpReHref()
{
    if(tt_tag)
    {
	tt_tag.setAttribute("href", tt_tag.t_href);
	window.status = tt_tag.stats;
	tt_tag = null;
    }
}
function tt_Show(t_e, t_id, t_sup, t_clk, t_delay, t_fix, t_left, t_offx, t_offy, t_static, t_sticky, t_temp)
{
    if(tt_obj) tt_Hide();
    tt_mf = document.onmousemove || null;
    if(window.dd && (window.DRAG && tt_mf == DRAG || window.RESIZE && tt_mf == RESIZE)) return;
    var t_sh, t_h;

    tt_obj = tt_GetDiv(t_id);
    if(tt_obj)
    {
	t_e = t_e || window.event;
	tt_sub = !(tt_sup = t_sup);
	tt_sticky = t_sticky;
	tt_objW = tt_GetDivW();
	tt_objH = tt_GetDivH();
	tt_offX = t_left? -(tt_objW+t_offx) : t_offx;
	tt_offY = t_offy;
	if(tt_op7) tt_OpDeHref(t_e);
	if(tt_n4)
	{
	    if(tt_obj.document.layers.length)
	    {
		t_sh = tt_obj.document.layers[0];
		t_sh.clip.height = tt_objH - Math.round(t_sh.clip.width*1.3);
	    }
	}
	else
	{
	    t_sh = tt_GetDiv(t_id+'R');
	    if(t_sh)
	    {
		t_h = tt_objH - tt_Int(t_sh.style.pixelTop || t_sh.style.top || 0);
		if(typeof t_sh.style.pixelHeight != tt_u) t_sh.style.pixelHeight = t_h;
		else t_sh.style.height = t_h+'px';
	    }
	}

	xlim = tt_Int((tt_db && tt_db.clientWidth)? tt_db.clientWidth : window.innerWidth) +
	    tt_Int(window.pageXOffset || (tt_db? tt_db.scrollLeft : 0) || 0) -
	    tt_objW -
	    (tt_n4? 21 : 0);
	ylim = tt_Int(window.innerHeight || tt_db.clientHeight) +
	    tt_Int(window.pageYOffset || (tt_db? tt_db.scrollTop : 0) || 0) -
	    tt_objH - tt_offY;

	tt_SetDivZ();
	if(t_fix) tt_SetDivPos(tt_Int((t_fix = t_fix.split(','))[0]), tt_Int(t_fix[1]));
	else tt_SetDivPos(tt_EvX(t_e), tt_EvY(t_e));

	var t_txt = 'tt_ShowDiv(\'true\');';
	if(t_sticky) t_txt += '{'+
		'tt_ReleasMov();'+
		(t_clk? ('window.tt_upFunc = document.onmouseup || null;'+
		'if(tt_ce) document.captureEvents(Event.MOUSEUP);'+
		'document.onmouseup = new Function("window.setTimeout(\'tt_Hide();\', 10);");') : '')+
	    '}';
	else if(t_static) t_txt += 'tt_ReleasMov();';
	if(t_temp > 0) t_txt += 'window.tt_rtm = window.setTimeout(\'tt_sticky = false; tt_Hide();\','+t_temp+');';
	window.tt_rdl = window.setTimeout(t_txt, t_delay);

	if(!t_fix)
	{
	    if(tt_ce) document.captureEvents(Event.MOUSEMOVE);
	    document.onmousemove = tt_Move;
	}
    }
}
var tt_area = false;
function tt_Move(t_ev)
{
    if(!tt_obj) return;
    if(tt_n6 || tt_w3c)
    {
	if(tt_wait) return;
	tt_wait = true;
	setTimeout('tt_wait = false;', 5);
    }
    var t_e = t_ev || window.event;
    tt_SetDivPos(tt_EvX(t_e), tt_EvY(t_e));
    if(window.tt_op6)
    {
	if(tt_area && t_e.target.tagName != 'AREA') tt_Hide();
	else if(t_e.target.tagName == 'AREA') tt_area = true;
    }
}
function tt_Hide()
{
    if(window.tt_obj)
    {
	if(window.tt_rdl) window.clearTimeout(tt_rdl);
	if(!tt_sticky || !tt_act)
	{
	    if(window.tt_rtm) window.clearTimeout(tt_rtm);
	    tt_ShowDiv(false);
	    tt_SetDivPos(-tt_objW, -tt_objH);
	    tt_obj = null;
	    if(typeof window.tt_upFunc != tt_u) document.onmouseup = window.tt_upFunc;
	}
	tt_sticky = false;
	if(tt_op6 && tt_area) tt_area = false;
	tt_ReleasMov();
	if(tt_op7) tt_OpReHref();
    }
}
function tt_Init()
{
    if(!(tt_op || tt_n4 || tt_n6 || tt_ie || tt_w3c)) return;

    var htm = tt_n4? '<div style="position:absolute;"></div>' : '',
    tags,
    t_tj,
    over,
    t_b,
    esc = 'return escape(';
    for(var i = tt_tags.length; i;)
    {--i;
	tags = tt_ie? (document.all.tags(tt_tags[i]) || 1)
	    : document.getElementsByTagName? (document.getElementsByTagName(tt_tags[i]) || 1)
	    : (!tt_n4 && tt_tags[i]=="a")? document.links
	    : 1;
	if(tt_n4 && (tt_tags[i] == "a" || tt_tags[i] == "layer")) tags = tt_N4Tags(tt_tags[i]);
	for(var j = tags.length; j;)
	{--j;
	    if(typeof (t_tj = tags[j]).onmouseover == "function" && t_tj.onmouseover.toString().indexOf(esc) != -1 && !tt_n6 || tt_n6 && (over = t_tj.getAttribute("onmouseover")) && over.indexOf(esc) != -1)
	    {
		if(over) t_tj.onmouseover = new Function(over);
		var txt = unescape(t_tj.onmouseover());
		htm += tt_Htm(
		    t_tj,
		    "tOoLtIp"+i+""+j,
		    txt.wzReplace("& ","&")
		);
		// window. to circumvent the FF Alzheimer Bug
		t_tj.onmouseover = new Function('e',
		    'if(window.tt_Show && tt_Show) tt_Show(e,'+
		    '"tOoLtIp' +i+''+j+ '",'+
		    ((typeof t_tj.T_ABOVE != tt_u)? t_tj.T_ABOVE : ttAbove)+','+
		    ((typeof t_tj.T_CLICKCLOSE != tt_u)? t_tj.T_CLICKCLOSE : ttClickClose)+','+
		    ((typeof t_tj.T_DELAY != tt_u)? t_tj.T_DELAY : ttDelay)+','+
		    ((typeof t_tj.T_FIX != tt_u)? '"'+t_tj.T_FIX+'"' : '""')+','+
		    ((typeof t_tj.T_LEFT != tt_u)? t_tj.T_LEFT : ttLeft)+','+
		    ((typeof t_tj.T_OFFSETX != tt_u)? t_tj.T_OFFSETX : ttOffsetX)+','+
		    ((typeof t_tj.T_OFFSETY != tt_u)? t_tj.T_OFFSETY : ttOffsetY)+','+
		    ((typeof t_tj.T_STATIC != tt_u)? t_tj.T_STATIC : ttStatic)+','+
		    ((typeof t_tj.T_STICKY != tt_u)? t_tj.T_STICKY : ttSticky)+','+
		    ((typeof t_tj.T_TEMP != tt_u)? t_tj.T_TEMP : ttTemp)+
		    ');'
		);
		t_tj.onmouseout = tt_Hide;
		tt_DeAlt(t_tj);
	    }
	}
    }
    if(tt_ie6) htm += '<iframe id="TTiEiFrM" src="javascript:false" scrolling="no" frameborder="0" style="filter:Alpha(opacity=0);position:absolute;top:0px;left:0px;display:none;"></iframe>';
    t_b = document.getElementsByTagName? document.getElementsByTagName("body")[0] : tt_db;
    if(t_b && t_b.insertAdjacentHTML) t_b.insertAdjacentHTML("AfterBegin", htm);
    else if(t_b && typeof t_b.innerHTML != tt_u && document.createElement && t_b.appendChild)
    {
	var t_el = document.createElement("div");
	t_b.appendChild(t_el);
	t_el.innerHTML = htm;
    }
    else
	document.write(htm);
    if(document.getElementById) tt_ifrm = document.getElementById("TTiEiFrM");
}
tt_Init();
</script>
==POPUP-JS==
    }

##############################################################################
#                  Argument and config file processing
##############################################################################

my $warned = 0;
my $erred = 0;
my $errstr;

format STDERR =
^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~
$errstr
     ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
     $errstr
.

sub cfg_warn {
    return	if $opt_nowarn;

    unless($warned++) {
	warn "\n" if $erred;
	warn "- Configuration warning(s) in $opt_config:\n\n";
	}
    $errstr = "- @{[join ' ', @_]}\n";
    write STDERR;
    }

sub cfg_die {
    my $str = join " ", @_;
    my $hdr = "Configuration error in $opt_config";

    $errstr = "*  $str\n";
    write STDERR;
    if ($ENV{REQUEST_METHOD}) {
	unless ($erred) {
	    print header(-content_type => "text/html; charset=@{[$lh->charset]}"),
		start_html("$hdr"),
		h1("Configuration error");
	    }
	$str =~ s/&/&amp;/g;
	$str =~ s/</&lt;/g;
	$str =~ s/>/&gt;/g;
	print p(), pre($str);
	}
    unless ($opt_checkconfig) {
	print end_html() if $ENV{REQUEST_METHOD};
	exit 1;
	}
    $erred++;
    }

sub walk_cfg {
    my $cfg = shift;
    my ($k, $v);

    while (($k, $v) = each %$cfg) {
	if (ref $v eq "HASH") {
	    walk_cfg($v);
	    }
	elsif (!defined $v) {
	    cfg_die "Missing value for attribute $k";
	    }
	}
    }

sub read_config {
    #
    # NOTE: we declare %cfg, $k, %used, $email_regex, $collector, and $clctr
    # as local so that the collector subroutines may up-level address them!
    #
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    die $lh->maketext("Shell redirection characters not allowed in filename '[_1]'", $opt_config)
	if $opt_config =~ /[`|<>]/;
    local %cfg = ParseConfig(-ConfigFile => $opt_config,
			    -AutoTrue => 1,
			    -LowerCaseNames => 1,
			    -MergeDuplicateBlocks => 1);
    local $email_regex = email_regex();
    my %facility = map {$_, 1} qw(console daemon user), ('local0'..'local7');
    my %trap_lookup;
    my @extra;

    walk_cfg(\%cfg);

    $config{logformat} = delete $cfg{logformat} || 'text';
    ($config{logformat}, @extra) =  split /\s+/, $config{logformat};
    $config{logformat} = lc $config{logformat};
    if ($config{logformat} eq "sql") {
	eval qq{ use DBI; };
	die $@ if $@;

	cfg_die "LogFormat SQL requires an SQL type, database name and authenication credentials" unless @extra == 3;
	($config{_sql_type}, $config{_database}, $config{_db_auth}) = @extra;
	for my $driver (DBI->available_drivers) {
	    if (lc($driver) eq lc($config{_sql_type})) {
		$config{_sql_type} = $driver;
		last;
		}
	    }
	connect_to_database(1);
	verify_database_format();
	cfg_warn "Ignoring LogRead - you don't need it when LogFormat is SQL"
	    if delete $cfg{logread};
	cfg_warn "Ignoring LogWrite - you don't need it when LogFormat is SQL"
	    if delete $cfg{logwrite};
	}
    elsif ($config{logformat} eq "text") {
	cfg_warn "Ignored extra fields after 'text' in LogFormat" if @extra;
	if ($cfg{logread} && $cfg{logwrite}) {
	    $config{logread} = delete $cfg{logread};
	    $config{logwrite} = delete $cfg{logwrite};
	    }
	elsif ($cfg{logread}) {
	    $config{logread} = $config{logwrite} = delete $cfg{logread};
	    }
	elsif ($cfg{logwrite}) {
	    $config{logwrite} = $config{logread} = delete $cfg{logwrite};
	    }
	else {
	    $config{logwrite} = $config{logread} = "/var/log/thermd";
	    }
	if (! $opt_daemon && ! (-d $config{logread} && -r _)) {
	    cfg_die "Cannot read LogRead directory $config{logread}";
	    }
	if ($opt_daemon && ! (-d $config{logwrite} && -w _)) {
	    cfg_die "Cannot write LogWrite directory $config{logwrite}";
	    }
	}
    else {
	cfg_die "Unknown LogFormat (try 'text' or 'sql')";
	}

    $config{pidfile} = delete $cfg{pidfile}  || '/var/run/thermd.pid';
    if ($opt_daemon && $config{pidfile} ne File::Spec->devnull()) {
	print "Initial Daemon PID == $$\n"		if $opt_verbose;
	print "Opening PID file\n"			if $opt_verbose;
	my $fd = new FileHandle;
	sysopen($fd, $config{pidfile}, (O_RDWR | O_CREAT), 0644)
	    or die "Can't open $config{pidfile} - $!";
	$fd->autoflush(1);
	unless (flock($fd, LOCK_EX|LOCK_NB)) {
	    my $pid = <$fd>;
	    warn "I think there is a thermd daemon already running as PID $pid";
	    die "Please stop any running daemon before starting a new one.\n";
	    }
	$config{_pidfile_fd} = $fd;
	}

    $config{loginterval} = $cfg{logfrequency} || delete $cfg{loginterval};
    if (delete $cfg{logfrequency}) {
	cfg_warn "LogFrequency is now deprecated - please use LogInterval";
	}
    $config{loginterval} = parse_reltime("LogInterval", $config{loginterval}, "10m");
    if ($config{loginterval} < 15 || $config{loginterval} > 30*60) {
	cfg_warn "LogInterval should probably be between 15s and 30m";
	}
    $config{_minpoll} = $config{loginterval};	# Reduced below...
    if ($^O eq "MSWin32") {
	if (defined delete $cfg{syslogfacility}) {
	    cfg_warn "Syslog is not available on Windows - ignoring SysLogFacility";
	    }
	}
    else {
	$config{syslogfacility}	= delete $cfg{syslogfacility}  || 'user';
	unless ($facility{$config{syslogfacility}}) {
	    cfg_die "Syslog Facility '$config{syslogfacility}' unknown.  Use one of", join(", ", sort keys %facility);
	    }
	print "Opening syslog\n"			if $opt_verbose;
	openlog("$script", "pid", $config{syslogfacility});
	if ($opt_daemon) {
	    msg("notice", "Starting daemon V@{[(split(/\s+/, VERSION))[2,3]]}");
	    }
	}
    $config{mailfrom} = delete $cfg{mailfrom} || ('root@' . hostname());
    unless ($config{mailfrom} =~ /^$email_regex$/) {
	cfg_die "Address in MailFrom is not a valid email address";
	}
    if ($config{smtphost} = delete $cfg{smtphost}) {
	if (my $s = delete $cfg{sendmail}) {
	    cfg_warn "Ignoring Sendmail $s when SMTPHost is used.  Pick one.";
	    }
	$config{smtpusername} = delete $cfg{smtpusername};
	$config{smtppassword} = delete $cfg{smtppassword};
	if (defined($config{smtpusername}) != defined($config{smtppassword})) {
	    cfg_warn "You must use both SMTPUsername and SMTPPassword for
	    authentication (or neither for no authentication)";
	    }
	}
    else {
	$config{sendmail} = delete $cfg{sendmail} || '/usr/sbin/sendmail';
	if (-e $config{sendmail}) {
	    unless (-x $config{sendmail}) {
		cfg_warn "Sendmail ($config{sendmail}) doesn't look executable";
		$config{sendmail} = File::Spec->devnull();
		}
	    }
	else {
	    cfg_warn "I can't find Sendmail ($config{sendmail})";
	    $config{sendmail} = File::Spec->devnull();
	    }
	if (delete $cfg{smtpusername}) {
	    cfg_warn "Ignoring SMTPUsername when Sendmail is used";
	    }
	if (delete $cfg{smtppassword}) {
	    cfg_warn "Ignoring SMTPPassword when Sendmail is used";
	    }
	}
    $config{location}	= delete $cfg{location};
    $config{timezone}	= uc(delete $cfg{timezone} || 'GMT');
    $config{gpscoordinates}	= delete $cfg{gpscoordinates};
    $config{mapurl}	= delete $cfg{mapurl};
    if (defined $cfg{displayin}) {
	cfg_warn "DisplayIn is now deprecated - thermd will make a reasonable choice based on your locale";
	}
    $config{displayin}	= ucfirst(lc(delete $cfg{displayin} || $lh->scale));
    $config{temperature}= uc(delete $cfg{temperature} ||
				$units{ $config{displayin} }{temperature});
    $config{windspeed}	= uc(delete $cfg{windspeed} ||
				$units{ $config{displayin} }{windspeed});
    $config{barometer}	= uc(delete $cfg{barometer} ||
				$units{ $config{displayin} }{barometer});
    $config{rainfall}	= uc(delete $cfg{rainfall} ||
				$units{ $config{displayin} }{rainfall});
    $config{graphwidth} = delete $cfg{graphwidth} || 750;
    $config{graphheight}= delete $cfg{graphheight} || 300;
    $config{refreshrate}= parse_reltime("RefreshRate", delete $cfg{refreshrate}, "30m");
    unless ($config{refreshrate} >= 1) {
	cfg_die "RefreshRate must be at least 1m (and expressed as minutes)";
	}
    $config{blurb}	= delete $cfg{blurb};
    $config{blurb2}	= delete $cfg{blurb2};
    $config{sensororder}= lc(delete $cfg{sensororder}) || "name";
    unless ($config{sensororder} =~ /^(((sub)?(name|id))|popup|nosort)$/) {
	cfg_warn "Unknown SensorOrder '$config{sensororder}' - using 'name'";
	$config{sensororder}= "name";
	}
    if ($config{displayin} =~ /^([FC]|English|Metric)$/) {
	#
	# This F/C -> English/Metric is for backwards compatability
	#
	$config{displayin} = "English"	if $config{displayin} eq "F";
	$config{displayin} = "Metric"	if $config{displayin} eq "C";
	}
    else {
	cfg_die "DisplayIn must be English or Metric";
	}
    #
    # Allow specific overrides on units
    #
    $config{temperature} ||= $units{ $config{displayin} }{temperature};
    $config{temperature} = uc($config{temperature});
    unless ($config{temperature} =~ /^[CF]$/) {
	cfg_die "Temperature can only be displayed in C or F";
	}
    $config{barometer} ||= $units{ $config{displayin} }{barometer};
    $config{barometer} = uc($config{barometer});
    unless ($config{barometer} =~ /^((IN|MM)HG|HPA|KPA|MILLIBAR|MBAR)$/) {
	cfg_die "Barometric pressure can only be displayed in inHg, mmHg, hPa, kPa, mBar or millibar";
	}
    $config{rainfall} ||= $units{ $config{displayin} }{rainfall};
    $config{rainfall} = uc($config{rainfall});
    unless ($config{rainfall} =~ /^(INCH(ES)?|MM)$/) {
	cfg_die "Rainfall can only be displayed in inches or mm";
	}
    $config{windspeed} ||= $units{ $config{displayin} }{windspeed};
    $config{windspeed} = uc($config{windspeed});
    unless ($config{windspeed} =~ /^(MPH|KPH|MPS|KNOTS?)$/) {
	cfg_die "WindSpeed can only be displayed in MPH, KPH, MPS, or Knots";
	}
    $config{snmptrapport} = delete $cfg{snmptrapport} || 162;
    unless ($config{snmptrapport} =~ /$numeric/) {
	cfg_warn "Non-numeric SNMPTrapPort - using 162";
	$config{snmptrapport} = 162;
	}
    #
    # I can't comment (no really, I can't comment).  But I'll give you a hint:
    # If you reverse engineer this or bypass it, you're accepting the risk
    #
    if ($config{decode_base64("YWNrbm93bGVkZ2U=")} =
	    lc(delete $cfg{decode_base64("YWNrbm93bGVkZ2U=")})) {
	if ($config{decode_base64("YWNrbm93bGVkZ2U=")} eq decode_base64("bm93YXJyYW50eQ==")) {
	    $opt_sesame++;
	    }
	else {
	    cfg_die "I don't recognize",
		ucfirst(decode_base64("YWNrbm93bGVkZ2U=")),
		ucfirst($config{decode_base64("YWNrbm93bGVkZ2U=")});
	    }
	}

    if ($cfg{rss}) {
	if (ref $cfg{rss} eq "HASH") {
	    if (keys %{$cfg{rss}} == 1) {
		my ($k, $v) = %{$cfg{rss}};
		my $showit;
		unless (-d $k) {
		    cfg_warn "RSS directory $k does not exist - creating it";
		    mkdir $k or cfg_die "Cannot create RSS subdirectory $k";
		    }
		cfg_die "RSS directory $k is not writable"	if $opt_daemon && ! -w $k;
		$config{rss}{_dir} = $k;
		$config{rss}{url} = delete $v->{url} ||
		    cfg_die "Missing URL in RSS directive";
		$config{rss}{webmaster} = delete $v->{webmaster} ||
		    cfg_die "Missing Webmaster in RSS directive";
		unless ($config{rss}{webmaster} =~ /^$email_regex$/) {
		    cfg_die "Address in RSS Webmaster is not a valid email address";
		    }
		$config{rss}{every} = delete $v->{every} || 1;
		cfg_die "Illegal value for Every in RSS block" unless $config{rss}{every} =~ /^\d+$/;
		$config{rss}{nice} = delete $v->{nice};
		if (keys %$v == 0) {
		    delete $cfg{rss};
		    }
		else {
		    cfg_warn "Unknown components in RSS directive ignored: ", join(", ", keys %$v);
		    }
		}
	    else {
		cfg_die "Only one RSS directive may be specified";
		}
	    }
	else {
	    cfg_die "RSS directive must be enclosed by <RSS dir> and </RSS>";
	    }
	}

    if ($cfg{collector}) {
	if (ref $cfg{collector} eq "HASH") {
	    #
	    # NOTE: $k is declared local so the various device subroutines
	    # can up-level address it.  The weird sort just forces derived
	    # collectors last (because we have to have all the other collectors
	    # defined before we can use their sensors)..
	    #
	    for $k (sort { lc($cfg{collector}{$a}{type}) eq "derived" ? 1 : -1 }keys %{$cfg{collector}}) {
		$collector = \%{$config{collector}{$k}};	# Autovivify
		$clctr = $cfg{collector}{$k};
		$collector->{_name} = $k;

		if (ref $clctr->{type}) {
		    die "You have two collectors named $k - each collector must have unique name\n";
		    }
		defined ($collector->{type} = lc(delete $clctr->{type})) ||
		    cfg_die "Missing Type directive in <Collector $k>";
		$collector->{_datatype} = $collector->{type};	# The default
		$collector->{readonly} = defined(delete $clctr->{readonly});
		if ($collector->{type} eq "qk145") {
		    parse_qk145_vk011();
		    if ($opt_daemon && ! $collector->{readonly}) {
			$collector->{baudrate} ||= B2400;
			$collector->{_fd} = unit_open($collector, $collector->{baudrate});
			}
		    }
		elsif ($collector->{type} eq "vk011") {
		    parse_qk145_vk011();
		    if ($opt_daemon && ! $collector->{readonly}) {
			$collector->{baudrate} ||= B9600;
			$collector->{_fd} = unit_open($collector, $collector->{baudrate});
			}
		    }
		elsif ($collector->{type} eq "maxbotix") {
		    parse_maxbotix();
		    if ($opt_daemon && ! $collector->{readonly}) {
			$collector->{baudrate} ||= B9600;
			$collector->{_fd} = unit_open($collector, $collector->{baudrate});
			}
		    }
		elsif ($collector->{type} =~ /^ow(fs|httpd|shell)$/) {
		    parse_owfs($collector->{type});
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_owfs_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} =~ /^newport\s+(ibtx(-m)?|ibthx|iptx-[dw]|itcx|ithx-[mw2])/) {
		    $collector->{type} = "newport";
		    $collector->{_subtype} = $1;
		    parse_newport();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_newport_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq "ha7net") {
		    parse_ha7net();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_ha7net_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} =~ /^(poseidon\s*\d+|damocles\s*(\d+e?|mini))$/) {
		    #
		    # We parse this device specially, but we poll it via SNMP
		    #
		    parse_hwg();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			if ($collector->{subtype} eq 'snmp') {
			    push @pollers, sub {
				$collector->{_subr} = \&fork_snmp_poller;
				create_child_process($collector);
				};
			    }
			elsif ($collector->{subtype} eq 'http') {
			    push @pollers, sub {
				$collector->{_subr} = \&fork_hwg_poller;
				create_child_process($collector);
				};
			    }
			else {
			    die "Unpossible SubType $collector->{subtype}";
			    }
			}
		    }
		elsif ($collector->{type} eq "snmp") {
		    parse_snmp();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_snmp_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} =~ /^veris\s+(H803[0156])$/i) {
		    $collector->{type} = "veris";
		    $collector->{_subtype} = $1;
		    parse_veris();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			eval qq{ use Modbus::Client; };
			die $@ if $@;
			$collector->{baudrate} ||= B9600;
			$collector->{_fd} = unit_open($collector, $collector->{baudrate});
			my $modbus = new Modbus::Client $collector->{_fd};
			$collector->{_modbus_device} = $modbus->device($collector->{modbusaddress});
			push @pollers, sub {
			    $collector->{_subr} = \&fork_veris_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq "enersure") {
		    parse_enersure();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			eval qq{ use Modbus::Client; };
			die $@ if $@;
			$collector->{baudrate} ||= B9600;
			$collector->{_fd} = unit_open($collector, $collector->{baudrate});
			my $modbus = new Modbus::Client $collector->{_fd};
			$collector->{_modbus_device} = $modbus->device($collector->{modbusaddress});
			push @pollers, sub {
			    $collector->{_subr} = \&fork_enersure_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq "wunderground") {
		    parse_wunderground();
		    if (exists $config{collector}{"WUNDERGROUND/"}) {
			my $sub = $collector;
			my $collector =			 # Autovivify
			    \%{ $config{collector}{"WUNDERGROUND/"} };
			push @{ $collector->{_c} }, $sub;
			}
		    else {
			#
			# Create a group collector - note that the name of
			# the pseudo-collector is illegal in the config
			# file, so we'll never have a real one named thus
			#
			my $sub = $collector;
			my $collector =			 # Autovivify
			    \%{ $config{collector}{"WUNDERGROUND/"} };
			$collector->{type} = "wunderground_group";
			$collector->{_name} = "WUNDERGROUND/";
			$collector->{_datatype} = "wunderground_group";
			$collector->{_c} = [ $sub ]; # List of collectors
			$collector->{_ua} = $sub->{_ua};
			undef $sub->{_ua};
			$collector->{pollinterval} = 60;	# Fake
			if ($opt_daemon && ! $collector->{readonly}) {
			    push @pollers, sub {
				$collector->{_subr} = \&fork_wunderground_poller;
				create_child_process($collector);
				};
			    }
			}
		    }
		#
		# We allow you to specifiy minigoose, weathergoose, supergoose,
		# racsense or powergoose, but we treat them all the same :-)
		#
		elsif ($collector->{type} =~ /^((mini|weather|super|power)goose|racsense)$/) {
		    parse_weathergoose();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_weathergoose_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} =~ /^proliphix\s*(.*)$/) {
		    parse_proliphix($1);
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_proliphix_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq 'smartnet') {
		    parse_smartnet();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_smartnet_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq "em1") {
		    parse_em1();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_em1_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq "tempager") {
		    parse_roomalert(4);
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_roomalert_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} =~ /^room\s*alert\s*(7|11|24|26)[ew]?$/) {
		    parse_roomalert($1);
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_roomalert_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} =~ "commandline") {
		    parse_commandline();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my $collector = $collector; 	# For closure
			push @pollers, sub {
			    $collector->{_subr} = \&fork_commandline_poller;
			    create_child_process($collector);
			    };
			}
		    }
		elsif ($collector->{type} eq "temp08") {
		    my $count;
		    parse_temp08();
		    if ($opt_daemon && ! $collector->{readonly}) {
			my ($char, $d, $h, $m, $s, $sck_str);
			$collector->{baudrate} ||= B9600;
			$collector->{_fd} = unit_open($collector, $collector->{baudrate});
			$collector->{_fd}->autoflush(1);
			print "Initializing TEMP08\n" if $opt_verbose;
			# You do NOT want to internationalize this
			($s, $m, $h, $d) = (localtime)[0..2, 6];
			$sck_str = sprintf "%02d,%02d,%02d,%02d",$d+1,$h,$m,$s;
			print "  Sending newline\n" if $opt_verbose;
			syswrite($collector->{_fd}, "\n");	# Synchronize
			eval {
			    local $SIG{ALRM} = \&unstick;
			    alarm 1;
			    while (sysread($collector->{_fd}, $char, 1)) {
				print $char if $opt_verbose;
				last if $char eq ">";
				}
			    alarm 0;
			    };
			print "  Sending 'SPT00'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "SPT00");	# Shaddup!
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			until ($collector->{_DIS_str} =~ /Qty/) {
			    undef $collector->{_DIS_str};
			    print "  Sending 'DIS'\n" if $opt_verbose;
			    syswrite($collector->{_fd}, "DIS");	# What's there?
			    while (sysread($collector->{_fd}, $char, 1)) {
				print $char if $opt_verbose;
				$collector->{_DIS_str} .= $char;# For sensor ID
				last if $char eq ">";
				}
			    sleep 1;
			    if ($count++ > 25) {
				msg("err", "Cannot communicate with Temp08\n");
				last;
				}
			    }
			print "$collector->{_DIS_str}\n" if $opt_verbose;
			print "  Sending 'SCK$sck_str'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "SCK$sck_str\n");# Set TOD
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			print "  Sending 'DTIon'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "DTIon");	# No TOD log
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			print "  Sending 'SIDon'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "SIDon");	# Seral #s
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			print "  Sending 'STDC'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "STDC");	# Centigrade
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			print "  Sending 'SPT01'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "SPT01");	# Every minute
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			print "  Sending 'TMP'\n" if $opt_verbose;
			syswrite($collector->{_fd}, "TMP");	# Read now!
			while (sysread($collector->{_fd}, $char, 1)) {
			    # print $char if $opt_verbose;
			    last if $char eq ">";
			    }
			#
			# Note: The ">" is printed before the data readings,
			# so this needs to be the last command (and let the
			# data readings sit in the input buffer)
			#
			}
		    }
		elsif ($collector->{type} eq "derived") {
		    parse_derived();
		    }
		else {
		    cfg_die "Unknown collector type '$collector->{type}'.  Try QK145, VK011, HA7Net, EM1, Temp08, TemPageR, RoomAlert 7E (11E, 24E, or 26W), owfs, owhttpd, owshell, Proliphix, Poseidon 1250 (3262, 3265, 3266, 3268, or 2251), Damocles 2405 (0808e, 0816, or MINI), Newport (or Omega) iBTX (iBTX-M, iBTHX, iPTX-D, iPTX-W, iTCX, iTHX-M, iTHX-W, or iTHX-2), Veris H8030 (H8031, H8035, H8036), EnerSure, or MaxBotix";
		    }
		if (keys %{$cfg{collector}{$k}}) {
		    cfg_warn "Unknown components in <Collector $k> ignored: ", join(", ", keys %{$cfg{collector}{$k}});
		    }
		delete $cfg{collector}{$k};
		}
	    delete $cfg{collector};
	    }
	else {
	    cfg_die "Collectors must be enclosed by <Collector name> and </Collector>";
	    }
	}
    else {
	cfg_die "You must specify at least one <Collector> directive";
	}

    if ($cfg{view}) {
	if (ref $cfg{view} eq "HASH") {
	    for my $Vn (keys %{$cfg{view}}) {
		my $view = \%{$config{view}{lc($Vn)}};	# Autovivify
		my $vw   = $cfg{view}{$Vn};
		if ($Vn =~ /^all$/i) {
		    cfg_die "<View All> is reserved and automatically defined.  Maybe you want to make another View the DefaultView?";
		    }
		$view->{_name} = $Vn;
		if (ref $vw eq "HASH") {
		    $view->{type} = lc(delete $vw->{type} || "graph");
		    cfg_warn "Unknown View Type '$view->{type}' in <View $Vn>"
			unless $view->{type} =~ /^(graph|image|wunderground)$/;
		    #
		    # (Mostly) Common View components
		    #
		    if ($view->{type} =~ /^(graph|image)$/) {
			$view->{rssname} = delete $vw->{rssname};
			$view->{rssorder} = delete $vw->{rssorder} || 999;
			cfg_warn "Non-numeric RSSOrder in <View $Vn>"
			    unless $view->{rssorder} =~ /$numeric/;
			$view->{buttonorder} = delete $vw->{buttonorder} || 999;
			cfg_warn "Non-numeric ButtonOrder in <View $Vn>"
			    unless $view->{buttonorder} =~ /$numeric/;
			}
		    #
		    # Type specific View components
		    #
		    if ($view->{type} eq "graph") {
			$view->{graphtype} =
			    lc(delete $vw->{graphtype} || "natural");
			cfg_warn "Unknown GraphType '$view->{graphtype}' in <View $Vn> - use Radar, Natural or Lines"
			    unless $view->{graphtype} =~ /^(radar|natural|lines)$/;
			$view->{maxmin} = delete $vw->{maxmin};
			cfg_warn "Non-numeric MaxMin in <View $Vn>"
			    unless $view->{maxmin} =~ /$optnumeric/;
			$view->{minmax} = delete $vw->{minmax};
			cfg_warn "Non-numeric MinMax in <View $Vn>"
			    unless $view->{minmax} =~ /$optnumeric/;
			$view->{cliplo} = delete $vw->{cliplo};
			cfg_warn "Non-numeric ClipLo in <View $Vn>"
			    unless $view->{cliplo} =~ /$optnumeric/;
			$view->{cliphi} = delete $vw->{cliphi};
			cfg_warn "Non-numeric ClipHi in <View $Vn>"
			    unless $view->{cliphi} =~ /$optnumeric/;
			if ($vw->{show}) {
			    if (ref $vw->{show} eq "ARRAY") {
				for (@{$vw->{show}}) {
				    check_and_set_view_item($Vn, $_);
				    }
				}
			    elsif (ref $vw->{show} eq "") {
				check_and_set_view_item($Vn, $vw->{show});
				}
			    else {
				cfg_die "Unexpected Show in <View $Vn>"
				}
			    delete $vw->{show};
			    }
			if ($vw->{"show+"}) {
			    if (ref $vw->{"show+"} eq "HASH") {
				for (keys %{$vw->{"show+"}}) {
				    check_and_set_graph_hash($Vn, $_);
				    }
				}
			    elsif (ref $vw->{"show+"} eq "") {
				check_and_set_view_item($Vn, $vw->{"show+"});
				}
			    else {
				cfg_die "Unexpected <Show+> in <View $Vn>"
				}
			    delete $vw->{"show+"};
			    }
			}
		    elsif ($view->{type} eq "image") {
			cfg_die "Missing Image <View $Vn>"
			    unless $view->{image} = delete $vw->{image};
			unless (substr($view->{image}, 0, 1) eq "/") {
			    if ($config{rss}{_dir}) {
				$view->{image} = "$config{rss}{_dir}/$view->{image}"
				}
			    else {
				cfg_die "Image <View $Vn> must start with a '/' if there is not RSS block"
				}
			    }
			cfg_warn "Can't find <View $Vn> Image $view->{image}"
			    unless -e $view->{image};
			$view->{font} = delete $vw->{font} || "Helvetica";
			$view->{fontsize} = delete $vw->{fontsize} || 14;
			cfg_warn "Non-numeric FontSize in <View $Vn>"
			    unless $view->{fontsize} =~ /$numeric/;
			if (my $color = lc(delete $vw->{textcolor})) {
			    if ($color_map{$color} || $color =~ /^#[0-9a-f]{6}$/) {
				$view->{textcolor} = $color;
				}
			    else {
				cfg_warn "Unknown TextColor '$color' - use one of ", join(", ", sort keys %color_map);
				$view->{textcolor} = 'black';
				}
			    }
			else {
			    $view->{textcolor} = 'black';
			    }
			$view->{textalign} = lc(delete $vw->{textalign} || "left");
			unless ($view->{textalign} =~ /^(left|center|right)$/) {
			    cfg_warn "Unknown TextAlign '$view->{textalign}' - use one of left, center, right";
			    $view->{textalign} = 'left';
			    }
			# It is legal to have a 0 textangle
			$view->{textangle} = delete $vw->{textangle} || 0;
			cfg_warn "Non-numeric TextAngle in <View $Vn>"
			    unless $view->{textangle} =~ /$numeric/;
			# It is legal to have a 0 precision
			$view->{precision} = delete $vw->{precision};
			$view->{precision} = 1 unless defined $view->{precision};
			cfg_warn "Non-numeric Precision in <View $Vn>"
			    unless $view->{precision} =~ /$numeric/;
			if ($vw->{"show+"}) {
			    if (ref $vw->{"show+"} eq "HASH") {
				for (keys %{$vw->{"show+"}}) {
				    check_and_set_image_hash($Vn, $_);
				    }
				}
			    else {
				cfg_die "Unexpected <Show+> in <View $Vn>"
				}
			    delete $vw->{"show+"};
			    }
			if ($vw->{date}) {
			    if (ref $vw->{date} eq "HASH") {
				check_and_set_datetime($Vn, "Date");
				}
			    else {
				cfg_die "Unexpected <Date> in <View $Vn>"
				}
			    delete $vw->{date};
			    }
			if ($vw->{time}) {
			    if (ref $vw->{time} eq "HASH") {
				check_and_set_datetime($Vn, "Time");
				}
			    else {
				cfg_die "Unexpected <Time> in <View $Vn>"
				}
			    delete $vw->{time};
			    }

			if (delete $vw->{show}) {
			    cfg_warn "Show ignored in <View $Vn> (use Show+ when Type=Image)";
			    }
			}
		    elsif ($view->{type} eq "wunderground") {
			my $ua;
			eval qq{ use LWP::UserAgent; };
			die $@ if $@;
			$ua = $view->{_ua} = new LWP::UserAgent;
			$ua->agent("$script/@{[(split(/\s+/,VERSION))[2,3]]})");
			unless ($view->{stationid} = delete $vw->{stationid}) {
			    cfg_die "Missing StationID in <View $Vn>";
			    }
			unless ($view->{password} = delete $vw->{password}) {
			    cfg_die "Missing Password in <View $Vn>";
			    }
			if ($vw->{show}) {
			    if (ref $vw->{show} eq "ARRAY") {
				for (@{$vw->{show}}) {
				    check_and_set_view_item($Vn, $_);
				    check_and_set_wunder_key($Vn, $_);
				    }
				}
			    elsif (ref $vw->{show} eq "") {
				check_and_set_view_item($Vn, $vw->{show});
				check_and_set_wunder_key($Vn, $vw->{show});
				}
			    else {
				cfg_die "Unexpected Show in <View $Vn>"
				}
			    delete $vw->{show};
			    }
			if ($vw->{"show+"}) {
			    if (ref $vw->{"show+"} eq "HASH") {
				for (keys %{$vw->{"show+"}}) {
				    check_and_set_wunder_hash($Vn, $_);
				    check_and_set_wunder_key($Vn, $_);
				    }
				}
			    elsif (ref $vw->{"show+"} eq "") {
				check_and_set_view_item($Vn, $vw->{"show+"});
				check_and_set_wunder_key($Vn, $vw->{"show+"});
				}
			    else {
				cfg_die "Unexpected <Show+> in <View $Vn>"
				}
			    delete $vw->{"show+"};
			    }
			#
			# Postprocess the view, since we have to send things
			# to wunderground in a specific format
			#
			assign_real_wunder_keys($config{_view}{lc $Vn});
			}
		    else {
			die "Sanity check error: Unknown View type $view->{type} in <View $Vn>";
			}
		    }
		else {
		    cfg_die "Views must contain sensors to Show";
		    }
		if (keys %$vw) {
		    cfg_warn "Unknown components in <View $Vn> ignored: ", join(", ", keys %$vw);
		    }
		delete $cfg{view}{$Vn};
		}
	    }
	else {
	    cfg_die "Views must be enclosed by <View name> and </View>";
	    }
	delete $cfg{view};
	}
    $config{view}{all}{rssname} = "Default Graphs";
    $config{view}{all}{type} = "graph";
    #
    # In rare cases (can't find collectors), we'll have no view named 'all',
    # so create it here, just in case!
    #
    $config{_view}{all} ||= {};				# Vivify
    #
    # Validate the views (specifically, warn of a view contains sensors
    # of more than one class).
    #
    for my $v (keys %{$config{_view}}) {
	my ($sensor, %scale, $scale);
	for my $id (keys %{ $config{_view}{$v} }) {
	    $sensor = $config{_view}{$v}{$id}{_sensor};
	    $scale = $sensor->{_scale};
	    if ($scale eq "C" || $scale eq "F") {
		$scale{Temperature}++;
		}
	    else {
		$scale{ $sensor->{_label} }++;
		}
	    }
	if ($opt_checkconfig && $config{view}{$v}{type} eq "graph" && keys %scale > 1) {
	    cfg_warn "<View $v> contains sensors with more than one scale: ",
		join (", ", keys %scale), "(Warning only - it will work OK)";
	    }
	}
    $config{defaultview} = lc(delete $cfg{defaultview} || 'all');
    unless (exists $config{_view}{ $config{defaultview} }) {
	cfg_warn "DefaultView $config{defaultview} doesn't exist - using 'all'";
	$config{defaultview} = 'all';
	}

    if (keys %cfg) {
	cfg_warn "Unknown top-level components ignored: ", join(", ", keys %cfg);
	}
    #
    # Make sure _minpoll has a reasonable value (it shouldn't be 0, which
    # will happen none of the collectors are polling collectors)
    #
    $config{_minpoll} ||= 10;
    #
    # I have no idea how it happens - sometimes the daemon gets a null-key
    # collector - so we need to delete it now before weird stuff happens.
    #
    delete $config{collector}{''};
    #
    # Walk the config tree one last time, checking sensors, alarms, actuators
    #
    for my $k (keys %{ $config{collector} }) {
	$collector = $config{collector}{$k};
	for my $n (keys %{ $collector->{sensor} }) {
	    my $sensor = $collector->{sensor}{$n};
	    #
	    # Fill in any missing sensor names.  This is needed because only
	    # some collectors can fill the names from the device
	    #
	    $sensor->{name} ||= "Sensor $n\@$k";
	    #
	    # Handle _min and _max temperatures for any sensor which does not
	    # explicitly set it.  Assume 1-wire sensors for range limiting.
	    #
	    $sensor->{_min_c} = -55	unless defined $sensor->{_min_c};
	    $sensor->{_min_f} = $sensor->{_min_c}*9/5+32;
	    $sensor->{_max_c} = 125	unless defined $sensor->{_max_c};
	    $sensor->{_max_f} = $sensor->{_max_c}*9/5+32;
	    #
	    # See if any sensors have AllowSNMPTrap set - if they do, then
	    # add the collector and sensor information to a cache which will
	    # be used in a special SnmpTrap collector, created below.
	    #
	    if ($sensor->{allowsnmptraps}) {
		$trap_lookup{"$collector->{_ipnumber}:$sensor->{_oid}"} =  {
		    collector 	=> $collector,
		    n		=> $n,
		    };
		}
	    #
	    # See if any alarms use Open or Close, and if so, make sure that
	    # the actuators actually exist, and convert the named requests
	    # into subroutines.  This is needed because an alarm in one
	    # collector can open/close an actuator in a different collector,
	    # and we can't guarantee (or require) parse order.
	    #
	    for my $a (keys %{ $sensor->{alarm} }) {
		my $alarm = $sensor->{alarm}{$a};
		my $actuator;
		for my $loc (qw(open lockopen close lockclose)) {
		    for my $action (@{ $alarm->{$loc} }) {
			my $oc;
			my ($N, $K) = split /\@/, $action, 2;
			unless ($K) {
			    cfg_die "\u$loc $action in Alarm $alarm->{_nka} is not an Actuator name with format nnn\@mmm";
			    }
			if ($actuator = $config{collector}{$K}{actuator}{$N}) {
			    #
			    # The alarm specifies an Open or Close, but the
			    # functions (being internal values) are in {_open}
			    # and {_close} of the actuator hash.  Only set
			    # the reset action if we don't have a lock action
			    #
			    ($oc = $loc) =~ s/lock//;
			    push @{$alarm->{_activate}}, $actuator->{"_$oc"};
			    push @{$alarm->{_reset}},
				$actuator->{$oc eq "open" ? "_close" : "_open"}
				    unless $loc =~ /^lock/;
			    }
			else {
			    cfg_die "\u$loc $action in Alarm $alarm->{_nka} is not a valid Actuator";
			    }
			}
		    }
		for my $action (@{ $alarm->{exec} }) {
		    push @{$alarm->{_activate}}, sub { system "$action" };
		    }
		}
	    }
	for my $n (keys %{ $collector->{actuator} }) {
	    my $actuator = $collector->{actuator}{$n};
	    #
	    # Fill in any missing actuator names.  This is needed because only
	    # some collectors can fill the names from the device
	    #
	    $actuator->{name} ||= "Actuator $n\@$k";
	    }
	}
    #
    # And finally, did we find any sensors that AllowSNMPTrap?  If so, create
    # a pseudo-collector (with an impossible-to-create name) to deal with them
    #
    if (keys %trap_lookup) {
	my $collector = {};		# So closure works
	my $ok;
	my $output = `snmptrapd -v`;
	if ($output =~ /NET-SNMP Version:\s*(\d+)\.(\d+)\.\d+/) {
	    if ($1 < 5 || $2 < 3) {	# 5.3.x
		cfg_warn "Found NET-SNMP snmptrapd, but must be at least version 5.3.0 - all AllowSNMPTrap's ignored";
		}
	    else {
		$ok++;
		}
	    }
	else {
	    cfg_warn "Could not find NET-SNMP snmptrapd - all AllowSNMPTrap's ignored";
	    }
	if ($ok) {
	    $collector->{name} = "Builtin SNMP Trap Handler";
	    $collector->{_datatype} = "snmp_trap";
	    $collector->{_trap_lookup} = \%trap_lookup;
	    push @pollers, sub {
		$collector->{_subr} = \&fork_snmp_traphandler;
		create_child_process($collector);
		};
	    $config{collector}{"snmp_trap_handler/"} = $collector;
	    }
	}
    }

sub check_and_set_view_item {
    my ($v, $elem) = @_;
    my ($n, $k) = determine_sensor_parts($v, $elem);

    # 
    # Note that view name is converted to lower case, and (very important)
    # they key is not a HASH, but just the STRING VALUE of the hash address...
    # We need a unique value that corresponds to the sensor, we don't really
    # care what it is (although the same value is used everywhere)...
    #
    if (exists $config{collector}{$k}{sensor}{$n}) {
	my $view = \%{ $config{_view}{lc($v)}{$elem} };	 	# Autovivify
	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{sensor}{$n}
	}
    elsif (exists $config{collector}{$k}{actuator}{$n}) {
	my $view = \%{ $config{_view}{lc($v)}{$elem} };	 	# Autovivify
	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{actuator}{$n}
	}
    else {
	cfg_warn "Sensor $n\@$k does not exist for use in View $v - ignoring"
	}
    }

sub check_and_set_graph_hash {
    my ($v, $elem) = @_;
    my ($n, $k) = determine_sensor_parts($v, $elem);
    my ($name, $color, $linetype, $popup, $view);

    my $elm = $cfg{view}{$v}{"show+"}{$elem};
    # 
    # Same comment as above about STRING VALUE of the hash address...
    #
    if (exists $config{collector}{$k}{sensor}{$n}) {
	$view = \%{ $config{_view}{lc($v)}{$elem} };	# Autovivify
	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{sensor}{$n};
	}
    elsif (exists $config{collector}{$k}{actuator}{$n}) {
	$view = \%{ $config{_view}{lc($v)}{$elem} };	# Autovivify
	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{actuator}{$n};
	}

    if (defined $view) {
	if ($name = delete $elm->{name}) {
	    $view->{name} = $name;
	    }
	if ($popup = delete $elm->{popup}) {
	    $view->{popup} = $popup;
	    $config{_has_popups} = 1;
	    }
	if ($color = lc delete $elm->{graphcolor}) {
	    if ($color_map{$color}) {
		$view->{graphcolor} = $color_map{$color};
		}
	    elsif ($color =~ /^#[0-9a-f]{6}$/) {
		$view->{graphcolor} = $color;
		}
	    else {
		cfg_warn "Unknown GraphColor '$color' - use one of ",
		    join(", ", sort keys %color_map);
		$view->{graphcolor} = $color_map{black};
		}
	    }
	if ($linetype = lc delete $elm->{linetype}) {
	    if ($linetype_map{$linetype}) {
		$view->{linetype} = $linetype_map{$linetype};
		}
	    else {
		cfg_warn "Unknown LineType '$linetype' - use one of ",
		    join(", ", sort keys %linetype_map);
		$view->{linetype} = $linetype_map{solid};
		}
	    }

	for (qw(X Y Font FontSize TextColor TextAngle TextAlign Precision)) {
	    cfg_warn "$_ is ignored in <View $elem> ($_ is only valid when Type=Image)"
		if delete $elm->{lc($_)};
	    }
	if (keys %{ $elm }) {
	    cfg_warn "Unknown components in <Show+ $elem> ignored: ",
		join(", ", keys %{ $elm });
	    }
	}
    else {
	cfg_warn "Sensor $n\@$k does not exist for use in View $v - ignoring"
	}
    }

sub check_and_set_image_hash {
    my ($V, $elem) = @_;
    my $v = lc $V;
    my ($n, $k) = determine_sensor_parts($V, $elem);
    my ($font, $fontsize, $precision, $textcolor, $textangle, $textalign,
	$view);

    my $elm = $cfg{view}{$V}{"show+"}{$elem};
    if (exists $config{collector}{$k}{sensor}{$n}) {
	$view = \%{ $config{_view}{$v}{$elem} };	# Autovivify
	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{sensor}{$n};
	}
    elsif (exists $config{collector}{$k}{actuator}{$n}) {
	$view = \%{ $config{_view}{$v}{$elem} };	# Autovivify
	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{actuator}{$n};
	}

    if (defined $view) {
	$view->{x} = delete $elm->{x};
	cfg_warn "Non-numeric or missing X coord in <View $V><Show+ $elem>"
	    unless $view->{x} =~ /$numeric/;
	$view->{y} = delete $elm->{y};
	cfg_warn "Non-numeric or missing Y coord in <View $V><Show+ $elem>"
	    unless $view->{y} =~ /$numeric/;
	if ($font = delete $elm->{font}) {
	    $view->{font} = $font;
	    }
	else {
	    $view->{font} = $config{view}{$v}{font};
	    }
	if ($fontsize = delete $elm->{fontsize}) {
	    if ($fontsize =~ /$numeric/) {
		$view->{fontsize} = $fontsize;
		}
	    else {
		cfg_warn "Non-numeric FontSize in <View $V><Show+ $elem>";
		}
	    }
	else {
	    $view->{fontsize} = $config{view}{$v}{fontsize};
	    }
	# Use "defined" because precision may be 0
	if (defined($precision = delete $elm->{precision})) {
	    if ($precision =~ /$numeric/) {
		$view->{precision} = $precision;
		}
	    else {
		cfg_warn "Non-numeric Precision in <View $V><Show+ $elem>";
		}
	    }
	else {
	    $view->{precision} = $config{view}{$v}{precision};
	    }
	if ($textcolor = lc(delete $elm->{textcolor})) {
	    if ($color_map{$textcolor} || $textcolor =~ /^#[0-9a-f]{6}$/) {
		$view->{textcolor} = $textcolor;
		}
	    else {
		cfg_warn "Unknown TextColor '$textcolor' - use one of ",
		    join(", ", sort keys %color_map);
		$view->{textcolor} = 'black';
		}
	    }
	else {
	    $view->{textcolor} = $config{view}{$v}{textcolor};
	    }
	if ($textalign = lc(delete $elm->{textalign})) {
	    $textalign = lc $textalign;
	    unless ($view->{textalign} =~ /^(left|center|right)$/) {
		cfg_warn "Unknown TextAlign '$view->{textalign}' - use one of left, center, right";
		$view->{textalign} = 'left';
		}
	    }
	else {
	    $view->{textalign} = $config{view}{$v}{textalign};
	    }
	# Use "defined" because textangle may be 0
	if (defined($textangle = delete $elm->{textangle})) {
	    if ($textangle =~ /$numeric/) {
		$view->{textangle} = $textangle;
		}
	    else {
		cfg_warn "Non-numeric TextAngle in <View $V><Show+ $elem>";
		}
	    }
	else {
	    $view->{textangle} = $config{view}{$v}{textangle};
	    }

	for (qw(LineType GraphColor)) {
	    cfg_warn "$_ is ignored in <View $elem> ($_ is only valid when Type=Graph)"
		if delete $elm->{lc($_)};
	    }
	if (keys %{ $elm }) {
	    cfg_warn "Unknown components in <Show+ $elem> ignored: ",
		join(", ", keys %{ $elm });
	    }
	}
    else {
	cfg_warn "Sensor $n\@$k does not exist for use in View $V - ignoring"
	}
    }

sub check_and_set_wunder_hash {
    my ($V, $elem) = @_;
    my $v = lc $V;
    my ($n, $k) = determine_sensor_parts($V, $elem);
    my $elm = $cfg{view}{$V}{"show+"}{$elem};

    if (exists $config{collector}{$k}{sensor}{$n}) {
	my $view = \%{ $config{_view}{$v}{$elem} };	# Autovivify

	$view->{_name} = $elem;
	$view->{_sensor} = $config{collector}{$k}{sensor}{$n};
	if ($view->{key} = lc(delete $elm->{key})) {
	    cfg_warn "Illegal Key '$view->{key}' in <View $V><Show+ $elem>.  Legal values are",
		    join(", ", sort keys %wunder_types)
		unless $wunder_types{ $view->{key} };
	    }
	if (keys %{ $elm }) {
	    cfg_warn "Unknown components in <Show+ $elem> ignored: ",
		join(", ", keys %{ $elm });
	    }
	}
    else {
	cfg_warn "Sensor $n\@$k does not exist for use in View $V - ignoring"
	}
    }

sub check_and_set_wunder_key {
    my ($V, $elem) = @_;
    my $v = lc $V;
    my ($n, $k) = determine_sensor_parts($V, $elem);

    if (exists $config{collector}{$k}{sensor}{$n}) {
	my $view = \%{ $config{_view}{$v}{$elem} };	# Autovivify

	if ($view->{_sensor}{type} eq "temperature") {
	    $view->{key} ||= "tempf";
	    cfg_warn "Temperature Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} =~ /tempf$/;
	    }
	elsif ($view->{_sensor}{type} eq "humidity") {
	    $view->{key} ||= "humidity";
	    cfg_warn "Humidity Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} =~ /(humidity|moisture)$/;
	    }
	elsif ($view->{_sensor}{type} eq "barometer") {
	    $view->{key} ||= "baromin";
	    cfg_warn "Barometer Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} eq "baromin";
	    }
	elsif ($view->{_sensor}{type} eq "rain") {
	    $view->{key} ||= "dailyrainin";
	    cfg_warn "Rain Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} eq "dailyrainin";
	    }
	elsif ($view->{_sensor}{type} eq "speed") {
	    $view->{key} ||= "windspeedmph";
	    cfg_warn "Windspeed Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} eq "windspeedmph";
	    }
	elsif ($view->{_sensor}{type} eq "gust") {
	    $view->{key} ||= "windgustmph";
	    cfg_warn "Wind gust Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} eq "windgustmph";
	    }
	elsif ($view->{_sensor}{type} eq "direction") {
	    $view->{key} ||= "winddir";
	    cfg_warn "Wind direction Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} eq "winddir";
	    }
	elsif ($view->{_sensor}{type} eq "dewpoint") {
	    $view->{key} ||= "dewptf";
	    cfg_warn "DewPoint Sensor $elem cannot have Key $view->{key}"
		unless $view->{key} eq "dewptf";
	    }
	else {
	    cfg_warn "\u$view->{_sensor}{type} Sensor $elem is not a type that can be sent to Wunderground";
	    }
	}
    }

sub assign_real_wunder_keys {
    my $view = shift;
    my ($count, %used);

    for my $k (keys %$view) {
	$used{ $view->{$k}{key} }++;
	}
    for my $u (keys %used) {
	next if $used{$u} == 1 || length($u) == 0;
	if ($u eq "tempf") {
	    $count = 0;
	    for my $k (keys %$view) {
		next unless $k eq $u;
		if ($count == 0) { # tempf, temp2f, temp3f...
		    $count = 2;
		    }
		else {
		    $view->{$k}{key} =~ s/temp/temp$count/;
		    $count++;
		    }
		}
	    }
	elsif ($u eq "soiltempf") {
	    $count = 0;
	    for my $k (keys %$view) {
		next unless $k eq $u;
		if ($count == 0) { # soiltempf, soiltemp2f, soiltemp3f...
		    $count = 2;
		    }
		else {
		    $view->{$k}{key} =~ s/temp/temp$count/;
		    $count++;
		    }
		}
	    }
	elsif ($u eq "soilmoisture") {
	    $count = 0;
	    for my $k (keys %$view) {
		next unless $k eq $u;
		if ($count == 0) { # soilmosture, soilmosture2, soilmoisture3...
		    $count = 2;
		    }
		else {
		    $view->{$k}{key} =~ s/$/$count/;
		    $count++;
		    }
		}
	    }
	else {
	    cfg_die "<View $view->{_name}>, the Wunderground upload protocol does not allow multiple '$u' sensors";
	    }
	}
    }

sub determine_sensor_parts {
    my ($v, $elem) = @_;
    my ($n, $k) = split '@', $elem, 2;
    unless ($k) {
	($k, $n) = split '[/:]', $elem, 2;
	if ($n) {
	    cfg_warn "$elem in <View $v> is deprecated, use $n\@$k instead";
	    $_[1] = "$n\@$k";		# Change by reference
	    }
	else {
	    cfg_die "Illegal sensor name in <View $v>, use sensor\@collector";
	    }
       }
    return ($n, $k);
    }

sub check_and_set_datetime {
    my ($V, $K) = @_;
    my $v = lc $V;
    my $k = lc $K;
    my ($font, $fontsize, $precision, $textcolor, $textangle, $textalign);
    my $kind = \%{ $config{view}{$v}{$k} };	# Autovivify
    my $knd = $cfg{view}{$V}{$k};

    $kind->{x} = delete $knd->{x};
    cfg_warn "Non-numeric or missing X coord in <View $V><$K>"
	unless $kind->{x} =~ /$numeric/;
    $kind->{y} = delete $knd->{y};
    cfg_warn "Non-numeric or missing Y coord in <View $V><$K>"
	unless $kind->{y} =~ /$numeric/;
    if ($font = delete $knd->{font}) {
	$kind->{font} = $font;
	}
    else {
	$kind->{font} = $config{view}{$v}{font};
	}
    if ($fontsize = delete $knd->{fontsize}) {
	if ($fontsize =~ /$numeric/) {
	    $kind->{fontsize} = $fontsize;
	    }
	else {
	    cfg_warn "Non-numeric FontSize in <View $V><$K>"
	    }
	}
    else {
	$kind->{fontsize} = $config{view}{$v}{fontsize};
	}
    # Use "defined" because precision may be 0
    if (defined($precision = delete $knd->{precision})) {
	if ($precision =~ /$numeric/) {
	    $kind->{precision} = $precision;
	    }
	else {
	    cfg_warn "Non-numeric Precision in <View $V><$K>"
	    }
	}
    else {
	$kind->{precision} = $config{view}{$v}{precision};
	}
    # Use "defined" because textangle may be 0
    if (defined($textangle = delete $knd->{textangle})) {
	if ($textangle =~ /$numeric/) {
	    $kind->{textangle} = $textangle;
	    }
	else {
	    cfg_warn "Non-numeric TextAngle in <View $V><$K>"
	    }
	}
    else {
	$kind->{textangle} = $config{view}{$v}{textangle};
	}
    if ($textcolor = lc(delete $knd->{textcolor})) {
	if ($color_map{$textcolor} || $textcolor =~ /^#[0-9a-f]{6}$/) {
	    $kind->{textcolor} = $textcolor;
	    }
	else {
	    cfg_warn "Unknown TextColor '$textcolor' - use one of ",
		join(", ", sort keys %color_map);
	    $kind->{textcolor} = 'black';
	    }
	}
    else {
	$kind->{textcolor} = $config{view}{$v}{textcolor};
	}
    if ($textalign = lc(delete $knd->{textalign})) {
	$textalign = lc $textalign;
	unless ($kind->{textalign} =~ /^(left|center|right)$/) {
	    cfg_warn "Unknown TextAlign '$kind->{textalign}' - use one of left, center, right";
	    $kind->{textalign} = 'left';
	    }
	}
    else {
	$kind->{textalign} = $config{view}{$v}{textalign};
	}
    if (keys %{ $cfg{view}{$V}{$k} }) {
	cfg_warn "Unknown components in <$K> ignored",
	    join(", ", keys %{ $cfg{view}{$V}{$k} });
	}
    }

sub get_web_addr {
    my ($uses_auth, $hack_auth) = @_;
    # Up-level addressed variables from read_config
    use vars qw($collector $clctr $k);
    my $retval;

    $collector->{username} = delete $clctr->{username};
    $collector->{password} = delete $clctr->{password};
    if ($collector->{username} || $collector->{password}) {
	if (!$uses_auth) {
	    cfg_warn "Ignoring Username and/or Password for <Collector $k> (HTTP authentication is not used)";
	    }
	elsif ($hack_auth) {
	    $retval = "$collector->{username}:$collector->{password}\@";
	    }
	}
    defined ($collector->{ipaddress} = delete $clctr->{ipaddress}) ||
	cfg_die "Missing IPAddress directive in <Collector $k>";
    unless ($collector->{ipaddress} =~ /^[\w.-]+$/) {
	cfg_die "IPAddress '$collector->{ipaddress}' must be a host name or a number";
	}
    if ($collector->{ipaddress} =~ /^[\d.]+$/) {
	$collector->{_ipnumber} = $collector->{ipaddress};
	}
    else {
	my $a = gethostbyname($collector->{ipaddress});
	if ($a) {
	    $collector->{_ipnumber} = inet_ntoa($a);
	    }
	else {
	    cfg_warn "<Collector $k> IPAddress $collector->{ipaddress} does not resolve to an IP number"
		unless $collector->{readonly};
	    }
	}
    $retval .= $collector->{ipaddress};
    $collector->{port} = delete $clctr->{port};
    if (defined $collector->{port} && $collector->{port} !~ /^\d+$/) {
	cfg_die "Port '$collector->{port}' must be a number";
	}
    $retval .= ":$collector->{port}"		if $collector->{port};
    return $retval;
    }

sub parse_device_or_ip_addr {
    # Up-level addressed variables from read_config
    use vars qw($k $collector $clctr);
    # We encapsulate everything in eval"" because some values are not defined
    # on some operating systems...
    my %baudrate = (	50	=> eval "B50",
			75	=> eval "B75",
			110	=> eval "B110",
			134	=> eval "B134",
			150	=> eval "B150",
			200	=> eval "B200",
			300	=> eval "B300",
			600	=> eval "B600",
			1200	=> eval "B1200",
			1800	=> eval "B1800",
			2400	=> eval "B2400",
			4800	=> eval "B4800",
			9600	=> eval "B9600",
			19200	=> eval "B19200",
			38400	=> eval "B38400",
			7200	=> eval "B7200",
			14400	=> eval "B14400",
			28800	=> eval "B28800",
			57600	=> eval "B57600",
			76800	=> eval "B76800",
			115200	=> eval "B115200",
			230400	=> eval "B230400",
			460800	=> eval "B460800",
			921600	=> eval "B921600",
		    );

    if (defined ($collector->{device} = delete $clctr->{device})) {
	if (delete($clctr->{ipaddress}) + delete($clctr->{port})) {
	    cfg_die "Choose ONE of Device or IPAddress/Port for <Collector $k>";
	    }
	if ($collector->{baudrate} = delete $clctr->{baudrate}) {
	    if ($baudrate{ $collector->{baudrate} }) {
		$collector->{baudrate} = $baudrate{ $collector->{baudrate} };
		}
	    else {
		if ($collector->{baudrate} =~ /^0/) {
		    $collector->{baudrate} = oct($collector->{baudrate});
		    }
		else {
		    cfg_warn "Unrecognized Baudrate $collector->{baudrate} for <Collector $k>";
		    }
		}
	    }
	}
    else {
	defined ($collector->{ipaddress} = delete $clctr->{ipaddress}) ||
	    cfg_die "Missing Device or IPAddress directive in <Collector $k>";
	unless ($collector->{ipaddress} =~ /^[\w.-]+$/) {
	    cfg_die "IPAddress '$collector->{ipaddress}' must be a host name or a number";
	    }
	if ($collector->{ipaddress} =~ /^[\d.]+$/) {
	    $collector->{_ipnumber} = $collector->{ipaddress};
	    }
	else {
	    my $a = gethostbyname($collector->{ipaddress});
	    if ($a) {
		$collector->{_ipnumber} = inet_ntoa($a);
		}
	    else {
		cfg_warn "<Collector $k> IPAddress $collector->{ipaddress} does not resolve to an IP number";
		}
	    }
	defined ($collector->{port} = delete $clctr->{port}) ||
	    cfg_die "Missing Port for IPAddress directive in <Collector $k>";
	if (defined $collector->{port} && $collector->{port} !~ /^\d+$/) {
	    cfg_die "Port '$collector->{port}' must be a number";
	    }
	}
    if (delete $clctr->{username}) {
	cfg_warn "Ignoring Username for <Collector $k> (HTTP is not used)";
	}
    if (delete $clctr->{password}) {
	cfg_warn "Ignoring Password for <Collector $k> (HTTP is not used)";
	}
    }

sub parse_qk145_vk011 {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    parse_device_or_ip_addr();
    parse_collector(sub { $_[0] =~ /^[1-4]$/ }, "a number between 1..4");
    }

sub parse_maxbotix {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    parse_device_or_ip_addr();
    parse_collector(sub { $_[0] =~ /^Range$/i }, "Range (only one sensor is allowed");
    }

sub parse_temp08 {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    parse_device_or_ip_addr();
    parse_collector(sub { $_[0] =~ /^[\dA-F]{16}(\.\w)?$/ },
	"a 16-digit uppercase hexadecimal 1-wire serial number (plus optional .letter or .digit)");
    }

sub parse_owfs {
    my $type = shift;
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    if ($type eq "owshell") {
	$collector->{_baseurl} =  get_web_addr(0,0);
	}
    elsif ($type eq "owhttpd") {
	$collector->{mountpoint} = delete $clctr->{mountpoint};
	unless (defined $collector->{mountpoint}) {
	    cfg_warn "Missing MountPoint for <Collector $k> - using '/', but you might want to specify 'uncached' or 'bus.0' or...";
	    $collector->{mountpoint} = "/";
	    }
	# Mountpoint must start with /, _baseurl doesn't end with /
	$collector->{mountpoint} =~ s#^(?!/)#/#;
	$collector->{_baseurl} = "http://" . get_web_addr(1,0) . $collector->{mountpoint};
	$collector->{_baseurl} =~ s#/*$##;
	}
    elsif ($type eq "owfs") {
	defined ($collector->{mountpoint} = delete $clctr->{mountpoint}) ||
	    cfg_die "Missing MountPoint for <Collector $k>";
	$collector->{mountpoint} =~ s#/$##;
	}
    else {
	die "Unpossible OW type";
	}

    $collector->{_datatype} = "generic";

    if ($opt_daemon || $opt_checkconfig) {
	if ($collector->{type} eq "owfs") {
	    unless (-d $collector->{mountpoint} && -r _ && -x _) {
		cfg_warn "Cannot read OWFS directory $collector->{mountpoint}";
		}
	    }
	elsif ($collector->{type} eq "owhttpd") {
	    $ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	    $ua->timeout(5);
	    $response = $ua->get("$collector->{_baseurl}/");
	    if ($response->is_error) {
		cfg_warn "Cannot contact OWHTTPD at $collector->{_baseurl}",
		    $response->status_line;
		}
	    $ua->timeout(30);
	    }
	elsif ($collector->{type} eq "owshell") {
	    # Nothing special, we read it with the owread command
	    }
	else {
	    die "Unknown collector type";
	    }
	}
    parse_collector(sub { $_[0] =~ /^[\dA-F]{2}\.?[\dA-F]{12}(\.?[\dA-F]{2})?(\.\w)?$/ },
	"a 14- or 16-digit uppercase hexadecimal OWFS f~[.~]i~[~[.~]c~] serial number (plus optional .letter or .digit)");
    }

sub parse_newport {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $ok);

    (undef) = get_web_addr(0,0);	# Parse the parts, don't use the URL
    $collector->{_datatype} = "generic";

    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly}) {
	eval qq{ use Net::Telnet; };
	die $@ if $@;
	$ua = $collector->{_ua} = new Net::Telnet(
	    Timeout			=> 10,
	    Output_record_separator	=> "",
	    Errmode			=> "return",
	    );
	unless ($ua) {
	    cfg_warn "Could not create Telnet object for Newport/Omega $collector->{_subtype}";
	    return;
	    }
	$ok = $ua->open (
	    host 			=> $collector->{ipaddress},
	    port			=> $collector->{port} || 2000,
	    );
	unless ($ok) {
	    cfg_warn "Could not create Telnet connection for Newport/Omega $collector->{_subtype}";
	    return;
	    }
	}
    parse_collector(sub { $_[0] =~ /$newport{$collector->{_subtype}}{regex}/ },
	$newport{$collector->{_subtype}}{errmsg});

    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly}) {
	$ua->close;	# We reopen it each time in the poller
	}
    }

sub parse_ha7net {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response, $version_str);

    $collector->{_baseurl} = "http://" . get_web_addr(1,0);
    $collector->{_datatype} = "generic";

    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly}) {
	$ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	$ua->timeout(5);
	$response = $ua->get("$collector->{_baseurl}/");
	if ($response->is_error) {
	    cfg_warn "Cannot contact HA7Net at $collector->{_baseurl}",
		$response->status_line;
	    }
	else {
	    ($version_str) = $response->content =~ /HA7Net:\s+(\d+(\.\d+)+)/;
	    print "Found HA7Net version $version_str at $collector->{_baseurl}\n"	if $opt_verbose;
	    # Convert from a string to a Perl multi-dotted version "number"
	    $collector->{_version} = eval $version_str;
	    if ($collector->{_version} lt 1.0.0.22) {
		cfg_warn "You are running HA7Net software version $version_str - please upgrade to at least 1.0.0.22, or many features may not work";
		}
	    }
	$ua->timeout(30);
	}
    parse_collector(
	    (
	    sub { $_[0] =~ /^[\dA-F]{16}(\.\w)?$/ },
	    "a 16-digit uppercase hexadecimal 1-wire serial number (plus optional .letter or .digit)",
	    undef
	    ) x 2
	);
    }

sub parse_hwg {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $error, $response, $sensor_conf, $actuator_conf, $key, $val, $idx,
	$addr, @sensID);

    ($addr) = get_web_addr(0,0);	# Parse the parts, don't use the URL
    $collector->{_real_type} = $collector->{type};
    $collector->{type} = "hwg";
    $collector->{subtype} = lc(delete $clctr->{subtype} || "http");
    if ($collector->{subtype} ne 'snmp' && $collector->{subtype} ne 'http') {
	cfg_die "Collector SubType must be one of HTTP or SNMP for $collector->{type}";
	}
    $collector->{_baseurl} = "http://$addr";
    $collector->{_datatype} = "generic";

    if (defined delete $clctr->{baseoid}) {
	cfg_warn "Ignoring BaseOID for <Collector $k>";
	}
    if ($collector->{_real_type} =~/^poseidon/) {
	$collector->{baseoid} = ".1.3.6.1.4.1.21796.3.3";
	}
    elsif ($collector->{_real_type} =~ /^damocles/) {
	$collector->{baseoid} = ".1.3.6.1.4.1.21796.3.4";
	}
    else {
	die "Unknown _real_type $collector->{_real_type} in parse_hwg";
	}
    cfg_die "BaseOID must start with a '.' and contain only numbers and '.'s"
	unless $collector->{baseoid} =~ /^(\.\d+)+$/;
    if ($collector->{subtype} eq 'snmp') {
	$collector->{port} ||= 161;
	$collector->{community} = delete $clctr->{community} || "public";
	# Binary sensor values (n=1..4, value off=1, on=2): 1.1.2.n (inpState)
	# Binary sensor names: 1.1.3.n (inpName)
	# Binary output values (n=1..2, value off=1, on=2): 2.1.2.n (outState)
	# Binary output names: 2.1.3.n (outName)
	# 1-wire sensor IDs: 3.1.8.n (sensID)
	# 1-wire sensor units: 3.1.9.n (sensUnit)
	# 1-wire sensor names: 3.1.2.n (sensName)
	# 1-wire sensor values (string 18.5 C): 3.1.5.n (sensString)
	# 1-wire sensor values (number 185): 3.1.6.n (sensValue)
	# Missing sensor: -999.9 
	if (! $collector->{readonly}) { AUTODETECT: {
	    eval qq{ use Net::SNMP; };
	    die $@ if $@;
	    ($ua, $error) = Net::SNMP->session(
		-hostname	=> $collector->{ipaddress},
		-port	=> $collector->{port},
		-community	=> $collector->{community},
		-version	=> 1,
		-timeout	=> 1.5,		# Default is 5.0
		-retries	=> 3,		# Default is 1
		);
	    cfg_die "Could not create SNMP agent to probe HWg - $error"
		unless $ua;
	    $collector->{_ua} = $ua;
	    $response = $ua->get_request(
		-varbindlist => [ ".1.3.6.1.2.1.1.1.0" ],	# SNMPv2-MIB::sysDescr.0
		);
	    unless (defined $response) {
		cfg_warn "Cannot contact HWg device at $collector->{ipaddress}:$collector->{port} - expect more errors due to unknown components.",
		    "Is the Community name '$collector->{community}' correct?";
		$collector->{readonly} = 1;
		last AUTODETECT;
		}
	    #
	    # Most HWg configuration is done by asking the device what is available.
	    # Start with dry contact inputs by listing POSEIDON-MIB::inpName
	    # or DAMOCLES-MIB::inpName (fortunately, the sub-OIDs are the same).
	    # Store all information in $sensor_conf (prepend a 'B' to the idx)
	    #
	    $response = $ua->get_table(-baseoid => "$collector->{baseoid}.1.1.3");
	    unless (defined $response) {
		cfg_warn "Cannot autodetect sensor IDs from <Collector $k>.",
		    "Expect more errors due to unknown components";
		$collector->{readonly} = 1;
		last AUTODETECT;
		}
	    while (($key, $val) = each %$response) {
    msg("debug","$key, $val");
		($idx) = $key =~ /(\d+)$/;
		$sensor_conf->{"B$idx"} = {
			    #  POSEIDON-MIB::inpState.$idx
		    oid	    => "$collector->{baseoid}.1.1.2.$idx",
		    type    => "onoff",
		    name    => $val,
		    };
		}
	    #
	    # Now look at the 1-wire and and RS485 sensors (they are not present
	    # on the Damocles).  Preserve the IDs without changes
	    #
	    if ($collector->{_real_type} =~ /^poseidon/) {
		#
		# Start by listing POSEIDON-MIB::sensID to the the exported ID, and
		# save the OID of POSEIDON-MIB::sensValue to read the values later
		#
		$response = $ua->get_table(-baseoid => "$collector->{baseoid}.3.1.8");
		unless (defined $response) {
		    cfg_warn "Cannot autodetect sensor IDs from <Collector $k>.",
			"Expect more errors due to unknown components";
		    $collector->{readonly} = 1;
		    last AUTODETECT;
		    }
		while (($key, $val) = each %$response) {
    msg("debug","$key, $val");
		    ($idx) = $key =~ /(\d+)$/;
		    $sensor_conf->{$val} = {
					#   POSEIDON-MIB::sensValue.$idx
			oid	    => "$collector->{baseoid}.3.1.6.$idx",
			};
		    $sensID[$idx] = $val;		# array by [device-index]
		    }
		#
		# Next suss out the device names from POSEIDON-MIB::sensName
		#
		$response = $ua->get_table(-baseoid => "$collector->{baseoid}.3.1.2");
		unless (defined $response) {
		    cfg_warn "Cannot autodetect sensor IDs from <Collector $k>.",
			"Expect more errors due to unknown components";
		    $collector->{readonly} = 1;
		    last AUTODETECT;
		    }
		while (($key, $val) = each %$response) {
    msg("debug","$key, $val");
		    ($idx) = $key =~ /(\d+)$/;
		    warn "Unexpected sensName.$idx found without sensID"
			    unless defined $sensID[$idx];
		    $sensor_conf->{ $sensID[$idx] }->{name} = $val;
		    }
		#
		# Next suss out the device types from POSEIDON-MIB::sensUnit
		#
		$response = $ua->get_table(-baseoid => "$collector->{baseoid}.3.1.9");
		unless (defined $response) {
		    cfg_warn "Cannot autodetect sensor IDs from <Collector $k>.",
			"Expect more errors due to unknown components";
		    $collector->{readonly} = 1;
		    last AUTODETECT;
		    }
		while (($key, $val) = each %$response) {
    msg("debug","$key, $val");
		    ($idx) = $key =~ /(\d+)$/;
		    warn "Unexpected sensUnit.$idx found without sensID"
			    unless defined $sensID[$idx];
		    if ($val == 0) {
			$sensor_conf->{ $sensID[$idx] }->{type} = "temperature";
			}
		    elsif ($val == 3) {
			$sensor_conf->{ $sensID[$idx] }->{type} = "humidity";
			}
		    elsif ($val == 4) {
			$sensor_conf->{ $sensID[$idx] }->{type} = "volts";
			}
		    elsif ($val == 5) {
			$sensor_conf->{ $sensID[$idx] }->{type} = "milliamps";
			}
		    else {
			warn "Ignoring unexpected sensUnit.$idx == $val in <Collector $k>";
			}
		    }
		}
	    #
	    # Now look at binary outputs by listing POSEIDON-MIB::outName or
	    # DAMOCLES-MIB::outName (again, the sub-OIDs are the same), but
	    # store the results in $actuator_conf (prepend an 'A' to the idx)
	    #
	    $response = $ua->get_table(-baseoid => "$collector->{baseoid}.2.1.3");
	    unless (defined $response) {
		cfg_warn "Cannot autodetect actuator IDs from <Collector $k>.",
		    "Expect more errors due to unknown components";
		$collector->{readonly} = 1;
		last AUTODETECT;
		}
	    while (($key, $val) = each %$response) {
    msg("debug","$key, $val");
		($idx) = $key =~ /(\d+)$/;
		$actuator_conf->{"A$idx"} = {
			    #  POSEIDON-MIB::outState.$idx
		    oid	    => "$collector->{baseoid}.2.1.2.$idx",
		    type    => "onoff",
		    name    => $val,
		    };
		}
	    } }	# Two close curleys, one for AUTODETECT
	}	# End if subtype is 'snmp'
    elsif ($collector->{subtype} eq 'http') {
	if (! $collector->{readonly}) {
	    $ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	    $ua->timeout(5);

	    $response = $ua->get("$collector->{_baseurl}/");
	    if ($response->is_error) {
		cfg_warn "Cannot contact HWg at $collector->{_baseurl}",
		    $response->status_line;
		}
	    $ua->timeout(30);
	    }
	}
    else {
	die "Unpossible SubType $collector->{subtype}";
	}

    parse_collector(
	sub { $_[0] =~ /^B?\d+$/ },
	"the letter 'B' and a string of digits (e.g., B3) for a binary (dry contact) input, or a string of digits (e.g., 64792) for a sensor",
	($collector->{subtype} eq 'snmp' ? $sensor_conf : undef),
	sub { $_[0] =~ /^A\d$/ },
	"the letter 'A' and a single digit (e.g., A2) for an actuator",
	($collector->{subtype} eq 'snmp' ? $actuator_conf : undef));

    if ($collector->{subtype} eq 'snmp') {
	#
	# Get the OIDs of the sensors/actuators the user has specified.  If
	# necessary/possible, get any auto-config's sensor/actuator names
	#
	for my $n (keys %{ $collector->{sensor} }) {
	    $collector->{sensor}{$n}{_oid}   = $sensor_conf->{$n}{oid};
	    $collector->{sensor}{$n}{name} ||= $sensor_conf->{$n}{name};
	    }
	for my $n (keys %{ $collector->{actuator} }) {
	    $collector->{actuator}{$n}{_oid}   = $actuator_conf->{$n}{oid};
	    $collector->{actuator}{$n}{name} ||= $actuator_conf->{$n}{name};
	    }
	}
    elsif ($collector->{subtype} eq 'http') {
	for my $n (keys %{ $collector->{sensor} }) {
	    my $oid = $n;
	    next unless $oid =~ s/^B/$collector->{baseoid}.1.1.2./;
	    $collector->{sensor}{$n}{_oid}   = $oid;
	    }
	}
    }

sub parse_snmp {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $error, $response);

    (undef) = get_web_addr(0,0);	# Parse the parts, don't use the URL
    $collector->{port} ||= 161;
    $collector->{_datatype} = "generic";

    $collector->{baseoid} = delete $clctr->{baseoid} || ".1.3.6.1.2.1";
    $collector->{community} = delete $clctr->{community} || "public";
    cfg_die "BaseOID must start with a '.' and contain only numbers and '.'s"
	unless $collector->{baseoid} =~ /^(\.\d+)+$/;
    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly}) {
	eval qq{ use Net::SNMP; };
	die $@ if $@;
	($ua, $error) = Net::SNMP->session(
	    -hostname	=> $collector->{ipaddress},
	    -port	=> $collector->{port},
	    -community	=> $collector->{community},
	    -version	=> 1,
	    -timeout	=> 1.5,		# Default is 5.0
	    -retries	=> 3,		# Default is 1
	    );
	cfg_die "Could not create SNMP agent - $error"	unless $ua;
	$collector->{_ua} = $ua;
	$response = $ua->get_request(
	    -varbindlist => [ ".1.3.6.1.2.1.1.1.0" ],	# SNMPv2-MIB::sysDescr.0
	    );
	unless (defined $response) {
	    cfg_warn "Cannot contact SNMP device at $collector->{ipaddress}:$collector->{port} - is the Community name '$collector->{community}' correct?";
	    }
	}
    parse_collector(
	    (
	    sub { $_[0] =~ /^[\w-]+$/ },
	    "an name consisting of alphanumerics, dashes and/or underscores",
	    undef
	    ) x 2
	);
    }

sub parse_veris {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    $collector->{_datatype} = "generic";
    if ($collector->{_subtype} =~ /H803[05]/i) {
	$collector->{_last_modbus} = 40003;
	}
    elsif ($collector->{_subtype} =~ /H803[16]/i) {
	$collector->{_last_modbus} = 40027;
	}
    else {
	die "Unpossible Veris subtype $collector->{_subtype}";
	}

    if ($collector->{_subtype} =~ /H803[01]/i) {
	$collector->{_multiplier_key} = "H8030/H8031"
	}
    elsif ($collector->{_subtype} =~ /H803[56]/i) {
	$collector->{_multiplier_key} = "H8035/H8036"
	}
    else {
	die "Unpossible Veris subtype $collector->{_subtype}";
	}

    parse_device_or_ip_addr();
    $collector->{modbusaddress} = delete($clctr->{modbusaddress}) || 1;
    if ($collector->{amperage} = delete($clctr->{amperage})) {
	if ($collector->{_subtype} =~ /H803[01]/i) {
	    cfg_die "Illegal Amperage for Collector $collector->{_name} - must be 100 or 300"
		unless $collector->{amperage} =~ /^[13]00$/;
	    }
	elsif ($collector->{_subtype} =~ /H803[56]/i) {
	    cfg_die "Illegal Amperage for Collector $collector->{_name} - must be 100, 300, 400, 800, 1600, or 2400"
		unless $collector->{amperage} =~ /^([1348]|16|24)00$/;
	    }
	else {
	    die "Unpossible veris subtype $collector->{_subtype}";
	    }
	}
    else {
	cfg_die "Missing Amperage in Collector $collector->{_name}";
	}
    cfg_die "Illegal ModbusAddress in Collector $collector->{_name}"
	unless $collector->{modbusaddress} =~ /^\d+$/;
    parse_collector(sub { $_[0] =~ /^(\d+)$/ && ($1 == 40001 || ($1 >= 40003 && $1 <= $collector->{_last_modbus}))},
	"a register number between 40001..$collector->{_last_modbus} (but not 40002)");
    }

sub parse_enersure {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    $collector->{_datatype} = "generic";

    parse_device_or_ip_addr();
    $collector->{modbusaddress} = delete($clctr->{modbusaddress}) || 1;
    cfg_die "Illegal ModbusAddress in Collector $collector->{_name}"
	unless $collector->{modbusaddress} =~ /^\d+$/;
    parse_collector(sub { $_[0] =~ /^[VIPWK](\d\d?)$/i && $1 > 0 && $1 <= 84},
	"the letters V, I, P, K, or W followed by a number between 1..84");
    }

sub parse_wunderground {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    $collector->{_datatype} = "generic";
    $collector->{stationid} = delete $clctr->{stationid};
    $collector->{staleafter} = parse_reltime("<Collector $k>StaleAfter", delete $clctr->{staleafter}, "65m");
    cfg_die "Missing StationID in <Collector $k>"
	unless$collector->{stationid};
    $collector->{_baseurl} = "http://api.wunderground.com/weatherstation/WXCurrentObXML.asp?ID=$collector->{stationid}";

    #
    # Since wunderground is handled by a group collector, we create a _ua
    # only in the first one we see (it is taken over by the group)
    #
    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly} &&
	    ! exists $config{collector}{"WUNDERGROUND/"}) {
	$ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	$ua->timeout(10);
	$response = $ua->get($collector->{_baseurl});
	if ($response->is_error) {
	    cfg_warn "Cannot contact Wunderground at $collector->{_baseurl}",
		$response->status_line;
	    }
	$ua->timeout(30);
	}
    parse_collector(sub { $_[0] =~ /^[THPSGDB]$/ },
	"One of the letters T, H, P, S, G, D, or B");
    }

sub parse_weathergoose {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    $collector->{_real_type} = $collector->{type};
    $collector->{type} = "weathergoose";
    $collector->{_datatype} = "generic";
    $collector->{_baseurl} = "http://" . get_web_addr(1,0);

    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly}) {
	$ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	$ua->timeout(5);
	$response = $ua->get("$collector->{_baseurl}/");
	if ($response->is_error) {
	    cfg_warn "Cannot contact WeatherGoose/RacSense at $collector->{_baseurl}",
		$response->status_line;
	    }
	$ua->timeout(30);
	}
    parse_collector(sub { $_[0] =~ /^[\dA-F]{16}(\.\w)?$/ },
	"a 16-digit uppercase hexadecimal 1-wire serial number (plus optional .letter or .digit)");
    }

sub parse_smartnet {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    #
    # RPC::XML::Client used LWP::UserAgent (not My::LWP::UserAgent, where we
    # do "proper" authentication callbacks), so when we get_web_addr, we ask
    # it to hack up credentials in the URL
    #
    $collector->{_baseurl} = "http://" . get_web_addr(1,1) .  "/XmlRpc";
    $collector->{_datatype} = "generic";

    if (! $collector->{readonly}) {
	eval qq{ use RPC::XML::Client; };
	die $@ if $@;
	$ua = $collector->{_ua} = RPC::XML::Client->new("$collector->{_baseurl}/");
	$response = $collector->{_ua}->simple_request('SmartNet.Time');
	$collector->{_offset} = time - $response;
	if ($RPC::XML::ERROR) {
	    cfg_warn "Cannot contact SmartNet at $collector->{_baseurl}";
	    }
	}

    parse_collector(sub { $_[0] =~ /^[\dA-F]{16}(-[12])?(\.\w)?$/ },
	"an uppercase hexadecimal ROMID (plus optional .letter or .digit)");

    #
    # If necessary, get any auto-config's sensor names
    #
    if (! $opt_daemon && ! $collector->{readonly}) {
	my ($need_names, @names);

	$need_names = 0;
	for my $n (keys %{ $collector->{sensor} }) {
	    unless ($collector->{sensor}{$n}{name}) {
		$need_names = 1;
		last;
		}
	    }
	if ($need_names) {
	    my (@hold, @query, $when, $err);
	    if ($collector->{_smartwatt}) {
		@hold = ();
		while (@query = splice(@{ $collector->{_smartwatt} }, 0, 10)) {
		    $response = $ua->simple_request('SmartWatt.Read', @query);
		    $when = shift @{$response};
		    if ($err = shift @{$response}) {
			msg("err", "Error code $err on SmartNet $k");
			}
		    for my $r (@{$response}) {
			for my $n (hunt_for_sensor($collector, $r->{ROMID}, "watts")) {
			    $collector->{sensor}{$n}{name} ||= $r->{UserID};
			    }
			for my $n (hunt_for_sensor($collector, $r->{ROMID}, "wh")) {
			    $collector->{sensor}{$n}{name} ||= $r->{UserID};
			    }
			}
		    push @hold, @query;
		    }
		$collector->{_smartwatt} = [ @hold ];
		}
	    if ($collector->{_smartsense}) {
		@hold = ();
		while (@query = splice(@{ $collector->{_smartsense} }, 0, 10)) {
		    $response = $ua->simple_request('SmartSenseTH.Read', @query);
		    $when = shift @{$response};
		    if ($err = shift @{$response}) {
			msg("err", "Error code $err on SmartNet $k");
			}
		    for my $r (@{$response}) {
			for my $n (hunt_for_sensor($collector, $r->{ROMID}, "temperature")) {
			    $collector->{sensor}{$n}{name} ||= $r->{UserID};
			    }
			for my $n (hunt_for_sensor($collector, $r->{ROMID}, "humidity")) {
			    $collector->{sensor}{$n}{name} ||= $r->{UserID};
			    }
			for my $n (hunt_for_sensor($collector, $r->{ROMID}, "dewpoint")) {
			    $collector->{sensor}{$n}{name} ||= $r->{UserID};
			    }
			}
		    push @hold, @query;
		    }
		$collector->{_smartsense} = [ @hold ];
		}
	    }
	}
    }

sub parse_proliphix {
    my $type = shift;
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    $type =~ s/^\s*//;
    $type =~ s/\s*$//;
    $collector->{type} = "proliphix";		# Change to generic type
    $collector->{_baseurl} = "http://" . get_web_addr(1,0);
    $collector->{_datatype} = "generic";

    if (($opt_daemon || $opt_checkconfig) && ! $collector->{readonly}) {
	$ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	$ua->timeout(10);
	$response = $ua->post("$collector->{_baseurl}/get",
		{ "OID2.7.1" => "" });
	if ($response->is_error) {
	    cfg_warn "Cannot contact Proliphix at $collector->{_baseurl}",
		$response->status_line;
	    }
	else {
	    for my $ov (split /&/, $response->content || "Error") {
		my ($o, $v) = split /=/, $ov;
		if ($o eq "OID2.7.1") {

		    my $abbr = $type;
		    $abbr =~ s/[eh]$//;
		    if (uc($abbr) ne uc($v)) {
			cfg_warn "You say collector $k is a Proliphix @{[uc($type)]}, but it says it is a $v";
			}
		    }
		else {
		    cfg_warn "Unexpected OID $o returned from Proliphix $k";
		    }
		}
	    }
	$ua->timeout(30);
	}
    for ($type) {
	/^nt10e$/				&& do {
	    parse_collector(sub { $_[0] =~ /^(T1|S)$/i }, "T1 or S");
	    last;
	    };
	/^(nt20e|(nt1[02]0|tm220)[eh])$/	&& do {
	    parse_collector(sub { $_[0] =~ /^(T[1-3]|S)$/i },
		"one of T1, T2, T3 or S");
	    last;
	    };
	/^(nt150|tm250)[eh]$/			&& do {
	    parse_collector(sub { $_[0] =~ /^(T[1-3]|H1|S)$/i },
		"one of T1, T2, T3, H1 or S");
	    last;
	    };
	die "Unknown type '$type' of Proliphix IP Thermostat";
	}
    }

sub parse_em1 {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    $collector->{_baseurl} = "http://" . get_web_addr(1,0);
    $collector->{_datatype} = "generic";

    if (! $collector->{readonly}) {
	$ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	$ua->timeout(5);

	$response = $ua->get("$collector->{_baseurl}/");
	if ($response->is_error) {
	    cfg_warn "Cannot contact EM1 at $collector->{_baseurl}",
		$response->status_line;
	    }
	$ua->timeout(30);
	}

    parse_collector(sub { $_[0] =~ /^[THW][1-4]$/ },
	"one of T[1-4], H[1-4], or W[1-4]");

    #
    # If necessary, get any auto-config's sensor names
    #
    if (! $opt_daemon && ! $collector->{readonly}) {
	my ($need_names, @names);

	# The indices of the sensor names from /config
	my %sn_idx = (T1 =>  8, H1 => 10, W1 => 12,
		      T2 => 16, H2 => 18, W2 => 20,
		      T3 => 24, H3 => 26, W3 => 28,
		      T4 => 32, H4 => 34, W4 => 36);

	$need_names = 0;
	for my $n (keys %{ $collector->{sensor} }) {
	    unless ($collector->{sensor}{$n}{name}) {
		$need_names = 1;
		last;
		}
	    }
	if ($need_names) {
	    $response = $ua->get("$collector->{_baseurl}/config");
	    if ($response->is_error) {
		cfg_warn "Can't fetch $collector->{_baseurl}/config";
		}
	    else {
		@names = split /\|/, $response->content;
		if (@names < 10) {
		    msg("err", "Unexpected response from EM1 $k");
		    }
		#
		# Output the values we just read, skipping missing sensors
		# Add the sensor names as found in /config, too
		#
		for my $n (keys %sn_idx) {
		    next unless exists $collector->{sensor}{$n};
		    $collector->{sensor}{$n}{name} ||= $names[$sn_idx{$n}];
		    }
		}
	    }
	}
    }

sub parse_commandline {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    $collector->{_datatype} = "generic";

    parse_collector(sub { $_[0] =~ /^\w+$/ },
	"Letters, digits, and underscores");
    }

sub parse_roomalert {
    my $type = shift;
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);
    my ($ua, $response);

    $collector->{_real_type} = $collector->{type};
    $collector->{type} = "roomalert";	# Change to generic type
    $collector->{_datatype} = "generic";
    $collector->{_baseurl} = "http://" . get_web_addr(1,0);

    if (! $collector->{readonly}) {
	$ua = $collector->{_ua} = new My::LWP::UserAgent($collector);
	$ua->timeout(5);
	$response = $ua->get("$collector->{_baseurl}/");
	if ($response->is_error) {
	    cfg_warn "Cannot contact TemPageR/RoomAlert at $collector->{_baseurl}",
		$response->status_line;
	    }
	$ua->timeout(30);
	}
    for ($type) {
	$_ == 4 && do {		# TemPageR
	    parse_collector(sub { $_[0] =~ /^T[1-4]$/ },
		"one of T1, T2, T3, or T4");
	    last;
	    };
	$_ == 7 && do {		# Room Alert 7e
	    parse_collector(sub { $_[0] =~ /^(T[1-4]|S[1-3])$/ },
		"one of T[1-4] or S[1-3]");
	    last;
	    };
	$_ == 11 && do {	# Room Alert 11e
	    parse_collector(sub { $_[0] =~ /^([TH][1-3]|S[1-8])$/ },
		"one of T[1-3], H[1-3], or S[1-8]");
	    last;
	    };
	$_ == 24 && do {	# Room Alert 24e
	    parse_collector(sub { $_[0] =~ /^([TH][0-6]|S([1-9]|1[0-6]))$/ },
		"one of T[0-6], H[1-6], or S[1-16]");
	    last;
	    };
	$_ == 26 && do {	# Room Alert 26w
	    parse_collector(sub { $_[0] =~ /^([TH][0-6]|S([1-9]|1[0-6])|[0-9A-F]{12}(T[0-2]|H[12]|S1))$/ },
		"one of T[0-6], H[0-6], S[1-16], or a wireless address + T[0-2], H[1-2] or S[0]");
	    last;
	    };
	die "Unknown type of AVTECH RoomAlert/TemPageR";
	}

    #
    # If necessary, get any auto-config's sensor names
    #
    if (! $opt_daemon && ! $collector->{readonly}) {
	my ($need_names, @names);

	$need_names = 0;
	for my $n (keys %{ $collector->{sensor} }) {
	    unless ($collector->{sensor}{$n}{name}) {
		$need_names = 1;
		last;
		}
	    }
	if ($need_names) {
	    my ($count, $out);
	    $response = $ua->get("$collector->{_baseurl}/getData.htm");
	    if ($response->is_error) {
		$response = $ua->get("$collector->{_baseurl}/getData.htm");
		if ($response->is_error) {
		    cfg_warn "Can't fetch $collector->{_baseurl}/getData.*";
		    }
		}
	    else {
		#
		# See fork_roomalert_poller for an explanation of this
		#
		$response = $response->content;
		if ($response =~ /^{name:/) {	# balance } in RE
		    $response =~ s/"([^"]*)"/'"' . encode_base64($1, "") . '"'/ge;
		    $response =~ s/:/ => /g;
		    $response =~ s/"([^"]*)"/'"' . decode_base64($1) . '"'/ge;
		    eval "\$out = $response";

		    $count = 0;
		    for my $th (@{ $out->{internal_sen} }) {
			$collector->{sensor}{T0}{name} ||= "$th->{label}"
			    if exists $collector->{sensor}{T0};
			$collector->{sensor}{H0}{name} ||= "$th->{label}"
			    if exists $collector->{sensor}{H0};
			$collector->{sensor}{Power}{name} ||= "$th->{label} Pwr"
			    if exists $collector->{sensor}{Power};
			$collector->{sensor}{Flood}{name} ||= "$th->{label} Fld"
			    if exists $collector->{sensor}{Flood};
			}
		    $count = 1;
		    for my $th (@{ $out->{sensor} }) {
			$collector->{sensor}{"T$count"}{name} ||= "$th->{label}"
			    if exists $collector->{sensor}{"T$count"};
			$collector->{sensor}{"H$count"}{name} ||= "$th->{label}"
			    if exists $collector->{sensor}{"H$count"};
			$count++;
			}
		    $count = 1;
		    for my $th (@{ $out->{switch_sen} }) {
			$collector->{sensor}{"S$count"}{name} ||= "$th->{label}"
			    if exists $collector->{sensor}{"S$count"};
			$count++;
			}
		    for my $th (@{ $out->{wireless_sen} }) {
			next if $th->{serial} eq "000000000000";
			$collector->{sensor}{"$th->{serial}T0"}{name} ||= "$th->{label}"
			    if exists $collector->{sensor}{"$th->{serial}T0"};
			$count = 1;
			for my $swit (@{ $th->{swit_sen} }) {
			    $collector->{sensor}{"$th->{serial}S$count"}{name} ||= "$swit->{label}"
			    	if exists $collector->{sensor}{"$th->{serial}S$count"};
			    $count++;
			    }
			$count = 1;
			for my $digi (@{ $th->{digi_sen} }) {
			    $collector->{sensor}{"$th->{serial}T$count"}{name} ||= "$digi->{label}"
			    	if exists $collector->{sensor}{"$th->{serial}T$count"};
			    $collector->{sensor}{"$th->{serial}H$count"}{name} ||= "$digi->{label}"
			    	if exists $collector->{sensor}{"$th->{serial}H$count"};
			    $count++;
			    }
			}
		    }
		else {
		    msg("err", "Unknown response from Room Alert $k");
		    }
		}
	    }
	}
    }

sub parse_dalsemi {
    my ($n, $nx, $sensor, $snsr) = @_;
    # Up-level addressed variables from read_config
    use vars qw($k $collector $clctr);
    # Up-level addressed variables from parse_collector
    use vars qw(@ds1820 @ds18b20 @ds2760 @humidity @analog
		@ds2406 %ds2423 %ds2438 %ds2450);
    my ($family, $do_probe);

    if ($collector->{type} =~ /^(owfs|owshell)$/) {
	$family = substr($nx, 0, 2);
	$do_probe = $opt_daemon && ! $collector->{readonly};
	}
    elsif ($collector->{type} eq "owhttpd") {
	$family = substr($nx, 0, 2);
	}
    elsif ($collector->{type} =~ /^(ha7net|temp08)$/) {
	$family = substr($nx, -2);
	}
    else {
	die "Unknown collector type in parse_dalsemi";
	}

    $sensor->{_family} = $family;
    $sensor->{_nx} = $nx;
    $sensor->{_n} = $n;
    for ($family) {
	/^10$/		&& do {
	    if ($sensor->{_sensor_actuator} eq "actuator") {
		cfg_die "The DS1820, DS1920 is a sensor, not an actuator";
		}
	    # Temperature: DS1820, DS18S20, DS1920
	    $sensor->{type} ||= "temperature";
	    cfg_die "Sensor $n\@$k DS18S20 ($sensor->{name}) Type must be Temperature"
		unless $sensor->{type} eq "temperature";
	    if ($collector->{type} eq "ha7net") {
		push @ds1820, $nx;
		}
	    elsif ($collector->{type} =~ /^(owfs|owhttpd)$/) {
		$sensor->{_owfs} = "temperature";
		cfg_warn "Cannot find sensor $n\@$k ($sensor->{name})"
		    if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		}
	    elsif ($collector->{type} eq "owshell") {
		$sensor->{_owfs} = "temperature";
		cfg_warn "Cannot find sensor $n\@$k ($sensor->{name})"
		    if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		}
	    last;
	    };
	/^12$/		&& do {
	    if ($sensor->{_sensor_actuator} eq "actuator") {
		cfg_die "The DS2406 is presently only supported as a sensor, not an actuator. Please email dan\@klein.com and we can fix this together...";
		}
	    # DS2406 (OnOff, Barometer in TAI8570,  or Humidity in HMP2001S)
	    cfg_die "Unknown DS2406 sensors for Sensor $n\@$k ($sensor->{name}).  Please mail dan\@klein.com and we can fix this together..."
		if $collector->{type} eq "temp08";
	    if ($sensor->{type} eq "onoff") {
		if ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
		    cfg_warn "I am not sure if DS2406 sensors (Sensor $n\@$k $sensor->{name}) work properly in owfs/owhttpd/owshell.  Please email dan\@klein.com and we can fix this together...";
		    }
		parse_onoff($snsr, $sensor, $k, $n);
		if (!defined ($sensor->{pio} = delete $snsr->{pio})) {
		    cfg_warn "Missing PIO A|B for sensor $sensor->{_nk}";
		    }
		elsif ($sensor->{pio} !~ /^[AB]$/) {
		    cfg_warn "Illegal PIO name on sensor $sensor->{_nk}";
		    }
		$sensor->{pio} = lc $sensor->{pio};
		if ($collector->{type} eq "ha7net") {
		    push @ds2406, $nx;
		    }
		}
	    elsif ($sensor->{type} eq "humidity") {
		cfg_die "The DS2406 is only supported as a humidity sensor on the HA7Net"
		    unless $collector->{type} eq "ha7net";
		if ($collector->{type} eq "ha7net") {
		    push @humidity, $nx;
		    }
		}
	    elsif ($sensor->{type} eq "barometer") {
		cfg_die "The DS2406 is only supported as a barometer on owfs/owhttpd/owshell"
		    unless $collector->{type} =~/^(owfs|owhttpd|owshell)$/;
		$sensor->{_owfs} = "TAI8570/pressure";
		#
		# This is a special case, where we don't use the default
		# values for _scale and _label
		#
		$sensor->{_scale} = "mBar";
		$sensor->{_label} = "mBar";
		}
	    elsif ($sensor->{type} eq "temperature") {
		cfg_die "The DS2406 is only supported as a temperature sensor on owfs/owhttpd/owshell"
		    unless $collector->{type} =~/^(owfs|owhttpd|owshell)$/;
		$sensor->{_owfs} = "TAI8570/temperature";
		}
	    else {
		cfg_die "DS2406 Sensor $sensor->{_nk} Type must be OnOff, Barometer, Temperature or Humidity";
		}

	    if ($collector->{type} eq "owfs") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		}
	    elsif ($collector->{type} eq "owshell") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		}
	    last;
	    };
	/^1D$/		&& do {
	    if ($sensor->{_sensor_actuator} eq "actuator") {
		cfg_die "The DS2423 is a sensor, not an actuator";
		}
	    # Rain, Lightning, Wind Speed, Gust, or Counter: DS2423
	    parse_ds2423($snsr, $sensor, $k, $n);
	    if ($collector->{type} eq "ha7net") {
		$ds2423{$nx}{ $sensor->{channel} }++;
		}
	    elsif ($collector->{type} eq "owfs") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		}
	    elsif ($collector->{type} eq "owshell") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		}
	    elsif ($collector->{type} eq "temp08") {
		cfg_warn "Channel B ignored for sensor $sensor->{_nk}"
		    if $sensor->{channel} eq "B";
		}
	    last;
	    };
	/^20$/		&& do {
	    if ($sensor->{_sensor_actuator} eq "actuator") {
		cfg_die "The DS2450 is a sensor, not an actuator";
		}
	    # Direction: DS2450
	    if (($sensor->{type} ||= "direction") ne "direction") {
		    cfg_die "Sensor $sensor->{_nk} Type must be Direction";
		}
	    $sensor->{inverted} = delete $snsr->{inverted};
	    if ($collector->{type} eq "temp08") {
		if ($sensor->{adjustby}) {
		    cfg_warn "Using AdjustBy for sensor $sensor->{_nk}, but do you know about the'NOR' command on TEMP08?";
		    }
		if ($sensor->{inverted}) {
		    cfg_warn "Using Inverted for sensor $sensor->{_nk}, but do you know about the 'WDR' command on TEMP08?";
		    }
		}
	    elsif ($collector->{type} eq "ha7net") {
		$ds2450{$nx}++;
		}
	    elsif ($collector->{type} eq "owfs") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		}
	    elsif ($collector->{type} eq "owshell") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		}
	    last;
	    };
	/^22$/		&& do {
	    $_ = "28";			# Fall through to /^28$/ DS10B20
	    };
	/^26$/		&& do {
	    if ($sensor->{_sensor_actuator} eq "actuator") {
		cfg_die "The DS2438, DS10B20 is a sensor, not an actuator";
		}
	    # Temperature, Barometric Pressure, Sunlight, or Humidity: DS2438
	    $sensor->{type} ||= "temperature";
	    if ($sensor->{type} eq "temperature") {
		# Do nothing much special
		if ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
		    $sensor->{_owfs} = "temperature";
		    }
		}
	    elsif ($sensor->{type} eq "humidity") {
		if ($collector->{type} =~ /^(owfs|owshell)$/) {
		    $sensor->{_owfs} = "HIH4000/humidity";
		    }
		elsif ($collector->{type} eq "owhttpd") {
		    $sensor->{_owfs} = "humidity";
		    }
		}
	    elsif ($sensor->{type} eq "barometer") {
		if ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
		    $sensor->{_owfs} = "VAD";
		    }
		defined ($sensor->{slope} = delete $snsr->{slope}) ||
		    cfg_die "Missing Slope in barometer $sensor->{_nk}";
		if ($sensor->{slope} !~ /$numeric/) {
		    cfg_die "Non-numeric Slope in $sensor->{_nk}";
		    }
		defined ($sensor->{intercept} = delete $snsr->{intercept}) ||
		    cfg_die "Missing Intercept in barometer $sensor->{_nk}";
		if ($sensor->{intercept} !~ /$numeric/) {
		    cfg_die "Non-numeric Intercept in $sensor->{_nk}";
		    }
		}
	    elsif ($sensor->{type} eq "sunlight") {
		if ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
		    $sensor->{_owfs} = "vis";
		    }
		#
		# Special case use for MultiplyBy, since this is raw data
		#
		$sensor->{multiplyby} = delete $snsr->{multiplyby};
		}
	    else {
		cfg_die "DS2438 Sensor $sensor->{_nk} Type must be one of Temperature, Barometer, Sunlight or Humidity";
		}

	    if ($collector->{type} eq "ha7net") {
		if ($sensor->{type} =~ /^(temperature|humidity)$/) {
		    push @humidity, $nx;
		    }
		else {
		    $ds2438{$nx}++;
		    }
		}
	    elsif ($collector->{type} eq "owfs") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		}
	    elsif ($collector->{type} eq "owshell") {
		cfg_warn "Cannot find sensor $sensor->{_nk}"
		    if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		}
	    last;
	    };
	/^28$/		&& do {
	    if ($sensor->{_sensor_actuator} eq "actuator") {
		cfg_die "The DS10B20, DS1822 is a sensor, not an actuator";
		}
	    # Temperature: DS10B20, DS1822
	    $sensor->{type} ||= "temperature";
	    if ($collector->{type} =~ /^(ha7net|owfs|owhttpd|owshell)$/) {
		$sensor->{resolution} = delete $snsr->{resolution} || 12;
		if ($sensor->{resolution} < 9 || $sensor->{resolution} > 12) {
		    cfg_warn "Sensor $sensor->{_nk} resolution must be between 9..12 (using 12)";
		    $sensor->{resolution} = 12;
		    }
		elsif ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/ &&
			$sensor->{resolution} != 9 &&
			$sensor->{resolution} != 12) {
		    cfg_warn "Sensor $sensor->{_nk} resolution may only be 9 or 12 (using 12)";
		    $sensor->{resolution} = 12;
		    }
		}
	    else {
		if (defined delete $snsr->{resolution}) {
		    cfg_warn "Sensor $sensor->{_nk} resolution ignored on ", ucfirst($collector->{type});
		    }
		}
	    cfg_die "DS18B20/DS1822 Sensor $sensor->{_nk} Type must be Temperature"
		unless $sensor->{type} eq "temperature";
	    if ($collector->{type} eq "ha7net") {
		push @ds18b20, "{$nx,$sensor->{resolution}}";
		}
	    elsif ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
		$sensor->{_owfs} = $sensor->{resolution} == 9 ?
		    "fasttemp" : "temperature";
		if ($collector->{type} eq "owfs") {
		    cfg_warn "Cannot find sensor $sensor->{_nk}"
			if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		    }
		elsif ($collector->{type} eq "owshell") {
		    cfg_warn "Cannot find sensor $sensor->{_nk}"
			if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		    }
		}
	    last;
	    };
	/^30$/		&& do {
	    # Thermocouple temperature: DS2760, also LED actuator
	    if ($sensor->{_sensor_actuator} eq "sensor") {
		$sensor->{type} ||= "temperature";
		if ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
		    $sensor->{_owfs} = "type\u$sensor->{thermocouple}/temperature";
		    }
		elsif ($collector->{type} eq "ha7net") {
		    # Nothing special here
		    }
		else {
		    cfg_die "DS2760 is presently only available on owfs/owhhtpd/owshell/ha7net";
		    }
		if (defined delete $snsr->{resolution}) {
		    cfg_warn "Sensor $sensor->{_nk} resolution ignored on ", ucfirst($collector->{type});
		    }
		cfg_die "DS2760 Sensor $sensor->{_nk} Type must be Temperature"
		    unless $sensor->{type} eq "temperature";
		if ($sensor->{thermocouple} = uc(delete $snsr->{thermocouple})) {
		    if ($sensor->{thermocouple} !~ /^[BEJKNRST]$/) {
			cfg_die "Thermocouple for Sensor $sensor->{_nk} must be one of [BEJKNRST]"
			}
		    }
		else {
		    cfg_die "Missing Thermocouple attribute for DS2760 Sensor $n\@$k ($sensor->{name})"
		    }
		if ($collector->{type} eq "owfs") {
		    cfg_warn "Cannot find sensor $sensor->{_nk}"
			if $do_probe && ! -d "$collector->{mountpoint}/$nx";
		    }
		elsif ($collector->{type} eq "owshell") {
		    cfg_warn "Cannot find sensor $sensor->{_nk}"
			if $do_probe && ! `owpresent -s $collector->{_baseurl} $nx`;
		    }
		$sensor->{_min_c} = $min_max{ $sensor->{thermocouple} }[0];
		$sensor->{_max_c} = $min_max{ $sensor->{thermocouple} }[1];
		push @ds2760, [$n, $sensor->{thermocouple}];
		}
	    elsif ($sensor->{_sensor_actuator} eq "actuator") {
		$sensor->{type} ||= "onoff";
		parse_onoff($snsr, $sensor, $k, $n);
		if ($collector->{type} eq "ha7net") {
		    # Nothing special
		    }
		else {
		    cfg_die "DS2760 (as an actuator) is presently only available on ha7net - write to dan\@klein.com to get it working on owfs/owhttpd";
		    }
		}
	    else {
		die "Unpossible _sensor_actuator!";
		}
	    last;
	    };
	cfg_die "Unknown sensor family $_ in Sensor $sensor->{_nk}";
	}
    }

sub parse_instances {
    # Up-level addressed variables from read_config
    use vars qw($k $collector $clctr);

    if ($clctr->{instance}) {
	if (ref $clctr->{instance} eq "HASH") {
	    for my $n (sort keys %{ $clctr->{instance} }) {
		my $instance = \%{$collector->{instance}{$n}};	# Autovivify
		my $inst = $clctr->{instance}{$n};
		cfg_die "Instance $n\@$k must specify an OID template"
		    unless defined($instance->{oid} = lc delete $inst->{oid});
		cfg_die "Instance $n\@$k OID is not correct format"
		    unless $instance->{oid} && $instance->{oid} =~ /^\.?\w+(\.\w+)*$/;
		$instance->{type} = delete $inst->{type};
		if (keys %{$inst}) {
		    cfg_warn "Unknown components in Instance $n\@$k ignored: ",
			join(", ", keys %{$inst});
		    }
		}
	    }
	else {
	    cfg_die "Instances must be enclosed by <Instance name> and </Instance>";
	    }
	delete $clctr->{instance};
	}
    }

sub expand_instances {
    my ($sensor, $n) = @_;
    my (%kv, $instance, $oid, @parts);
    # Up-level addressed variables from read_config
    use vars qw($k $collector);

    if ($sensor->{oid} && $sensor->{oid} =~ /^\.?\d+(\.\d+)*$/) {
	$sensor->{_oid} = $sensor->{oid};	# Already an absolute OID
	return;
	}
    for (split /,\s+/, $sensor->{oid}) {
	my ($k, $v) = split /\s*=\s*/;
	cfg_die "Sensor $sensor->{_nk} illegal OID format" unless defined $v;
	$k = lc $k;
	cfg_die "Duplicate component $k in Sensor $sensor->{_nk} OID"
	    if defined $kv{$k};
	$kv{$k} = $v;
	}
    cfg_die "Missing 'Instance' component in Sensor $sensor->{_nk} OID"
	unless defined $kv{instance};
    cfg_die "Instance $kv{instance} not found"
	unless $instance = $collector->{instance}{ $kv{instance} };
    $sensor->{type} ||= $instance->{type};	# Inherit sensor type
    $oid = $instance->{oid};
    @parts = $oid =~ /([a-z_]+)/g;
    for my $part (@parts) {
	cfg_die "'$part' not defined in use of Instance $kv{instance} for Sensor $n\@$k"
	    unless defined $kv{$part};
	$oid =~ s/\b$part\b/$kv{$part}/g;
	delete $kv{$part};
	}
    delete $kv{instance};
    if (keys %kv) {
	cfg_warn "Sensor $sensor->{_nk} has OID components not found in the template: ", join(", ", keys %kv);
	}
    $sensor->{_oid} = $sensor->{oid} = $oid;	# Expanded OID
    }

sub parse_derived {
    # Up-level addressed variables from read_config
    use vars qw(%cfg $k %used $email_regex $collector $clctr);

    $collector->{_datatype} = "generic";

    parse_collector(sub { $_[0] =~ /^[a-z]\w*$/i },
	"Letters, digits, and underscores starting with a letter");
    }

sub lock_scale {
    my $scale = shift;
    use vars qw($k $collector $clctr);

    cfg_warn "Scale directive ignored for $collector->{type} <Collector $k>"
	if defined delete $clctr->{scale};
    $collector->{scale} = $scale;		# Cannot be changed
    }

sub coerce_to_scale {
    my ($sensor, $alarm, $part) = @_;
    my $item_scale;

    if ($sensor->{type} eq "temperature") {
	if ($alarm) {
	    #
	    # If we measure in C or F, make sure the alarm values have units
	    #
	    unless ($alarm->{$part} =~ /^$number(\s*[CF])?$/) {
		cfg_die "Alarm $alarm->{_nka} \u$part must be numeric";
		}
	    if ($1) {
		$item_scale = chop($alarm->{$part});
		if ($item_scale eq 'F' && $sensor->{_scale} eq 'C') {
		    $alarm->{$part} = ($alarm->{$part}-32) * 5/9;
		    }
		elsif ($item_scale eq 'C' && $sensor->{_scale} eq 'F') {
		    $alarm->{$part} = ($alarm->{$part}*9/5) + 32;
		    }
		}
	    else {
		cfg_warn "Alarm $alarm->{_nka} \u$part $alarm->{$part} assumed to mean \u$part $alarm->{$part}$sensor->{_scale} (append a 'C' or an 'F'  to silence this warning)";
		}
	    }
	else {
	    #
	    # If we measure in C or F, make sure the values have units
	    #
	    unless ($sensor->{$part} =~ /^$number(\s*[CF])?$/) {
		cfg_die "Sensor $sensor->{_nk} \u$part must be numeric";
		}
	    if ($1) {
		$item_scale = chop($sensor->{$part});
		if ($item_scale eq 'F' && $sensor->{_scale} eq 'C') {
		    if ($part eq "adjustby") {
			# Adjustby is relative, not absolute temperature
			$sensor->{$part} = $sensor->{$part} * 5/9;
			}
		    else {
			$sensor->{$part} = ($sensor->{$part}-32) * 5/9;
			}
		    }
		elsif ($item_scale eq 'C' && $sensor->{_scale} eq 'F') {
		    if ($part eq "adjustby") {
			# Adjustby is relative, not absolute temperature
			$sensor->{$part} = $sensor->{$part} + 32;
			}
		    else {
			$sensor->{$part} = ($sensor->{$part}*9/5) + 32;
			}
		    }
		}
	    else {
		cfg_warn "Sensor $sensor->{_nk} \u$part $sensor->{$part} assumed to mean \u$part $sensor->{$part}$sensor->{_scale} (append a 'C' or an 'F'  to silence this warning)";
		}
	    }
	}
    else {
	#
	# Not a temperature sensor, so just worry of it is a number
	#
	if ($alarm) {
	    unless ($alarm->{$part} =~ /$numeric/) {
		cfg_die "Alarm $alarm->{_nka} \u$part must be numeric";
		}
	    }
	else {
	    unless ($sensor->{$part} =~ /$numeric/) {
		cfg_die "Sensor $sensor->{_nk} \u$part must be numeric";
		}
	    }
	}
    }

sub parse_collector {
    #
    # If the collector only supports sensors, we will be called with at
    # least 2 parameters, and an optional third.  If the collector also
    # supports actuators, we will be called with an additional 2 or 3 parms
    #
    my ($check_sensor_sub, $sensor_errstr, $sensor_conf,
	$check_actuator_sub, $actuator_errstr, $actuator_conf) = @_;
    # Up-level addressed variables from read_config
    use vars qw($k %used $email_regex $collector $clctr);
    my ($msg);
    # These will be up-level addressed in parse_dalsemi, but are re-initialized
    # for each collector we process;
    our (@ds1820, @ds18b20, @ds2760, @humidity, @analog, @ds2406,
	 %ds2423, %ds2438, %ds2450) = ();

    if ($collector->{type} =~ /^(ha7net|weathergoose|temp08|roomalert|smartnet|hwg|derived|newport)$/) {
	lock_scale('C');
	}
    elsif ($collector->{type} =~ /^(proliphix.*|wunderground)$/) {
	lock_scale('F');
	}
    elsif ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
	$collector->{scale} = delete $clctr->{scale} || 'C';
	}
    elsif ($collector->{type} =~ /^(qk145|vk011|em1)$/) {
	$collector->{scale} = delete $clctr->{scale} || 'F';
	}
    elsif ($collector->{type} eq "snmp") {
	$collector->{scale} = delete $clctr->{scale} || 'F';
	parse_instances();
	}
    elsif ($collector->{type} eq "maxbotix") {
	lock_scale(undef);		# No scale/inheritance in MaxBotix!
	}
    elsif ($collector->{type} eq "veris") {
	lock_scale(undef);		# No scale/inheritance in Veris!
	}
    elsif ($collector->{type} eq "enersure") {
	lock_scale(undef);		# No scale/inheritance in Enersure!
	}
    elsif ($collector->{type} eq "commandline") {
	lock_scale(undef);		# No scale/inheritance in CommandLine!
	}
    else {
	die "Unexpected collector type $collector->{type}";
	}
    if (exists $clctr->{sensor}) {
	unless ($check_sensor_sub) {
	    cfg_die "<Collector $k> does not support Sensors";
	    }
	if (ref $clctr->{sensor} eq "HASH") {
	    for my $n (sort keys %{ $clctr->{sensor} }) {
		unless ($check_sensor_sub->($n)) {
		    cfg_die "Sensor number '$n' must be $sensor_errstr for <Collector $k>";
		    }
		my $sensor = \%{ $collector->{sensor}{$n} };	# Autovivify
		my $snsr = $clctr->{sensor}{$n};
		#
		# Common sensor/actuator parsing
		#
		$sensor->{_collector} = $collector;
		parse_sensor_actuator("Sensor", $sensor, $snsr, $n, $sensor_conf);
		#
		# Process any out-of-spec alarms that were specified (alarms
		# can only exist in sensors, not actuators)
		#
		if ($snsr->{alarm}) {
		    if (ref $snsr->{alarm} eq "HASH") {
			for my $a (keys %{ $snsr->{alarm} }) {
			    my $alarm = \%{$sensor->{alarm}{$a}}; # Autovivify
			    my $alm = $snsr->{alarm}{$a};
			    $alarm->{_name} = $a;
			    $alarm->{_sensor} = $sensor;
			    $alarm->{_nka} = "$sensor->{_nk}:$a";
			    unless (ref $alm eq "HASH") {
				cfg_die "Alarm in $sensor->{_nk} must be named";
				}
			    if (defined($alarm->{above} = delete $alm->{above})) {
				coerce_to_scale($sensor, $alarm, "above");
				cfg_die "Alarm $alarm->{_nka} can't be both Above & Below"
				    if defined $alm->{below};
				if (defined($alarm->{resetat} =
					delete $alm->{resetat})) {
				    coerce_to_scale($sensor, $alarm, "resetat");
				    }
				else {
				    $alarm->{resetat} = $alarm->{above} - 2;
				    }
				if ($alarm->{resetat} >= $alarm->{above}) {
				    cfg_warn "ResetAt must be less than Above in Alarm $alarm->{_nka} - adjusting...";
				    $alarm->{resetat} = $alarm->{above} - 2;
				    }
				}
			    if (defined($alarm->{below} = delete $alm->{below})) {
				coerce_to_scale($sensor, $alarm, "below");
				cfg_die "Alarm $alarm->{_nka} can't be both Above & Below"
				    if defined $alm->{above};
				if (defined($alarm->{resetat} =
					delete $alm->{resetat})) {
				    coerce_to_scale($sensor, $alarm, "resetat");
				    }
				else {
				    $alarm->{resetat} = $alarm->{below} + 2;
				    }
				if ($alarm->{resetat} <= $alarm->{below}) {
				    cfg_warn "ResetAt must be greater than Below in Alarm $alarm->{_nka} - adjusting...";
				    $alarm->{resetat} = $alarm->{below} + 2;
				    }
				}
			    if ($^O eq "MSWin32") {
				if (defined delete $alm->{syslog}) {
				    cfg_warn "Syslog in Alarm $alarm->{_nka} is not available on Windows - ignoring";
				    }
				}
			    else {
				if (defined($alarm->{syslog} = delete $alm->{syslog})) {
				    my @pri = qw(info notice warning err crit alert emerg);
				    my %pri = map { $_, 1} @pri;
				    unless ($pri{$alarm->{syslog}}) {
					cfg_die "Syslog '$alarm->{syslog}' illegal, use one of: ", join(", ", @pri);
					}
				    }
				}
			    if ($alarm->{notify} = delete $alm->{notify}) {
				unless (ref $alarm->{notify}) {
				    $alarm->{notify} = [ $alarm->{notify} ];
				    }
				for my $e (@{$alarm->{notify}}) {
				    unless ($e =~ /^$email_regex(,$email_regex)*$/) {
					cfg_die "Notify '$e' in $alarm->{_nka} is not a valid email address";
					}
				    }
				}
			    if ($alarm->{subject} = delete $alm->{subject}) {
				cfg_warn "Subject in Alarm $alarm->{_nka} ignored without any Notify's"
				    unless $alarm->{notify};
				}
			    $alarm->{subject} ||= "Temperature Alert!";
			    if ($alarm->{message} = delete $alm->{message}) {
				cfg_warn "Message in Alarm $alarm->{_nka} ignored without any Notify's or SysLog"
				    unless $alarm->{notify} || $alarm->{syslog};
				}
			    $alarm->{message} ||= "%s is %.1f Degrees %s";
			    $alarm->{dates} = delete $alm->{dates};
			    $alarm->{times} = delete $alm->{times};
			    if ($msg = parse_alarm_constraints($alarm)) {
				cfg_die "Unknown Dates/Times '$msg' in Alarm $alarm->{_nka}";
				}
			    #
			    # (Lock)Open and (Lock)Close are checked for
			    # validity at the end of read_config(), but make
			    # the array refs in case they are only singletons.
			    #
			    if ($alarm->{open} = delete $alm->{open}) {
				check_sesame()	unless $opt_sesame;
				unless (ref $alarm->{open}) {
				    $alarm->{open} = [ $alarm->{open} ];
				    }
				}
			    if ($alarm->{lockopen} = delete $alm->{lockopen}) {
				check_sesame()	unless $opt_sesame;
				unless (ref $alarm->{lockopen}) {
				    $alarm->{lockopen} = [ $alarm->{lockopen} ];
				    }
				}
			    if ($alarm->{close} = delete $alm->{close}) {
				check_sesame()	unless $opt_sesame;
				unless (ref $alarm->{close}) {
				    $alarm->{close} = [ $alarm->{close} ];
				    }
				}
			    if ($alarm->{lockclose}=delete $alm->{lockclose}) {
				check_sesame()	unless $opt_sesame;
				unless (ref $alarm->{lockclose}) {
				    $alarm->{lockclose} = [$alarm->{lockclose}];
				    }
				}
			    #
			    # Exec is not checked for validity until it is
			    # activated, and only then by attempting to use it
			    #
			    if ($alarm->{exec} = delete $alm->{exec}) {
				check_sesame()	unless $opt_sesame;
				unless (ref $alarm->{exec}) {
				    $alarm->{exec} = [ $alarm->{exec} ];
				    }
				}

			    unless ($alarm->{notify} || $alarm->{syslog} ||
				    $alarm->{open} || $alarm->{close} ||
				    $alarm->{lockopen} || $alarm->{lockclose} ||
				    $alarm->{exec}) {
				cfg_die "Alarm $alarm->{_nka} must have at least one Notify, Syslog, (Lock)Open, (Lock)Close, or Exec";
				}
			    #
			    # Anything else is ignored
			    #
			    if (keys %{$alm}) {
				cfg_warn "Unknown components in Alarm $alarm->{_nka} ignored: ", join(", ", keys %{$alm});
				}
			    delete $snsr->{alarm}{$a};
			    }
			}
		    else {
			cfg_die "Alarm in $n\@$k ($sensor->{name}) must be a named block";
			}
		    delete $snsr->{alarm};
		    }
		#
		# Are there any components unaccounted for?
		#
		if (delete $snsr->{units}) {
		    our $unit_warning;
		    cfg_warn "Ignoring Units for Sensor $sensor->{_nk}",
			$unit_warning++ ? "" : "- Units are only used for sensors of type Counter or Math, all other sensors are read in their 'native' units (and I know what they are).  You might want to globally set the DisplayIn value, but that's not a great idea..."
		    }
		if (keys %{ $clctr->{sensor}{$n} }) {
		    cfg_warn "Unknown components in Sensor $sensor->{_nk} ignored: ", join(", ", keys %{$clctr->{sensor}{$n}});
		    }
		delete $clctr->{sensor}{$n};
		}
	    }
	else {
	    cfg_die "Sensors must be enclosed by <Sensor num> and </Sensor>";
	    }
	if (keys %{ $clctr->{sensor} } == 0) {
	    delete $clctr->{sensor};
	    }
	else {
	    cfg_warn "Unknown components in <Sensor $k> ignored: ",
		join(", ", keys %{ $clctr->{sensor} });
	    }
	}
    else {
	cfg_die "At least one <Sensor num> must be defined for <Collector $k>";
	}
    if (exists $clctr->{actuator}) {
	unless ($check_actuator_sub) {
	    cfg_die "<Collector $k> does not support Actuators";
	    }
	if (ref $clctr->{actuator} eq "HASH") {
	    for my $n (sort keys %{ $clctr->{actuator} }) {
		unless ($check_actuator_sub->($n)) {
		    cfg_die "Actuator number '$n' must be $actuator_errstr for <Collector $k>";
		    }
		my $actuator = \%{$collector->{actuator}{$n}};	# Autovivify
		my $actr = $clctr->{actuator}{$n};
		check_sesame()	unless $opt_sesame;
		#
		# Common sensor/actuator parsing
		#
		$actuator->{_collector} = $collector;
		parse_sensor_actuator("Actuator", $actuator, $actr, $n, $actuator_conf);
		cfg_die "Type for Actuator $actuator->{_nk} must be OnOff"
		    unless $actuator->{type} eq "onoff";
		if ($actr->{alarm}) {
		    cfg_die "Alarms are not allowed in Actuators ($actuator->{_nk})";
		    }
		#
		# Actuator specific stuff
		#
		$actuator->{normally} = lc(delete $actr->{normally});
		#
		# OnOff switches will probably be opened and closed, so
		# set up the routines to do the work
		#
		my $collector = $collector;		# Ickiness for closure
		if ($collector->{type} eq "snmp" ||
			($collector->{type} eq "hwg" && $collector->{type} eq "snmp")) {
		    $actuator->{_open} = sub {
			snmp_set_switch($collector, $actuator, 'off');
			};
		    $actuator->{_close} = sub {
			snmp_set_switch($collector, $actuator, 'on');
			};
		    }
		elsif ($collector->{type} eq "hwg" && $collector->{type} eq "http") {
		    $actuator->{_open} = sub {
			hwg_xml_set_switch($collector, $actuator, 'off');
			};
		    $actuator->{_close} = sub {
			hwg_xml_set_switch($collector, $actuator, 'on');
			};
		    }
		elsif ($collector->{type} eq "ha7net") {
		    $actuator->{_open} = sub {
			ha7net_set_switch($collector, $actuator, 'off');
			};
		    $actuator->{_close} = sub {
			ha7net_set_switch($collector, $actuator, 'on');
			};
		    }
		else {
		    die "Unpossible collector type in actuator-land";
		    }
		#
		# Queue the actions to put the switch in it's default state
		#
		if ($actuator->{normally} eq "open") {
		    if ($opt_daemon) {
			push @actions, $actuator->{_open};
			}
		    }
		elsif ($actuator->{normally} eq "closed") {
		    if ($opt_daemon) {
			push @actions, $actuator->{_close};
			}
		    }
		elsif ($actuator->{normally} eq "") {
		    cfg_die "OnOff switches must be Normally Open or Normally Closed.";
		    }
		else {
		    cfg_die "OnOff switches must be Normally Open or Normally Closed.  You said Normally \u$actuator->{normally}.";
		    }
		#
		# Are there any components unaccounted for?
		#
		if (keys %{ $clctr->{actuator}{$n} }) {
		    cfg_warn "Unknown components in Actuator $actuator->{_nk} ignored: ", join(", ", keys %{$clctr->{actuator}{$n}});
		    }
		delete $clctr->{actuator}{$n};
		#
		# Make a copy of the actuator in the sensor list, so we
		# can read and record the values when possible
		#
		if ($collector->{type} =~ /^(hwg|snmp)$/) {
		    $collector->{sensor}{$n} = $collector->{actuator}{$n};
		    }
		}
	    }
	else {
	    cfg_die "Actuators must be enclosed by <Actuator num> and </Actuator>";
	    }
	if (keys %{ $clctr->{actuator} } == 0) {
	    delete $clctr->{actuator};
	    }
	else {
	    cfg_warn "Unknown components in <Actuator $k> ignored: ",
		join(", ", keys %{ $clctr->{actuator} });
	    }
	}
    #
    # Handle the PollInterval for each collector
    #
    if ($collector->{type} =~ /^(temp08|qk145|vk011|maxbotix)$/) {
	cfg_warn "PollInterval ignored in non-polling <Collector $k>"
	    if delete $clctr->{pollinterval};
	$collector->{pollinterval} = 30;	# Fake, used for failure detect
	}
    else {
	if ($collector->{pollinterval} = delete $clctr->{pollinterval}) {
	    $collector->{pollinterval} = parse_reltime("<Collector $k>PollInterval", $collector->{pollinterval}, "1m");
	    if ($collector->{pollinterval} >= $config{loginterval}) {
		cfg_die "PollInterval in <Collector $k> must be less than LogInterval of $config{loginterval}s";
		}
	    elsif ($collector->{pollinterval} < 5) {
		cfg_die "PollInterval in <Collector $k> must be >= 5s";
		}
	    elsif ($collector->{pollinterval} > 5*60) {
		cfg_die "PollInterval in <Collector $k> must be <= 5m";
		}
	    }
	unless ($collector->{pollinterval}) {
	    $collector->{pollinterval} = $collector->{type} eq "smartnet" ?
		5 * 60 : int($config{loginterval} / 10);
	    $collector->{pollinterval} = $config{loginterval}
		if $collector->{pollinterval} > $config{loginterval};
	    $collector->{pollinterval} = 5 if $collector->{pollinterval} < 5;
	    }
	$config{_minpoll} = min($collector->{pollinterval}, $config{_minpoll});
	}
    #
    # Postprocess HA7Net, Temp08, and OWFS to ensure that the DS2438 sensors
    # have different types listed if two values are read.  OnOff and Counter
    # sensors are the exception, since we can have two of them per unit.
    #
    if ($collector->{type} =~ /^(ha7net|temp08|owfs|owhttpd|owshell)$/) {
	my (%type, $nx, $sensor);
	for my $n (sort keys %{ $collector->{sensor} }) {
	    $sensor = $collector->{sensor}{$n};
	    next if $sensor->{type} =~ /^(onoff|counter)$/;
	    ($nx = $n) =~ s/\.\w$//;
	    if ($type{$nx . $sensor->{type} . $sensor->{channel}}++) {
		cfg_warn "Multiple subsensors of $nx\@$k ($sensor->{name}) read $sensor->{type}.  Did you mean to specify types Temperature+Humidity or types Temperature+Barometer?";
		}
	    }
	}
    #
    # For the HA7Net, if a DS2438 is used only for temperature, move it to
    # @ds1820 for a faster style of data collection (ReadTemperature).  Then
    # build the URLs for all of the sensors we found (except for DS2438,
    # which is done later in a more complicated form)
    #
    if ($collector->{type} eq "ha7net") {
	my ($save, $nx, $sensor, $ret);
	DS2438: for my $th (keys %ds2438) {
	    for my $n (sort keys %{ $collector->{sensor} }) {
		$sensor = $collector->{sensor}{$n};
		($nx = $n) =~ s/\.\w$//;
		$save = $n  if $sensor->{type} eq "temperature" && $nx eq $th;
		next DS2438 if $sensor->{type} ne "temperature" && $nx eq $th;
		}
	    delete $ds2438{$th};
	    push @ds1820, $save;
	    }
	#
	# The "right" way to do this is to make one request per type of sensor,
	# with potentially more than one sensor per request.  As of version
	# 1.0.0.13 of the HA7Net firmware, this works correctly even when a
	# sensor has failed or been removed.
	#
	# However, unless you have version > 1.0.0.14, it fails when more than
	# 10 Sensors are used :-(  Rather than doing another version check, we
	# just build URLs with no more than 10 sensors per request.
	#
	$collector->{_urls} = [];	# Manually vivify
	while (@ds1820) {
	    push @{$collector->{_urls}},
		"$collector->{_baseurl}/1Wire/ReadTemperature.html?Address_Array=" . join(",", splice(@ds1820, 0, 10));
	    }
	while (@ds18b20) {
	    push @{$collector->{_urls}},
		"$collector->{_baseurl}/1Wire/ReadDS18B20.html?DS18B20Request=" . join(",", splice(@ds18b20, 0, 10));
	    }
	#
	# Humidity sensors get an entry for the humidity sensor, and another
	# for the temperature sensor.  We only need one entry, because one
	# read will give us both values.  So eliminate duplicates
	#
	@humidity = keys %{ { map { ($_, 1) } @humidity } };
	while (@humidity) {
	    push @{$collector->{_urls}},
		"$collector->{_baseurl}/1Wire/ReadHumidity.html?Address_Array=" . join(",", splice(@humidity, 0, 10));
	    }
	while (@analog) {
	    push @{$collector->{_urls}},
		"$collector->{_baseurl}/1Wire/ReadAnalogProbe.html?Address_Array=" . join(",", splice(@analog, 0, 10));
	    }
	#
	# Multi-port DS2406's will likewise be multiply listed - we only need
	# one address to get multiple readings
	#
	$collector->{_2406} = [ keys %{ { map { ($_, 1) } @ds2406 } } ];
	#
	# Everything else is "simple".  Yeah, right.
	#
	$collector->{_2423} = { %ds2423 };
	$collector->{_2438} = [ keys %ds2438 ];
	$collector->{_2450} = [ keys %ds2450 ];
	$collector->{_2760} = [ @ds2760 ];
	}
    #
    # For the SmartNet, turn the consolidated hash entries into the list
    # we'll use when polling
    #
    elsif ($collector->{type} eq "smartnet") {
	my ($romid, $parentid);
	while (($romid, $parentid) = each %{$collector->{_extra}{smartwatt}}) {
	    push @{ $collector->{_smartwatt} }, {
		ROMID => $romid, 
		$parentid ?  (ParentID => $parentid) : (),
		};
	    }
	while (($romid, $parentid) = each %{$collector->{_extra}{smartsense}}) {
	    push @{ $collector->{_smartsense} }, {
		ROMID => $romid, 
		$parentid ?  (ParentID => $parentid) : (),
		};
	    }
	}
    #
    # For the enersure, keep track of the banks of sensors being read
    #
    elsif ($collector->{type} eq "enersure") {
	$collector->{_bank} = [ sort {$a <=> $b }
	    keys %{$collector->{_extra}{bank}} ];
	}
    }

sub parse_sensor_actuator ($$$$$) {
    my ($str, $sensor, $snsr, $n, $sensor_conf) = @_;
    # Up-level addressed variables from read_config
    use vars qw($k %used $email_regex $collector $clctr);
    my ($color, $bank, %bank);
    $sensor->{_sensor_actuator} = lc($str);
    $sensor->{_name} = $n;
    $sensor->{_nk} = "$n\@$sensor->{_collector}{_name}";
    $sensor->{_last5} = [];				# Manually vivify
    $sensor->{_lastdata} = time;
    $sensor->{name} = delete $snsr->{name};
    $sensor->{type} = lc delete $snsr->{type};

    #
    # Now parse out the logfile, and assign the file descriptor,
    # parse the name, graphcolor, adjustby, etc.  Any user-named
    # logfile name is left "as is", but sensor-derived logfile
    # names are converted to UPPER CASE.  A sensor may also be
    # marked as readonly (it exists, but no data is collected
    # for it).
    #
    # Sensor ReadOnly inherits from Collector Readonly (a sensor can be
    # readonly on a read/write collector, but not vise versa)
    #
    $sensor->{readonly} = defined(delete $snsr->{readonly}) || $collector->{readonly};
    $sensor->{logfile} = delete $snsr->{logfile} || "$k/\U$n";
    $sensor->{logfile} =~ s#/+#/#;		# No double //'s
    if ($logfile{$sensor->{logfile}}++) {
	cfg_die "Logfile '$sensor->{logfile}' cannot be used for multiple sensors";
	}
    if ($config{logformat} eq "text") {
	$sensor->{_fd} = log_open($sensor,
	    ($opt_daemon && ! $sensor->{readonly}) || 0);
	}
    elsif ($config{logformat} eq "sql") {
	my $name = $sensor->{logfile};
	$sth = $dbh->prepare(qq{
	    SELECT log_id FROM logfiles WHERE name = '$name'
	    });
	$sth->execute();
	my $h = $sth->fetchrow_hashref();
	if ($h->{log_id}) {
	    $sensor->{_id} = $h->{log_id};
	    }
	else {
	    $sth = $dbh->prepare(qq{
		INSERT INTO logfiles (name) VALUES ('$name')
		});
	    $sth->execute();
	    $sth = $dbh->prepare(qq{
		SELECT log_id FROM logfiles WHERE name = '$name'}
		);
	    $sth->execute();
	    $h = $sth->fetchrow_hashref();
	    $sensor->{_id} = $h->{log_id};
	    }
	}
    else {
	die "Unknown LogFormat";
	}

    if ($sensor->{popup} = delete $snsr->{popup}) {
	$config{_has_popups} = 1;
	}

    if ($color = delete $snsr->{graphcolor}) {
	$used{$color = lc $color}++;
	if ($color_map{$color}) {
	    $sensor->{graphcolor} = $color_map{$color};
	    }
	elsif ($color =~ /^#[0-9a-f]{6}$/) {
	    $sensor->{graphcolor} = $color;
	    }
	else {
	    cfg_warn "Unknown GraphColor '$color' - use one of ",
		join(", ", sort keys %color_map);
	    $sensor->{graphcolor} = $color_map{black};
	    }
	}
    else {
	do {
	    $color = shift @color_wheel;
	    push @color_wheel, $color;
	    } until (keys %used >= @color_wheel || !$used{$color}++);
	$sensor->{graphcolor} = $color_map{$color};
	}

    $sensor->{linetype} = lc(delete $snsr->{linetype} || "solid");
    if ($linetype_map{$sensor->{linetype}}) {
	$sensor->{linetype} = $linetype_map{$sensor->{linetype}};
	}
    else {
	cfg_warn "Unknown LineType '$sensor->{linetype}' - use one of ",
	    join(", ", sort keys %linetype_map);
	$sensor->{linetype} = $linetype_map{black};
	}

    if (delete $snsr->{combo}) {
	cfg_warn "The 'Combo' keyword is deprecated and is now ignored";
	}

    #
    # Do post-processing on the sensor type that was specified.
    # We do not specify the _label for temperature devices, because
    # that is set when we graph the data
    #
    if ($collector->{type} =~ /^(qk145|vk011)$/) {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the QK145/VK011/MaxBotix";
	    }
	$sensor->{type} ||= "temperature";
	cfg_die "$str $sensor->{_nk} Type must be Temperature"
	    unless $sensor->{type} eq "temperature";
	}
    elsif ($collector->{type} =~ /^maxbotix$/) {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the QK145/VK011/MaxBotix";
	    }
	$sensor->{type} ||= "range";
	cfg_die "$str $sensor->{_nk} Type must be Range"
	    unless $sensor->{type} eq "range";
	}
    elsif ($collector->{type} eq "ha7net") {
	my $nx;
	($nx = $n) =~ s/\.\w$//;	# Remove possible subindex
	parse_dalsemi($n, $nx, $sensor, $snsr);
	}
    elsif ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported yet on owfs/owhttp - write to dan\@klein.com if you want to help implement them";
	    }
	my $nx;
	# NOTE: even though the F.I.C and FI.C notations use dots
	# (http://owfs.sourceforge.net/naming.html), a subindex
	# is still different enough not to clash with this (as it
	# is a single additional character, and the .C component
	# has two characters after the dot)
	($nx = $n) =~ s/\.\w$//;	# Remove possible subindex
	parse_dalsemi($n, $nx, $sensor, $snsr);
	}
    elsif ($collector->{type} eq "temp08") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported yet on the temp08 - write to dan\@klein.com if you want to help implement them";
	    }
	my $nx;
	($nx = $n) =~ s/\.\w$//;	# Remove possible subindex
	parse_dalsemi($n, $nx, $sensor, $snsr);
	}
    elsif ($collector->{type} eq "smartnet") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the smartnet";
	    }
	my $nx;
	($nx = $n) =~ s/\.\w$//;	# Remove possible subindex
	#
	# This is sufficiently divergent from the usual that
	# I choose not to use parse_dalsemi here...
	#
	if (($sensor->{parentid} = delete $snsr->{parentid}) &&
		$sensor->{parentid} !~ /^[\dA-F]{14}1F$/ ) {
	    cfg_warn "ParentID for sensor $sensor->{_nk} does not look like a SmartPDU ROMID";
	    }
	for ($nx) {
	    /1D(-\d)?$/	&& do {	# This is a SmartWatt
		if ($sensor->{type} eq "watts") {
		    # Nothing special
		    }
		elsif ($sensor->{type} eq "wh") {
		    # Nothing special
		    }
		else {
		    cfg_die "$str $sensor->{_nk} Type must be one of Watts or Wh";
		    }
		$collector->{_extra}{smartwatt}{$nx} = $sensor->{parentid};
		last;
		};
	    /26$/		&& do {	# This is SmartSenseTH
		if ($sensor->{type} eq "temperature") {
		    # Do nothing special
		    }
		elsif ($sensor->{type} eq "humidity") {
		    # Nothing special
		    }
		elsif ($sensor->{type} eq "dewpoint") {
		    # Nothing special
		    }
		else {
		    cfg_die "$str $sensor->{_nk} Type must be one of Temperature, Humidity, or DewPoint";
		    }
		#
		# Consolidate all sensors (up to 3) into a single
		# entry, so that we read each sensor once
		#
		$collector->{_extra}{smartsense}{$nx} = $sensor->{parentid};
		last;
		};
	    /1F$/		&& do {	# This is a SmartPDU
		cfg_die "SmartPDU $sensor->{_nk} has no useful values to read";
		last;
		};
	    cfg_die "Unknown 1-wire family $_ in $str $sensor->{_nk}";
	    }
	}
    elsif ($collector->{type} eq "weathergoose") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the Weathergoose family";
	    }

	my $nx;
	($nx = $n) =~ s/\.\w$//;	# Remove possible subindex
	#
	# This is sufficiently divergent from the usual that I cannot use
	# parse_dalsemi here...  The problem is that Geist Engineering uses
	# RANDOM numbers for their main devices :-(
	#
	if ($sensor->{type} eq "temperature") {
	    # Do nothing special
	    }
	elsif ($sensor->{type} eq "humidity") {
	    # Nothing special
	    }
	elsif ($sensor->{type} eq "airflow") {
	    # Nothing special
	    }
	elsif ($sensor->{type} eq "sound") {
	    # Nothing special
	    }
	elsif ($sensor->{type} eq "light") {
	    # Nothing special
	    }
	elsif ($sensor->{type} =~ /^io[123]$/) {
	    # Nothing special
	    }
	elsif ($sensor->{type} =~ /^volts/) {	# Volts, Volts-{Min,Max,Peak}
	    # Nothing special
	    }
	elsif ($sensor->{type} =~ /^amps/) {	# Amps and Amps-Peak
	    # Nothing special
	    }
	elsif ($sensor->{type} eq "kwh") {
	    # Nothing special
	    }
	elsif ($sensor->{type} =~ /power$/) {	# Real-Power and Apparent Power
	    # Nothing special
	    }
	elsif ($sensor->{type}  eq "power-factor") {
	    # Nothing special
	    }
	else {
	    cfg_die "$str $sensor->{_nk} Type must be one of Temperature, Humidity, AirFlow, Light, Sound, IO1, IO2, IO3, Volts, Volts-Min, Volts-Max, Volts-Peak, Amps, Amps-Peak, KWh, Real-Power, Apparent-Power, or Power-Factor";
	    }
	}
    elsif ($collector->{type} eq "snmp") {
	cfg_die "$str $sensor->{_nk} must have an OID"
	    unless defined ($sensor->{oid} = delete $snsr->{oid});
	expand_instances($sensor, $n);
	#
	# Determine if type is known (may be inherited from expand_instances)
	#
	cfg_die "Missing Type for $str $sensor->{_nk}"
	    unless $sensor->{type};
	#
	# After possibly expanding instances, now check for
	# numeric, and absolute versus relative
	#
	cfg_die "$str $sensor->{_nk} OID must resolve to only numbers and '.'s"
	    unless $sensor->{_oid} =~ /^\.?\d+(\.\d+)*$/;
	$sensor->{_oid} = "$collector->{baseoid}.$sensor->{_oid}"
	    unless $sensor->{_oid} =~ /^\./;
	# TODO also check for 2 OIDs with same numbers on collector
	}
    elsif ($collector->{type} eq "newport") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported yet on the Newport/Omega family - write to dan\@klein.com if you want to help implement them";
	    }
	my $required = $newport{$collector->{_subtype}}{type}{$n};
	unless ($required) {
	    warn "Internal consistency error - please report to dan\@klein.com - found a missing Newport/Omega  sensor->{type} == $sensor->{type}"
	    }
	if (($sensor->{type} ||= $required) ne $required) {
	    cfg_die "$str $sensor->{_nk} Type must be \u$required";
	    }
	}
    elsif ($collector->{type} eq "veris") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the Veris family";
	    }
	$sensor->{type} ||= lc($veris{$n}{type});
	cfg_die "$str $sensor->{_nk} Type must be $veris{$n}{type}"
	    unless $sensor->{type} eq lc($veris{$n}{type});
	$sensor->{_scale} ||= $veris{$n}{_scale};
	$sensor->{_label} ||= $veris{$n}{_label};
	}
    elsif ($collector->{type} eq "enersure") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the Enersure";
	    }
	if ($n =~ /^[Vv]/) {
	    if (($sensor->{type} ||= "volts") ne "volts") {
		cfg_die "$str $sensor->{_nk} Type must be Volts";
		}
	    }
	elsif ($n =~ /^[Ii]/) {
	    if (($sensor->{type} ||= "amps") ne "amps") {
		cfg_die "$str $sensor->{_nk} Type must be Amps";
		}
	    }
	elsif ($n =~ /^[Pp]/) {
	    if (($sensor->{type} ||= "power-factor") ne "power-factor") {
		cfg_die "$str $sensor->{_nk} Type must be Wetness";
		}
	    }
	elsif ($n =~ /^[Kk]/) {
	    if (($sensor->{type} ||= "wh") ne "wh") {
		cfg_die "$str $sensor->{_nk} Type must be Wh";
		}
	    }
	elsif ($n =~ /^[Ww]/) {
	    if (($sensor->{type} ||= "watts") ne "watts") {
		cfg_die "$str $sensor->{_nk} Type must be Watts";
		}
	    }
	#
	# To optimize communication, keep track of which set
	# of sensors are used.  Any sensor in the set of [VIPKW]n
	# that is used causes all sensors in that set to be read.
	#
	($bank = $n) =~ s/^\D//;
	$collector->{_extra}{bank}{$bank} = 1;
	}
    elsif ($collector->{type} eq "proliphix") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported yet on the Proliphix family - write to dan\@klein.com if you want to help implement them";
	    }

	if ($n =~ /^[Tt]/) {
	    if (($sensor->{type} ||= "temperature") ne "temperature") {
		cfg_die "$str $sensor->{_nk} Type must be Temperature";
		}
	    }
	elsif ($n =~ /^[Hh]/) {
	    if (($sensor->{type} ||= "humidity") ne "humidity") {
		cfg_die "$str $sensor->{_nk} Type must be Humidity";
		}
	    }
	elsif ($n =~ /^[Ss]$/) {
	    if (($sensor->{type} ||= "state") ne "state") {
		cfg_die "$str $sensor->{_nk} Type must be State";
		}
	    }
	}
    elsif ($collector->{type} eq "em1") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the EM1";
	    }

	if ($n =~ /^[Tt]/) {
	    if (($sensor->{type} ||= "temperature") ne "temperature") {
		cfg_die "$str $sensor->{_nk} Type must be Temperature";
		}
	    }
	elsif ($n =~ /^[Hh]/) {
	    if (($sensor->{type} ||= "humidity") ne "humidity") {
		cfg_die "$str $sensor->{_nk} Type must be Humidity";
		}
	    }
	elsif ($n =~ /^[Ww]/) {
	    if (($sensor->{type} ||= "wetness") ne "wetness") {
		cfg_die "$str $sensor->{_nk} Type must be Wetness";
		}
	    }
	}
    elsif ($collector->{type} eq "wunderground") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on Wunderground";
	    }

	if ($n =~ /^[Tt]$/) {
	    if (($sensor->{type} ||= "temperature") ne "temperature") {
		cfg_die "$str $sensor->{_nk} Type must be Temperature";
		}
	    }
	elsif ($n =~ /^[Hh]$/) {
	    if (($sensor->{type} ||= "humidity") ne "humidity") {
		cfg_die "$str $sensor->{_nk} Type must be Humidity";
		}
	    }
	elsif ($n =~ /^[Pp]$/) {
	    if (($sensor->{type} ||= "dewpoint") ne "dewpoint") {
		cfg_die "$str $sensor->{_nk} Type must be DewPoint";
		}
	    }
	elsif ($n =~ /^[Dd]$/) {
	    if (($sensor->{type} ||= "direction") ne "direction") {
		cfg_die "$str $sensor->{_nk} Type must be Direction";
		}
	    }
	elsif ($n =~ /^[Ss]$/) {
	    if (($sensor->{type} ||= "speed") ne "speed") {
		cfg_die "$str $sensor->{_nk} Type must be Speed";
		}
	    }
	elsif ($n =~ /^[Gg]$/) {
	    if (($sensor->{type} ||= "gust") ne "gust") {
		cfg_die "$str $sensor->{_nk} Type must be Gust";
		}
	    }
	elsif ($n =~ /^[Bb]$/) {
	    if (($sensor->{type} ||= "barometer") ne "barometer") {
		cfg_die "$str $sensor->{_nk} Type must be Barometer";
		}
	    }
	else {
	    die "Unknown Type $sensor->{type} <Collector $k><$str $n>";
	    }
	}
    elsif ($collector->{type} eq "commandline") {
	unless ($sensor->{type}) {
	    cfg_die "Missing Type in <Collector $k><Sensor $n>";
	    }
	if ($sensor->{type} eq "onoff") {
	    parse_onoff($snsr, $sensor, $k, $n);
	    }
	unless (defined($sensor->{command} = delete $snsr->{command})) {
	    cfg_die "Missing Command in Collector $k><Sensor $n>";
	    }
	}
    elsif ($collector->{type} eq "roomalert") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on the Roomalert family";
	    }
	if ($n =~ /^([0-9a-f]{12})?T/i) {
	    if (($sensor->{type} ||= "temperature") ne "temperature") {
		cfg_die "$str $sensor->{_nk} Type must be Temperature";
		}
	    }
	elsif ($n =~ /^([0-9a-f]{12})?H/i) {
	    if (($sensor->{type} ||= "humidity") ne "humidity") {
		cfg_die "$str $sensor->{_nk} Type must be Humidity";
		}
	    }
	elsif ($n =~ /^(([0-9a-f]{12})?S|Flood|Power)/i) {
	    if (($sensor->{type} ||= "onoff") ne "onoff") {
		cfg_die "$str $sensor->{_nk} Type must be OnOff";
		}
	    parse_onoff($snsr, $sensor, $k, $n);
	    }
	}
    elsif ($collector->{type} eq "hwg") {
	if ($collector->{subtype} eq "snmp") {
	    unless (exists $sensor_conf->{$n}) {
		cfg_die "<$str $n> was not found during scan of <Collector $k>"
		    if $opt_daemon && !$collector->{readonly};
		}
	    # All sensor types must match the built-in type
	    $sensor->{type} ||= $sensor_conf->{$n}{type};
	    unless ($sensor_conf->{$n}{type}) {	# That is, autoconfigure failed
		if ($n =~ /^B/ || $n =~ /^\d$/) {
		    $sensor->{type} ||= "onoff";
		    }
		else {
		    $sensor->{type} ||= "temperature";
		    }
		cfg_warn "Assuming $str $sensor->{_nk} Type is $sensor->{type}";
		$sensor_conf->{$n}{type} = $sensor->{type};
		}

	    cfg_die "$str $sensor->{_nk} Type must be $sensor->{_type}"
		unless $sensor->{type} eq $sensor_conf->{$n}{type};
	    }
	elsif ($collector->{subtype} eq "http") {
	    if ($n =~ /^B/ || $n =~ /^\d$/) {
		cfg_warn "Assuming $str $sensor->{_nk} Type is OnOff" unless $sensor->{type};
		$sensor->{type} ||= "onoff";
		}
	    else {
		cfg_warn "Assuming $str $sensor->{_nk} Type is Temperature" unless $sensor->{type};
		$sensor->{type} ||= "temperature";
		}
	    }
	else {
	    die "Unpossible subtype $collector->{subtype}";
	    }
	if ($sensor->{type} eq "humidity") {
	    # nothing special
	    }
	elsif ($sensor->{type} eq "milliamps") {
	    # nothing special
	    }
	elsif ($sensor->{type} eq "onoff") {
	    parse_onoff($snsr, $sensor, $k, $n);
	    }
	elsif ($sensor->{type} eq "temperature") {
	    # nothing special
	    }
	elsif ($sensor->{type} eq "volts") {
	    # nothing special
	    }
	else {
	    die "Unpossible Type $sensor->{type} <Collector $k><$str $n>";
	    }
	}
    elsif ($collector->{type} eq "derived") {
	if ($sensor->{_sensor_actuator} eq "actuator") {
	    cfg_die "Actuators are not supported on Derived collectors";
	    }
	unless ($sensor->{type}) {
	    cfg_die "Missing Type in <Collector $k><Sensor $n>";
	    }
	if ($sensor->{type} eq "math") {
	    parse_math_sensor($snsr, $sensor);
	    }
	elsif ($sensor->{type} eq "windchill") {
	    parse_builtin_computation($snsr, $sensor,
		qw(windchill temperature speed));
	    }
	elsif ($sensor->{type} eq "dewpoint") {
	    parse_builtin_computation($snsr, $sensor,
		qw(dewpoint temperature humidity));
	    }
	elsif ($sensor->{type} eq "heatindex") {
	    parse_builtin_computation($snsr, $sensor,
		qw(heatindex temperature humidity));
	    }
	elsif ($sensor->{type} eq "humidex") {
	    parse_builtin_computation($snsr, $sensor,
		qw(humidex temperature humidity));
	    }
	else {
	    cfg_die "Illegal Derived sensor type $sensor->{type} - use one of Math, WindChill, DewPoint, HeatIndex, or Humidex";
	    }
	}
    else {
	die "Unknown collector type $collector->{type}";
	}
    #
    # Regardless of the collector type, handle special sensor types and their
    # associated parameters.  Label each sensor and assign a scale where needed.
    # PLEASE TRY TO KEEP THIS LIST ALPHABETICAL
    #
    if ($sensor->{type} =~ /^amps/) {	# Amps and Amps-Peak
	$sensor->{_scale} ||= "A";
	$sensor->{_label} ||= "Amps";
	}
    elsif ($sensor->{type} eq "airflow") {
	$sensor->{_scale} ||= "Airflow";
	$sensor->{_label} ||= "Relative AirFlow";
	}
    elsif ($sensor->{type} eq "barometer") {
	$sensor->{_scale} ||= "inHg";
	$sensor->{_label} ||= "inches Hg";
	}
    elsif ($sensor->{type} eq "counter") {
	$sensor->{units} = delete $snsr->{units};
	$sensor->{_scale} ||= $sensor->{units} || "Count";
	$sensor->{_scale} =~ s/\t/ /g;
	$sensor->{_label} ||= $sensor->{units} || "Count";

	if ($sensor->{subtype} = lc(delete $snsr->{subtype})) {
	    if ($sensor->{subtype} =~ /^(avgrate|maxrate|minrate|count|raw)$/) {
		cfg_warn "Ignoring illegal InactivityReset for sensor $sensor->{_nk}"
		    if delete $snsr->{inactivityreset};
		}
	    elsif ($sensor->{subtype} eq "total") {
		$sensor->{inactivityreset} = parse_reltime("<Sensor $n\@$k>InactivityReset", delete $snsr->{inactivityreset}, "12h");
		get_last_logged_counter_value($sensor);
		}
	    else {
		cfg_die "Unknown SubType '$sensor->{subtype}' for Counter $sensor->{_nk}",
		    "(choose from AvgRate, MaxRate, MinRate, Count, or Raw)";
		}
	    }
	else {
	    cfg_die "Missing SubType for Counter $sensor->{_nk}";
	    }
	$sensor->{multiplyby} = delete $snsr->{multiplyby} || 1;
	if ($sensor->{multiplyby} !~ /$numeric/) {
	    $sensor->{multiplyby} = 1;
	    cfg_warn "Ignoring illegal MultiplyBy for sensor $sensor->{_nk}";
	    }
	}
    elsif ($sensor->{type} eq "dewpoint") {
	# Scale is inherited from temperature, inherited from the collector
	$sensor->{_scale} ||= $collector->{scale};
	$sensor->{_label} ||= "DewPoint";
	}
    elsif ($sensor->{type} eq "direction") {
	$sensor->{_scale} ||= "Deg";
	$sensor->{_label} ||= "Direction";
	}
    elsif ($sensor->{type} eq "gauge") {
	$sensor->{multiplyby} = delete $snsr->{multiplyby} || 1;
	if ($sensor->{multiplyby} !~ /$numeric/) {
	    $sensor->{multiplyby} = 1;
	    cfg_warn "Ignoring illegal MultiplyBy for sensor $sensor->{_nk}\n";
	    }
	$sensor->{units} = delete $snsr->{units} || "Gauge";
	$sensor->{_scale} ||= $sensor->{units};
	$sensor->{_scale} =~ s/\t/ /g;
	$sensor->{_label} ||= $sensor->{units};
	}
    elsif ($sensor->{type} eq "heatindex") {
	# Scale is inherited from temperature, inherited from the collector
	$sensor->{_scale} ||= $collector->{scale};
	$sensor->{_label} ||= "Heat Index";
	}
    elsif ($sensor->{type} eq "humidex") {
	# Scale is inherited from temperature, inherited from the collector
	$sensor->{_scale} ||= $collector->{scale};
	$sensor->{_label} ||= "Humidex";
	}
    elsif ($sensor->{type} eq "humidity") {
	$sensor->{_scale} ||= "%";
	$sensor->{_label} ||= "% Relative Humidity";
	}
    elsif ($sensor->{type} =~ /^io[123]$/) {
	$sensor->{_scale} ||= "IO";
	$sensor->{_label} ||= "IO Level";
	}
    elsif ($sensor->{type} eq "kwh") {
	$sensor->{_scale} ||= "KWh";
	$sensor->{_label} ||= "KWh";
	}
    elsif ($sensor->{type} eq "light") {
	$sensor->{_scale} ||= "Light";
	$sensor->{_label} ||= "Light Level";
	}
    elsif ($sensor->{type} eq "lightning") {
	$sensor->{_scale} ||= "Count";
	$sensor->{_label} ||= "Count";
	$sensor->{multiplyby} ||= 1;
	cfg_warn "Ignoring illegal InactivityReset for lightning sensor $sensor->{_nk}"
	    if delete $snsr->{inactivityreset};
	}
    elsif ($sensor->{type} eq "math") {
	$sensor->{_scale} = $sensor->{units} || "Computation";
	$sensor->{_scale} =~ s/\t/ /g;
	$sensor->{_label} = $sensor->{units} || "";	# "" is defined :-)
	}
    elsif ($sensor->{type} eq "milliamps") {
	$sensor->{_scale} ||= "mA";
	$sensor->{_label} ||= "milliAmps";
	}
    elsif ($sensor->{type} eq "onoff") {
	$sensor->{_scale} ||= "OnOff";
	$sensor->{_label} ||= "OnOff";
	}
    elsif ($sensor->{type} =~ /power$/) {	# Real-Power and Apparent Power
	$sensor->{_scale} ||= "W";
	$sensor->{_label} ||= "Power";
	}
    elsif ($sensor->{type} eq "power-factor") {
	$sensor->{_scale} ||= "PF";
	$sensor->{_label} ||= "Power Factor";
	}
    elsif ($sensor->{type} eq "pressure") {
	$sensor->{_scale} ||= "kPa";
	$sensor->{_label} ||= "Pressure";
	}
    elsif ($sensor->{type} eq "rain") {
	my ($time, $val);
	#
	# The rain gauge measures 0.01" per click (if desired,
	# we convert to millimeters when we display).  However, the
	# Temp08 already has the multiplier built in...
	#
	$sensor->{_scale} ||= "Inch";
	$sensor->{_label} ||= "Inches";
	unless ($collector->{type} eq "temp08") {
	    $sensor->{multiplyby} ||= .01;
	    }
	#
	# See if the log for this sensor has any data we want.  If we have
	# valid data, continue the rain from that time - else reset to zero.
	#
	$sensor->{inactivityreset} = parse_reltime("<Sensor $n\@$k>InactivityReset", delete $snsr->{inactivityreset}, "12h");
	get_last_logged_counter_value($sensor);
	}
    elsif ($sensor->{type} eq "range") {
	$sensor->{_scale} ||= "in";
	$sensor->{_label} ||= "inches";
	}
    elsif ($sensor->{type} eq "sound") {
	$sensor->{_scale} ||= "Sound";
	$sensor->{_label} ||= "Sound Level";
	}
    elsif ($sensor->{type} =~ /^(speed|gust)$/) {
	# Record MPH - other options converted in adjust_for_scale()
	$sensor->{_scale} ||= "MPH";
	$sensor->{_label} ||= "MPH";
	$sensor->{multiplyby} ||= 2.453;
	cfg_warn "Ignoring illegal InactivityReset for sensor $sensor->{_nk}"
	    if delete $snsr->{inactivityreset};
	}
    elsif ($sensor->{type} eq "state") {
	$sensor->{_scale} ||= "#";
	$sensor->{_label} ||= "State";
	}
    elsif ($sensor->{type} eq "sunlight") {
	$sensor->{_scale} ||= "raw";
	$sensor->{_label} ||= "raw current";
	}
    elsif ($sensor->{type} eq "temperature") {
	# Scale is inherited from the collector
	$sensor->{_scale} ||= $collector->{scale};
	# Do nothing special for label - this is a weird special case handled
	# in the display portions of the code
	}
    elsif ($sensor->{type} =~ /^volts/) {	# Volts, Volts-{Min,Max,Peak}
	$sensor->{_scale} ||= "V";
	$sensor->{_label} ||= "Volts";
	}
    elsif ($sensor->{type} eq "watts") {
	$sensor->{_scale} ||= "W";
	$sensor->{_label} ||= "Watts";
	}
    elsif ($sensor->{type} eq "wetness") {
	$sensor->{_scale} ||= "Wetness";
	$sensor->{_label} ||= "Wetness";
	}
    elsif ($sensor->{type} eq "wh") {
	$sensor->{_scale} ||= "Wh";
	$sensor->{_label} ||= "Watt Hours";
	}
    elsif ($sensor->{type} eq "windchill") {
	# Scale is inherited from temperature
	$sensor->{_label} ||= "Windchill";
	}
    else {
	warn "Internal consistency error - please report to dan\@klein.com - found an unknown sensor->{type} == $sensor->{type}"
	}

    if (! defined $sensor->{_label} && $sensor->{type} ne "temperature") {
	warn "Internal consistency error - please report to dan\@klein.com - found an unlabeled $sensor->{_label} sensor->{type} == $sensor->{type}"
	}


    if ($sensor->{type} =~ /^(rain|lightning|speed|gust|counter)$/) {
	if ($sensor->{adjustby}) {
	    $sensor->{adjustby} = 0;
	    cfg_warn "Ignoring AdjustBy for $sensor->{type} sensor $sensor->{_nk}";
	    }
	}

    if (defined $sensor->{inactivityreset} && $sensor->{inactivityreset} < 2*$config{loginterval}) {
	cfg_die "<Sensor $n\@$k> InactivityReset must be larger than 2x LogInterval";
	}

    #
    # Parse the AdjustBy values late, after the (possibly default) sensor type
    # has been determined
    #
    if ($sensor->{adjustby} = delete $snsr->{adjustby}) {
	coerce_to_scale($sensor, undef, "adjustby");
	}
    #
    # Add every completed sensor to the view named 'all' unless
    # it is explicitly excluded
    #
    unless ($sensor->{hide} = defined delete $snsr->{hide}) {
	$config{_view}{all}{"$n\@$k"}{_name} = "$n\@$k";
	$config{_view}{all}{"$n\@$k"}{_sensor} = $sensor;
	}
    }

sub get_last_logged_counter_value {
    my ($sensor) = @_;
    my ($fd, $save, $line, $val);

    return unless $opt_daemon && $sensor->{inactivityreset};
    #
    # First check the current file - it will always be more recent than
    # the available logs
    #
    my ($time, @lines) = read_current_values();
    if (defined $time && $time > PROGRAM_START - $sensor->{inactivityreset}) {
	for my $line (@lines) {
	    my ($val, $units, $nk) = split /\t/, $line, 3;
	    if ($nk eq $sensor->{_nk}) {
		preset_sensor($sensor, $time, $val);
		return;
		}
	    }
	}

    #
    # If we didn't find it (perhaps because we crashed while updating the
    # current file), then check the logs
    #
    if ($config{logformat} eq "text") {
	$fd = log_open($sensor, 0);
	#
	# Dict::Search::look sets the file pointer to the first line
	# that is greater than or equal to the value we want.  It will
	# fail under three conditions
	#	1) The file starts after the date we want (returns -1)
	#	2) The file ends before the date we want (we are at eof)
	#	3) The file is empty (it could happen! :-) (returns -1)
	# We need to assess the success and two failure modes below
	#
	# Find the first entry that is no more than InactivityReset seconds ago
	#
	if (look($fd, (PROGRAM_START - $sensor->{inactivityreset}),
		{comp => sub { $_[0] <=> $_[1] }}) == -1) {
	    return;
	    }
	#
	# And then get the most recent reading
	#
	chomp($save = $fd->getline);
	while ($line = $fd->getline) {
	    chomp ($save = $line);
	    }
	close $fd;
	#
	# If we can't read anything, then we're at the end of the file
	# (conditions 2 or 3, above).  Skip this file - we have no time
	# references to use.  We'll fill it in at the very end.
	#
	($time, $val) = split /\t/, $save;
	preset_sensor($sensor, $time, $val)	if $time;
	}
    elsif ($config{logformat} eq "sql") {
	$sth = $dbh->prepare(qq{
	    SELECT logtime, value FROM readings
		WHERE log_id = $sensor->{_id}
		AND   logtime >= ?
	    ORDER BY logtime DESC LIMIT 1});
	$sth->execute(PROGRAM_START - $sensor->{inactivityreset});
	($time, $val) = $sth->fetchrow_array();
	preset_sensor($sensor, $time, $val)	if $time;
	}
    else {
	die "Unknown LogFormat";
	}
    }

sub preset_sensor {
    my ($sensor, $time, $val) = @_;

    $sensor->{_sum} = $val;
    $sensor->{_lastchange} = $time;
    $sensor->{_count} = 1;
    $sensor->{_last5} = [ $sensor->{_sum} ];
    msg("notice","Counter $sensor->{_nk} $sensor->{_name} value set to $val from @{[strftime '%c', localtime $time]}");
    }

sub parse_math_sensor {
    my ($snsr, $sensor) = @_;
    my ($in, $out, $tok, $n, $k);

    $sensor->{units} = delete $snsr->{units};

    unless ($sensor->{value} = delete $snsr->{value}) {
	cfg_die "Missing Value for computation for $sensor->{_nk}";
	}
    $in = $sensor->{value};
    while (length($in)) {
	$in =~ s/^(\s*)//;
	$out .= $1;

	if ($in =~ s/^([\w.]+(?:\.\w)?)\@(\w+)//) {
	    $n = $1;
	    $k = $2;
	    if (exists $config{collector}{$k}{sensor}{$n}) {
		cfg_die "You cannot use derived sensors as components of other derived sensor (in Sensor $sensor->{_nk})"
		    if $config{collector}{$k}{type} eq "derived";
		$out .= "compute_average(\$config{collector}{'$k'}{sensor}{'$n'})";
		push @{ $sensor->{_ref} }, $config{collector}{$k}{sensor}{$n};
		}
	    else {
		cfg_die "Sensor $n\@$k in computation for $sensor->{_nk} does not exist";
		}
	    }
	elsif ($in =~ s/^(min|max|abs|sign|log|sin|cos|atan)\b//) {
	    $out .= $1;
	    }
	elsif ($in =~ s/^($number)\b//) {
	    $out .= $1;
	    }
	elsif ($in =~ s/^(\W+)//) {
	    $out .= $1;
	    }
	else {
	    cfg_die "Syntax error near $in in computation for $sensor->{_nk}";
	    last;
	    }
	}
    eval $out;
    if ($@) {
	cfg_die "Syntax error in computation for $sensor->{_nk} - $@";
	}
    $sensor->{_eval} = $out;
    }

#
# NOTE: This subroutine creates a string which is eval-uated to a call to one
# of the compute_mumble routines, below.  Therefore the order of the arguments
# of compute_mumble must match those in the call to parse_builtin_computation.
# BEWARE of clever use of adjust_for_scale (since the computations are all in
# Metric, regardless of how we read the data)
#
sub parse_builtin_computation {
    my ($snsr, $sensor, $type, @need) = @_;
    my (@vars, $n, $k);

    { no strict 'refs';
      die "No such subroutine compute_$type" unless defined &{"compute_$type"};
    }
    for my $t (@need) {
	unless ($sensor->{$t} = delete $snsr->{$t}) {
	    cfg_die "Missing \u$t for for \u$type $sensor->{_nk}";
	    }
	if ($sensor->{$t} =~ s/^([\w.]+(?:\.\w)?)\@(\w+)//) {
	    $n = $1;
	    $k = $2;
	    #
	    # WARNING! $config{collector}{$k}{sensor}{$n} is the sensor
	    # referenced by the derived sensor; $sensor is the derived sensor
	    #
	    if (exists $config{collector}{$k}{sensor}{$n}) {
		cfg_die "\u$t sensor $sensor->{$t} in \u$type sensor $sensor->{_nk} is not Type \u$t!"
		    unless $config{collector}{$k}{sensor}{$n}{type} eq $t;
		push @vars, "adjust_for_scale(compute_average(\$config{collector}{'$k'}{sensor}{'$n'}), '$config{collector}{$k}{sensor}{$n}{_scale}', -2)";
		push @{ $sensor->{_ref} }, $config{collector}{$k}{sensor}{$n};
		}
	    else {
		cfg_die "\u$t $sensor->{$t} in \u$type $sensor->{_nk} does not exist";
		}
	    }
	else {
	    cfg_die "\u$t sensor $sensor->{$t} in \u$type $sensor->{_nk} does not exist (format is SensorNN\@CollectorXX)";
	    }
	}
    $sensor->{_eval} = "{"
		     . "local \$opt_temperature = 'C';"
		     . "local \$opt_windspeed = 'KPH';"
		     . "compute_$type(" .  join(',', @vars) . ")"
		     . "}";
    }

sub compute_windchill {
    #
    # See http://en.wikipedia.org/wiki/Wind_chill - requires degrees C, KPH
    #
    my ($T, $V) = @_;
    my $Ve = $V ** 0.16;
    return undef if $T < -50 || $T > 5 || $V < 3;
    return 13.12 + 0.6215*$T - 11.37 * $Ve + 0.3965 * $T * $Ve;
    }

sub compute_dewpoint {
    #
    # See http://en.wikipedia.org/wiki/Dew_point - requires degrees C, %RH
    #
    my ($T, $RH) = @_;
    return undef if $T <= 0 || $T >= 60;
    my $a = 17.27;
    my $b = 237.7;
    my $gamma = (($a * $T) / ($b + $T)) + log($RH / 100);
    return ($b * $gamma) / ($a - $gamma);
    }

sub compute_heatindex {
    #
    # See http://en.wikipedia.org/wiki/Heat_index - the subroutine requires
    # degrees C and %RH, but the formula requires degrees F, so we convert
    # back and forth.
    #
    my ($T, $RH) = @_;
    $T = $T * (9/5) + 32;
    return undef if $T < 80 || $RH < 40;

    my $c1 = -42.379;
    my $c2 =   2.04901523;
    my $c3 =  10.14333127;
    my $c4 = - 0.22475541;
    my $c5 = - 6.83783e-3;
    my $c6 = - 5.481717e-2;
    my $c7 =   1.22874e-3;
    my $c8 =   8.5282e-4;
    my $c9 = - 1.99e-6;

    return (($c1 + $c2*$T + $c3*$RH + $c4*$T*$RH + $c5*$T**2 + $c6*$RH**2 +
	     $c7*$T**2*$RH + $c8*$T*$RH**2 + $c9*$T**2*$RH**2) - 32) * (5/9);
    }

sub compute_humidex {
    #
    # See http://en.wikipedia.org/wiki/Humidex and
    # http://everything2.com/?node_id=87523  - requires degrees C and %RH
    #
    my ($T, $RH) = @_;
    return undef if $T < 20 || $RH < 40;

    my $e = 6.112 * 10**((7.5*$T)/(237.7+$T)) * ($RH/100);
    return $T + (5/9)*($e - 10);
    }

sub percolate_actuator_values {
    my ($actuator, $collector, $ok);
    my $now = time;
    # Actuator values are only altered when an alarm goes on or off.  But we
    # want the values to look like they're updated regularly, so we take the
    # most recent value and add it into the chain of values we've seen

    COLLECTOR: for my $k (keys %{ $config{collector} }) {
	$collector = $config{collector}{$k};
	next COLLECTOR if $collector->{readonly};
	ACTUATOR: for my $n (keys %{ $collector->{actuator} }) {
	    $actuator = $collector->{actuator}{$n};
	    next ACTUATOR if $actuator->{readonly};
	    append_to_average($actuator, $actuator->{_last5}[0]);
	    }
	}
    }

sub compute_derived_sensors {
    my ($sensor, $collector, $ok);
    my $now = time;

    COLLECTOR: for my $k (keys %{ $config{collector} }) {
	$collector = $config{collector}{$k};
	next COLLECTOR if $collector->{readonly};
	next COLLECTOR unless $collector->{type} eq "derived";
	#
	# If this is a derived collector, then each sensor gets updated here
	# IFF all of the real sensors it references have also been updated
	# within PollInterval for each sensor's collector.
	#
	SENSOR: for my $n (keys %{ $collector->{sensor} }) {
	    $sensor = $collector->{sensor}{$n};
	    next SENSOR if $sensor->{readonly};
	    $ok = 1;
	    for my $ref (@{ $sensor->{_ref} }) {
		if ($now - $ref->{_lastdata} > $collector->{pollinterval} ||
			! defined $ref->{_sum}) {
		    $ok = 0;
		    last;
		    }
		}
	    if ($ok) {
		#
		# Since there may be min/max/average computations for each
		# component sensor, we only save the most recent computation
		# for each derived sensor.  If there are no valid computations,
		# the computed sensor value will be nulled in update_current
		#
		my $val = eval $sensor->{_eval};
		if (defined $val) {
		    $sensor->{_sum} = $val;
		    $sensor->{_count} = 1;
		    $sensor->{_last5} = [ $val ];
		    $sensor->{_lastdata} = $now;
		    }
		}
	    }
	}
    }

sub parse_onoff {
    my ($snsr, $sensor, $k, $n) = @_;
    our $traphandler;			# Persistent data, locally scoped
    if ($sensor->{adjustby}) {
	$sensor->{adjustby} = 0;
	cfg_warn "Ignoring AdjustBy for on/off sensor $sensor->{_nk} - use OnValue and OffValue instead";
	}
    if (defined $snsr->{onvalue} && $snsr->{onvalue} !~ /$numeric/) {
	cfg_warn "Ignoring non-numeric OnValue in $sensor->{_nk}";
	delete $snsr->{onvalue};
	}
    $sensor->{allowsnmptraps} = defined delete $snsr->{allowsnmptraps};
    $sensor->{onvalue} = delete $snsr->{onvalue} || 1;
    if (defined $snsr->{offvalue} && $snsr->{offvalue} !~ /$numeric/) {
	cfg_warn "Ignoring non-numeric OffValue in $sensor->{_nk}";
	delete $snsr->{offvalue};
	}
    $sensor->{offvalue} = delete $snsr->{offvalue} || 0;
    if ($config{collector}{$k}{type} eq "snmp") {
	if (defined $snsr->{snmponvalue} && $snsr->{snmponvalue} !~ /$numeric/) {
	    cfg_warn "Ignoring non-numeric SNMPOnValue in $sensor->{_nk}";
	    delete $snsr->{snmponvalue};
	    }
	$sensor->{snmponvalue} = delete $snsr->{snmponvalue} || 1;

	if (defined $snsr->{snmpoffvalue} && $snsr->{snmpoffvalue} !~ /$numeric/) {
	    cfg_warn "Ignoring non-numeric SNMPOffValue in $sensor->{_nk}";
	    delete $snsr->{snmpoffvalue};
	    }
	$sensor->{snmpoffvalue} = delete $snsr->{snmpoffvalue} || 2;
	}
    elsif ($config{collector}{$k}{type} eq "hwg") {
	if (defined delete $snsr->{snmponvalue}) {
	    cfg_warn "Ignoring SNMPOnValue in $sensor->{_nk} - it is predefined in the MIB as 2";
	    }
	$sensor->{snmponvalue} = 1;

	if (defined delete $snsr->{snmpoffvalue}) {
	    cfg_warn "Ignoring SNMPOffValue in $sensor->{_nk} - it is predefined in the MIB as 1";
	    }
	$sensor->{snmpoffvalue} = 0;
	}
    }

sub parse_ds2423 {
    my ($snsr, $sensor, $k, $n) = @_;

    $sensor->{type} ||= "unspecified";		# Only used for error message
    $sensor->{channel} = uc delete $snsr->{channel};

    if ($sensor->{type} =~ /^(speed|gust|rain|lightning)$/) {
	$sensor->{channel} ||= 'A';	# These default to channel A
	}
    elsif ($sensor->{type} eq "counter") {
	# Nothing special, just no default channel...
	}
    else {
	cfg_die "DS2423 Sensor $sensor->{_nk} must be Speed, Gust, Lightning, Rain, or Counter";
	}

    cfg_die "Must specify Channel A or B for Sensor $sensor->{_nk}"
	unless $sensor->{channel} =~ /^[AB]$/;

    if ($collector->{type} =~ /^(owfs|owhttpd|owshell)$/) {
	$sensor->{_owfs} = "counters.$sensor->{channel}";
	}
    }

sub parse_alarm_constraints {
    my $alarm = shift;
    my ($from, $to, $month);

    #
    # CGI scripts set the locale of the USER.  Don't parse constraints that
    # aren't used anyway in a CGI, since they may generate errors.
    #
    return undef if $is_cgi;
    #
    # Alarm dates can be specified in English or in locale specific notation
    #
    if ($alarm->{dates}) {
	($from, $to) = split /\s*-\s*/, $alarm->{dates}, 2;
	if ($from =~ /^(\d+)\s+(\w+)$/) {
	    return $from unless $1 <= 31 &&
		($month = ($e_month{lc $2} || $l_month{lc $2}));
	    $from = sprintf "%02d%02d", $month, $1;
	    }
	elsif ($from =~ /^(\w+)\s+(\d+)$/) {
	    return $from unless $2 <= 31 &&
		($month = ($e_month{lc $1} || $l_month{lc $1}));
	    $from = sprintf "%02d%02d", $month, $2;
	    }
	else {
	    return $from;
	    }

	if ($to =~ /^(\d+)\s+(\w+)$/) {
	    return $to unless $1 <= 31 &&
		($month = ($e_month{lc $2} || $l_month{lc $2}));
	    $to = sprintf "%02d%02d", $month, $1;
	    }
	elsif ($to =~ /^(\w+)\s+(\d+)$/) {
	    return $to unless $2 <= 31 &&
		($month = ($e_month{lc $1} || $l_month{lc $1}));
	    $to = sprintf "%02d%02d", $month, $2;
	    }
	else {
	    return $to;
	    }

	$alarm->{_date_from} = $from;
	$alarm->{_date_to} = $to;
	$alarm->{_date_reverse}++  if $to < $from;
	}

    if ($alarm->{times}) {
	($from, $to) = split /\s*-\s*/, $alarm->{times}, 2;
	if ($from =~ /^(\d+):(\d+)$/) {
	    return $from unless $1 <= 23 && $2 <= 59;
	    $from = sprintf "%02d%02d", $1, $2;
	    }
	else {
	    return $from;
	    }

	if ($to =~ /^(\d+):(\d+)$/) {
	    return $to unless $1 <= 23 && $2 <= 59;
	    $to = sprintf "%02d%02d", $1, $2;
	    }
	else {
	    return $to;
	    }

	$alarm->{_time_from} = $from;
	$alarm->{_time_to} = $to;
	$alarm->{_time_reverse}++  if $to < $from;
	}
    return undef;
    }

sub snmp_set_switch {
    my ($collector, $actuator, $direction) = @_;
    my $desired = $actuator->{"snmp${direction}value"};
    my $ua = $collector->{_ua};
    my $result = $ua->set_request(
	-varbindlist => [
	    $actuator->{_oid},
	    Net::SNMP::INTEGER(),
	    $desired,
	    ]);
    unless (defined $result) {
	msg("err", "SNMP failure ($desired -> $actuator->{_oid}) - could not turn $direction $actuator->{name}.  %s",
	    $ua->error());
	return 0;
	}
    $result = $ua->get_request(
	-varbindlist => [ $actuator->{_oid} ],
	);
    unless (defined $result) {
	msg("err", "Could not check turn $direction $actuator->{name}");
	return 0;
	}
    if ($result->{ $actuator->{_oid} } == $desired) {
	append_to_average($actuator, $actuator->{"${direction}value"});
	return 1;
	}
    else {
	msg("err", "Actuator failed - did not turn $direction $actuator->{name}");
	return 0;
	}
    }

sub hwg_xml_set_switch {
    my ($collector, $actuator, $direction) = @_;
    my $ua = $collector->{_ua};
    my $url = "$collector->{_baseurl}/setup.xml";
    my $id = $actuator->{_name};
       $id =~ s/^A//;
       $id += 150;	# Actuators have IDs 151, 152...
    my $xml = "<set:Root><BinaryOutSet><Entry><ID>$id</ID><Value>%d</Value></Entry></BinaryOutSet></set:Root>";
    my $val;

    if ($direction eq 'on') {
	$val = $actuator->{onvalue};
	}
    elsif ($direction eq 'off') {
	$val = $actuator->{offvalue};
	}
    else {
	msg("crit", "Impossible on/off direction: $direction");
	}
    #
    # Send the XML command to twitch the sensor, but don't check results
    #
    append_to_average($actuator, $val);
    $ua->post($url, content_type => "text/xml", content => sprintf($xml, $val));
    }

sub ha7net_set_switch {
    my ($collector, $actuator, $direction) = @_;
    my $ua = $collector->{_ua};
    my $url;
    if ($actuator->{_family} eq "30") {
	$url = "$collector->{_baseurl}/1Wire/WriteBlock.html?Address=$actuator->{_nx}";
	#
	# Turn the LED on/off in the TAI8560, don't bother parsing the retval
	#
	if ($direction eq 'on') {
	    append_to_average($actuator, $actuator->{onvalue});
	    $ua->get("$url&Data=6C089F");
	    }
	elsif ($direction eq 'off') {
	    append_to_average($actuator, $actuator->{offvalue});
	    $ua->get("$url&Data=6C08DF");
	    }
	else {
	    msg("crit", "Impossible on/off direction: $direction");
	    }
	}
    else {
	msg("crit", "Impossible DalSemi actuator family: $actuator->{_family}");
	}
    return 1;
    }

#
# NOTE: check_opts is called AFTER read_config - we check or assign some
# options based on values in the config file
#
sub check_opts {
    my $main = shift;
    my %allowed = map { ($_, 1) } @_;
    my ($k, $v);

    while (($k, $v) = each %options) {
	next if $k eq $main;
	$k =~ s/(\w+)\W?.*/$1/;		# Change "mumble=s" => "mumble"
	if ($$v && ! $allowed{$k}) {	# $v is a REF to the switch value
	    die "Error: -$k is not permitted when you use -$main\n";
	    }
	}

    if ($opt_units) {
	$opt_units = ucfirst(lc($opt_units));
	#
	# This F/C -> English/Metric is for backwards compatability
	#
	$opt_units = "English"	if $opt_units eq "F";
	$opt_units = "Metric"	if $opt_units eq "C";

	$opt_temperature = uc($opt_temperature ||= $units{$opt_units}{temperature});
	$opt_barometer = uc($opt_barometer ||= $units{$opt_units}{barometer});
	$opt_rainfall = uc($opt_rainfall ||= $units{$opt_units}{rainfall});
	$opt_windspeed = uc($opt_windspeed ||= $units{$opt_units}{windspeed});
	}
    else {
	$opt_units = ucfirst(lc($config{displayin}));
	#
	# F/C -> English/Metric handled in read_config
	#
	$opt_temperature = uc($opt_temperature ||= $config{temperature});
	$opt_barometer = uc($opt_barometer ||= $config{barometer});
	$opt_rainfall = uc($opt_rainfall ||= $config{rainfall});
	$opt_windspeed = uc($opt_windspeed ||= $config{windspeed});
	}

    unless ($opt_units =~ /^(English|Metric)$/) {
	die "Choose English or Metric for -units\n";
	}
    unless ($opt_temperature =~ /^[FC]$/) {
	die "Choose F or C for -temperature\n";
	}
    unless ($opt_barometer =~ /^((IN|MM)HG|HPA|KPA|MBAR|MILLIBAR)$/) {
	die "Choose inHg, mmHg, hPa, kPa, mBar or millibar for -barometer\n";
	}
    unless ($opt_rainfall =~ /^(INCH(ES)?|MM)$/) {
	die "Choose inches or mm for -rainfall\n";
	}
    unless ($opt_windspeed =~ /^(MPH|KPH|MPS|KNOTS?)$/) {
	die "Choose MPH, KPH, MPS, or Knots for -windspeed\n";
	}

    $opt_view = lc($opt_view) || $config{defaultview} || "all";
    if (!$config{_view}{$opt_view}) {
	die "-view $opt_view not defined in $opt_config\n";
	}

    if ($opt_current && ($opt_from || $opt_to ||$opt_center || $opt_span)) {
	die "Cannot use -from, -to, -center or -span with -current\n";
	}
    if (($opt_from || $opt_to) && ($opt_center || $opt_span)) {
	die "Cannot use either -from or -to with -center or -span\n";
	}

    if (defined $opt_outfile) {
	my $tmp_fd = new FileHandle;
	open $ofd, ">", $opt_outfile  or die "Cannot write $opt_outfile - $!\n";
	}
    else {
	$ofd = \*STDOUT;
	}
    #
    # If height/width not specified, pretend they were by using config values
    #
    $opt_height ||= $config{graphheight};
    $opt_width ||= $config{graphwidth};
    if (@ARGV) {
	die "Extra arguments after switches not allowed\n";
	}
    }

sub parse_and_assign_dates {
    my ($sec_f, $sec_t, $rel_f, $rel_t, $sign_f, $sign_t);
    if ($opt_center) {
	($sec_f, $rel_f, $sign_f) = parse_date(center => $opt_center);
	$sec_f = PROGRAM_START - $sec_f	if $rel_f;
	if ($opt_span) {
	    ($sec_t, $rel_t, $sign_t) = parse_date(span => $opt_span);
	    die "-span must be a relative time\n"	unless $rel_t;
	    }
	else {
	    ($sec_t, $rel_t, $sign_t) = parse_date(span => '1d');
	    }
	$date_from = $sec_f - int($sec_t / 2);
	$date_to   = $sec_f + int($sec_t / 2);
	}
    elsif ($opt_from && $opt_to) {
	($sec_f, $rel_f, $sign_f) = parse_date(from => $opt_from);
	($sec_t, $rel_t, $sign_t) = parse_date(from => $opt_to);
	if ($rel_f && $rel_t) {		# -from relative -to relative
	    $date_from = PROGRAM_START - $sec_f;
	    if ($sign_t eq '+') {
		$date_to    = $date_from + $sec_t;
		}
	    else {
		$date_to    = PROGRAM_START - $sec_t;
		}
	    }
	elsif ($rel_f) {		# -from relative -to absolute
	    $date_from = PROGRAM_START - $sec_f;
	    $date_to   = $sec_t;
	    }
	elsif ($rel_t) {		# -from absolute -to relative
	    $date_from = $sec_f;
	    if ($sign_t eq '-') {
		$date_to    = PROGRAM_START - $sec_t;
		}
	    else {
		$date_to    = $sec_f + $sec_t;
		}
	    }
	else {				# -from absolute -to absolute
	    $date_from = $sec_f;
	    $date_to   = $sec_t;
	    }
	}
    elsif ($opt_from && ! $opt_to) {
	($sec_f, $rel_f) = parse_date(from => $opt_from);
	$date_from = $rel_f ? PROGRAM_START - $sec_f : $sec_f;
	$date_to   = PROGRAM_START;
	}
    elsif (! $opt_from && $opt_to) {
	($sec_t, $rel_t) = parse_date(from => $opt_to);
	$date_to   = $rel_t ? PROGRAM_START - $sec_t : $sec_t;
	$date_from = $date_to - 86400;
	}
    elsif (! $opt_from && ! $opt_to) {
	$date_from = PROGRAM_START - 86400;
	$date_to   = PROGRAM_START;
	}
    else {
	die "Utterly unexpected date configuration";
	}

    die "-to date antecedes -from date\n"	if $date_from > $date_to;
    $date_to = min(PROGRAM_START, $date_to);	# Can't see into the future

    print "From = ", strftime("%c", localtime $date_from), "\nTo = ", strftime("%c", localtime $date_to), "\n" if $opt_verbose;
    }

sub parse_date {
    my ($seconds, $sign, $y, $m, $w, $d, $h);
    my ($opt, $date) = @_;
    my $N = qr/\d+(?:\.\d*)?|\.\d+/;

    if (lc($date) eq 'now' || lc($date) eq 'today') {
	return (PROGRAM_START, 0, undef);
	}
    elsif (lc($date) eq 'yesterday') {
	return (PROGRAM_START - 86400, 0, undef);
	}
    elsif ($seconds = str2time($date)) {
	return ($seconds, 0, undef);
	}
    elsif (($sign, $y, $m, $w, $d, $h) = $date =~ /^\s*([+-])?(?:($N)y)?(?:($N)m)?(?:($N)w)?(?:($N)d)?(?:($N)h)?\s*$/) {
	return ((3600*$h + 86400*$d + 7*86400*$w + 30*86400*$m + 365*86400*$y),
	    1, $sign);
	}
    else {
	die "Unrecognizeable $opt date/time format '$date'\n";
	}
    }

sub parse_reltime {
    my ($what, $str, $default) = @_;
    my ($seconds, $d, $h, $m, $s);
    my $N = qr/\d+(?:\.\d*)?|\.\d+/;
    $str = $default	unless defined $str;

    if (($d, $h, $m, $s) = $str =~ /^(?:($N)d)?(?:($N)h)?(?:($N)m)?(?:($N)s)?$/i) {
	return int($s + 60*$m + 3600*$h + 86400*$d);
	}
    else {
	cfg_die "Unrecognizeable relative time '$str' for $what.  All relative times must have units (e.g., 30s, 1m30s, 12h10m, 1d, etc.)\n";
	}
    }

sub connect_to_database {
    my $fatal = shift;

    my ($username, $password, $host) =
	$config{_db_auth} =~ /^(\w+)(?::(.*)?)@(.*)/;
    my $dsn;

    print "Connecting to database @{[scalar localtime]} from @{[caller()]}\n" if $opt_verbose;
    if ($config{_sql_type} eq "postgres") {
	$dsn = "DBI:$config{_sql_type}:dbname=$config{_database}:$host";
	}
    else {
	$dsn = "DBI:$config{_sql_type}:$config{_database}:$host";
	}
    $dbh = DBI->connect_cached($dsn, $username, $password,
	{ AutoCommit => 1, PrintError => 0, PrintWarn => 0 });
    #
    # If we could (re)connect, catch bad behavior (there should be none :-)
    # and return success
    #
    if ($dbh) {
	$dbh->{PrintError} = 1;
	$dbh->{PrintWarn} = 1;
	return 1;
	}
    #
    # Otherwise, whine with the appropriate intensity, and possibly return
    # an indication of failure
    #
    if ($fatal) {
	cfg_die "Can't connect to $config{_sql_type} DB $config{_database}.  Do you need to create it?\n\tEx: mysqladmin create $config{_database}\n\tDB Error was: $DBI::errstr";
	}
    else {
	msg("err", "Can't connect to $config{_sql_type} DB $config{_database}: $DBI::errstr");
	}
    return 0;
    }

sub verify_database_format {
    my (%tables, $sth);
    #
    # Create the tables if necessary
    #
    %tables = map { s/`//g; s/.*\.//; ($_, 1) } $dbh->tables();
    unless ($tables{logfiles}) {
	warn "Creating table 'logfiles' in database '$config{_database}'\n";
	if ($config{_sql_type} eq "postgres") {
	    $sth = $dbh->prepare(qq{
		CREATE TABLE logfiles (
		    name	VARCHAR(80) NOT NULL,
		    log_id	SERIAL PRIMARY KEY
		    );
		});
	    }
	else {
	    $sth = $dbh->prepare(qq{
		CREATE TABLE logfiles (
		    name	VARCHAR(80) NOT NULL,
		    log_id	INT AUTO_INCREMENT PRIMARY KEY
		    );
		});
	    }
	$sth->execute();
	die $sth->errstr()	if $sth->err();
	}

    unless ($tables{readings}) {
	warn "Creating table 'readings' in database '$config{_database}'\n";
	if ($config{_sql_type} eq "postgres") {
	    $sth = $dbh->prepare(qq{
		CREATE TABLE readings (
		    logtime	INT,
			INDEX (logtime),
		    log_id	INT
			REFERENCES logfiles (log_id),
		    value	FLOAT
		    );
		});
	    }
	else {
	    $sth = $dbh->prepare(qq{
		CREATE TABLE readings (
		    logtime	INT,
			INDEX (logtime),
		    log_id	INT,
			FOREIGN KEY (log_id) REFERENCES logfiles (log_id),
		    value	FLOAT
		    );
		});
	    }
	$sth->execute();
	die $sth->errstr()	if $sth->err();
	}
    unless ($tables{current}) {
	warn "Creating table 'current' in database '$config{_database}'\n";
	if ($config{_sql_type} eq "postgres") {
	    $sth = $dbh->prepare(qq{
		CREATE TABLE current (
		    logtime     INT,
		    value       FLOAT,
		    units       VARCHAR(15),
		    log_id      INT,
			REFERENCES logfiles(log_id),
		    log_name    VARCHAR(80)
		    );
		});
	    }
	else {
	    # In MySQL, the default table type is MyISAM, which does not
	    # support transaction locking, but InnoDB tables do (and since
	    # we clear the "current" table and rewrite it while in a
	    # transaction, we need to explicitly use a special type here).
	    $sth = $dbh->prepare(qq{
		CREATE TABLE current (
		    logtime     INT,
		    value       FLOAT,
		    units       VARCHAR(15),
		    log_id      INT,
			FOREIGN KEY(log_id) REFERENCES logfiles(log_id),
		    log_name    VARCHAR(80)
		    ) ENGINE = innodb;
		});
	    }
	$sth->execute();
	die $sth->errstr()	if $sth->err();
	}

    #
    # Verify that the tables look correct from a column standpoint.  We
    # don't have a problem with extra columns, as long as "ours" look okay.
    #
    my %template = (
	readings => {
	    logtime	=> "INT",
	    log_id	=> "INT",
	    value	=> "FLOAT",
	    },
	logfiles => {
	    name	=> "VARCHAR",
	    log_id	=> "INT",
	    },
	current => {
	    logtime     => "INT",
	    value       => "FLOAT",
	    units       => "VARCHAR",
	    log_id      => "INT",
	    log_name    => "VARCHAR",
	    },
	);
    %tables = map { s/`//g; s/.*\.//; ($_, 1) } $dbh->tables();
    if ($tables{readings} && $tables{logfiles} && $tables{current}) {
	for my $tname (keys %template) {
	    my $sth = $dbh->column_info(undef, $config{_database}, $tname, "%");
	    my $info = $sth->fetchall_arrayref({});
	    my %sql_table = ();
	    for my $col (@$info) {
		$sql_table{ $col->{COLUMN_NAME} } = $col->{TYPE_NAME};
		}
	    for my $cname (keys %{ $template{$tname} }) {
		die "Column $cname is missing in table '$tname'\n"
		    unless exists $sql_table{$cname};
		die "Column $cname in table '$tname' is $sql_table{$cname}, should be $template{$tname}{$cname}\n"
		    unless $sql_table{$cname} eq $template{$tname}{$cname};
		}
	    }
	}
    else {
	die "Can't find all required tables 'readings', 'logfiles' and 'current' in database '$config{_database}'\n";
	}
    }

sub dump_config {
    my ($collector, $sensor, $actuator, $alarm, $view, $_view, $str);

    if ($opt_verbose && ! $erred) {
	print "#Warning, the config file below is out of sync with reality\n";
	print "#The routine that generates it has not been kept up-to-date\n\n";
	for my $k (sort keys %config) {
	    next if ref ($str = $config{$k});
	    if (! defined $str) {
		$k = "# \u$k";
		$str = "[no default value]";
		}
	    if ($str =~ /\s{2}|#/) {
		$str = qq("$str");
		}
	    printf "%-15s %s\n", ucfirst $k, $str;
	    }
	print "\n";

	if ($config{rss}) {
	    print "<RSS $config{rss}{_dir}>\n";
	    for my $k (sort keys %{ $config{rss} }) {
		next if $k =~ /^_/;
		$str = $config{rss}{$k};
		if (! defined $str) {
		    $k = "# \u$k";
		    $str = "[no default value]";
		    }
		if ($str =~ /\s{2}|#/) {
		    $str = qq("$str");
		    }
		printf "    %-11s %s\n", ucfirst $k, $str;
		}
	    print "</RSS>\n\n";
	    }

	for my $k (sort keys %{ $config{collector} }) {
	    next if $k eq "WUNDERGROUND/";
	    print "<Collector $k>\n";
	    $collector = $config{collector}{$k};
	    for my $x (sort keys %$collector) {
		next if $x eq "sensor" || $x =~ /^_/;
		$str = $collector->{$x};
		if (! defined $str) {
		    $x = "# \u$x";
		    $str = "[no default value]";
		    }
		if ($str =~ /\s{2}|#/) {
		    $str = qq("$str");
		    }
		printf "    %-11s %s\n", ucfirst $x, $str;
		}
	    for my $n (sort keys %{ $collector->{sensor} }) {
		next if exists $collector->{actuator}{$n};
		print "    <Sensor $n>\n";
		$sensor = $collector->{sensor}{$n};
		for my $x (sort keys %$sensor) {
		    next if $x eq "alarm" || $x =~ /^_/;
		    $str = $sensor->{$x};
		    if (! defined $str) {
			if ($x eq "adjustby") {
			    $str = "0";
			    }
			else {
			    $x = "# \u$x";
			    $str = "[no default value]";
			    }
			}
		    if ($str =~ /\s{2}|#/) {
			$str = qq("$str");
			}
		    printf "\t%-11s %s\n", ucfirst $x, $str;
		    }
		for my $a (sort keys %{ $sensor->{alarm} }) {
		    print "\t<Alarm $a>\n";
		    $alarm = $sensor->{alarm}{$a};
		    for my $x (sort keys %$alarm) {
			next if $x =~ /^(lock)?(open|close)$/ ||
				$x eq "exec" || $x =~ /^_/;
			if ($x eq "notify") {
			    for (@{$alarm->{notify}}) {
				print "\t    Notify      $_\n";
				}
			    next;
			    }
			$str = $alarm->{$x};
			if ($x =~ /^(above|below|resetat)$/ &&
				$sensor->{_scale} =~ /^[CF]$/) {
			    $str .= $sensor->{_scale};
			    }
			if (! defined $str) {
			    if ($x eq "dates") {
				$str = "[default is all dates]";
				}
			    elsif ($x eq "above" || $x eq "below") {
				$str = "[alarm cannot be both above and below]";
				}
			    elsif ($x eq "times") {
				$str = "[default is all times]";
				}
			    else {
				$str = "[no default value]";
				}
			    $x = "# \u$x";
			    }
			if ($str =~ /\s{2}|#/) {
			    $str = qq("$str");
			    }
			printf "\t    %-11s %s\n", ucfirst $x, $str;
			}
		    for my $e (qw(Open LockOpen Close LockClose Exec)) {
			for my $x (@{ $alarm->{lc($e)} }) {
			    printf "\t    %-9s   $x\n", $e;
			    }
			}
		    print "\t</Alarm>\n";
		    }
		print "    </Sensor>\n";
		}
	    for my $n (sort keys %{ $collector->{actuator} }) {
		print "    <Actuator $n>\n";
		$actuator = $collector->{actuator}{n};
		for my $x (sort keys %$actuator) {
		    next if $x =~ /^_/;
		    $str = $actuator->{$x};
		    if (! defined $str) {
			$x = "# \u$x";
			$str = "[no default value]";
			}
		    if ($str =~ /\s{2}|#/) {
			$str = qq("$str");
			}
		    printf "\t%-11s %s\n", ucfirst $x, $str;
		    }
		print "    </Actuator>\n";
		}
	    print "</Collector>\n\n";
	    }

	for my $v (sort keys %{ $config{_view} }) {
	    print "<View $v>\n";
	    $view = $config{view}{$v};
	    if (defined $view->{rssname}) {
		print "    RSSName\t$view->{rssname}\n";
		}
	    if ($view->{rssorder} != 999) {
		print "    RSSOrder\t$view->{rssorder}\n";
		}
	    if ($view->{buttonorder} != 999) {
		print "    ButtonOrder\t$view->{buttonorder}\n";
		}
	    for my $k (sort keys %{ $config{_view}{$v} }) {
		$_view = $config{_view}{$v}{$k};
		if (grep { ! /^_/ } keys %$_view) {	# not _name, _sensor
		    print "    <Show+\t$_view->{_name}>\n";
		    while (my ($vk, $vv) = each %$_view) {
			next if $vk =~ /^_/;
			print "\t\u$vk\t$vv\n";
			}
		    print "    </Show+>\n";
		    }
		else {
		    print "    Show\t$_view->{_name}\n";
		    }
		}
	    print "</View>\n\n";
	    }
	}
    else {
	warn "Config file OK\n"	unless $warned || $erred;
	}
    }

sub send_test_emails {
    my ($collector, $sensor, $actuator, $alarm, $view, $_view, $str);

    if (! $erred) {
	for my $k (sort keys %{ $config{collector} }) {
	    next if $k eq "WUNDERGROUND/";
	    $collector = $config{collector}{$k};
	    for my $n (sort keys %{ $collector->{sensor} }) {
		$sensor = $collector->{sensor}{$n};
		for my $a (sort keys %{ $sensor->{alarm} }) {
		    $alarm = $sensor->{alarm}{$a};
		    if (@{$alarm->{notify}}) {
			send_alarm_email($sensor, $alarm, 999.9);
			}
		    }
		}
	    }
	}
    else {
	print "\n\nNo test emails attempted due to configuration errors!!\n";
	}
    }

sub check_sesame {
    die decode_base64 << "==END==";
CiEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEh
ISEhISEhISEhISEhISEhISEhISEhIQpXQVJOSU5HOiBVc2Ugb2YgQWN0dWF0b3JzICh3aXRoIE9w
ZW4sIENsb3NlKSwgYW5kL29yIEV4ZWMgSVMgQVQgWU9VUiBPV04gUklTSyEKISEhISEhISEhISEh
ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEh
ISEhISEhISEhCgpOb3RlIHRoYXQgdGhlIGF1dGhvcihzKSBvZiB0aGVybWQgY2Fubm90IGJlIHJl
c3BvbnNpYmxlIGZvciBhbnkgcmVhbCBvcgppbmNpZGVudGFsIGxvc3Mgb3IgZGFtYWdlIG9mIEFO
WSBraW5kIGJ5IHVzaW5nIHRoZXJtZCB0byBjb250cm9sIGFueSBkZXZpY2VzCm9yIHN3aXRjaGVz
IGluIHJlc3BvbnNlIHRvIGFsYXJtIGNvbmRpdGlvbnMuICBBbGwgY29uZmlndXJhdGlvbiwgdGVz
dGluZywKdmVyaWZpY2F0aW9uIGFuZCBndWFyYW50ZWUgb2YgY29udGludWVkIGFuZCBjb3JyZWN0
IG9wZXJhdGlvbiBpcyBZT1VSCnJlc3BvbnNpYmlsdHksIG5vdCBvdXJzLgoKSWYgeW91IGFncmVl
IHdpdGggdGhlIGZvcmVnb2luZyBzdGF0ZW1lbnRzLCBhZGQgdGhlIGZvbGxvd2luZyBsaW5lIHRv
IHRoZQpiZWdpbm5pbmcgb2YgeW91ciBjb25maWd1cmF0aW9uIGZpbGUgdG8gZGlzYWJsZSB0aGlz
IG5vdGljZSBhbmQgdG8gZW5hYmxlCk9wZW4sIENsb3NlLCBhbmQgRXhlYyBmdW5jdGlvbmFsaXR5
OgoKQWNrbm93bGVkZ2UgTm9XYXJyYW50eQkJIyBFbmFibGUgT3BlbiwgQ2xvc2UgYW5kIEV4ZWMg
QWxhcm0gZmVhdHVyZXMKCg==
==END==
    }

#######################################################################
#                      File Handling 
#######################################################################

sub log_open {
    my ($sensor, $lock) = @_;
    my $fd = new FileHandle;
    my $flags = $lock ? (O_RDWR | O_CREAT) : O_RDONLY;
    my $logdir = $lock ? $config{logwrite} : $config{logread};
    my $file = $sensor->{logfile};
    if (substr($file, 0, 1) ne '/') {
	#
	# Make sure all the directories exist if we're writing
	#
	if ($lock) {
	    my $p = index($file, '/');
	    while ($p != -1) {
		my $subdir = substr($file, 0, $p);
		if (-d "$config{logwrite}/$subdir") {
		    unless (-w _) {
			cfg_die "Cannot write LogWrite subdirectory $config{logwrite}/$subdir";
			}
		    }
		else {
		    warn $lh->maketext("WARNING: creating LogWrite subdirectory [_1]", "$config{logwrite}/$subdir");
		    mkdir "$config{logwrite}/$subdir" or cfg_die "Cannot create LogWrite subdirectory $config{logwrite}/$subdir";
		    }
		$p = index($file, '/', $p+1);
		}
	    }
	$file = "$logdir/$file";
	}

    unless ($opt_checkconfig) {
	print "Opening logfile for $file (lock=$lock)\n" if $opt_verbose;
	sysopen($fd, "$file", $flags, 0644) or warn("Can't open $file - $!");
	$fd->autoflush(1);
	if ($lock) {
	    flock($fd, LOCK_EX|LOCK_NB)	or die "Can't lock $file - $!\n";
	    }
	return $fd;
	}
    }

sub close_logfiles {
    #
    # The pollers do not need the logfiles open...
    #
    for my $k (sort keys %{ $config{collector} }) {
	for my $n (sort keys %{ $config{collector}{$k}{sensor} }) {
	    my $sensor = $config{collector}{$k}{sensor}{$n};
	    close $sensor->{_fd};
	    }
	for my $n (sort keys %{ $config{collector}{$k}{actuator} }) {
	    my $actuator = $config{collector}{$k}{actuator}{$n};
	    close $actuator->{_fd};
	    }
	}
    }

sub unit_open {
    my ($collector, $speed) = @_;
    my ($paddr, $remote, $fd);
    if ($collector->{device}) {
	return device_open($collector->{device}, $speed);
	}

    $fd = new FileHandle;
    $remote = inet_aton($collector->{ipaddress})		||
	die "No such host $collector->{ipaddress}";
    socket($fd, PF_INET, SOCK_STREAM, getprotobyname('tcp')) 	||
	die "Can't create socket - $!";
    $paddr = sockaddr_in($collector->{port}, $remote);
    connect($fd, $paddr)	 				||
	warn "Can't connect to $collector->{ipaddress}:$collector->{port} - $!";
    $fd->autoflush(1);
    return $fd
    }

sub device_open {
    my ($device, $speed) = @_;
    ##########################################################################
    #
    # Open the collector, ready for recording...
    #
    # System dependent?
    # 1) Open device non-blocking, but then reset that flag (has to do with
    # 	DTR not being asserted)
    # 2) Set baud rate
    # 3) Ignore CR (the QK145 sends \n\r [not \r\n as expected], so ignore
    #	the \r) and ignore partity errors
    # 4) Enable reader, don't monitor DTR, etc, 8 bits, 2 stop bits
    # 5) Reopen device, blocking
    #
    ##########################################################################
    print "Opening collector on $device\n"	if $opt_verbose;
    my $serial = new FileHandle("$device", O_RDWR | O_NDELAY | O_NOCTTY) ||
	die "  Can't open $device $!";
    print "  Filenumber ", $serial->fileno(), "\n"	if $opt_verbose;

    my $cflag= CS8 | HUPCL | CREAD | CLOCAL;
    my $lflag= 0;
    my $iflag= IGNBRK | IGNPAR;
    my $oflag= 0;
    my $termios = POSIX::Termios->new();

    print "  Getting flags\n"			if $opt_verbose;
    $termios->getattr($serial->fileno())	|| die "getattr: $!\n";

    $termios->setcflag($cflag);
    $termios->setlflag($lflag);
    $termios->setiflag($iflag);
    $termios->setoflag($oflag);

    $termios->setattr($serial->fileno(),TCSANOW) || die "setattr 1: $!\n";
    $termios->setospeed($speed) 		|| die "setospeed: $!\n";
    $termios->setispeed($speed) 		|| die "setispeed: $!\n";
    print "  Setting flags\n"			if $opt_verbose;
    $termios->setattr($serial->fileno(),TCSANOW) || die "setattr 2: $!\n";

    # This gets rid of all the special characters..
    print "  Getting flags (again)\n"		if $opt_verbose;
    $termios->getattr($serial->fileno())	|| die "getattr: $!\n";

    print "  Reopening collector $device\n"	if $opt_verbose;
    my $reopen = new FileHandle("$device", O_RDWR | O_NOCTTY) ||
	die "  Can't re-open $device $!\n";
    print "  Filenumber ", $reopen->fileno(), "\n"	if $opt_verbose;
    close $serial;
    return $reopen;
    }

##############################################################################
#                            Support routines
##############################################################################

sub adjust_for_scale {
    my ($val, $from_scale, $precision) = @_;
    my ($retval, $to_scale);
    if ($from_scale eq 'C') {
	if ($opt_temperature eq 'F') {
	    ($retval, $to_scale) = (($val*9/5) + 32, "\N{U+00b0} F");
	    }
	else {
	    ($retval, $to_scale) = ($val, "\N{U+00b0} $from_scale");
	    }
	}
    elsif ($from_scale eq 'F') {
	if ($opt_temperature eq 'C') {
	    ($retval, $to_scale) = (($val-32) * 5/9, "\N{U+00b0} C");
	    }
	else {
	    ($retval, $to_scale) = ($val, "\N{U+00b0} $from_scale");
	    }
	}
    elsif ($from_scale eq 'MPH' && $opt_windspeed eq 'KPH') {
	($retval, $to_scale) = ($val * 1.609344, "KPH");
	}
    elsif ($from_scale eq 'MPH' && $opt_windspeed eq 'MPS') {
	($retval, $to_scale) = ($val / 2.2369363, "m/sec");
	}
    elsif ($from_scale eq 'MPH' && $opt_windspeed =~ /KNOTS?/) {
	($retval, $to_scale) = ($val / 1.1507794, "Knot");
	}
    elsif ($from_scale =~ /Inch(es)?/) {
	if ($opt_rainfall eq 'MM') {
	    ($retval, $to_scale) = ($val * 25.4, "mm");
	    }
	else {
	    ($retval, $to_scale) = ($val, q("));
	    }
	}
    elsif ($from_scale =~ /inHg/ && $opt_barometer eq 'MMHG') {
	($retval, $to_scale) = ($val * 25.4, "mmHg");
	}
    elsif ($from_scale =~ /inHg/ && $opt_barometer =~ /(MBAR|MILLIBAR)/) {
	($retval, $to_scale) = ($val * 33.863788, "mBar");
	}
    elsif ($from_scale =~ /inHg/ && $opt_barometer eq 'HPA') {
	($retval, $to_scale) = ($val * 33.863788, "hPa");
	}
    elsif ($from_scale =~ /inHg/ && $opt_barometer eq 'KPA') {
	($retval, $to_scale) = ($val * 3.3863788, "kPa");
	}
    # Three cheats for Newport/Omega instruments
    elsif ($from_scale =~ /kPa/ && $opt_barometer eq 'MMHG') {		# Cheat
	($retval, $to_scale) = ($val * 1.0197E-2, "kg/cm^2");
	}
    elsif ($from_scale =~ /kPa/ && $opt_barometer eq 'INHG') {		# Cheat
	($retval, $to_scale) = ($val * 145.04E-3, "PSI");
	}
    elsif ($from_scale =~ /kPa/ && $opt_barometer eq 'HPA') {		# Cheat
	($retval, $to_scale) = ($val * 10, "hPa");
	}
    elsif ($from_scale =~ /%/) {
	($retval, $to_scale) = ($val, "% RH");
	}
    # Don't adjust wind here - this is used for ALL values, we only want to
    # do adjusting after averaging, etc.
    #elsif ($from_scale eq 'Deg') {
    #	($retval, $to_scale) = (wind_direction_str($val),$lh->maketext('wind'));
    #	}
    else {
	($retval, $to_scale) = @_;	# Otherwise, failsafe with GIGO
	}

    if (wantarray && $precision >= 0) {
	return (sprintf("%.*f", $precision, $retval), $to_scale);
	}
    else {
	unless ($retval =~ /[a-zA-Z]/) {	# Wind, and maybe others...
	    $retval = sprintf("%.*f", abs($precision), $retval);
	    }
	$to_scale = " $to_scale"	if $to_scale =~ /^[a-zA-Z]/;
	if ($precision >= 0) {
	    return "$retval$to_scale";
	    }
	else {
	    return $retval;
	    }
	}
    }

sub sign {
    shift() < 0 ? -1 : 1
    }

sub min {
    $_[0] < $_[1] ? $_[0] : $_[1]
    }

sub max {
    $_[0] > $_[1] ? $_[0] : $_[1]
    }

# You are not expected to understand this, mainly because unless I work hard
# at it, I don't understand it either :-)  But I *do* trust it.  It returns a
# regular expression that evaluates whether the purported email address is
# indeed RFC822 compliant, and is a compressed version of that found in
# Jeffrey Friedl's excellent book "Mastering Regular Expressions" by O'Reilly
# The version below derives http://examples.oreilly.com/regex/email-opt.pl

sub email_regex {
    my $esc        = '\\\\';	my $Period      = '\.';
    my $space      = '\040';	my $tab         = '\t';
    my $OpenBR     = '\[';		my $CloseBR     = '\]';
    my $OpenPar  = '\(';		my $ClosePar  = '\)';
    my $NonASCII   = '\x80-\xff';	my $ctrl        = '\000-\037';
    my $CRlist     = '\n\015';  
    my $qtext=qq/[^$esc$NonASCII$CRlist\"]/;
    my $dtext=qq/[^$esc$NonASCII$CRlist$OpenBR$CloseBR]/;
    my $quoted_pair=qq<${esc}[^$NonASCII]>;
    my $ctext=qq<[^$esc$NonASCII$CRlist()]>;
    my $Cnested=qq<$OpenPar$ctext*(?:$quoted_pair$ctext*)*$ClosePar>;
    my $comment=qq<$OpenPar$ctext*(?:(?:$quoted_pair|$Cnested)$ctext*)*$ClosePar>;
    my $X=qq<[$space$tab]*(?:${comment}[$space$tab]*)*>;
    my $atom_char=qq/[^($space)<>\@,;:\".$esc$OpenBR$CloseBR$ctrl$NonASCII]/;
    my $atom=qq<$atom_char+(?!$atom_char)>;
    my $quoted_str=qq<\"$qtext*(?:$quoted_pair$qtext*)*\">;
    my $word=qq<(?:$atom|$quoted_str)>;
    my $domain_ref=$atom;
    my $domain_lit=qq<$OpenBR(?:$dtext|$quoted_pair)*$CloseBR>;
    my $sub_domain=qq<(?:$domain_ref|$domain_lit)$X>;
    my $domain=qq<$sub_domain(?:$Period$X$sub_domain)*>;
    my $route=qq<\@$X$domain(?:,$X\@$X$domain)*:$X>;
    my $local_part=qq<$word$X(?:$Period$X$word$X)*>;
    my $addr_spec=qq<$local_part\@$X$domain>;
    my $route_addr=qq[<$X(?:$route)?$addr_spec>];
    my $phrase_ctrl='\000-\010\012-\037';
    my $phrase_char=qq/[^()<>\@,;:\".$esc$OpenBR$CloseBR$NonASCII$phrase_ctrl]/;
    my $phrase=qq<$word$phrase_char*(?:(?:$comment|$quoted_str)$phrase_char*)*>;

    return qr<$X(?:$addr_spec|$phrase$route_addr)>;
    }

sub check_alarm {
    my ($k, $n, $val, $timestr) = @_;
    my ($hr, $day) = split /\s+/, $timestr;

    for my $a (keys %{ $config{collector}{$k}{sensor}{$n}{alarm} }) {
	my $alarm = $config{collector}{$k}{sensor}{$n}{alarm}{$a};
	if ($alarm->{dates}) {
	    if ($alarm->{_date_reverse}) {
		next unless $day >= $alarm->{_date_from} ||
			    $day <= $alarm->{_date_to};
		}
	    else {
		next if	    $day < $alarm->{_date_from} ||
			    $day > $alarm->{_date_to};
		}
	    }
	if ($alarm->{times}) {
	    if ($alarm->{_time_reverse}) {
		next unless $hr >= $alarm->{_time_from} ||
			    $hr <= $alarm->{_time_to};
		}
	    else {
		next if	    $hr < $alarm->{_time_from} ||
			    $hr > $alarm->{_time_to};
		}
	    }
	if (defined $alarm->{above}) {
	    if (!$alarm->{_triggered} && $val > $alarm->{above}) {
		$alarm->{_triggered} = trigger_alarm($k, $n, $a, $val);
		}
	    elsif ($alarm->{_triggered} && $val < $alarm->{resetat}) {
		$alarm->{_triggered} = reset_alarm($k, $n, $a, $val);
		}
	    }
	elsif (defined $alarm->{below}) {
	    if (!$alarm->{_triggered} && $val < $alarm->{below}) {
		$alarm->{_triggered} = trigger_alarm($k, $n, $a, $val);
		}
	    elsif ($alarm->{_triggered} && $val > $alarm->{resetat}) {
		$alarm->{_triggered} = reset_alarm($k, $n, $a, $val);
		}
	    }
	}
    }

sub trigger_alarm {
    my ($k, $n, $a, $val) = @_;
    my $sensor = $config{collector}{$k}{sensor}{$n};
    my $alarm = $sensor->{alarm}{$a};
    my ($status);

    print "Alarm $alarm->{_nka} has been triggered!\n"	if $opt_verbose;
    if ($alarm->{syslog}) {
	print "\tFiling alarm with syslog\n"	if $opt_verbose;
	msg($alarm->{syslog}, $alarm->{message}, 
	    $sensor->{name}, $val, $sensor->{_scale});
	$status++;
	}
    if ($alarm->{notify}) {
	print "\tSending alarm with email\n"	if $opt_verbose;
	$status += send_alarm_email($sensor, $alarm, $val);
	}
    for my $activate (@{ $alarm->{_activate} }) {
	print "\tToggling actuator for alarm\n"	if $opt_verbose;
	$status += $activate->();
	}
    return $status;		# We sent it!
    }

sub send_alarm_email {
    my ($sensor, $alarm, $val) = @_;
    local $opt_verbose = $opt_email || $opt_verbose;
    my $email = <<"==END==";
To:      @{[ join(", ", @{$alarm->{notify}}) ]};
From:    $config{mailfrom}
Subject: $alarm->{subject}

@{[ sprintf $alarm->{message}, $sensor->{name}, $val, $sensor->{_scale} ]}
==END==

    print "   @ Emailing @{$alarm->{notify}} for Sensor $sensor->{name}\n"
	if $opt_verbose;
    if ($config{smtphost}) {
	eval qq{ use Net::SMTP; };
	if ($@) {
	    msg("crit", $@);		# We don't want to die for this
	    return 0;
	    }
	my $smtp = Net::SMTP->new($config{smtphost});
	unless ($smtp) {
	    msg("crit", "Couldn't connect to $config{smtphost} to send email!");
	    return 0;
	    }
	if ($config{smtpusername}) {
	    msg("err", "Email authentication failed")
		unless $smtp->auth($config{smtpusername},$config{smtppassword});
	    }
	$smtp->mail($config{mailfrom});
	$smtp->recipient(@{$alarm->{notify}}, { SkipBad => 1 });
	$smtp->data();
	$smtp->datasend($email);
	$smtp->dataend();
	$smtp->quit();
	}
    else {
	open MAIL, "|-", "$config{sendmail} -oi -t"	or return 0;
	print MAIL $email;
	close MAIL;
	return 1;
	}
}

sub reset_alarm {
    my ($k, $n, $a, $val) = @_;
    my $sensor = $config{collector}{$k}{sensor}{$n};
    my $alarm = $sensor->{alarm}{$a};
    my ($status);

    print "Alarm $n\@$k:$a has reset $val >= $alarm->{resetat}!\n"
	if $opt_verbose;
    if ($alarm->{syslog}) {
	msg("notice", "Alarm $n\@$k:$a has reset %.3f > %.3f",
	    $val, $alarm->{resetat});
	}
    for my $reset (@{ $alarm->{_reset} }) {
	$reset->();
	}
    return 0;
    }

sub send_to_wunderground {
    my $name = shift;
    my $view = $config{view}{$name};
    my $_view = $config{_view}{$name};
    my ($request, $response, @gmt);
    # Locally override the global values, wunderground wants English
    local $opt_temperature = 'F';
    local $opt_windspeed = 'MPH';

    print "Generating Wunderground feed for <View $view->{_name}>\n"
	if $opt_verbose;
    #
    # See http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol
    #
    $request = "http://weatherstation.wunderground.com/weatherstation/updateweatherstation.php?action=updateraw";
    $request .= "&ID=$view->{stationid}&PASSWORD=$view->{password}";
    $request .= "&softwaretype=thermd";
    @gmt = (gmtime)[0..5];
    $gmt[4]++;		# month
    $gmt[5] += 1900;	# year
    $request .= sprintf "&dateutc=%4d-%02d-%02d+%02d:%02d:%02d", reverse @gmt;
    for my $show (keys %$_view) {
	my $sensor = $_view->{$show}{_sensor};
	my $key = $_view->{$show}{key};
	next unless defined $sensor->{_last};
	if ($sensor->{_scale} eq 'Deg') {
	    $request .= "&$key=$sensor->{_last}";
	    }
	else {
	    # Locally override the global values, wunderground wants English
	    local $opt_temperature = 'F';
	    local $opt_windspeed = 'MPH';
	    local $opt_barometer = 'INHG';
	    local $opt_rainfall = 'INCH';
	    $request .= sprintf "&$key=%s",
		adjust_for_scale($sensor->{_last}, $sensor->{_scale}, 3);
	    }
	}
    print "Sending $request\n"	if $opt_verbose;
    $response = $view->{_ua}->get($request);
    if ($response->is_error) {
	msg("err", "Cannot contact Wunderground at $request, " .
	    $response->status_line);
	}
    else {
	chomp($response = $response->content);
	if ($response ne "success") {
	    msg("err", "Got '$response' when contacting $request");
	    }
	}
    }

sub generate_rss {
    my $tz = $config{timezone};
    my ($now_g, $now_l, $howoften);

    print "Generating RSS\n"	if $opt_verbose;
    if (length $tz == 3 && (localtime)[8]) {
	substr($tz, 1, 1) = 'D';
	}
    # The GMT string is used for refresh fields, and should be in English.
    # The local time string is displayed as text, and is internationalized
    $now_g = sprintf "%s, %02d %s %4d %s GMT", (split /\s+/, gmtime)[0,2,1,4,3];
    $now_l = strftime "%a, %d %b %Y %R %Z", localtime;
    $howoften = ($config{loginterval} / 60) * $config{rss}{every};
    for my $view (keys %{ $config{_view} }) {
	next unless $config{view}{$view}{rssname};
	if ($config{view}{$view}{type} eq "graph") {
	    system "nice $config{rss}{nice} $0 -graph -nowarn -config $opt_config -from -28h -view $view -outfile $config{rss}{_dir}/$view.day.png.new";
	    system "nice $config{rss}{nice} $0 -graph -nowarn -config $opt_config -from -1w -view $view -outfile $config{rss}{_dir}/$view.week.png.new";
	    system "nice $config{rss}{nice} $0 -graph -nowarn -config $opt_config -from -1m -view $view -outfile $config{rss}{_dir}/$view.month.png.new";
	    }
	elsif ($config{view}{$view}{type} eq "image") {
	    system "nice $config{rss}{nice} $0 -annotate -nowarn -config $opt_config -view $view -outfile $config{rss}{_dir}/$view.annotated.png.new";
	    }
	elsif ($config{view}{$view}{type} eq "wunderground") {
	    # Nothing - handled elsewhere
	    }
	}
    unless (open(RSS, ">", "$config{rss}{_dir}/index.xml.new")) {
	msg("err", "Can't open RSS feed at $config{rss}{_dir}/index.xml.new");
	return;
	}
    print RSS <<"==END==";
<?xml version="1.0" encoding="ISO-8859-1"?>
<rss version="2.0">
<channel>
    <title>Thermd - $config{location}</title>
    <link>$config{rss}{url}</link>
    <description>@{[$config{location} ?
	$lh->maketext("The environment in and around [_1]", $config{location}) :
	$lh->maketext("Temperature and environment")]}</description>
    <lastBuildDate>$now_g</lastBuildDate>
    <ttl>$howoften</ttl>
    <generator>$script V@{[(split(/\s+/, VERSION))[2,3]]}</generator>
    <webMaster>$config{rss}{webmaster}</webMaster>
    <copyright>Copyright 2001-2009 Daniel V. Klein</copyright>
    <item>
	<title>@{[$lh->maketext("Latest Readings")]}</title>
	<link>$config{rss}{url}</link>
	<pubDate>$now_g</pubDate>
	<description>
	    <![CDATA[<b>$now_l</b><br>
==END==
    for my $k (sort keys %{ $config{collector} }) {
	for my $n (sort keys %{ $config{collector}{$k}{sensor} }) {
	    my $sensor = $config{collector}{$k}{sensor}{$n};
	    my $str = adjust_for_scale($sensor->{_last}, $sensor->{_scale}, 3);
	    print RSS "$sensor->{name}: $str<br>\n";
	    }
	}
    print RSS <<"==END==";
	    <small>@{[$lh->maketext("Data is refreshed every [_1] minutes", $howoften)]}<br>
		<a href="$config{mapurl}">$config{gpscoordinates}</a></small>
	    ]]>
	</description>
    </item>
==END==
    for my $view (sort {
	    $config{view}{$a}{rssorder} <=> $config{view}{$b}{rssorder} ||
	    $config{view}{$a}{rssname} cmp $config{view}{$b}{rssname}
	    } keys %{ $config{_view} }) {
	next unless defined $config{view}{$view}{rssname};
	if ($config{view}{$view}{type} eq "graph") {
	    print RSS <<"==END==";
    <item>
	<title>$config{view}{$view}{rssname}</title>
	<link>$config{rss}{url}</link>
	<pubDate>$now_g</pubDate>
	<description>
	    <![CDATA[<b>$now_l</b><br>
		<img src="$config{rss}{url}/$view.day.png" height=$opt_height>
		<p>
		<img src="$config{rss}{url}/$view.week.png" height=$opt_height>
		<p>
		<img src="$config{rss}{url}/$view.month.png" height=$opt_height>
		<p>
		<small>@{[$lh->maketext("Graphs are updated every [_1] minutes", $howoften)]}<br>
		    <a href="$config{mapurl}">$config{gpscoordinates}</a></small>
	    ]]>
	</description>
    </item>
==END==
	    }
	elsif ($config{view}{$view}{type} eq "image") {
	    print RSS <<"==END==";
    <item>
	<title>$config{view}{$view}{rssname}</title>
	<link>$config{rss}{url}</link>
	<pubDate>$now_g</pubDate>
	<description>
	    <![CDATA[<b>$now_l</b><br>
		<img src="$config{rss}{url}/$view.annotated.png" height=$opt_height>
		<p>
		<small>@{[$lh->maketext("Graphs are updated every [_1] minutes", $howoften)]}<br>
		    <a href="$config{mapurl}">$config{gpscoordinates}</a></small>
	    ]]>
	</description>
    </item>
==END==
	    }
	elsif ($config{view}{$view}{type} eq "wunderground") {
	    # Nothing - handled elsewhere
	    }
	}
    print RSS <<"==END==";
</channel>
</rss>
==END==
    close RSS;
    for my $view (keys %{ $config{_view} }) {
	next unless defined $config{view}{$view}{rssname};
	if ($config{view}{$view}{type} eq "graph") {
	    rename "$config{rss}{_dir}/$view.day.png.new", "$config{rss}{_dir}/$view.day.png";
	    rename "$config{rss}{_dir}/$view.week.png.new", "$config{rss}{_dir}/$view.week.png";
	    rename "$config{rss}{_dir}/$view.month.png.new", "$config{rss}{_dir}/$view.month.png";
	    }
	elsif ($config{view}{$view}{type} eq "image") {
	    rename "$config{rss}{_dir}/$view.annotated.png.new", "$config{rss}{_dir}/$view.annotated.png";
	    }
	elsif ($config{view}{$view}{type} eq "wunderground") {
	    # Nothing - handled elsewhere
	    }
	}
    rename "$config{rss}{_dir}/index.xml.new", "$config{rss}{_dir}/index.xml";
    }

sub msg {
	my ($level, @message) = @_;

	if ($opt_verbose || $ENV{REQUEST_METHOD}) {
	    if ($is_child || $ENV{REQUEST_METHOD}) {
		printf STDERR "$message[0]\n", @message[1..$#message];
		}
	    else {
		printf "$message[0]\n", @message[1..$#message];
		}
	    }
	unless ($^O eq "MSWin32") {
	    syslog($level, @message);
	    }
    }

##############################################################################
# Private version of LWP::UserAgent, to get generic authentication to work
##############################################################################
{
    package My::LWP::UserAgent;
    our @ISA = qw(LWP::UserAgent);

    sub new	# Takes collector, followed by usual parameters
    {
	my $class = shift;
	my $collector = shift;
	eval qq{ use LWP::UserAgent; };
	die $@ if $@;
        my $ua = new LWP::UserAgent;
	$ua->agent("$main::script/@{[(split(/\s+/, main::VERSION()))[2,3]]})");
	$ua->{THERMD_username} = $collector->{username};	# May be undef
	$ua->{THERMD_password} = $collector->{password};	# May be undef
        
	return $ua;
    }

    # Callback routine, used if auth is required
    sub get_basic_credentials
    {
        my($self, $realm, $uri) = @_;
        return ($self->{THERMD_username}, $self->{THERMD_password});
    }
}


##############################################################################
#                            Temporary Repository
#
#	I need to rework this as a proper module (it was a module called
# GD::Chart::Radial, and a good start.  But it really needs to be a module
# called GD::Graph::radial, and I have to work out a lot more bugs that I
# have already found, and I wanted to get a release with all the new features
# andf support that I had already worked in, so this cruft stays here until
# the "next release"...
#
##############################################################################

sub get_radial_code {
    eval {
#####################################################################
# Radial - A module to generate radial charts as JPG and PNG images #
# (c) Copyright 2002,2004 Aaron J  Trevena                          #
# (c) Copyright 2007 Daniel V. Klein
#####################################################################
package GD::Chart::Radial;

use strict;
use warnings;
use GD;

our $VERSION = 0.02;

sub new {
  my ($class, $width, $height, $debug) = (@_,0);

  # instantiate Chart
  my $Chart = bless({}, ref($class) || $class);

  # initialise Chart
  $Chart->{debug} = $debug;
  $Chart->{PI} = 4 * atan2 1, 1;
  return $Chart;
}

sub set {
  my $self = shift;
  my %attributes = @_;
  my ($k, $v);
  while (($k, $v) = each %attributes) {
    $self->{$k} = $v;
  }
  return $self;
}

sub set_legend {
  my $self = shift;
  $self->{legend} = [ @_ ];
  return $self;
}

sub plot {
  my $self = shift;
  my @values = @{shift(@_)};
  my @labels = @{shift(@values)};
  my @records;
  my $r = 0;
  my @colours = qw/red blue green/;
  foreach my $values (@values) {
    my $record;
    my $colour = shift @colours;  push @colours, $colour;
    $record = { Label => $self->{legend}->[$r], Colour => $colour, };
    my $v = 0;
    foreach my $value (@$values) {
      $record->{Values}->{$labels[$v]} = $value;
      $v++;
    }
    push(@records,$record);
    $r++;
  }

  $self->{records} = \@records;

  my $PI = $self->{PI};
  my $title = $self->{title};
  # style can be circle, polygon or notch
  my %scale = (
	       Max=>$self->{y_max_value},
	       Divisions=>$self->{y_tick_number},
	       Style=>(lc($self->{style}) || "circle"),
	       Colour => "light_grey"
	      );

  my (@axis, %axis_lookup);
  my $longest_axis_label = 0;
  my $a = 0;
  foreach my $key (@labels) {
    push (@axis, { Label => "$key", });
    $axis_lookup{$key} = $a;
    $longest_axis_label = length $key
      if (length $key > $longest_axis_label);
    $a++;
  }

  my $number_of_axis = scalar @axis;
  my $legend_height = 15 * scalar @{$self->{records}};

  my $left_space = 15 + $longest_axis_label * 6;
  my $right_space = 15 + $longest_axis_label * 6;
  my $top_space = 50;
  my $bottom_space = 30 + $legend_height;
  my $max_length = 100;

  my $x_centre = $left_space + $max_length;
  my $y_centre = $top_space + $max_length;
  my $height = (2 * $max_length) + $bottom_space + $top_space;
  my $width  = (2 * $max_length) + $left_space + $right_space;

  $self->{_im} = new GD::Image($width,$height);

  my %colours = (
		 white => $self->{_im}->colorAllocate(255,255,255),
		 black => $self->{_im}->colorAllocate(0,0,0),
		 red => $self->{_im}->colorAllocate(255,0,0),
		 blue => $self->{_im}->colorAllocate(0,0,255),
		 purple => $self->{_im}->colorAllocate(230,0,230),
		 green => $self->{_im}->colorAllocate(0,255,0),
		 grey => $self->{_im}->colorAllocate(128,128,128),
		 light_grey => $self->{_im}->colorAllocate(170,170,170),
		 dark_grey => $self->{_im}->colorAllocate(75,75,75),
		 cream => $self->{_im}->colorAllocate(200,200,240),
		);

  $self->{colours} = \%colours;

  my @shape_subs = (
		    \&draw_triangle,
		    \&draw_circle,
		    \&draw_square,
		    \&draw_diamond,
		   );

  my $i=0;
  foreach my $axis (@axis) {
    my $proportion;
    my $theta;
    my $x;
    my $y;
    if ($i > 0) {
      $proportion = $i / $number_of_axis;
      $theta = 360 * $proportion + $self->{start_angle};
    } else {
      $theta = $self->{start_angle};
    }
    $axis->{theta} = $theta;
    $theta *= ((2 * $PI) / 360);
    $x = cos $theta - (2 * $theta);
    $y = sin $theta - (2 * $theta);
    my $x_outer = ($x * 100) + $x_centre;
    my $x_proportion =  ($x >= 0) ? $x : $x - (2 * $x) ;
    my $x_label = ($x_outer >= $x_centre) ?
      $x_outer + 3 : $x_outer - ((length ( $axis->{Label} ) * 5) + (3 * $x_proportion));
    my $y_outer = ($y * 100) + $y_centre;
    my $y_proportion =  ($y >= 0) ? $y : $y - (2 * $y) ;
    my $y_label = ($y_outer >= $y_centre) ? $y_outer + (3 * $y_proportion) : $y_outer - (9 * $y_proportion);

    $axis->{X} = $x;
    $axis->{Y} = $y;

    # round down coords
    $x_outer = int($x_outer);
    $y_outer = int($y_outer);
    $x_label = int($x_label);
    $y_label = int($y_label);

    # draw axis
    $self->{_im}->line($x_outer,$y_outer,$x_centre,$y_centre,$colours{black});
    # add label for axis
    $self->{_im}->string(gdTinyFont,$x_label, $y_label, $axis->{Label}, $colours{dark_grey});      
    $i++;
  }

  # loop through adding scale, and values

  $r = 0;
  $i = 0;
  foreach my $axis (@axis) {
    my $x = $axis->{X};
    my $y = $axis->{Y};
    # draw scale
    my ($theta1, $theta2, $x_interval, $y_interval, $x1, $y1, $x2, $y2,
	$x1_outer, $y1_outer, $x2_outer, $y2_outer);
    if ($scale{Style} eq "notch")  {
      $theta1 = $axis->{theta} + 90;
      $theta2 = $axis->{theta} - 90;
      # convert theta to radians
      $theta1 *= ((2 * $PI) / 360);
      $theta2 *= ((2 * $PI) / 360);
      for (my $j = 0 ; $j <= $scale{Max} ; $j+= int($scale{Max} / $scale{Divisions})) {
	next if ($j == 0);
	$x_interval = $x_centre + ($x * (100 / $scale{Max}) * $j);
	$y_interval = $y_centre + ($y * (100 / $scale{Max}) * $j);
	$x1 = cos $theta1 - (2 *$theta1);
	$y1 = sin $theta1 - (2 * $theta1);
	$x2 = cos $theta2 - (2 *$theta2);
	$y2 = sin $theta2 - (2 * $theta2);
	$x1_outer = ($x1 * 3 * ($j / $scale{Max})) + $x_interval;
	$y1_outer = ($y1 * 3 * ($j / $scale{Max})) + $y_interval;
	$x2_outer = ($x2 * 3 * ($j / $scale{Max})) + $x_interval;
	$y2_outer = ($y2 * 3 * ($j / $scale{Max})) + $y_interval;
	$self->{_im}->line($x1_outer,$y1_outer,$x_interval,$y_interval,$colours{$scale{Colour}});
	$self->{_im}->line($x2_outer,$y2_outer,$x_interval,$y_interval,$colours{$scale{Colour}});
	# Add Numbers to scale
	if ($i == 0) {
	  $self->{_im}->string(gdTinyFont,$x_interval + 2 ,$y_interval - 11 ,$j,$colours{$scale{Colour}});
	}
        }
    }
    elsif ($scale{Style} eq "polygon")  {
      my ($x_interval_1, $y_interval_1, $x_interval_2, $y_interval_2);
      for (my $j = 0 ; $j <= $scale{Max} ; $j+=($scale{Max} / $scale{Divisions})) {
	next if ($j == 0);
	$x_interval_1 = $x_centre + ($x * (100 / $scale{Max}) * $j);
	$y_interval_1= $y_centre + ($y * (100 / $scale{Max}) * $j);
	$x_interval_2 = $x_centre + ($axis[$i-1]->{X} * (100 / $scale{Max}) * $j);
	$y_interval_2= $y_centre + ($axis[$i-1]->{Y} * (100 / $scale{Max}) * $j);
	# Add Numbers to scale
	if ($i == 0) {
	  $self->{_im}->string(gdTinyFont,$x_interval_1 + 2 ,$y_interval_1 - 11 ,$j,$colours{$scale{Colour}});
	} else {
	  $self->{_im}->line($x_interval_1,$y_interval_1,$x_interval_2,$y_interval_2,$colours{$scale{Colour}});
	  if ($i == $number_of_axis -1) {
	    my $x_interval_2 = $x_centre + ($axis[0]->{X} * (100 / $scale{Max}) * $j);
	    my $y_interval_2= $y_centre + ($axis[0]->{Y} * (100 / $scale{Max}) * $j);
	    $self->{_im}->line($x_interval_1,$y_interval_1,$x_interval_2,$y_interval_2,$colours{$scale{Colour}});
	  }
	}
      }
    }
    elsif ($scale{Style} eq "circle")  {
      for (my $j = 0 ; $j <= $scale{Max} ; $j+=($scale{Max} / $scale{Divisions})) {
	if ($i == 0) {
	  my $x_interval = $x_centre + ($x * (100 / $scale{Max}) * $j);
	  my $y_interval = $y_centre + ($y * (100 / $scale{Max}) * $j);next if ($j == 0);
	  $self->{_im}->string(gdTinyFont,$x_interval +2 ,$y_interval - 11 ,$j,$colours{$scale{Colour}});
	} else {
	  my $radius = (200 / $scale{Max}) * $j;
	  $self->{_im}->arc($x_centre,$y_centre,$radius,$radius,$axis->{theta}-2,$axis[$i-1]->{theta}-2,$colours{$scale{Colour}});
	  $self->{_im}->arc($x_centre,$y_centre,$radius,$radius,$axis->{theta}+2,$axis[0]->{theta}-2,$colours{$scale{Colour}}) if ($i == $number_of_axis);
	}
      }
    }
    else { die "style must be one of 'notch', 'polygon', or 'circle'"; }

    # draw value
    if ($i != 0) {
      my $r = 0;
      foreach my $record (@{$self->{records}}) {
	my $value = $record->{Values}->{$axis->{Label}};
	my $last_value = $record->{Values}->{$axis[$i-1]->{Label}};
	my $colour = $colours{$record->{Colour}};
	my $x_interval_1 = $x_centre + ($x * (100 / $scale{Max}) * $value);
	my $y_interval_1= $y_centre + ($y * (100 / $scale{Max}) * $value);
	my $shape = $record->{Shape};
	$self->draw_shape($x_interval_1,$y_interval_1,$record->{Colour}, $r);
	my $x_interval_2 = $x_centre + ($axis[$i-1]->{X} * (100 / $scale{Max}) * $last_value);
	my $y_interval_2= $y_centre + ($axis[$i-1]->{Y} * (100 / $scale{Max}) * $last_value);
	$self->{_im}->line($x_interval_1,$y_interval_1,$x_interval_2,$y_interval_2,$colour);
	if ($i == $number_of_axis -1) {
	  my $first_value = $record->{Values}->{$axis[0]->{Label}};
	  my $x_interval_2 = $x_centre + ($axis[0]->{X} * (100 / $scale{Max}) * $first_value);
	  my $y_interval_2= $y_centre + ($axis[0]->{Y} * (100 / $scale{Max}) * $first_value);
	  $self->{_im}->line($x_interval_1,$y_interval_1,$x_interval_2,$y_interval_2,$colour);
	  $self->draw_shape($x_interval_2,$y_interval_2,$record->{Colour}, $r);
	}
	$r++;
      }
    } else {
    }
    $i++;
  }

  # draw Legend
  my $longest_legend = 0;
  foreach my $record (@{$self->{records}}) {
     $longest_legend = length $record->{Label} if ( length $record->{Label} > $longest_legend );
     }
  my ($legendX, $legendY) = ( ($width / 2), ($height - ($legend_height + 10)) );

  my $legendX2 = $legendX - (($longest_legend * 5) + 2);
  $legendY += 15;
  $r = 0;
  foreach my $record (@{$self->{records}}) {
    my $colour = $record->{Colour};
    $self->{_im}->string(gdTinyFont,$legendX2,$legendY,$record->{Label},$colours{$colour});
    $self->{_im}->line($legendX+10,$legendY+4,$legendX + 35,$legendY+4,$colours{$colour});
    $self->draw_shape($legendX+22,$legendY+4,$record->{Colour}, $r);
    $legendY += 15;
    $r++;
  }

  # draw title
  my ($titleX, $titleY) = ( ($width / 2) - (6 * (length $title) / 2) ,20);
  $self->{_im}->string(gdSmallFont,$titleX,$titleY,$title,$colours{dark_grey});
  return $self;
}

sub png {
  my $self = shift;
  return $self->{_im}->png();
}

sub jpg {
  my $self = shift;
  return $self->{_im}->jpeg(95);
}

##########################################################

sub draw_shape {
    my ($self,$x,$y,$colour,$i) = @_;
    my $shape;
    if (exists $self->{records}->[$i]->{Shape} ) {
        $shape = $self->{records}->[$i]->{Shape};
    } else {
        $shape = ($i > 3) ? int ($i / ($i / 4))  : $i ;
        $self->{records}->[$i]->{Shape} = $shape;
    }
    if ($shape == 0) {
        $self->draw_diamond($x,$y,$colour);
        return 1;
    }
    if ($shape == 1) {
        $self->draw_square($x,$y,$colour);
        return 1;
    }
    if ($shape == 2) {
        $self->draw_circle($x,$y,$colour);
        return 1;
    }
    if ($shape == 3) {
        $self->draw_triangle($x,$y,$colour);
        return 1;
    }
}

sub draw_diamond {
    my ($self,$x,$y,$colour) = @_;
    $x-=3;
    my $poly = new GD::Polygon;
    $poly->addPt($x,$y);
    $poly->addPt($x+3,$y-3);
    $poly->addPt($x+6,$y);
    $poly->addPt($x+3,$y+3);
    $poly->addPt($x,$y);
    $self->{_im}->filledPolygon($poly,$self->{colours}->{$colour});
    return 1;
}

sub draw_square {
    my ($self,$x,$y,$colour) = @_;
    $x-=3;
    $y-=3;
    my $poly = new GD::Polygon;
    $poly->addPt($x,$y);
    $poly->addPt($x+6,$y);
    $poly->addPt($x+6,$y+6);
    $poly->addPt($x,$y+6);
    $poly->addPt($x,$y);
    $self->{_im}->filledPolygon($poly,$self->{colours}->{$colour});
    return 1;
}

sub draw_circle {
    my ($self,$x,$y,$colour) = @_;
    $self->{_im}->arc($x,$y,7,7,0,360,$self->{colours}->{$colour});
    $self->{_im}->fillToBorder($x,$y,$self->{colours}->{$colour},$self->{colours}->{$colour});
    return 1;
}

sub draw_triangle {
    my ($self,$x,$y,$colour) = @_;
    $x-=3;
    $y+=3;
    my $poly = new GD::Polygon;
    $poly->addPt($x,$y);
    $poly->addPt($x+3,$y-6);
    $poly->addPt($x+6,$y);
    $poly->addPt($x,$y);
    $self->{_im}->filledPolygon($poly,$self->{colours}->{$colour});
    return 1;
}

sub draw_cross {
    warn "not drawing crosses yet!\n";
    return 1;
}
}
}

##############################################################################
# Localization and Internationalization stuff.  Weird but powerful voodoo.
##############################################################################
sub check_i18n {
    my @default_keys = sort keys %Thermd::I18N::language_template::Lexicon;
    for my $lg (sort keys %Thermd::I18N::) {
	next unless $lg =~ s/::$//;
	next if $lg eq "en" || $lg eq "i_default" || $lg eq "language_template";
	if (length($opt_i18n)) {
	    next unless $lg eq $opt_i18n;
	    }
	print "\n\nEncoding: $lg\n=====================\n";
	no strict 'refs';
	my $lang = \%{"Thermd::I18N::${lg}::Lexicon"};
	use strict 'refs';
	my (%empty, %missing, @keys);
	for my $key (@default_keys) {
	    if (defined $lang->{$key}) {
		$empty{$key}++ unless $lang->{$key} =~ /\S/;
		delete $lang->{$key};
		}
	    else {
		$missing{$key}++;
		}
	    }
	if (keys %empty) {
	    chomp(@keys = sort keys %empty);
	    print join("\n",
		"-------------------------",
		"Empty translations (BAD!)",
		"-------------------------",
		@keys), "\n";
	    }
	if (keys %missing) {
	    chomp(@keys = sort keys %missing);
	    print join("\n",
		"-------------------------",
		"Missing translations (OK)",
		"-------------------------",
		@keys), "\n";
	    }
	if (keys %$lang) {
	    chomp(@keys = sort keys %$lang);
	    print join("\n",
		"-------------------------",
		"Extra translations (WTF?)",
		"-------------------------",
		@keys), "\n";
	    }
	}
    }

package Thermd::I18N;
use base qw(Locale::Maketext);
use vars qw(%Lexicon);

sub locale { "en_US.ISO8859-1" }
sub charset { my $cs = $lh->locale(); $cs =~ s/.*\.//; return $cs; }
sub scale { "Metric" }
sub baroscale { "hPa" }

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    "_AUTO"		=> 1,
    );
}	# Only needed because this is not a separate module file

##############################################################################

package Thermd::I18N::i_default;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);

##############################################################################

package Thermd::I18N::en;	# Must come before en_*, which uses en
use base qw(Thermd::I18N);
use vars qw(%Lexicon);

sub baroscale { "mBar" }

###

package Thermd::I18N::en_us;
use base qw(Thermd::I18N::en);
use vars qw(%Lexicon);

sub scale { "English" }

###

package Thermd::I18N::en_ca;
use base qw(Thermd::I18N::en);
use vars qw(%Lexicon);

sub baroscale { "kPa" }

#
# NOTES TO TRANSLATORS:
#
# This is a sample translation lexicon.  To make a new language, just change
# the name of the language from language_template to "your" language (either
# a 2-character ISO language name (like "fr" for French) or a 5 character ISO
# language and subtype (like "fr_BE" for Belgian French), correct the
# full_languagetag.ISO8859-1 to your locale name, do as many translations as
# you can stand, and send the results back to me: dan@klein.com  You may find
# a lexicon already in place, in which case, feel free to add to it.  Note that
# the "map" for compass directions is mildly magical - if you want to change
# it, make sure you know what's going on in it.
#
# There are four classes of messages that thermd will create.  You don't need
# to translate them all, but please do everything in in a class.  Every line
# starts commented out (so if you translate, also delete the leading '#').  If
# you  don't want to translate a class, just DO NOTHING and they will stay in
# English (in fact, any phrase you leave in will stay in English).  The classes
# are (in order of my belief of their importance): 1) Web and RSS messages
# and input strings, 2) Configuration messages (the biggest set), 4) everything
# else, mostly debugging messages :-)
#
# Briefly - you must keep the English phrase as-is, but you add to the lexicon
# by adding your language's words.  The magic [_1], [_2] and so on must be
# preserved (they get replaced with names and numbers), but they can be moved
# around in the string.  There are weird ways of keeping numbers and words
# in synch - there should be very few of those needed, if any (look at "quant"
# in the Maketext docs).  If you want to see where/how a string is used, just
# search the code, and you'll see it.
#
# So if you wanted to translate "Cannot contact [_1] at [_2]" into really
# bad Spanish, you'd replace the "" with "Yo no parlar con [_1] [_2]".
# Note that there are a lot of similar messages - you need to translate them
# all... (sorry :-)
#
# More complete details of how translations are done can be found in
# http://search.cpan.org/~petdance/Locale-Maketext-1.10/lib/Locale/Maketext.pod
#
# Please also provide the "full name" of your encoding in the locale() method.
# This is so setlocale() will work.  In the case of Swedish, for example, the
# locale is sv_SE.ISO8859-1.  This is needed because for French, there is a
# difference between fr_FR, fr_BE, fr_CA, and fr_CH.
#
# I would prefer that you use escaped ASCII hex-codes for accented characters,
# but if that is a pain, I'll just use "vi" to replace then.  Look at
# http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
# but this is a quick list:
#	A-umlaut	\x{C4}	a-umlaut	\x{E4}
#	A-ring		\x{C5}	a-ring		\x{E5}
#	A-grave		\x{C1}	a-grave		\x{E1}
#	AE-ligature	\x{C6}	ae-ligature	\x{E6}
#	E-grave		\x{C9}	e-grave		\x{E9}
#	N-tilde		\x{D1}	n-tilde		\x{F1}
#	O-grave		\x{D3}	o-grave		\x{F3}
#	O-umlaut	\x{D6}	o-umlaut	\x{F6}
#	O-slash		\x{D8}	o-slash		\x{F8}
#	OE-ligature	\x{152}	oe-ligature	\x{153}
#	U-umlaut	\x{DB}	o-umlaut	\x{FB}
#
#
# To test your translation objectively, use the -i18n option to thermd.  To
# test subjectively, you may need to add your language to the parameter list
# of the call to get_handle near line 74 (for example, you may need to say
# get_handle('sv') to test for Swedish).  If you are good with language and
# lousy with Perl, don't worry about it - I will fix the Perl errors.  Once I
# install your translation, things ought to work "as-is" for your browser
# (since language is chosen by your browser and login specs).  But if you need
# to force a language choice for testing (e.g., you're in the US translating
# for me), that's how...
#
package Thermd::I18N::language_template;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);

sub locale { "full_languagetag.ISO8859-1" }
sub scale { "Metric" }	# Or "English", or leave off to inherit default
sub baroscale { "hPa" } # Or "inHg", "mBar", etc. or leave off to inherit

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    ###########################################################
    # These are the LOWERCASE! three-letter month abbreviations
    ###########################################################
    "jan" => "",
    "feb" => "",
    "mar" => "",
    "apr" => "",
    "may" => "",
    "jun" => "",
    "jul" => "",
    "aug" => "",
    "sep" => "",
    "oct" => "",
    "nov" => "",
    "dec" => "",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" => "",
    "Temperature and Environment" => "",
    "The environment in and around [_1]" => "",
    "Refresh failed, hit Reload" => "",
    "Refreshing in" => "",
    "Refreshing..." => "",
    "Type:" => "",
    "View:" => "",
    "Redraw" => "",
    "To:" => "",
    "From:" => "",
    "Hi/Lo graph:" => "",
    "English" => "",
    "Metric" => "",
    "Scale:" => "",
    "Latest Readings: [_1]" => "",
    "[_1] wind" => "",
    "TSV Epoch" => "",
    "TSV Human" => "",
    "CSV UNIX Epoch Time" => "",
    "CSV Human Time" => "",
    "Excel" => "",
    "XML" => "",
    "Graphical" => "",
    "Direction" => "",
    "Time" => "",
    "Temperature [_1]" => "",
    "No data available in selected date range\n" => "",
    "The logging daemon does not seem to be running\n" => "",

    ###########################################################
    # Date-format strings - see do_graph and strftime
    ###########################################################
    "%a %m/%d" => "",
    "%b %d" => "",
    "%a %b %e" => "",
    "%a %b %e %Y" => "",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "",
    "Data is refreshed every [_1] minutes" => "",
    "Graphs are updated every [_1] minutes" => "",

    ###########################################################
    # Maps first letters of North, South, East, West => Up, Down, Right, Left
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/UDRL/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

##############################################################################

package Thermd::I18N::es;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);
#
# Spanish translation thanks to Mario Berges
#

sub locale { "es_ES.ISO8859-1" }

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    ###########################################################
    # These are the LOWERCASE! three-letter month abbreviations
    ###########################################################
    "jan" => "ene",
    "feb" => "feb",
    "mar" => "mar",
    "apr" => "abr",
    "may" => "may",
    "jun" => "jun",
    "jul" => "jul",
    "aug" => "ago",
    "sep" => "sep",
    "oct" => "oct",
    "nov" => "nov",
    "dec" => "dec",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" => "Temperatura y ambiente en [_1]",
    "Temperature and Environment" => "Temperatura y ambiente",
    "The environment in and around [_1]" =>
	"El ambiente en y al rededor de [_1]",
    "Refresh failed, hit Reload" =>
	"La actualizaci\x{f3}n fall\x{f3}, presione Actualizar",
    "Refreshing in " => "Actualizando en ",
    "Refreshing..." => "Actualizando...",
    "Type:" => "Tipo:",
    "View:" => "Vista:",
    "Redraw" => "Redibujar",
    "To:" => "Para:",
    "From:" => "De:",
    "Hi/Lo graph:" => "Gr\x{e1}fica Alto/Bajo",
    "English" => "Ingl\x{e9}s",
    "Metric" => "M\x{e9}trico",
    "Scale:" => "Escala",
    "Latest Readings: [_1]" => "Lecturas m\x{e1}s recientes: [_1]",
    "[_1] wind" => "[_1] viento",
    "TSV Epoch" => "TSV \x{c9}poca",
    "TSV Human" => "TSV Humano",
    "CSV UNIX Epoch Time" => "CSV Tiempo en \x{c9}pocas UNIX",
    "CSV Human Time" => "CSV Tiempo Humano",
    "Excel" => "Excel",
    "XML" => "XML",
    "Graphical" => "Gr\x{e1}fico",
    "Direction" => "Direcci\x{f3}n",
    "Time" => "Tiempo",
    "Temperature [_1]" => "Temperatura",
    "No data available in selected date range\n" =>
	"No hay datos disponibles en el rango seleccionado.\n",

    ###########################################################
    # Date-format strings - see do_graph and strftime
    ###########################################################
    "%a %m/%d" => "",
    "%b %d" => "",
    "%a %b %e" => "",
    "%a %b %e %Y" => "",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "Lecturas m\x{e1}s recientes",
    "Data is refreshed every [_1] minutes" =>
	"Los datos se actualizan cada [_1] minutos",
    "Graphs are updated every [_1] minutes" =>
	"Los gr\x{e1}ficos se actualizan cada [_1] minutos",

    ###########################################################
    # Maps first letters of W(est) onto O(este)
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/NSEO/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

##############################################################################

package Thermd::I18N::de;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);
#
# German translation thanks to Patrick Ben Koetter
#

sub locale { "de_DE.ISO8859-1" }

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    ###########################################################
    # These are the LOWERCASE! three-letter month abbreviations
    ###########################################################
    "jan" => "jan",
    "feb" => "feb",
    "mar" => "m\x{E4}r",
    "apr" => "apr",
    "may" => "mai",
    "jun" => "jun",
    "jul" => "jul",
    "aug" => "aug",
    "sep" => "sep",
    "oct" => "okt",
    "nov" => "nov",
    "dec" => "dez",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" => "Temperatur und Umwelt im [_1]",
    "Temperature and Environment" => "Temperatur und Umwelt",
    "The environment in and around [_1]" => "Die Umwelt in und um [_1]",
    "Refresh failed, hit Reload" =>
	"Aktualisierung fehlgeschlagen, klicken Sie Aktualisieren",
    "Refreshing in" => "Aktualisiere in",
    "Refreshing..." => "Aktualisiere...",
    "Type:" => "Typ:",
    "View:" => "Ansicht:",
    "Redraw" => "Neu zeichnen",
    "To:" => "An:",
    "From:" => "Von:",
    "Hi/Lo graph:" => "Hoch/Niedrig-Graph",
    "English" => "Englisch",
    "Metric" => "Metrik",
    "Scale:" => "Skala:",
    "Latest Readings: [_1]" => "Letzte Ergebnisse: [_1]",
    "[_1] wind" => "[_1] Wind",
    "TSV Epoch" => "TSV Epoch",
    "TSV Human" => "TSV Human",
    "CSV UNIX Epoch Time" => "CSV UNIX Epoch Zeit",
    "CSV Human Time" => "CSV Human Zeit",
    "Excel" => "Excel",
    "XML" => "XML",
    "Graphical" => "Graphisch",
    "Direction" => "Richtung",
    "Time" => "Zeit",
    "Temperature [_1]" => "Temperatur [_1]",
    "No data available in selected date range\n" =>
	"Keine Daten in ausgew\x{E4}hltem Zeitraum verf\x{FC}gbar\n",
    "The logging daemon does not seem to be running\n" =>
	"Der Logging-Daemon scheint nicht in Betrieb zu sein\n",

    ###########################################################
    # Date-format strings - see do_graph and strftime
    ###########################################################
    "%a %m/%d" => "%a %d/%m",
    "%b %d" => "%d %b",
    "%a %b %e" => "%a %e %b",
    "%a %b %e %Y" => "%a %e %b %Y",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "Letzte Ergebnisse",
    "Data is refreshed every [_1] minutes" =>
	"Die Daten werden alle [_1] Minuten aktualisiert",
    "Graphs are updated every [_1] minutes" =>
	"Grafiken werden aktualisiert, alle [_1] Minuten",

    ###########################################################
    # Maps first letters of E(ast) onto O(st)
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSWE/NSOW/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

##############################################################################

package Thermd::I18N::sv;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);
#
# Swedish translation thanks to Roger Andersson and Lars Karlander
#

sub locale { "sv_SE.ISO8859-1" }

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    ###########################################################
    # These are the LOWERCASE! three-letter month abbreviations
    ###########################################################
    "jan" => "jan",
    "feb" => "feb",
    "mar" => "mar",
    "apr" => "apr",
    "may" => "maj",
    "jun" => "jun",
    "jul" => "jul",
    "aug" => "aug",
    "sep" => "sep",
    "oct" => "okt",
    "nov" => "nov",
    "dec" => "dec",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" =>
	"Temperatur och andra m\x{E4}tv\x{E4}rden i [_1]",
    "Temperature and Environment" =>
	"Temperatur och andra m\x{E4}tv\x{E4}rden",
    "The environment in and around [_1]" =>
	"Diverse m\x{E4}tv\x{E4}rden i och omkring [_1]",
    "Refresh failed, hit Reload" =>
	"Uppdateringen misslyckades. Klicka p\x{E5} Uppdatera",
    "Refreshing in" => "Uppdaterar om",
    "Refreshing..." => "Uppdaterar...",
    "Type:" => "Typ:",
    "View:" => "Visa:",
    "Redraw" => "Rita om",
    "To:" => "Till:",
    "From:" => "Fr\x{E5}n:",
    "Hi/Lo graph:" => "Max/Min-graf:",
    "English" => "Engelsk",
    "Metric" => "Metrisk",
    "Scale:" => "Skala:",
    "Latest Readings: [_1]" => "Senaste avl\x{E4}sningar: [_1]",
    "[_1] wind" => "[_1] vind",
    "TSV Epoch" => "TSV Unixtid",
    "TSV Human" => "TSV vanlig tid",
    "CSV UNIX Epoch Time" => "CSV Unixtid",
    "CSV Human Time" => "CSV vanlig tid",
    "Excel" => "Excel",
    "XML" => "XML",
    "Graphical" => "Grafisk",
    "Direction" => "Riktning",
    "Time" => "Tid",
    "Temperature [_1]" => "Temperatur [_1]",
    "No data available in selected date range\n" =>
	"Inga v\x{E4}rden finns tillg\{E4}ngliga i det valda datumomr\x{E5}det\n",
    "The logging daemon does not seem to be running\n" =>
	"Loggningsdemonen verkar inte vara ig\x{E5}ng\n",

    ###########################################################
    # Date-format strings - see do_graph and strftime
    ###########################################################
    "%a %m/%d" => "%a %d/%m",
    "%b %d" => "%d %b",
    "%a %b %e" => "%a %e %b",
    "%a %b %e %Y" => "%a %e %b %Y",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "Senaste avl\x{E4}sningar",
    "Data is refreshed every [_1] minutes" =>
	"Data uppdateras i intervall om [_1] minuter",
    "Graphs are updated every [_1] minutes" =>
     	"Graferna uppdateras i intervall om [_1] minuter",

    ###########################################################
    # Maps first letters of E(ast) onto O-umlaut(st), W(est) onto V(ast)
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/NS\x{D6}V/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

##############################################################################

package Thermd::I18N::da;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);
#
# Danish translation thanks to Kristian Vilmann
#

sub locale { "da_DK.ISO8859-1" }

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    ###########################################################
    # These are the LOWERCASE! three-letter month abbreviations
    ###########################################################
    "jan" => "jan",
    "feb" => "feb",
    "mar" => "mar",
    "apr" => "apr",
    "may" => "maj",
    "jun" => "jun",
    "jul" => "jul",
    "aug" => "aug",
    "sep" => "sep",
    "oct" => "okt",
    "nov" => "nov",
    "dec" => "dec",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" => "Temperatur og klima i [_1]",
    "Temperature and Environment" => "Temperatur og klima",
    "The environment in and around [_1]" => "Klima i og omkring [_1]",
    "Refresh failed, hit Reload" =>
	"Opdatering mislykkedes. Klik p\x{E5} Opdat\x{e9}r",
    "Refreshing in" => "Opdaterer om",
    "Refreshing..." => "Opdaterer...",
    "Type:" => "Type:",
    "View:" => "Vis:",
    "Redraw" => "Tegn igen",
    "To:" => "Til:",
    "From:" => "Fra",
    "Hi/Lo graph:" => "Max/Min-graf",
    "English" => "Engelsk",
    "Metric" => "Metrisk",
    "Scale:" => "Skala:",
    "Latest Readings: [_1]" => "Seneste afl\x{E6}sninger: [_1]",
    "[_1] wind" => "[_1] vind",
    "TSV Epoch" => "TSV Tidsangivelse: UNIX",
    "TSV Human" => "TSV Tidsangivelse: Standard",
    "CSV UNIX Epoch Time" => "CSV Tidsangivelse: UNIX",
    "CSV Human Time" => "CSV Tidsangivelse: Standard",
    "Excel" => "Excel",
    "XML" => "XML",
    "Graphical" => "Grafisk",
    "Direction" => "Retning",
    "Time" => "Tid",
    "Temperature [_1]" => "Temperatur [_1]",
    "No data available in selected date range\n" =>
	"Data er ikke tilg\x{E6}ngelig i det valgte datointerval\n",
    "The logging daemon does not seem to be running\n" =>
	"Logningsd\x{E6}monen lader ikke til at k\x{F8}re\n",

    ###########################################################
    # Date-format strings - see do_graph and strftime
    ###########################################################
    "%a %m/%d" => "%a %d/%m",
    "%b %d" => "%b %d",
    "%a %b %e" => "%a %b %e",
    "%a %b %e %Y" => "%a %b %e %Y",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "Seneste afl\x{E6}sninger",
    "Data is refreshed every [_1] minutes" =>
	"Data opdateres hvert [_1]. minut",
    "Graphs are updated every [_1] minutes" =>
	"Graferne opdateres hvert [_1]. minut",

    ###########################################################
    # Maps first letters of E(ast) onto O-slash(st), W(est) onto V(est)
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/NS\x{D8}V/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file


##############################################################################

package Thermd::I18N::nl;	# Must come before nl_NL and nl_BE
use base qw(Thermd::I18N);
use vars qw(%Lexicon);
#
# Dutch translation thanks to Frank Kuiper
#

sub locale { "nl_NL.ISO8859-1" }	# I choose NL Dutch over BE Dutch

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    ###########################################################
    # These are the LOWERCASE! three-letter month abbreviations
    ###########################################################
    "jan" => "jan",
    "feb" => "feb",
    "mar" => "maa",
    "apr" => "apr",
    "may" => "mei",
    "jun" => "jun",
    "jul" => "jul",
    "aug" => "aug",
    "sep" => "sep",
    "oct" => "okt",
    "nov" => "nov",
    "dec" => "dec",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" => "Temperatuur en omgeving in [_1]",
    "Temperature and Environment" => "Temperatuur en omgeving",
    "The environment in and around [_1]" => "De omgeving in en rondom [_1]",
    "Refresh failed, hit Reload" =>
	"Verversen is mislukt, selecteeer 'Huidige pagina vernieuwen'",
    "Refreshing in" => "Verversen in",
    "Refreshing..." => "Verversen...",
    "Type:" => "Soort:",
    "View:" => "Beeld:",
    "Redraw" => "Opnieuw weergeven",
    "To:" => "Tot:",
    "From:" => "Vanaf:",
    "Hi/Lo graph:" => "Hoogste/laagste grafiek:",
    "English" => "Engels",
    "Metric" => "Metrisch",
    "Scale:" => "Schaal:",
    "Latest Readings: [_1]" => "Laatste metingen: [_1]",
    "[_1] wind" => "[_1] wind",
    "TSV Epoch" => "TSV tijdperk",
    "TSV Human" => "TSV menselijk",
    "CSV UNIX Epoch Time" => "CSV Unix tijdperk tijd",
    "CSV Human Time" => "CSV menselijke tijd",
    "Excel" => "Excel",
    "XML" => "XML",
    "Graphical" => "Grafisch",
    "Direction" => "Richting",
    "Time" => "Tijd",
    "Temperature [_1]" => "Temperatuur [_1]",
    "No data available in selected date range\n" =>
	"Geen gegevens beschikbaar in geselecteerde selectie\n",
    "The logging daemon does not seem to be running\n" =>
	"Loggen lijkt niet geactiveerd\n",

    ###########################################################
    # Date-format strings - see do_graph and strftime
    # Date settings in dutch are either day-month-year, or year-month-day.
    ###########################################################
    "%a %m/%d" => "%a %m/%d",
    "%b %d" => "%b %d",
    "%a %b %e" => "%a %e %b",
    "%a %b %e %Y" => "%a %e %b %Y",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "Laatste weergave",
    "Data is refreshed every [_1] minutes" =>
	"Gegevens worden elke [_1] minuten ververst",
    "Graphs are updated every [_1] minutes" =>
	"Grafische weergave wordt elke [_1] minuten ververst",

    ###########################################################
    # Maps first letters of S(outh) onto Z(uid), E(ast) onto O(osten)
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/NZOW/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

###

package Thermd::I18N::nl_nl;
use base qw(Thermd::I18N::nl);
use vars qw(%Lexicon);

sub locale { "nl_NL.ISO8859-1" }

###

package Thermd::I18N::nl_be;
use base qw(Thermd::I18N::nl);
use vars qw(%Lexicon);

sub locale { "nl_BE.ISO8859-1" }

##############################################################################

package Thermd::I18N::ru;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);
#
# Russian translation thanks to Vladas Leonas
#

sub locale { "ru_RU.UTF-8" }

# Sigh - Russian has a number of encodings.  Make sure you use characters
# from the Unicode/ISO-8859-5 UTF-8 encoding.
# See http://czyborra.com/charsets/cyrillic.html

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
     ###########################################################
     # These are the LOWERCASE! three-letter month abbreviations
     ###########################################################
     "jan" => "\x{44f}\x{43d}\x{432}",
     "feb" => "\x{444}\x{435}\x{432}",
     "mar" => "\x{43c}\x{430}\x{440}",
     "apr" => "\x{430}\x{43f}\x{440}",
     "may" => "\x{43c}\x{430}\x{439}",
     "jun" => "\x{438}\x{44e}\x{43d}",
     "jul" => "\x{438}\x{44e}\x{43b}",
     "aug" => "\x{430}\x{432}\x{433}",
     "sep" => "\x{441}\x{435}\x{43d}",
     "oct" => "\x{43e}\x{43a}\x{442}",
     "nov" => "\x{43d}\x{43e}\x{44f}",
     "dec" => "\x{434}\x{435}\x{43a}",

    ###########################################################
    # Web-based strings
    ###########################################################
    "Temperature and Environment in [_1]" => "\x{422}\x{435}\x{43c}\x{43f}\x{435}\x{440}\x{430}\x{442}\x{443}\x{440}\x{430} \x{438} \x{441}\x{440}\x{435}\x{434}\x{430} \x{432} [_1]",
    "Temperature and Environment" => "\x{422}\x{435}\x{43c}\x{43f}\x{435}\x{440}\x{430}\x{442}\x{443}\x{440}\x{430} \x{438} \x{441}\x{440}\x{435}\x{434}\x{430}",
    "The environment in and around [_1]" => "\x{421}\x{440}\x{435}\x{434}\x{430} \x{432} \x{438} \x{432}\x{43e}\x{43a}\x{440}\x{443}\x{433} [_1]",
    "Refresh failed, hit Reload" => "\x{41e}\x{431}\x{43d}\x{43e}\x{432}\x{43b}\x{435}\x{43d}\x{438}\x{435} \x{43d}\x{435} \x{443}\x{434}\x{430}\x{43b}\x{43e}\x{441}\x{44c}, \x{441}\x{434}\x{435}\x{43b}\x{430}\x{439}\x{442}\x{435} \x{43f}\x{435}\x{440}\x{435}\x{437}\x{430}\x{433}\x{440}\x{443}\x{437}\x{43a}\x{443}",
    "Refreshing in" => "\x{41e}\x{431}\x{43d}\x{43e}\x{432}\x{43b}\x{435}\x{43d}\x{438}\x{435} \x{447}\x{435}\x{440}\x{435}\x{437}",
    "Refreshing..." => "\x{41e}\x{431}\x{43d}\x{43e}\x{432}\x{43b}\x{435}\x{43d}\x{438}\x{435}...",
    "Type:" => "\x{422}\x{438}\x{43f}:",
    "View:" => "\x{412}\x{438}\x{434}:",
    "Redraw" => "\x{41f}\x{435}\x{440}\x{435}\x{440}\x{438}\x{441}\x{43e}\x{432}\x{430}\x{442}\x{44c}",
    "To:" => "\x{412}:",
    "From:" => "\x{418}\x{437}:",
    "Hi/Lo graph:" => "\x{413}\x{440}\x{430}\x{444} \x{432}\x{44b}\x{441}/\x{43d}\x{438}\x{437}",
    "English" => "\x{410}\x{43d}\x{433}\x{43b}.",
    "Metric" => "\x{41c}\x{435}\x{442}\x{440}.",
    "Scale:" => "\x{41c}\x{430}\x{441}\x{448}\x{442}\x{430}\x{431}:",
    "Latest Readings: [_1]" => "\x{41f}\x{43e}\x{441}\x{43b}\x{435}\x{434}\x{43d}\x{438}\x{435} \x{43f}\x{43e}\x{43a}\x{430}\x{437}\x{430}\x{43d}\x{438}\x{44f}:
[_1]",
    "[_1] wind" => "[_1] \x{432}\x{435}\x{442}\x{435}\x{440}",
#    "TSV Epoch" => "",
#    "TSV Human" => "",
#    "CSV UNIX Epoch Time" => "",
#    "CSV Human Time" => "",
#    "Excel" => "Excel",
#    "XML" => "XML",
    "Graphical" => "\x{413}\x{440}\x{430}\x{444}.",
    "Direction" => "\x{41d}\x{430}\x{43f}\x{440}\x{430}\x{432}\x{43b}\x{435}\x{43d}\x{438}\x{435}",
    "Time" => "\x{412}\x{440}\x{435}\x{43c}\x{44f}",
    "Temperature [_1]" => "\x{422}\x{435}\x{43c}\x{43f}\x{435}\x{440}\x{430}\x{442}\x{443}\x{440}\x{430} [_1]",
    "No data available in selected date range\n" => "\x{418}\x{43d}\x{444}\x{43e}\x{440}\x{43c}\x{430}\x{446}\x{438}\x{44f} \x{43e} \x{432}\x{44b}\x{431}\x{440}\x{430}\x{43d}\x{43e}\x{43c} \x{434}\x{438}\x{430}\x{43f}\x{430}\x{437}\x{43e}\x{43d}\x{435} \x{434}\x{430}\x{442} \x{43d}\x{435}\x{434}\x{43e}\x{441}\x{442}\x{443}\x{43f}\x{43d}\x{430}",
    "The logging daemon does not seem to be running\n" => "\x{41f}\x{43e}\x{445}\x{43e}\x{436}\x{435}, \x{447}\x{442}\x{43e} \x{434}\x{435}\x{43c}\x{43e}\x{43d} \x{43d}\x{435} \x{440}\x{430}\x{431}\x{43e}\x{442}\x{430}\x{435}\x{442}",

    ###########################################################
    # RSS strings
    ###########################################################
    "Latest Readings" => "\x{41f}\x{43e}\x{441}\x{43b}\x{435}\x{434}\x{43d}\x{438}\x{435} \x{43f}\x{43e}\x{43a}\x{430}\x{437}\x{430}\x{43d}\x{438}\x{44f}",
    "Data is refreshed every [_1] minutes" => "\x{414}\x{430}\x{43d}\x{44b}\x{435} \x{43e}\x{431}\x{43d}\x{43e}\x{432}\x{43b}\x{44f}\x{44e}\x{442}\x{441}\x{44f} \x{43a}\x{430}\x{436}\x{434}\x{44b}\x{435} [_1] \x{43c}\x{438}\x{43d}\x{443}\x{442}",
    "Graphs are updated every [_1] minutes" => "\x{413}\x{440}\x{430}\x{444}. \x{43e}\x{431}\x{43d}\x{43e}\x{432}\x{43b}\x{44f}\x{44e}\x{442}\x{441}\x{44f} \x{43a}\x{430}\x{436}\x{434}\x{44b}\x{435} [_1] \x{43c}\x{438}\x{43d}\x{443}\x{442}",

    ###########################################################
    # Maps first letters of NSEW into Cyrillic ES, YU, VE, ZE
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/\x{441}\x{42E}\x{432}\x{417}/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

##############################################################################

package Thermd::I18N::it;
use base qw(Thermd::I18N);
use vars qw(%Lexicon);

sub locale { "it_IT.ISO8859-1" }

BEGIN{	# Only needed because this is not a separate module file
%Lexicon = (
    #
    # Put your Lexicon 
    #

    ###########################################################
    # Maps first letters of W(est) onto O(vest)
    ###########################################################
    map { my $x = $_; $x  =~ tr/NSEW/NSEO/; ($_, $x) }
		qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE),
    );
}	# Only needed because this is not a separate module file

__END__

=head1 NAME

thermd - Thermometer Daemon supporting HA7Net, EM1, TEMP08, RoomAlert, TemPageR,
WeatherGoose (and their ilk), RacSense, serial port host adapters (ibuttonlink,
DS9097E, DS9097, and DS2480B) and USB host adapters (DS9490 or PuceBaboon),
QK145, VK011, and EnerSure data collectors, the Poseidon and Damocles series
from HWg, 
LX-4008 and IR5150 from MRV (and other SNMP devices),
Veris H8030, H8031, H8035 and H8036,
Smart-Watt, Smart-SenseRH, and Smart-PDU from Smart Works,
MaxBotix Sonar sensors,
and the Proliphix line of IP-enabled thermostats.

=head1 DESCRIPTION

Thermd is a four-function program to work with a very large family of
environmental monitors.

Currently, I<thermd> supports the following hardware and software devices:

=over 4

=for html <dd>

=item HA7Net

Embedded Data Systems L<http://embeddeddatasystems.com>

=item EM1

Sensatronics L<http://www.sensatronics.com>

=item TEMP08

Midon Design L<http://www.midondesign>

=item RoomAlert 7E, RoomAlert 11E, RoomAlert 24E, RoomAlert 26W, and TemPageR

RoomAlert L<http://www.roomalert.com>

=item MiniGoose, WeatherGoose, SuperGoose, RacSense or PowerGoose

Geist Manufacturing L<http://www.geistmfg.com/> and ITWatchDogs L<http://www.itwatchdogs.com>

=item owfs, owhttpd, owshell

L<http://www. owfs.org> and L<http://owfs.sourceforge.net>

=item Serial port adaptors based on ibuttonlink, DS9097E, DS9097 and
USB port adaptors based on DS9490 or PuceBaboon

Hobby Boards L<http://www.hobby-boards.com>, AAG Electronica
L<http://www.aagelectronica.com>, and others (via I<owfs>, I<owhttpd>, and
I<owshell>)

=item QK145 and VK011

Quality Kits L<http://www.qkits.com>

=item Enersure

Trendpoint L<http://www.trendpoint.com>

=item A wide variety of SNMP-enabled devices, including 

=over 4

=for html <dd>

=item Poseidon and Damocles

HWg L<http://www.hw-group.com/index_en.html>,

=item MRV LX-4008 and IR5150

MRV L<http://www.mrv.com>

=back

=for html </dd>

=item SmartWatt, SmartSenseTH, SmartNet and SmartPDU

Smart Works L<http://www.smart-works.com>

=item Veris

Veris H8030, H8031, H8035, and H8035 Modbus Energy Meters
L<http://www.veris.com/modbus.asp>

=item MaxBotix

MaxBotix EZ1, EZ2, EZ3, and EZ4 sonar rangefinders from
L<http://www.maxbotix.com/>

=item Proliphix line of IP-enabled thermostats

Proliphix L<http://www.proliphix.com>

=item Newport and Omega iBTX, iBTX-M, iBTHX, iPTX-D, iPTX-W, iTCX, iTHX-M, iTHX-W, and iTHX-2 Microservers

From Newport Electronics L<http://www.newportus.com/iServer/> or Omega
Engineering L<http://www.omega.com/>

=item Personal weather stations

Weather Underground L<http://www.wunderground.com> (both writing your
data and reading other stations' data).

=item Command line

Command line programs (such as C<sysctl> or C<snmpwalk> can be called to
extract data that is otherwise not accessable through thermd.

=back

=for html </dd>

The three operational modes of I<thermd> are:

=over 4

=item 1 Logging daemon

The program will sample temperature readings and log them to
a data file (one log file per sensor).  The program will also optionally
generate an RSS feed, with graphs (see below).  The program will also
optionally send weather information to Weather Underground
(http://www.wunderground.com/) for local weather station information.

=item 2 Plotting/Printing/Image-annotating program

The program will also create Portable Network Graphic (PNG) files, text
files which plot/list sensor readings, or will annotate an image with sensor
readings (using ImageMagick)

=item 3 CGI script

The program will act as a CGI script to interactively plot data,
and allow a web surfer to step forwards and backwards in the logged data,
looking a different views of the data.

=back

=head1 INTERNATIONALIZATION

Thermd natively supports English, Swedish, German, Dutch, Russian, and
Spanish (translations to other
languages will be welcomed).  Web pages will be translated to the language
of the person viewing them, while all other actions are done in the language
in which the host machine resides.

Documentation and configuration error messages remain in English.

=head1 REQUIRED MODULES

You must download and install the following modules from the CPAN in order
to get I<thermd> to function (I<thermd> uses other modules, but they are part
of the Perl default distribution).

=over 4

=for html <dd>

=item HTTP::Date

=item Config::General

=item GD

=item GD::Graph

The GD and GD::Graph modules are only necessary if you want I<thermd> to plot
graphical data (used in the CGI script and the RSS feed).  You will also need
the GD libraries, of course (see L<http://www.libgd.org/>).  B<NOTE:> I have
not been able to get this working on Windows, so currently any reference to
GD is disabled when running on Windows.

=item LWP::UserAgent

=item Crypt::SSLeay

The LWP::UserAgent module is usually all that is needed if you specify the
HA7Net Room Alert, TemPageR, EM1, Proliphix, or owhttpd in the configuration
file (or if you have a C<View> of type C<Wunderground>). If you need SSL (that
is, if you access a device via port 443) then you will also need the
Crypt::SSLeay module.

=item XML::Simple

If you use the MiniGoose, WeatherGoose, SuperGoose, RacSense or PowerGoose,
of have a collector of type Poseidon or Wunderground you will need the
XML::Simple module in addition to LWP::UserAgent.

If you use the "XML" output format, you will also need the XML::Simple module.

=item Net::SNMP

If you use the SNMP Subtype for Poseidon or Damocles series from HWg, or the MRV LX-4008, IR5150,
from MRV, or other
SNMP-based devices, you will also need the Net::SNMP module.

=item Modbus::Client

If you use the Enersure or any of the Veris Energy Meters, you will also need
Modbus::Client

=item DBI (and DBD)

If you use C<LogFormat SQL>, then you will also need the DBI module and the
DBD module that matches your database.

=item Digest::CRC

If you use the HA7Net, you will need the Digest::CRC module

=item Net::Telnet

If you use any of the Newport Electronics or Omega Engineering iServer
Microservers, you will need the Net::Telnet module.

=item Image::Magick

If you use the I<Type=Image> in any C<View> in the config-file, you will
need the Image::Magick module as well as the ImageMagick libraries
(see L<http://www.imagemagick.org/>).  You will need a version greater or
equal to 6.3.0.

=item RPC::XML and RPC::XML::Client

If you use the SmartWatt, SmartNet, SmartSenseRH, or SmartPDU, you'll need
RPC::XML and RPC::XML::Client

=item Spreadsheet::WriteExcel

If you use the Excel output feature, you will also need
Spreadsheet::WriteExcel.  This feature is also exposed to the web, so you
may want to include this module to be nice to anyone who can view your
website.

=item Authen::SASL

If you use the C<SMTPHost> configuration option, and your SMPT Host requires
authentication (which means you will also need to use the C<SMTPUsername> and
C<SMTPPassword> configuration options), then you will also need the
Authen::SASL module.

=back

=for html </dd>

=head1 OPERATION

When run from the command line, I<thermd> can run either as a logging daemon,
or as a plotting/reporting script.  ALL RUNTIME FLAGS MAY BE ABBREVIATED!
Note that when a flag is specified, it overrides any corresponding value in
the config file (so specifying -width overrides the value of the GraphWidth
configuration item).

=head2 Logging daemon

    thermd -daemon [-config file] [-nofork [-verbose]]
	-config file	Use this as a config file (default=/etc/thermd.conf)
        -nofork         Don't fork to background (usually only for debugging)
        -verbose        Verbose output (definitely for debugging :-)

Sending a C<HUP> signal to the daemon will cause it to restart and re-read
it's configuration file.  The daemon automatically restarts every week to
avoid possible memory leaks.

=head2 Reporting

    thermd -report [report-args] [config-args, unit-args, range-args]
        -format type    One of CSV (comma separated values), TSV (tab
			separated values), XML, CER (Eddy Common Event
			Record) or Excel.
			Default format is TSV
        -epochtime      When printing time, print Unix time(2), instead of
			the default human-readable time
	-current	Print current values only
	-raw		Print all current values, in raw form

=head2 Graphing

    thermd -graph [graph-args] [config-args, unit-args, range-args]
	-width pixels	Override the width of the graph.  If not specified
			here, default value comes from the config file, and
			that default value is 750
	-height pixels	Override the height of the graph.  If not specified
			here, default value comes from the config file, and
			that default value is 300
	-hilo		Graph 24-hour extremes instead of individual datapoints
			    This may get a bit hard to read if you have too
			    many sensors being graphed
        -nosmooth	When graphs get dense (typ. > 1 month of data), graph
			    data is automatically smoothed.  This switch
			    defeats that function.

=head2 Image Annotation

    thermd -annotate [annotate-args] [config-args, unit-args]
	-current	Print current temperatures only (default)

=head2 Shared arguments available to reporting, graphing and annotating

    Common config-args
	-config file	Use this as a config file (default=/etc/thermd.conf)
        -outfile file   Output graph/table to this file (instead of STDOUT)
        -view name      Show a named view (defined in config file, default=all)
	-all_sensors	Ignore/override any -view, and show *all* sensors
	-nowarn		Do not print configuration warnings

    Common unit-args
	-units {English|Metric}
			Use English/Metric units (override config file)
        -temperature {C|F}
			Show temperature values as (override config and -units)
        -windspeed {mph|kph|mps|knots}
			Show windspeed values as (override config and -units)
        -rainfall {inches|mm}
			Show rainfall values as (override config and -units)
			(also affects MaxBotix "range" values)
        -barometer {inHg|mmHg|hPa|kPa|mBar|millibar}
			Show barometer values as (override config and -units)

    Common range-args
        -from daytime   From date/time *
        -to daytime     To date/time *
        -center daytime Center chart on date/time *
        -span reltime	Span for centered chart **

        * Date/time values may be specified as:
            absolute (ex: "20030228" or "20030228T1535" - actually, any
                format allowed by HTTP::Date)
            relative (ex: 3d, -2.5h, 3w, +5m3d1h, etc., suffixes are y=>year,
		m=>month, w=>week, d=>day, h=>hour)  Note: sign is highly
		significant, and changes what it is relative to...
            named (i.e., "now", "today", "yesterday"), considered absolute
		times as far as discussion below is considered

            If -from and -to are both missing, we assume: -from -1d -to now
            If -from is specified but -to is missing, we assume: -to now
            If -from is missing but -to is specified, we assume that -from
		is 1d before -from
            If -from is relative and -to is absolute, we assume that -from
                is relative to now and comes before -to (which means that
                sign is ignored!), so that if today is 20030327:
		    -from -1m -to 20030228	 means the same as:
		    -from 20030227 -to 20030228
                Since sign is ignored here, you could also say:
		    -from 1m -to 20030228	or:
		    -from 1m -to yesterday	or:
		    -from +1m -to 20030228	(counterituitive, but legal)
            If -from is absolute and -to is relative, we assume that -to
		is relative to -from and comes after -from, so that:
		    -from 20030228 -to 1d	means the same as:
		    -from 20030228 -to +1d	means the same as:
		    -from 20030228 -to 20030301
		There is one special case: if -to is negative, it
		is interpreted as relative to "now", so:
		    -from 20030228 -to -1d	(from 20030228 to yesterday)
            If both -from and -to are relative, then both are considered to
		be relative to "now" (with one exception), so that:
		    -from -5d -to -4d		means the same as:
		    -from +5d -to 4d		ignore sign on -from, same as:
		    -from 5d -to 4d		but:
		    -from -4d -to -5d		illegal reverse range!
		There is one special case: if -to is positive (i.e., +time),
		then it is relative to -from, so:
		    -from -5d -to +1d		means the same as:
		    -from -5d -to -4d

        ** Span values may only be relative, see above
            If -center is specified, but -span is missing, then -span
            defaults to 1d (i.e., from 12h before to 12h after -center)

=head2 Checking your configuration

    thermd -checkconfig [check-args]
	-checkconfig	Check the validity of the config file, and exit
	-config file	Use this as a config file (default=/etc/thermd.conf)
	-verbose	Output a full config file, with all defaults expressed
	-list		Prints a list of logfiles used by thermd (overrides
			-verbose, and only prints the list)

=head2 CGI Script

Theoretically, just put C<thermd> in your CGI area (or a link to the program,
if you want to run the daemon froma different directory), and the rest should
happen automatically :-)  If you don't know how to set up a CGI script, you'll
need to familiarize yourself more with your web server - there are too many
variables to deal with in this document.

One word of advice - if it doesn't work as expected, look at the error logs
for your web server (a lot of people forget to do this).

=head1 CONFIGURATION FILE

The configuration file (located in F</etc/thermd.conf> by default, and
specified by the C<-config> runtime option).

=head2 Contents of the file (overview)

The configuration file is read once at program startup.

=over 4

=for html <dd>

=item Case Sensitivity

Field names are case-insensitive, but case is preserved (but ignored) in
the values.

=item Style

Configuration entries resemble an Apache config file.  If you really want to
learn what that looks like, read the documentation for the Perl module
C<Config::General>

=item Comments

Comments start with a pound sign (C<#>) and continue until the end of the line.
If you want a pound sign to be part of a name or string, precede it with a
backslash, like this:

    Name    Sensor \#1      # That wasn't a comment, but this is

=item Blank lines

Blank lines are ignored.  Erroneous lines will generate warnings, but
C<thermd> will try I<not> abort unless it has to.

=back

=for html </dd>

=head2 Global Configuration Attributes

The following attributes are used to configure many of the global attributes
of thermd.  All of them are optional, and have reasonable default values.

=over 4

=for html <dd>

=item MailFrom

Who alarm messages come from (in email).  I<Default value is "Thermometer
Daemon E<lt>rootE<gt>".>  Email addresses are checked for compliance with
RFC822.  Note that a I<compliant> email address may still not be a I<valid>
address, but at least it stands a fighting chance!

=item PIDFile

Where the process ID of the current instance of the C<thermd> daemon is
running (this file is also used as a lockfile to prevent multiple daemons
from running - if you I<want> multiple daemons, you will need to specify
a different value in each configuration file).  B<Default value is
C</var/run/thermd.pid>.>

I<If you specify a PIDFile of "/dev/null" (or whatever your local equivalent
of the "null file" is), no PIDFile will be created (but
you will also lose the ability to lock against competing simultaneous thermd
daemons).>

=item SMTPHost

Which mail transfer host to use to send email.  If you have I<qmail>,
I<sendmail>, I<postfix>, I<Exchange>, etc. running on a different machine from
I<thermd>, specify its IP address or hostname (with optional port numbers);
you can also specify C<localhost> if your mailer is running on the same
machine as I<thermd>.  B<For historical reasons, if you do not specify an
C<SMTPHost>, the default is to use a local copy of sendmail. I<This will
change in Version 3.x of thermd.>>

We strongly recommend using I<-checkconfig -email> to test the correctness of
your email setup before putting I<thermd> into production.

=item SMTPUsername

If you use C<SMTPHost> and the host requires authentication, the username to
authenticate with.  B<Default value is not to use authentication.>

We strongly recommend using I<-checkconfig -email> to test the correctness of
your email setup before putting I<thermd> into production.

=item SMTPPassword

If you use C<SMTPHost> and the host requires authentication, the password to
authenticate with.  B<Default value is not to use authentication.>

We strongly recommend using I<-checkconfig -email> to test the correctness of
your email setup before putting I<thermd> into production.

=item Sendmail

Where the locally executable copy of Sendmail is located.  B<Default value is
C</usr/sbin/sendmail>.>  If you use I<qmail>, I<postfix>, I<Exchange>, etc,
then you probably want to specify an C<SMTPHost> instead.

We strongly recommend using I<-checkconfig -email> to test the correctness of
your email setup before putting I<thermd> into production.

=item LogFormat

The format used to store the log data.  The two acceptable LogFormat types
are (currently) C<Text> and C<SQL>. B<Default value is C<Text>.>

If C<LogFormat SQL> is used, then the remainder of the line contains
information about the database and server.  The additional parameters
are the database type (e.g., mysql, Oracle, etc.), the database name (I
use C<thermd>, but you may use any name you like), and the "standard"
user:pass@host describing the location of the database server.

Valid examples of the C<LogFormat> declaration are:

	LogFormat Text
	LogFormat SQL mysql thermd root@localhost
	LogFormat SQL Informix thermd dvk:monKey@dbhost.klein.com

If C<LogFormat Text> is used, then thermd will create one logfile for each
sensor as well as a "current" values file.  If C<LogFormat SQL> is used, then
thermd will create three tables - one for filename to id mapping, another for
current values, and a third for historical logging of values.

If C<LogFormat SQL> is used, then three tables will be created.  The
I<logfiles> table will contain the name of the sensor and a unique SQL
log_id (B<NOTE:> if you wish to access the tables with other applications,
you may wish to add other descriptive columns to this table).  The
I<readings> table will contain all historical readings (the columns are the
logtime, the log_id for the reading, and the value of the reading.  The
I<current> table will contain the most recent readings (akin to the
I<current> file for C<LogFormat Text>).

=item LogWrite

Where the logfiles will be written by the daemon.  B<Default value is
C</var/log/thermd>.>  If C<LogFormat> is SQL, then this directive is ignored.

=item LogRead

Where the files are read from.  B<Default value is whatever value
C<LogWrite> has (which has its own defaults - see above).>  The reason for
the two different variables is when you have an asymmetric system - for
example, a chrooted webserver, or a webserver that reads NFS data from a
thermd daemon on a different machine.  If C<LogFormat> is SQL, then this
directive is ignored.

=item LogInterval

How often we write data to the logfiles.  B<Default value is C<10m> (10
minutes).>  Values less than 15s or greater than 30m will generate a warning
as possibly too extreme...

=item TimeZone

If the timezone string is two letters long, it will be printed
as-is.  If it is three letters long, the script will replace the
middle letter with 'D' if daylight savings time is in effect.
I would have loved to not use this variable, but there is no neat
cross platform way of finding out your current timezone (that I
know of - please tell me if there is).  B<Default value is C<GMT>.>

=item DisplayIn

B<This field now deprecated - thermd will choose a value based on your locale
(so the US gets English, the rest of the world gets Metric).  Note
that if you specificy a value for DisplayIn, it also affects how people
viewing your web page will see your data.>

What units are used when reporting or graphing (you can choose between
C<English> and C<Metric>).  This is field is B<IRRESPECTIVE> of how the sensors
actually collect data (which for some collectors can be hardware or firmware
configured to either C or F for temperature).  B<Default value is C<Metric>>
(because even though I live in the United States, and in the past 30 years,
we have proven ourselves to be in incapable of converting to the metric
system, I want to effect change).  I<For
backwards compatability, you can also use C<F> (which is equivalent to
C<English>) and C<C> (which is equivalent to C<Metric>).>

=item Temperature

What temperature units are used when reporting or graphing (you can choose
between C<C> and C<F>).  B<Default value depends on the value of
C<DisplayIn>.  For English, the default is C<F>, and for Metric the default
value is C<C>.>

=item Rainfall

What rainfall units are used when reporting or graphing (you can choose
between C<Inches> and C<mm>).  B<Default value depends on the value of
C<DisplayIn>.  For English, the default is C<Inches>, and for Metric the
default value is C<mm>.>

C<Rainfall> units also affect the way the C<MaxBotix> range sensors report
their values (that is, the C<MaxBotix will report ranges in Inches or MM, and
the C<Rainfall> units also affect C<Range>s)

=item Barometer

What barometric units are used when reporting or graphing (you can choose
between C<inHg>, C<mmHg>, C<hPa>, C<kPa>, C<millibar> or C<mBar>).  B<Default value depends
on the value of C<DisplayIn> and/or the language preferences of the web
browser being used.  For English, the default is C<inHg>, and for
Metric the default value is C<hPa> (but C<kPa> for Canada and C<mBar> for
England).>

=item WindSpeed

What windspeed units are used when reporting or graphing (you can choose
between C<MPH>, C<KPH>, C<MPS> (meters/sec), and C<Knots>).  B<Default value
depends on the value of C<DisplayIn>.  For English, the default value is
C<MPH>, and for Metric the default value is C<KPH>.>

=item Location

Where you have the sensors (I use "Klone's House").  This value is displayed
at the top of the CGI page.  B<Default value is an empty location string.>

=item GPSCoordinates

Also displayed at the top of the CGI page, the text surrounding the MapURL
configuration item.  B<Default value is the empty string>.

=item MapURL

Also displayed at the top of the CGI page, the hyperlink of (usually) a map when
you click on the GPSCoordinates configuration item text.  B<Default value
is the empty string>.

=item SensorOrder

The order in which sensors are displayed in graphs and reports.  You may
specify "ID" (which will sort by sensor ID), "SubID" (which will sort by
collector name, and then by sensor ID) "Name" (which will sort by the names
you assign the sensor), "SubName" (which will sort by collector name, and
then by sensor name), "PopUp" (which will sort by the PopUp description of
the sensor, and if that is absent, by name), or "NoSort" (which will put
the sensors in whatever order the hash tables put them. B<The default value
is Name.>

=item GraphWidth

The width of the graph that is created from the CGI script or the -graph
option.  B<Default value is C<750>.>

=item GraphHeight

The height of the graph that is created from the CGI script or the -graph
option.  B<Default value is C<300>.>

=item DefaultView

If this qualifier is used, then that C<View> is marked as the default view,
and the view named "All" will I<not> be automatically generated.  If this
qualifier is not used, the the view named "All" will be the default.

=item RefreshRate

The number of minutes between automatic refresh of the CGI page.  B<Default
value is C<30m>.> 

=item Blurb

A (potentially long) string that is printed near the I<top> of the CGI page.
B<Default value is the empty string.>  The string may contain HTML elements.

=item Blurb2

A (potentially long) string that is printed near the I<bottom> of the CGI page.
B<Default value is the empty string.>  The string may contain HTML elements.

=item SyslogFacility

The facility to which syslog messages are logged.  Syslog is mainly used for
C<Alarm> conditions, but is also used for sensor failure, etc.  You can
specify the facility which thermd uses.  Your choices are console, daemon,
user, and local0 through local7.  B<The default value is C<user>.>  This
option is ignored on Windows machines.

=item SNMPTrapPort

The port number to use for SNMP traps.  These are presently only used for
the OnOff sensors for the Poseidon and Damocles series from HWg (future
expansion depends on user feedback).  B<NOTE:> You can use SNMP traps wven if
you use the HTTP SubType for the Poseidon and Damocles series.

=for html <ul>

=for html <li>

For all SNMP-based devices, the default SNMP trap port is 162.  Consequently,
B<the default value for SNMPTrapPort is 162.>

=for html <li>

Port 162 is a "privileged port", which means that if you run I<thermd> with
a UID other than root, you will need to choose a different port for I<thermd>
to use, and you will need to configure your Poseidon or Damocles to send traps
to this alternate address.

=for html <li>

Thermd has a built-in
SNMP trap handler (that uses I<snmptrapd> as its core), but if you are also
running I<snmptrapd> on your system, you will probably need to choose a
different port for I<thermd> to use, and you will need to configure your
Poseidon or Damocles to send traps to this alternate address.

=for html <li>

You must be running at least version 5.3.0 of NET-SNMP in order to use SNMP
traps.  See also C<AllowSNMPTraps> in the C<OnOff> sensor.

=for html <li>

You must configure your SNMP-enabled device to I<send> SNMP traps - they do
not happen by default.

=for html </ul>

There is only one trap handler used in I<thermd>, regardless of how many
collectors exist that can generate SNMP traps (that is, one handler is
responsible for all the traps, and it will figure out which device has sent
the trap).  This means that B<I<all>> SNMP devices that I<thermd> monitors
for traps need to be configured to send traps to this same port.

=back

=for html </dd>

=head2 RSS Block

If it is present, then the RSS block defines the directory where the RSS
information is written.  If the RSS block is missing, no RSS feed will be
generated.  Note that it is an RSS I<block>, so you must say something like:

    <RSS /var/www/KLEIN/thermd>
	URL		http://www.klein.com/thermd/
	Webmaster	dan@klein.com
    </RSS>

Unless some other View is marked as the C<DefaultView>, the C<View> named
'all' is automatically included in the RSS feed.  See the
C<View> block and the C<RSSName> directive below to learn what else can be
shown in the RSS feed.

=over 4

=for html <dd>

=item URL

The URL that is listed in the generated XML  files.  B<If there is an RSS
block, then this field is required and has no default value.>

=item Webmaster

The email address of the webmaster.  B<If there is an RSS block, then this
field is required and has no default value.>

=item Every

If present, the value of the Every attribute says "Every N times the logfiles
are written, also generate the RSS files".  B<The default value is 1>, but if
you specify a small LogInterval, you may want to say "Every 2" or "Every 3" to
reduce your CPU load a bit...

=item Nice

In order to conserve CPU resources, the RSS-generting commands are run with
the I<nice> command.  However, some low-horsepower machines may need to
throttle back even more.  If present, the value of the Nice attribute is the
nicevalue parameter added to the I<nice> command.  B<The default value is
blank>, which means that the RSS-generating commands will be run at the
default nice value.  B<NOTE:> The format of the nicevalue argument has become
nonstandard across operating systems.  Look at the man-pages for your system
and shell before using this, and check the syslog files for errors after
changing it.

=back

=for html </dd>

=head2 Collector, Sensor, and Actuator Blocks

C<Thermd> supports multiple collectors (something that has one or more
sensors on it), and multiple sensors (something that reports a temperature,
humidity, wind speed, etc.).  Your sensor network can be homo- or heterogenous
(my development network has had dozens of sensors on 9 collectors of 7
different types active at one time).

Collector blocks are named (and the name can be anything you like, as long as
each collector has a unique name)), so for example you would say:

    <Collector Fred>
	Type	QK145
	Device	/dev/cuaa0
	Scale	F
	<Sensor 1>
	    Name	"Computer exhaust"
	</Sensor>
	<Sensor 2>
	    Name	Refrigerator
	    Adjust	-1
	</Sensor>
	...
    </Collector>

    <Collector Ethel>
	Type		HA7Net
	IPAddress	wumpus.klein.com
	<Sensor 68000800141A8B10>
	    Name	Basement
	    Type	Temperature
	</Sensor>
	...
    </Collector>

Each collector has a number of attributes, and also has included Sensor
blocks (one for each sensor that is configured).  The attributes for a
collector block are as follows:

=over 4

=for html <dd>

=item Type

The type of the collector.  B<The Type is required, and there is no
default value.>  The valid types of collector are:

=for html <ul>

=for html <li>

C<QK145> and C<VK011>
from QKits L<http://www.qkits.com/>

=for html <li>

C<HA7Net> from Embedded Data Systems
L<http://embeddeddatasystems.com/>

=for html <li>

C<TEMP08> from Midon Design
L<http://www.midondesign.com/>

=for html <li>

C<EnerSure> from Trendpoint
L<http://www.trendpoint.com/>

=for html <li>

C<EM1> from SensaTronics
L<http://www.sensatronics.com/>

=for html <li>

C<TemPageR>, C<RoomAlert 7E>, C<RoomAlert 11E>, C<RoomAlert 24E>, and
C<RoomAlert 26W> from AVTECH Software
L<http://www.roomalert.com/>

=for html <li>

and C<MiniGoose>, C<WeatherGoose>, C<SuperGoose>
C<RacSense> and C<PowerGoose> from IT Watchdogs L<http://www.itwatchdogs.com/>

=for html <li>

C<Enersure> from Trendpoint L<http://www.trendpoint.com>

=for html <li>

C<Veris H8030>, C<Veris H8031>, C<Veris H8035>, and C<H8036> from Veris
Industries L<http://www.veris.com/modbus.asp>

=for html <li>

C<SmartNet> for the Smart-Watt, Smart-PDU, SmartSenseTH, Smart-Net sensors
from L<http://www.smart-works.com>

=for html <li>

C<MaxBotix> for any of the MaxBotix line of sonar rangefinders
from L<http://www.maxbotix.com/>.

=for html <li>

C<Proliphix> for any of the IP enabled thermostats from
L<http://www.proliphix.com>

=for html <li>

C<owfs>, C<owhttpd> or C<owshell>, which supports Serial
port
(ibuttonlink, DS9097E and DS9097) or USB (DS9490 or PuceBaboon) bus masters.

=for html <li>

C<SNMP> (for the MRV LX-4008, IR5150, or any other
SNMP-enabled device)

=for html <li>

C<Poseidon 1250>, C<Poseidon 2251>,
C<Poseidon 3262>, C<Poseidon 3265>, C<Poseidon 3266>, C<Poseidon 3266>,
C<Damocles 0808e>, C<Damocles 0816>, C<Damocles 2404>, or C<Damocles MINI>,
data collectors from L<http://www.hw-group.com/> (which primarily use SNMP for
data collection, but specifying C<Poseisdon NNNN> or C<Damocles NNNN>
establishes some defaults and additional functions available only on that
device).

=for html <li>

C<Newport iBTX>, C<Newport iBTX-M>, C<Newport iBTHX>, C<Newport iPTX-D>,
C<Newport iPTX-W>, C<Newport iTCX>, C<Newport iTHX-M>, C<Newport iTHX-W>, or
C<Newport iTHX-2> from L<http://www.newportus.com/>
and the co-branded
C<Omega iBTX>, C<Omega iBTX-M>, C<Omega iBTHX>, C<Omega iPTX-D>,
C<Omega iPTX-W>, C<Omega iTCX>, C<Omega iTHX-M>, C<Omega iTHX-W>, or
C<Omega iTHX-2> from L<http://www.omega.com>/

=for html <li>

C<CommandLine> is a special type of collector for which each sensor is read by
executing a command.  Useful for those systems which already have commands for
interfacing with devices and sensors.

=for html <li>

C<Derived> does not correspond to a specific piece of hardware, but is used
for a pseudo-collector containing only special computed sensors (that is,
sensors, whose values are based on mathematical formulae involving other
sensors.  See L<Derived Sensors> for details.

=for html </ul>

B<NOTE:> AVTECH Software used to manufacture a serial-line version of the
TemPageR.  If you have one of these, specify its C<Type> as C<QK145>, and it'll
work just fine.

=item Device

The serial device to which the collector is connected.  B<The Device is
required for the C<QK145>, C<VK011>, C<TEMP08>, C<MaxBotix>, and C<Enersure>,
and there is no default value.  For all other collector types, the C<Device>
directive is not allowed (see C<IPAddress> and C<MountPoint>, instead).>

NOTE: Thermd also supports the notion of terminal servers for serial devices.
In this way, a device like the Digi One SP
L<http://www.digi.com/products/serialservers/digionesp.jsp> can be used to
provide ether-to-serial connectivity.  Any device which requires a C<Device>
attribute can also use an C<IPaddress> and C<Port> instead.  For configurations
such as this, you probably want to configure the device for binary communication
with no flow control, and then specify the IPAddress of the device using Port
2101.

NOTE: Some terminal servers do not properly handle binary communications.
For example, older versions of the Avocent terminal servers do not pass the NUL
character.  This is not a problem for ASCII communications, but will cause
binary communications used by the Modbus to fail.  Caveat emptor!

=item BaudRate

The speed at which to communicate with the serial device.  Generally, you
should not specify a baud rate, because the defaults should work.  An
exception is when you use RealPort mode for a Digi serial interface that
works over TCP/IP.  In this case, you should try 115200 (and on Linux
systems, if this fails, try 0010002).   B<The default value for BaudRate is
2400 for the QK145, and 9600 for all other serial collectors.>

=item IPAddress

The IP Address for the HTTP-based, SNMP-based or terminal-server device that is
connected.  The address may either
be a host name (e.g., sensor.yourcompany.com) or a numeric value (e.g.,
10.0.1.2).  B<The Address is required and there is no default value.  For other
collector types, see C<Device> and C<MountPoint>, instead.>

Note: If you are concerned about speed, specify numeric addresses.  If you
want reliability, names are better.  Usually, the speed difference is
negligible, but your mileage may vary if your name server is flaky.

=item Port

The port number to use for IP based connections.  B<For most devices, the
default value is 80, the normal value for C<http>.  For SNMP-based devices,
the default value is 161.  For C<Newport> and C<Omega> collectors, the default
value is 2000.  For serial devices connected via a terminal server, there is
no default value>.  

=item Username

The username to use for devices that require a username and password (such as
the Proliphix thermostats). B<The default behavior is to not use
authentication, so the default value is "no username".>

=item Password

The password to use for devices that require a username and password (such as
the Proliphix thermostats). B<The default behavior is to not use
authentication, so the default value is "no password".>

=item MountPoint

The directory that the one wire file system is mounted for C<owfs> or the
top-level file component for C<owhttpd> (this attribute is not used for
C<owshell>).  If you specify the base directory
(that is, '/'), then
I<thermd> will read the cached values (note that you specify the cache time
when you start I<owfs> or C<owhttpd>).  If you want to have
I<thermd> read up-to-the-second values, the use the C<uncached> subdirectory
of the I<owfs> or I<owhttpd> directory as your C<MountPoint>.  For other
collector types, the MountPoint directive is not allowed (see C<Device> and
C<IPAddress>, instead).>

B<For C<owhttpd>, the default value is '/'.  For C<owfs>, there is no default
value - it must be explicitly specified.>
NOTE that for C<owhttpd>, you must also specify an C<IPAddress> and may
specify a C<Port>.

=item BaseOID

For sensors in an SNMP-based collector, the BaseOID specifies the "base
address" upon which non-rooted OIDs are based.  See the L</SNMP> section
for complete details.  B<For the Poseidon series from HWg, the default
BaseOID is C<.1.3.6.1.4.1.21796.3.3> and for the Damocles series from HWg,
the default BaseOID is C<.1.3.6.1.4.1.21796.3.4>,
(i.e., .iso.org.dod.internet.private.enterprises.hwgroup.charonII); for all
other SNMP-based collectors it is C<.1.3.6.1.2.1> (i.e.,
.iso.org.dod.internet.mgmt.mib-2).  The BaseOID is automatically set for the
the Poseidon and Damocles series from HWg, and cannot be changed.>

=item Community

For sensors in an SNMP-based collector, the Community attribute specifies
the SNMP community string.  Only SNMPv1 is currently supported.  B<The
default Community is "public".>

=item ModbusAddress

For Modbus devices, the device address on the bus (presently, only the
Enersure  and Veris use Modbus for communication).  B<The default value is 1.>

=item Scale

The degree scale that the collector I<reads> temperature in - you may choose
either C<C> or C<F> for certain collectors.  The scale that is I<reported> is
set with the C<Temperature>, C<WindSpeed> and C<Rainfall> attributes.  B<The
default value for C<Scale> is C<F>> (because I live in the United States, and
in the past 30 years, we have proven ourselves to be in incapable of
converting to the metric system, sorry, my government and business leaders
are stupid).

=over 4

=for html <dd>

=item C<QK145> and C<VK011>

You must specify the scale manually for these devices.
Yes, I know that both the QK145 and the VK011 advertise the scale
that they read in.  However, in order to ascertain that scale, you
must open the device and read from it - and if the logging daemon
is running, the reporting and CGI scripts cannot open the device.
I could change the script so that it would record the value in the
log files, but that would require users to manually alter their log
data, and that is more "risky" than many people would like.  So,
you I<must> specify the scale manually.

=item C<owfs>, C<owhttpd> and C<owshell>

The default C<Scale> is C<C>, but may be overridden.

=item  C<HA7Net>

C<Scale> is not
allowed (it only reads in C<C>).  

=item C<TEMP08>, C<RoomAlert>, C<TemPageR>, C<MiniGoose>, C<WeatherGoose>, C<SuperGoose>, C<RacSense>, C<PowerGoose>, C<HWg> and C<Enersure>

C<Scale> is also not
allowed, because it is automatically configured by C<thermd> to read in C<C>

=back

=for html </dd>

B<Note> that the scale I<thermd> reads in is distinct from the scale it
I<reports> values in - see C<English>, C<Metric>, C<Temperature>,
C<Rainfall>, etc., as well as information on Localization.  For
non-temperature devices (humidity, rainfall, power, etc.), the C<Scale>
attribute is not allowed.

=item ReadOnly

Any collector which is marked ReadOnly will not attempt to collect or log
data, but thermd will report on existing historical log data.  This is useful
if you have decommissioned a collector, but still wish to view its data.  See
also C<ReadOnly> for sensors.

NOTE that if you do not specify a sensor's name, I<thermd> will attempt
to read the sensor names from the device for some collectors.  If the
collector is marked ReadOnly, I<thermd> will not attempt to contact the
device at all (and some default names or types may be not available).

=item PollInterval

Any collector which must be polled for data (which means anything except the
C<QK145>, C<VK011>, C<MaxBotix>, or C<TEMP08>, is typically polled once every
minute or so for its data.  These values are averaged or summed and then
logged every C<LogInterval> minutes.  Setting C<PollInterval> can change the
speed at which the collector is polled.

The polling interval has two effects.  The first is how granular the logged
averages are - if you have a sensor which changes rapidly or has spiky
values, you may want a shorter C<PollInterval> to catch the changes.  Second
is how often the current-values-logfile is updated.  While the historical
logs are updated every C<LogInterval> minutes, the "current" file is updated
as fast as the most quickly changing C<PollInterval>.

C<PollInterval> may be any value between
5s (for 5 seconds) and 5m (for 5 minutes).  In no event can PollInterval be
I<longer> than C<LogInterval>.  B<The default value is such that a collector
will be polled roughly 10 times for every C<LogInterval>.  For C<SmartNet>
devices, the default value is 5m, to reduce rounding errors on the WattHour
sensor.>

=item StaleAfter

This attribute is only value for C<Wunderground> collectors, and indicates
when the collector should consider the personal weather station data to be
stale.  Each personal weather station is supposed to update its information
at least every hour, and data that is older than C<StaleAfter> will not be
reported to your local I<thermd> logfiles. B<The default value is 65m>, which
is fine for hourly-updated weather stations, but you might want to decrease
it when monitoring rapid-fire weather stations.

=item Amperage

For the Veris H8030, H8031, H8035, and H8036, this specifies the amperage at
which the Power Meter is operating.  For the H8030/H8031, the possible values
are 100 or 300; for the H8035/H8036 the possible values are 100, 300, 400,
800, 1600, and 2400.  B<There is no default value.>

=back

=for html </dd>

=head3 Sensors

Sensors report on their environment - for example, temperature, humidity, etc.
(some collectors also have C<Actuator>s, see below).
A sensor block (which must have a name) specifies all the attributes of a
specific sensor.  The sensor naming system depends on the collector, but each
sensor must have a unique name:

=over 4

=for html <dd>

=item C<QK145> and C<VK011>

For the C<QK145> and the C<VK011>, the sensor name must be an
integer number between 1..4, and corresponds to the physical sensor number.

=item C<MaxBotix>

For the C<MaxBotix>, there is only one sensor, and it is named C<Range>
(you must nevertheless still specify both a Collector and a Sensor).

=item One Wire bus devices

For the C<HA7Net>, C<SmartNet> family, C<WeatherGoose> family, 
C<RacSense>, C<PowerGoose>, C<owfs>, and C<TEMP08>, the sensor name must be
the 16-digit hexadecimal 1-wire sensor serial number (for C<owfs> and
C<owhttpd>, this number may be in any of the allowed C<f[.]i[[.]c]> formats;
for the C<SmartNet>, sensors on a SmartPDU may have a hyphenated suffix).

Some sensors measure more than one thing (for example, temperature and humidity
on DS2438-based sensors, pressure and temperature for the AAG TAI8570 and for
the multiple built-in sensors for the C<RacSense>, C<WeatherGoose> or
C<SmartNet> family). For these sensors, you must specify distinct sensor blocks
to read each value, and the I<thermd> sensor names must be unique.  To do this,
append a C<.N> to the hexadecimal sensor number, where C<N> is a number or
letter (see the L</Sample configuration file> for an example).

=item C<Enersure>

For the C<Enersure>, the sensor names are of the format C<Key>I<B<n>>,
where C<Key> is one of:

	V (for VRMS)
	I (for IRMS)
	P (for Power Factor)
	W (for Watts)
	K (for Watt Hours)

and I<B<n>> is a number between 1 and 84 (up to 4 analog boards with up to 21
circuits monitored per board).  Thus for circuit #1, there could be up to 5
sensors named V1, I1, P1, W1, and K1 (then V2, I2, etc. for circuit #2, and
so on).

=item Veris

For the Veris H8030 and Veris H8035, the sensor names are the Modbus
register address between 40001 and 40003

For the Veris H8031 and Veris H8036, the sensor names are the Modbus 
register address between 40001 and 40027.

In all cases, the type of the sensor type is directly corellated with the value
that can be read from it.  B<NOTE:> On the hardware device, register 40001 and
40002 are the LSW and MSW of the Energy consumption, respectively.  In
C<thermd>, sensor 40001 is the combination of both values, and sensor 40002
is not accessable.  B<NOTE:> these are the integer registers - I<thermd> does
not use the floating point registers, but instead reads the integer values and
multipies by a constant determined by the C<Amperage> setting for the unit).

=item EM1

For the C<EM1>, the sensor name must be one of T1, H1, W1, T2, H2, W2, T3, H3,
W3, T4, H4, or W4 (for Block 1 temperature, humidity, and wetness sensors,
etc).

=item Proliphix

For the C<Proliphix> thermostats, the sensor name must be one of T1, T2, or
T3 for temperature (external sensors T2 and T3 are not available on the NT10e
nor the NT100e/h), or H1 (for the external humidity sensor on the NT150e/h or
TM250e/h).  Additionally, the sensor TA is the averaged temperature as
returned by the Proliphix.  This is available for all models, but only really
makes sense for those thermostats with external sensors that are enabled for
averaging.

Additionally, the sensor name may be C<S>, which is the state of the HVAC,
and can have the following values: 1=Initializing, 2=Off, 3=Heat, 4=Heat2
(professional series only), 5=Heat3 (Heat Pump only), 6=Cool, 7=Cool2
(professional series only), 8=Delay, 9=ResetRelays.  I<Future versions of
thermd will make prettier graphs using these values.>

The device type must be specified as a full model number (e.g.,
I<Proliphix NT120h> or I<Proliphix 10e>).

=item AVTECH Devices

=over 4

=for html <dd>

=item C<TemPageR>

For the C<TemPageR>, the sensor name must be one of T1 (temperature sensor
in plug #1), T2, T3, or T4.

=item C<RoomAlert 7E>

For the C<RoomAlert 7E>, the sensor name must be one of T1, T2, T3, T4
(temperature), S1, S2, or S3 (switch).

=item C<RoomAlert 11E>

For the C<RoomAlert 11E>, the sensor name must be one of T1, T2, T3
(temperature), H1, H2, H3 (humidity), S1, S2 ... S8 (switch).

=item C<RoomAlert 24e>

For the C<RoomAlert 24e>, the sensor name must be one of T1, T2 ... T6
(temperature), H1, H2 ... H6 (humidity), S1, S2 ... S16 (switch),
InternalT or InternalH..

=item C<RoomAlert 26W>

For the C<RoomAlert 26W>, the sensor name must be one of T0 (the internal
temperature sensor) T1, T2 ... T6 (external temperature sensors), H0 (the
internal humidity sensor) H1, H2 ... H6 (external humidity sensors), S1,
S2 ... S16 (switch), Flood, or Power.  Additionally, WiSH sensors are
identified by their 12-hexdigit serial numbers and the sensor type.

The sensor type may be T0 for internal temperature sensor or T1, T2, H1, H2,
or S1 for external temperature, humidity or switch sensors (for example,
"DAEDA8000000T0" is the temperature sensor inside WiSH serial number
DAEDA8000000, and "DAEDA8000000T1" and "DAEDA8000000H1" are temperature and
humidity for external device #1 connected to that WiSH.

=back

=for html </dd>

=item C<Newport and Omega iServer Microservers>

B<NOTE:> Almost all of the Newport and Omega iServer Microservers all provide
a means for reading temperature (and dewpoint) in either Celsius or
Fahrenheit, and pressure in inHG, mmHg, kPA, or PSI (depending on what is
being measured).  However, thermd only monitors in Celsius, inHG or kPA, and
then displays the monitored values in the units of your choice.  Therefore,
use SRTC for temperature readings and not SRTF, etc.  If you I<must> override
the locale settings for graphing, see the C<DisplayIn> directive.

=over 4

=for html <dd>

=item C<iBTHX> and C<IBTX>

For the C<iBTX> and C<iBTHX>, the sensor name may
be SRTC for temperature, SRH2 for humidity, SRDC2 for dewpoint, and SRHi for
barometric pressure. 

=item C<IBTX-M>

For the C<iBTX-M> the sensor name may be SRTC for temperature, or
SRHi for barometric pressure.

=item C<IPTX-D> and C<IPTX-W>

For the C<iPTX-D> and C<IPTX-W> the sensor name may
be SRTC for temperature or SRHb for pressure.

=item C<ICTX>

For the C<iCTX> the sensor name may be SRTC, SRHC, or SRDC for the
three temperatures available on the device.

=item C<iTHX-M>

For the C<iTHX-M> the sensor name may be SRT for temperature, SRH for
humidity, SRD for dewpoint.

=item C<iTHX-W> and C<iTHX-2>

For the C<iTHX-W> and C<iTHX-2> the sensor name may be SRTC
or SRTC2 for temperature, SRH or SRH2 for humidity, and SRDC or SRDC2 for
dewpoint.

=back

=item C<Poseidon> and C<Damocles> series from C<HWg>

For the Poseidon series from HWg, sensor names must either be the letter
B<B> followed by a single digit (for dry contact inputs) or the 2 through
5-digit Sensor IDs.

For the Damocles series from HWg, sensor names must be the letter B<B>
followed by one or two digits (for binary dry contact inputs).

The sensor type (which may be explicitly specified for clarity with SubType SNMP
configuratuon, but which is automatically checked for validity; and which
I<must> be explicitly set if you use SubType HTTP configuration) and OID are
dynamically determined based on the
sensor names.  Note that the HWg collectors use SNMP or HTTP for
communication, but many of the details of SNMP are hidden from view to ease configuration.

One detail that is exposed is the SNMP trap handling behavior (which is
usable regardless of query SubType).  See the
C<SNMPTrapPort> global attribute and C<AllowSNMPTraps> in the C<OnOff> sensor
type for details.

=item Other SNMP-based devices

For all other SNMP-based devices, the sensor name must simply be an
alphanumeric string.  We recommend using the OID name,
but any alphanumeric string is acceptable.  See also the C<Instance> block.

=item C<CommandLine> collector

The sensor name is any mix of letters, digits, and underscores you wish to use.
See the C<Command> attribute for important details.

=item C<Wunderground> collector

The Wunderground collectors allow you to read data published by other
personal weather stations (see the special C<StaleAfter> attribute).
This enables you to compare other stations' data
with your own.  The sensor names are limited to 7 types, and must be:
B<T> for temperature, B<H> for humidity, B<B> for barometer, B<P> for
dewpoint, B<S> for wind
speed, B<G> for wind gust, and B<D> for wind direction.

=back

=for html </dd>

=head3 Sensor Attributes

Regardless of the collector or sensor type, the sensor attributes are:

=over 4

=for html <dd>

=item Name

A descriptive name for the sensor.  You can use any description you like.
Some data collectors (the EM1, SmartNet, RoomAlert series, the Poseidon and
Damocles series from HWg) have the ability to configure the sensor name on the
data collector.  B<For these collectors, the default value will be obtained
from the collector.  For all other collectors, the default value is Sensor
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name.>

=item Type

The kind of sensor this is.  B<This field is optional for DS1820, DS18B20 and
DS1822 sensors (since they can only be temperature sensors), for all sensors
on the QK145 and VK011 (since they can only be temperature sensors), for all
sensors on the C<Enersure>, C<RoomAlert>, C<EM1> or C<Proliphix> (since their
name specifies their type), and for all sensors on the Poseidon and Damocles
series from HWg (since the sensor type can be queried from the device),
and for the Range sensor on the C<MaxBotix> sonar sensor,
but is I<required> for other sensors.>  (For devices that do not use 1wire
sensors, ignore the "DSnnnn" numbers).  The valid types are:

=over 4

=for html <dd>

=item Temperature

This is the default for all DS1820, DS18S20, DS18B20, DS1822 or DS2760 devices
(and for the temperature sensors in the C<QK145>, C<VK011>, C<RoomAlert>,
C<EM1> and C<Proliphix>) but still needs to be specified for DS2438
temperature + A/D devices.  For the C<TEMP08>, if Temperature is the only type
specified for a dual sensor, the only temperature will be recorded.

NOTE: for DS2760 devices, a C<Thermocouple> type must also be specified
(presently only available for C<owfs>, C<owhttpd>, and C<HA7Net> data
collectors).

=item Humidity

For the C<TEMP08>, C<HA7Net> and C<OWFS>, a single DS2438 can provide two
measurements (temperature and humidity).  For the Poseidon series from HWg,
the DS2438 sensors only measure humidity.  For the C<WeatherGoose> (etc.), and
the C<RacSense>, some sensors can
also read airflow.  If you wish to log humidity, use this type
(and use temperature with a separate sensor ID for the other measurement from
the DS2438 sensor).  For the C<HA7Net>, a DS2406 or DS2407 is also
used to measure humidity, and has a single sensor per device.  For the
C<RoomAlert>, C<EM1> and Proliphix>, humidity is measured with a device with a
single address.

=item DewPoint

This sensor is available natively on the C<SmartSenseTH>, and is available
as a built-in computation for any pair of temperature and humidity devices.
See C<Derived Sensors> for details.

=item Range

This sensor is only available on the C<MaxBotix> line of sonar rangefinders.

=item Humidex

This sensor is available as a built-in computation for any pair of temperature
and humidity devices.  See C<Derived Sensors> for details.

=item HeatIndex

This sensor is available as a built-in computation for any pair of temperature
and humidity devices.  See C<Derived Sensors> for details.

=item Barometer

For the C<TEMP08>, C<HA7Net> and C<OWFS>, a Hobby Boards barometer sensor
(built on the DS2438) can provide two measurements.  If you wish to log
barometric pressure, use this type (temperature is the other
measurement from the DS2438 sensor).  See also C<Slope> and C<Intercept>
for barometer calibration.

=item Sunlight

For the C<HA7Net>, a Hobby Boards solar radiation detector (built on the
DS2438) can provide two measurements.  If you wish to log sunlight, use this
type (temperature is the other measurement from the DS2438 sensor).  I<See
also C<MultiplyBy>.>

=item Direction

For the One Wire Weather device, wind direction is measured using a DS2450.
I<See also C<AdjustBy> and C<Inverted>.>

=item Speed

For the One Wire Weather device, wind speed is measured with a DS2423.  If
you specify type "Speed", only speed (and not gusts) will be recorded.
See next for how to record gusts with a re-specification of the same sensor
name and a numeric suffix.  See also C<Channel> (which defaults to Channel A).

The Speed sensor is implemented internally as a Counter with a SubType
"AvgRate".

=item Gust

This type allows you to record gusts in wind speed from the One Wire Weather
device.  See also C<Channel> (which defaults to Channel A).

The Gust sensor is implemented internally as a Counter with a SubType "MaxRate".

=item WindChill

This sensor is available as a built-in computation for any pair of temperature
and wind speed devices.  See C<Derived Sensors> for details.

=item Rain

AAG sells a TAI8575B rain gauge that is based on the La Crosse Technology
WS-7047U.  The gauge uses a DS2423 to measure 0.01" increments in rainfall.
If you specify type "Rain", then this device is assumed.  See also
C<InactivityReset> (which defaults to 12h for rain sensors) and C<Channel> 
(which defaults to Channel A).

The Rain sensor is implemented internally as a Counter with a SubType
"Total".

=item Lightning

The lightning gauge uses a DS2423 to count nearby lightning strikes.
If you specify type "Lightning", then this device is assumed.  See also
C<Channel> (which defaults to Channel A).

The Lightning sensor is implemented internally as a Counter with a SubType
"Count".

=item Wetness

This sensor type is currently only valid for the wetness sensor on the C<EM1>.
If you have a HobbyBoards Leaf Wetness sensor, please contact me for support.

=item OnOff

This sensor is a on/off sensor currently supported only on the Poseidon and
Damocles collectors from C<HWg>, the C<HA7Net>,
and the C<Room Alert>.  A closed (shorted) sensor is considered "on".
I<See also C<OnValue> and C<OffValue>.  For C<SNMP> devices, see also
C<SNMPOnValue> and C<SNMPOffValue>.  For the C<HA7Net>, see also C<PIO>.>

For the C<Poseison> and the C<Damocles> series from HWg, the sensor name
must be the letter B<B> followed by a one or two digit sensor number (B1,
B2, etc.).  They are normally only
read when the device is polled.  This means that if your C<PollInterval> is
C<1m>, changes in OnOff sensors will only be tested every minute.  If the
OnOff sensor is connected to a door-sensor, and the door opens and closes
between readings, the state-change will not be detected.

However, I<thermd> can also use an SNMP trap handler to register changes to
the OnOff sensors.  You must configure your Poseidon or Damocles collectors to
send SNMP traps to the same machine that I<thermd> is running on.  See the
global C<TrapPort> attribute and the sensor-specific C<AllowTraps>.

=item Airflow

This is the airflow sensor available only with the C<WeatherGoose> and
C<RacSense> family, and measures relative airflow.

=item Sound

This is the sound sensor available only with the C<WeatherGoose> 
and C<RacSense> family.

=item Light

This is the light sensor available only with the C<WeatherGoose> 
and C<RacSense> family.

=item IO

This is the IO sensor available only with the C<WeatherGoose> 
and C<RacSense> family.

=item Volts

This is the voltage sensor available only with the C<PowerGoose>, C<Veris>,
C<RacSense>, C<EnerSure>, and C<Poseidon> series.

=item Volts-Min

This is the voltage sensor available only with the C<PowerGoose>
and C<RacSense>.

=item Volts-Max

This is the voltage sensor available only with the C<PowerGoose>
and C<RacSense>.

=item Volts-Peak

This is the voltage sensor available only with the C<PowerGoose>
and C<RacSense>.

=item VA

This is the volt-amp sensor available only with the C<Veris>.

=item VAR

This is the reactive power volt-amp sensor available only with the C<Veris>.

=item Amps

This is the amperage sensor available only with the C<PowerGoose>, C<Veris>,
C<RacSense>, and C<EnerSure>.

=item Amps-Peak

This is the amperage sensor available only with the C<PowerGoose>
and C<RacSense>.

=item MilliAmps

This is the milliamperage sensor available only with the C<Poseidon> series.

=item Real-Power

This is the power sensor available only with the C<PowerGoose>
and C<RacSense>.

=item Apparent-Power

This is the power sensor available only with the C<PowerGoose>
and C<RacSense>.

=item Watts

This is the wattage sensor available on the C<EnerSure> (and is a derived
value for the C<SmartWatt>).  Wattage is logged as the average value measured
over the C<LogInterval>.

=item KWatts

This is the wattage sensor available on the C<Veris>.

=item Wh

This is the watt-hour sensor available with the C<EnerSure> and C<SmartWatt>.
Watt-hours measured cumulatively over the C<LogInterval>.

=item KWh

This is the Kwatt-hour sensor available with the C<RacSense> and C<Veris>.

=item Power-Factor

This is the power sensor available only with the C<PowerGoose>, C<Veris>,
C<RacSense>, and C<EnerSure>.

=item Counter

This is a generic use of a DS2423 2-channel counter, or a generic SNMP
counter value (that is, a reading whose current value is measured relative
to the previous value, and which recognizes rollover of a 32-bit numbers).
I<See also C<SubType>, C<Channel>, C<MultiplyBy>, C<Units>, and
C<InactivityReset>.>

Counters have five distinct C<SubType>s, one of which B<must> be chosen.

=over 4

=for html <dd>

=item AvgRate

The value supplied by the counter is the I<average> number of counts per second
for the logging period (I<see> C<LogInterval>).  You may wish to adjust the
value to an appropriate C<Scale> by using C<MultiplyBy>.

The C<Speed> sensor is implemented as a special "AvgRate" counter.

=item MinRate

The value supplied by the counter is the I<minimum> number of counts per second
for the logging period (I<see> C<LogInterval>), measured every C<PollInterval>.
You may wish to adjust the value to an appropriate C<scale> by using
C<MultiplyBy>.

=item MaxRate

The value supplied by the counter is the I<maximum> number of counts per second
for the logging period (I<see> C<LogInterval>), measured every C<PollInterval>.
You may wish to adjust the value to an appropriate C<scale> by using
C<MultiplyBy>.

The C<Gust> sensor is implemented as a special "MaxRate" counter.

=item Count

The value supplied by the counter is the number of counts for the logging
period (I<see> C<LogInterval>).  You may wish to adjust the  value to an
appropriate C<Scale> by using C<MultiplyBy>.

The C<Lightning> sensor is implemented as a special "Count" counter.

=item Total

The value supplied by the counter is the I<sum> of number of counts.  You
must also specify a value for C<InactvityReset>, which is the the time after
which (if there is no activity on the counter), the accumulated value will be
reset to 0.  You may wish to adjust the value to an appropriate C<Scale>
by using C<MultiplyBy>.

The C<Rain> sensor is implemented as a special "Total" counter.

=item Raw

The value supplied by the counter is the raw number of counts provided by
the counter.  For those counters with a "memory" (that is, a battery backup),
the value will never decrease until it wraps around (typically when it
exceeds a 32-bit signed or unsigned number, either 2,147,483,648 or
4,294,967,296).  You may wish to adjust the  value to an appropriate C<Scale>
by using C<MultiplyBy>.

=back

=for html </dd>

=item Gauge

This is a generic SNMP gauge (that is, a reading whose current value is
significant, irrespective of the previous value).  I<See also C<MultiplyBy>
and C<Units>.>

The C<Gauge> sensor is implemented as a special "Raw" counter.

=item Math

This sensor provides a mathematical computation based on other sensors.  See
C<Derived Sensors> for details.

=back

=for html </dd>

=item GraphColor

The color that this sensor is graphed in (see also C<Show+> for the ability to
override this value in a specific C<View>). Your choices are C<red>, C<teal>,
C<blue>, C<fuschia>, C<olive>, C<aqua>, C<green>, C<black>, C<orange>,
C<purple>, C<silver>, C<white>, C<pink>, C<yellow>, C<lime>, C<gray>, C<navy>,
and C<maroon>.  B<The default value is an as-yet unused color (excluding
C<white>, C<gray>, or C<silver>).> Note: due to the non-linear nature of
parsing the configuration file, we cannot guarantee that colors will be unique
unless I<every> color is automatically selected.  We therefore make a
best-effort to pick an unused color, but mistakes are guaranteed to be
I<possible>.

If you do not like the colors listed above, you can specify a color as a
6-digit hexadecimal RGB color starting with a #-sign.  Note that you need to
escape the #-sign (since it is a comment character) as:

	GraphColor	\#a0a0FF

=item LineType

The type of line that this sensor is graphed in (see also C<Show+> for the
ability to override this value in a specific C<View>).  Your choices are
C<solid> or C<dotted>.  B<The default value is C<solid>.>  The values of
C<dashed> and C<dotdashed> may also be used, but these do not show up well. 

=item PopUp

If this attribute is present, a popup tooltip will appear when you
mouse-over the current readings of the sensor.   PopUp may contain multiple
lines of text if you use the "here document" format (see C<Blurb> in the
sample config file below.  B<Default value is no popup.>

NOTE:  Single quote characters must be escaped, as B<\'>.

=item LogFile

Where the data is logged.  An absolute filename (that is, a file starting
with a C<'/'>) is put where you say, otherwise the filenames are relative to
C<LogRead> and C<LogWrite>, as appropriate.  B<The default filename is in a
directory with the same name as the collector, and a filename the same as the
sensor name.>  So for

	<Collector Upstairs>
	    <Sensor 1>

the default logfile would be I<$LogRead>/Upstairs/1.  B<Each sensor must
have a unique filename.>

=item ReadOnly

Any Sensor which is marked ReadOnly will not collect or log data, but thermd
will report on existing historical log data.  This is useful if you have
decommissioned a sensor (or it is behaving erratically), but still wish to
view it's data.  See also C<ReadOnly> for collectors.

=item AdjustBy

The number to add (or subtract) from every data reading.  Not all DS1820's are
the same, and there is an expected variance (likewise, humidity and barometer
sensors may vary).  If you calibrate your sensors, then this is a means
whereby you can define that calibration.  Note that the value is adjusted
I<after> the value is read, but I<before> it is logged, so your logs will
reflect the adjusted values.  Note also that the scale of C<AdjustBy> is the
same as whatever C<Scale> your collector is configured for.  Thermd does not
allow you to adjust wind speed, wind gust, lightning, rainfall, or on/off
sensors.  I<For Counter sensors, see C<MultiplyBy>; for OnOff sensors, see
C<OnValue> and C<OffValue>; for Direction
sensors, C<AdjustBy> is always calculated so that the direction value will
always be in the range of [0,360).  If you use the Temp08 collector, you
should probably use the C<NOR> command on the Temp08 to set the North value.>

=item MultiplyBy

For counters, the value to attribute to each "click" of the counter.  For
gauges, the scaling factor for the gauge.  For Sunlight sensors, the scaling
factor for the raw current measurement.

For example, certain European power meters have an LED that blinks 500 times
for every kWH (and with a photodetector coupled with a counter, you can measure
your power consumption).  With a C<LogInterval> of 5 minutes, a MultiplyBy of
24 will display the counter value as Watts.  The MultiplyBy value may be
fractional value if you really want to divide instead.  B<The default value
is 1.  MultiplyBy may not be 0.>

=item Inverted

In rare cases the sensor board in the OneWireWeather system is mounted upside
down (or maybe you mounted the whole unit upside down - the anemometer goes
on top!).  There is a C<WDR> command on the Temp08 to handle this, and the
C<Inverted> attribute will do the same.

=item Resolution

I<This attribute is only valid for DS18B20 and DS1822 sensors and the
C<HA7Net>, C<owfs> and C<owhttpd> collectors.>
It specifies the number of bits of resolution that the DS18B20 or DS1822
should read
(the fewer bits, the faster the sensor may be polled).  B<The default
resolution is 12 bits, and if specified, must be a value between 9 and 12.>

=item Thermocouple

I<This attribute is only valid for DS2790 sensors and C<owfs>, C<owhttpd>,
and C<HA7Net> collectors.>  It specifies the thermocouple type being used,
and must be one of B, E, J, K, N, R, S or T.  B<There is no default value.>

=item InactivityReset

I<This attribute is only valid for Rain and Counter (SubType Total) sensors.> 
Since rainfall is measured cumulatively for at most a period of time (and not
forever), the InactivityReset attribute allows you to specify a time
after which the counter will be reset.  When there has not been any activity
on a counter for the InactivityReset period (that is, the counter has not
increased at all), it will be reset to zero.  B<The default value is 12h for
C<Rain>, but must be specified for any other Total type Counter.>

=item Channel

I<This attribute is valid for Lightning, Rain, Speed, Gust, and Counter
sensors.>  Since DS2423 sensors have two counters, you must specifiy either
I<Channel A> or I<Channel B>.  B<The default value for Lignting, Rain, Speed,
and Gust sensors is I<Channel A> (since they come pre-built and usually use
Channel A), but there is no default value for Counters (since they are usually home-brew and can use either or both channels).>

=item Hide

By default, C<thermd> puts all sensors into a view named "All" (see the
C<View> documentation, below).  If you wish to exclude a sensor from this
global view, the C<Hide> attribute is used (or you can disable the "All" view
by specifying that another view is the C<DefaultView>.  Note that data is
still collected for the sensor even if it is hidden!  However, unless the
sensor is explicitly placed in a C<View>, the data will be invisible.

=item OnValue

For sensors of type C<OnOff>, this attribute specifies the numeric value the
sensor will be I<logged at> when it is on.  Optionally combined with
C<OffValue>, these allow you to graph many C<OnOff> sensors on the same graph
and still be able to see them as separate lines.  B<The default value for
C<OnValue> is 1.>

=item OffValue

For sensors of type C<OnOff>, this attribute specifies the numeric value the
sensor will be I<logged at> when it is off.  B<The default value for
C<OffValue> is 0.>

=item SNMPOnValue

For C<SNMP> sensors of type C<OnOff>, this attribute specifies the numeric
value that the sensor I<reports> when it is on.  B<The default value is 1.>

=item SNMPOffValue

For C<SNMP> sensors of type C<OnOff>, this attribute specifies the numeric
value that the sensor I<reports> when it is off.  B<The default value is 2.>

=item AllowSNMPTraps

For C<SNMP> sensors of type C<OnOff> (this includes the Poseison and Damocles
data collectors), this attribute states that the sensor will also respond to
SNMP Traps (see C<SNMPTrapPort>) in addition to changes detected by
periodically polling its value.

You must configure your SNMP-enabled device to I<send> SNMP traps - they do
not happen by default.  The C<AllowSNMPTraps> directive merely instructs
Ithermd> to pay attention to them if they are received.

=item PIO

For sensors of type C<OnOff> on the C<HA7Net> (the C<D2P> family, built from
the DS2406 with two PIO sensors), this attribute specifies the PIO channel. 
The allowable values are C<A> or C<B>.  B<There is no default value for the
C<PIO> attribute, and it is required for the C<D2P> sensors.>

=item OID

For sensors in an SNMP-based collector,
the OID specifies the "address" to read.  The OID may be relative to the
C<BaseOID> or absolute.  See the L</SNMP> section for complete details.
For the Poseidon and Damocles series from HWg, the OID is not used - instead
the sensor name is used to automatically determine the OID.

=item Instance

For sensors in an SNMP-based collector, an Instance block is used to define
an address class.  See the L</SNMP> section for complete details.  This is
not used for the Poseidon and Damocles series from HWg.

=item Units

For sensors of type C<Counter> or C<Gauge>, the units that measurements are
reported in (for example 'W' for Watts, 'F' for degrees Fahrenheit, or 'Bq'
for Becquerel, number of recorded decays per second in a Geiger counter).
B<The default values are C<Count> and C<Gauge>, respectively.>

=item Slope

I<This attribute is only valid for DS2438-based barometer sensors.>
For sensors of type C<Barometer> (presently, this is only supported as a
HobbyBoards sensor
L<http://www.hobby-boards.com/catalog/product_info.php?cPath=22&products_id=36>
on the C<HA7Net>), you must specify a slope and intercept value for
barometric pressure caluculations.  If you have purchased your barometer as a
precalibrated product, these numbers can be calculated at
L<http://www.hobby-boards.com/catalog/baro_calc.php>.  If you have built your
barometer as a kit, or have changed your elevation, you must first
(re)calibrate your barometer via this page:
L<http://www.hobby-boards.com/catalog/howto_barometer.php>.  Once you have
done this, specify the C<Slope> and an C<Intercept> attributes that have been
computed.

B<There is no default value, and a Slope must be specified for all
sensors of type Barometer.  Incorrect values will result in errorneous
barometric pressure readings.>

=item Intercept

I<This attribute is only valid for DS2438-based barometer sensors.>
See C<Slope> (above) for an explanation of Intercept.

B<There is no default value, and an Intercept must be specified for all
sensors of type Barometer.  Incorrect values will result in errorneous
barometric pressure readings.>

=item Command

I<This attribute is valid only for sensors in a C<CommandLine> collector.>
This causes the specified command to be run (which output a single value on
a single line).  The command may be a pipeline to edit the output of a more
general command.  B<NOTE> that the command(s) run with the same UID as thermd,
and depending on your configuration, this may present a security risk to your
system!  The program string is passed I<UNEDITED> and I<UNCHECKED> to the
shell.  Caveat Emptor!

	<Collector DiskTemperartures>
	    Type        CommandLine
	    <Sensor Temp_ad0>
		Name    "DiskTemp-ad0"
		Type    Temperature
		Command "/usr/local/sbin/smartctl --attributes /dev/ad0 | egrep '^194' | cut -c88-"
	    </Sensor>
	    <Sensor Temp_ad3>
		Name    "DiskTemp-ad3"
		Type    Temperature
		Command "/usr/local/sbin/smartctl --attributes /dev/ad3 | egrep '^194' | cut -c88-"
	    </Sensor>
	</Collector>


=back

=for html </dd>

=head3 Alarms

An Alarm block defines an extraordinary condition that you want to know about.
Each Alarm must be named, and contains one or more alarm-related directives.
The name of the alarm is arbitrary, so long as no two alarms for a single
sensor have the same name.  I<It is B<legal> to specify B<multiple alarms per
sensor>>, so you can have an Above and a Below alarm, three different Above
alarms, etc.  An alarm might look like this:

	    <Alarm Roasting>
		Above	95
		ResetAt	90
		Times	08:00 - 23:59
		Notify	Fred Murwick <fred@lonewolf.com>
		Notify	fred-page@lonewolf.com, quincy-page@lonewolf.com
		Syslog	crit
		Close		A1@Mambo
		LockClose	A3@Salsa
		Open		A2@Rhumba
		Exec	"shutdown -h 5"
	    </Alarm>

This would cause an alarm to be sounded when the temperature exceeded 95
degrees, and both Fred and Quincy would receive a pager message (in addition
Fred receiving a regular email, a message being recorded in syslog, the
switches A1 on the collector named Mambo and A3 on Salsa being closed, the
switch A2 on the collector Rhumba being opened, and the "shutdown" program
will be run) - but C<only if> the alarm action occurred between 8am and
one minute before midnight.

Once triggered, the alarm condition will reset only after the temperature
drops below 90 degrees (note that resetting the alarm will also cause the
switch A1@Mambo (but not A3@Salsa) to be Opened, and A2@Rhumba to be Closed,
but the shutdown program will NOT be stopped if it is still running).

B<NOTE:> See the global configuration options C<SMTPHost> and C<Sendmail> and
the switches I<-checkconfig -email> before using email-alarms.

B<IMPLEMENTATION NOTE:> Since some sensors can occasionally give erratic
values, Alarms are (generally) only triggered after the average of 5
consecutive readings exceeds the alarm criteria.  This means that Alarms are
triggered after some delay.  If you wish a speedier response, you may
consider changing the collector's C<PollInterval>. I<If you need the ability
to have alarms trigger on the first reading that exceeds the alarm criteria,
write to dan@klein.com, and I will implement the necessary changes as a
configuration option.>

B<IMPLEMENTATION NOTE:> Alarms are not evaluated in any particular order.
Thus if one alarm opens a switch and another alarm closes a switch, and both
alarms trigger (or reset) at the same time, the final state of the switch
cannot be predicted.

The attributes for Alarm blocks are:

=over

=for comment This is so the next items get indented in HTML

=for html <dd>

=item Above

The value (in whatever C<Scale> the sensor reads) above which the
alarm will be sounded. B<There is no default value, and you must specify either
Above or C<Below> if you have an alarm block.>

=item Below

The value (in whatever C<Scale> the sensor reads) below which the
alarm will be sounded. B<There is no default value, and you must specify either
C<Above> or Below if you have an alarm block.>

=item ResetAt

The value (in whatever C<Scale> the sensor reads) at which the alarm
reset for reissuance.  If the alarm is an C<Above> alarm, then ResetAt must be
less than the Above value (and greated than a C<Below> value).  B<The default
value is 2 units above/below the alarm value.>

=item Alarm Actions

For every Alarm, you must specify at least one of the C<Notify>, C<Syslog>,
C<Open>, C<LockOpen>, C<Close>, C<LockClose>, or C<Exec> actions.  You may
specify more than one action, and more than one instance of any action..
B<There is no default alarm action - you must choose one or more action.>

=over

=for comment This is so the next items get indented in HTML

=for html <dd>

=item Notify

Who to notify (via email) if the alarm condition exists.  You may specify more
than one Notify field, and all will be notified (you can also combine
multiple Notify fields into a comma-separated list of reciepients on one line).
Email addresses are checked for compliance with
RFC822.  Note that a I<compliant> email address may still not be a I<valid>
address, but at least it stands a fighting chance!

=item Syslog I<priority>

Notify via syslog that the the alarm condition exists (see also the
C<SyslogFacility> global parameter).  The default is not to use Syslog
for alarms, although syslog will still be used to flag other conditions such
as failed sensors, system startup, restart, etc.  The
value of this attribute is the message priority.  Your choices are (in order
of increasing severity) info, notice, warning, err, crit, alert, emerg.  This
option is ignored on Windows machines.

=item Open

Cause a specied C<OnOff> switch to be opened (if the switch is defined to be
Normally Open, it will remain open).  When the alarm is reset, the same
switch will be closed.  Actuator names are referenced the same as in
C<Show> and C<Show+>, below.  You may specify more than one Open attribute.

B<NOTE:> The actuator and collector names referenced in C<Open> and
C<LockOpen> are Case SeNSiTive, and must exactly match the declared names.

=item LockOpen

Cause a specied C<OnOff> switch to be opened (if the switch is defined to be
Normally Open, it will remain open).  As opposed to C<Open>, with
C<LockOpen>, when the alarm is reset, the switch I<stays> open (if you want
it to close, you must have a different alarm to close it, or restart the
server, which will set the switch to its C<Normally> position).  Actuator
names are referenced the same as in C<Show> and C<Show+>, below.  You may
specify more than one LockOpen attribute.

=item Close

Cause a specified C<OnOff> switch to be closed (if the switch is defined to be
Normally Closed, it will remain closed).  When the alarm is reset, the same
switch will be opened.  Actuator names are referenced the same as was in
C<Show> and C<Show+>, below.  You may specify more than one Close attribute.

B<NOTE:> The actuator and collector names referenced in C<Close> and
C<LockClose> are Case SeNSiTive, and must exactly match the declared names.

=item LockClose

Cause a specied C<OnOff> switch to be closed (if the switch is defined to be
Normally Closed, it will remain closed).  As opposed to C<Close>, with
C<LockClose>, when the alarm is reset, the switch I<stays> closed (if you
want it to open, you must have a different alarm to open it, or restart the
server, which will set the switch to its C<Normally> position).  Actuator
names are referenced the same as in C<Show> and C<Show+>, below.  You may
specify more than one LockClose attribute.

=item Exec

Cause the specified program to be run.  B<NOTE> that the program runs with the
same UID as thermd, and depending on your configuration, this may present a
security risk to your system!  The program string is passed I<UNEDITED> and
I<UNCHECKED> to the shell.  Caveat Emptor!

=back

=for html </dd>

=item Dates

The (inclusive) range of dates on which the alarm will be sounded.  Dates are
represented as a hyphen-separated pair of dates, each expressed as DayNum
MonthName (where MonthName is exactly 3 letters long, for example, C<12 Aug> or
C<Aug 12>).  B<The default value is C<1 Jan - 31 Dec>.>  Dates are assumed to
be sequential, so it is legal to have a date range that wraps around the new
year, for example C<21 Dec - 21 Mar>.

I<The month names used in Dates may either be the English abbreviations or if
the appropriate internationalization code exists, the locale month names of
the server (so on a German server, you can use either of C<31 Oct - 25 Dec>
or C<31 Okt - 25 Dez>).>

=item Times

The (inclusive) range of times on which the alarm will be sounded.  Times are
represented as a hyphen-separated pair of times, each expressed as Hour:Minute
in 24-hour format (for example, 13:27).  B<The default value is C<00:00 -
23:59>.>  Times are assumed to be sequential, so it is legal to have a time
range that wraps around midnight, for example C<23:00 - 01:15>.

=item Subject

If the C<Notify> action is selected for an alarm, this attribute specifies
the subject message in the alarm email.  B<The default value is
"Temperature Alert!".>

=item Message

If the C<Notify> action is selected for an alarm, this attribute specifies
the message text in the alarm email.  This field is a C<printf> string, and
has three parameters - the name of the sensor, the temperature, and the scale.
B<The default is C<"The temperature in %s is %.3f degrees %s">.>  If you do
not want to print a field (because you want to use a short version,
for example, then the C<printf> field of C<%.0s> will supress it, so your
message might be as simple as C<"Alarm 2:%.0s%d%s"> or C<"%s over temp">.
Since thermd is designed for temperature, you'll need to change the message
if you're reporting on humidity or wind speed, for example.
Remember this is written in Perl, so even though the temperature is a floating
point number, you can print it as C<%d> if you want.

=back

=for html </dd>

=head3 Derived Sensors

There is a special type of pseudo-collector whose type is C<Derived>.  Within
this collector, there can be a number of special computed sensors (that is,
sensors whose value is mathematically based on other sensors).  These sensors
have all of the standard attributes (C<Name>, C<Type>, C<GraphColor>,
C<Alarm>, etc), but also have special attributes found only in derived
sensors.  The sensor types and their special attributes are:

=over

=for html <dd>

=item Math

The Math sensor is used to compute values based on arbitrary mathematical
equations using other sensors (the sensors are referenced by their names
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).
The one special C<Math> attribute is:

=over

=for html <dd>

=item Value

This attribute contains the mathematical expression used to compute the
sensor's value.  The following example shows four real sensors on two real
collectors (Temperature, Speed and Gust are builtin types, and Slack is a
special Counter that measures the minimum windspeed for a C<LogInterval>).  It
also shows three computed sensors on a derived collector.  The first derived
sensor computes the turbulence (the difference between the highest and lowest
windspeed), while the second computes the mean of the highest and lowest
windspeed (which is different from the average windspeed calculated by the
Speed sensor).  The third sensor computes C<WindChill> (see below), and the
fourth sensor (for people living in Southern California) only shows winds
which are above 20MPH and between NNW and ENE (the Santa Ana's).

    <Collector Winken>
	Type		QK145
	<Sensor 1>
	    Name	Outside
	    Type	Temperature
	</Sensor>
    </Collector>

    <Collector Blinken>
	Type		HA7Net
	IPAddress	ha7net.klein.com
	<Sensor 1500000001622C1D.s>
	    Name	Speed
	    Type	Speed
	</Sensor>
	<Sensor 1500000001622C1D.g>
	    Name	Gust
	    Type	Gust
	</Sensor>
	<Sensor 1500000001622C1D.m>
	    Name	Slack
	    Type	Counter
	    SubType	MinRate
	    Units	MPH
	    MultiplyBy	2.453
	</Sensor>
	<Sensor C2000000012E0B20>
	    Name	Wind Direction
	    Type	Direction
	</Sensor>
    </Collector>

    <Collector ExtraStuff>
	Type Derived
	<Sensor Turbulence>
	    Type  Math
	    Value 1500000001622C1D.g@Blinken - 1500000001622C1D.m@Blinken
	    Units MPH
	</Sensor>
	<Sensor MeanSpeed>
	    Type  Math
	    Value (1500000001622C1D.g@Blinken + 1500000001622C1D.n@Blinken)/2
	    Units MPH
	</Sensor>
	<Sensor WindChill>
	    Type  WindChill
	    Temperature	1@Winken
	    Speed	1500000001622C1D.s@Blinken
	    Units C
	</Sensor>
	<Sensor SantaAna>
	    Type  Math
	    Value (C2000000012E0B20@Blinken > 355 || \
		    C2000000012E0B20@Blinken < 75) && \
		6A00000000DB141D@Blinken > 20 \
		    ? 6A00000000DB141D@Blinken \
		    : 0
	    Units MPH
	</Sensor>
    </Collector>

All of the standard Perl math operators are allowed (+ - * / % ! & | and so
on), boolean operators (&& and ||) as well as the ternary operator ?: and the
functions I<abs(x)>, I<sign(x)>,
I<min(a,b)>, I<max(a,b)>, I<sin(x)>, I<cos(x)>, I<atan(x)>, and I<log(x)>
(natural logarithm).  B<It does I<not> support full Perl syntax, of course,
but if I have missed anything that you need, please let me know!>

=back

=for html </dd>

=item WindChill

The WindChill sensor computes the wind chill using the formula found in
L<http://en.wikipedia.org/wiki/Dew_point> (B<NOTE:> the WindChill is only
computed if the temperature is between -50C/-58F and 5C/41F, and the
windspeed is at least 3KPH/1.86MPH).  The B<required> two special C<WindChill>
attributes are:

=over

=for html <dd>

=item Temperature

The name of the temperature sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=item Speed

The name of the speed  sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=back

=for html </dd>

See the C<Math> sensor for an example of C<WindChill>.

=item HeatIndex

The HeatIndex sensor computes the wind chill using the formula found in
L<http://en.wikipedia.org/wiki/Heat_index> (B<NOTE:>  the HeatIndex is only
computed if the temperature is at least 80F/26.67C and the relative
humidity is at least 40%).  The B<required> two special C<HeatIndex>
attributes are:

=over

=for html <dd>

=item Temperature

The name of the temperature sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=item Humidity

The name of the humidity  sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=back

=for html </dd>

=item Humidex

The Humidex sensor computes the wind chill using the formula found in
L<http://everything2.com/?node_id=87523> (see 
L<http://en.wikipedia.org/wiki/Humidex> for an uglier equation).  (B<NOTE:>
the Humidex is only computed if the temperture is at least 20C/68F and the
relative humidity is at least 40%).  The B<required> two special C<Humidex>
attributes are:

=over

=for html <dd>

=item Temperature

The name of the temperature sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=item Humidity

The name of the humidity  sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=back

=for html </dd>

=item DewPoint

The DewPoint sensor computes the wind chill using the formula found in
L<http://en.wikipedia.org/wiki/Dew_point> (B<NOTE:>  the DewPoint is only
computed if the temperature between 0C/32F and 60C/140F).  The B<required>
two special C<DewPoint> attributes are:

=over

=for html <dd>

=item Temperature

The name of the temperature sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=item Humidity

The name of the humidity  sensor to use for the computation (specified as
I<N>@I<K>, where I<N> is the sensor name and I<K> is the collector name).

=back

=for html </dd>

=back

=for html </dd>

=head3 Actuators and Actuator Attributes

An Actuator block is available for some Collectors (for example, the built-in
relays on the Poseidon and Damocles series from HWg, and "relays" for Modbus
devices).  Presently, actuators are used exclusively with Alarms (so when an
alarm is triggered, an actuator can be opened or closed).  This functionality
may be expanded in the future - suggestions are welcome).

Actuators are also logged (so you can graph when they are actuated or not).
With the exception of an C<Alarm> block, B<all of the attributes that are valid
for a C<Sensor> are also valid for an Actuator>.  This includes the C<Name>,
and C<Type>, but also the C<LogFile>, C<Hide>, C<ReadOnly>, C<OnValue>,
C<OffValue>, C<SNMPOnValue>, C<SNMPOffValue>, C<Instance>, C<GraphColor>, and
C<LineType> attributes (etc).

Actuators change their environment.
An Actuator block (which must have a unique name that is distinct from any
Sensor names)) specifies all the attributes of a
specific actuator.  The actuator naming system depends on the collector:

=over 4

=for html <dd>

=item C<Poseidon> and C<Damocles>

For the C<Poseison> and the C<Damocles> series from HWg, the actuator name
must be the letter B<A> followed by a one or two digit actuator number (A1,
A2, etc.).

=item C<SNMP> devices

The actuator name may be any string you like, but an C<OID> must also be
specified.

=item C<HA7Net>

The actuator name must be a 16-digit hexadecimal 1-wire sensor serial number 
(with an optionally appended a .N, where N is a number or letter, to maintain
the uniqueness of the name).  Currently, the only actuator supported on the
C<HA7Net> is the LED in the DS2760-based TAI3560 thermocouple adapter
L<http://www.aagelectronica.com/pdf%20docs/Thermocouple(TAI8560).pdf>, but
I will add support for any devices that you are willing to help me with...

=back

=for html </dd>

Regardless of the collector or actuator type, the actuator-specific attributes
are:

=over 4

=for comment This is so the next items get indented in HTML

=for html <dd>

=item Name

A descriptive name for the actuator.  You can use any description you like.
Some data collectors (e.g., the Poseidon and Damocles series from HWg)
have the ability to configure the sensor name on the data collector.
B<For these collectors, the default value will be obtained from the collector.
For all other collectors, the default value is C<Actuator I<N>@I<K>>, where
I<N> is the actuator name and I<K> is the collector name.>

=item Type

The kind of actuator this is.  The valid types are:

=over 4

=for html <dd>

=item OnOff

This is the default for all actuators in the Poseidon and Damocles series from
HWg, SNMP, and HA7Net actuators.  When Type OnOff is specified, one of
"Normally Open" and "Normally Closed" must also be specified.

=back

=for html </dd>

=item Normally

When the C<Type> of actuator is OnOff, the C<Normally> attribute specifies
what the normal state of the switch is to be (this is the state it is
initialized to when I<thermd> starts up).  You can either specify C<Normally
Open> or C<Normally Closed>.  I<See also the C<Open>, C<LockOpen>, C<Close>,
and C<LockClose> attributes of an C<Alarm>.>

=item SNMPOnValue

For C<SNMP> actuators of type C<OnOff> switch, this attribute specifies the
numeric value that the actuator must be set to in order to turn on (close) the
switch.  B<The default value is 1.>

=item SNMPOffValue

For C<SNMP> actuators of type C<OnOff> switch, this attribute specifies the
numeric value that the actuator must be set to in order to turn off (open) the
switch.  B<The default value is 2.>

=back

=for html </dd>

=head2 View Block

A View block defines a way of looking at your data.  When you have a lot of
sensors, you can have multiple Views to allow you to look at your data in a
variety of ways (for example, a view of upstairs data and a view of downstairs
data, another for inside, outside, etc.)  Each View much be named, and contains
one or more C<Show> or C<Show+> declarations.  You may use any name for the
view except C<All>, which is reserved and automatically defined to contain
every sensor (see the Sensor attribute C<Hide> for specific exceptions to this
rule).  

There are three kinds of View: B<Graph> (the default), which displays a
time-based graph of data; B<Image>, which writes only the current
temperatures on top of a picture or drawing that you supply; and
B<Wunderground>, which sends weather data to Weather Underground
(http://www.wunderground.com/) for personal weather station information (you
can also have a C<collector> of type C<Wunderground> - see above).
The Graph and Image Views are listed in the output of the CGI script, and are
also accessible from the command line via the C<-view> option with C<-graph>,
C<-annotate>, and C<-report>.  The Wunderground Views are silently sent to
Weather Underground every C<LogInterval>.

A sample graphical View block could look as follows.  This would cause the
View named "Inside" to be created, which would contain three sensors to show.
It uses simple C<Show> statements and more complicated C<Show+> blocks to
define how sensors are graphed.

    <View Inside>
	RSSName	Inside the House
	Show	1@Upstairs		# Sensor "1" on collector "Upstairs"
	<Show+	3@Upstairs>		# Sensor "3" on collector "Upstairs"
	    GraphColor	black
	    LineType	dotted
	</Show>
	Show	1@Downstairs
    </View>

B<NOTE:> The sensor and collector names referenced in C<Show> and C<Show+> are
Case SeNSiTive, and must exactly match the declared names.

Another View block could look as follows.  This would create an annotated
image, where the values of the same sensors were overlaid on the image of
the house (along with the date and time).  Image views can only use the more
complicated C<Show+> blocks to define how sensors are displayed.

    <View Overview>
	RSSName	Picture View
	Type	Image
	Image	/var/www/images/house.png
	<Date>
	    X	400
	    Y	402
	</Date>
	<Time>
	    X	400
	    Y	425
	</Time>
	<Show+	1@Upstairs>
	    X	100
	    Y	125
	</Show+>
	<Show+	3@Upstairs>
	    X	327
	    Y	125
	</Show+>
	<Show+	1@Downstairs>
	    X	95
	    Y	262
	</Show+>
    </View>

B<Note> that if graphical View contains more than one class of sensor (for
example, temperature and humidity), then the y-axis will be adjusted to contain
the range of samples.  This adjustment does not, of course, apply to
annotated images (since there is no axis).

Another View block could look as follows.  This would Send data to Weather
Underground, where most of the sensor keys are automatically determines, but
one of them is specified as sending indoor temperature.

    <View Weather>
	Type Wunderground
	StationID	KPAPITTS33
	Password	smashpunk3
	Show 1500000001622C1D.s@HA7Net	# Wind speed
	Show 1500000001622C1D.g@HA7Net	# Wind gust
	Show C2000000012E0B20@HA7Net	# Wind direction	
	<Show+	3@Upstairs>
	    Key	indoortempf		# Indoor temperature
	</Show>
	Show B80000004FC30A26@HA7Net	# Outdoor temperature
    </View>

=head3 Common View Attributes

The following attributes can be used in any C<View>:

=over

=for comment This is so the next items get indented in HTML

=for html <dd>

=item Type

The type of the C<View>, which may either be C<Graph> (a graph of historical
values) or C<Image> (an image which is annotated with the current values).
B<The default value is Graph.>

=item Show  I<(only valid when Type=Graph)>

Each show item contains the sensor@collector name of a sensor.  B<There is no
default Show value.>  Sensor names are a combination of the C<Sensor> name
and the C<Collector> name, separated by an at sign ('@'), e.g, "3@upstairs".
B<NOTE:> It is also legal (but now deprecated) to specify sensors as
"Upstairs/3".  This alternate format will soon become unavailable...

You may use a sensor in more than one view, and sensors are not required to be
present in any view (remember that the view named I<all> automatically
contains all the sensors that are not explicitly hidden with the C<Hide>
attribute).

=item Show+

Each C<Show+> block contains the collector/sensor name of a sensor
(C<Show+> is a bit of a hack - it will be eliminated in favor a more
consistent use of C<Show> as soon as a bug in C<Config::General> is fixed).
Sensor names are the same as in a Show item, however you may override the
following attributes of any sensor in a C<Show+> block:

=for html <ul>

=for html <li>

I<When Type=Graph:> C<Name>, C<GraphColor> and C<LineType>,

=for html <li>

I<When Type=Image:> C<Font>, C<FontSize>, C<TextAlign>, C<TextAngle>,
C<TextColor>, and C<Precision>.
Additionally, each C<Show+> block I<must> also contain an
C<X> and a C<Y> coordinate (for which there is no default value).

=for html </ul>

=back

=for html </dd>

=head3 View attributes for Type=Graph and Type=Image

=over

=for html <dd>

=item RSSName

If a C<View> is given an RSSName, then that view is also shown in the C<RSS>
feed (if it is enabled).

=item RSSOrder

If C<View>s are given an RSSOrder, then they are shown in that order in the
C<RSS> feed (if it is enabled).  RSSOrder is numeric in ascending order.
B<The default value for RSSOrder is 999 (i.e., "last"), but you can use
larger values if you want things listed after the default "last".  Views with
the same view order are listed alphabetically.>

=item ButtonOrder

If C<View>s are given an ButtonOrder, then they are shown in that order in the
list of radio buttons in the main CGI window.  ButtonOrder is numeric in
ascending order.  B<The default value for ButtonOrder is 999 (i.e.,
"last"), but you can use larger values if you want things listed after the
default "last".  Views with the same view order are listed alphabetically.>

=back

=for html </dd>

=head3 View attributes for Type=Graph

=over

=for html <dd>

=item GraphType

Specifies the type of graph that is generated for the View.  I<This option
currently only works for graphs which contain only wind (speed, gust, and
direction) data.>  GraphType C<Natural> is the default, and plots wind speed
and gusts as lines with the wind direction as lettered annotation.  GraphType
C<Radar> plots the wind speed and gusts in a radial plot using wind direction
for the value of I<theta> (the number of times a direction has been seen
is also used as the direction summary plot).  GraphType C<Lines> forces wind
direction to be plotted as a line (IMHO this is ugly, as angles wrap around to
zero from 359 degrees, and generally dwarf wind speeds).

=item MinMax

This specifies the minimum value that the maximum of the graph can be - that
is, the top of the graph will never be lower than this value.  Yes,
it sounds odd - consider this.  You have a lightning sensor, which usually
only shows 1 or 2 clicks every minute, but when there is a storm, you get
hundreds or more.  Specifying a MinMax value of 100 means that the top of the
graph is at least always 100 (but it may be more if the data has values
larger than this).  B<The default value for C<MinMax> is automatically
determined by the data beng graphed.>

=item ClipHi

This specifies the maximum value a value may have when graphing.  Any values
higher than this are clipped.  B<Note:> If you set C<MinMax> and C<ClipHi> to
be the same value, then you are setting the top of the graph to be that value
(because any larger values will be clipped). B<There is no default value for
C<ClipHi>, and "no clipping" is the default graphing behavior.>

=item MaxMin

This specifies the maximum value that the minimum of the graph can be - that
is, the bottom of the graph will never be higher than this value (but it may
be lower if the data has values lower than this).  B<The default value for
C<MaxMin> is automatically determined by the data being graphed.>

=item ClipLo

This specifies the minimum value a value may have when graphing.  Any values
lower than this are clipped. B<Note:> If you set C<MaxMin> and C<ClipLo> to
be the same value, then you are setting the bottom of the graph to be that
value (because any smaller values will be clipped). B<There is no default
value for C<ClipLo>, and "no clipping" is the default graphing behavior.>

=back

=for html </dd>

=head3 View attributes for Type=Image

=over

=for html <dd>

=item Image

The filename where the "master" image may be found.  This image will have the
values of the specified sensors overwritten upon it.  A relative filename
(that is, one which does not start with a '/') will be relative to the C<RSS>
directory.  If the C<RSS> block is not defined, the filename must be
specified as an absolute path (starting with a '/').

=item Font

The name of the font to be used to annotate the image.  The fonts that are
available will vary from system to system.  B<The default value for C<Font>
is "Helvetica".>

=item FontSize

The size of the font to be used to annotate the image.  B<The default value
for C<FontSize> is 14.>

=item TextColor

The color of the text used to annotate the image.  The color may be one of
a large set described on L<http://www.imagemagick.org/script/color.php>, or
may be a 6-digit hexadecimal RGB color starting with a #-sign.  Note that you
need to escape the #-sign (since it is a comment character) as:

	TextColor	\#a0a0FF

B<The default value for C<TextColor> is "black".>

=item TextAngle

The rotation angle to apply to the text used to annotate the image.  The
angle for left-to-right text is 0, top-to-bottom text is 90, and bottom to
top text is -90 (or 270).  B<The default value for C<TextRotation> is 0.>
NOTE: ImageMagic does weird things with the C<X> and C<Y> positions any time
text is rotated.  If you want rotated text, be prepared to tweak these
values.

=item TextAlign

The alignment which is applied to the text when annotating the image, the
choices are "left", "right" and "center".  Note that C<TextClign> changes how
the values of C<X> and C<Y> are intepreted (that is, the coordinates are where
the left, right or center of the text is placed).

=item Precision

The number of digits of floating point precision to annotate.  A value of "0"
will result in only the integer portion being displayed. B<The default value
for C<Precision> is 1.>

=item Date

If there is a C<Date> block (see the example above), the date will be
annotated on the image at the specified X and Y coordinates.  The C<Date>
block may also contain values that override C<Font>, C<FontSize> and
C<Precision>.  B<There is no default value for the C<Date> block, and the
default behavior is to not overlay the date on the image.>

=item Time

If there is a C<Time> block (see the example above), the time will be
annotated on the image at the specified X and Y coordinates.  The C<Time>
block may also contain values that override C<Font>, C<FontSize> and
C<Precision>.  B<There is no default value for the C<Time> block, and the
default behavior is to not overlay the time on the image.>

=item X  I<(only in C<Show+>, C<Date>, or C<Time> blocks)>

The X coordinate at which the the annotated value is placed.  B<The C<X> must
be specified for C<Show+>, C<Date>, and C<Time> blocks - there is no default
value.>

=item Y  I<(only in C<Show+>, C<Date>, or C<Time> blocks)>

The Y coordinate at which the the annotated value is placed.  B<The C<Y> must
be specified for C<Show+>, C<Date>, and C<Time> blocks - there is no default
value.>

=back

=for html </dd>

=head3 View attributes for Type=Wunderground

=over

=for html <dd>

=item Key

The key used when marking data - generally, this is only needed to
distinguish an indoor temperature (or humidity) from an outdoor temperature
(or humidity).  The legal C<Key> values are humidity, tempf, dewptf, winddir,
windspeedmph, windgustmph, dailyrainin, baromin, soiltempf,
soilmoisture, indoortempf, and indoorhumidity.  See
http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol for details (if
you have more than one type of the same sensor, I<thermd> will automatically
add the disambiguating numbers that wunderground requires).
B<The default value is determined automatically based on the sensor type, with
I<thermd> assuming the sensors are located outdoors.>

=back

=for html </dd>


=head2 SNMP details

SNMP is pretty complicated, and I<thermd> only supports the small fraction of
it that it needs to get its job done (namely measuring and logging values).
Thermd only supports static OIDs - not OIDs which are based on the contents
of other OIDs, or "mapping" as it is sometimes called.

OIDs are expressed one of three ways in a sensor block (for the Poseidon
and Damocles series from HWg, the OIDs are automatically determined from the
sensor name, and do not need to be specified).

=over 4

=for html <dd>

=item Absolute OID

These are OID values which start with a '.', for example,
.1.3.6.1.4.1.33.100.2.4.1.3.1.1

=item Relative OID

These are OID values which start with a number, and which are
relative to whatever value has been specified for the C<BaseOID>.  For example,
if the C<BaseOID> is .1.3.6.1.4.1.33.100.2 and an OID is specified as a
relative (that is, not starting with a '.') value of 4.1.3.1.1, the resulting
OID is the concatenation of the two, or .1.3.6.1.4.1.33.100.2.4.1.3.1.1

=item Instance-based OID

These are OIDs based on a template defined in an Instance
block.  An Instance is a named block with an OID that contains non-numeric
variable names.  For example:

    <Instance Outlet>
	OID 7.1.4.N.M
	Type Counter
    </Instance>

defines an instance of a SNMP Counter (named "Outlet"), with two parameters
name "N" and "M". An OID in a sensor may then be specified as

    OID Instance=Outlet, N=3, M=1

This will be expanded into 7.1.4.3.1 (where 3 replaces the "N" and 1 replaces
the "M".  Further, since 7.1.4.3.1 is a relative OID, this will be
concatenated with the C<BaseOID>, resulting in .1.3.6.1.4.1.33.100.2.7.1.4.3.1.
The sensor C<Type> is also inherited from the Instance definition (although if
it is specified in the specific sensor, that value will override the Type
defined in the Instance).
Instance definitions and use must be specified inside a collector block, and
apply only to that block.

=back

=for html </dd>

=head2 Sample configuration file

This is a simple configuration file, which resembles (but is I<not exactly
the same> as) that used on L<http://www.klein.com/thermd>

    MailFrom	Thermometer Daemon <root@klein.com>

    <RSS /var/www/KLEIN/thermd>
	URL		http://www.klein.com/thermd/
	Webmaster	dan@klein.com
    </RSS>

    TimeZone	EST

    Location	"Klone's House"
    GPSCoordinates	"N40.441242456 W79.927382469 (WGS84)"
    MapURL		"http://maps.google.com/maps?q=5606+Northumberland,+Pittsburgh+PA&spn=0.032227,0.064703&hl=en"

    <Collector CheesePuff>
	Type	QK145
	Device	/dev/cuaa0
	Scale	F
	<Sensor 1>
	    Name		"Computer exhaust"
	    GraphColor	red
	    <Alarm DangerWillRobinson>
		Above	95
		Subject	"Maxwell over temp!"
		Message	"%.0s%.1f%s"
		Notify	dvks-pager@lonewolf.com
	    </Alarm>
	</Sensor>
	<Sensor 2>
	    Name	"Basement"
	    GraphColor	green
	    LineType	dashed
	    AdjustBy	+4
	</Sensor>
	<Sensor 3>
	    Name	"Outside 1st floor"
	    GraphColor	blue
	    AdjustBy	+6
	    <Alarm Freezing>
		Below	33
		ResetAt	40
		Dates	1 Apr - 1 Dec
		Times	08:00 - 23:59
		Notify	dan@klein.com
		Notify	dvks-pager@lonewolf.com
	    </Alarm>
	    <Alarm Roasting>
		Above	95
		Times	08:00 - 23:59
		Notify	dvks-pager@lonewolf.com
	    </Alarm>
	</Sensor>
	<Sensor 4>
	    Name	"Inside 3rd floor"
	    GraphColor	purple
	    AdjustBy	+3
	</Sensor>
    </Collector>

    <Collector BaconBits>
	Type	TEMP08
	Device	/dev/cuaa1
	<Sensor B80000004FC30A26>
	    Name	Garage
	    Type	Temperature
	</Sensor>
	<Sensor B80000004FC30A26.1>
	    Name	Garage Humidity
	    Type	Humidity
	</Sensor>
	<Sensor 1500000001622C1D.s>
	    Name	Wind Speed
	    Type	Speed
	    <Alarm Hurricane>
		Above	75
		Notify	dan@klein.com
		Message	"%.0s Hurricane Force Winds - %.1fMPH!"
	    </Alarm>
	</Sensor>
	<Sensor 1500000001622C1D.g>
	    Name	Wind Gusts
	    Type	Gust
	</Sensor>
	<Sensor C2000000012E0B20>
	    Name	Wind Direction
	    Type	Direction
	</Sensor>
	<Sensor 4300000005B9FA1D>
	    Name	Rainfall
	    Type	Rain
	</Sensor>
    </Collector>

    <Collector MRV>
	Type SNMP
	IPAddress 10.10.1.243
	Community ShrimpSalad
	BaseOID .1.3.6.1.4.1.33.100.2  # mrvBpd.mrvLx.irCharMib
	<Instance Temperature>
	    OID 4.1.3.N.1	# irTempValue
	    Type Gauge		# Type will be inherited
	</Instance>
	<Instance Humidity>
	    OID 5.1.3.N.1	# irHumidityValue
	    Type Gauge		# Type will be inherited
	</Instance>

	# The IR5150 power outlet controller
	<Instance Power>
	    OID 6.1.2.N		# PowerCurrentLoad
	    Type Gauge
	</Instance>
	<Instance Outlet>
	    OID 7.1.4.N.M	# irPowerOutletStatus
	    Type OnOff
	</Instance>

	# T1 uses a relative OID, T2, H2 and the outlets use instances instead
	<Sensor T1>
	    OID  4.1.3.1.1	# Or .1.3.6.1.4.1.33.100.2.4.1.3.1.1 (port 1)
	</Sensor>
	<Sensor T2>
	    Name	Computer Temp
	    OID Instance=Temperature, N=2	# port 2
	</Sensor>
	<Sensor H2>
	    Name	Computer Hum
	    OID Instance=Humidity, N=2		# port 2
	</Sensor>
	<Sensor O1>
	    Name	Computer Power
	    OID Instance=Outlet, N=3, M=1	# port 3, outlet 1
	</Sensor>
	<Sensor O2>
	    Name	Router Power
	    OID Instance=Outlet, N=3, M=2	# port 3, outlet 2
	    Type	Gauge			# Overrides Type from Instance
	</Sensor>
	<Sensor O3>
	    Name	Switch Power
	    OID Instance=Outlet, N=3, M=3	# port 3, outlet 3
	</Sensor>
    </Collector>

    Blurb = <<EndOfBlurb
    I bought an inexpensive <a target="_blank"
    href="http://www.qkits.com/serv/qkits/diy/pages/QK145.asp">serial
    interface temperature sensor kit</a> (about US$30) from
    <a target="_blank" href="http://www.qkits.com">QKits</a>
    and after I built the kit, I also purchased an extra 3 Dallas
    Semiconductor sensors (another US$18).  It is a very easy kit
    to build (it doesn't even need a power supply, it derives power
    from the RS-232 line it talks on), and the
    <a target="_blank" href="http://www.qkits.com">QKits</a>
    folks are pleasant people to deal with.  Then I wrote a nice
    Perl-based thermometer logging daemon, log dumper, and CGI script
    (one program does all three functions!) available with both
    <a target="_blank" href="http://www.klein.com/thermd/thermd">source code</a>
    and <a target="_blank"
    href="http://www.klein.com/thermd/t2.cgi?docs=1">documentation.</a>
    This latest version of the Thermometer Daemon supports multiple sensors.
    <p />
    EndOfBlurb

    <View Inside>
	Show	1@CheesePuff
	Show	2@CheesePuff
	Show	4@CheesePuff
	<Show+	T2@MRV>
	    LineType	Dashed
	    Color	Green
	</Show+>
    </View>

    <View Outside>
	Show	3@CheesePuff
	Show	B80000004FC30A26@BaconBits
	Show	B80000004FC30A26.1@BaconBits
    </View>

    <View Pix>
	Type		Image
	Image		/home/dvk/house.jpg
        Font		Helvetica
        FontSize	14
        Precision	1
        <Date>
           X		610
           Y		41
        </Date>
        <Time>
           X		620
           Y		58
        </Time>
	<Show+	1@CheesePuff>
	    X		15
	    Y		125
	</Show+>
	<Show+	B80000004FC30A26@BaconBits>
	    X		50
	    Y		17
	    Font	CourierBold
	    FontSize	18
	    TextColor	red
	    Precision	3
	    TextAlign	Center
	    TextAngle	90
	</Show+>
    </View>

=head1 DEVICE SPECIFIC NOTES

=head2 RacSense and WeatherGoose series

Each sensor has a Sensor Name and a Friendly Name (configurable by the
Display tab on the device's web interface).  I<Thermd> uses the Sensor Name
to read the sensors, so you may need to determine what the sensor's name is
before configuring I<thermd>.

It is also possible to configure temperature units that the web interfce
uses.  Since I<thermd> reads the XML values from the device (which provides
both Celsius and Fahrenheit), you may set the web interface to whatever you
like, without worrying about how it will affect I<thermd>.

=head2 Poseidon and Damocles series from HWg

=head3 SubType

You may specify a SubType of either C<HTTP> (the default) or C<SNMP>.  The
subtype determines how sensors are read.  In SNMP mode, the sensors can be
autoconfigured, but B<we no longer recommend this>, as autoconfiguration must
be done for every daemon, CGI script, RSS feed, etc.

=head3 Actuators

If you plan on using Actuators on the Poseidon or Damocles, you must:

=for html <ol>

=for html <li>

Create a read-write community in the "SNMP Setup" tab of the device's Flash
Setup).

=for html <li>

Specify the same community name in the I<thermd> configuration file (see the
C<Community> attribute for the collector).

=for html </ol>

If you specify the name of a read-only C<Community> in your config file,
I<thermd> will
be unable to change the values of the actuators.  Check the syslog files for
error messages on daemon startup.

=head3 SNMP Traps and fast-reading dry-contact sensors

In order to take advantantage of the SNMP trap information for rapidly
changing dry-contact values, a few specific details need to be configured to
match on the Poseidon or Damocles and in I<thermd>:

=for html <ol>

=for html <li>

You must have NET-SNMP version 5.3.0 or greater available on the same machine
that I<thermd> is running.  This version will contain the correct version of
I<snmptrapd>.

=for html <li>

You must enable trap reception in the I<thermd> configuration file.  See the
C<SNMPTrapPort> and C<AllowSNMPTraps> directives.

=for html <li>

You must enable the generation of traps on the Poseidon or Damocles.  This
means that in the Sensor Setup tab, you need to select one of the Active
alarm states for each sensor you wish to be SNMP-trap monitored.

=for html <li>

In the SNMP Setup tab of the Poseidon or Damocles, for one of the SNMP Trap
Destinations, you must check the "enable" checkbox and select a trap
destination that is the same IP address as the machine upon which I<thermd>
is running, with a Port that is specified in the C<SNMPTrapPort> attribute
in the I<thermd> configuraion file.

=for html </ol>

=for html </ol>

=head1 FILES

=over 1

=item /etc/thermd.conf

Default location of configuration file for thermometer program

=item /var/log/thermd/

Default directory for logging temperature information (if LogFormat is
SQL, then this directory is not used).

=item /var/log/thermd/current

Current temperatures (values are updated every 10 seconds or so; if
LogFormat is SQL, then this directory is not used).

=item /var/run/thermd.pid

Default location of process ID of the logging daemon, if daemon is running.

=back

=head1 AUTHOR and CREDITS

This program was written by Daniel V. Klein, and is Copyright 2001-2009.  All
rights reserved - this program may be freely distributed so long as all
copyright claims are preserved.  Neither this program nor any derived
programs may be sold without express written agreement by Daniel V. Klein,
C<dan@klein.com>.

Contributions of code have also been made by Aaron J Trevena (the original
radial plot module) and Chris Kampmeier (the original image annotation code),
along with contributions from numerous sources, as noted in the changelog.

Lars Karlander, Adam Thomas, Michael G. Petry, Nathan Glaser, Mario Berges,
Tom Buskey, and Anders Brownworth have provided access to their networks for
feature development, and Embedded Data Systems, Midon Design, AVTECH Software,
SensaTronics, Trendline Data Systems, HW Group (of the Czech Republic),
and Applied Power Technologies (APT) have graciously provided hardware.

Thank you to the following translators for internationalization:
Roger Andersson for Swedish;
Patrick Ben Koetter for German;
Kristian Vilmann for Danish;
Frank Kuiper for Dutch;
Vladas Leonas for Russian;
Mario Berges for Spanish.

=head1 VERSION

 $Revision: 2.85 $
 $Date: 2014/08/24 15:21:31 $

=head1 CHANGELOG

 $Log: thermd,v $
 Revision 2.85  2014/08/24 15:21:31  root
 Wunderground was getting mm listed as in for rainfall

 Revision 2.84  2012/10/28 15:59:34  root
 Fixed bug in Newport/IOmega (thanks stillman@kernelworks.org!) and in Gauges

 Revision 2.83  2010/02/16 20:00:51  root
 Drat - I forgot to take out the debug essages from the last checkin :-/

 Revision 2.82  2010/02/08 19:39:41  root
 Stupid error - I broke wind plotting before...

 Revision 2.81  2010/01/10 02:53:06  root
 Fixed readings for Omega/Newport iTHX-2 sensors (there were problems when
 there were more than one digit past the decimal point - a common occurance).
 Thank you to John Kielkopf for finding the bug.

 Revision 2.80  2009/12/30 22:37:57  root
 1) Added hPa and kPa (hecto- and kilo-Pascals) as Metric measures of
    barometric pressure, and made the default be hPa for everywhere except
    Canada (which is kPa) and England (which is mBar) - as opposed to my
    original wrong-sighted use of mmHg.
 2) Added XML as a web-based display type (it was previously added in v2.62
    as a command-line option, but now it is on the web).  The web display
    shows the current values for ALL sensors (regardless of View).
 3) Rewrote the interface for the Poseidon and Damocles series of collectors.
    Previously, they used SNMP to autoconfigure, read data, and deal with
    asynchronous events - now HTTP/XML is the default value with SNMP only used
    *optionally* used for asynchronous events (although you can still use SNMP
    for everything if you like)

 Revision 2.79  2009/11/13 01:09:23  root
 Discovered an OLD :-( bug in AdjustBy, wheree if you specified a value in
 degrees C and the sensor was in degrees F (or vise versa), it would asjust
 by the wrong value...

 Also, cleaned up some crap in the RoomAlert code

 Revision 2.78  2009/08/27 18:24:06  root
 1) Although owfs, owhttpd, owshell all read in C by default, the daemon
    can be started to read in F.  I now allow the Scale directive for them.
 2) Improved backgrounding a little.
 3) The 'current' table in MySql is now created with "ENGINE=innodb" to allow
    for table locking (the default is MyISAM, which did not allow locking).

 Revision 2.77  2009/06/16 22:53:35  root
 1) When a child process dies (that is, a poller process), thermd will now
    automatically restart it.  This should help with data dropouts.
 2) Fixed a bug in the Temp08 where rain sensors were being multiplied by .01
   (.01" is the multiplier for all rain sensors EXCEPT the Temp08)
 3) Largely rehacked the way readinglines and timing works in the daemon, so
    as to avoid the occasional data dropouts that I (and others) were seeing
    when the system was loaded.  It works better, but I still see dropouts :-(
 4) Updated the round-robin scheduler when a collector gets "stuck" (a rare,
    but annoying).
 5) Also fixed a bug in Math sensors, where you were penalized for not
    specifying Units.  I whine now, but don't crash.

 Revision 2.76  2009/04/30 14:31:52  root
 Added support for the MaxBotix line of sonar rangefinders

 Revision 2.75  2009/03/09 20:56:54  root
 New data collectors:
 --------------------
 1) Added a new CommandLine collector type.  This type executes shell commands
    to read sensor values, for those systems for which you have existing
    commands to extract values (for example, sysctl can be used to determine
    the temperature of the CPU).  Thanks to Todd Giles for the idea and
    preliminary version of the code!
 2) Added the Newport/Omega Engineering iServer family of collectors (thanks to
    help from Steve Lancaster of Xerox).  This includes the

 New sensors/actuators:
 ---------------------
 1) The TAI8560 thermocouple module now works on the HA7Net (previously it
    only worked with owfs/owhttpd).  Thanks to Thomas Olson for the detective
    work needed to figure out the 1wire commands and the link to NIST.
 2) The LED in the TAI8560 thermocouple module now works on the HA7Net as an
    actuator (thanks again to Thomas Olson).

 Bug fixes and enhancements:
 ---------------------------
 1) Found and fixed a bug in HTTP authorization (thanks to Todd Giles)
 2) Fixed a few bugs in OWFS Sunlight and Barometer sensors (thanks Iain Mason)
 3) Fixed parsing of Wunderground data, so that empty data does not report
    as having the value 0 (but instead reports as "undefined").
 4) It seems that the multiplier tables for the Veris H8030/H8031 are somewhat
    different than the H8035/H8036, so these tables have now been fixed.  Also
    fixed a bug where register 40001 was not being reported (both thanks to
    Dan Hassler).
 5) Alarm thresholds (Above, Below, and ResetAt) and AdjustBy values used to
    be assumed to be in the Scale of the collector (which was an *optional*
    field, and might be C or F depending on the collector).  To avoid possible
    confusion, you must now specify temperature units in all these attributes.
 6) Actuators are now more correctly logged and reported on.

 Revision 2.74  2008/11/30 01:49:16  root
 A small bug in relative-date parsing was found and fixed by Joe Peters

 Revision 2.73  2008/11/23 23:36:06  root
 A new sensor family and some bug fixes

 1) Added support for the Veris Industries H8030, H8031, H8035, and H8036
    energy meters.

 2) Fixed a bug in HUP restart (found thanks to Dan Hassler)

 3) When I originally released them, I orgot to add dewpoint to Wunderground
    remote weather stations (also found thanks to Dan Hassler)

 Revision 2.72  2008/09/22 14:09:50  root
 Fixed two minor bugs in Derived collectors
 1) Would not correctly recognize the F.IC sensor naming format for owfs
 2) Would not properly handle floating point numbers

 Revision 2.71  2008/09/16 12:40:41  root
 This release features a large number of new features but also introduces a
 couple backwards incompatabilities (I apologize, but I feel it is all for the
 best, especially as they all have simple work-arounds).

 1) Added a new collector type Derived, which allows for "computed" sensors
    (that is, sensors whose value is derived from mathematical equations that
    involve other sensors).  The most flexible type is Math (which allows an
    arbitrary expression), but there are also builtin convenience types for
    WindChill, DewPoint, HeatIndex, and Humidex.

 2) Extended the Wunderground Collector (and View) to allow DewPoint

 3) There are six new Counter types, supported with the SubType attribute
    a) AvgRate - the number of clicks/second, averaged over LogInterval
    b) MaxRate - the highest value of clicks/second over LogInterval
    c) MinRate - the lowest value of clicks/second over LogInterval
    d) Count - the number of clicks over LogInterval
    e) Total - the cumulative number of clicks.  Resets to 0 after the time
       specified in InactivityReset has passed with no further increase.
    f) Raw - the number on the dial

    All the old counting types are still supported as special instances of the
    above (Speed is an AvgRate, Gust is a MaxRate, Lightning is a Count, Rain
    is a Total, and Gauge is a Raw).  You can us the old types without change.

 4) In general, you may now specify multiple reads per sensor (this means that
    you can, for example, show the raw and adjusted temperatures, or the CuFt
    and BTU equivalent for your gas meter, etc)

 5) For the HA7Net collector:
    a) The HA7Net firmware must be upgraded to at least version 1.0.0.22.
       Keeping track of special cases was getting too difficult.
    b) For rate calculations, I now use the time field from HA7net (which means
       greater accuracy, since network delays no longer figure in calculations)
    c) You can now read either Channel A or Channel B (or both) on DS2423
       (previously, only Channel A was supported).

 6) For the RacSense collectors:
    a) Added KWh, Volts-Min and Volts-Max sensors for devices that support it.
    b) Cleaned up how sensors are detected (but based on revised information
       from Geist Manufacturing, it is no longer possible to detect impossible
       sensor/collector combinations).

 7) Added a Nice attribute to the RSS block for cycle-saving on low-power CPUs

 8) INCOMPATABILITIES (and work-arounds):
    a) All counters must have a Subtype.  If you want to preserve your existing
       behavior, use Subtype Count.
    b) I changed ResetAfter to InactivityReset (the documentation was confusing
       and I fixed both the attribute and the wording)
    c) The Lightning sensor type no longer allows ResetAfter.  If you *really*
       want that behavior, create a Counter with Subtype Total.
    d) The Combo attribute is gone.  There were very few sensors that needed it,
       it was confusing, and was there to allow a negligible optimization.
       Just delete it from your config files.

 Revision 2.70  2008/08/26 20:08:13  root
 Sigh - when I fixed a bug for thermocouples, I introduced on for other
 temperature sensors, which is now fixed.

 Revision 2.69  2008/08/25 22:46:23  root
 Martin Strandbygaard found a bug with DS18S20 and owhttpd

 Revision 2.68  2008/08/21 20:14:22  root
 Someone finally used the thermocouple code, and uncovered a few bugs, which
 are now fixed.

 Revision 2.67  2008/06/18 00:22:46  root
 Autoconfigure had some problems - if a Poseison collector was not found,
 then daemon startup could be compomised.  Not anymore!

 Revision 2.66  2008/05/10 16:16:20  root
 Fixed a bug with multiple pollers, where one of them exiting (an unusual
 event) might kill the other pollers.  Hopefully fixed another bug where
 the CGI script might report that the daemon was not running.

 Revision 2.65  2008/04/03 04:32:29  root
 Whoops!  Barometric pressure was being incorrectly reported to Wunderground

 Revision 2.64  2008/04/01 03:19:12  root
 1) Documented the PopUp configuration option (it has been available for
    a while now - oops)
 2) Added configuration options SMTPUsername and SMTPPassword for those
    users who need to specify SMTP SASL authentication to send email.

 Revision 2.63  2008/03/25 03:37:41  root
 1) Eliminated "Daemon not running" errors
 2) Added ModbusAddress qualifier for Enersure (and other future devices)
 3) Added experimental use of the MIT Timeplot graphing system (see the
    -format timeplot directive and the TimePlot button in the CGI interface)

 Revision 2.62  2008/03/13 19:49:13  root
 1) Added Excel and XML as an output format.  See the -format option.
 2) Added generic email usage (we no longer rely on Sendmail) as well as the
    configuration option SMTPHost (this and #3 suggested by Anthony Watts).
 3) Added -email flag to -checkconfig, to test email messages.
 4) Any cumulating sensor (rain, lighting, or counter sensor) with a non-zero
    ResetAfter will now retain its value when the daemon is restarted.  This
    used to only apply to rain gauges.  Suggested by Adam Crewe.
 4) Fixed a degenerate case where Wunderground would report a date and
    str2time would fail to parse it correctly.
 5) Fixed a small bug in RSS and "funny characters", found by Adam Crewe.
 6) Fixed a bug in SQL tables (where new sensors could not be created if the
    user has added additional columns to the sensors table), found by Ethan
    Goldman of CMU.
 7) Changed the behavior of -current.  When -current is used now, the -from
    and -to values are both set to "now".  The old behavior of dumping all
    current readings is assumed by the new -raw option.
 8) Although the "why" of it makes no sense, I fixed a bug which would cause
    the daemon to sometimes crash on startup when a previous daemon had just
    been stopped.

 Revision 2.61  2008/03/06 03:19:00  root
 1) Reduced the number of processes used for Wunderground (since all remote
    personal weather stations are read from the same wunderground site, we
    don't need a separate polling collector for each one).
 2) Cosmetic cleanups to wunderground that I missed in the previous version
 3) Fixed an "uninitialized hash" error in the internal GD::Graph::Radial
 4) Fixed a bug where a sensor's Name (if overwritten in a View) would not
    be reflected in the current readings

 Revision 2.60  2008/02/27 01:29:18  root
 The latest revision provides a boatload of new features as well as a bunch
 of bug fixes (and a couple of minor incompatabilities).  Enough of them that
 I felt it was better to divide them up:

 New Features:
  1) Added Actuators.  This is a BIG FEATURE - it means that you can now
     turn switches on or off in response to alarm conditions.  You can also
     run external programs.  Because of the potential for harm to whatever
     system you are controlling, there are special configuration issues that
     must be satisfied before you can use this feature.
  2) Added support for the HWg series of Poseidon data collector (these
     devices have actuators included in them)
  3) Added the ability to publish weather information to Weather Underground
     http://www.wunderground.com/ as a "personal weather station"
  4) Added support for Weather Underground as a data collector (so you can
     compare your weather data with other stations).
  5) Added support for owshell, in addition to the existing owfs and owhttpd.
  6) Added support for the TAI8570 pressure sensor from AAG to owfs, owhttpd,
     and owshell
  7) Added support for DS2760 (thermocouple-based temperature sensors) for
     owfs, owhhtp, and owshell
  8) Added ability to auto-fetch sensor names from certain collectors (EM1,
     RoomAlert series, SmartWatt and HWg series), which means that you can
     delete the Name attributes from the config file and simply configure
     your collector (eliminating "version skew")
  9) Added Spanish internationalization (thank you to Mario Berges).
 10) Added ability to specify Type in an SNMP Instance declaration (which
     allows Type to be inherited, simplifying SNMP collectors in configuration
     files).
 11) Added SNMPOnValue and SNMPOffValue to SNMP OnOff sensors - the default
     values are 1 and 2 respectively, but some MIBs may use different values.
 12) Added SensorOrder global configuration attribute to change how sensors
     are ordered in reports and graphs
 13) Since the USA (and Liberia and Burma) are the only countries that use
     English units, I made Metric be the default units for all other countries.
 14) Thermd now uses the PIDFile as a lockfile, so it will warn you if you are
     trying to start a daemon when one is already running.  I also allow you
     to specify /dev/null for a PID file, which also disables locking.
 15) Added "MultiplyBy" to Gauges (was previously only available for counters).

 Incompatabilities:
  1) All relative time values (like ResetAfter, LogInterval, PollInterval, etc)
     must now have associated units (for example, 30s, or 1m30s or 1d6h)

 Bug Fixes:
  1) In alarms - the open mode is "|-" not "|".  This was causing monitoring
     daemon crashes :-(
  2) If SQL logging is enabled, the DB connection is remade every time the
     daemon needs to log data (this becomes necessary if the SQL server is
     restarted without also restarting thermd)
  3) Fixed a small bug in SQL reporting/graphing if there is insufficient
     log data available.
  4) WattHours for the EnerSure and SmartNet are the "wattage values since the
     last reading we made".  We used to (incorrectly) average those values
     over LogInterval minutes - now we (correctly) accumulate them.
  5) Lowered the minimum value of LogInterval to 15s
  6) Some Linux versions of strftime do not recognize "%+", so I reverted to
     "%c" in the internatonalization code
  7) Fixed a bug where we would sometimes erroneously report that the logging
     daemon was not running.
  8) Maybe I have graph limits right this time?  I think I have fixed the
     algorithms properly.
  9) Sometimes, when errors were encountered during -daemon parsing, some
     superflous child processes would be left running (config file parsing
     has been largely overhauled).
 10) PollInterval was being ignored for SNMP collectors
 11) It was possible to start two simultaneous SQL-based daemons (text-based
     daemons were locked out by logfiles).  I changed the pidfile to also be
     a lock, preventing duplicate daemons of any form.
 12) Added a few missing (Slope, Intercept) or unclear (lots) components to
     documentation.
 13) Fixed Metric display of barometer readings (they used to be stuck on inHg)
 14) If a collector is marked as ReadOnly, we don't try to talk to it at all now
     Likewise, if a collector is ReadOnly, all of the sensors on it are also
     ReadOnly (but now you can have a ReadOnly sensor on a read/write collector)
 15) Lots of general code cleanup, getting rid of cruft, modularizing, etc.

 Revision 2.59  2007/12/29 18:27:37  root
 I rethought server I18N.  You may use locale-based month names in yourr
 config file now (for alarms).

 Revision 2.58  2007/12/29 16:40:18  root
 Cleaned up I18N a little (problems with mixed encodings in a single
 string.  Added Russian language encoding.

 It is a known bug that some weird characters may appear in graphs when
 using I18N - I may need to take this up with the author of GD::Graph

 Revision 2.57  2007/12/19 23:50:02  root
 1) Added Danish I18N thanks for Kristian Vilmann
 2) Added Dutch I18N thanks to Frank Kuiper
 3) Deprecated DisplayIn - thermd will now choose the appropriate value based
    on the locale (setting DisplayIn will force an initial scale on all
    viewers, regardless of locale)

 Revision 2.56  2007/11/29 22:02:30  root
 NOTE: You must restart the thermd logging daemon when you install this new
 version, otherwise the CGI script will always report that the daemon is not
 running.

 1) Added SmartNet (and SmartWatt, SmartPDU, and SmartSenseTH) XmlRpc sensors.
 2) If the "current" values are too old, the CGI script and other reports will
    advise you that the logging daemon may not be running.
 3) DefaultView was not working - fixed
 4) Added -i18n option to check completeness of internationalization strings

 Revision 2.55  2007/11/14 18:05:58  root
 Thanks to Anders Brownworth, I verified the correctness of the Proliphix
 additions, and also fixed a few minor bugs introduced when I did the
 internationalization.

 Revision 2.54  2007/11/10 18:34:20  root
 Cleaned up error mesages a bit, and after much consultation, removed the
 usage message from the internationalized code.

 Revision 2.53  2007/11/07 19:54:28  root
 1)  Internationalization (or I18N).  We now support Swedish (thank you to
     Roger Andersson of bjorktorp.se) and German (thank you to Patrick Ben
     Koetter of state of mind).
 2)  Added support for Proliphix IP enabled thermostats.  The devices themselves
     are read-write (that is, you can program your thermostat from the web), but
     thermd just reads the values from the thermostat.
 3)  Added UserName and Password for IP-based connections (in case your device
     requires it).  This was necessitated by the Proliphix, and is now supported
     for all IP devices.
 4)  Added Baudrate for serial-based devices.  In general, the default is correct
     but when you use a Digi IP-to-serial device in RealPort mode, it is
     sometime necessary to change the baudrate, so baudrate changes are now
     supported for all serial devices.
 5)  Added restart support for the Enersure.  In my setup, my computers are on
     a battery backup, but the Enersure is not.  When my power company drops
     power, the computers stay up, but the enersure resets - and the IP
     connectiion gets all messed up.  I now attempt to fix it.
 6)  Yet again, fixed the boundary conditions of graphs (sometimes graphs would
     print as solid blue blobs).
 7)  Fixed runaway rain sensors (by rejecting counts of > .1"/minute)
 8)  Added the ability to continue with the last Rain value after a restart (so
     it doesn't automatically reset to 0 when you restart the daemon).  I have
     not tested this on SQL, so feedback would be appreciated.  Thermd will make
     a note in syslog if it sees a recent value...
 9)  Cleaned up units code, so expressed values should be more consistent
 10) Cleaned up a few bugs in image annotation (color of text in CGI wrapper,
     units annotation, wind direction values, etc).
 11) Cleaned up SQL a little to guard against injection attacks from the web

 Revision 2.52  2007/09/23 16:46:00  root
 A few minor changes and tweaks:
 1) SNMP sensor names may now also have '-' characters in them
 2) Counters may now have a negative "MultiplyBy" (so you can graph two
    counters against each other up/down
 3) Annotated images are now displayed fullsize, not scaled to the same
    dimensions as the graphs
 4) Counters are now displayed as integers, when appropriate (which is
    usually, since counters are integral unless altered by MultiplyBy)

 Revision 2.51  2007/09/18 16:15:40  root
 1) With thanks to Chris Kampmeier for the concept (and the initial patches!),
    we have an exciting new feature!  It is now possible to annotate an image
    with the current readings from thermd - which means that you can have a
    an image (photo or drawing) of your site, and you can have thermd label
    it.  Look at http://blogs.kampmeier.com/?p=3 - the graph is generated by
    thermd, but the diagram under it has been annotated by thermd, too!  Look
    in the documentation for View for details.
 2) Further improved the timeout behavior from the previous version
 3) Added a ButtonOrder attribute to views, so you don't need to have your
    radio buttons in alphabetical order if you don't want.
 4) Changed the (default) maximum sort value for RSSOrder from 999999 to 999,
    so numbers greater than 1000 sort after the default "last" value
 5) An important format change in Views.  Sensors used to be labelled as
    Collector/Sensor, and now they are better specified as Sensor@Collector
    (this is anticipation of a future development of computational sensors).
    The old style will work for a while, and there is a conversion script
    on the main webpage
 6) Changed the scale for Power Factor from "%" to "PF", and for relative
    humidity from "%" to "% RH" to avoid ambiguous readings.
 7) Made Unicode the internal standard, to avoid switching between &deg; in
    HTML, \a in GD::Graph, and \N{U+00b0} in Image::Magick (the latter is now
    used throughout).

 Revision 2.50  2007/09/06 20:22:14  root
 Some BSD and some Linux systems have a highly intermittent bug in select()
 but Linux and BSD have different signal behavior, so this latest fix to
 my_select() just forgets about using signals and does its magic manually.

 Revision 2.49  2007/08/22 04:11:14  root
 What a difference a single character in a regex can make :-)  I messed
 up the RoomAlert humidity readings - and just noticed :-(

 Revision 2.48  2007/08/11 17:26:51  root
 Added a test for TAI humidity sensors for the HA7Net.  Version 1.0.0.16 of
 the HA7Net understands the new EDS humidity sensors, but not the old-style
 TAI version.  EDS was supposed to release a new version of the firmware, but
 have fallen behind, so since one of my users complained, I installed a
 temporary workaround.

 Revision 2.47  2007/07/03 03:27:32  root
 Added support for the RoomAlert 24E and RoomAlert 26W

 Revision 2.46  2007/06/07 15:16:11  root
 1) Added more control over how graphs are rendered, by adding the MinMin,
    MinMax, MaxMin, and MaxMax attributes to View's
 2) Added support for Postgres, in addition to MySQL.  It seems that they are
    very slightly different...
 3) Fixed small bug IXON/IXOFF for enersure that was causing some data to
    be missed
 4) It seems that Linux behaves slightly differently for signals than does
    my FreeBSD system.  This manifested itself in a timeout/my_select bug on
    Linux, which is now fixed.
 5) Eliminated reference to "temperature" in the RSS feed, since some folks
    use thermd for just power monitoring :-)

 Revision 2.45  2007/05/02 21:43:58  root
 The HA7Net code now looks at CRCs of returned data values, and reports when
 the CRC check fails.  Also fixed a small bug in PollInterval.

 Revision 2.44  2007/04/18 00:19:28  root
  1) The EnerSure is now ready for release.  You must now download the
     Modbus::Client module from the CPAN to use it.  A number of additions
     are still planned...
  2) Any serial device may now be addressed by IPAddress/Port in addition to
     Device.  I have a DigiOne SP ether-to-serial interface, and I now provide
     support for that too.
  3) Improved behavior of HA7Net Data aquisition with regards to locking.  We
     now grab the lock once per transaction, instead of once per device, which
     should speed up data acquisition and reduce network traffic.
  4) Better tweaking of graph ranges - the closer together the minimum and
     maximums of a graph, the closer the limits of the axes
  5) Changed format of the "current" logfile - instead of referencing sensors
     by the name gien in the config file, I now use the -> Collector and Sensor
     number.  No one should care, but it's better and faster this way, and lets
     you change a sensor name and still have views work without restarting the
     daemon.
  6) Changed logformat - it no longer pads values with leading zeros (this was
     a holdover from the days of fixed-length logfile records - why waste disk
     space when we don't need to?)
  7) Added the ReadOnly attribute to sensors.
  8) Added PopUp attribute for sensors.
  9) Added PollInterval for changing the scan rate of collectors.
 10) Changed LogFrequency to LogInterval, which is a more accurate term.  I
     still allow LogFrequency, though :-)
 11) Allow European format numbers (0,15) in addition to American format (0.15)

 Revision 2.43  2007/03/15 03:51:37  root
 This version is the preliminary release of support for the Enersure
 power monitor from Trendline.  I have tested it on my system (which is
 operational, but not connected to my circuit-breaker box).

 Revision 2.42  2007/03/14 15:04:47  root
 After a short amount of experience with the barometer, added a custom
 graph type if the only thing being plotted is barometric pressure (so
 you can actually see the changes :-)

 Revision 2.41  2007/03/13 20:12:51  root
 One (important) bug fix, two new sensors!
 1) I now support the Hobby Boards barometer and solar radiation sensors
 2) While implementing them, I discovered a bug in my temperature conversion
    routines for all DS2438 sensors :-(  This would affect humidity sensors
    and temperature readings when the temperature is below 0C

 Revision 2.40  2007/03/12 04:51:27  root
 This month was pretty productive, and a rather large change to thermd is
 the result.  A big THANK YOU goes to Lars Karlander, of N64.45 E20.52 in
 Northern part of Sweden for items 1, 2, and 4 below - he helped specify and
 debug the code as it was being written.  Thank you also to Daniel Johnson of
 Computer Resources who originally suggest the idea, and then was patient
 during the 6 months it took me to get started :-)

 NEW DATA COLLECTORS
 -------------------
 1)  Added support for OWFS (see http://owfs.sourceforge.net).  This means
     that thermd now supports the serial port host adapters (ibuttonlink,
     DS9097E, DS9097, and DS2480B) or USB host adapters (DS9490 or PuceBaboon),
     available from HobbyBoards and elsewhere.
 2)  Added support for OWHTTPD (see http://owfs.sourceforge.net/owhttpd.1.html)
 3)  Added support for SNMP-based devices, such as the MRV LX-4008 and IR5150
 4)  Added a new sensor type Counter.  This is just a raw counter device (for
     1-Wire and SNMP collectors), and reports how many "clicks" have gone by
     in the past LogMinute interval.
 5)  Added a new sensor type Gauge.  This is just a raw measure, and is only
     available on an SNMP collector.
 6)  Added Units attribute for Counters and Gauges, and MultiplyBy for Counters.
 7)  Refactored how the lightning gauges read.  I used to show them a count of
     strikes over a long interval (like rain) but now that I actually *have*
     a lightning sensor, I see it makes more sense simply to show a count of
     strikes per measurement period.  If you like it better the other way,
     you can fake it with Counter :-)
 8)  Cleaned up the counter code for lightning & rain - they are now just
     special cases of type Counter
 9)  Added support for the low-precision DS1822
 10) Cleaned up logging/error messages (so now they go to STDERR and/or syslog
     as appropriate)
 11) Fixed a bug with upper/lower case hexadecimal strings in the HA7Net
 12) Added a Port directive (for non-standard HTTP connections, and for SNMP)

 Revision 2.39  2007/01/16 14:29:35  root
 Added Blurb2 keyword - someone wanted to be able to add text after the
 chart instead of before it (you can actually use both if you want)

 Revision 2.38  2007/01/14 17:45:21  root
 We had more than 1" of rain in 24 hours, and I discovered a bug in my
 scaling algorithm for graphing rain :-/

 Revision 2.37  2007/01/02 12:47:23  root
 A small graphing improvement: if the graph ends "now", the graphs now
 include the current readings (which have not yet been logged into the
 every-LogInterval-minutes file)

 Revision 2.36  2006/12/27 23:45:16  root
 1) By popular request, log data may now be stored in SQL format.  I have only
    tested the MySQL interface, but I don't do any "funny" SQL calls, so it
    ought to work with any SQL interface.  An external program convert_to_sql.pl
    is available to help you translate your logfiles.
 2) Added options -list and -nowarn to -checkconfig, in support of conversion
    of databases to SQL
 3) Fixed a rare crash when the AAG wind sensor is used with the HA7Net.  I
    was correcting for AdjustBy values in the wrong place, and so in addition
    to crashing, I was recording incorrect wind directions IF you tried to
    adjust the wind direction.  Both bugs are fixed.

 Revision 2.35  2006/12/16 00:58:25  root
 A number of changes, enhancements, and bug fixes
 1) Added support for the ITWatchDogs WeatherGoose, MiniGoose, SuperGoose,
    PowerGoose and RacSense data collectors.  Thank you to Adam Thomas for
    his assistance.
 2) A small but noteworthy change to logfile parsing - if you have a sensor
    name that has lowercase letters in it (the sensor names are usually
    16 character 1Wire names with hexadecimal characters), the logfile will
    be stored as an uppercase name (unless you explicitly set a LogFile
    directive, which will be untouched).
 3) Fixed autoscale on a Rain-only graph - now sets limits of 0-1" in English
    mode, and uses 2-digit labels (so you can see small amounts of rain)
 4) Fixed conversion on rain values when displaying in Metric
 5) Fixed one last bug in Rain counting, so the rain values correctly reset
    after ResetAfter hours

 Revision 2.34  2006/12/01 22:50:34  root
 Theoretically, theory is harder than practice.  Practically speaking, it
 is the other way around.

 The rain sensor worked fine on the TEMP08, so theoretically it should have
 worked on the HA7Net.  Practically, that was not true.  I fixed that bug,
 so now it works on both.  Theoretically.

 Revision 2.33  2006/11/27 06:21:54  root
 Fixed bug in wind direction measurement (could have halted daemon)
 Made some diagnostics more friendly

 Revision 2.32  2006/11/26 14:56:42  root
 A few bug fixes to the previous release - you always find them after you
 release the code :-(  Also added autosubmit when you change any radio
 button or checkbox.

 Revision 2.31  2006/11/24 18:30:44  root
 1) Added support for EmbeddedDataSystems D2C switch sensors
 2) Removed the restriction that OnValue need to be larger than OffValue
 3) Added support for new HA7Net high-level humidity-reading to support
    their new sensor design.

 Revision 2.30  2006/10/22 03:54:34  root
 1) Added override of Name in Show+ block
 2) Fixed small bug in overtemp reporting
 3) Fixed up pod documentation a little

 Revision 2.29  2006/10/13 20:49:06  root
 1) AVTECH Software released new firmware for the Room Alert, which changed
    the URL that they used to fetch data.  Updated to allow both new and old
    style URLs
 2) Cleaned up POD documentation (I missed a few closing '>'s before)

 Revision 2.28  2006/09/12 22:49:34  root
 It turns out that in practice, wind direction measured from the OneWire
 Weather station is a lot more erratic than it should be in theory.  So
 the voting average that I implemented did not work well.  I have replaced
 this with a consensus average algorithm which seems to give better results.

 Revision 2.27  2006/09/10 18:02:35  root
 OWW! One big bug, one small one, one new feature
 1) Wind gusts were being accumulated, instead of maximized.  This resulted
    in ridiculously high gust values.  Whoops!
 2) Small bug in View's and Show+
 3) Added a Wind Direction graph to radar, to show the weighted average of
    wind direction (the more measurements in a given direction, the larger
    the measurement shown, so you can see teh predominant wind direction)

 Revision 2.26  2006/09/04 05:02:02  root
 Once a production release is made, it seems that there are always one
 or two bugs.  They are fixed, and not worth mentioning...

 Revision 2.25  2006/09/03 18:24:40  root
 This is a big release with a lot of changes.  I owe a BIG "thank you" to
 Michael G. Petry who reported the bugs addressed in items 2 and 10, and who
 helped me with the low-level OneWire protocol and the code necessary to get
 items 4, 5, and 6 working (all of those mean that thermd now supports the
 OneWireWeather station on the HA7Net! :-)

 1)  Added initial support for the Room Alert 7E, Room Alert 11E, and
     TemPageR systems
 2)  Fixed an off-by-one bug in my HA7Net data-collection code, where thermd
     read too much too often
 3)  Added support for the Lightning and Barometer sensors for TEMP08
 4)  Added support for DS2438-based Humidity sensors for HA7Net
 5)  Added support for DS2423-based windspeed, gust, lighting and rainfall
     sensors to the HA7Net
 6)  Added support for DS2450-based wind directon sensors to the HA7Net
 7)  Added a Show+ block to Views, and added GraphColor and LineType in these
     blocks to override the lines/colors specified in the Sensor blocks
 8)  Added a new GraphType qualifier to Views, to support the new Radar chart
     for wind history
 9)  Added a DefaultView configuration item, to change what the default radio
     button is selected
 10) Version 1.0.0.13 of the HA7Net firmware had a bug where if you read more
     than 10 DS1820 sensors, it would hang.  The bug has been fixed in 1.0.0.14
     but thermd will currently only read blocks of 10 sensors.
 11) Changed DisplayIn to accept English or Metric (C and F will still work).
     This will change all units of display, but all can be overridden.
 12) Added Temperature, Rainfall and WindSpeed to override DisplayIn.
 13) Changed -units switch to accept English or Metric (C and F will still
     work).  This will change all units of display, overrides the config file,
     but all individual units can be overridden.
 14) Added -temperature, -rainfall, and -windspeed override switches, too.
 15) Improved periodicity of readings for HA7Net and EM1.  New reading starts
     are based on the completion of past readings - now we read darn close to
     every 60 seconds (extremely important for wind seed measurements).
 16) Fixed averaging of wind direction.  Previously, an equal number of N & NW
     readings would correctly yield NNW, but since a compass is round, an
     equal number of NNW and NNE readings would incorrectly yield S!  Now it
     gives the correct value of N.

 Revision 2.24  2006/07/22 19:49:50  root
 The best laid plans... fixed over/under range sensor check.

 Revision 2.23  2006/07/21 21:09:59  root
 Added the RSSOrder attribute to View's

 Revision 2.22  2006/07/21 18:14:40  root
 1) Added support for the EM1
 2) I apologize, but I renamed the BaseURL attribute to IPAddress (and
    slightly changed the semantics).  This was necessary as I find that
    the Trendline power monitoring uses an IP address but not a URL, and
    I wanted to be consistent...
 3) Underdriven sensors (humidity and wind speeds less than 0) will now
    be ignored

 Revision 2.21  2006/07/16 14:37:32  root
 1) Fixed a bug in the rain measurement code - it now works a lot better :-)
 2) Started adding Trendpoint EnerSure and Sensatronics EM1
 3) Deleted some old unused code for decaying stale data

 Revision 2.20  2006/07/05 15:33:17  root
 1) Found a synchronization bug in initializing the TEMP08, fixed.
 2) Wind directions are now represented as a compass point (and not degrees)
    in current log and RSS feed

 Revision 2.19  2006/06/30 16:16:20  root
 1) Fixed a bug in RSS - previously, all the values in the "current readings"
    file were in Degrees F.  They are now in their correct units.
 2) Made a special wind-only graph format.  If a view has only wind speed
    and direction (and optionally gusts), this format is automatically used.
    I don't think it will work if other readings ar used, so it is not a
    selectable type.
 3) If you don't like my choice of named colors, you can now also use an
    RGB 6-digit hexadecimal value.
 4) Occasionally erroneous data will be seen on 1-Wire busses.  Thermd now
    ignores temperatures of 85C/185F, as well as humidities and windspeeds
    that are >= 100 %/MPH.  I may make this a configurable option...
 5) The code has been rock solid without any obvious memory leaks.  I changed
    the automatic restart from weekly to monthly.

 Revision 2.18  2006/06/13 13:22:17  root
 Embedded Data Systems released firmware version 1.0.0.13, which fixed the
 bug I had to make a workaround for in thermd version 2.12, so this release
 backs out that section of code to the more efficient use of the HA7Net
 code.  Now, if a sensor fails or is simply unplugged, it will "go missing"
 instead of invalidating all of the sensor readings.

 Revision 2.17  2006/05/30 17:44:29  root
 Added start of customized RSS - see the "RSSName" keyword in View's

 Revision 2.16  2006/05/24 15:56:07  root
 One small change, one big one
 1) Added support for the One Wire Weather rain gauge on the Temp08
 2) Completely rethought how I read data fromn sensors (for the THIRD time)
    When I just supported a single QK145, it was easy - just read streaming
    data.  Adding the HA7Net meant changing to a polled structure, and then
    when I saw I was getting data dropouts, I switched to a raw-mode polling
    system.  Dropouts kept occurring, and I discovered what I think is a bug
    in the select() call, *and* I was using a LOT of CPU time, so now I have
    a raw-mode polling system with an exception-raising mechanism if there
    is a blockage in sysread() or select(), and I only collect data every
    30 seconds (although it buffers up in the interval).  This means no more
    heavy CPU load, no more missed data, and no more missed log intervals.

 Revision 2.15  2006/05/02 18:19:48  root
 Someone complained that the new data collection wasn't working, and for
 their flawed logfile, they were right,  We now handle errors in logfiles
 a bit better, as well as fixed an error in default MailFrom.

 Revision 2.14  2006/05/01 20:17:13  root
 1) Added support for the TEMP08 data collector from Midon Design.  Also
    added suppot for the DS2438 temperature (and humidity) sensor, plus the
    DS2423 wind speed and the DS2450 wind direction sensor in the AAG One Wire
    Weather system!  Note that OWW will not be fully supported until I mount
    give a degree value between 0 and 359, instead of something more pretty).
    I have plans to add the rain gauge, too.
 2) Added the Type attribute to <Sensor>.  This is optional for temperature
    sensors.
 3) Completely rethought how data is collected - this was because of yet
 4) Added the Type attribute to <Sensor>.  This is optional for temperature
    sensors.
 5) Completely rethought how to log data and report dead sensors.  Since Perl
    after version 5.7.3 has safe signals, I now use alarm timeouts to do both
    functions, which means enhanced reliability and more accurate logging,
    even when the RSS feature slows down measurements.  This also entailed
    dealing with interrupted selects and restarting system calls.
 6) Reduced the frequency with which I poll the HA7Net.  Originallym this was
    even when the RSS feature slows down measurements.  This also entailed
    dealing with interrupted selects and restarting system calls.
    of continually.  This also helps reduce network traffic from the daemon.
 7) Improved restarting of the daemon when HUPped or on the weekly autorestart.
 8) At the request of a user of the old system, I reincorporated Hi/Lo graphs.
    They are no longer restricted to 1 year, but will show highs and lows for
    every 24-hour period that is graphed.
 9) Changed the way ticks are generated on the X axis.  Now completely general,
    instead of being special-cased for each range.
 10)Graphing now should behave correctly even if you change LogFrequency
 11)Return a special image when no data is available to be plotted in the date
    range specified.

 Revision 2.13  2006/04/18 17:04:22  root
 Added "Combo" keyword to support combination sensors from Embedded Data
 Systems (such as the HMP2001S)

 Revision 2.12  2006/04/18 16:24:52  root
 There is a problem with the HA7Net... It allows you to read multiple
 sensors at one go, but if any single requested sensor is missing, the
 entire request fails.  This means that if you disconnect a sensor, it
 appears that all sensors of that type have failed.  The solution is
 to read each sensor individually, which means slightly more network
 traffic and processing.  No big deal, but it is annoying...

 Revision 2.11  2006/04/14 22:17:32  root
 Correctly handles sensors which do not read temperature - so the relative
 humidity sensor will no longer have readings converted to Fahrenheit :-/

 Revision 2.10  2006/04/13 12:26:27  root
 1) Prettied up latest readings when there are > 5 sensors
 2) Fixed bug where there was no relevant data in the first file looked at
    caused a doubling of dates for the remaining files

 Revision 2.9  2006/04/07 13:45:24  root
 Added LineType attribute - my graphs were getting crowded, and having a
 discriminator was nice

 Revision 2.8  2006/04/07 03:33:45  root
 1) Improved I/O - you aren't supposed to use buffered I/O with select(2),
    and I was.  So now, we use only unbuffered I/O for reading from the
    sensors.  There are no longer any dropouts (I hope)
 2) Fixed a bug where failed sensors would not be detected if they had
    had _any_ previous readings
 3) Closed some unused file descriptors on read

 Revision 2.7  2006/03/28 03:25:29  root
 1) Fixed bugs alluded to in previous version
 2) Added Hide attribute to sensors
 3) Beefed up printing of current temps in graph to only show what is in
    the current View

 Revision 2.6  2006/03/27 04:53:07  root
 First major pass at integrating the HA7Net - but be forewarned, there
 are still some bugs that need to be fixed

 Revision 2.5  2006/03/22 17:36:21  root
 Found an interesting graphing bug when there is no data for a sensor
 (that was not the last sensor examined!) in the selected time period.

 Revision 2.4  2006/03/21 17:04:51  root
 1) Fixed fatal bug when a collector fails (it used to die when it wrote the
    logfiles (I had previously only checked at the update of "current")
 2) Fixed bug where collector log subdirectories were checked before the names
    of the actual logfiles were specified.
 3) Started adding HA7Net from Embedded Data Systems - they say they'll send
    me a unit to evaluate and integrate.

 Revision 2.3  2006/03/12 17:58:15  root
 We now allow fractional relative times (so, -1.5d is the same as -1d12h
 or if you want to be perverse -1.25d6h :-)

 Revision 2.2  2006/02/26 00:13:25  root
 Fixed bad bug in averaging software.  I forgot to zero out the sum and
 count after logging, so it wound up calculating avergage since reboot
 instead of average since last logging.

 Revision 2.1  2006/02/25 00:11:01  root
 There's always one... last... bug... after you make a release :-/

 Revision 2.0  2006/02/25 00:00:45  root
 Massive update and complete rewrite!  Notable changes are:
 0) Complete rewrite of code.  Much more readable and modular, and designed to
    be a new release (instead of a accumulation of features over time :-)
 1) New configuration file format (now resembles Apache config file), and
    all parameters are in config file (no more changing Perl code).  Just
    about everything is tunable.
 2) Supports multiple data collection devices, no limit on number of sensors.
 3) Supports QK145 and VK011 devices from QKits.  Other devices may follow...
 4) Dramatically increased graphing speed.
 5) Enhanced display ranges (arbitrary start/end points).
 6) Named views (so you can choose groups of sensors to display).
 7) Improved alarms - can specify multiple alarms per sensor, with multiple
    delivery options, can also specify when an alarm resets, when it can be
    delivered, etc.
 8) Improved argument processing (uses long options, instead of 1-letter).
 9) Improved documentation (I hope!)
 10) Lots of bug fixes, more robust behavior!


 Revision 1.77  2006/01/06 00:28:05  www
 Final version 1 release - now contains graphics, tracking, trend analysis,
 alarms, and lots more...

 *Revision* 1.1  2001/07/31 20:43:36  www
 Initial release - a pretty simple program
