00_MAXLAN.pm 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  1. ##############################################
  2. # $Id: 00_MAXLAN.pm 11307 2016-04-25 08:02:06Z rudolfkoenig $
  3. # Written by Matthias Gehre, M.Gehre@gmx.de, 2012-2013
  4. package main;
  5. use strict;
  6. use warnings;
  7. use MIME::Base64;
  8. use POSIX;
  9. use MaxCommon;
  10. sub MAXLAN_Parse($$);
  11. sub MAXLAN_Read($);
  12. sub MAXLAN_Write(@);
  13. sub MAXLAN_ReadSingleResponse($$);
  14. sub MAXLAN_SimpleWrite(@);
  15. sub MAXLAN_Poll($);
  16. sub MAXLAN_Send(@);
  17. sub MAXLAN_RequestConfiguration($$);
  18. sub MAXLAN_RemoveDevice($$);
  19. my $reconnect_interval = 60; #seconds
  20. #the time it takes after sending one command till we see its effect in the L: response
  21. my $roundtriptime = 3; #seconds
  22. my $read_timeout = 3; #seconds. How long to wait for an answer from the Cube over TCP/IP
  23. my $metadata_magic = 0x56;
  24. my $metadata_version = 2;
  25. my $defaultPollInterval = 60;
  26. sub
  27. MAXLAN_Initialize($)
  28. {
  29. my ($hash) = @_;
  30. require "$attr{global}{modpath}/FHEM/DevIo.pm";
  31. # Provider
  32. $hash->{ReadFn} = "MAXLAN_Read";
  33. $hash->{SetFn} = "MAXLAN_Set";
  34. $hash->{Clients} = ":MAX:";
  35. my %mc = (
  36. "1:MAX" => "^MAX",
  37. );
  38. $hash->{MatchList} = \%mc;
  39. # Normal devices
  40. $hash->{DefFn} = "MAXLAN_Define";
  41. $hash->{UndefFn} = "MAXLAN_Undef";
  42. $hash->{AttrList}= "do_not_notify:1,0 dummy:1,0 set-clock-on-init:1,0 " .
  43. "loglevel:0,1,2,3,4,5,6 addvaltrigger " .
  44. "timezone:CET-CEST,GMT-BST,EET-EEST,FET-FEST,MSK-MSD,GMT,CET,EET " .
  45. $readingFnAttributes;
  46. }
  47. #####################################
  48. sub
  49. MAXLAN_Define($$)
  50. {
  51. my ($hash, $def) = @_;
  52. my @a = split("[ \t][ \t]*", $def);
  53. if(@a < 3) {
  54. my $msg = "wrong syntax: define <name> MAXLAN ip[:port] [pollintervall [ondemand]]";
  55. Log3 $hash, 2, $msg;
  56. return $msg;
  57. }
  58. my $name = shift @a;
  59. shift @a;
  60. my $dev = shift @a;
  61. $dev .= ":62910" if($dev !~ m/:/ && $dev ne "none" && $dev !~ m/\@/);
  62. if($dev eq "none") {
  63. Log3 $hash, 1, "$name device is none, commands will be echoed only";
  64. $attr{$name}{dummy} = 1;
  65. return undef;
  66. }
  67. $hash->{INTERVAL} = $defaultPollInterval;
  68. $hash->{persistent} = 1;
  69. if(@a) {
  70. $hash->{INTERVAL} = shift @a;
  71. while(@a) {
  72. my $arg = shift @a;
  73. if($arg eq "ondemand") {
  74. $hash->{persistent} = 0;
  75. } else {
  76. my $msg = "unknown argument $arg";
  77. Log3 $hash, 1, $msg;
  78. return $msg;
  79. }
  80. }
  81. }
  82. $hash->{cubeTimeDifference} = 99999;
  83. $hash->{pairmode} = 0;
  84. $hash->{PARTIAL} = "";
  85. $hash->{DeviceName} = $dev;
  86. #This interface is shared with 14_CUL_MAX.pm
  87. $hash->{Send} = \&MAXLAN_Send;
  88. $hash->{RemoveDevice} = \&MAXLAN_RemoveDevice;
  89. #Wait until all device definitions have been loaded
  90. InternalTimer(gettimeofday()+1, "MAXLAN_Poll", $hash, 0);
  91. return undef;
  92. }
  93. sub
  94. MAXLAN_IsConnected($)
  95. {
  96. return 0 if(!exists($_[0]->{FD}));
  97. if(!defined($_[0]->{TCPDev})) {
  98. MAXLAN_Disconnect($_[0]);
  99. return 0;
  100. }
  101. return 1;
  102. }
  103. #Disconnects from the Cube. It is safe to call this when already disconnected.
  104. sub
  105. MAXLAN_Disconnect($)
  106. {
  107. my $hash = shift;
  108. Log3 $hash, 5, "MAXLAN_Disconnect";
  109. #All operations here are no-op if already disconnected
  110. DevIo_CloseDev($hash);
  111. RemoveInternalTimer($hash);
  112. }
  113. #Connects to the Cube. If already connected, disconnects first.
  114. #Returns undef of success, otherwise an error message
  115. sub
  116. MAXLAN_Connect($)
  117. {
  118. my $hash = shift;
  119. return undef if(MAXLAN_IsConnected($hash));
  120. delete($hash->{NEXT_OPEN}); #work around the connection rate limiter in DevIo
  121. DevIo_OpenDev($hash, 0, "");
  122. if(!MAXLAN_IsConnected($hash)) {
  123. my $msg = "MAXLAN_Connect: Could not connect";
  124. Log3 $hash, 2, $msg;
  125. return $msg;
  126. }
  127. my $ret;
  128. #Read initial configuration data
  129. $ret = MAXLAN_ExpectAnswer($hash,"H:");
  130. return "MAXLAN_Connect: $ret" if($ret);
  131. $ret = MAXLAN_ExpectAnswer($hash,"M:");
  132. return "MAXLAN_Connect: $ret" if($ret);
  133. #We first reset the IODev for all MAX devices using this MAXLAN as a backend.
  134. #Parsing the "C:" responses later on will set IODev correctly again.
  135. #This effectively removes IODev from all devices that are not longer paired to our Cube.
  136. foreach (%{$modules{MAX}{defptr}}) {
  137. $modules{MAX}{defptr}{$_}{IODev} = undef if(defined($modules{MAX}{defptr}{$_}{IODev}) and $modules{MAX}{defptr}{$_}{IODev} == $hash);
  138. }
  139. my $rmsg;
  140. do
  141. {
  142. #Receive one "C:" per device
  143. $rmsg = MAXLAN_ReadSingleResponse($hash, 1);
  144. return "MAXLAN_Connect: Error in ReadSingleResponse while waiting for C:" if(!defined($rmsg));
  145. MAXLAN_Parse($hash, $rmsg);
  146. } until($rmsg =~ m/^L:/);
  147. #At the end, the cube sends a "L:"
  148. #Handle deferred setting of time
  149. if(AttrVal($hash->{NAME},"set-clock-on-init","1") && ($hash->{cubeTimeDifference} > 1 || !$hash->{clockset})) {
  150. MAXLAN_Set($hash,$hash->{NAME},"clock");
  151. }
  152. return undef;
  153. }
  154. #####################################
  155. sub
  156. MAXLAN_Undef($$)
  157. {
  158. my ($hash, $arg) = @_;
  159. #MAXLAN_Write($hash,"q:"); #unnecessary
  160. MAXLAN_Disconnect($hash);
  161. return undef;
  162. }
  163. #####################################
  164. sub
  165. MAXLAN_Set($@)
  166. {
  167. my ($hash, $device, @a) = @_;
  168. return "\"set MAXLAN\" needs at least one parameter" if(@a < 1);
  169. my ($setting, @args) = @a;
  170. if($setting eq "pairmode"){
  171. if(@args > 0 and $args[0] eq "cancel") {
  172. MAXLAN_Write($hash,"x:", "N:");
  173. } else {
  174. my $duration = 60;
  175. $duration = $args[0] if(@args > 0);
  176. $hash->{pairmode} = 1;
  177. MAXLAN_Write($hash,"n:".sprintf("%04x",$duration));
  178. $hash->{STATE} = "pairing";
  179. }
  180. }elsif($setting eq "raw"){
  181. MAXLAN_Write($hash,$args[0]);
  182. }elsif($setting eq "clock") {
  183. #Set timezone from attribute
  184. #All strings are taken from MAX! software network analysis
  185. #Base64 hex decode of the CET strings gives eg.
  186. #CET[00][00][0a][00][03][00][00][0e][10]CEST[00][03][00][02][00][00][1c][20] for DST
  187. #CET[00][00][0a][00][03][00][00][0e][10]CEST[00][03][00][02][00][00][0e][10] for no DST
  188. #bytes 10-11 and 22-23 of each string appear to represent time offset from UTC in seconds
  189. #a guess is that bytes 5 & 17 represent month no.
  190. #All strings below appear to follow the same pattern & identical except for name & offset.
  191. #The currently set string appears at the end of the decoded C: device message for the Cube
  192. my $timezoneAttr = AttrVal($hash->{NAME},"timezone","CET-CEST");
  193. my %tz_list = ( #timezone & strings
  194. "GMT-BST" => "R01UAAAKAAMAAAAAQlNUAAADAAIAAA4Q", #DST strings
  195. "CET-CEST" => "Q0VUAAAKAAMAAA4QQ0VTVAADAAIAABwg",
  196. "EET-EEST" => "RUVUAAAKAAMAABwgRUVTVAADAAIAACow",
  197. "FET-FEST" => "RkVUAAAKAAMAACowRkVTVAADAAIAACow", #No DST for this region or next
  198. "MSK-MSD" => "TVNLAAAKAAMAADhATVNEAAADAAIAADhA",
  199. "GMT" => "R01UAAAKAAMAAAAAQlNUAAADAAIAAAAA", #No DST strings
  200. "CET" => "Q0VUAAAKAAMAAA4QQ0VTVAADAAIAAA4Q",
  201. "EET" => "RUVUAAAKAAMAABwgRUVTVAADAAIAABwg"
  202. );
  203. my $timezones;
  204. if(exists($tz_list{$timezoneAttr})) {
  205. $timezones = $tz_list{$timezoneAttr};
  206. Log3 $hash, 3, "MAX Cube is set to timezone $timezoneAttr";
  207. } else {
  208. Log3 $hash, 2, "ERROR: Timezone $timezoneAttr of MAX Cube is invalid. Using CET-CEST";
  209. $timezones = $tz_list{"CET-CEST"};
  210. }
  211. #From various sources Cube base time is year 2000, offset should perhaps be number
  212. #of secs diff between 1/Jan/1970 and 1/Jan/2000 ie. 946684800, ie. 26 secs diff
  213. #Occasional 1 min diffs seen in logs when close to minute rollover
  214. my $time = time()-946684800;
  215. my $rmsg = "v:".$timezones.",".sprintf("%08x",$time);
  216. my $ret = MAXLAN_Write($hash,$rmsg, "A:");
  217. $hash->{clockset} = 1;
  218. return $ret;
  219. }elsif($setting eq "factoryReset") {
  220. MAXLAN_RequestReset($hash);
  221. }elsif($setting eq "reconnect") {
  222. MAXLAN_Disconnect($hash);
  223. MAXLAN_Connect($hash) if($hash->{persistent});
  224. }elsif($setting eq "inject") {
  225. MAXLAN_Parse($hash,$args[0]);
  226. }else{
  227. return "Unknown argument $setting, choose one of pairmode raw clock factoryReset reconnect";
  228. }
  229. return undef;
  230. }
  231. #Returns error string if failed, undef on success
  232. sub
  233. MAXLAN_ExpectAnswer($$)
  234. {
  235. my ($hash,$expectedanswer) = @_;
  236. my $rmsg = MAXLAN_ReadSingleResponse($hash, 1);
  237. if(!defined($rmsg)) {
  238. my $msg = "MAXLAN_ExpectAnswer: Error while waiting for answer $expectedanswer";
  239. Log3 $hash, 1, $msg;
  240. return $msg;
  241. }
  242. my $ret = undef;
  243. if($rmsg !~ m/^$expectedanswer/) {
  244. Log3 $hash, 2, "MAXLAN_ExpectAnswer: Got unexpected response, expected $expectedanswer";
  245. MAXLAN_Parse($hash,$rmsg);
  246. return "Got unexpected response, expected $expectedanswer";
  247. }
  248. MAXLAN_Parse($hash,$rmsg);
  249. return undef;
  250. }
  251. #Reads single line from the Cube
  252. #blocks if waitForResponse is true
  253. #
  254. #returns undef, if an error occured,
  255. #otherwise the line
  256. sub
  257. MAXLAN_ReadSingleResponse($$)
  258. {
  259. my ($hash,$waitForResponse) = @_;
  260. return undef if(!MAXLAN_IsConnected($hash));
  261. my ($rin, $win, $ein, $rout, $wout, $eout);
  262. $rin = $win = $ein = '';
  263. vec($rin,fileno($hash->{TCPDev}),1) = 1;
  264. $ein = $rin;
  265. my $maxTime = gettimeofday()+$read_timeout;
  266. #Read until we have a complete line
  267. until($hash->{PARTIAL} =~ m/\n/) {
  268. #Check timeout
  269. if(gettimeofday() > $maxTime) {
  270. if($waitForResponse) {
  271. Log3 $hash, 1, "MAXLAN_ReadSingleResponse: timeout while reading from socket, disconnecting";
  272. MAXLAN_Disconnect($hash);
  273. }
  274. return undef;;
  275. }
  276. #Wait for data
  277. my $nfound = select($rout=$rin, $wout=$win, $eout=$ein, $read_timeout);
  278. if($nfound == -1) {
  279. Log3 $hash, 1, "MAXLAN_ReadSingleResponse: error during select, ret = $nfound";
  280. return undef;
  281. }
  282. last if($nfound == 0 and !$waitForResponse);
  283. next if($nfound == 0); #Sometimes select() returns early, just try again
  284. #Blocking read
  285. my $buf;
  286. my $res = sysread($hash->{TCPDev}, $buf, 256);
  287. if(!defined($res)){
  288. Log3 $hash, 1, "MAXLAN_ReadSingleResponse: error during read";
  289. return undef; #error occured
  290. }
  291. #Append data to partial data we got before
  292. $hash->{PARTIAL} .= $buf;
  293. }
  294. my $rmsg;
  295. ($rmsg,$hash->{PARTIAL}) = split("\n", $hash->{PARTIAL}, 2);
  296. $rmsg =~ s/\r//; #remove \r
  297. return $rmsg;
  298. }
  299. my %lhash;
  300. #####################################
  301. #Sends given msg and checks for/parses the answer
  302. #returns undef on success
  303. sub
  304. MAXLAN_Write(@)
  305. {
  306. my ($hash,$msg,$expectedAnswer) = @_;
  307. my $ret = undef;
  308. $ret = MAXLAN_Connect($hash); #It's a no-op if already connected
  309. return "MAXLAN_Write: $ret" if($ret);
  310. $ret = MAXLAN_SimpleWrite($hash, $msg);
  311. return "MAXLAN_Write: $ret" if($ret);
  312. if($expectedAnswer) {
  313. $ret = MAXLAN_ExpectAnswer($hash, $expectedAnswer);
  314. return "MAXLAN_Write: $ret" if($ret);
  315. }
  316. MAXLAN_Disconnect($hash) if(!$hash->{persistent} && !$hash->{pairmode});
  317. return undef;
  318. }
  319. #####################################
  320. # called from the global loop, when the select for hash->{FD} reports data
  321. sub
  322. MAXLAN_Read($)
  323. {
  324. my ($hash) = @_;
  325. while(1) {
  326. my $rmsg = MAXLAN_ReadSingleResponse($hash, 0);
  327. last if(!$rmsg);
  328. # The Msg N: .... is the only one that may come spontanously from
  329. # the cube while we are in pairmode
  330. Log3 $hash, 2, "Unsolicated response from Cube: $rmsg" unless($hash->{pairmode} and substr($rmsg,0,2) eq "N:");
  331. MAXLAN_Parse($hash, $rmsg);
  332. }
  333. }
  334. sub
  335. MAXLAN_SendMetadata($)
  336. {
  337. my $hash = shift;
  338. if(defined($hash->{metadataVersionMismatch})){
  339. Log3 $hash, 3,"MAXLAN_SendMetadata: current version of metadata unexpected, not overwriting!";
  340. return;
  341. }
  342. my $maxNameLength = 32;
  343. my $maxGroupCount = 20;
  344. my $maxDeviceCount = 140;
  345. my @groups = @{$hash->{groups}};
  346. my @devices = @{$hash->{devices}};
  347. if(@groups > $maxGroupCount || @devices > $maxDeviceCount) {
  348. Log3 $hash, 1, "MAXLAN_SendMetadata: you got more than $maxGroupCount groups or $maxDeviceCount devices";
  349. return;
  350. }
  351. my $metadata = pack("CC",$metadata_magic,$metadata_version);
  352. $metadata .= pack("C",scalar(@groups));
  353. foreach(@groups){
  354. if(length($_->{name}) > $maxNameLength) {
  355. Log3 $hash, 1, "Group name $_->{name} is too long, maximum of $maxNameLength characters allowed";
  356. return;
  357. }
  358. $metadata .= pack("CC/aH6",$_->{id}, $_->{name}, $_->{masterAddr});
  359. }
  360. $metadata .= pack("C",scalar(@devices));
  361. foreach(@devices){
  362. if(length($_->{name}) > $maxNameLength) {
  363. Log3 $hash, 1, "Device name $_->{name} is too long, maximum of $maxNameLength characters allowed";
  364. return;
  365. }
  366. $metadata .= pack("CH6a[10]C/aC",$_->{type}, $_->{addr}, $_->{serial}, $_->{name}, $_->{groupid});
  367. }
  368. $metadata .= pack("C",1); #dstenables, should always be 1
  369. my $blocksize = 1900;
  370. $metadata = encode_base64($metadata,"");
  371. my $numpackages = ceil(length($metadata)/$blocksize);
  372. for(my $i=0;$i < $numpackages; $i++) {
  373. my $package = substr($metadata,$i*$blocksize,$blocksize);
  374. return MAXLAN_Write($hash,"m:".sprintf("%02d",$i).",".$package, "A:");
  375. }
  376. }
  377. # Maps [9,61] -> [off,5.0,5.5,...,30.0,on]
  378. sub
  379. MAXLAN_ExtractTemperature($)
  380. {
  381. return $_[0] == 61 ? "on" : ($_[0] == 9 ? "off" : sprintf("%2.1f",$_[0]/2));
  382. }
  383. sub
  384. MAXLAN_Parse($$)
  385. {
  386. #http://www.domoticaforum.eu/viewtopic.php?f=66&t=6654
  387. my ($hash, $rmsg) = @_;
  388. my $name = $hash->{NAME};
  389. Log3 $hash, 5, "Msg $rmsg";
  390. my $cmd = substr($rmsg,0,1); # get leading char
  391. my @args = split(',', substr($rmsg,2));
  392. if ($cmd eq 'H'){ #Hello
  393. $hash->{serial} = $args[0];
  394. $hash->{addr} = $args[1];
  395. $modules{MAX}{defptr}{$hash->{addr}} = $hash;
  396. $hash->{fwversion} = $args[2];
  397. my $dutycycle = 0;
  398. if(@args > 5){
  399. $dutycycle = hex($args[5]);
  400. $hash->{dutycycle} = sprintf("%3.0f %%", $dutycycle);
  401. readingsSingleUpdate( $hash, 'dutycycle', $dutycycle, 1 );
  402. }
  403. my $freememory = 0;
  404. if(@args > 6){
  405. $freememory = $args[6];
  406. }
  407. my $cubedatetime = {
  408. year => 2000+hex(substr($args[7],0,2)),
  409. month => hex(substr($args[7],2,2)),
  410. day => hex(substr($args[7],4,2)),
  411. hour => hex(substr($args[8],0,2)),
  412. minute => hex(substr($args[8],2,2)),
  413. };
  414. $hash->{clockset} = hex($args[9]);
  415. #$cubedatetime field is only valid if $clockset is 1
  416. if($hash->{clockset}) {
  417. my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
  418. my $difference = ((((($cubedatetime->{year} - $year-1900)*12
  419. + $cubedatetime->{month} - $mon-1)*30
  420. + $cubedatetime->{day} - $mday)*24
  421. + $cubedatetime->{hour} - $hour)*60
  422. + $cubedatetime->{minute} - $min);
  423. $hash->{cubeTimeDifference} = $difference;
  424. if($difference > 1) {
  425. Log3 $hash, 2, "MAXLAN_Parse: Cube thinks it is $cubedatetime->{day}.$cubedatetime->{month}.$cubedatetime->{year} $cubedatetime->{hour}:$cubedatetime->{minute}";
  426. Log3 $hash, 2, "MAXLAN_Parse: Time difference is $difference minutes";
  427. }
  428. } else {
  429. Log3 $hash, 2, "MAXLAN_Parse: Cube has no time set";
  430. }
  431. Log3 $hash, 5, "MAXLAN_Parse: Got hello, connection ip $args[4], duty cycle $dutycycle, freememory $freememory, clockset $hash->{clockset}";
  432. } elsif($cmd eq 'M') {
  433. #Metadata, this is basically a readwrite part of the cube's memory.
  434. #I don't think that the cube interprets any of that data.
  435. #One can write to that memory with the "m:" command
  436. #The actual configuration comes with the "C:" response and can be set
  437. #with the "s:" command.
  438. return $name if(@args < 3); #On virgin devices, we get nothing, not even $magic$version$numgroups$numdevices
  439. my $bindata = decode_base64($args[2]);
  440. #$version is the version the serialized data format I guess
  441. my ($magic,$version,$numgroups,@groupsdevices);
  442. eval {
  443. ($magic,$version,$numgroups,@groupsdevices) = unpack("CCCXC/(CC/aH6)C/(CH6a[10]C/aC)C",$bindata);
  444. 1;
  445. } or do {
  446. Log3 $hash, 1, "MAXLAN_Parse: Metadata response is malformed!";
  447. return $name;
  448. };
  449. if($magic != $metadata_magic || $version != $metadata_version) {
  450. Log3 $hash, 3, "MAXLAN_Parse: magic $magic/version $version are not $metadata_magic/$metadata_version as expected";
  451. $hash->{metadataVersionMismatch} = 1;
  452. }
  453. my $daylightsaving = pop(@groupsdevices); #should be always true (=0x01)
  454. my $i;
  455. $hash->{groups} = ();
  456. for($i=0;$i<3*$numgroups;$i+=3){
  457. $hash->{groups}[@{$hash->{groups}}]->{id} = $groupsdevices[$i];
  458. $hash->{groups}[-1]->{name} = $groupsdevices[$i+1];
  459. $hash->{groups}[-1]->{masterAddr} = $groupsdevices[$i+2];
  460. }
  461. #After a device is freshly paired, it does not appear in this metadata response,
  462. #we first have to set some metadata for it
  463. $hash->{devices} = ();
  464. for(;$i<@groupsdevices;$i+=5){
  465. $hash->{devices}[@{$hash->{devices}}]->{type} = $groupsdevices[$i];
  466. $hash->{devices}[-1]->{addr} = $groupsdevices[$i+1];
  467. $hash->{devices}[-1]->{serial} = $groupsdevices[$i+2];
  468. $hash->{devices}[-1]->{name} = $groupsdevices[$i+3];
  469. $hash->{devices}[-1]->{groupid} = $groupsdevices[$i+4];
  470. }
  471. }elsif($cmd eq "C"){#Configuration
  472. return $name if(@args < 2);
  473. my $bindata = decode_base64($args[1]);
  474. if(length($bindata) < 18) {
  475. Log3 $hash, 1, "Invalid C: response, not enough data";
  476. return $name;
  477. }
  478. #Parse the first 18 bytes, those are send for every device
  479. my ($len,$addr,$devicetype,$groupid,$firmware,$testresult,$serial) = unpack("CH6CCCCa[10]", $bindata);
  480. Log3 $hash, 5, "MAXLAN_Parse: len $len, addr $addr, devicetype $devicetype, firmware $firmware, testresult $testresult, groupid $groupid, serial $serial";
  481. $len = $len+1; #The len field itself was not counted
  482. Dispatch($hash, "MAX,1,define,$addr,$device_types{$devicetype},$serial,$groupid", {}) if($device_types{$devicetype} ne "Cube");
  483. #Set firmware and testresult on device
  484. my $dhash = $modules{MAX}{defptr}{$addr};
  485. if(defined($dhash)) {
  486. readingsBeginUpdate($dhash);
  487. readingsBulkUpdate($dhash, "firmware", sprintf("%u.%u",int($firmware/16),$firmware%16));
  488. readingsBulkUpdate($dhash, "testresult", $testresult);
  489. readingsEndUpdate($dhash, 1);
  490. }
  491. if($len != length($bindata)) {
  492. Dispatch($hash, "MAX,1,Error,$addr,Parts of configuration are missing", {});
  493. return $name;
  494. }
  495. #devicetype: Cube = 0, HeatingThermostat = 1, HeatingThermostatPlus = 2, WallMountedThermostat = 3, ShutterContact = 4, PushButton = 5
  496. #Seems that ShutterContact does not have any configdata
  497. if($device_types{$devicetype} eq "Cube"){
  498. #TODO: there is a lot of data left to interpret
  499. }elsif($device_types{$devicetype} =~ /HeatingThermostat.*/){
  500. my ($comforttemp,$ecotemp,$maxsetpointtemp,$minsetpointtemp,$tempoffset,$windowopentemp,$windowopendur,$boost,$decalcifiction,$maxvalvesetting,$valveoffset,$weekprofile) = unpack("CCCCCCCCCCCH364",substr($bindata,18));
  501. my $boostValve = ($boost & 0x1F) * 5;
  502. my $boostDuration = $boost >> 5;
  503. $comforttemp = MAXLAN_ExtractTemperature($comforttemp); #convert to degree celcius
  504. $ecotemp = MAXLAN_ExtractTemperature($ecotemp); #convert to degree celcius
  505. $tempoffset = $tempoffset/2.0-3.5; #convert to degree
  506. $maxsetpointtemp = MAXLAN_ExtractTemperature($maxsetpointtemp);
  507. $minsetpointtemp = MAXLAN_ExtractTemperature($minsetpointtemp);
  508. $windowopentemp = MAXLAN_ExtractTemperature($windowopentemp);
  509. $windowopendur *= 5;
  510. $maxvalvesetting = int($maxvalvesetting*100/255 + 0.5); # + 0.5 for correct rounding
  511. $valveoffset = int($valveoffset*100/255 + 0.5); # + 0.5 for correct rounding
  512. my $decalcDay = ($decalcifiction >> 5) & 0x07;
  513. my $decalcTime = $decalcifiction & 0x1F;
  514. Log3 $hash, 5, "comfortemp $comforttemp, ecotemp $ecotemp, boostValve $boostValve, boostDuration $boostDuration, tempoffset $tempoffset, minsetpointtemp $minsetpointtemp, maxsetpointtemp $maxsetpointtemp, windowopentemp $windowopentemp, windowopendur $windowopendur";
  515. Dispatch($hash, "MAX,1,HeatingThermostatConfig,$addr,$ecotemp,$comforttemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile,$boostValve,$boostDuration,$tempoffset,$windowopentemp,$windowopendur,$maxvalvesetting,$valveoffset,$decalcDay,$decalcTime", {});
  516. }elsif($device_types{$devicetype} eq "WallMountedThermostat"){
  517. my ($comforttemp,$ecotemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile,$tempoffset,$windowopentemp,$boost) = unpack("CCCCH364CCC",substr($bindata,18));
  518. $comforttemp = MAXLAN_ExtractTemperature($comforttemp);
  519. $ecotemp = MAXLAN_ExtractTemperature($ecotemp);
  520. $maxsetpointtemp = MAXLAN_ExtractTemperature($maxsetpointtemp);
  521. $minsetpointtemp = MAXLAN_ExtractTemperature($minsetpointtemp);
  522. Log3 $hash, 5, "comfortemp $comforttemp, ecotemp $ecotemp, minsetpointtemp $minsetpointtemp, maxsetpointtemp $maxsetpointtemp";
  523. if(defined($tempoffset)) { #With firmware 18 (opposed to firmware 16)
  524. $tempoffset = $tempoffset/2.0-3.5; #convert to degree
  525. my $boostValve = ($boost & 0x1F) * 5;
  526. my $boostDuration = $boost >> 5;
  527. $windowopentemp = MAXLAN_ExtractTemperature($windowopentemp);
  528. Log3 $hash, 5, "tempoffset $tempoffset, boostValve $boostValve, boostDuration $boostDuration, windowOpenTemp $windowopentemp";
  529. Dispatch($hash, "MAX,1,WallThermostatConfig,$addr,$ecotemp,$comforttemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile,$boostValve,$boostDuration,$tempoffset,$windowopentemp", {});
  530. } else {
  531. Dispatch($hash, "MAX,1,WallThermostatConfig,$addr,$ecotemp,$comforttemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile", {});
  532. }
  533. }elsif($device_types{$devicetype} eq "ShutterContact"){
  534. Log3 $hash, 2, "MAXLAN_Parse: ShutterContact send some configuration, but none was expected" if($len > 18);
  535. }elsif($device_types{$devicetype} eq "PushButton"){
  536. Log3 $hash, 2, "MAXLAN_Parse: PushButton send some configuration, but none was expected" if($len > 18);
  537. }else{ #TODO
  538. Log3 $hash, 2, "MAXLAN_Parse: Got configdata for unimplemented devicetype $devicetype";
  539. }
  540. #Clear Error
  541. Dispatch($hash, "MAX,1,Error,$addr", {}) if($addr ne $hash->{addr}); #don't clear own error
  542. #Check if it is already recorded in devices
  543. my $found = 0;
  544. foreach (@{$hash->{devices}}) {
  545. $found = 1 if($_->{addr} eq $addr);
  546. }
  547. #Add device if it is not already known and not the cube itself
  548. if(!$found && $devicetype != 0){
  549. $hash->{devices}[@{$hash->{devices}}]->{type} = $devicetype;
  550. $hash->{devices}[-1]->{addr} = $addr;
  551. $hash->{devices}[-1]->{serial} = $serial;
  552. $hash->{devices}[-1]->{name} = "no name";
  553. $hash->{devices}[-1]->{groupid} = $groupid;
  554. }
  555. }elsif($cmd eq 'L'){#List
  556. my $bindata = "";
  557. $bindata = decode_base64($args[0]) if(@args > 0);
  558. #The L command consists of blocks of states (one for each device)
  559. while(length($bindata)){
  560. my ($len,$addr,$errCmd,$bits1) = unpack("CH6H2a",$bindata);
  561. $errCmd = uc($errCmd);
  562. my $unkbit1 = vec($bits1,0,1);
  563. my $initialized = vec($bits1,1,1); #I never saw this beeing 0
  564. my $answer = vec($bits1,2,1); #answer to what?
  565. my $error = vec($bits1,3,1); # if 1 then see errframetype
  566. my $valid = vec($bits1,4,1); #is the status following the common header valid
  567. my $unkbit2 = vec($bits1,5,2);
  568. my $unkbit3 = vec($bits1,7,1);
  569. Log3 $hash, 5, "len $len, addr $addr, initialized $initialized, valid $valid, error $error, errCmd $errCmd, answer $answer, unkbit ($unkbit1,$unkbit2,$unkbit3)";
  570. my $payload = unpack("H*",substr($bindata,6,$len-6+1)); #+1 because the len field is not counted
  571. if($valid) {
  572. my $shash = $modules{MAX}{defptr}{$addr};
  573. if(!$shash) {
  574. Log3 $hash, 2, "Got List response for undefined device with addr $addr";
  575. }elsif($shash->{type} =~ /HeatingThermostat.*/){
  576. Dispatch($hash, "MAX,1,ThermostatState,$addr,$payload", {});
  577. }elsif($shash->{type} eq "WallMountedThermostat"){
  578. Dispatch($hash, "MAX,1,WallThermostatState,$addr,$payload", {});
  579. }elsif($shash->{type} eq "ShutterContact"){
  580. Dispatch($hash, "MAX,1,ShutterContactState,$addr,$payload", {});
  581. }elsif($shash->{type} eq "PushButton"){
  582. Dispatch($hash, "MAX,1,PushButtonState,$addr,$payload", {});
  583. }else{
  584. Log3 $hash, 2, "MAXLAN_Parse: Got status for unimplemented device type $shash->{type}";
  585. }
  586. }
  587. my $dhash = $modules{MAX}{defptr}{$addr};
  588. if(defined($dhash)) {
  589. readingsBeginUpdate($dhash);
  590. readingsBulkUpdate($dhash, "MAXLAN_initialized", $initialized);
  591. readingsBulkUpdate($dhash, "MAXLAN_error", $error);
  592. readingsBulkUpdate($dhash, "MAXLAN_errorInCommand", $error ? (exists($msgId2Cmd{$errCmd}) ? $msgId2Cmd{$errCmd} : $errCmd) : "");
  593. readingsBulkUpdate($dhash, "MAXLAN_valid", $valid);
  594. readingsBulkUpdate($dhash, "MAXLAN_isAnswer", $answer);
  595. readingsEndUpdate($dhash, 1);
  596. if($error) {
  597. MAXLAN_Write($hash,"r:01,".encode_base64(pack("H*",$addr),""), "S:");
  598. }
  599. }
  600. $bindata=substr($bindata,$len+1); #+1 because the len field is not counted
  601. } # while(length($bindata))
  602. }elsif($cmd eq "N"){#New device paired
  603. if(@args==0){
  604. $hash->{STATE} = "initalized"; #pairing ended
  605. $hash->{pairmode} = 0;
  606. return $name;
  607. }
  608. my ($type, $addr, $serial) = unpack("CH6a[10]", decode_base64($args[0]));
  609. Log3 $hash, 2, "MAXLAN_Parse: Paired new device, type $device_types{$type}, addr $addr, serial $serial";
  610. Dispatch($hash, "MAX,1,define,$addr,$device_types{$type},$serial,0", {});
  611. #After a device has been paired, it automatically appears in the "L" and "C" commands,
  612. MAXLAN_RequestConfiguration($hash,$addr);
  613. } elsif($cmd eq "A"){#Acknowledged
  614. } elsif($cmd eq "S"){#Response to s:
  615. $hash->{dutycycle} = hex($args[0]); #number of command send over the air
  616. readingsSingleUpdate( $hash, 'dutycycle', $hash->{dutycycle}, 1 );
  617. my $discarded = $args[1];
  618. $hash->{freememoryslot} = hex($args[2]);
  619. Log3 $hash, 5, "MAXLAN_Parse: dutycyle $hash->{dutycycle}, freememoryslot $hash->{freememoryslot}";
  620. Log3 $hash, 3, "MAXLAN_Parse: 1% rule: we sent too much, cmd is now in queue" if($hash->{dutycycle} == 100 && $hash->{freememoryslot} > 0);
  621. Log3 $hash, 2, "MAXLAN_Parse: 1% rule: we sent too much, queue is full" if($hash->{dutycycle} == 100 && $hash->{freememoryslot} == 0);
  622. Log3 $hash, 2, "MAXLAN_Parse: Command was discarded" if($discarded);
  623. } else {
  624. Log3 $hash, 2, "MAXLAN_Parse: Unknown command $cmd";
  625. }
  626. return $name;
  627. }
  628. ########################
  629. #Returns undef on sucess
  630. sub
  631. MAXLAN_SimpleWrite(@)
  632. {
  633. my ($hash, $msg) = @_;
  634. my $name = $hash->{NAME};
  635. Log3 $hash, 5, 'MAXLAN_SimpleWrite: '.$msg;
  636. return "MAXLAN_SimpleWrite: Not connected" if(!MAXLAN_IsConnected($hash));
  637. $msg .= "\r\n";
  638. my $ret = syswrite($hash->{TCPDev}, $msg);
  639. #TODO: none of those conditions detect if the connection is actually lost!
  640. if(!$hash->{TCPDev} || !defined($ret) || !$hash->{TCPDev}->connected) {
  641. Log3 $hash, 1, 'MAXLAN_SimpleWrite failed';
  642. MAXLAN_Disconnect($hash);
  643. return "MAXLAN_SimpleWrite: syswrite failed";
  644. }
  645. return undef;
  646. }
  647. ########################
  648. sub
  649. MAXLAN_DoInit($)
  650. {
  651. my ($hash) = @_;
  652. return undef;
  653. }
  654. #Returns undef on success
  655. sub
  656. MAXLAN_RequestList($)
  657. {
  658. my $hash = shift;
  659. return MAXLAN_Write($hash, "l:", "L:");
  660. }
  661. #####################################
  662. sub
  663. MAXLAN_Poll($)
  664. {
  665. my $hash = shift;
  666. my $ret = undef;
  667. if(MAXLAN_IsConnected($hash)) {
  668. $ret = MAXLAN_RequestList($hash);
  669. } else {
  670. #Connecting gives us a RequestList for free
  671. $ret = MAXLAN_Connect($hash);
  672. }
  673. if($ret) {
  674. #Connecting failed/Got invalid answer
  675. MAXLAN_Disconnect($hash);
  676. InternalTimer(gettimeofday()+$reconnect_interval, "MAXLAN_Poll", $hash, 0);
  677. return;
  678. }
  679. MAXLAN_Disconnect($hash) if(!$hash->{persistent} && !$hash->{pairmode});
  680. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MAXLAN_Poll", $hash, 0);
  681. }
  682. #This only works for a device that got just paired
  683. sub
  684. MAXLAN_RequestConfiguration($$)
  685. {
  686. my ($hash,$addr) = @_;
  687. return MAXLAN_Write($hash,"c:$addr", "C:");
  688. }
  689. sub
  690. MAXLAN_Send(@)
  691. {
  692. my ($hash, $cmd, $dst, $payload, %opts) = @_;
  693. my $flags = "00";
  694. my $groupId = "00";
  695. my $callbackParam = undef;
  696. $flags = $opts{flags} if(exists($opts{flags}));
  697. $groupId = $opts{groupId} if(exists($opts{groupId}));
  698. Log3 $hash, 2, "MAXLAN_Send: MAXLAN does not support src" if(exists($opts{src}));
  699. $callbackParam = $opts{callbackParam} if(exists($opts{callbackParam}));
  700. $payload = pack("H*","00".$flags.$msgCmd2Id{$cmd}."000000".$dst.$groupId.$payload);
  701. my $ret = MAXLAN_Write($hash,"s:".encode_base64($payload,""), "S:");
  702. #TODO: actually check return value
  703. if(defined($opts{callbackParam})) {
  704. Dispatch($hash, "MAX,1,Ack$cmd,$dst,$opts{callbackParam}", {});
  705. }
  706. #Reschedule a poll in the near future after the cube will
  707. #have gotten an answer
  708. RemoveInternalTimer($hash);
  709. InternalTimer(gettimeofday()+$roundtriptime, "MAXLAN_Poll", $hash, 0);
  710. return $ret;
  711. }
  712. #Resets the cube, i.e. do a factory reset. All pairings will be lost from the cube
  713. #(but you will have to manually reset each individual device.
  714. sub
  715. MAXLAN_RequestReset($)
  716. {
  717. my $hash = shift;
  718. return MAXLAN_Write($hash,"a:", "A:");
  719. }
  720. #Remove the device from the cube, i.e. deletes the pairing
  721. sub
  722. MAXLAN_RemoveDevice($$)
  723. {
  724. my ($hash,$addr) = @_;
  725. #This does a factoryReset on the Device
  726. my $ret = MAXLAN_Write($hash,"t:1,1,".encode_base64(pack("H6",$addr),""), "A:");
  727. if(!defined($ret)) { #success
  728. #The device is not longer accessable by the Cube
  729. $modules{MAX}{defptr}{$addr}{IODev} = undef;
  730. }
  731. return $ret;
  732. }
  733. 1;
  734. =pod
  735. =begin html
  736. <a name="MAXLAN"></a>
  737. <h3>MAXLAN</h3>
  738. <ul>
  739. The MAXLAN is the fhem module for the eQ-3 MAX! Cube LAN Gateway.
  740. <br><br>
  741. The fhem module makes the MAX! "bus" accessible to fhem, automatically detecting paired MAX! devices. It also represents properties of the MAX! Cube. The other devices are handled by the <a href="#MAX">MAX</a> module, which uses this module as its backend.<br>
  742. <br>
  743. <a name="MAXLANdefine"></a>
  744. <b>Define</b>
  745. <ul>
  746. <code>define &lt;name&gt; MAXLAN &lt;ip-address&gt;[:port] [&lt;pollintervall&gt; [ondemand]]</code><br>
  747. <br>
  748. port is 62910 by default. (If your Cube listens on port 80, you have to update the firmware with
  749. the official MAX! software).
  750. If the ip-address is called none, then no device will be opened, so you
  751. can experiment without hardware attached.<br>
  752. The optional parameter &lt;pollintervall&gt; defines the time in seconds between each polling of data from the cube.<br>
  753. You may provide the option <code>ondemand</code> forcing the MAXLAN module to tear-down the connection as often as possible
  754. thus making the cube usable by other applications or the web portal.
  755. </ul>
  756. <br>
  757. <a name="MAXLANset"></a>
  758. <b>Set</b>
  759. <ul>
  760. <li>pairmode [&lt;n&gt;,cancel]<br>
  761. Sets the cube into pairing mode for &lt;n&gt; seconds (default is 60s ) where it can be paired with other devices (Thermostats, Buttons, etc.). You also have to set the other device into pairing mode manually. (For Thermostats, this is pressing the "Boost" button for 3 seconds, for example).
  762. Setting pairmode to "cancel" puts the cube out of pairing mode.</li>
  763. <li>raw &lt;data&gt;<br>
  764. Sends the raw &lt;data&gt; to the cube.</li>
  765. <li>clock<br>
  766. Sets the internal clock in the cube to the current system time of fhem's machine (uses timezone attribute if set). You can add<br>
  767. <code>attr ml set-clock-on-init</code><br>
  768. to your fhem.cfg to do this automatically on startup.</li>
  769. <li>factorReset<br>
  770. Reset the cube to factory defaults.</li>
  771. <li>reconnect<br>
  772. FHEM will terminate the current connection to the cube and then reconnect. This allows
  773. re-reading the configuration data from the cube, as it is only send after establishing a new connection.</li>
  774. </ul>
  775. <br>
  776. <a name="MAXLANget"></a>
  777. <b>Get</b>
  778. <ul>
  779. N/A
  780. </ul>
  781. <br>
  782. <br>
  783. <a name="MAXLANattr"></a>
  784. <b>Attributes</b>
  785. <ul>
  786. <li>set-clock-on-init<br>
  787. (Default: 1). Automatically call "set clock" after connecting to the cube.</li>
  788. <li><a href="#do_not_notify">do_not_notify</a></li>
  789. <li><a href="#attrdummy">dummy</a></li>
  790. <li><a href="#loglevel">loglevel</a></li>
  791. <li><a href="#addvaltrigger">addvaltrigger</a></li>
  792. <li>timezone<br>
  793. (Default: CET-CEST). Set MAX Cube timezone (requires "set clock" to take effect).<br>
  794. <b>NB.</b>Cube time and cubeTimeDifference will not change until Cube next connects.<br>
  795. <ul>
  796. <li>GMT-BST - (UTC +0, UTC+1)</li>
  797. <li>CET-CEST - (UTC +1, UTC+2)</li>
  798. <li>EET-EEST - (UTC +2, UTC+3)</li>
  799. <li>FET-FEST - (UTC +3)</li>
  800. <li>MSK-MSD - (UTC +4)</li>
  801. </ul>
  802. The following are settings with no DST (daylight saving time)
  803. <ul>
  804. <li>GMT - (UTC +0)</li>
  805. <li>CET - (UTC +1)</li>
  806. <li>EET - (UTC +2)</li>
  807. </ul>
  808. </li>
  809. </ul>
  810. </ul>
  811. =end html
  812. =cut