14_CUL_MAX.pm 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. ##############################################
  2. # $Id: 14_CUL_MAX.pm 12440 2016-10-26 20:24:45Z mgehre $
  3. # Written by Matthias Gehre, M.Gehre@gmx.de, 2012-2013
  4. package main;
  5. use strict;
  6. use warnings;
  7. use MaxCommon;
  8. use POSIX;
  9. sub CUL_MAX_BroadcastTime(@);
  10. sub CUL_MAX_Set($@);
  11. sub CUL_MAX_SendTimeInformation(@);
  12. sub CUL_MAX_GetTimeInformationPayload();
  13. sub CUL_MAX_Send(@);
  14. sub CUL_MAX_SendQueueHandler($$);
  15. my $pairmodeDuration = 60; #seconds
  16. my $ackTimeout = 3; #seconds
  17. my $maxRetryCnt = 3;
  18. sub
  19. CUL_MAX_Initialize($)
  20. {
  21. my ($hash) = @_;
  22. $hash->{Match} = "^Z";
  23. $hash->{DefFn} = "CUL_MAX_Define";
  24. $hash->{Clients} = ":MAX:";
  25. my %mc = (
  26. "1:MAX" => "^MAX",
  27. );
  28. $hash->{MatchList} = \%mc;
  29. $hash->{UndefFn} = "CUL_MAX_Undef";
  30. $hash->{ParseFn} = "CUL_MAX_Parse";
  31. $hash->{SetFn} = "CUL_MAX_Set";
  32. $hash->{AttrFn} = "CUL_MAX_Attr";
  33. $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:0,1 " .
  34. "showtime:1,0 ".
  35. $readingFnAttributes;
  36. $hash->{sendQueue} = [];
  37. }
  38. #############################
  39. sub
  40. CUL_MAX_SetupCUL($)
  41. {
  42. my $hash = $_[0];
  43. AssignIoPort($hash);
  44. if(!defined($hash->{IODev})) {
  45. Log3 $hash, 1, "$hash->{NAME}: did not find suitable IODev (CUL etc. in rfmode MAX)! You may want to execute 'attr $hash->{NAME} IODev SomeCUL'";
  46. return 0;
  47. }
  48. my $version = CUL_MAX_Check($hash);
  49. Log3 $hash, 3, "CUL_MAX_Check: Detected firmware version $version of the CUL-compatible IODev";
  50. if($version >= 152) {
  51. #Doing this on older firmware disables MAX mode
  52. IOWrite($hash, "", "Za". $hash->{addr});
  53. #Append to initString, so this is resend if cul disappears and then reappears
  54. $hash->{IODev}{initString} .= "\nZa". $hash->{addr};
  55. }
  56. if($version >= 153) {
  57. #Doing this on older firmware disables MAX mode
  58. my $cmd = "Zw". CUL_MAX_fakeWTaddr($hash);
  59. IOWrite($hash, "", $cmd);
  60. $hash->{IODev}{initString} .= "\n".$cmd;
  61. }
  62. return 1
  63. }
  64. sub
  65. CUL_MAX_Define($$)
  66. {
  67. my ($hash, $def) = @_;
  68. my @a = split("[ \t][ \t]*", $def);
  69. return "wrong syntax: define <name> CUL_MAX <srcAddr>" if(@a<3);
  70. if (length($a[2]) != 6) {
  71. Log3 $hash, 1, "The adress must be 6 hexadecimal digits";
  72. return "The adress must be 6 hexadecimal digits";
  73. }
  74. $hash->{addr} = lc($a[2]);
  75. $hash->{STATE} = "Defined";
  76. $hash->{cnt} = 0;
  77. $hash->{pairmode} = 0;
  78. $hash->{retryCount} = 0;
  79. $hash->{sendQueue} = [];
  80. CUL_MAX_SetupCUL($hash);
  81. #This interface is shared with 00_MAXLAN.pm
  82. $hash->{Send} = \&CUL_MAX_Send;
  83. #Start broadcasting time after 30 seconds, so there is enough time to parse the config
  84. InternalTimer(gettimeofday()+30, "CUL_MAX_BroadcastTime", $hash, 0);
  85. return undef;
  86. }
  87. #####################################
  88. sub
  89. CUL_MAX_Undef($$)
  90. {
  91. my ($hash, $name) = @_;
  92. RemoveInternalTimer($hash);
  93. return undef;
  94. }
  95. sub
  96. CUL_MAX_DisablePairmode($)
  97. {
  98. my $hash = shift;
  99. $hash->{pairmode} = 0;
  100. }
  101. sub
  102. CUL_MAX_Check($@)
  103. {
  104. my ($hash) = @_;
  105. if(!defined($hash->{IODev})) {
  106. Log3 $hash, 1, "CUL_MAX_Check: No IODev found.";
  107. return 0;
  108. }
  109. if(!defined($hash->{IODev}{VERSION})) {
  110. Log3 $hash, 1, "CUL_MAX_Check: No IODev has no VERSION";
  111. return 0;
  112. }
  113. my $version = $hash->{IODev}{VERSION};
  114. if($version =~ m/.*a-culfw.*/) {
  115. #a-culfw is compatibel to culfw 154
  116. return 154;
  117. }
  118. #Looks like "V 1.49 CUL868"
  119. if($version =~ m/V (.*)\.(.*) .*/) {
  120. my ($major_version,$minorversion) = ($1, $2);
  121. $version = 100*$major_version + $minorversion;
  122. if($version < 154) {
  123. Log3 $hash, 2, "CUL_MAX_Check: You are using an old version of the CUL firmware, which has known bugs with respect to MAX! support. Please update.";
  124. }
  125. return $version;
  126. } else {
  127. Log3 $hash, 1, "CUL_MAX_Check: Could not correctly parse IODev->{VERSION} = '$version'";
  128. return 0;
  129. }
  130. }
  131. sub
  132. CUL_MAX_Attr(@)
  133. {
  134. my ($hash, $action, $name, $attr, $value) = @_;
  135. if ($action eq "set") {
  136. return "No such attribute" if($attr !~ ["fakeWTaddr", "fakeSCaddr", "IODev"]);
  137. return "Invalid value" if(grep( /^\Q$attr\E$/, ("fakeWTaddr", "fakeSCaddr")) && $value !~ /^[0-9a-fA-F]{6}$/);
  138. }
  139. }
  140. sub
  141. CUL_MAX_fakeWTaddr($)
  142. {
  143. return lc(AttrVal($_[0]->{NAME}, "fakeWTaddr", "111111"));
  144. }
  145. sub
  146. CUL_MAX_fakeSCaddr($)
  147. {
  148. return lc(AttrVal($_[0]->{NAME}, "fakeSCaddr", "222222"));
  149. }
  150. sub
  151. CUL_MAX_Set($@)
  152. {
  153. my ($hash, $device, @a) = @_;
  154. return "\"set MAXLAN\" needs at least one parameter" if(@a < 1);
  155. my ($setting, @args) = @a;
  156. if($setting eq "pairmode") {
  157. $hash->{pairmode} = 1;
  158. InternalTimer(gettimeofday()+$pairmodeDuration, "CUL_MAX_DisablePairmode", $hash, 0);
  159. } elsif($setting eq "broadcastTime") {
  160. CUL_MAX_BroadcastTime($hash, 1);
  161. } elsif(grep /^\Q$setting\E$/, ("fakeSC", "fakeWT")) {
  162. return "Invalid number of arguments" if(@args == 0);
  163. my $dest = $args[0];
  164. my $destname;
  165. #$dest may be either a name or an address
  166. if(exists($defs{$dest})) {
  167. return "Destination is not a MAX device" if($defs{$dest}{TYPE} ne "MAX");
  168. $destname = $dest;
  169. $dest = $defs{$dest}{addr};
  170. } else {
  171. $dest = lc($dest); #address to lower-case
  172. return "No MAX device with address $dest" if(!exists($modules{MAX}{defptr}{$dest}));
  173. $destname = $modules{MAX}{defptr}{$dest}{NAME};
  174. }
  175. if($setting eq "fakeSC") {
  176. return "Invalid number of arguments" if(@args != 2);
  177. return "Invalid fakeSCaddr attribute set (must not be 000000)" if(CUL_MAX_fakeSCaddr($hash) eq "000000");
  178. my $state = $args[1] ? "12" : "10";
  179. my $groupid = ReadingsVal($destname,"groupid",0);
  180. return CUL_MAX_Send($hash, "ShutterContactState",$dest,$state,
  181. groupId => sprintf("%02x",$groupid),
  182. flags => ( $groupid ? "04" : "06" ),
  183. src => CUL_MAX_fakeSCaddr($hash));
  184. } elsif($setting eq "fakeWT") {
  185. return "Invalid number of arguments" if(@args != 3);
  186. return "desiredTemperature is invalid" if(!validTemperature($args[1]));
  187. return "Invalid fakeWTaddr attribute set (must not be 000000)" if(CUL_MAX_fakeWTaddr($hash) eq "000000");
  188. #Valid range for measured temperature is 0 - 51.1 degree
  189. $args[2] = 0 if($args[2] < 0); #Clamp temperature to minimum of 0 degree
  190. #Encode into binary form
  191. my $arg2 = int(10*$args[2]);
  192. #First bit is 9th bit of temperature, rest is desiredTemperature
  193. my $arg1 = (($arg2&0x100)>>1) | (int(2*MAX_ParseTemperature($args[1]))&0x7F);
  194. $arg2 &= 0xFF; #only take the lower 8 bits
  195. my $groupid = ReadingsVal($destname,"groupid",0);
  196. return CUL_MAX_Send($hash,"WallThermostatControl",$dest,
  197. sprintf("%02x%02x",$arg1,$arg2), groupId => sprintf("%02x",$groupid),
  198. flags => ( $groupid ? "04" : "00" ),
  199. src => CUL_MAX_fakeWTaddr($hash));
  200. }
  201. } else {
  202. return "Unknown argument $setting, choose one of pairmode broadcastTime";
  203. }
  204. return undef;
  205. }
  206. sub
  207. CUL_MAX_Parse($$)
  208. {
  209. #Attention: there is a limit in the culfw firmware: It only receives messages shorter than 30 bytes (see rf_moritz.h)
  210. # $hash is for the CUL instance
  211. my ($hash, $rmsg) = @_;
  212. my $shash = undef; #shash is for the CUL_MAX instance
  213. #Find a CUL_MAX that has the CUL $hash as its IODev;
  214. #if no matching is found, just use the last encountered CUL_MAX.
  215. foreach my $d (keys %defs) {
  216. if($defs{$d}{TYPE} eq "CUL_MAX") {
  217. $shash = $defs{$d};
  218. last if($defs{$d}{IODev} == $hash);
  219. }
  220. }
  221. if(!defined($shash)) {
  222. Log3 $hash, 2, "No CUL_MAX defined";
  223. return "UNDEFINED CULMAX0 CUL_MAX 123456";
  224. }
  225. return () if($rmsg !~ m/Z(..)(..)(..)(..)(......)(......)(..)(.*)/);
  226. my ($len,$msgcnt,$msgFlag,$msgTypeRaw,$src,$dst,$groupid,$payload) = ($1,$2,$3,$4,$5,$6,$7,$8);
  227. $len = hex($len);
  228. if(2*$len+3 != length($rmsg)) { #+3 = +1 for 'Z' and +2 for len field in hex
  229. Log3 $hash, 1, "CUL_MAX_Parse: len mismatch";
  230. return $shash->{NAME};
  231. }
  232. $groupid = hex($groupid);
  233. #convert adresses to lower case
  234. $src = lc($src);
  235. $dst = lc($dst);
  236. my $msgType = exists($msgId2Cmd{$msgTypeRaw}) ? $msgId2Cmd{$msgTypeRaw} : $msgTypeRaw;
  237. Log3 $hash, 5, "CUL_MAX_Parse: len $len, msgcnt $msgcnt, msgflag $msgFlag, msgTypeRaw $msgType, src $src, dst $dst, groupid $groupid, payload $payload";
  238. return $shash->{NAME} if (exists($modules{MAX}{defptr}{$src}) && IsIgnored($modules{MAX}{defptr}{$src}{NAME}));
  239. my $isToMe = ($dst eq $shash->{addr}) ? 1 : 0; # $isToMe is true if that packet was directed at us
  240. #Set RSSI on MAX device
  241. if(exists($modules{MAX}{defptr}{$src}) && exists($hash->{RSSI})) {
  242. Log3 $hash, 5, "CUL_MAX_Parse: rssi: $hash->{RSSI}";
  243. $modules{MAX}{defptr}{$src}{RSSI} = $hash->{RSSI};
  244. }
  245. if(exists($msgId2Cmd{$msgTypeRaw})) {
  246. if($msgType eq "Ack") {
  247. #Ignore packets generated by culfw's auto-Ack
  248. return $shash->{NAME} if($src eq $shash->{addr});
  249. return $shash->{NAME} if($src eq CUL_MAX_fakeWTaddr($hash));
  250. return $shash->{NAME} if($src eq CUL_MAX_fakeSCaddr($hash));
  251. Dispatch($shash, "MAX,$isToMe,Ack,$src,$payload", {});
  252. return $shash->{NAME} if(!@{$shash->{sendQueue}}); #we are not waiting for any Ack
  253. for my $i (0 .. $#{$shash->{sendQueue}}) {
  254. my $packet = $shash->{sendQueue}[$i];
  255. if($packet->{src} eq $dst and $packet->{dst} eq $src and $packet->{cnt} == hex($msgcnt)) {
  256. Log3 $hash, 5, "Got matching ack";
  257. my $isnak = unpack("C",pack("H*",$payload)) & 0x80;
  258. $packet->{sent} = $isnak ? 3 : 2;
  259. }
  260. }
  261. #Handle outgoing messages to that ShutterContact. It is only awake shortly
  262. #after sending an Ack to a PairPong
  263. CUL_MAX_SendQueueHandler($shash, $src) if(exists($modules{MAX}{defptr}{$src}) && $modules{MAX}{defptr}{$src}{type} eq "ShutterContact");
  264. return $shash->{NAME};
  265. } elsif($msgType eq "TimeInformation") {
  266. if($isToMe) {
  267. #This is a request for TimeInformation send to us
  268. Log3 $hash, 5, "Got request for TimeInformation, sending it";
  269. CUL_MAX_SendTimeInformation($shash, $src);
  270. } elsif(length($payload) > 0) {
  271. my ($f1,$f2,$f3,$f4,$f5) = unpack("CCCCC",pack("H*",$payload));
  272. #For all fields but the month I'm quite sure
  273. my $year = $f1 + 2000;
  274. my $day = $f2;
  275. my $hour = ($f3 & 0x1F);
  276. my $min = $f4 & 0x3F;
  277. my $sec = $f5 & 0x3F;
  278. my $month = (($f4 >> 6) << 2) | ($f5 >> 6); #this is just guessed
  279. my $unk1 = $f3 >> 5;
  280. my $unk2 = $f4 >> 6;
  281. my $unk3 = $f5 >> 6;
  282. #I guess the unk1,2,3 encode if we are in DST?
  283. Log3 $hash, 5, "CUL_MAX_Parse: Got TimeInformation: (in GMT) year $year, mon $month, day $day, hour $hour, min $min, sec $sec, unk ($unk1, $unk2, $unk3)";
  284. }
  285. } elsif($msgType eq "PairPing") {
  286. my ($firmware,$type,$testresult,$serial) = unpack("CCCa*",pack("H*",$payload));
  287. #What does testresult mean?
  288. Log3 $hash, 5, "CUL_MAX_Parse: Got PairPing (dst $dst, pairmode $shash->{pairmode}), firmware $firmware, type $type, testresult $testresult, serial $serial";
  289. #There are two variants of PairPing:
  290. #1. It has a destination address of "000000" and can be paired to any device.
  291. #2. It is sent after changing batteries or repressing the pair button (without factory reset) and has a destination address of the last paired device. We can answer it with PairPong and even get an Ack, but it will still not be paired to us. A factory reset (originating from the last paired device) is needed first.
  292. if(($dst ne "000000") and !$isToMe) {
  293. Log3 $hash,5 , "Device want's to be re-paired to $dst, not to us";
  294. return $shash->{NAME};
  295. }
  296. #If $isToMe is true, this device is already paired and just wants to be reacknowledged
  297. #If we already have the device created but it was reseted (batteries changed?), we directly re-pair (without pairmode)
  298. if($shash->{pairmode} || $isToMe || exists($modules{MAX}{defptr}{$src})) {
  299. Log3 $hash, 3, "CUL_MAX_Parse: " . ($isToMe ? "Re-Pairing" : "Pairing") . " device $src of type $device_types{$type} with serial $serial";
  300. Dispatch($shash, "MAX,$isToMe,define,$src,$device_types{$type},$serial,0", {});
  301. #Set firmware and testresult on device
  302. my $dhash = CUL_MAX_DeviceHash($src);
  303. if(defined($dhash)) {
  304. readingsBeginUpdate($dhash);
  305. readingsBulkUpdate($dhash, "firmware", sprintf("%u.%u",int($firmware/16),$firmware%16));
  306. readingsBulkUpdate($dhash, "testresult", $testresult);
  307. readingsEndUpdate($dhash, 1);
  308. }
  309. #Send after dispatch the define, otherwise Send will create an invalid device
  310. CUL_MAX_Send($shash, "PairPong", $src, "00");
  311. return $shash->{NAME} if($isToMe); #if just re-pairing, default values are not restored (I checked)
  312. #This are the default values that a device has after factory reset or pairing
  313. if($device_types{$type} =~ /HeatingThermostat.*/) {
  314. Dispatch($shash, "MAX,$isToMe,HeatingThermostatConfig,$src,17,21,30.5,4.5,$defaultWeekProfile,80,5,0,12,15,100,0,0,12", {});
  315. } elsif($device_types{$type} eq "WallMountedThermostat") {
  316. Dispatch($shash, "MAX,$isToMe,WallThermostatConfig,$src,17,21,30.5,4.5,$defaultWeekProfile,80,5,0,12", {});
  317. }
  318. }
  319. } elsif(grep /^$msgType$/, ("ShutterContactState", "WallThermostatState", "WallThermostatControl", "ThermostatState", "PushButtonState", "SetTemperature")) {
  320. Dispatch($shash, "MAX,$isToMe,$msgType,$src,$payload", {});
  321. } else {
  322. Log3 $hash,5 , "Unhandled message $msgType";
  323. }
  324. } else {
  325. Log3 $hash, 2, "CUL_MAX_Parse: Got unhandled message type $msgTypeRaw";
  326. }
  327. return $shash->{NAME};
  328. }
  329. #All inputs are hex strings, $cmd is one from %msgCmd2Id
  330. sub
  331. CUL_MAX_Send(@)
  332. {
  333. # $cmd is one of
  334. my ($hash, $cmd, $dst, $payload, %opts) = @_;
  335. my $flags = "00";
  336. my $groupId = "00";
  337. my $src = $hash->{addr};
  338. my $callbackParam = undef;
  339. $flags = $opts{flags} if(exists($opts{flags}));
  340. $groupId = $opts{groupId} if(exists($opts{groupId}));
  341. $src = $opts{src} if(exists($opts{src}));
  342. $callbackParam = $opts{callbackParam} if(exists($opts{callbackParam}));
  343. my $dhash = CUL_MAX_DeviceHash($dst);
  344. $dhash->{READINGS}{msgcnt}{VAL} += 1;
  345. $dhash->{READINGS}{msgcnt}{VAL} &= 0xFF;
  346. $dhash->{READINGS}{msgcnt}{TIME} = TimeNow();
  347. my $msgcnt = sprintf("%02x",$dhash->{READINGS}{msgcnt}{VAL});
  348. my $packet = $msgcnt . $flags . $msgCmd2Id{$cmd} . $src . $dst . $groupId . $payload;
  349. #prefix length in bytes
  350. $packet = sprintf("%02x",length($packet)/2) . $packet;
  351. Log3 $hash, 5, "CUL_MAX_Send: enqueuing $packet";
  352. my $timeout = gettimeofday()+$ackTimeout;
  353. my $aref = $hash->{sendQueue};
  354. push(@{$aref}, { "packet" => $packet,
  355. "src" => $src,
  356. "dst" => $dst,
  357. "cnt" => hex($msgcnt),
  358. "time" => $timeout,
  359. "sent" => "0",
  360. "cmd" => $cmd,
  361. "callbackParam" => $callbackParam,
  362. });
  363. #Call CUL_MAX_SendQueueHandler if we just enqueued the only packet
  364. #otherwise it is already in the InternalTimer list
  365. CUL_MAX_SendQueueHandler($hash,undef) if(@{$hash->{sendQueue}} == 1);
  366. return undef;
  367. }
  368. sub
  369. CUL_MAX_DeviceHash($)
  370. {
  371. my $addr = shift;
  372. return $modules{MAX}{defptr}{$addr};
  373. }
  374. #This can be called for two reasons:
  375. #1. @sendQueue was empty, CUL_MAX_Send added a packet and then called us
  376. #2. We sent a packet from @sendQueue and now the ackTimeout is over.
  377. # The packet my still be in @sendQueue (timed out) or removed when the Ack was received.
  378. # Arguments are hash and responseToShutterContact.
  379. # If SendQueueHandler was called after receiving a message from a shutter contact, responseToShutterContact
  380. # holds the address of the respective shutter contact. Otherwise, it is empty.
  381. sub
  382. CUL_MAX_SendQueueHandler($$)
  383. {
  384. my $hash = shift;
  385. my $responseToShutterContact = shift;
  386. Log3 $hash, 5, "CUL_MAX_SendQueueHandler: " . @{$hash->{sendQueue}} . " items in queue";
  387. return if(!@{$hash->{sendQueue}}); #nothing to do
  388. my $timeout = gettimeofday(); #reschedule immediatly
  389. #Check if we have an IODev
  390. if(!defined($hash->{IODev})) {
  391. Log3 $hash, 1, "$hash->{NAME}: did not find suitable IODev (CUL etc. in rfmode MAX), cannot send! You may want to execute 'attr $hash->{NAME} IODev SomeCUL'";
  392. #Maybe some CUL will appear magically in some seconds
  393. #At least we cannot quit here with an non-empty queue, so we have two alternatives:
  394. #1. Delete the packet from queue and quit -> packet is lost
  395. #2. Wait, recheck, wait, recheck ... -> a lot of logs
  396. #InternalTimer($timeout+60, "CUL_MAX_SendQueueHandler", $hash, 0);
  397. $hash->{sendQueue} = [];
  398. return undef;
  399. }
  400. my ($packet, $pktIdx, $packetForShutterContactInQueue);
  401. for($pktIdx = 0; $pktIdx < @{$hash->{sendQueue}}; $pktIdx += 1) {
  402. $packet = $hash->{sendQueue}[$pktIdx];
  403. if(defined($responseToShutterContact)) {
  404. #Find a packet to the ShutterContact in $responseToShutterContact
  405. last if($packet->{dst} eq $responseToShutterContact);
  406. } else {
  407. #We cannot sent packets to a ShutterContact directly, everything else is possible
  408. last if($packet->{cmd} eq "PairPong"
  409. || $packet->{sent} != 0
  410. || $modules{MAX}{defptr}{$packet->{dst}}{type} ne "ShutterContact");
  411. $packetForShutterContactInQueue = $modules{MAX}{defptr}{$packet->{dst}}{NAME};
  412. }
  413. }
  414. if($pktIdx == @{$hash->{sendQueue}} && !defined($responseToShutterContact)) {
  415. Log3 $hash, 2, "There is a packet for ShutterContact $packetForShutterContactInQueue in queue. Please trigger a window action (open or close the window) to wake up the respective ShutterContact and let it receive the packet.";
  416. $timeout += 3;
  417. InternalTimer($timeout, "CUL_MAX_SendQueueHandler", $hash, 0);
  418. return undef;
  419. }
  420. if( $packet->{sent} == 0 ) { #Need to send it first
  421. #We can use fast sending without preamble on culfw 1.53 and higher when the devices has been woken up
  422. my $needPreamble = ((CUL_MAX_Check($hash) < 153)
  423. || (!defined($responseToShutterContact) &&
  424. (!defined($modules{MAX}{defptr}{$packet->{dst}}{wakeUpUntil})
  425. || $modules{MAX}{defptr}{$packet->{dst}}{wakeUpUntil} < gettimeofday()))) ? 1 : 0;
  426. #Send to CUL
  427. my ($credit10ms) = (CommandGet("","$hash->{IODev}{NAME} credit10ms") =~ /[^ ]* [^ ]* => (.*)/);
  428. if(!defined($credit10ms) || $credit10ms eq "No answer") {
  429. Log3 $hash, 1, "Error in CUL_MAX_SendQueueHandler: CUL $hash->{IODev}{NAME} did not answer request for current credits. Waiting 5 seconds.";
  430. $timeout += 5;
  431. } else {
  432. # We need 1000ms for preamble + len in bits (=hex len * 4) ms for payload. Divide by 10 to get credit10ms units
  433. # keep this in sync with culfw's code in clib/rf_moritz.c!
  434. my $necessaryCredit = ceil(100*$needPreamble + (length($packet->{packet})*4)/10);
  435. Log3 $hash, 5, "needPreamble: $needPreamble, necessaryCredit: $necessaryCredit, credit10ms: $credit10ms";
  436. if( defined($credit10ms) && $credit10ms < $necessaryCredit ) {
  437. my $waitTime = $necessaryCredit-$credit10ms; #we get one credit10ms every second
  438. $timeout += $waitTime + 1;
  439. Log3 $hash, 2, "CUL_MAX_SendQueueHandler: Not enough credit! credit10ms is $credit10ms, but we need $necessaryCredit. Waiting $waitTime seconds. Currently " . @{$hash->{sendQueue}} . " messages are waiting to be sent.";
  440. } else {
  441. #Update TimeInformation payload. It should reflect the current time when sending,
  442. #not the time when it was enqueued. A low credit10ms can defer such a packet for multiple
  443. #minutes
  444. if( $msgId2Cmd{substr($packet->{packet},6,2)} eq "TimeInformation" ) {
  445. Log3 $hash, 5, "Updating TimeInformation payload";
  446. substr($packet->{packet},22) = CUL_MAX_GetTimeInformationPayload();
  447. }
  448. IOWrite($hash, "", ($needPreamble ? "Zs" : "Zf") . $packet->{packet});
  449. $packet->{sent} = 1;
  450. $packet->{sentTime} = gettimeofday();
  451. if(!defined($packet->{retryCnt})){
  452. $packet->{retryCnt} = $maxRetryCnt;
  453. }
  454. $timeout += 0.5; #recheck for Ack
  455. }
  456. } # $credit10ms ne "No answer"
  457. } elsif( $packet->{sent} == 1 ) { #Already sent it, got no Ack
  458. if( $packet->{sentTime} + $ackTimeout < gettimeofday() ) {
  459. # ackTimeout exceeded
  460. if( $packet->{retryCnt} > 0 ) {
  461. Log3 $hash, 5, "CUL_MAX_SendQueueHandler: Retry $packet->{dst} for $packet->{packet} count: $packet->{retryCnt}";
  462. $packet->{sent} = 0;
  463. $packet->{retryCnt}--;
  464. $timeout += 3;
  465. } else {
  466. Log3 $hash, 2, "CUL_MAX_SendQueueHandler: Missing ack from $packet->{dst} for $packet->{packet}";
  467. splice @{$hash->{sendQueue}}, $pktIdx, 1; #Remove from array
  468. readingsSingleUpdate($hash, "packetsLost", ReadingsVal($hash->{NAME}, "packetsLost", 0) + 1, 1);
  469. }
  470. } else {
  471. # Recheck for Ack
  472. $timeout += 0.5;
  473. }
  474. } elsif( $packet->{sent} == 2 ) { #Got ack
  475. if(defined($packet->{callbackParam})) {
  476. Dispatch($hash, "MAX,1,Ack$packet->{cmd},$packet->{dst},$packet->{callbackParam}", {});
  477. }
  478. splice @{$hash->{sendQueue}}, $pktIdx, 1; #Remove from array
  479. } elsif( $packet->{sent} == 3 ) { #Got nack
  480. splice @{$hash->{sendQueue}}, $pktIdx, 1; #Remove from array
  481. }
  482. return if(!@{$hash->{sendQueue}}); #everything done
  483. return if(defined($responseToShutterContact)); #this was not called from InternalTimer
  484. InternalTimer($timeout, "CUL_MAX_SendQueueHandler", $hash, 0);
  485. }
  486. sub
  487. CUL_MAX_GetTimeInformationPayload()
  488. {
  489. my ($sec,$min,$hour,$day,$mon,$year,$wday,$yday,$isdst) = localtime(time());
  490. $mon += 1; #make month 1-based
  491. #month encoding is just guessed
  492. #perls localtime gives years since 1900, and we need years since 2000
  493. return unpack("H*",pack("CCCCC", $year - 100, $day, $hour, $min | (($mon & 0x0C) << 4), $sec | (($mon & 0x03) << 6)));
  494. }
  495. sub
  496. CUL_MAX_SendTimeInformation(@)
  497. {
  498. my ($hash,$addr,$payload) = @_;
  499. $payload = CUL_MAX_GetTimeInformationPayload() if(!defined($payload));
  500. Log3 $hash, 5, "Broadcast time to $addr";
  501. CUL_MAX_Send($hash, "TimeInformation", $addr, $payload, flags => "04");
  502. }
  503. sub
  504. CUL_MAX_BroadcastTime(@)
  505. {
  506. my ($hash,$manual) = @_;
  507. my $payload = CUL_MAX_GetTimeInformationPayload();
  508. Log3 $hash, 5, "CUL_MAX_BroadcastTime: payload $payload ";
  509. my $i = 1;
  510. my @used_slots = ( 0, 0, 0, 0, 0, 0 );
  511. # First, lookup all thermstats for their current TimeInformationHour
  512. foreach my $addr (keys %{$modules{MAX}{defptr}}) {
  513. my $dhash = $modules{MAX}{defptr}{$addr};
  514. if(exists($dhash->{IODev}) && $dhash->{IODev} == $hash
  515. && $dhash->{type} =~ /.*Thermostat.*/ ) {
  516. my $h = ReadingsVal($dhash->{NAME},"TimeInformationHour","");
  517. $used_slots[$h]++ if( $h =~ /^[0-5]$/);
  518. }
  519. }
  520. foreach my $addr (keys %{$modules{MAX}{defptr}}) {
  521. my $dhash = $modules{MAX}{defptr}{$addr};
  522. #Check that
  523. #1. the MAX device dhash uses this MAX_CUL as IODev
  524. #2. the MAX device is a Wall/HeatingThermostat
  525. if(exists($dhash->{IODev}) && $dhash->{IODev} == $hash
  526. && $dhash->{type} =~ /.*Thermostat.*/
  527. && AttrVal($dhash->{NAME},"ignore","0") eq "0" ) {
  528. my $h = ReadingsVal($dhash->{NAME},"TimeInformationHour","");
  529. if( $h !~ /^[0-5]$/ ) {
  530. #Find the used_slot with the smallest number of entries
  531. $h = (sort { $used_slots[$a] cmp $used_slots[$b] } 0 .. 5)[0];
  532. readingsSingleUpdate($dhash, "TimeInformationHour", $h, 1);
  533. $used_slots[$h]++;
  534. }
  535. CUL_MAX_SendTimeInformation($hash, $addr, $payload) if( [gmtime()]->[2] % 6 == $h );
  536. }
  537. }
  538. #Check again in 1 hour if some thermostats with the right TimeInformationHour need updating
  539. InternalTimer(gettimeofday() + 3600, "CUL_MAX_BroadcastTime", $hash, 0) unless(defined($manual));
  540. }
  541. 1;
  542. =pod
  543. =begin html
  544. <a name="CUL_MAX"></a>
  545. <h3>CUL_MAX</h3>
  546. <ul>
  547. The CUL_MAX module interprets MAX! messages received by the CUL. It will be automatically created by autocreate, just make sure
  548. that you set the right rfmode like <code>attr CUL0 rfmode MAX</code>.<br>
  549. <br><br>
  550. <a name="CUL_MAXdefine"></a>
  551. <b>Define</b>
  552. <ul>
  553. <code>define &lt;name&gt; CUL_MAX &lt;addr&gt;</code>
  554. <br><br>
  555. Defines an CUL_MAX device of type &lt;type&gt; and rf address &lt;addr&gt. The rf address
  556. must not be in use by any other MAX device.
  557. </ul>
  558. <br>
  559. <a name="CUL_MAXset"></a>
  560. <b>Set</b>
  561. <ul>
  562. <li>pairmode<br>
  563. Sets the CUL_MAX into pairing mode for 60 seconds where it can be paired with
  564. other devices (Thermostats, Buttons, etc.). You also have to set the other device
  565. into pairing mode manually. (For Thermostats, this is pressing the "Boost" button
  566. for 3 seconds, for example).</li>
  567. <li>fakeSC &lt;device&gt; &lt;open&gt;<br>
  568. Sends a fake ShutterContactState message &lt;open&gt; must be 0 or 1 for
  569. "window closed" or "window opened". If the &lt;device&gt; has a non-zero groupId,
  570. the fake ShutterContactState message affects all devices with that groupId.
  571. Make sure you associate the target device(s) with fakeShutterContact beforehand.</li>
  572. <li>fakeWT &lt;device&gt; &lt;desiredTemperature&gt; &lt;measuredTemperature&gt;<br>
  573. Sends a fake WallThermostatControl message (parameters both may have one digit
  574. after the decimal point, for desiredTemperature it may only by 0 or 5).
  575. If the &lt;device&gt; has a non-zero groupId, the fake WallThermostatControl
  576. message affects all devices with that groupId. Make sure you associate the target
  577. device with fakeWallThermostat beforehand.</li>
  578. </ul>
  579. <br>
  580. <a name="CUL_MAXget"></a>
  581. <b>Get</b> <ul>N/A</ul><br>
  582. <a name="CUL_MAXattr"></a>
  583. <b>Attributes</b>
  584. <ul>
  585. <li><a href="#ignore">ignore</a></li><br>
  586. <li><a href="#do_not_notify">do_not_notify</a></li><br>
  587. <li><a href="#showtime">showtime</a></li><br>
  588. <li><a href="#loglevel">loglevel</a></li><br>
  589. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  590. </ul>
  591. <br>
  592. <a name="CUL_MAXevents"></a>
  593. <b>Generated events:</b>
  594. <ul>N/A</ul>
  595. <br>
  596. </ul>
  597. =end html
  598. =device
  599. =item summary Uses a CUL (or compatible) to control MAX! devices.
  600. =item summary_DE Benutzt einen CUL (oder kompatibles Gerät) um MAX! Geräte zu steuern.
  601. =begin html_DE
  602. <a name="CUL_MAX"></a>
  603. <h3>CUL_MAX</h3>
  604. <ul>
  605. Das Modul CUL_MAX wertet von einem CUL empfangene MAX! Botschaften aus.
  606. Es wird mit Hilfe von autocreate automatisch generiert, es muss nur sichergestellt
  607. werden, dass der richtige rfmode gesetzt wird, z.B. <code>attr CUL0 rfmode MAX</code>.<br>
  608. <br>
  609. <a name="CUL_MAXdefine"></a>
  610. <b>Define</b>
  611. <ul>
  612. <code>define &lt;name&gt; CUL_MAX &lt;addr&gt;</code>
  613. <br><br>
  614. Definiert ein CUL_MAX Ger&auml;t des Typs &lt;type&gt; und der Adresse &lt;addr&gt.
  615. Die Adresse darf nicht schon von einem anderen MAX! Ger&auml;t verwendet werden.
  616. </ul>
  617. <br>
  618. <a name="CUL_MAXset"></a>
  619. <b>Set</b>
  620. <ul>
  621. <li>pairmode<br>
  622. Versetzt den CUL_MAX f&uuml;r 60 Sekunden in den Pairing Modus, w&auml;hrend dieser Zeit
  623. kann das Ger&auml;t mit anderen Ger&auml;ten gepaart werden (Heizk&ouml;rperthermostate,
  624. Eco-Taster, etc.). Auch das zu paarende Ger&auml;t muss manuell in den Pairing Modus
  625. versetzt werden (z.B. beim Heizk&ouml;rperthermostat durch Dr&uuml;cken der "Boost"
  626. Taste f&uuml;r 3 Sekunden).</li>
  627. <li>fakeSC &lt;device&gt; &lt;open&gt;<br>
  628. Sendet eine fingierte <i>ShutterContactState</i> Meldung &lt;open&gt;, dies muss 0 bzw. 1 f&uuml;r
  629. "Fenster geschlossen" bzw. "Fenster offen" sein. Wenn das &lt;device&gt; eine Gruppen-ID
  630. ungleich Null hat, beeinflusst diese fingierte <i>ShutterContactState</i> Meldung alle Ger&auml;te
  631. mit dieser Gruppen-ID. Es muss sichergestellt werden, dass vorher alle Zielger&auml;te
  632. mit <i>fakeShutterContact</i> verbunden werden.</li>
  633. <li>fakeWT &lt;device&gt; &lt;desiredTemperature&gt; &lt;measuredTemperature&gt;<br>
  634. Sendet eine fingierte <i>WallThermostatControl</i> Meldung (beide Parameter k&ouml;nnen
  635. eine Nachkommastelle haben, f&uuml;r <i>desiredTemperature</i> darf die Nachkommastelle nur 0 bzw. 5 sein).
  636. Wenn das &lt;device&gt; eine Gruppen-ID ungleich Null hat, beeinflusst diese fingierte
  637. <i>WallThermostatControl</i> Meldung alle Ger&auml;te mit dieser Gruppen-ID.
  638. Es muss sichergestellt werden, dass vorher alle Zielger&auml;te
  639. mit <i>fakeWallThermostat</i> verbunden werden.</li>
  640. </ul>
  641. <br>
  642. <a name="CUL_MAXget"></a>
  643. <b>Get</b> <ul>N/A</ul><br>
  644. <a name="CUL_MAXattr"></a>
  645. <b>Attributes</b>
  646. <ul>
  647. <li><a href="#ignore">ignore</a></li><br>
  648. <li><a href="#do_not_notify">do_not_notify</a></li><br>
  649. <li><a href="#showtime">showtime</a></li><br>
  650. <li><a href="#loglevel">loglevel</a></li><br>
  651. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  652. </ul>
  653. <br>
  654. <a name="CUL_MAXevents"></a>
  655. <b>Events</b>
  656. <ul>N/A</ul>
  657. <br>
  658. </ul>
  659. =end html_DE
  660. =cut