mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-03-07 01:06:53 +00:00
817 lines
29 KiB
Perl
Executable File
817 lines
29 KiB
Perl
Executable File
##############################################
|
|
# 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", # great part 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,ne", # great part of all messages
|
|
"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", # great part 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,ne", # great part of all messages
|
|
"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", # great part of all messages
|
|
"Brenner_Einschalttemperatur" => "882c:0",
|
|
"Brenner_Ausschalttemperatur" => "882d:0",
|
|
"Kessel_Integral1" => "882e:0,ne",
|
|
"Kessel_Integral" => "882f:0,mb:2,ne", # great part 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;
|