10_MAX.pm 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. ##############################################
  2. # $Id: 10_MAX.pm 16847 2018-06-10 18:42:19Z rudolfkoenig $
  3. # Written by Matthias Gehre, M.Gehre@gmx.de, 2012-2013
  4. #
  5. package main;
  6. use strict;
  7. use warnings;
  8. use MIME::Base64;
  9. use MaxCommon;
  10. sub MAX_Define($$);
  11. sub MAX_Undef($$);
  12. sub MAX_Initialize($);
  13. sub MAX_Parse($$);
  14. sub MAX_Set($@);
  15. sub MAX_MD15Cmd($$$);
  16. sub MAX_DateTime2Internal($);
  17. sub MAX_DbLog_splitFn($);
  18. my @ctrl_modes = ( "auto", "manual", "temporary", "boost" );
  19. my %boost_durations = (0 => 0, 1 => 5, 2 => 10, 3 => 15, 4 => 20, 5 => 25, 6 => 30, 7 => 60);
  20. my %boost_durationsInv = reverse %boost_durations;
  21. my %decalcDays = (0 => "Sat", 1 => "Sun", 2 => "Mon", 3 => "Tue", 4 => "Wed", 5 => "Thu", 6 => "Fri");
  22. my @weekDays = ("Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri");
  23. my %decalcDaysInv = reverse %decalcDays;
  24. sub validWindowOpenDuration { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 60; }
  25. sub validMeasurementOffset { return $_[0] =~ /^-?\d+(\.[05])?$/ && $_[0] >= -3.5 && $_[0] <= 3.5; }
  26. sub validBoostDuration { return $_[0] =~ /^\d+$/ && exists($boost_durationsInv{$_[0]}); }
  27. sub validValveposition { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 100; }
  28. sub validDecalcification { my ($decalcDay, $decalcHour) = ($_[0] =~ /^(...) (\d{1,2}):00$/);
  29. return defined($decalcDay) && defined($decalcHour) && exists($decalcDaysInv{$decalcDay}) && 0 <= $decalcHour && $decalcHour < 24; }
  30. sub validWeekProfile { return length($_[0]) == 4*13*7; }
  31. sub validGroupid { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 255; }
  32. my %readingDef = ( #min/max/default
  33. "maximumTemperature" => [ \&validTemperature, "on"],
  34. "minimumTemperature" => [ \&validTemperature, "off"],
  35. "comfortTemperature" => [ \&validTemperature, 21],
  36. "ecoTemperature" => [ \&validTemperature, 17],
  37. "windowOpenTemperature" => [ \&validTemperature, 12],
  38. "windowOpenDuration" => [ \&validWindowOpenDuration, 15],
  39. "measurementOffset" => [ \&validMeasurementOffset, 0],
  40. "boostDuration" => [ \&validBoostDuration, 5 ],
  41. "boostValveposition" => [ \&validValveposition, 80 ],
  42. "decalcification" => [ \&validDecalcification, "Sat 12:00" ],
  43. "maxValveSetting" => [ \&validValveposition, 100 ],
  44. "valveOffset" => [ \&validValveposition, 00 ],
  45. "groupid" => [ \&validGroupid, 0 ],
  46. ".weekProfile" => [ \&validWeekProfile, $defaultWeekProfile ],
  47. );
  48. my %interfaces = (
  49. "Cube" => undef,
  50. "HeatingThermostat" => "thermostat;battery;temperature",
  51. "HeatingThermostatPlus" => "thermostat;battery;temperature",
  52. "WallMountedThermostat" => "thermostat;temperature;battery",
  53. "ShutterContact" => "switch_active;battery",
  54. "PushButton" => "switch_passive;battery"
  55. );
  56. sub
  57. MAX_Initialize($)
  58. {
  59. my ($hash) = @_;
  60. Log3 $hash, 5, "Calling MAX_Initialize";
  61. $hash->{Match} = "^MAX";
  62. $hash->{DefFn} = "MAX_Define";
  63. $hash->{UndefFn} = "MAX_Undef";
  64. $hash->{ParseFn} = "MAX_Parse";
  65. $hash->{SetFn} = "MAX_Set";
  66. $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:0,1 dummy:0,1 " .
  67. "showtime:1,0 keepAuto:0,1 scanTemp:0,1 ".
  68. $readingFnAttributes;
  69. $hash->{DbLog_splitFn} = "MAX_DbLog_splitFn";
  70. return undef;
  71. }
  72. #############################
  73. sub
  74. MAX_Define($$)
  75. {
  76. my ($hash, $def) = @_;
  77. my @a = split("[ \t][ \t]*", $def);
  78. my $name = $hash->{NAME};
  79. return "name \"$name\" is reserved for internal use" if($name eq "fakeWallThermostat" or $name eq "fakeShutterContact");
  80. return "wrong syntax: define <name> MAX type addr"
  81. if(int(@a)!=4 || $a[3] !~ m/^[A-F0-9]{6}$/i);
  82. my $type = $a[2];
  83. my $addr = lc($a[3]); #all addr should be lowercase
  84. if(exists($modules{MAX}{defptr}{$addr})) {
  85. my $msg = "MAX_Define: Device with addr $addr is already defined";
  86. Log3 $hash, 1, $msg;
  87. return $msg;
  88. }
  89. if($type eq "Cube") {
  90. my $msg = "MAX_Define: Device type 'Cube' is deprecated. All properties have been moved to the MAXLAN device.";
  91. Log3 $hash, 1, $msg;
  92. return $msg;
  93. }
  94. Log3 $hash, 5, "Max_define $type with addr $addr ";
  95. $hash->{type} = $type;
  96. $hash->{addr} = $addr;
  97. $modules{MAX}{defptr}{$addr} = $hash;
  98. $hash->{internals}{interfaces} = $interfaces{$type};
  99. AssignIoPort($hash);
  100. return undef;
  101. }
  102. sub
  103. MAX_Undef($$)
  104. {
  105. my ($hash,$name) = @_;
  106. delete($modules{MAX}{defptr}{$hash->{addr}});
  107. return undef;
  108. }
  109. sub
  110. MAX_DateTime2Internal($)
  111. {
  112. my($day, $month, $year, $hour, $min) = ($_[0] =~ /^(\d{2}).(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/);
  113. return (($month&0xE) << 20) | ($day << 16) | (($month&1) << 15) | (($year-2000) << 8) | ($hour*2 + int($min/30));
  114. }
  115. sub
  116. MAX_TypeToTypeId($)
  117. {
  118. foreach (keys %device_types) {
  119. return $_ if($_[0] eq $device_types{$_});
  120. }
  121. Log 1, "MAX_TypeToTypeId: Invalid type $_[0]";
  122. return 0;
  123. }
  124. sub
  125. MAX_CheckIODev($)
  126. {
  127. my $hash = shift;
  128. return !defined($hash->{IODev}) || ($hash->{IODev}{TYPE} ne "MAXLAN" && $hash->{IODev}{TYPE} ne "CUL_MAX");
  129. }
  130. # Print number in format "0.0", pass "on" and "off" verbatim, convert 30.5 and 4.5 to "on" and "off"
  131. # Used for "desiredTemperature", "ecoTemperature" etc. but not "temperature"
  132. sub
  133. MAX_SerializeTemperature($)
  134. {
  135. if($_[0] eq "on" or $_[0] eq "off") {
  136. return $_[0];
  137. } elsif($_[0] == 4.5) {
  138. return "off";
  139. } elsif($_[0] == 30.5) {
  140. return "on";
  141. } else {
  142. return sprintf("%2.1f",$_[0]);
  143. }
  144. }
  145. sub
  146. MAX_Validate(@)
  147. {
  148. my ($name,$val) = @_;
  149. return 1 if(!exists($readingDef{$name}));
  150. return $readingDef{$name}[0]->($val);
  151. }
  152. #Get a reading, validating it's current value (maybe forcing to the default if invalid)
  153. #"on" and "off" are converted to their numeric values
  154. sub
  155. MAX_ReadingsVal(@)
  156. {
  157. my ($hash,$name) = @_;
  158. my $val = ReadingsVal($hash->{NAME},$name,"");
  159. #$readingDef{$name} array is [validatingFunc, defaultValue]
  160. if(exists($readingDef{$name}) and !$readingDef{$name}[0]->($val)) {
  161. #Error: invalid value
  162. Log3 $hash, 2, "MAX: Invalid value $val for READING $name on $hash->{NAME}. Forcing to $readingDef{$name}[1]";
  163. $val = $readingDef{$name}[1];
  164. #Save default value to READINGS
  165. if(exists($hash->{".updateTimestamp"})) {
  166. readingsBulkUpdate($hash,$name,$val);
  167. } else {
  168. readingsSingleUpdate($hash,$name,$val,0);
  169. }
  170. }
  171. return MAX_ParseTemperature($val);
  172. }
  173. sub
  174. MAX_ParseWeekProfile(@) {
  175. my ($hash ) = @_;
  176. # Format of weekprofile: 16 bit integer (high byte first) for every control point, 13 control points for every day
  177. # each 16 bit integer value is parsed as
  178. # int time = (value & 0x1FF) * 5;
  179. # int hour = (time / 60) % 24;
  180. # int minute = time % 60;
  181. # int temperature = ((value >> 9) & 0x3F) / 2;
  182. my $curWeekProfile = MAX_ReadingsVal($hash, ".weekProfile");
  183. #parse weekprofiles for each day
  184. for (my $i=0;$i<7;$i++) {
  185. my (@time_prof, @temp_prof);
  186. for(my $j=0;$j<13;$j++) {
  187. $time_prof[$j] = (hex(substr($curWeekProfile,($i*52)+ 4*$j,4))& 0x1FF) * 5;
  188. $temp_prof[$j] = (hex(substr($curWeekProfile,($i*52)+ 4*$j,4))>> 9 & 0x3F ) / 2;
  189. }
  190. my @hours;
  191. my @minutes;
  192. my $j;
  193. for($j=0;$j<13;$j++) {
  194. $hours[$j] = ($time_prof[$j] / 60 % 24);
  195. $minutes[$j] = ($time_prof[$j]%60);
  196. #if 00:00 reached, last point in profile was found
  197. last if(int($hours[$j])==0 && int($minutes[$j])==0 );
  198. }
  199. my $time_prof_str = "00:00";
  200. my $temp_prof_str;
  201. for (my $k=0;$k<=$j;$k++) {
  202. $time_prof_str .= sprintf("-%02d:%02d", $hours[$k], $minutes[$k]);
  203. $temp_prof_str .= sprintf("%2.1f °C",$temp_prof[$k]);
  204. if ($k < $j) {
  205. $time_prof_str .= " / " . sprintf("%02d:%02d", $hours[$k], $minutes[$k]);
  206. $temp_prof_str .= " / ";
  207. }
  208. }
  209. readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-time", $time_prof_str );
  210. readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-temp", $temp_prof_str );
  211. }
  212. }
  213. #############################
  214. sub
  215. MAX_WakeUp($)
  216. {
  217. my $hash = $_[0];
  218. #3F corresponds to 31 seconds wakeup (so its probably the lower 5 bits)
  219. return ($hash->{IODev}{Send})->($hash->{IODev},"WakeUp",$hash->{addr}, "3F", callbackParam => "31" );
  220. }
  221. sub
  222. MAX_Set($@)
  223. {
  224. my ($hash, $devname, @a) = @_;
  225. my ($setting, @args) = @a;
  226. return "Invalid IODev" if(MAX_CheckIODev($hash));
  227. if($setting eq "desiredTemperature" and $hash->{type} =~ /.*Thermostat.*/) {
  228. return "missing a value" if(@args == 0);
  229. my $temperature;
  230. my $until = undef;
  231. my $ctrlmode = 1; #0=auto, 1=manual; 2=temporary
  232. if($args[0] eq "auto") {
  233. #This enables the automatic/schedule mode where the thermostat follows the weekly program
  234. #There can be a temperature supplied, which will be kept until the next switch point of the weekly program
  235. if(@args == 2) {
  236. if($args[1] eq "eco") {
  237. $temperature = MAX_ReadingsVal($hash,"ecoTemperature");
  238. } elsif($args[1] eq "comfort") {
  239. $temperature = MAX_ReadingsVal($hash,"comfortTemperature");
  240. } else {
  241. $temperature = MAX_ParseTemperature($args[1]);
  242. }
  243. } elsif(@args == 1) {
  244. $temperature = 0; #use temperature from weekly program
  245. } else {
  246. return "Too many parameters: desiredTemperature auto [<temperature>]";
  247. }
  248. $ctrlmode = 0; #auto
  249. } elsif($args[0] eq "boost") {
  250. return "Too many parameters: desiredTemperature boost" if(@args > 1);
  251. $temperature = 0;
  252. $ctrlmode = 3;
  253. #TODO: auto mode with temperature is also possible
  254. } else {
  255. if($args[0] eq "manual") {
  256. #User explicitly asked for manual mode
  257. $ctrlmode = 1; #manual, possibly overwriting keepAuto
  258. shift @args;
  259. return "Not enough parameters after 'desiredTemperature manual'" if(@args == 0);
  260. } elsif(AttrVal($hash->{NAME},"keepAuto","0") ne "0"
  261. && MAX_ReadingsVal($hash,"mode") eq "auto") {
  262. #User did not ask for any mode explicitly, but has keepAuto
  263. Log3 $hash, 5, "MAX_Set: staying in auto mode";
  264. $ctrlmode = 0; #auto
  265. }
  266. if($args[0] eq "eco") {
  267. $temperature = MAX_ReadingsVal($hash,"ecoTemperature");
  268. } elsif($args[0] eq "comfort") {
  269. $temperature = MAX_ReadingsVal($hash,"comfortTemperature");
  270. } else {
  271. $temperature = MAX_ParseTemperature($args[0]);
  272. }
  273. if(@args > 1) {
  274. #@args == 3 and $args[1] == "until"
  275. return "Second parameter must be 'until'" if($args[1] ne "until");
  276. return "Not enough parameters: desiredTemperature [manual] <temp> [until <date> <time>]" if(@args == 3);
  277. return "Too many parameters: desiredTemperature [manual] <temp> [until <date> <time>]" if(@args > 4);
  278. $ctrlmode = 2; #switch manual to temporary
  279. $until = sprintf("%06x",MAX_DateTime2Internal($args[2]." ".$args[3]));
  280. }
  281. }
  282. my $payload = sprintf("%02x",int($temperature*2.0) | ($ctrlmode << 6));
  283. $payload .= $until if(defined($until));
  284. my $groupid = MAX_ReadingsVal($hash,"groupid");
  285. return ($hash->{IODev}{Send})->($hash->{IODev},"SetTemperature",$hash->{addr},$payload, groupId => sprintf("%02x",$groupid), flags => ( $groupid ? "04" : "00" ));
  286. }elsif(grep (/^\Q$setting\E$/, ("boostDuration", "boostValveposition", "decalcification","maxValveSetting","valveOffset"))
  287. and $hash->{type} =~ /.*Thermostat.*/){
  288. my $val = join(" ",@args); #decalcification contains a space
  289. if(!MAX_Validate($setting, $val)) {
  290. my $msg = "Invalid value $args[0] for $setting";
  291. Log3 $hash, 1, $msg;
  292. return $msg;
  293. }
  294. my %h;
  295. $h{boostDuration} = MAX_ReadingsVal($hash,"boostDuration");
  296. $h{boostValveposition} = MAX_ReadingsVal($hash,"boostValveposition");
  297. $h{decalcification} = MAX_ReadingsVal($hash,"decalcification");
  298. $h{maxValveSetting} = MAX_ReadingsVal($hash,"maxValveSetting");
  299. $h{valveOffset} = MAX_ReadingsVal($hash,"valveOffset");
  300. $h{$setting} = MAX_ParseTemperature($val);
  301. my ($decalcDay, $decalcHour) = ($h{decalcification} =~ /^(...) (\d{1,2}):00$/);
  302. my $decalc = ($decalcDaysInv{$decalcDay} << 5) | $decalcHour;
  303. my $boost = ($boost_durationsInv{$h{boostDuration}} << 5) | int($h{boostValveposition}/5);
  304. my $payload = sprintf("%02x%02x%02x%02x", $boost, $decalc, int($h{maxValveSetting}*255/100), int($h{valveOffset}*255/100));
  305. return ($hash->{IODev}{Send})->($hash->{IODev},"ConfigValve",$hash->{addr},$payload,callbackParam => "$setting,$val");
  306. }elsif($setting eq "groupid"){
  307. return "argument needed" if(@args == 0);
  308. if($args[0]) {
  309. return ($hash->{IODev}{Send})->($hash->{IODev},"SetGroupId",$hash->{addr}, sprintf("%02x",$args[0]), callbackParam => "$args[0]" );
  310. } else {
  311. return ($hash->{IODev}{Send})->($hash->{IODev},"RemoveGroupId",$hash->{addr}, "00", callbackParam => "0");
  312. }
  313. }elsif( grep (/^\Q$setting\E$/, ("ecoTemperature", "comfortTemperature", "measurementOffset", "maximumTemperature", "minimumTemperature", "windowOpenTemperature", "windowOpenDuration" )) and $hash->{type} =~ /.*Thermostat.*/) {
  314. return "Cannot set without IODev" if(!exists($hash->{IODev}));
  315. if(!MAX_Validate($setting, $args[0])) {
  316. my $msg = "Invalid value $args[0] for $setting";
  317. Log3 $hash, 1, $msg;
  318. return $msg;
  319. }
  320. my %h;
  321. $h{comfortTemperature} = MAX_ReadingsVal($hash,"comfortTemperature");
  322. $h{ecoTemperature} = MAX_ReadingsVal($hash,"ecoTemperature");
  323. $h{maximumTemperature} = MAX_ReadingsVal($hash,"maximumTemperature");
  324. $h{minimumTemperature} = MAX_ReadingsVal($hash,"minimumTemperature");
  325. $h{windowOpenTemperature} = MAX_ReadingsVal($hash,"windowOpenTemperature");
  326. $h{windowOpenDuration} = MAX_ReadingsVal($hash,"windowOpenDuration");
  327. $h{measurementOffset} = MAX_ReadingsVal($hash,"measurementOffset");
  328. $h{$setting} = MAX_ParseTemperature($args[0]);
  329. my $comfort = int($h{comfortTemperature}*2);
  330. my $eco = int($h{ecoTemperature}*2);
  331. my $max = int($h{maximumTemperature}*2);
  332. my $min = int($h{minimumTemperature}*2);
  333. my $offset = int(($h{measurementOffset} + 3.5)*2);
  334. my $windowOpenTemp = int($h{windowOpenTemperature}*2);
  335. my $windowOpenTime = int($h{windowOpenDuration}/5);
  336. my $groupid = MAX_ReadingsVal($hash,"groupid");
  337. my $payload = sprintf("%02x%02x%02x%02x%02x%02x%02x",$comfort,$eco,$max,$min,$offset,$windowOpenTemp,$windowOpenTime);
  338. if($setting eq "measurementOffset") {
  339. return ($hash->{IODev}{Send})->($hash->{IODev},"ConfigTemperatures",$hash->{addr},$payload, groupId => "00", flags => "00", callbackParam => "$setting,$args[0]");
  340. } else {
  341. return ($hash->{IODev}{Send})->($hash->{IODev},"ConfigTemperatures",$hash->{addr},$payload, groupId => sprintf("%02x",$groupid), flags => ( $groupid ? "04" : "00" ), callbackParam => "$setting,$args[0]");
  342. }
  343. } elsif($setting eq "displayActualTemperature" and $hash->{type} eq "WallMountedThermostat") {
  344. return "Invalid arg" if($args[0] ne "0" and $args[0] ne "1");
  345. return ($hash->{IODev}{Send})->($hash->{IODev},"SetDisplayActualTemperature",$hash->{addr},
  346. sprintf("%02x",$args[0] ? 4 : 0), callbackParam => "$setting,$args[0]");
  347. } elsif($setting eq "fake") { #Deprecated, use fakeWT and fakeSC of CUL_MAX
  348. #Resolve first argument to address
  349. return "Invalid number of arguments" if(@args == 0);
  350. my $dest = $args[0];
  351. if(exists($defs{$dest})) {
  352. return "Destination is not a MAX device" if($defs{$dest}{TYPE} ne "MAX");
  353. $dest = $defs{$dest}{addr};
  354. } else {
  355. return "No MAX device with address $dest" if(!exists($modules{MAX}{defptr}{$dest}));
  356. }
  357. if($hash->{type} eq "ShutterContact") {
  358. return "Invalid number of arguments" if(@args != 2);
  359. Log3 $hash, 2, "fake is deprectaed and will be removed. Please use CUL_MAX's fakeSC";
  360. my $state = $args[1] ? "12" : "10";
  361. return ($hash->{IODev}{Send})->($hash->{IODev},"ShutterContactState",$dest,$state, flags => "06", src => $hash->{addr});
  362. } elsif($hash->{type} eq "WallMountedThermostat") {
  363. return "Invalid number of arguments" if(@args != 3);
  364. return "desiredTemperature is invalid" if($args[1] < 4.5 || $args[2] > 30.5);
  365. Log3 $hash, 2, "fake is deprectaed and will be removed. Please use CUL_MAX's fakeWT";
  366. $args[2] = 0 if($args[2] < 0); #Clamp temperature to minimum of 0 degree
  367. #Encode into binary form
  368. my $arg2 = int(10*$args[2]);
  369. #First bit is 9th bit of temperature, rest is desiredTemperature
  370. my $arg1 = (($arg2&0x100)>>1) | (int(2*$args[1])&0x7F);
  371. $arg2 &= 0xFF; #only take the lower 8 bits
  372. return ($hash->{IODev}{Send})->($hash->{IODev},"WallThermostatControl",$dest,
  373. sprintf("%02x%02x",$arg1,$arg2),flags => "04", src => $hash->{addr});
  374. } else {
  375. return "fake does not work for device type $hash->{type}";
  376. }
  377. } elsif(grep /^\Q$setting\E$/, ("associate", "deassociate")) {
  378. my $dest = $args[0];
  379. my $destType;
  380. if($dest eq "fakeWallThermostat") {
  381. return "IODev is not CUL_MAX" if($hash->{IODev}->{TYPE} ne "CUL_MAX");
  382. $dest = AttrVal($hash->{IODev}->{NAME}, "fakeWTaddr", "111111");
  383. return "Invalid fakeWTaddr attribute set (must not be 000000)" if($dest eq "000000");
  384. $destType = MAX_TypeToTypeId("WallMountedThermostat");
  385. } elsif($dest eq "fakeShutterContact") {
  386. return "IODev is not CUL_MAX" if($hash->{IODev}->{TYPE} ne "CUL_MAX");
  387. $dest = AttrVal($hash->{IODev}->{NAME}, "fakeSCaddr", "222222");
  388. return "Invalid fakeSCaddr attribute set (must not be 000000)" if($dest eq "000000");
  389. $destType = MAX_TypeToTypeId("ShutterContact");
  390. } else {
  391. if(exists($defs{$dest})) {
  392. return "Destination is not a MAX device" if($defs{$dest}{TYPE} ne "MAX");
  393. $dest = $defs{$dest}{addr};
  394. } else {
  395. return "No MAX device with address $dest" if(!exists($modules{MAX}{defptr}{$dest}));
  396. }
  397. $destType = MAX_TypeToTypeId($modules{MAX}{defptr}{$dest}{type});
  398. }
  399. Log3 $hash, 5, "Using dest $dest, destType $destType";
  400. if($setting eq "associate") {
  401. return ($hash->{IODev}{Send})->($hash->{IODev},"AddLinkPartner",$hash->{addr},sprintf("%s%02x", $dest, $destType));
  402. } else {
  403. return ($hash->{IODev}{Send})->($hash->{IODev},"RemoveLinkPartner",$hash->{addr},sprintf("%s%02x", $dest, $destType));
  404. }
  405. } elsif($setting eq "factoryReset") {
  406. if(exists($hash->{IODev}{RemoveDevice})) {
  407. #MAXLAN
  408. return ($hash->{IODev}{RemoveDevice})->($hash->{IODev},$hash->{addr});
  409. } else {
  410. #CUL_MAX
  411. return ($hash->{IODev}{Send})->($hash->{IODev},"Reset",$hash->{addr});
  412. }
  413. } elsif($setting eq "wakeUp") {
  414. return MAX_WakeUp($hash);
  415. } elsif($setting eq "weekProfile" and $hash->{type} =~ /.*Thermostat.*/) {
  416. return "Invalid arguments. You must specify at least one: <weekDay> <temp[,hh:mm]>\nExample: Mon 10,06:00,17,09:00" if((@args%2 == 1)||(@args == 0));
  417. #Send wakeUp, so we can send the weekprofile pakets without preamble
  418. #Disabled for now. Seems like the first packet is lost. Maybe inserting a delay after the wakeup will fix this
  419. #MAX_WakeUp($hash) if( @args > 2 );
  420. for(my $i = 0; $i < @args; $i += 2) {
  421. return "Expected day (one of ".join (",",@weekDays)."), got $args[$i]" if(!exists($decalcDaysInv{$args[$i]}));
  422. my $day = $decalcDaysInv{$args[$i]};
  423. my @controlpoints = split(',',$args[$i+1]);
  424. return "Not more than 13 control points are allowed!" if(@controlpoints > 13*2);
  425. my $newWeekprofilePart = "";
  426. for(my $j = 0; $j < 13*2; $j += 2) {
  427. if( $j >= @controlpoints ) {
  428. $newWeekprofilePart .= "4520";
  429. next;
  430. }
  431. my ($hour, $min);
  432. if($j + 1 == @controlpoints) {
  433. $hour = 0; $min = 0;
  434. } else {
  435. ($hour, $min) = ($controlpoints[$j+1] =~ /^(\d{1,2}):(\d{1,2})$/);
  436. }
  437. my $temperature = $controlpoints[$j];
  438. return "Invalid time: $controlpoints[$j+1]" if(!defined($hour) || !defined($min) || $hour > 23 || $min > 59);
  439. return "Invalid temperature (Must be one of: off|on|5|5.5|6|6.5..30)" if(!validTemperature($temperature));
  440. $temperature = MAX_ParseTemperature($temperature); #replace "on" and "off" by their values
  441. $newWeekprofilePart .= sprintf("%04x", (int($temperature*2) << 9) | int(($hour * 60 + $min)/5));
  442. }
  443. Log3 $hash, 5, "New Temperature part for $day: $newWeekprofilePart";
  444. #Each day has 2 bytes * 13 controlpoints = 26 bytes = 52 hex characters
  445. #we don't have to update the rest, because the active part is terminated by the time 0:00
  446. #First 7 controlpoints (2*7=14 bytes => 2*2*7=28 hex characters )
  447. ($hash->{IODev}{Send})->($hash->{IODev},"ConfigWeekProfile",$hash->{addr},
  448. sprintf("0%1d%s", $day, substr($newWeekprofilePart,0,2*2*7)),
  449. callbackParam => "$day,0,".substr($newWeekprofilePart,0,2*2*7));
  450. #And then the remaining 6
  451. ($hash->{IODev}{Send})->($hash->{IODev},"ConfigWeekProfile",$hash->{addr},
  452. sprintf("1%1d%s", $day, substr($newWeekprofilePart,2*2*7,2*2*6)),
  453. callbackParam => "$day,1,".substr($newWeekprofilePart,2*2*7,2*2*6))
  454. if(@controlpoints > 2*7);
  455. }
  456. Log3 $hash, 5, "New weekProfile: " . MAX_ReadingsVal($hash, ".weekProfile");
  457. }else{
  458. my $templist = join(",",map { MAX_SerializeTemperature($_/2) } (9..61));
  459. my $ret = "Unknown argument $setting, choose one of wakeUp factoryReset groupid";
  460. my $assoclist;
  461. #Build list of devices which this device can be associated to
  462. if($hash->{type} =~ /HeatingThermostat.*/) {
  463. $assoclist = join(",", map { defined($_->{type}) &&
  464. ($_->{type} eq "HeatingThermostat"
  465. || $_->{type} eq "HeatingThermostatPlus"
  466. || $_->{type} eq "WallMountedThermostat"
  467. || $_->{type} eq "ShutterContact")
  468. && $_ != $hash ? $_->{NAME} : () } values %{$modules{MAX}{defptr}});
  469. if($hash->{IODev}->{TYPE} eq "CUL_MAX") {
  470. $assoclist .= "," if(length($assoclist));
  471. $assoclist .= "fakeWallThermostat,fakeShutterContact";
  472. }
  473. } elsif($hash->{type} =~ /WallMountedThermostat/) {
  474. $assoclist = join(",", map { defined($_->{type}) &&
  475. ($_->{type} eq "HeatingThermostat"
  476. || $_->{type} eq "HeatingThermostatPlus"
  477. || $_->{type} eq "ShutterContact")
  478. && $_ != $hash ? $_->{NAME} : () } values %{$modules{MAX}{defptr}});
  479. if($hash->{IODev}->{TYPE} eq "CUL_MAX") {
  480. $assoclist .= "," if(length($assoclist));
  481. $assoclist .= "fakeShutterContact";
  482. }
  483. } elsif($hash->{type} eq "ShutterContact") {
  484. $assoclist = join(",", map { defined($_->{type}) && $_->{type} =~ /.*Thermostat.*/ ? $_->{NAME} : () } values %{$modules{MAX}{defptr}});
  485. }
  486. my $templistOffset = join(",",map { MAX_SerializeTemperature(($_-7)/2) } (0..14));
  487. my $boostDurVal = join(",", values(%boost_durations));
  488. if($hash->{type} =~ /HeatingThermostat.*/) {
  489. my $shash;
  490. my $wallthermo = 0;
  491. # check if Wallthermo is in same group
  492. foreach my $addr ( keys %{$modules{MAX}{defptr}} ) {
  493. $shash = $modules{MAX}{defptr}{$addr};
  494. $wallthermo = 1 if(defined $shash->{type} && $shash->{type} eq "WallMountedThermostat" && (MAX_ReadingsVal($shash,"groupid") eq MAX_ReadingsVal($hash,"groupid")));
  495. }
  496. if ($wallthermo eq 1) {
  497. return "$ret associate:$assoclist deassociate:$assoclist desiredTemperature:eco,comfort,boost,auto,$templist measurementOffset:$templistOffset windowOpenDuration boostDuration:$boostDurVal boostValveposition decalcification maxValveSetting valveOffset weekProfile";
  498. } else {
  499. return "$ret associate:$assoclist deassociate:$assoclist desiredTemperature:eco,comfort,boost,auto,$templist ecoTemperature:$templist comfortTemperature:$templist measurementOffset:$templistOffset maximumTemperature:$templist minimumTemperature:$templist windowOpenTemperature:$templist windowOpenDuration boostDuration:$boostDurVal boostValveposition decalcification maxValveSetting valveOffset weekProfile";
  500. }
  501. } elsif($hash->{type} eq "WallMountedThermostat") {
  502. return "$ret associate:$assoclist deassociate:$assoclist displayActualTemperature:0,1 desiredTemperature:eco,comfort,boost,auto,$templist ecoTemperature:$templist comfortTemperature:$templist maximumTemperature:$templist minimumTemperature:$templist measurementOffset:$templistOffset windowOpenTemperature:$templist boostDuration:$boostDurVal boostValveposition ";
  503. } elsif($hash->{type} eq "ShutterContact") {
  504. return "$ret associate:$assoclist deassociate:$assoclist";
  505. } else {
  506. return $ret;
  507. }
  508. }
  509. }
  510. #############################
  511. sub
  512. MAX_ParseDateTime($$$)
  513. {
  514. my ($byte1,$byte2,$byte3) = @_;
  515. my $day = $byte1 & 0x1F;
  516. my $month = (($byte1 & 0xE0) >> 4) | ($byte2 >> 7);
  517. my $year = $byte2 & 0x3F;
  518. my $time = ($byte3 & 0x3F);
  519. if($time%2){
  520. $time = int($time/2).":30";
  521. }else{
  522. $time = int($time/2).":00";
  523. }
  524. return { "day" => $day, "month" => $month, "year" => $year, "time" => $time, "str" => "$day.$month.$year $time" };
  525. }
  526. #############################
  527. sub
  528. MAX_Parse($$)
  529. {
  530. my ($hash, $msg) = @_;
  531. my ($MAX,$isToMe,$msgtype,$addr,@args) = split(",",$msg);
  532. #$isToMe is 1 if the message was direct at the device $hash, and 0
  533. #if we just snooped a message directed at a different device (by CUL_MAX).
  534. return () if($MAX ne "MAX");
  535. Log3 $hash, 5, "MAX_Parse $msg";
  536. if(!exists($modules{MAX}{defptr}{$addr}))
  537. {
  538. my $devicetype = undef;
  539. $devicetype = $args[0] if($msgtype eq "define" and $args[0] ne "Cube");
  540. $devicetype = "ShutterContact" if($msgtype eq "ShutterContactState");
  541. $devicetype = "WallMountedThermostat" if(grep /^$msgtype$/, ("WallThermostatConfig","WallThermostatState","WallThermostatControl","SetTemperature"));
  542. $devicetype = "HeatingThermostat" if(grep /^$msgtype$/, ("HeatingThermostatConfig", "ThermostatState"));
  543. if($devicetype) {
  544. return "UNDEFINED MAX_$addr MAX $devicetype $addr";
  545. } else {
  546. Log3 $hash, 2, "Got message for undefined device $addr, and failed to guess type from msg '$msgtype' - ignoring";
  547. return $hash->{NAME};
  548. }
  549. }
  550. my $shash = $modules{MAX}{defptr}{$addr};
  551. #if $isToMe is true, then the message was directed at device $hash, thus we can also use it for sending
  552. if($isToMe) {
  553. $shash->{IODev} = $hash;
  554. $shash->{backend} = $hash->{NAME}; #for user information
  555. }
  556. readingsBeginUpdate($shash);
  557. if($msgtype eq "define"){
  558. my $devicetype = $args[0];
  559. Log3 $hash, 1, "Device changed type from $shash->{type} to $devicetype" if($shash->{type} ne $devicetype);
  560. $shash->{type} = $devicetype;
  561. if(@args > 1){
  562. my $serial = $args[1];
  563. Log3 $hash, 1, "Device changed serial from $shash->{serial} to $serial" if($shash->{serial} and ($shash->{serial} ne $serial));
  564. $shash->{serial} = $serial;
  565. }
  566. readingsBulkUpdate($shash, "groupid", $args[2]);
  567. $shash->{IODev} = $hash;
  568. } elsif($msgtype eq "ThermostatState") {
  569. my ($bits2,$valveposition,$desiredTemperature,$until1,$until2,$until3) = unpack("aCCCCC",pack("H*",$args[0]));
  570. my $mode = vec($bits2, 0, 2); #
  571. my $dstsetting = vec($bits2, 3, 1); #is automatically switching to DST activated
  572. my $langateway = vec($bits2, 4, 1); #??
  573. my $panel = vec($bits2, 5, 1); #1 if the heating thermostat is locked for manually setting the temperature at the device
  574. my $rferror = vec($bits2, 6, 1); #communication with link partner (what does that mean?)
  575. my $batterylow = vec($bits2, 7, 1); #1 if battery is low
  576. my $untilStr = defined($until3) ? MAX_ParseDateTime($until1,$until2,$until3)->{str} : "";
  577. my $measuredTemperature = defined($until2) ? ((($until1 &0x01)<<8) + $until2)/10 : 0;
  578. #If the control mode is not "temporary", the cube sends the current (measured) temperature
  579. $measuredTemperature = "" if($mode == 2 || $measuredTemperature == 0);
  580. $untilStr = "" if($mode != 2);
  581. $desiredTemperature = ($desiredTemperature&0x7F)/2.0; #convert to degree celcius
  582. Log3 $hash, 5, "battery $batterylow, rferror $rferror, panel $panel, langateway $langateway, dstsetting $dstsetting, mode $mode, valveposition $valveposition %, desiredTemperature $desiredTemperature, until $untilStr, curTemp $measuredTemperature";
  583. #Very seldomly, the HeatingThermostat sends us temperatures like 0.2 or 0.3 degree Celcius - ignore them
  584. $measuredTemperature = "" if($measuredTemperature ne "" and $measuredTemperature < 1);
  585. $shash->{mode} = $mode;
  586. $shash->{rferror} = $rferror;
  587. $shash->{dstsetting} = $dstsetting;
  588. if($mode == 2){
  589. $shash->{until} = "$untilStr";
  590. }else{
  591. delete($shash->{until});
  592. }
  593. readingsBulkUpdate($shash, "mode", $ctrl_modes[$mode] );
  594. readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok");
  595. readingsBulkUpdate($shash, "batteryState", $batterylow ? "low" : "ok"); # Forum #87575
  596. #The formatting of desiredTemperature must match with in MAX_Set:$templist
  597. #Sometime we get an MAX_Parse MAX,1,ThermostatState,01090d,180000000000, where desiredTemperature is 0 - ignore it
  598. readingsBulkUpdate($shash, "desiredTemperature", MAX_SerializeTemperature($desiredTemperature)) if($desiredTemperature != 0);
  599. if($measuredTemperature ne "") {
  600. readingsBulkUpdate($shash, "temperature", MAX_SerializeTemperature($measuredTemperature));
  601. }
  602. if($shash->{type} =~ /HeatingThermostatPlus/ and $hash->{TYPE} eq "MAXLAN") {
  603. readingsBulkUpdate($shash, "valveposition", int($valveposition*MAX_ReadingsVal($shash,"maxValveSetting")/100));
  604. } else {
  605. readingsBulkUpdate($shash, "valveposition", $valveposition);
  606. }
  607. }elsif(grep /^$msgtype$/, ("WallThermostatState", "WallThermostatControl" )){
  608. my ($bits2,$displayActualTemperature,$desiredTemperatureRaw,$null1,$heaterTemperature,$null2,$temperature);
  609. if( length($args[0]) == 4 ) { #WallThermostatControl
  610. #This is the message that WallMountedThermostats send to paired HeatingThermostats
  611. ($desiredTemperatureRaw,$temperature) = unpack("CC",pack("H*",$args[0]));
  612. } elsif( length($args[0]) >= 6 and length($args[0]) <= 14) { #WallThermostatState
  613. #len=14: This is the message we get from the Cube over MAXLAN and which is probably send by WallMountedThermostats to the Cube
  614. #len=12: Payload of an Ack message, last field "temperature" is missing
  615. #len=10: Received by MAX_CUL as WallThermostatState
  616. #len=6 : Payload of an Ack message, last four fields (especially $heaterTemperature and $temperature) are missing
  617. ($bits2,$displayActualTemperature,$desiredTemperatureRaw,$null1,$heaterTemperature,$null2,$temperature) = unpack("aCCCCCC",pack("H*",$args[0]));
  618. #$heaterTemperature/10 is the temperature measured by a paired HeatingThermostat
  619. #we don't do anything with it here, because this value also appears as temperature in the HeatingThermostat's ThermostatState message
  620. my $mode = vec($bits2, 0, 2); #
  621. my $dstsetting = vec($bits2, 3, 1); #is automatically switching to DST activated
  622. my $langateway = vec($bits2, 4, 1); #??
  623. my $panel = vec($bits2, 5, 1); #1 if the heating thermostat is locked for manually setting the temperature at the device
  624. my $rferror = vec($bits2, 6, 1); #communication with link partner (what does that mean?)
  625. my $batterylow = vec($bits2, 7, 1); #1 if battery is low
  626. my $untilStr = "";
  627. if(defined($null2) and ($null1 != 0 or $null2 != 0)) {
  628. $untilStr = MAX_ParseDateTime($null1,$heaterTemperature,$null2)->{str};
  629. $heaterTemperature = "";
  630. $shash->{until} = "$untilStr";
  631. } else {
  632. delete($shash->{until});
  633. }
  634. $heaterTemperature = "" if(!defined($heaterTemperature));
  635. Log3 $hash, 5, "battery $batterylow, rferror $rferror, panel $panel, langateway $langateway, dstsetting $dstsetting, mode $mode, displayActualTemperature $displayActualTemperature, heaterTemperature $heaterTemperature, untilStr $untilStr";
  636. $shash->{rferror} = $rferror;
  637. readingsBulkUpdate($shash, "mode", $ctrl_modes[$mode] );
  638. readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok");
  639. readingsBulkUpdate($shash, "batteryState", $batterylow ? "low" : "ok"); # Forum #87575
  640. readingsBulkUpdate($shash, "displayActualTemperature", ($displayActualTemperature) ? 1 : 0);
  641. } else {
  642. Log3 $hash, 2, "Invalid $msgtype packet"
  643. }
  644. my $desiredTemperature = ($desiredTemperatureRaw &0x7F)/2.0; #convert to degree celcius
  645. if(defined($temperature)) {
  646. $temperature = ((($desiredTemperatureRaw &0x80)<<1) + $temperature)/10; # auch Temperaturen über 25.5 °C werden angezeigt !
  647. Log3 $hash, 5, "desiredTemperature $desiredTemperature, temperature $temperature";
  648. readingsBulkUpdate($shash, "temperature", sprintf("%2.1f",$temperature));
  649. } else {
  650. Log3 $hash, 5, "desiredTemperature $desiredTemperature"
  651. }
  652. #This formatting must match with in MAX_Set:$templist
  653. readingsBulkUpdate($shash, "desiredTemperature", MAX_SerializeTemperature($desiredTemperature));
  654. }elsif($msgtype eq "ShutterContactState"){
  655. my $bits = pack("H2",$args[0]);
  656. my $isopen = vec($bits,0,2) == 0 ? 0 : 1;
  657. my $unkbits = vec($bits,2,4);
  658. my $rferror = vec($bits,6,1);
  659. my $batterylow = vec($bits,7,1);
  660. Log3 $hash, 5, "ShutterContact isopen $isopen, rferror $rferror, battery $batterylow, unkbits $unkbits";
  661. $shash->{rferror} = $rferror;
  662. readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok");
  663. readingsBulkUpdate($shash, "batteryState", $batterylow ? "low" : "ok"); # Forum #87575
  664. readingsBulkUpdate($shash,"onoff",$isopen);
  665. }elsif($msgtype eq "PushButtonState") {
  666. my ($bits2, $onoff) = unpack("aC",pack("H*",$args[0]));
  667. #The meaning of $bits2 is completly guessed based on similarity to other devices, TODO: confirm
  668. my $gateway = vec($bits2, 4, 1); #Paired to a CUBE?
  669. my $rferror = vec($bits2, 6, 1); #communication with link partner (1 if we did not sent an Ack)
  670. my $batterylow = vec($bits2, 7, 1); #1 if battery is low
  671. readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok");
  672. readingsBulkUpdate($shash, "batteryState", $batterylow ? "low" : "ok"); # Forum #87575
  673. readingsBulkUpdate($shash, "onoff", $onoff);
  674. readingsBulkUpdate($shash, "connection", $gateway);
  675. } elsif(grep /^$msgtype$/, ("HeatingThermostatConfig", "WallThermostatConfig")) {
  676. readingsBulkUpdate($shash, "ecoTemperature", MAX_SerializeTemperature($args[0]));
  677. readingsBulkUpdate($shash, "comfortTemperature", MAX_SerializeTemperature($args[1]));
  678. readingsBulkUpdate($shash, "maximumTemperature", MAX_SerializeTemperature($args[2]));
  679. readingsBulkUpdate($shash, "minimumTemperature", MAX_SerializeTemperature($args[3]));
  680. readingsBulkUpdate($shash, ".weekProfile", $args[4]);
  681. if(@args > 5) { #HeatingThermostat and WallThermostat with new firmware
  682. readingsBulkUpdate($shash, "boostValveposition", $args[5]);
  683. readingsBulkUpdate($shash, "boostDuration", $boost_durations{$args[6]});
  684. readingsBulkUpdate($shash, "measurementOffset", MAX_SerializeTemperature($args[7]));
  685. readingsBulkUpdate($shash, "windowOpenTemperature", MAX_SerializeTemperature($args[8]));
  686. }
  687. if(@args > 9) { #HeatingThermostat
  688. readingsBulkUpdate($shash, "windowOpenDuration", $args[9]);
  689. readingsBulkUpdate($shash, "maxValveSetting", $args[10]);
  690. readingsBulkUpdate($shash, "valveOffset", $args[11]);
  691. readingsBulkUpdate($shash, "decalcification", "$decalcDays{$args[12]} $args[13]:00");
  692. }
  693. MAX_ParseWeekProfile($shash);
  694. } elsif($msgtype eq "Error") {
  695. if(@args == 0) {
  696. delete $shash->{ERROR} if(exists($shash->{ERROR}));
  697. } else {
  698. $shash->{ERROR} = join(",",@args);
  699. }
  700. } elsif($msgtype eq "AckWakeUp") {
  701. my ($duration) = @args;
  702. #substract five seconds safety margin
  703. $shash->{wakeUpUntil} = gettimeofday() + $duration - 5;
  704. } elsif($msgtype eq "AckConfigWeekProfile") {
  705. my ($day, $part, $profile) = @args;
  706. my $curWeekProfile = MAX_ReadingsVal($shash, ".weekProfile");
  707. substr($curWeekProfile, $day*52+$part*2*2*7, length($profile)) = $profile;
  708. readingsBulkUpdate($shash, ".weekProfile", $curWeekProfile);
  709. MAX_ParseWeekProfile($shash);
  710. } elsif(grep /^$msgtype$/, ("AckConfigValve", "AckConfigTemperatures", "AckSetDisplayActualTemperature" )) {
  711. if($args[0] eq "windowOpenTemperature"
  712. || $args[0] eq "comfortTemperature"
  713. || $args[0] eq "ecoTemperature"
  714. || $args[0] eq "maximumTemperature"
  715. || $args[0] eq "minimumTemperature" ) {
  716. readingsBulkUpdate($shash, $args[0], MAX_SerializeTemperature($args[1]));
  717. } else {
  718. #displayActualTemperature, boostDuration, boostValveSetting, maxValve, decalcification, valveOffset
  719. readingsBulkUpdate($shash, $args[0], $args[1]);
  720. }
  721. } elsif(grep /^$msgtype$/, ("AckSetGroupId", "AckRemoveGroupId" )) {
  722. readingsBulkUpdate($shash, "groupid", $args[0]);
  723. } elsif($msgtype eq "Ack") {
  724. #The payload of an Ack is a 2-digit hex number (being "01" for okey and "81" for "invalid command/argument"
  725. if($isToMe and (unpack("C",pack("H*",$args[0])) & 0x80)) {
  726. my $device = $addr;
  727. $device = $modules{MAX}{defptr}{$device}{NAME} if(exists($modules{MAX}{defptr}{$device}));
  728. Log3 $hash, 1, "Device $device answered with: Invalid command/argument";
  729. }
  730. #with unknown meaning plus the data of a State broadcast from the same device
  731. #For HeatingThermostats, it does not contain the last three "until" bytes (or measured temperature)
  732. if($shash->{type} =~ /HeatingThermostat.*/ ) {
  733. return MAX_Parse($hash, "MAX,$isToMe,ThermostatState,$addr,". substr($args[0],2));
  734. } elsif($shash->{type} eq "WallMountedThermostat") {
  735. return MAX_Parse($hash, "MAX,$isToMe,WallThermostatState,$addr,". substr($args[0],2));
  736. } elsif($shash->{type} eq "ShutterContact") {
  737. return MAX_Parse($hash, "MAX,$isToMe,ShutterContactState,$addr,". substr($args[0],2));
  738. } elsif($shash->{type} eq "PushButton") {
  739. return MAX_Parse($hash, "MAX,$isToMe,PushButtonState,$addr,". substr($args[0],2));
  740. } elsif($shash->{type} eq "Cube") {
  741. ; #Payload is always "00"
  742. } else {
  743. Log3 $hash, 2, "MAX_Parse: Don't know how to interpret Ack payload for $shash->{type}";
  744. }
  745. } elsif(grep /^$msgtype$/, ("SetTemperature")) { # SetTemperature is send by WallThermostat e.g. when pressing the boost button
  746. my $bits = unpack("C",pack("H*",$args[0]));
  747. my $mode = $bits >> 6;
  748. my $desiredTemperature = ($bits & 0x3F) /2.0; #convert to degree celcius
  749. readingsBulkUpdate($shash, "mode", $ctrl_modes[$mode] );
  750. #This formatting must match with in MAX_Set:$templist
  751. readingsBulkUpdate($shash, "desiredTemperature", MAX_SerializeTemperature($desiredTemperature));
  752. Log3 $hash, 5, "SetTemperature mode $ctrl_modes[$mode], desiredTemperature $desiredTemperature";
  753. } else {
  754. Log3 $hash, 1, "MAX_Parse: Unknown message $msgtype";
  755. }
  756. #Build state READING
  757. my $state = "waiting for data";
  758. if(exists($shash->{READINGS})) {
  759. $state = $shash->{READINGS}{connection}{VAL} ? "connected" : "not connected" if(exists($shash->{READINGS}{connection}));
  760. $state = "$shash->{READINGS}{desiredTemperature}{VAL} °C" if(exists($shash->{READINGS}{desiredTemperature}));
  761. $state = $shash->{READINGS}{onoff}{VAL} ? "opened" : "closed" if(exists($shash->{READINGS}{onoff}));
  762. }
  763. $state .= " (clock not set)" if($shash->{clocknotset});
  764. $state .= " (auto)" if(exists($shash->{mode}) and $shash->{mode} eq "auto");
  765. #Don't print this: it's the standard mode
  766. #$state .= " (manual)" if(exists($shash->{mode}) and $shash->{mode} eq "manual");
  767. $state .= " (until ".$shash->{until}.")" if(exists($shash->{mode}) and $shash->{mode} eq "temporary" );
  768. $state .= " (battery low)" if($shash->{batterylow});
  769. $state .= " (rf error)" if($shash->{rferror});
  770. readingsBulkUpdate($shash, "state", $state);
  771. readingsBulkUpdate($shash, "RSSI", $shash->{RSSI}) if ($shash->{RSSI});
  772. readingsEndUpdate($shash, 1);
  773. return $shash->{NAME}
  774. }
  775. #############################
  776. sub
  777. MAX_DbLog_splitFn($)
  778. {
  779. my ($event) = @_;
  780. my ($reading, $value, $unit) = "";
  781. my @parts = split(/ /,$event);
  782. $reading = shift @parts;
  783. $reading =~ tr/://d;
  784. $value = $parts[0];
  785. $value = $parts[1] if(defined($value) && lc($value) =~ m/auto/);
  786. $unit = "\xB0C" if(lc($reading) =~ m/temp/);
  787. $unit = "%" if(lc($reading) =~ m/valve/);
  788. return ($reading, $value, $unit);
  789. }
  790. 1;
  791. =pod
  792. =item device
  793. =item summary controls an MAX! device
  794. =item summary_DE Steuerung eines MAX! Geräts
  795. =begin html
  796. <a name="MAX"></a>
  797. <h3>MAX</h3>
  798. <ul>
  799. Devices from the eQ-3 MAX! group.<br>
  800. When heating thermostats show a temperature of zero degrees, they didn't yet send any data to the cube. You can
  801. force the device to send data to the cube by physically setting a temperature directly at the device (not through fhem).
  802. <br><br>
  803. <a name="MAXdefine"></a>
  804. <b>Define</b>
  805. <ul>
  806. <code>define &lt;name&gt; MAX &lt;type&gt; &lt;addr&gt;</code>
  807. <br><br>
  808. Define an MAX device of type &lt;type&gt; and rf address &lt;addr&gt.
  809. The &lt;type&gt; is one of HeatingThermostat, HeatingThermostatPlus, WallMountedThermostat, ShutterContact, PushButton.
  810. The &lt;addr&gt; is a 6 digit hex number.
  811. You should never need to specify this by yourself, the <a href="#autocreate">autocreate</a> module will do it for you.<br>
  812. It's advisable to set event-on-change-reading, like
  813. <code>attr MAX_123456 event-on-change-reading .*</code>
  814. because the polling mechanism will otherwise create events every 10 seconds.<br>
  815. Example:
  816. <ul>
  817. <code>define switch1 MAX PushButton ffc545</code><br>
  818. </ul>
  819. </ul>
  820. <br>
  821. <a name="MAXset"></a>
  822. <b>Set</b>
  823. <ul>
  824. <li>desiredTemperature auto [&lt;temperature&gt;]<br>
  825. For devices of type HeatingThermostat only. If &lt;temperature&gt; is omitted,
  826. the current temperature according to the week profile is used. If &lt;temperature&gt; is provided,
  827. it is used until the next switch point of the week porfile. It maybe one of
  828. <ul>
  829. <li>degree celcius between 4.5 and 30.5 in 0.5 degree steps</li>
  830. <li>"on" or "off" set the thermostat to full or no heating, respectively</li>
  831. <li>"eco" or "comfort" using the eco/comfort temperature set on the device (just as the right-most physical button on the device itself does)</li>
  832. </ul></li>
  833. <li>desiredTemperature [manual] &lt;value&gt; [until &lt;date&gt;]<br>
  834. For devices of type HeatingThermostat only. &lt;value&gt; maybe one of
  835. <ul>
  836. <li>degree celcius between 4.5 and 30.5 in 0.5 degree steps</li>
  837. <li>"on" or "off" set the thermostat to full or no heating, respectively</li>
  838. <li>"eco" or "comfort" using the eco/comfort temperature set on the device (just as the right-most physical button on the device itself does)</li>
  839. </ul>
  840. The optional "until" clause, with &lt;data&gt; in format "dd.mm.yyyy HH:MM" (minutes may only be "30" or "00"!),
  841. sets the temperature until that date/time. Make sure that the cube/device has a correct system time.
  842. If the keepAuto attribute is 1 and the device is currently in auto mode, 'desiredTemperature &lt;value&gt;'
  843. behaves as 'desiredTemperature auto &lt;value&gt;'. If the 'manual' keyword is used, the keepAuto attribute is ignored
  844. and the device goes into manual mode.</li>
  845. <li>desiredTemperature boost<br>
  846. For devices of type HeatingThermostat only.
  847. Activates the boost mode, where for boostDuration minutes the valve is opened up boostValveposition percent.</li>
  848. <li>groupid &lt;id&gt;<br>
  849. For devices of type HeatingThermostat only.
  850. Writes the given group id the device's memory. To sync all devices in one room, set them to the same groupid greater than zero.</li>
  851. <li>ecoTemperature &lt;value&gt;<br>
  852. For devices of type HeatingThermostat only. Writes the given eco temperature to the device's memory. It can be activated by pressing the rightmost physical button on the device.</li>
  853. <li>comfortTemperature &lt;value&gt;<br>
  854. For devices of type HeatingThermostat only. Writes the given comfort temperature to the device's memory. It can be activated by pressing the rightmost physical button on the device.</li>
  855. <li>measurementOffset &lt;value&gt;<br>
  856. For devices of type HeatingThermostat only. Writes the given temperature offset to the device's memory. If the internal temperature sensor is not well calibrated, it may produce a systematic error. Using measurementOffset, this error can be compensated. The reading temperature is equal to the measured temperature at sensor + measurementOffset. Usually, the internally measured temperature is a bit higher than the overall room temperature (due to closeness to the heater), so one uses a small negative offset. Must be between -3.5 and 3.5 degree celsius.</li>
  857. <li>minimumTemperature &lt;value&gt;<br>
  858. For devices of type HeatingThermostat only. Writes the given minimum temperature to the device's memory. It confines the temperature that can be manually set on the device.</li>
  859. <li>maximumTemperature &lt;value&gt;<br>
  860. For devices of type HeatingThermostat only. Writes the given maximum temperature to the device's memory. It confines the temperature that can be manually set on the device.</li>
  861. <li>windowOpenTemperature &lt;value&gt;<br>
  862. For devices of type HeatingThermostat only. Writes the given window open temperature to the device's memory. That is the temperature the heater will temporarily set if an open window is detected. Setting it to 4.5 degree or "off" will turn off reacting on open windows.</li>
  863. <li>windowOpenDuration &lt;value&gt;<br>
  864. For devices of type HeatingThermostat only. Writes the given window open duration to the device's memory. That is the duration the heater will temporarily set the window open temperature if an open window is detected by a rapid temperature decrease. (Not used if open window is detected by ShutterControl. Must be between 0 and 60 minutes in multiples of 5.</li>
  865. <li>decalcification &lt;value&gt;<br>
  866. For devices of type HeatingThermostat only. Writes the given decalcification time to the device's memory. Value must be of format "Sat 12:00" with minutes being "00". Once per week during that time, the HeatingThermostat will open the valves shortly for decalcification.</li>
  867. <li>boostDuration &lt;value&gt;<br>
  868. For devices of type HeatingThermostat only. Writes the given boost duration to the device's memory. Value must be one of 5, 10, 15, 20, 25, 30, 60. It is the duration of the boost function in minutes.</li>
  869. <li>boostValveposition &lt;value&gt;<br>
  870. For devices of type HeatingThermostat only. Writes the given boost valveposition to the device's memory. It is the valve position in percent during the boost function.</li>
  871. <li>maxValveSetting &lt;value&gt;<br>
  872. For devices of type HeatingThermostat only. Writes the given maximum valveposition to the device's memory. The heating thermostat will not open the valve more than this value (in percent).</li>
  873. <li>valveOffset &lt;value&gt;<br>
  874. For devices of type HeatingThermostat only. Writes the given valve offset to the device's memory. The heating thermostat will add this to all computed valvepositions during control.</li>
  875. <li>factoryReset<br>
  876. Resets the device to factory values. It has to be paired again afterwards.<br>
  877. ATTENTION: When using this on a ShutterContact using the MAXLAN backend, the ShutterContact has to be triggered once manually to complete
  878. the factoryReset.</li>
  879. <li>associate &lt;value&gt;<br>
  880. Associated one device to another. &lt;value&gt; can be the name of MAX device or its 6-digit hex address.<br>
  881. Associating a ShutterContact to a {Heating,WallMounted}Thermostat makes it send message to that device to automatically lower temperature to windowOpenTemperature while the shutter is opened. The thermostat must be associated to the ShutterContact, too, to accept those messages.
  882. <b>!Attention: After sending this associate command to the ShutterContact, you have to press the button on the ShutterContact to wake it up and accept the command. See the log for a message regarding this!</b>
  883. Associating HeatingThermostat and WallMountedThermostat makes them sync their desiredTemperature and uses the measured temperature of the
  884. WallMountedThermostat for control.</li>
  885. <li>deassociate &lt;value&gt;<br>
  886. Removes the association set by associate.</li>
  887. <li>weekProfile [&lt;day&gt; &lt;temp1&gt;,&lt;until1&gt;,&lt;temp2&gt;,&lt;until2&gt;] [&lt;day&gt; &lt;temp1&gt;,&lt;until1&gt;,&lt;temp2&gt;,&lt;until2&gt;] ...<br>
  888. Allows setting the week profile. For devices of type HeatingThermostat or WallMountedThermostat only. Example:<br>
  889. <code>set MAX_12345 weekProfile Fri 24.5,6:00,12,15:00,5 Sat 7,4:30,19,12:55,6</code><br>
  890. sets the profile <br>
  891. <code>Friday: 24.5 &deg;C for 0:00 - 6:00, 12 &deg;C for 6:00 - 15:00, 5 &deg;C for 15:00 - 0:00<br>
  892. Saturday: 7 &deg;C for 0:00 - 4:30, 19 &deg;C for 4:30 - 12:55, 6 &deg;C for 12:55 - 0:00</code><br>
  893. while keeping the old profile for all other days.
  894. </li>
  895. </ul>
  896. <br>
  897. <a name="MAXget"></a>
  898. <b>Get</b> <ul>N/A</ul><br>
  899. <a name="MAXattr"></a>
  900. <b>Attributes</b>
  901. <ul>
  902. <li><a href="#eventMap">eventMap</a></li>
  903. <li><a href="#IODev">IODev</a></li>
  904. <li><a href="#loglevel">loglevel</a></li>
  905. <li><a href="#do_not_notify">do_not_notify</a></li>
  906. <li><a href="#ignore">ignore</a></li>
  907. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  908. <li>keepAuto<br>Default: 0. If set to 1, it will stay in the auto mode when you set a desiredTemperature while the auto (=weekly program) mode is active.</li>
  909. </ul>
  910. <br>
  911. <a name="MAXevents"></a>
  912. <b>Generated events:</b>
  913. <ul>
  914. <li>desiredTemperature<br>Only for HeatingThermostat and WallMountedThermostat</li>
  915. <li>valveposition<br>Only for HeatingThermostat</li>
  916. <li>battery</li>
  917. <li>batteryState</li>
  918. <li>temperature<br>The measured temperature (= measured temperature at sensor + measurementOffset), only for HeatingThermostat and WallMountedThermostat</li>
  919. </ul>
  920. </ul>
  921. =end html
  922. =begin html_DE
  923. <a name="MAX"></a>
  924. <h3>MAX</h3>
  925. <ul>
  926. Verarbeitet MAX! Ger&auml;te, die von der eQ-3 MAX! Gruppe hergestellt werden.<br>
  927. Falls Heizk&ouml;rperthermostate eine Temperatur von Null Grad zeigen, wurde von ihnen
  928. noch nie Daten an den MAX Cube gesendet. In diesem Fall kann das Senden von Daten an
  929. den Cube durch Einstellen einer Temeratur direkt am Ger&auml;t (nicht &uuml;ber fhem)
  930. erzwungen werden.
  931. <br><br>
  932. <a name="MAXdefine"></a>
  933. <b>Define</b>
  934. <ul>
  935. <code>define &lt;name&gt; MAX &lt;type&gt; &lt;addr&gt;</code>
  936. <br><br>
  937. Erstellt ein MAX Ger&auml;t des Typs &lt;type&gt; und der RF Adresse &lt;addr&gt;.
  938. Als &lt;type&gt; kann entweder <code>HeatingThermostat</code> (Heizk&ouml;rperthermostat),
  939. <code>HeatingThermostatPlus</code> (Heizk&ouml;rperthermostat Plus),
  940. <code>WallMountedThermostat</code> (Wandthermostat), <code>ShutterContact</code> (Fensterkontakt)
  941. oder <code>PushButton</code> (Eco-Taster) gew&auml;hlt werden.
  942. Die Adresse &lt;addr&gt; ist eine 6-stellige hexadezimale Zahl.
  943. Da <a href="#autocreate">autocreate</a> diese vergibt, sollte diese nie h&auml;ndisch gew&auml;hlt
  944. werden m&uuml;ssen.<br>
  945. Es ist empfehlenswert, das Atribut <code>event-on-change-reading</code> zu setzen, z.B.
  946. <code>attr MAX_123456 event-on-change-reading .*</code> da ansonsten der "Polling" Mechanismus
  947. alle 10 s ein Ereignis erzeugt.<br>
  948. Beispiel:
  949. <ul>
  950. <code>define switch1 MAX PushButton ffc545</code><br>
  951. </ul>
  952. </ul>
  953. <br>
  954. <a name="MAXset"></a>
  955. <b>Set</b>
  956. <ul>
  957. <li>desiredTemperature &lt;value&gt; [until &lt;date&gt;]<br>
  958. Nur f&uuml;r Heizk&ouml;rperthermostate. &lt;value&gt; kann einer aus folgenden Werten sein
  959. <ul>
  960. <li>Grad Celsius zwischen 3,5 und 30,5 Grad in 0,5 Kelvin Schritten</li>
  961. <li>"on" oder "off" versetzt den Thermostat in volle bzw. keine Heizleistung</li>
  962. <li>"eco" oder "comfort" mit der eco/comfort Temperatur, die direkt am Ger&auml;t
  963. eingestellt wurde (&auml;nhlich wie die rechte Taste am Ger&auml;t selbst)</li>
  964. <li>"auto &lt;temperature&gt;". Damit wird das am Thermostat eingestellte Wochenprogramm
  965. abgearbeitet. Wenn optional die Temperatur &lt;temperature&gt; angegeben wird, wird diese
  966. bis zum n&auml;sten Schaltzeitpunkt des Wochenprogramms als
  967. <code>desiredTemperature</code> gesetzt.</li>
  968. <li>"boost" aktiviert den Boost Modus, wobei f&uuml;r <code>boostDuration</code> Minuten
  969. das Ventil <code>boostValveposition</code> Prozent ge&ouml;ffnet wird.</li>
  970. </ul>
  971. Alle Werte au&szlig;er "auto" k&ouml;nnen zus&auml;zlich den Wert "until" erhalten,
  972. wobei &lt;date&gt; in folgendem Format sein mu&szlig;: "dd.mm.yyyy HH:MM"
  973. (Minuten nur "30" bzw. "00"!), um kurzzeitige eine andere Temperatur bis zu diesem Datum/dieser
  974. Zeit einzustellen. Bitte sicherstellen, dass der Cube bzw. das Ger&auml;t die korrekte Systemzeit hat.</li>
  975. <li>groupid &lt;id&gt;<br>
  976. Nur f&uuml;r Heizk&ouml;rperthermostate.
  977. Schreibt die angegebene Gruppen ID in den Speicher des Ger&auml;tes.
  978. Um alle Ger&auml;te in einem Raum zu synchronisieren, k&ouml;nnen diese derselben Gruppen ID
  979. zugeordnet werden, diese mu&szlig; gr&ouml;&szlig;er Null sein.</li>
  980. <li>ecoTemperature &lt;value&gt;<br>
  981. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene <code>eco</code> Temperatur in den Speicher
  982. des Ger&auml;tes. Diese kann durch Dr&uuml;cken der rechten Taste am Ger&auml;t aktiviert werden.</li>
  983. <li>comfortTemperature &lt;value&gt;<br>
  984. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene <code>comfort</code> Temperatur in den Speicher
  985. des Ger&auml;tes. Diese kann durch Dr&uuml;cken der rechten Taste am Ger&auml;t aktiviert werden.</li>
  986. <li>measurementOffset &lt;value&gt;<br>
  987. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene <code>offset</code> Temperatur in den Speicher
  988. des Ger&auml;tes. Wenn der interne Temperatursensor nicht korrekt kalibriert ist, kann dieses einen
  989. systematischen Fehler erzeugen. Mit dem Wert <code>measurementOffset</code>, kann dieser Fehler
  990. kompensiert werden. Die ausgelese Temperatur ist gleich der gemessenen
  991. Temperatur + <code>measurementOffset</code>. Normalerweise ist die intern gemessene Temperatur h&ouml;her
  992. als die Raumtemperatur, da der Sensor n&auml;her am Heizk&ouml;rper ist und man verwendet einen
  993. kleinen negativen Offset, der zwischen -3,5 und 3,5 Kelvin sein mu&szlig;.</li>
  994. <li>minimumTemperature &lt;value&gt;<br>
  995. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegemene <code>minimum</code> Temperatur in der Speicher
  996. des Ger&auml;tes. Diese begrenzt die Temperatur, die am Ger&auml;t manuell eingestellt werden kann.</li>
  997. <li>maximumTemperature &lt;value&gt;<br>
  998. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegemene <code>maximum</code> Temperatur in der Speicher
  999. des Ger&auml;tes. Diese begrenzt die Temperatur, die am Ger&auml;t manuell eingestellt werden kann.</li>
  1000. <li>windowOpenTemperature &lt;value&gt;<br>
  1001. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegemene <code>window open</code> Temperatur in den Speicher
  1002. des Ger&auml;tes. Das ist die Tempereratur, die an der Heizung kurzfristig eingestellt wird, wenn ein
  1003. ge&ouml;ffnetes Fenster erkannt wird. Der Wert 4,5 Grad bzw. "off" schaltet die Reaktion auf
  1004. ein offenes Fenster aus.</li>
  1005. <li>windowOpenDuration &lt;value&gt;<br>
  1006. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene <code>window</code> open Dauer in den Speicher
  1007. des Ger&auml;tes. Dies ist die Dauer, w&auml;hrend der die Heizung kurzfristig die window open Temperatur
  1008. einstellt, wenn ein offenes Fenster durch einen schnellen Temperatursturz erkannt wird.
  1009. (Wird nicht verwendet, wenn das offene Fenster von <code>ShutterControl</code> erkannt wird.)
  1010. Parameter muss zwischen Null und 60 Minuten sein als Vielfaches von 5.</li>
  1011. <li>decalcification &lt;value&gt;<br>
  1012. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene Zeit f&uuml;r <code>decalcification</code>
  1013. in den Speicher des Ger&auml;tes. Parameter muss im Format "Sat 12:00" sein, wobei die Minuten
  1014. "00" sein m&uuml;ssen. Zu dieser angegebenen Zeit wird das Heizk&ouml;rperthermostat das Ventil
  1015. kurz ganz &ouml;ffnen, um vor Schwerg&auml;ngigkeit durch Kalk zu sch&uuml;tzen.</li>
  1016. <li>boostDuration &lt;value&gt;<br>
  1017. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene Boost Dauer in den Speicher
  1018. des Ger&auml;tes. Der gew&auml;hlte Parameter muss einer aus 5, 10, 15, 20, 25, 30 oder 60 sein
  1019. und gibt die Dauer der Boost-Funktion in Minuten an.</li>
  1020. <li>boostValveposition &lt;value&gt;<br>
  1021. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene Boost Ventilstellung in den Speicher
  1022. des Ger&auml;tes. Dies ist die Ventilstellung (in Prozent) die bei der Boost-Fumktion eingestellt wird.</li>
  1023. <li>maxValveSetting &lt;value&gt;<br>
  1024. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt die angegebene maximale Ventilposition in den Speicher
  1025. des Ger&auml;tes. Der Heizk&ouml;rperthermostat wird das Ventil nicht weiter &ouml;ffnen als diesen Wert
  1026. (Angabe in Prozent).</li>
  1027. <li>valveOffset &lt;value&gt;<br>
  1028. Nur f&uuml;r Heizk&ouml;rperthermostate. Schreibt den angegebenen <code>offset</code> Wert der Ventilstellung
  1029. in den Speicher des Ger&auml;tes Der Heizk&ouml;rperthermostat wird diesen Wert w&auml;hrend der Regelung
  1030. zu den berechneten Ventilstellungen hinzuaddieren.</li>
  1031. <li>factoryReset<br>
  1032. Setzt das Ger&auml;t auf die Werkseinstellungen zur&uuml;ck. Das Ger&auml;t muss anschlie&szlig;end neu
  1033. angelernt werden.<br>
  1034. ACHTUNG: Wenn dies in Kombination mit einem Fensterkontakt und dem MAXLAN Modul
  1035. verwendet wird, muss der Fensterkontakt einmal manuell ausgel&ouml;st werden, damit das
  1036. Zur&uuml;cksetzen auf Werkseinstellungen beendet werden kann.</li>
  1037. <li>associate &lt;value&gt;<br>
  1038. Verbindet ein Ger&auml;t mit einem anderen. &lt;value&gt; kann entweder der Name eines MAX Ger&auml;tes oder
  1039. seine 6-stellige hexadezimale Adresse sein.<br>
  1040. Wenn ein Fensterkontakt mit einem {Heizk&ouml;rper-/Wand-}Thermostat verbunden wird, sendet der
  1041. Fensterkontakt automatisch die <code>windowOpenTemperature</code> Temperatur wenn der Kontakt
  1042. ge&ouml;ffnet ist. Das Thermostat muss ebenfalls mit dem Fensterkontakt verbunden werden, um diese
  1043. Botschaften zu verarbeiten.
  1044. <b>Achtung: Nach dem Senden der Botschaft zum Verbinden an den Fensterkontakt muss der Knopf am
  1045. Fensterkontakt gedr&uuml;ckt werden um den Fensterkonakt einzuschalten und den Befehl zu verarbeiten.
  1046. Details &uuml;ber das erfolgreiche Verbinden finden sich in der fhem Logdatei!</b>
  1047. Das Verbinden eines Heizk&ouml;rperthermostates und eines Wandthermostates synchronisiert deren
  1048. <code>desiredTemperature</code> und verwendet die am Wandthermostat gemessene Temperatur f&uuml;r
  1049. die Regelung.</li>
  1050. <li>deassociate &lt;value&gt;<br>
  1051. L&ouml;st die Verbindung, die mit <code>associate</code> gemacht wurde, wieder auf.</li>
  1052. <li>weekProfile [&lt;day&gt; &lt;temp1&gt;,&lt;until1&gt;,&lt;temp2&gt;,&lt;until2&gt;]
  1053. [&lt;day&gt; &lt;temp1&gt;,&lt;until1&gt;,&lt;temp2&gt;,&lt;until2&gt;] ...<br>
  1054. Erlaubt das Setzen eines Wochenprofils. Nur f&uuml;r Heizk&oum;rperthermostate bzw. Wandthermostate.<br>
  1055. Beispiel:<br>
  1056. <code>set MAX_12345 weekProfile Fri 24.5,6:00,12,15:00,5 Sat 7,4:30,19,12:55,6</code><br>
  1057. stellt das folgende Profil ein<br>
  1058. <code>Freitag: 24.5 &deg;C von 0:00 - 6:00, 12 &deg;C von 6:00 - 15:00, 5 &deg;C von 15:00 - 0:00<br>
  1059. Samstag: 7 &deg;C von 0:00 - 4:30, 19 &deg;C von 4:30 - 12:55, 6 &deg;C von 12:55 - 0:00</code><br>
  1060. und beh&auml;lt die Profile f&uuml;r die anderen Wochentage bei.
  1061. </li>
  1062. </ul>
  1063. <br>
  1064. <a name="MAXget"></a>
  1065. <b>Get</b> <ul>N/A</ul><br>
  1066. <a name="MAXattr"></a>
  1067. <b>Attributes</b>
  1068. <ul>
  1069. <li><a href="#eventMap">eventMap</a></li>
  1070. <li><a href="#IODev">IODev</a></li>
  1071. <li><a href="#loglevel">loglevel</a></li>
  1072. <li><a href="#do_not_notify">do_not_notify</a></li>
  1073. <li><a href="#ignore">ignore</a></li>
  1074. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  1075. <li>keepAuto<br>Standardwert: 0. Wenn der Wert auf 1 gesetzt wird, bleibt das Ger&auml;t im
  1076. Wochenprogramm auch wenn eine <code>desiredTemperature</code> gesendet wird.</li>
  1077. </ul>
  1078. <br>
  1079. <a name="MAXevents"></a>
  1080. <b>Erzeugte Ereignisse:</b>
  1081. <ul>
  1082. <li>desiredTemperature<br>Nur f&uuml;r Heizk&ouml;rperthermostate und Wandthermostate</li>
  1083. <li>valveposition<br>Nur f&uuml;r Heizk&ouml;rperthermostate</li>
  1084. <li>battery</li>
  1085. <li>batteryState</li>
  1086. <li>temperature<br>Die gemessene Temperatur (= gemessene Temperatur + <code>measurementOffset</code>),
  1087. nur f&uuml;r Heizk&ouml;rperthermostate und Wandthermostate</li>
  1088. </ul>
  1089. </ul>
  1090. =end html_DE
  1091. =cut