##############################################
# $Id$
package main;

use strict;
use warnings;


#####################################
sub
KS300_Initialize($)
{
  my ($hash) = @_;

  # Message is like
  # 810d04f94027a00171212730000008
  # 81 0d 04 f9 4027a00171 212730000008

  $hash->{Match}     = "^810d04..4027a001";
  $hash->{DefFn}     = "KS300_Define";
  $hash->{UndefFn}   = "KS300_Undef";
  $hash->{ParseFn}   = "KS300_Parse";
  $hash->{AttrList}  = "IODev do_not_notify:0,1 showtime:0,1 model:ks300 loglevel:0,1 rainadjustment:0,1 ignore:0,1";
}

#####################################
sub
KS300_Define($$)
{
  my ($hash, $def) = @_;
  my @a = split("[ \t][ \t]*", $def);

  return "wrong syntax: define <name> KS300 <code> " .
          "[ml/raincounter] [wind-factor]" if(int(@a) < 3 || int(@a) > 5);
  $a[2] = lc($a[2]);
  return "Define $a[0]: wrong CODE format: specify a 4 digit hex value"
                if($a[2] !~ m/^[a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/);

  $hash->{CODE} = $a[2];
  my $rainunit = ((int(@a) > 3) ? $a[3] : 255);
  my $windunit = ((int(@a) > 4) ? $a[4] : 1.0);
  $hash->{CODE} = $a[2];
  $hash->{RAINUNIT} = $rainunit;
  $hash->{WINDUNIT} = $windunit;
  $modules{KS300}{defptr}{$a[2]} = $hash;
  AssignIoPort($hash);

  return undef;
}

#####################################
sub
KS300_Undef($$)
{
  my ($hash, $name) = @_;
  delete($modules{KS300}{defptr}{$hash->{CODE}});
  return undef;
}


#####################################
sub
KS300_Parse($$)
{
  my ($hash,$msg) = @_;

  ###############################
  #          1          2
  #0123456789012345 67890123456789
  #
  #810d04f94027a001 71212730000008
  ###############################
  my @a = split("", $msg);

  ##########################
  # I've seldom (1 out of 700) seen messages of length 10 and 11 with correct
  # CRC, they seem to contain partial data (e.g. temp/wind/hum but not rain)
  # They are suppressed as of now.
  if(hex($a[3]) != 13) {
    Log 4, "Strange KS300 message received, won't decode ($msg)";
    return "";
  }

  if(int(keys %{ $modules{KS300}{defptr} })) {

    my @arr = keys(%{ $modules{KS300}{defptr} }); # No code is known yet
    my $dev = shift(@arr);
    my $def = $modules{KS300}{defptr}{$dev};
    my $haverain = 0;
    my $name= $def->{NAME};
    return "" if(IsIgnored($name));

    my @v;
    my @txt = ( "rain_raw", "rain", "wind", "humidity", "temperature",
                "israining", "unknown1", "unknown2", "unknown3");
    my @sfx = ( "(counter)", "(l/m2)", "(km/h)", "(%)", "(Celsius)",
                "(yes/no)", "","","");
    my %repchanged = ("rain"=>1, "wind"=>1, "humidity"=>1, "temperature"=>1,
                "israining"=>1);

    # counter for the change hash
    my $n= 1; # 0 is STATE and will b explicitely set

    # time
    my $tm = TimeNow();
    my $tsecs= time();  # number of non-leap seconds since January 1, 1970, UTC

    # The next instr wont work for empty hashes, so we init it now
    $def->{READINGS}{$txt[0]}{VAL} = 0 if(!$def->{READINGS});
    my $r = $def->{READINGS};


    # preset current $rain_raw
    $v[0] = hex("$a[28]$a[27]$a[26]");
    my $rain_raw= $v[0];

    # get previous rain_raw
    my $rain_raw_prev= $rain_raw;
    if(defined($r->{rain_raw})) {
      ($rain_raw_prev, undef)= split(" ", $r->{rain_raw}{VAL}); # cut off "(counter)"
    };

    # unadjusted value as default
    my $rain_raw_adj= $rain_raw;

    # get previous rain_raw_adj
    my $rain_raw_adj_prev= $rain_raw;
    if(defined($r->{rain_raw_adj})) {
         $rain_raw_adj_prev= $r->{rain_raw_adj}{VAL};
    };

    if(defined($attr{$name}) &&
       defined($attr{$name}{"rainadjustment"}) &&
       ($attr{$name}{"rainadjustment"}>0)) {

       # The rain values delivered by my KS300 randomly switch between two
       # different values. The offset between the two values follows no
       # identifiable principle. It is even unclear whether the problem is
       # caused by KS300 or by FHZ1300. ELV denies any problem with the KS300.
       # The problem is known to several people. For instance, see
       # http://www.ipsymcon.de/forum/showthread.php?t=3303&highlight=ks300+regen&page=3
       # The following code detects and automatically corrects these offsets.

       my $rain_raw_ofs;
       my $rain_raw_ofs_prev;
       my $tsecs_prev;

       # get previous offet
       if(defined($r->{rain_raw_ofs})) {
         $rain_raw_ofs_prev= $r->{rain_raw_ofs}{VAL};
       } else{
         $rain_raw_ofs_prev= 0;
       }

       # the current offset is the same, but this may change later
       $rain_raw_ofs= $rain_raw_ofs_prev;

       # get previous tsecs
       if(defined($r->{tsecs})) {
         $tsecs_prev= $r->{tsecs}{VAL};
       } else{
         $tsecs_prev= 0; # 1970-01-01
       }

       # detect error condition
       # delta is negative or delta is too large
       # see http://de.wikipedia.org/wiki/Niederschlagsintensit??t#Niederschlagsintensit.C3.A4t
       # during a thunderstorm in middle europe, 50l/m^2 rain may fall per hour
       # 50l/(m^2*h) correspond to 200 ticks/h
       # Since KS300 sends every 2,5 minutes, a maximum delta of 8 ticks would
       # be reasonable. The observed deltas are in most cases 1 or 2 orders
       # of magnitude larger.
       # The code also handles counter resets after battery replacement

       my $rain_raw_delta= $rain_raw- $rain_raw_prev;
       if($tsecs!= $tsecs_prev) { # avoids a rare but relevant condition
            my $thours_delta= ($tsecs- $tsecs_prev)/3600.0; # in hours
            my $rain_raw_per_hour= $rain_raw_delta/$thours_delta;
            if(($rain_raw_delta<0) || ($rain_raw_per_hour> 200.0)) {
                $rain_raw_ofs= $rain_raw_ofs_prev-$rain_raw_delta;
                # If the switch in the tick count occurs simultaneously with an
                # increase due to rain, the tick is lost. We therefore assume that
                # offsets between -5 and 0 are indeed rain.
                if(($rain_raw_ofs>=-5) && ($rain_raw_ofs<0)) { $rain_raw_ofs= 0; }
                $r->{rain_raw_ofs}{TIME} = $tm;
                $r->{rain_raw_ofs}{VAL} = $rain_raw_ofs;
                $def->{CHANGED}[$n++] = "rain_raw_ofs: $rain_raw_ofs";
            }
       }
       $rain_raw_adj= $rain_raw+ $rain_raw_ofs;

    }

    # remember tsecs
    $r->{tsecs}{TIME} = $tm;
    $r->{tsecs}{VAL} = "$tsecs";

    # remember rain_raw_adj
    $r->{rain_raw_adj}{TIME} = $tm;
    $r->{rain_raw_adj}{VAL} = $rain_raw_adj;


    # KS300 has a sensor which detects any drop of rain and immediately
    # sends out the israining message. The sensors consists of two parallel
    # strips of metal separated by a small gap. The rain bridges the gap
    # and closes the contact. If the KS300 pole is not perfectly vertical the
    # drop runs along only one side and the contact is not closed. To get the
    # israining information anyway, the respective flag is also set when the
    # a positive amount of rain is detected.
    $haverain = 1 if($rain_raw_adj != $rain_raw_adj_prev);

    $v[1] = sprintf("%0.1f", $rain_raw_adj * $def->{RAINUNIT} / 1000);
    $v[2] = sprintf("%0.1f", ("$a[25]$a[24].$a[23]"+0) * $def->{WINDUNIT});
    $v[3] = "$a[22]$a[21]" + 0;
    $v[4] = "$a[20]$a[19].$a[18]" + 0; $v[4] = "-$v[4]" if($a[17] eq "7");
    $v[4] = sprintf("%0.1f", $v[4]);

    $v[5] = ((hex($a[17]) & 0x2) || $haverain) ? "yes" : "no";
    $v[6] = $a[29];
    $v[7] = $a[16];
    $v[8] = $a[17];

    # Negative temp
    $v[4] = -$v[4] if($v[8] & 8);

    Log GetLogLevel($def->{NAME},4), "KS300 $dev: $msg";

    my $max = int(@v);

    # For logging/summary
    my $val = "T: $v[4]  H: $v[3]  W: $v[2]  R: $v[1]  IR: $v[5]";
    Log GetLogLevel($def->{NAME},4), "KS300 $dev: $val";
    $def->{STATE} = $val;
    $def->{CHANGED}[0] = $val;

    for(my $i = 0; $i < $max; $i++) {
      $r->{$txt[$i]}{TIME} = $tm;
      #$val = "$v[$i] $sfx[$i]";
      $val = $v[$i];
      $r->{$txt[$i]}{VAL} = $val;
      $def->{CHANGED}[$n++] = "$txt[$i]: $val"
                if(defined($repchanged{$txt[$i]}));
    }

    ###################################
    # AVG computing
    if(!$r->{cum_day}) {

      $r->{cum_day}{VAL} = "$tm T: 0 H: 0 W: 0 R: $v[1]";
      $r->{avg_day}{VAL} = "T: $v[4]  H: $v[3]  W: $v[2]  R: $v[1]";

    } else {

      my @cv = split(" ", $r->{cum_day}{VAL});

      my @cd = split("[ :-]", $r->{cum_day}{TIME});
      my $csec = 3600*$cd[3] + 60*$cd[4] + $cd[5]; # Sec of last reading

      my @d = split("[ :-]", $tm);
      my $sec = 3600*$d[3] + 60*$d[4] + $d[5];     # Sec now

      my @sd = split("[ :-]", "$cv[0] $cv[1]");
      my $ssec = 3600*$sd[3] + 60*$sd[4] + $sd[5]; # Sec at start of day

      my $difft = $sec - $csec;
      $difft += 86400 if($d[2] != $cd[2]);         # Sec since last reading

      my $t = $cv[3] + $difft * $v[4];
      my $h = $cv[5] + $difft * $v[3];
      my $w = $cv[7] + $difft * $v[2];
      my $e = $cv[9];

      $r->{cum_day}{VAL} = "$cv[0] $cv[1] T: $t  H: $h  W: $w  R: $e";

      $difft = $sec - $ssec;
      $difft += 86400 if($d[2] != $sd[2]);         # Sec since last reading

      $difft = 1 if(!$difft);                      # Don't want illegal division.
      $t /= $difft; $h /= $difft; $w /= $difft; $e = $v[1] - $cv[9];
      $r->{avg_day}{VAL} =
      		sprintf("T: %.1f  H: %d  W: %.1f  R: %.1f", $t, $h, $w, $e);

      if($d[2] != $sd[2]) {			   # Day changed, report it

        $def->{CHANGED}[$n++] = "avg_day $r->{avg_day}{VAL}";
        $r->{cum_day}{VAL} = "$tm T: 0 H: 0 W: 0 R: $v[1]";

	if(!$r->{cum_month}) {                     # Check the month

	  $r->{cum_month}{VAL} = "1 $r->{avg_day}{VAL}";
	  $r->{avg_month}{VAL} = $r->{avg_day}{VAL};

	} else {

	  my @cmv = split(" ", $r->{cum_month}{VAL});
	  $t += $cmv[2]; $w += $cmv[4]; $h += $cmv[6];

	  $cmv[0]++;
	  $r->{cum_month}{VAL} =
	  	sprintf("%d T: %.1f  H: %d  W: %.1f  R: %.1f",
				$cmv[0], $t, $h, $w, $cmv[8]+$e);
	  $r->{avg_month}{VAL} =
	  	sprintf("T: %.1f  H: %d  W: %.1f  R: %.1f",
				$t/$cmv[0], $h/$cmv[0], $w/$cmv[0], $cmv[8]+$e);

	  if($d[1] != $sd[1]) {                   # Month changed, report it

	    $def->{CHANGED}[$n++] = "avg_month $r->{avg_month}{VAL}";
	    $r->{cum_month}{VAL} = "0 T: 0 H: 0 W: 0 R: 0";

	  }

        }
        $r->{cum_month}{TIME} = $r->{avg_month}{TIME} = $tm;

      }

    }
    $r->{cum_day}{TIME} = $r->{avg_day}{TIME} = $tm;
    # AVG computing
    ###################################

    return $name;

  } else {

    Log 4, "KS300 detected: $msg";
    return "UNDEFINED KS300 KS300 1234";

  }

}

1;
