#!/usr/bin/perl
# -*- perl -*-

=head1 NAME

=encoding utf8

currentcost - Munin plugin to monitor a CurrentCost energy monitor

=head1 APPLICABLE SYSTEMS

Any system connected to a CurrentCost monitor. These can be purchased from L<http://www.currentcost.com>.

=head1 CONFIGURATION

This plugin requires the following Perl modules. Either fetch them from CPAN or your distribution.

=over

=item *

XML::Simple

=item *

Device::SerialPort

=item *

YAML

=item *

Time::Local

=back

This configuration section shows the defaults of the plugin:

    [currentcost]
    env.device          /dev/ttyUSB0
    env.baud            2400
    env.tick            6
    env.currency        £
    env.rate1           13.9
    nenv.rate1qty       900
    env.rate2           8.2
    env.nightrate       0
    env.nighthours      23:30-06:30
    env.standingcharge  0.0
    env.metertype       CC128

The configuration can be broken down into the following subsections:

=head2 DEVICE

=over

=item env.device

Specfies the device node where the CurrentCost monitor can be found. You may find it useful to use a udev rule to symlink this somewhere permanent.

=item env.baud

Specifies the baud rate to use. CurrentCost devices may speak at 2400, 9600 or 57600 baud, depending on their age.

=item env.tick

How long, in seconds, to consider data valid for. CurrentCost monitors typically put out data every 6 or 10 seconds. If Munin does a data run less than C<env.tick> seconds after a config run, there's no need to wait for more data.

=back

=head2 COSTS

=over

=item env.currency

The currency symbol to use on the cost graph. CurrentCost typically uses "E<pound>" or "E<euro>", but you may find "$" more to your taste.

=item env.rate1

The primary rate in hundredths of a C<env.currency> per kWh. (i.e. pence/cents per kWh)

=item env.rate1qty

How many kWh per month are charged at C<env.rate1>. Some tariffs charge one rate for the first so many units and then another rate for the remainder. If you are charged a flat rate per unit, set this to 0.

=item env.rate2

The secondary rate in hundredths of a C<env.currency> per kWh. (i.e. pence/cents per kWh)

=item env.nightrate

The night rate in hundredths of a C<env.currency> per kWh. Some tariffs (such as Economy 7) charge differently during the night and typically require a meter capable of reading two rates. If you do not have such a tariff, set this to 0.

=item env.nighthours

The time period for which C<env.nightrate> applies. This should be of the form C<hh:mm-hh:mm> and should span midnight.

=item env.standingcharge

The standing charge in hundreths of a C<env.currency> per month. If you do not have a standing charge, set this to 0.

=item env.metertype

The type of the meter. Currently "CC128" and "CC02" are supported.

=back

=head1 MAGIC MARKERS

 #%# family=auto contrib
 #%# capabilities=multigraph autoconf

=head1 AUTHOR

Paul Saunders L<darac+munin@darac.org.uk>

=cut

use strict;
use warnings;
use utf8;
use Munin::Plugin;

need_multigraph();

my $device_node    = $ENV{device}         || "/dev/ttyUSB0";
my $baud_rate      = $ENV{baud}           || "2400";    # or 9600 or 57600
my $tick_rate      = $ENV{tick}           || "6";
# Tick_Rate is how long to consider data valid for (in seconds)

# Costs
my $currency       = $ENV{currency}       || "£";       # £ or €
my $rate1          = $ENV{rate1}          || "13.9";    # in pence/cents
my $rate1qty       = $ENV{rate1qty}       || "900";     # in kWh, 0 to use fixed rate2
my $rate2          = $ENV{rate2}          || "8.2";     # in pence/cents
my $nightrate      = $ENV{nightrate}      || "0";       # 0 = disabled
my $nighthours     = $ENV{nighthours}     || "23:30-06:30";
my $standingcharge = $ENV{standingcharge} || "0.0";     # pence/cents per month
my $metertype      = $ENV{metertype}      || "CC128";   # or "CC02"

my $MUNIN_DEBUG    = $ENV{MUNIN_DEBUG}    || 0;         # Set by munin-run

my $ret;
if ( !eval "require XML::Simple;" ) {
    $ret .= "Could not load XML::Simple; ";
}
if ( !eval "require Device::SerialPort;" ) {
    $ret .= "Could not load Device::SerialPort; ";
}
if ( !eval "require YAML;" ) {
    $ret .= "Could not load YAML; ";
}
if ( !eval "require Time::Local;" ) {
    $ret .= "Could not load Time::Local; ";
}

if ( defined $ARGV[0] and $ARGV[0] eq 'autoconf' ) {

    # Shouldn't autoconfigure as there's no reliable way to detect
    # serial devices.
    print "no\n";
    exit 0;
}

my @lastread;

sub save_data {
    my @savedata = @_;

    # Do we need to save this data?
    if ( !@lastread
        or time >= Time::Local::timelocal(@lastread) + ($tick_rate) )
    {
        print "# Saving Data (Data is "
          . ( time - Time::Local::timelocal(@lastread) )
          . " seconds old)\n"
          if $MUNIN_DEBUG;
        @lastread = localtime(time);

        my @save_vector;
        push @save_vector, YAML::Dump(@lastread);
        foreach ( split /\n/, YAML::Dump(@savedata) ) {
            push @save_vector, $_;
        }
        save_state(@save_vector);
    }
}

sub load_data {

    # Bring the data back in
    my @save_vector = restore_state();

    # Read the timestamp, Do we need to refresh the data?
    my @lastread = YAML::Load( shift @save_vector );

    my $yamlstr = '';
    foreach (@save_vector) {
        $yamlstr .= "$_\n";
    }
    my @dataarray = YAML::Load($yamlstr);

    if ( !@lastread
        or time >= ( Time::Local::timelocal(@lastread) + ($tick_rate) ) )
    {

        # Data is stale
        print "# Data is stale ("
          . ( time - Time::Local::timelocal(@lastread) )
          . " seconds). Re-reading device\n"
          if $MUNIN_DEBUG;

        eval {

            # Fetch the XML
            my @temparray;
            if ( $metertype eq "CC128" ) {
                @temparray = collect_cc128_data( $device_node, $baud_rate );
            }
            elsif ( $metertype eq "CC02" ) {
                @temparray = collect_cc02_data( $device_node, $baud_rate );
            }
            else {
                die "Unknown meter type $metertype.";
            }

            # Read the time so we know whether to reset
            #  daily/monthly/yearly counters
            my @now = localtime(time);
            my %is_new;
            $is_new{daily}   = ( $now[3] != $lastread[3] ) ? 1 : 0;
            $is_new{monthly} = ( $now[4] != $lastread[4] ) ? 1 : 0;
            $is_new{yearly}  = ( $now[5] != $lastread[5] ) ? 1 : 0;

            for ( my $i = 0 ; $i <= $#temparray ; $i++ ) {
                my $datum = $temparray[$i];
                for ( my $j = 0 ; $j <= $#{ $datum->{data} } ; $j++ ) {
                    for my $period (qw(daily monthly yearly)) {
                        $period = "n$period" if is_night_rate();
                        if ( defined( $dataarray[$i]->{data}[$j]->{$period} ) )
                        {

                            # There's old data. Consider incrementing it
                            if ( $is_new{$period} ) {

                                # Start of a new period, reset the counter
                                print "# Start of a new $period period." .
							          " Resetting counter\n" if $MUNIN_DEBUG;
                                $temparray[$i]->{data}[$j]->{$period} = 0;
                            }
                            else {
                                $temparray[$i]->{data}[$j]->{$period} =
                                  $dataarray[$i]->{data}[$j]->{$period} +
                                  $temparray[$i]->{data}[$j]->{value} / 12;
                            }
                        }
                        else {

                            # No old data. Set it.
                            $temparray[$i]->{data}[$j]->{$period} =
                              $temparray[$i]->{data}[$j]->{value} / 12;
                        }
                    }
                }
            }

            # If the above threw an error, we won't overwrite the old data
            @dataarray = @temparray;

            1;
        } or do {
            print $@;
          }
    }
    return @dataarray;
}

sub is_night_rate {

    # Determine if we're on night rate
    return 0 if not $nightrate;
    my ( $nightstart, $nightstop ) = split /-/, $nighthours;
    my ( $start_h,    $start_m )   = split /:/, $nightstart;
    my ( $stop_h,     $stop_m )    = split /:/, $nightstop;
    my $start_time = $start_m + ( $start_h * 60 );
    my $stop_time  = $stop_m +  ( $stop_h * 60 );
    my @now        = localtime(time);
    my $now_time = $now[1] + ( $now[2] * 60 );

    if (   $now_time >= $start_time
        or $now_time <= $stop_time )
    {
        print "# Night rate is ACTIVE\n" if $MUNIN_DEBUG;
        return 1;
    }
    else {
        print "# Night rate is enabled, but NOT active\n" if $MUNIN_DEBUG;
        return 0;
    }
}

=head1 EXAMPLE INPUT

The device will periodically output a string of XML. The string will be all on one line, they are expanded here for clarity.

=head2 Classic format

As per L<http://cumbers.wordpress.com/2008/05/07/breakdown-of-currentcost-xml-output/>:

 <msg>
    <date>
        <dsb>00014</dsb>        days since birth
        <hr>14</hr>             the time
        <min>07</min> 
        <sec>07</sec> 
    </date>
    <src>
        <name>CC02</name>       name of this device
        <id>03280</id>          communication channel for device
        <type>1</type>          hardware version of the device
        <sver>0.07</sver>       software version
    </src>
    <ch1>
        <watts>00080</watts>    value from the first channel clamp
    </ch1>
    <ch2>
        <watts>00000</watts>    value from the second channel clamp
    </ch2>
    <ch3>
        <watts>00000</watts>    value from the third channel clamp
    </ch3>
    <tmpr>28.8</tmpr>           current temperature (degrees celsius)
    <hist>
        <hrs>
            <h02>000.0</h02>    total Kwh used in 2 hour blocks
            <h04>000.1</h04> 
            <h06>000.1</h06> 
            <h08>000.0</h08> 
            <h10>000.0</h10> 
            <h12>000.0</h12> 
            <h14>000.0</h14> 
            <h16>000.1</h16> 
            <h18>000.1</h18> 
            <h20>000.1</h20> 
            <h22>000.1</h22> 
            <h24>000.0</h24> 
            <h26>000.0</h26> 
        </hrs>
        <days>
            <d01>0000</d01>     total Kwh used per day(s)
            <d02>0000</d02> 
            <d03>0000</d03> 
            <d04>0000</d04> 
            <d05>0000</d05> 
            <d06>0000</d06> 
            <d07>0000</d07> 
            <d08>0000</d08> 
            <d09>0000</d09> 
            <d10>0000</d10> 
            <d11>0000</d11> 
            <d12>0000</d12> 
            <d13>0000</d13> 
            <d14>0000</d14> 
            <d15>0000</d15> 
            <d16>0000</d16> 
            <d17>0000</d17> 
            <d18>0000</d18> 
            <d19>0000</d19> 
            <d20>0000</d20> 
            <d21>0000</d21> 
            <d22>0000</d22> 
            <d23>0000</d23> 
            <d24>0000</d24> 
            <d25>0000</d25> 
            <d26>0000</d26> 
            <d27>0000</d27> 
            <d28>0000</d28> 
            <d29>0000</d29> 
            <d30>0000</d30> 
            <d31>0000</d31> 
        </days>
        <mths>
            <m01>0000</m01>     total Kwh used per month(s)
            <m02>0000</m02> 
            <m03>0000</m03> 
            <m04>0000</m04> 
            <m05>0000</m05> 
            <m06>0000</m06> 
            <m07>0000</m07> 
            <m08>0000</m08> 
            <m09>0000</m09> 
            <m10>0000</m10> 
            <m11>0000</m11> 
            <m12>0000</m12> 
        </mths>
        <yrs>
            <y1>0000000</y1>    total Kwh used per year(s)
            <y2>0000000</y2> 
            <y3>0000000</y3> 
            <y4>0000000</y4> 
        </yrs>
    </hist>
 </msg>

=head2 CC128 format

For full definition, see L<http://www.currentcost.com/xx128/xml.html>

 <msg>                           start of message
   <src>CC128-v0.11</src>        source and software version
   <dsb>00089</dsb>              days since birth, ie days run
   <time>13:02:39</time>         24 hour clock time as displayed
   <tmpr>18.7</tmpr>             temperature as displayed
   <sensor>1</sensor>            Appliance Number as displayed
   <id>01234</id>                radio ID received from the sensor
   <type>1</type>                sensor Type, "1" = electricity
   <ch1>                         sensor channel
      <watts>00345</watts>       data and units
   </ch1>
   <ch2>
      <watts>02151</watts>
   </ch2>
   <ch3>
      <watts>00000</watts>
   </ch3>
 </msg>                           end of message
 
=cut

sub collect_cc128_data {

    # Read data from the serial port until we see a repeated sensor
    my ( $port, $baud ) = @_;

    my $tty = Device::SerialPort->new($port) || die "Can't open $port: $!";
    $tty->baudrate($baud)   || die "Can't set serial baudrate";
    $tty->parity("none")    || die "Can't set serial parity";
    $tty->databits(8)       || die "Can't set serial databits";
    $tty->handshake("none") || die "Can't set serial handshake";
    $tty->write_settings    || die "Can't set serial parameters";

    open( my $ttydev, "<", $port ) || die "Can't open $port: $?";

    my @cc_data_arr;
    my %seen_sensors;

    while (<$ttydev>) {
        print "# Read from device: $_\n" if $MUNIN_DEBUG;
        if (m{(<msg>.*</msg>)}) {
            my $xmlref = XML::Simple::XMLin( $1, KeepRoot => 1 );

            my $sensor = $xmlref->{msg}->{sensor};
            print "# Parsing Sensor $sensor\n" if $MUNIN_DEBUG;
            next unless defined $sensor;
            if ( defined $seen_sensors{$sensor} ) {

                # We've seen this sensor before.
                # Time to stop reading data
                print "# Hello again, Sensor $sensor\n" if $MUNIN_DEBUG;
                last;
            }
            $seen_sensors{$sensor} = 1;

            my $temphash;
            $temphash->{sensor} = $sensor;
            $temphash->{temp}   = $xmlref->{msg}->{tmpr};
            my @temparr;
            foreach my $key ( keys %{ $xmlref->{msg} } ) {
                if ( $key =~ /ch(\d+)/ ) {
                    my $channel = $1;
                    my $unit = ( keys %{ $xmlref->{msg}->{"ch$channel"} } )[0];
                    my $val  = $xmlref->{msg}->{"ch$channel"}->{$unit};
                    print "# Channel $channel, Unit $unit, Value $val\n"
                      if $MUNIN_DEBUG;
                    push @temparr,
                      {
                        "channel" => $channel,
                        "unit"    => $unit,
                        "value"   => $val
                      };
                }
            }
            $temphash->{data} = \@temparr;

            push @cc_data_arr, $temphash;
        }
    }
    close($ttydev);

    return @cc_data_arr;
}

sub collect_cc02_data {
    # Function supplied by David Edmondson <dme@dme.org>

    # Read data from the serial port
    my ( $port, $baud ) = @_;

    my $tty = Device::SerialPort->new($port) || die "Can't open $port: $!";
    $tty->baudrate($baud)   || die "Can't set serial baudrate";
    $tty->parity("none")    || die "Can't set serial parity";
    $tty->databits(8)       || die "Can't set serial databits";
    $tty->handshake("none") || die "Can't set serial handshake";
    $tty->write_settings    || die "Can't set serial parameters";

    open( my $ttydev, "<", $port ) || die "Can't open $port: $?";

    my @cc_data_arr;

    while (<$ttydev>) {
        if (m{(<msg>.*</msg>)}) {
            my $xmlref = XML::Simple::XMLin( $1, KeepRoot => 1 );

            my $temphash;
            $temphash->{sensor} = 0;    # Only one sensor.
            $temphash->{temp}   = $xmlref->{msg}->{tmpr};
            my @temparr;
            foreach my $key ( keys %{ $xmlref->{msg} } ) {
                if ( $key =~ /ch(\d+)/ ) {
                    my $channel = $1;
                    my $unit = ( keys %{ $xmlref->{msg}->{"ch$channel"} } )[0];
                    my $val  = $xmlref->{msg}->{"ch$channel"}->{$unit};
                    push @temparr,
                      {
                        "channel" => $channel,
                        "unit"    => $unit,
                        "value"   => $val
                      };
                }
            }
            $temphash->{data} = \@temparr;

            push @cc_data_arr, $temphash;

            last;
        }
    }
    close($ttydev);

    return @cc_data_arr;
}

my @cc_data = load_data();

if ( defined $ARGV[0] and $ARGV[0] eq 'config' ) {
    if ($ret) {
        print $ret;
        exit 1;
    }

    for my $datum (@cc_data) {
        my $unit = '';
        foreach my $key ( @{ $datum->{data} } ) {
            if ( $unit eq '' ) {
                $unit = $key->{unit};
            }
            elsif ( $unit != $key->{unit} ) {
                print STDERR
"# Conflicting units ($unit and $key->{unit}) on sensor $datum->{sensor}";
            }
        }

        if ( $datum->{sensor} == 0 ) {

            # Output the Root graph (being sensor 0)
            print <<EOF;
multigraph currentcost
graph_title CurrentCost Consumption
graph_args --base 1000 -l 0
graph_vlabel $unit
graph_category sensors
graph_info This graph shows the 'whole house' data.
EOF
            if ( scalar( @{ $datum->{data} } ) > 1 ) {
                print "graph_total Total\n";
            }
            foreach my $channel ( @{ $datum->{data} } ) {
                my $fieldname = "ch" . $channel->{channel};
                print <<EOF;
$fieldname.label Channel $channel->{channel}
$fieldname.type GAUGE
$fieldname.min 0
$fieldname.draw AREA
${fieldname}_t.label Channel $channel->{channel} Trend
${fieldname}_t.type GAUGE
${fieldname}_t.min 0
${fieldname}_t.draw LINE2
EOF
                if ($nightrate) {
                    print <<EOF;
${fieldname}_n.label Channel $channel->{channel} Night
${fieldname}_n.type GAUGE
${fieldname}_n.min 0
${fieldname}_t.cdef ${fieldname}_n,UN,${fieldname},${fieldname},IF,3600,TRENDNAN
EOF
                }
                else {
                    print "${fieldname}_t.cdef ${fieldname},3600,TRENDNAN\n";
                }
            }

            # Output the Root cumulative graph
            print <<EOF;
multigraph currentcost_cumulative
graph_title CurrentCost Total Usage
graph_args --base 1000 -l 0
graph_vlabel $unit * hours
graph_category sensors
EOF
            print "graph_order ";
            my $confstr = '';
            foreach my $channel ( @{ $datum->{data} } ) {
                my $fieldname = "ch" . $channel->{channel};
                print "${fieldname}=currentcost.$fieldname ${fieldname}_d ";
                $confstr .= <<EOF;
${fieldname}.update no
${fieldname}.label Channel $channel->{channel}
${fieldname}.type GAUGE
${fieldname}.min 0
${fieldname}.cdef PREV,${fieldname},12,/,ADDNAN
${fieldname}_d.label Channel $channel->{channel} Daily
${fieldname}_d.type GAUGE
${fieldname}_d.min 0
EOF
            }
            print "\n$confstr";
            print <<EOF;
fudge.label (fudge)
fudge.type GAUGE
fudge.graph yes
EOF

            # Output the root cost graph
            print <<EOF;
multigraph currentcost_cost
graph_title CurrentCost Estimated Cost
graph_args --base 1000 -l 0
graph_vlabel ${currency}
graph_category sensors
EOF
            if ( scalar( @{ $datum->{data} } ) > 1 ) {
                print "graph_total Total\n";
            }
            foreach my $channel ( @{ $datum->{data} } ) {
                my $fieldname = "ch" . $channel->{channel};
                print <<EOF;
$fieldname.label Channel $channel->{channel}
$fieldname.type GAUGE
$fieldname.min 0
EOF
            }

        }
        else {

            # Output a subordinate graph (being an appliance)
            my $sensor = $datum->{sensor};
            print <<EOF;
multigraph currentcost.appliance$sensor
graph_title CurrentCost Consumption (Appliance $sensor)
graph_args --base 1000 -l 0
graph_vlabel $unit
graph_category sensors
graph_info This graph shows the data for Appliance $sensor
EOF
            if ( scalar( @{ $datum->{data} } ) > 1 ) {
                print "graph_total Total\n";
            }
            foreach my $channel ( @{ $datum->{data} } ) {
                my $fieldname = "ch" . $channel->{channel};
                print <<EOF;
$fieldname.label Channel $channel->{channel}
$fieldname.type GAUGE
$fieldname.min 0
EOF
                if ($nightrate) {
                    print <<EOF;
${fieldname}_n.label Channel $channel->{channel} Night
${fieldname}_n.type GAUGE
${fieldname}_n.min 0
EOF
                }
            }

            # Output the subordinate cumulative graph
            print <<EOF;
multigraph currentcost_cumulative.appliance$sensor
graph_title CurrentCost Total Usage (Appliance $sensor)
graph_args --base 1000 -l 0
graph_vlabel $unit * hours
graph_category sensors
EOF
            print "graph_order ";
            my $confstr = '';
            foreach my $channel ( @{ $datum->{data} } ) {
                my $fieldname = "ch" . $channel->{channel};
                print "$fieldname=currentcost.$fieldname ";
                $confstr .= <<EOF;
$fieldname.label Channel $channel->{channel}
$fieldname.type GAUGE
$fieldname.min 0
$fieldname.cdef PREV,$fieldname,12,/,ADDNAN
EOF
            }
            print "\n$confstr";

            # Output the subordinate Cost graph
            print <<EOF;
multigraph currentcost_cost.appliance$sensor
graph CurrentCost Estimated Cost (Appliance $sensor)
graph_args --base 1000 -l 0
graph_vlabel ${currency}
graph_category sensors
EOF
            if ( scalar( @{ $datum->{data} } ) > 1 ) {
                print "graph_total Total\n";
            }
            foreach my $channel ( @{ $datum->{data} } ) {
                my $fieldname = "ch" . $channel->{channel};
                print <<EOF;
$fieldname.label Channel $channel->{channel}
$fieldname.type GAUGE
$fieldname.min 0
EOF
            }
        }
    }
    save_data(@cc_data);
    exit 0;
}

# Output the value data
for my $datum (@cc_data) {
    if ( $datum->{sensor} == 0 ) {

        # Output the Root graph (being sensor 0)
        print "multigraph currentcost\n";
    }
    else {
        my $sensor = $datum->{sensor};
        print "multigraph currentcost.appliance$sensor\n";
    }
    foreach my $channel ( @{ $datum->{data} } ) {
        my $fieldname = "ch" . $channel->{channel};
        if ( is_night_rate() ) {
            print "${fieldname}_n.value " . $channel->{value} . "\n";
            print "${fieldname}.value 0\n";
        }
        else {
            print "${fieldname}.value " . $channel->{value} . "\n";
            print "${fieldname}_n.value 0\n";
        }
    }
    if ( $datum->{sensor} == 0 ) {

        # Output the Root graph (being sensor 0)
        print "multigraph currentcost_cumulative\n";
    }
    else {
        my $sensor = $datum->{sensor};
        print "multigraph currentcost_cumulative.appliance$sensor\n";
    }
    foreach my $channel ( @{ $datum->{data} } ) {
        my $fieldname = "ch" . $channel->{channel};
        my $value     = $channel->{daily};
        $value += $channel->{ndaily} if defined $channel->{ndaily};
        print "${fieldname}_d.value $value\n";
    }
    my @now = localtime(time);
    if ( $now[3] == 1 and $now[2] == 0 and $now[1] <= 5 ) {

        # First reading of month, reset.
        print "fudge.value 0\n";
    }
    else {
        print "fudge.value 1\n";
    }

    if ( $datum->{sensor} == 0 ) {

        # Output the Root Cost graph (being sensor 0)
        print "multigraph currentcost_cost\n";
    }
    else {
        my $sensor = $datum->{sensor};
        print "multigraph currentcost_cost.appliance$sensor\n";
    }
    foreach my $channel ( @{ $datum->{data} } ) {
        my $fieldname = "ch" . $channel->{channel};
        my $kWh       = $channel->{monthly} / 1000;
        my $nightkWh  = $channel->{nmonthly} / 1000
          if defined $channel->{nmonthly};
        my $cost = $standingcharge;
        if ( $nightrate and defined $nightkWh ) {
            $cost = $nightkWh * $nightrate;
        }
        if ( $kWh <= $rate1qty ) {
            $cost += $kWh * $rate1;
        }
        else {
            $cost += ( ( $kWh - $rate1qty ) * $rate2 ) + ( $rate1qty * $rate1 );
        }
        $cost = $cost / 100;    # Convert pence/cents into pounds/euros
        print "$fieldname.value $cost\n";
        my $extinfo = sprintf( "Usage is %.3f kWh.", $kWh );
        $extinfo .= sprintf( " Night usage is %.3f kWh.", $nightkWh )
          if defined $nightkWh;
        print "$fieldname.extinfo $extinfo\n";
    }
}
save_data(@cc_data);
exit 0;
