##############################################
# Thx to Himtronics
# http://www.mikrocontroller.net/topic/141831
# http://www.mikrocontroller.net/attachment/63563/km271-protokoll.txt
# Buderus documents: 63011376, 63011377, 63011378
# e.g. http://www.buderus.de/pdf/unterlagen/0063061377.pdf

package main;

use strict;
use warnings;
use Time::HiRes qw(gettimeofday);

sub KM271_Read($);
sub KM271_Ready($);
sub KM271_OpenDev($);
sub KM271_CloseDev($);
sub KM271_SimpleWrite(@);
sub KM271_SimpleRead($);
sub KM271_crc($);
sub KM271_setbits($$);
sub KM271_GetReading($$);
sub KM271_SetReading($$$$$);

my %km271_sets = (
  "hk1_nachtsoll"   => "07006565%02x656565", # 0.5 celsius
  "hk1_tagsoll"     => "0700656565%02x6565", # 0.5 celsius
  "hk1_betriebsart" => "070065656565%02x65",

  "ww_soll"         => "0C07656565%02x6565", # 1.0 celsius
  "ww_betriebsart"  => "0C0E%02x6565656565", 

  "logmode"         => "EE0000",
);


# Message address:byte_offset in the message
# Attributes:
#   d:x (divide), p:x (add), bf:x (bitfield), a:x (array) ne (generate no event)
#   mb:x (multi-byte-message, x-bytes, low byte), s (signed value)

my %km271_tr = (
  "CFG_SommerAb"                    => "0000:1",     # 6510242a021e
  "CFG_Raum_Temp_Nacht"             => "0000:2,d:2",
  "CFG_Raum_Temp_Tag"               => "0000:3,d:2",
  "CFG_Betriebsart"                 => "0000:4,a:4",
  "CFG_Auslegung"                   => "000e:4",     # 01045a054d65
  "CFG_FrostAb"                     => "0015:2",     # 030104650005
  "CFG_Raum_Temp_Aufschalt"         => "0015:0,s",
  "CFG_Absenkungsart"               => "001c:1,a:6", # 0c0101656565
  "CFG_Fernbedienung"               => "0031:4,a:0", # 656565fc0104
  "CFG_Raum_Temp_Offset"            => "0031:3,s",   # 
  "CFG_GebaeudeArt"                 => "0070:2,p:1", # f66502066565
  "CFG_WW_Temperatur"               => "007e:3",     # 65fb28373c65
  "CFG_ZirkPumpe"                   => "0085:5",     # 026565016502
  "CFG_Warmwasser"                  => "0085:3,a:0",
  "CFG_Display_Lang"                => "0093:0,a:3", # 000302656565
  "CFG_Display"                     => "0093:1,a:1",
  "CFG_MaxAus"                      => "009a:3",     # 65016554050c
  "CFG_PumpLogik"                   => "00a1:0",     # 2a0565656509
  "CFG_Abgastemp"                   => "00a1:5,p:-9,a:5",
  "CFG_Programm"                    => "0100:0,a:2", # 01ffff00ffff
  "CFG_UrlaubsTage"                 => "0169:3",     # 01ffff03ffff
  "CFG_UhrDiff"                     => "01e0:1,s",   # 010065656565

  "HK1_Betriebswerte1"              => "8000:0,bf:0",
  "HK1_Betriebswerte2"              => "8001:0,bf:1",
  "HK1_Vorlaufsolltemperatur"       => "8002:0",
  "HK1_Vorlaufisttemperatur"        => "8003:0,ne",  # 23% of all messages
  "HK1_Raumsolltemperatur"          => "8004:0,d:2",
  "HK1_Raumisttemperatur"           => "8005:0,d:2",
  "HK1_Einschaltoptimierungszeit"   => "8006:0",
  "HK1_Ausschaltoptimierungszeit"   => "8007:0",
  "HK1_Pumpenleistung"              => "8008:0",
  "HK1_Mischerstellung"             => "8009:0",
  "HK1_Heizkennlinie_bei_+_10_Grad" => "800c:0",
  "HK1_Heizkennlinie_bei_0_Grad"    => "800d:0",
  "HK1_Heizkennlinie_bei_-_10_Grad" => "800e:0",
  "HK2_Betriebswerte1"              => "8112:0,bf:0",
  "HK2_Betriebswerte2"              => "8113:0,bf:1",
  "HK2_Vorlaufsolltemperatur"       => "8114:0",
  "HK2_Vorlaufisttemperatur"        => "8115:0,ne",
  "HK2_Raumsolltemperatur"          => "8116:0,d:2",
  "HK2_Raumisttemperatur"           => "8117:0,d:2",
  "HK2_Einschaltoptimierungszeit"   => "8118:0",
  "HK2_Ausschaltoptimierungszeit"   => "8119:0",
  "HK2_Pumpenleistung"              => "811a:0",
  "HK2_Mischerstellung"             => "811b:0",
  "HK2_Heizkennlinie_bei_+_10_Grad" => "811e:0",
  "HK2_Heizkennlinie_bei_0_Grad"    => "811f:0",
  "HK2_Heizkennlinie_bei_-_10_Grad" => "8120:0",
  "WW_Betriebswerte1"               => "8424:0,bf:2",
  "WW_Betriebswerte2"               => "8425:0,bf:3",
  "WW_Solltemperatur"               => "8426:0",
  "WW_Isttemperatur"                => "8427:0",
  "WW_Einschaltoptimierungszeit"    => "8428:0",
  "WW_Ladepumpe"                    => "8429:0,bf:5",
  "Kessel_Vorlaufsolltemperatur"    => "882a:0",
  "Kessel_Vorlaufisttemperatur"     => "882b:0,ne",  # 23% of all messages
  "Brenner_Einschalttemperatur"     => "882c:0",
  "Brenner_Ausschalttemperatur"     => "882d:0",
  "Kessel_Integral1"                => "882e:0,ne",
  "Kessel_Integral"                 => "882f:0,ne,mb:2", # 46% of all messages
  "Kessel_Fehler"                   => "8830:0,bf:6",
  "Kessel_Betrieb"                  => "8831:0,bf:4",
  "Brenner_Ansteuerung"             => "8832:0,a:0",
  "Abgastemperatur"                 => "8833:0",
  "Brenner_Stellwert"               => "8834:0",
  "Brenner_Laufzeit1_Minuten2"      => "8836:0",
  "Brenner_Laufzeit1_Minuten1"      => "8837:0",
  "Brenner_Laufzeit1_Minuten"       => "8838:0,mb:3",
  "Brenner_Laufzeit2_Minuten2"      => "8839:0",
  "Brenner_Laufzeit2_Minuten1"      => "883a:0",
  "Brenner_Laufzeit2_Minuten"       => "883b:0,mb:3",
  "Aussentemperatur"                => "893c:0,s",
  "Aussentemperatur_gedaempft"      => "893d:0,s",
  "Versionsnummer_VK"               => "893e:0",
  "Versionsnummer_NK"               => "893f:0",
  "Modulkennung"                    => "8940:0",
);
my %km271_rev;

my @km271_bitarrays = (
  # 0 - HK_Betriebswerte1
  [ "leer", "Ausschaltoptimierung", "Einschaltoptimierung", "Automatik",
          "Warmwasservorrang", "Estrichtrocknung", "Ferien", "Frostschutz",
          "Manuell" ],
  # 1 - HK_Betriebswerte2
  [ "leer", "Sommer", "Tag", "keine Kommunikation mit FB", "FB fehlerhaft",
          "Fehler Vorlauffuehler", "maximaler Vorlauf",
          "externer Stoehreingang", "frei" ],
  # 2 - WW_Betriebswerte1
  [ "aus", "Automatik", "Desinfektion", "Nachladung", "Ferien",
         "Fehler Desinfektion", "Fehler Fuehler", "Fehler WW bleibt kalt",
         "Fehler Anode" ],
  # 3 - WW_Betriebswerte2
  [ "aus", "Laden", "Manuell", "Nachladen", "Ausschaltoptimierung",
         "Einschaltoptimierung", "Tag", "Warm", "Vorrang" ],
  # 4 - Kessel_Betrieb 
  [ "aus", "Tag", "Automatik", "Sommer",
         "Bit3", "Bit4", "Bit5", "Bit6", "Bit7" ],
  # 5 - WW_Ladepumpe
  [ "aus", "Ladepumpe", "Zirkulationspumpe", "Absenkung Solar",
         "Bit3", "Bit4", "Bit5", "Bit6", "Bit7" ],
  # 6 - Kessel_Fehler
  [ "keine", "Bit1", "Bit2", "Bit3", "Bit4",
        "Abgastemperatur ueberschritten", "Bit6", "Bit7" ],
);

my @km271_arrays = (
# 0 - Brenner_Ansteuerung , CFG_Fernbedienung, CFG_Warmwasser
  [ "aus", "an" ],
# 1 - CFG_Display 
  [ "Automatik", "Kessel", "Warmwasser", "Aussen" ],
# 2 - CFG_Programm 
  [ "Eigen1", "Familie", "Frueh", "Spaet", "Vormit", "Nachmit",
    "Mittag", "Single", "Senior" ],
# 3 - CFG_Display_Lang 
  [ "DE", "FR", "IT", "NL", "EN", "PL" ],
# 4 - CFG_Betriebsart
  [ "Nacht", "Tag", "Automatik" ],
# 5 - CFG_Abgastemp
  [ "Aus","50","55","60","65","70","75","80","85","90","95","100","105",
    "110","115","120","125","130","135","140","145","150","155","160","165",
    "170","175","180","185","190","195","200","205","210","215","220","225",
    "230","235","240","245","250" ],
# 6 - CFG_Absenkungsart
  [ "Abschalt","Reduziert","Raumhal","Aussenhal"]
);

my %km271_set_betriebsart = (
  "nacht"     => 0,
  "tag"       => 1,
  "automatik" => 2,
);


sub
KM271_Initialize($)
{
  my ($hash) = @_;

  $hash->{ReadFn}  = "KM271_Read";
  $hash->{ReadyFn} = "KM271_Ready";

  $hash->{DefFn}   = "KM271_Define";
  $hash->{UndefFn} = "KM271_Undef";
  $hash->{SetFn}   = "KM271_Set";
  $hash->{AttrList}= "do_not_notify:1,0 all_km271_events loglevel:0,1,2,3,4,5,6";
  my @a = ();
  $hash->{SENDBUFFER} = \@a;

  %km271_rev = ();
  foreach my $k (sort keys %km271_tr) {      # Reverse map
    my $v = $km271_tr{$k};
    my ($addr, $b) = split("[:,]", $v);
    $km271_rev{$addr}{$b} = $k;
  }
}

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

  return "wrong syntax: define <name> KM271 [devicename|none]"
    if(@a != 3);

  KM271_CloseDev($hash);
  my $name = $a[0];
  my $dev = $a[2];

  if($dev eq "none") {
    Log 1, "KM271 device is none, commands will be echoed only";
    return undef;
  }
  
  $hash->{DeviceName} = $dev;
  my $ret = KM271_OpenDev($hash);
  return $ret;
}


#####################################
sub
KM271_Undef($$)
{
  my ($hash, $arg) = @_;
  KM271_CloseDev($hash); 
  return undef;
}

#####################################
sub
KM271_Set($@)
{
  my ($hash, @a) = @_;

  return "\"set KM271\" needs at least an argument" if(@a < 2);

  my $fmt = $km271_sets{$a[1]};
  return "Unknown argument $a[1], choose one of " . 
                join(" ", sort keys %km271_sets) if(!defined($fmt));

  my ($val, $numeric_val);
  if($fmt =~ m/%/) {
    return "\"set KM271 $a[1]\" needs at least one parameter" if(@a < 3);
    $val = $a[2];
    $numeric_val = ($val =~ m/^[.0-9]+$/);
  }


  if($a[1] =~ m/^hk.*soll$/) {
    return "Argument must be numeric (between 10 and 30)" if(!$numeric_val);
    $val *= 2;
  }
  if($a[1] =~ m/^ww.*soll$/) {
    return "Argument must be numeric (between 30 and 60)" if(!$numeric_val);
  }

  if($a[1] =~ m/_betriebsart/) {
    $val = $km271_set_betriebsart{$val};
    return "Unknown arg, use one of " .
      join(" ", sort keys %km271_set_betriebsart) if(!defined($val));
  }

  my $data = ($val ? sprintf($fmt, $val) : $fmt);

  push @{$hash->{SENDBUFFER}}, $data;
  KM271_SimpleWrite($hash, "02") if(!$hash->{WAITING});

  return undef;
}


#####################################
# called from the global loop, when the select for hash->{FD} reports data
sub
KM271_Read($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};
  my ($data, $crc);

  my $buf = KM271_SimpleRead($hash);
  Log 5, "KM271RAW: " . unpack('H*', $buf);

  if(!defined($buf)) {
    Log 1, "$name: EOF";
    KM271_CloseDev($hash);
    return;
  }

  $buf = unpack('H*', $buf);

  if(@{$hash->{SENDBUFFER}} || $hash->{DATASENT}) {               # Send data

    if($buf eq "02") {                    # KM271 Wants to send, override
      KM271_SimpleWrite($hash, "02");
      return;
    }

    if($buf eq "10") {
      if($hash->{DATASENT}) {
        delete($hash->{DATASENT});
        KM271_SimpleWrite($hash, "02") if(@{$hash->{SENDBUFFER}});
        return;
      }
      $data = pop @{ $hash->{SENDBUFFER} };
      $data =~ s/10/1010/g;
      $crc = KM271_crc($data);
      KM271_SimpleWrite($hash, $data."1003$crc");  # Send the data
    }

    if($buf eq "15") {                        # NACK from the KM271
      Log 1, "$name: NACK!";
      delete($hash->{DATASENT});
      KM271_SimpleWrite($hash, "02") if(@{$hash->{SENDBUFFER}});
      return;
    }

  } elsif($buf eq "02") {                    # KM271 Wants to send
    KM271_SimpleWrite($hash, "10");     # We are ready
    $hash->{PARTIAL} = "";
    $hash->{WAITING} = 1;
    return;

  }


  $hash->{PARTIAL} .= $buf;
  my $len = length($hash->{PARTIAL});
  return if($hash->{PARTIAL} !~ m/^(.*)1003(..)$/);
  ($data, $crc) = ($1, $2);
  $hash->{PARTIAL} = "";
  delete($hash->{WAITING});

  if(KM271_crc($data) ne $crc) {
    Log 1, "Wrong CRC in $hash->{PARTIAL}: $crc vs. ". KM271_crc($data);
    KM271_SimpleWrite($hash, "15"); # NAK
    KM271_SimpleWrite($hash, "02") if(@{$hash->{SENDBUFFER}}); # want to send
    return;
  }

  KM271_SimpleWrite($hash, "10");       # ACK, Data received ok


  $data =~ s/1010/10/g;
  if($data !~ m/^(....)(.*)/) {
    Log 1, "$name: Bogus message: $data";
    return;
  }

  ######################################
  # Analyze the data
  my ($fn, $arg) = ($1, $2);
  my $msghash = $km271_rev{$fn};
  my $all_events = KM271_attr($name, "all_km271_events") ;
  my $tn = TimeNow();

  #Log 1, "$data" if($fn ne "0400");

  if($msghash) {
    foreach my $off (keys %{$msghash}) {

      my $key = $msghash->{$off};
      my $val = hex(substr($arg, $off*2, 2));
      my $ntfy = 1;
      my @postprocessing = split(",", $km271_tr{$key});
      shift @postprocessing;
      while(@postprocessing) {
        my ($f,$farg) = split(":", shift @postprocessing);

           if($f eq "d")  { $val /= $farg; }
        elsif($f eq "p")  { $val += $farg; }
        elsif($f eq "ne") { $ntfy = $all_events; }
        elsif($f eq "s")  { $val = $val-256 if($val > 128); }
        elsif($f eq "bf") { $val = KM271_setbits($val, $farg); }
        elsif($f eq "a")  { $val = $km271_arrays[$farg][$val]; }
        elsif($f eq "mb") {
          $val += KM271_GetReading($hash, $key."1") * 256;
          $val += KM271_GetReading($hash, $key."2") * 65536 if($farg == 3);
        } 
      }
      KM271_SetReading($hash, $tn, $key, $val, $ntfy);
    }

  } elsif($fn eq "0400") {
    KM271_SetReading($hash, $tn, "NoData", $arg, 0);

  } elsif($all_events) { 
    KM271_SetReading($hash, $tn, "UNKNOWN_$fn", $data, 1);

  } else {            # Just ignore
    return;

  }

}

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

  # This is relevant for windows/USB only
  my $po = $hash->{Dev};
  my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
  return ($InBytes>0);
}

########################
sub
KM271_SimpleWrite(@)
{
  my ($hash, $msg) = @_;
  Log 3, "KM271 SimpleWrite $msg" if(length($msg) != 2);
  $hash->{Dev}->write(pack('H*',$msg)) if($hash->{DeviceName});
}

########################
sub
KM271_SimpleRead($)
{
  my ($hash) = @_;
  return $hash->{Dev}->input() if($hash->{Dev});
  return undef;
}

########################
sub
KM271_CloseDev($)
{
  my ($hash) = @_;
  my $name = $hash->{NAME};
  my $dev = $hash->{DeviceName};

  return if(!$dev);     # "none"
  
  if($hash->{Dev}) {
    $hash->{Dev}->close() ;
    delete($hash->{Dev});

  }
  delete($selectlist{"$name.$dev"});
  delete($readyfnlist{"$name.$dev"});
  delete($hash->{FD});
  delete($hash->{DeviceName});
}

########################
sub
KM271_OpenDev($)
{
  my ($hash) = @_;
  my $dev = $hash->{DeviceName};
  my $name = $hash->{NAME};
  my $po;

  $hash->{PARTIAL} = "";
  Log 3, "KM271 opening $name device $dev";

  if ($^O=~/Win/) {
   require Win32::SerialPort;
   $po = new Win32::SerialPort ($dev);
  } else  {
   require Device::SerialPort;
   $po = new Device::SerialPort ($dev);
  }

  if(!$po) {
    Log(3, "Can't open $dev: $!");
    return "";
  }
  $hash->{Dev} = $po;
  if( $^O =~ /Win/ ) {
    $readyfnlist{"$name.$dev"} = $hash;
  } else {
    $hash->{FD} = $po->FILENO;
    delete($readyfnlist{"$name.$dev"});
    $selectlist{"$name.$dev"} = $hash;
  }

  $po->reset_error();
  $po->baudrate(2400);
  $po->databits(8);
  $po->parity('none');
  $po->stopbits(1);
  $po->handshake('none');

  $hash->{STATE} = "Initialized";

  push @{$hash->{SENDBUFFER}}, "EE0000";
  KM271_SimpleWrite($hash, "02");      # STX

  Log 3, "$dev opened";
  return undef;
}

sub
KM271_setbits($$)
{
  my ($val, $arridx) = @_;
  my @ret;
  for(my $idx = 1; $idx <= 8; $idx++) {
    push(@ret, $km271_bitarrays[$arridx][$idx]) if($val & (1<<($idx-1)));
  }
  return $km271_bitarrays[$arridx][0] if(!int(@ret));
  return join(",", @ret);
}

sub
KM271_crc($)
{
  my $in = shift;
  my $x = 0;
  foreach my $a (split("", pack('H*', $in))) {
    $x ^= ord($a);
  }
  $x ^= 0x10;
  $x ^= 0x03;
  return sprintf("%02x", $x);
}

sub
KM271_attr($$)
{
  my ($name, $attr) = @_;
  return $attr{$name}{$attr} if($attr{$name} && $attr{$name}{$attr});
  return "";
}

sub
KM271_GetReading($$)
{
  my ($hash, $msg) = @_;
  return $hash->{READINGS}{$msg}{VAL}
    if($hash->{READINGS} && $hash->{READINGS}{$msg});
  return 0;
}

sub
KM271_SetReading($$$$$)
{
  my ($hash,$tn,$key,$val,$ntfy) = @_;
  my $name = $hash->{NAME};
  Log GetLogLevel($name,4), "$name: $key $val" if($key ne "NoData");
  $hash->{READINGS}{$key}{TIME} = $tn;
  $hash->{READINGS}{$key}{VAL} = $val;
  DoTrigger($name, "$key: $val") if($ntfy);
}


1;
