##############################################
# 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
# $Id$

package main;

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

sub KM271_Read($);
sub KM271_Ready($);
sub KM271_crc($);
sub KM271_setbits($$);
sub KM271_SetReading($$$$$);

my %km271_sets = (
  "hk1_nachtsoll"   => "07006565%02x656565:0700%02x", # 0.5 celsius
  "hk1_tagsoll"     => "0700656565%02x6565:0701%02x", # 0.5 celsius
  "hk1_betriebsart" => "070065656565%02x65:0702%02x",
  "hk2_nachtsoll"   => "08006565%02x656565:0800%02x", # 0.5 celsius
  "hk2_tagsoll"     => "0800656565%02x6565:0801%02x", # 0.5 celsius
  "hk2_betriebsart" => "080065656565%02x65:0802%02x",

  "ww_soll"         => "0C07656565%02x6565:0c07%02x", # 1.0 celsius
  "ww_betriebsart"  => "0C0E%02x6565656565:0c0e%02x",
  "ww_on-for-timer" => "0C0E%02x6565656565:0c0e%02x",

  "hk1_programm"    => "1100%02x6565656565",
  "hk1_timer"       => "11%s",
  "hk2_programm"    => "1200%02x6565656565",  
  "hk2_timer"       => "12%s",
  
  "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)
#   em (error message - special handling), t (timer - special handling), eh (error history - special handling)
my %km271_tr = (
  "CFG_Sommer_ab"                   => "0000:1,p:-9,a:8",
  "CFG_HK1_Nachttemperatur"         => "0000:2,d:2",
  "cFG_HK1_Nachttemperatur"         => "0700:0,d:2",  # fake reading for internal notify
  "CFG_HK1_Tagtemperatur"           => "0000:3,d:2",
  "cFG_HK1_Tagtemperatur"           => "0701:0,d:2",  # fake reading for internal notify
  "CFG_HK1_Betriebsart"             => "0000:4,a:4",
  "cFG_HK1_Betriebsart"             => "0702:0,a:4",  # fake reading for internal notify
  "CFG_HK1_Max_Temperatur"          => "000e:2",
  "CFG_HK1_Auslegung"               => "000e:4",
  "CFG_HK1_Aufschalttemperatur"     => "0015:0,a:9",
  "CFG_Frost_ab"                    => "0015:2,s",
  "CFG_HK1_Absenkungsart"           => "001c:1,a:6",
  "CFG_HK1_Heizsystem"              => "001c:2,a:7",
  "CFG_HK1_Temperatur_Offset"       => "0031:3,s,d:2",
  "CFG_HK1_Fernbedienung"           => "0031:4,a:0",
  "CFG_HK2_Nachttemperatur"         => "0038:2,d:2",
  "cFG_HK2_Nachttemperatur"         => "0800:0,d:2",  # fake reading for internal notify
  "CFG_HK2_Tagtemperatur"           => "0038:3,d:2",
  "cFG_HK2_Tagtemperatur"           => "0801:0,d:2",  # fake reading for internal notify
  "CFG_HK2_Betriebsart"             => "0038:4,a:4",
  "cFG_HK2_Betriebsart"             => "0802:0,a:4",  # fake reading for internal notify
  "CFG_HK2_Max_Temperatur"          => "0046:2",
  "CFG_HK2_Auslegung"               => "0046:4",
  "CFG_HK2_Aufschalttemperatur"     => "004d:0,a:9",
  "CFG_WW_Vorrang"                  => "004d:1,a:0",
  "CFG_HK2_Absenkungsart"           => "0054:1,a:6",
  "CFG_HK2_Heizsystem"              => "0054:2,a:7",
  "CFG_HK2_Temperatur_Offset"       => "0069:3,s,d:2",
  "CFG_HK2_Fernbedienung"           => "0069:4,a:0",
  "CFG_Gebaeudeart"                 => "0070:2,p:1",
  "CFG_WW_Temperatur"               => "007e:3",
  "cFG_WW_Temperatur"               => "0c07:0",      # fake reading for internal notify
  "CFG_WW_Betriebsart"              => "0085:0,a:4",
  "cFG_WW_Betriebsart"              => "0c0e:0,a:4",  # fake reading for internal notify
  "CFG_WW_Aufbereitung"             => "0085:3,a:0",
  "CFG_WW_Zirkulation"              => "0085:5,a:9",
  "CFG_Sprache"                     => "0093:0,a:3",
  "CFG_Anzeige"                     => "0093:1,a:1",
  "CFG_Max_Kesseltemperatur"        => "009a:3",
  "CFG_Pumplogik"                   => "00a1:0",
  "CFG_Abgastemperaturschwelle"     => "00a1:5,p:-9,a:5",
  
  "PRG_HK1_Programm"                => "0100:0,a:2",
  "CFG_Urlaubstage"                 => "0100:3",
  "PRG_HK1_Timer01"                 => "0107:0,t",
  "PRG_HK1_Timer02"                 => "010e:0,t",
  "PRG_HK1_Timer03"                 => "0115:0,t",
  "PRG_HK1_Timer04"                 => "011c:0,t",
  "PRG_HK1_Timer05"                 => "0123:0,t",
  "PRG_HK1_Timer06"                 => "012a:0,t",
  "PRG_HK1_Timer07"                 => "0131:0,t",
  "PRG_HK1_Timer08"                 => "0138:0,t",
  "PRG_HK1_Timer09"                 => "013f:0,t",
  "PRG_HK1_Timer10"                 => "0146:0,t",
  "PRG_HK1_Timer11"                 => "014d:0,t",
  "PRG_HK1_Timer12"                 => "0154:0,t",
  "PRG_HK1_Timer13"                 => "015b:0,t",
  "PRG_HK1_Timer14"                 => "0162:0,t",
  "PRG_HK2_Programm"                => "0169:0,a:2",
  "PRG_HK2_Timer01"                 => "0170:0,t",
  "PRG_HK2_Timer02"                 => "0177:0,t",
  "PRG_HK2_Timer03"                 => "017e:0,t",
  "PRG_HK2_Timer04"                 => "0185:0,t",
  "PRG_HK2_Timer05"                 => "018c:0,t",
  "PRG_HK2_Timer06"                 => "0193:0,t",
  "PRG_HK2_Timer07"                 => "019a:0,t",
  "PRG_HK2_Timer08"                 => "01a1:0,t",
  "PRG_HK2_Timer09"                 => "01a8:0,t",
  "PRG_HK2_Timer10"                 => "01af:0,t",
  "PRG_HK2_Timer11"                 => "01b6:0,t",
  "PRG_HK2_Timer12"                 => "01bd:0,t",
  "PRG_HK2_Timer13"                 => "01c4:0,t",
  "PRG_HK2_Timer14"                 => "01cb:0,t",
  "CFG_Uhrzeit_Offset"              => "01e0:1,s",
  
  "ERR_Fehlerspeicher1"             => "0300:0,eh",
  "ERR_Fehlerspeicher2"             => "0307:0,eh",
  "ERR_Fehlerspeicher3"             => "030e:0,eh",
  "ERR_Fehlerspeicher4"             => "0315:0,eh",

  "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_Einschaltoptimierung"        => "8006:0",
  "HK1_Ausschaltoptimierung"        => "8007:0",
  "HK1_Pumpe"                       => "8008:0",
  "HK1_Mischerstellung"             => "8009:0",
  "HK1_Heizkennlinie_+10_Grad"      => "800c:0",
  "HK1_Heizkennlinie_0_Grad"        => "800d:0",
  "HK1_Heizkennlinie_-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",  # 23% of all messages
  "HK2_Raumsolltemperatur"          => "8116:0,d:2",
  "HK2_Raumisttemperatur"           => "8117:0,d:2",
  "HK2_Einschaltoptimierung"        => "8118:0",
  "HK2_Ausschaltoptimierung"        => "8119:0",
  "HK2_Pumpe"                       => "811a:0",
  "HK2_Mischerstellung"             => "811b:0",
  "HK2_Heizkennlinie_+10_Grad"      => "811e:0",
  "HK2_Heizkennlinie_0_Grad"        => "811f:0",
  "HK2_Heizkennlinie_-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_Einschaltoptimierung"         => "8428:0",
  "WW_Pumpentyp"                    => "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,mb:2,ne", # 46% of all messages
  "Kessel_Fehler"                   => "8830:0,bf:6",
  "Kessel_Betrieb"                  => "8831:0,bf:4",
  "Brenner_Ansteuerung"             => "8832:0,a:10",
  "Abgastemperatur"                 => "8833:0",
  "Brenner_Mod_Stellglied"          => "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",

  "ERR_Letzter_Fehlerstatus"        => "aa:0,em",
);

my %km271_rev;

my @km271_bitarrays = (
  # 0 - HK_Betriebswerte1
  [ "-", "Ausschaltoptimierung", "Einschaltoptimierung", "Automatik",
          "Warmwasservorrang", "Estrichtrocknung", "Ferien", "Frostschutz",
          "Manuell" ],
  # 1 - HK_Betriebswerte2
  [ "-", "Sommer", "Tag", "Keine Kommunikation mit FB", "FB fehlerhaft",
          "Fehler Vorlauffuehler", "Maximaler Vorlauf",
          "Externer Stoehreingang", "Frei" ],
  # 2 - WW_Betriebswerte1
  [ "-", "Automatik", "Desinfektion", "Nachladung", "Ferien",
         "Fehler Desinfektion", "Fehler Fuehler", "Fehler WW bleibt kalt",
         "Fehler Anode" ],
  # 3 - WW_Betriebswerte2
  [ "-", "Laden", "Manuell", "Nachladen", "Ausschaltoptimierung",
         "Einschaltoptimierung", "Tag", "Warm", "Vorrang" ],
  # 4 - Kessel_Betrieb 
  [ "-", "Abgastest", "Betrieb 1.Stufe", "Kesselschutz",
         "Unter Betrieb", "Leistung frei", "Leistung hoch", "Betrieb 2.Stufe", "Frei" ],
  # 5 - WW_Pumpentyp
  [ "-", "Ladepumpe", "Zirkulationspumpe", "Absenkung Solar",
         "Frei", "Frei", "Frei", "Frei", "Frei" ],
  # 6 - Kessel_Fehler
  [ "-", "Brennerstoerung", "Kesselfuehler", "Zusatzfuehler", "Kessel bleibt kalt",
        "Abgasfuehler", "Abgas ueber Grenzwert", "Sicherungskette ausgeloest", "Externe Stoerung" ],
);

my @km271_arrays = (
  # 0 - CFG_Fernbedienung, CFG_WW_Vorrang, CFG_Warmwasser
  [ "Aus", "An" ],
  # 1 - CFG_Anzeige
  [ "Automatik", "Kessel", "Warmwasser", "Aussen" ],
  # 2 - CFG_Programm
  [ "Eigen", "Familie", "Frueh", "Spaet", "Vormittag", "Nachmittag",
    "Mittag", "Single", "Senior" ],
  # 3 - CFG_Sprache
  [ "DE", "FR", "IT", "NL", "EN", "PL" ],
  # 4 - CFG_Betriebsart
  [ "Nacht", "Tag", "Automatik" ],
  # 5 - CFG_Abgastemperaturschwelle
  [ "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","Raumhalt","Aussenhalt" ],
  # 7 - CFG_Heizsystem
  [ "Aus","Heizkoerper","-","Fussboden" ],
  # 8 - CFG_Sommer_ab
  [ "Sommer","10","11","12","13","14","15","16","17","18","19",
    "20","21","22","23","24","25","26","27","28","29","30","Winter" ],
  # 9 - CFG_Aufschalttemperatur, CFG_Zirkulation
  [ "Aus","1","2","3","4","5","6","7","8","9","10" ],
  # 10 - Brenneransteuerung
  [ "Kessel aus", "1.Stufe an", "-", "-", "2.Stufe an bzw. Modulation frei" ],
);

# PRG_HK1_TimerXX, PRG_HK2_TimerXX
my %km271_days = (
  0x00  => "Mo",
  0x20  => "Di",
  0x40  => "Mi",
  0x60  => "Do",
  0x80  => "Fr",
  0xa0  => "Sa",
  0xc0  => "So",
);

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

# Used by set hk?_programm
my %km271_set_programm = (
  "eigen"      => 0,
  "familie"    => 1,
  "frueh"      => 2,
  "spaet"      => 3,
  "vormittag"  => 4,
  "nachmittag" => 5,
  "mittag"     => 6,
  "single"     => 7,
  "senior"     => 8,
);

# Used by set hk?_timer
my %km271_set_day = (
  "mo"  => 0x00,
  "di"  => 0x20,
  "mi"  => 0x40,
  "do"  => 0x60,
  "fr"  => 0x80,
  "sa"  => 0xa0,
  "so"  => 0xc0,
);

# Internal hash for storing actual timing parameter of heater, populated by "logmode" command
my %km271_timer;

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

  require "$attr{global}{modpath}/FHEM/DevIo.pm";

  $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 ww_timermode:automatik,tag";
  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);

  DevIo_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 = DevIo_OpenDev($hash, 0, "KM271_DoInit");
  return $ret;
}

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

#####################################
sub
KM271_Set($@)
{
  my ($hash, @a) = @_;
  my $name = $hash->{NAME};

  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 < 10 || $val > 30);
    $val *= 2;
  }
  elsif($a[1] =~ m/^ww.*soll$/) {
    return "Argument must be numeric (between 30 and 60)" if(!$numeric_val || $val < 30 || $val > 60);
  }
  elsif($a[1] =~ m/_betriebsart$/) {
    $val = $km271_set_betriebsart{$val};
    return "Unknown arg, use one of " .
      join(" ", sort keys %km271_set_betriebsart) if(!defined($val));
  }
  elsif($a[1] =~ m/_programm$/) {
    $val = $km271_set_programm{$val};
    return "Unknown arg, use one of " .
      join(" ", sort keys %km271_set_programm) if(!defined($val));
  }
  elsif($a[1] =~ m/^ww.*for-timer$/) {	# WW on-for-timer command
    my @time = split(":", $val);
    return "Duration must have the format HH:MM" if(@time < 2);
    $val = $time[0];
    $numeric_val = ($val =~ m/^[.0-9]+$/);
    return "Duration must have the format HH:MM" if(!$numeric_val || $val < 0 || $val > 23);
    my $val2 = $time[1];
    $numeric_val = ($val2 =~ m/^[.0-9]+$/);
    return "Duration must have the format HH:MM" if(!$numeric_val || $val2 < 0 || $val2 > 59);
    return "Duration must be greater than 00:00" if($val + $val2 == 0);

    if($modules{KM271}{ldata}{$name}) {
      CommandDelete(undef, $name . "_ww_autoOff");
      delete $modules{KM271}{ldata}{$name};
    }
    my $to = sprintf("%02d:%02d", $val, $val2);
    CommandDefine(undef, $name . "_ww_autoOff at +$to set $name ww_betriebsart nacht");
    $modules{KM271}{ldata}{$name} = $to;
    
    $val = $km271_set_betriebsart{AttrVal($name, "ww_timermode", "tag")};
  }
  elsif($a[1] =~ m/^hk.*timer$/) {  # Timer calculation
    return "\"set KM271 $a[1]\" needs typically 5 parameters (position on-day on-time off-day off-time)" if(@a < 4);
    $val = $a[2];
    $numeric_val = ($val =~ m/^[.0-9]+$/);
    # 42 slots for a timer, but each interval uses two of them (on and off)
    return "Position must be numeric (between 1 and 21)" if(!$numeric_val || $val < 1 || $val > 21);
    my $pos = $val;
    my $offval;
    if ($a[3] eq "delete") {
      # Delete the interval
      $offval = "c290";   # Code for not used
      $val = $offval;
    } else {
      # Set interval: more arguments are needed
      return "\"set KM271 $a[1]\" needs at least 5 parameters (position day on-time day off-time)" if(@a < 7);
      my $offday = $km271_set_day{$a[5]};
      return "Unknown day, use one of " .
        join(" ", sort keys %km271_set_day) if(!defined($offday));

      # Time validation off-time
      my @time = split(":", $a[6]);
      return "Fifth argument must be a valid time (e.g. 14:50)" if(@time < 2);
      $val = $time[0];
      $numeric_val = ($val =~ m/^[.0-9]+$/);
      return "Fifth argument must be a valid time (e.g. 14:50)" if(!$numeric_val || $val < 0 || $val > 23);
      $val = $time[1];
      $numeric_val = ($val =~ m/^[.0-9]+$/);
      return "Fifth argument must be a valid time (e.g. 14:50)" if(!$numeric_val || $val < 0 || $val > 59);
      # Calculate off-day and -time (unit: 10 min) for heater
      $offval = sprintf("%02x%02x", $offday, int(($time[0]*60 + $val) / 10));

      my $onday = $km271_set_day{$a[3]};
      return "Unknown day, use one of " .
        join(" ", sort keys %km271_set_day) if(!defined($onday));

      # Time validation on-time
      @time = split(":", $a[4]);
      return "Third argument must be a valid time (e.g. 13:40)" if(@time < 2);
      $val = $time[0];
      $numeric_val = ($val =~ m/^[.0-9]+$/);
      return "Third argument must be a valid time (e.g. 13:40)" if(!$numeric_val || $val < 0 || $val > 23);
      $val = $time[1];
      $numeric_val = ($val =~ m/^[.0-9]+$/);
      return "Third argument must be a valid time (e.g. 13:40)" if(!$numeric_val || $val < 0 || $val > 59);
      # Calculate on-day and time (unit: 10 min) for heater
      $val = sprintf("%02x%02x", $onday | 0x01, int(($time[0]*60 + $val) / 10));
      
      return "On- and off timepoints must not be identical" if(substr($val, 2, 2) eq substr($offval, 2, 2) && $onday == $offday);
    }
    # Calculate offsets for command and internal timer hash
    my $offset = int(($pos*2 + 1)/3)*7;
    my $keyoffset = $offset + ($a[1] =~ m/^hk1/ ? 0 : 15)*7;
    my $key = sprintf("01%02x", $keyoffset);
    # Are two updates needed (intervall is spread over two lines)?
    if (($pos + 1) % 3 == 0) {
      my $key2 = sprintf("01%02x", $keyoffset + 7);
      return "Internal timer-hash is not populated, use logmode command and try again later"
        if (!defined($km271_timer{$key}{0}) || !defined($km271_timer{$key}{1}) || !defined($km271_timer{$key2}{1}) || !defined($km271_timer{$key2}{2}));

      # Update internal hash
      $km271_timer{$key}{2} = $val;
      $km271_timer{$key2}{0} = $offval;
      $offval .= $km271_timer{$key2}{1} . $km271_timer{$key2}{2};
      # Dirty trick: Changes of the timer are not notified by the heater, so internal notification is added after the colon
      $offval = sprintf("%02x%s:%s%s", $offset + 7, $offval, $key2, $offval);
      # Push first command
      push @{$hash->{SENDBUFFER}}, sprintf($fmt, $offval);      
    } else {
      # Only one update needed
      if ($pos % 3 == 1) {
        return "Internal timer-hash is not populated, use logmode command and try again later" if (!defined($km271_timer{$key}{2}));
        # Update internal hash
        $km271_timer{$key}{0} = $val;
        $km271_timer{$key}{1} = $offval;
      } else {
        return "Internal timer-hash is not populated, use logmode command and try again later" if (!defined($km271_timer{$key}{0}));
        # Update internal hash
        $km271_timer{$key}{1} = $val;
        $km271_timer{$key}{2} = $offval;
      }
    }
    
    $val = $km271_timer{$key}{0} . $km271_timer{$key}{1} . $km271_timer{$key}{2};
    # Dirty trick: Changes of the timer are not notified by the heater, so internal notification is added after the colon
    $val = sprintf("%02x%s:%s%s", $offset, $val, $key, $val);
  }

  my $data = sprintf($fmt, $val, $val);
  push @{$hash->{SENDBUFFER}}, $data;
  if(!exists($hash->{WAITING}) && !exists($hash->{DATASENT})) {
    DevIo_DoSimpleRead($hash);
    DevIo_SimpleWrite($hash, "02", 1);
  }
  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 = DevIo_SimpleRead($hash);
  return "" if(!defined($buf));

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

  # Check, if we are waiting for a message from the heater
  if (!exists($hash->{WAITING})) {
    # Send data or waiting for acknowlegde
    if(@{$hash->{SENDBUFFER}} || $hash->{DATASENT}) {

      if($buf eq "10") {
        if($hash->{DATASENT}) {
          delete($hash->{DATASENT});
          delete($hash->{RETRYCOUNT});
          # Delete the command from the list
          shift @{$hash->{SENDBUFFER}};
          if ($hash->{NOTIFY}) {
            $data = $hash->{NOTIFY};
            delete($hash->{NOTIFY});
            goto INTERNAL_NOTIFY;                             # Timer changes are not reflected by the heater
          }
          DevIo_SimpleWrite($hash, "02", 1) if(@{$hash->{SENDBUFFER}});
        } else {
          # Delete the command only after receiving ACK
          $data = shift @{$hash->{SENDBUFFER}};
          unshift @{$hash->{SENDBUFFER}}, $data;
          # Dirty trick: separate notify message after the colon
          my @dataList = split(":", $data);
          $data = $dataList[0];
          $data = KM271_encode($data);
          $data .= "1003";
          $crc = KM271_crc($data);
          $data .= $crc;
          $hash->{DATASENT} = $data;
          $hash->{ERROR} = 0;
          $hash->{RETRYCOUNT} = 0;
          if (@dataList > 1) {
            # Set notify message
            $hash->{NOTIFY} = $dataList[1];
          } else {
            delete($hash->{NOTIFY});
          }
          DevIo_SimpleWrite($hash, $data, 1);  # Send the data
        }
      } else {
        if($hash->{DATASENT}) {
          my $newStart = 0;
          if ($buf eq "15") {
            Log 1, "$name: NAK!";            # NACK from the KM271
            if(++$hash->{ERROR} > 5) {
              $newStart = 1;
            } else {
              DevIo_SimpleWrite($hash, $hash->{DATASENT}, 1);
            }
          } else {
            Log 1, "$name: Bogus data after sending packet ($buf)";  # Strange response from the KM271
            $newStart = 1;
          }
          if ($newStart) {
            # Start all over again
            Log 1, "$name: Sending attempt for ($hash->{DATASENT}) failed!";
            if(++$hash->{RETRYCOUNT} > 3) {
              # Abort sending the actual command
              Log 1, "$name: Sending ($hash->{DATASENT}) not successful!";
              shift @{$hash->{SENDBUFFER}};
              delete($hash->{RETRYCOUNT});
            }
            delete($hash->{DATASENT});
            delete($hash->{NOTIFY});
            DevIo_SimpleWrite($hash, "02", 1) if(@{$hash->{SENDBUFFER}});
          }
        } else {
          DevIo_SimpleWrite($hash, "02", 1);
        }
      }
    } else {
      if ($buf eq "02") {                       # KM271 Wants to send
        DevIo_SimpleWrite($hash, "10", 1);      # We are ready
        $hash->{PARTIAL} = "";
        $hash->{WAITING} = time;
        $hash->{ERROR} = 0;
      }
    }
    return;
  } else {
    # After timeout get out of waiting mode
    delete($hash->{WAITING}) if(time - $hash->{WAITING} > 2.5);
  }
  
  $hash->{PARTIAL} .= $buf;
  return if($hash->{PARTIAL} !~ m/^(.*)1003(..)$/);
  ($data, $crc) = ($1, $2);
  $hash->{PARTIAL} = "";

  if(KM271_crc($data . "1003") ne $crc) {
    Log 1, "Wrong CRC in $name: $crc";
    DevIo_SimpleWrite($hash, "15", 1); # NAK
    if(++$hash->{ERROR} > 5) {
      delete($hash->{WAITING});
      DevIo_SimpleWrite($hash, "02", 1) if(@{$hash->{SENDBUFFER}}); # Want to send
    }
    return;
  }

  delete($hash->{WAITING});
  DevIo_SimpleWrite($hash, "10", 1);       # ACK, Data received ok
  $data = KM271_decode($data);
  
  INTERNAL_NOTIFY:
  DevIo_SimpleWrite($hash, "02", 1) if(@{$hash->{SENDBUFFER}}); # Want to send

  # Check for Error-Messages beginning with 'aa' first
  if($data =~ m/^(aa)(.*)/) {}
  elsif($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 = AttrVal($name, "all_km271_events", "") ;
  my $tn = TimeNow();

  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 > 127); }
        elsif($f eq "bf") { $val = KM271_setbits($val, $farg); }
        elsif($f eq "a")  { $val = $km271_arrays[$farg][$val]; }
        elsif($f eq "mb") {
          $val += ReadingsVal($name, $key."1", 0) * 256;
          $val += ReadingsVal($name, $key."2", 0) * 65536 if($farg == 3); }
        elsif($f eq "em") { $val = "Fehlercode $val " . (hex(substr($arg, ($off+1)*2, 2)) == 0 ? "aufgehoben" : "aufgetreten"); }
        elsif($f eq "t")  { $val = sprintf("%s | %s | %s", KM271_setprg($val, hex(substr($arg, ($off+1)*2, 2)))
                                                         , KM271_setprg(hex(substr($arg, ($off+2)*2, 2)), hex(substr($arg, ($off+3)*2, 2)))
                                                         , KM271_setprg(hex(substr($arg, ($off+4)*2, 2)), hex(substr($arg, ($off+5)*2, 2)))); 
                            # Fill internal timer hash
                            $km271_timer{$fn}{0} = substr($arg, 0, 4);
                            $km271_timer{$fn}{1} = substr($arg, 4, 4);
                            $km271_timer{$fn}{2} = substr($arg, 8, 4); }
        elsif($f eq "eh") { $val = KM271_seterror($arg); }
      }
      $key = ucfirst($key);   # Hack to match the original and the fake reading
      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);

  }
}

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

  return DevIo_OpenDev($hash, 1, undef)
                if($hash->{STATE} eq "disconnected");

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

#####################################
sub
KM271_DoInit($)
{
  my ($hash) = @_;
  push @{$hash->{SENDBUFFER}}, $km271_sets{"logmode"};
  DevIo_DoSimpleRead($hash);
  DevIo_SimpleWrite($hash, "02", 1);      # STX
  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_setprg($$)
{
  my ($val, $time) = @_;
  my $ret = "-";
  my $switch = $val & 0x0f;
  if ($switch < 2) {
    $ret = $switch == 0 ? "Aus: " : "An: ";
    $ret .= $km271_days{$val & 0xf0};
    $ret .= sprintf(" %02d:%02d", int($time / 6), ($time % 6)*10);
  }
  return $ret;
}

#####################################
sub
KM271_seterror($)
{
  my $val = shift;
  my $error = hex(substr($val, 0, 2));
  my $ret = "Kein Fehler";
  if ($error != 0) {
    $ret = sprintf("Code %02d (+): %02d:%02dUhr vor ", $error, hex(substr($val, 2, 2)), hex(substr($val, 4, 2)));
    $error = hex(substr($val, 8, 2));
    if ($error == 0xff) {
      $ret .= sprintf("%d Tagen | Fehler noch offen", hex(substr($val, 6, 2)));
    } else {
      my $days = hex(substr($val, 12, 2));
      $ret .= sprintf("%d Tagen | (-): %02d:%02dUhr vor %d Tagen", hex(substr($val, 6, 2)) + $days, $error, hex(substr($val, 10, 2)), $days);
    }
  }
  return $ret;
}

#####################################
# Replacement for regular expression - s/10/1010/g - which works wrong for "0101"
sub
KM271_encode($)
{
  my $in = shift;
  my $out = '';
  foreach my $a (split("", pack('H*', $in))) {
    my $c = sprintf("%02x", ord($a));
    $c =~ s/10/1010/g;
    $out .= $c;
  }
  return $out;
}

#####################################
# Replacement for regular expression - s/1010/10/g - which works wrong for "010101"
sub
KM271_decode($)
{
  my $in = shift;
  my $out = '';
  my $flag = 0;
  foreach my $a (split("", pack('H*', $in))) {
    my $c = sprintf("%02x", ord($a));
    if ($c eq "10") {
      if ($flag) {
        $flag = 0;
        $c = '';
      } else {
        $flag = 1;
      }
    } else {
      $flag = 0;
    }
    $out .= $c;
  }
  return $out;
}

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

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

1;
