38_CO20.pm 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. # $Id: 38_CO20.pm 14433 2017-05-30 20:29:44Z moises $
  2. # basic idea from http://code.google.com/p/airsensor-linux-usb
  3. package main;
  4. use strict;
  5. use warnings;
  6. BEGIN {
  7. $ENV{CFLAGS} = '';
  8. $ENV{CPPFLAGS} = '';
  9. $ENV{LDFLAGS} = '';
  10. }
  11. use Device::USB;
  12. sub
  13. CO20_Initialize($)
  14. {
  15. my ($hash) = @_;
  16. $hash->{DefFn} = "CO20_Define";
  17. $hash->{NotifyFn} = "CO20_Notify";
  18. $hash->{UndefFn} = "CO20_Undefine";
  19. $hash->{SetFn} = "CO20_Set";
  20. $hash->{GetFn} = "CO20_Get";
  21. $hash->{AttrFn} = "CO20_Attr";
  22. $hash->{AttrList} = "disable:1,0 ".
  23. "advanced:1,0 ".
  24. "interval ".
  25. "retries ".
  26. "timeout ".
  27. $readingFnAttributes;
  28. }
  29. #####################################
  30. sub
  31. CO20_Define($$)
  32. {
  33. my ($hash, $def) = @_;
  34. my @a = split("[ \t][ \t]*", $def);
  35. return "Usage: define <name> CO20 [bus:device]" if(@a < 2);
  36. delete $hash->{ID};
  37. my $name = $a[0];
  38. #$hash->{ID} = undef;
  39. $hash->{SERIALNUMBER} = undef;
  40. $hash->{helper}{defined} = "none";
  41. if( defined($a[2]))
  42. {
  43. if($a[2] =~ m/(\d.*):(\d.*)/)
  44. {
  45. $hash->{ID} = $a[2];
  46. $hash->{helper}{defined} = "id";
  47. }
  48. elsif($a[2] =~ m/(\d.*)/)
  49. {
  50. $hash->{SERIALNUMBER} = $a[2];
  51. $hash->{helper}{defined} = "serial";
  52. }
  53. }
  54. $hash->{NAME} = $name;
  55. $hash->{FAIL} = 0;
  56. $hash->{RECONNECT} = 0;
  57. $hash->{helper}{seq2} = 0x67;
  58. $hash->{helper}{seq4} = 0x0001;
  59. $hash->{NOTIFYDEV} = "global";
  60. $hash->{helper}{retries} = AttrVal($name,"retries",3);
  61. $hash->{helper}{timeout} = AttrVal($name,"timeout",1000);
  62. if( $init_done ) {
  63. CO20_Disconnect($hash);
  64. CO20_Connect($hash);
  65. } else {
  66. readingsSingleUpdate($hash, 'state', 'initialized', 1 );
  67. }
  68. return undef;
  69. }
  70. sub
  71. CO20_Notify($$)
  72. {
  73. my ($hash,$dev) = @_;
  74. return if($dev->{NAME} ne "global");
  75. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  76. CO20_Connect($hash);
  77. }
  78. my $VENDOR = 0x03eb;
  79. my $PRODUCT = 0x2013;
  80. sub
  81. CO20_SetStickData($$)
  82. {
  83. my ($hash, $data) = @_;
  84. my $name = $hash->{NAME};
  85. my $strlen = length($data);
  86. my $ind = 0;
  87. Log3 $name, 5, "datalen $strlen";
  88. if($strlen == 240) {
  89. $ind = index($data, "warn1")+22;
  90. $hash->{KNOB_CO2_VOC_level_warn1} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  91. $ind = index($data, "warn2")+22;
  92. $hash->{KNOB_CO2_VOC_level_warn2} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  93. $ind = index($data, "Reg_Set")+20;
  94. $hash->{KNOB_Reg_Set} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  95. $ind = index($data, "Reg_P")+19;
  96. $hash->{KNOB_Reg_P} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  97. $ind = index($data, "Reg_I")+19;
  98. $hash->{KNOB_Reg_I} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  99. $ind = index($data, "Reg_D")+19;
  100. $hash->{KNOB_Reg_D} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  101. $ind = index($data, "LogInterval")+27;
  102. $hash->{KNOB_LogInterval} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  103. $ind = index($data, "ui16StartupBits")+30;
  104. $hash->{KNOB_ui16StartupBits} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1));
  105. } elsif($strlen == 32) {
  106. $ind = index($data, ";");
  107. $hash->{FLAG_WARMUP} = ord(substr($data,$ind+3,1))*256 + ord(substr($data,$ind+2,1));
  108. $hash->{FLAG_BURN_IN} = ord(substr($data,$ind+7,1))*256 + ord(substr($data,$ind+6,1));
  109. $hash->{FLAG_RESET_BASELINE} = ord(substr($data,$ind+11,1))*256 + ord(substr($data,$ind+10,1));
  110. $hash->{FLAG_CALIBRATE_HEATER} = ord(substr($data,$ind+15,1))*256 + ord(substr($data,$ind+14,1));
  111. $hash->{FLAG_LOGGING} = ord(substr($data,$ind+19,1))*256 + ord(substr($data,$ind+18,1));
  112. } elsif($strlen == 1) {
  113. delete( $hash->{KNOB_CO2_VOC_level_warn1} );
  114. delete( $hash->{KNOB_CO2_VOC_level_warn2} );
  115. delete( $hash->{KNOB_Reg_Set} );
  116. delete( $hash->{KNOB_Reg_P} );
  117. delete( $hash->{KNOB_Reg_I} );
  118. delete( $hash->{KNOB_Reg_D} );
  119. delete( $hash->{KNOB_LogInterval} );
  120. delete( $hash->{KNOB_ui16StartupBits} );
  121. delete( $hash->{FLAG_WARMUP} );
  122. delete( $hash->{FLAG_BURN_IN} );
  123. delete( $hash->{FLAG_RESET_BASELINE} );
  124. delete( $hash->{FLAG_CALIBRATE_HEATER} );
  125. delete( $hash->{FLAG_LOGGING} );
  126. }
  127. return undef;
  128. }
  129. sub
  130. CO20_Connect($)
  131. {
  132. my ($hash) = @_;
  133. my $name = $hash->{NAME};
  134. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  135. Log3 $name, 4, "$name: start CO20 connect";
  136. delete $hash->{DEV};
  137. Log3 $name, 4, "$name: delete CO20 dev";
  138. $hash->{USB} = Device::USB->new() if( !$hash->{USB} );
  139. Log3 $name, 3, "$name: CO20 USB connect";
  140. if( $hash->{helper}{defined} eq "id" && $hash->{ID} && $hash->{ID} =~ m/(\d.*):(\d.*)/ ) {
  141. my $dirname = $1;
  142. my $filename = $2;
  143. foreach my $bus ($hash->{USB}->list_busses())
  144. {
  145. next if( $bus->{dirname} != $dirname );
  146. foreach my $device (@{$bus->{devices}}) {
  147. next if( $device->idVendor() != $VENDOR );
  148. next if( $device->idProduct() != $PRODUCT );
  149. next if( $device->{filename} != $filename );
  150. Log3 $name, 4, "$name: found CO20 device with id";
  151. $hash->{DEV} = $device;
  152. last;
  153. }
  154. last if( $hash->{DEV} );
  155. }
  156. }
  157. elsif( $hash->{helper}{defined} eq "serial" && !$hash->{DEV} && $hash->{SERIALNUMBER} )
  158. {
  159. foreach my $bus ($hash->{USB}->list_busses())
  160. {
  161. foreach my $device (@{$bus->{devices}}) {
  162. next if( $device->idVendor() != $VENDOR );
  163. next if( $device->idProduct() != $PRODUCT );
  164. $hash->{DEV} = $device;
  165. $hash->{DEV}->open();
  166. $hash->{DEV}->detach_kernel_driver_np(0) if( $hash->{DEV}->get_driver_np(0) );
  167. my $ret = $hash->{DEV}->claim_interface( 0 );
  168. if( $ret == -16 ) {
  169. Log3 $name, 2, "$name: USB timeout for CO20 device on identify";
  170. return;
  171. } elsif( $ret != 0 ) {
  172. Log3 $name, 2, "$name: failed to claim CO20 device on identify";
  173. CO20_Disconnect($hash);
  174. return;
  175. }
  176. Log3 $name, 4, "$name: claimed CO20 device on identify";
  177. my $buf;
  178. $hash->{DEV}->bulk_read(0x00000081, $buf, 16, 1000);
  179. Log3 $name, 4, "$name: read CO20 device on identify";
  180. my $currentid = CO20_identify($hash);
  181. if(!$currentid)
  182. {
  183. Log3 $name, 2, "$name: found CO20 device without id while looking for ".$hash->{SERIALNUMBER};
  184. }
  185. elsif($currentid ne $hash->{SERIALNUMBER})
  186. {
  187. Log3 $name, 2, "$name: found CO20 device with id $currentid while looking for ".$hash->{SERIALNUMBER};
  188. }
  189. else
  190. {
  191. Log3 $name, 2, "$name: found CO20 device with id $currentid";
  192. last;
  193. }
  194. $hash->{DEV}->release_interface(0);
  195. Log3 $name, 4, "$name: released interface on identify";
  196. delete $hash->{DEV};
  197. delete $hash->{manufacturer};
  198. delete $hash->{product};
  199. }
  200. last if( $hash->{DEV} );
  201. }
  202. }
  203. else
  204. {
  205. Log3 $name, 4, "$name: searching CO20 device on identify";
  206. $hash->{DEV} = $hash->{USB}->find_device( $VENDOR, $PRODUCT );
  207. Log3 $name, 4, "$name: found CO20 device on identify";
  208. }
  209. if( !$hash->{DEV} ) {
  210. Log3 $name, 2, "$name: failed to find CO20 device";
  211. CO20_Disconnect($hash);
  212. return undef;
  213. }
  214. else
  215. {
  216. Log3 $name, 4, "$name: found one CO20 device on identify";
  217. if( !$hash->{ID} ) {
  218. foreach my $bus ($hash->{USB}->list_busses()) {
  219. foreach my $device (@{$bus->{devices}}) {
  220. next if( $device->idVendor() != $VENDOR );
  221. next if( $device->idProduct() != $PRODUCT );
  222. next if( $device->{filename} != $hash->{DEV}->{filename} );
  223. $hash->{ID} = $bus->{dirname} . ":" . $device->{filename};
  224. last if( $hash->{ID} );
  225. }
  226. last if( $hash->{ID} );
  227. }}
  228. #
  229. readingsSingleUpdate($hash, 'state', 'found', 1 );
  230. Log3 $name, 3, "$name: CO20 device found";
  231. $hash->{DEV}->open();
  232. my $dev_man = $hash->{DEV}->manufacturer();
  233. my $dev_pro = $hash->{DEV}->product();
  234. my $dev_ser = $hash->{DEV}->serial_number();
  235. $hash->{manufacturer} = $dev_man if(defined($dev_man));
  236. $hash->{product} = $dev_pro if(defined($dev_pro));
  237. $hash->{SERIAL} = $dev_ser if(defined($dev_ser) && $dev_ser ne "?");
  238. if( $dev_man && $dev_pro ) {
  239. $hash->{DEV}->detach_kernel_driver_np(0) if( $hash->{DEV}->get_driver_np(0) );
  240. my $ret = $hash->{DEV}->claim_interface( 0 );
  241. if( $ret == -16 ) {
  242. readingsSingleUpdate($hash, 'state', 'waiting', 1 );
  243. Log3 $name, 3, "$name: waiting for CO20 device";
  244. return;
  245. } elsif( $ret != 0 ) {
  246. readingsSingleUpdate($hash, 'state', 'error', 1 );
  247. Log3 $name, 3, "$name: failed to claim CO20 device";
  248. CO20_Disconnect($hash);
  249. return;
  250. }
  251. readingsSingleUpdate($hash, 'state', 'opened', 1 );
  252. Log3 $name, 3, "$name: CO20 device opened";
  253. $hash->{INTERVAL} = AttrVal($name, "interval", 300);
  254. RemoveInternalTimer($hash);
  255. InternalTimer(gettimeofday()+10, "CO20_poll", $hash, 0);
  256. Log3 $name, 4, "$name: polling CO20 device on identify";
  257. my $buf;
  258. $hash->{DEV}->bulk_read(0x00000081, $buf, 16, 1000);
  259. } else {
  260. Log3 $name, 3, "$name: failed to open CO20 device";
  261. CO20_Disconnect($hash);
  262. }
  263. }
  264. return undef;
  265. }
  266. sub
  267. CO20_Disconnect($)
  268. {
  269. my ($hash) = @_;
  270. my $name = $hash->{NAME};
  271. RemoveInternalTimer($hash);
  272. if( !$hash->{USB} )
  273. {
  274. readingsSingleUpdate($hash, 'state', 'disconnected', 1 );
  275. if( $hash->{manufacturer} && $hash->{product} ) {
  276. Log3 $name, 4, "$name: disconnected release";
  277. $hash->{DEV}->release_interface(0) if($hash->{DEV});
  278. }
  279. return;
  280. }
  281. if( $hash->{manufacturer} && $hash->{product} ) {
  282. Log3 $name, 4, "$name: disconnect release";
  283. $hash->{DEV}->release_interface(0) if($hash->{DEV});
  284. }
  285. delete( $hash->{USB} ) if($hash->{USB});
  286. delete( $hash->{DEV} ) if($hash->{DEV});
  287. delete( $hash->{manufacturer} );
  288. delete( $hash->{product} );
  289. delete( $hash->{BLOCKED} );
  290. delete $hash->{FIRMWARE};
  291. readingsSingleUpdate($hash, 'state', 'disconnected', 1 );
  292. Log3 $name, 3, "$name: disconnected";
  293. CO20_SetStickData($hash,"X");
  294. return undef;
  295. }
  296. sub
  297. CO20_Undefine($$)
  298. {
  299. my ($hash, $arg) = @_;
  300. CO20_Disconnect($hash);
  301. $hash->{FAIL} = 0;
  302. return undef;
  303. }
  304. sub
  305. CO20_identify($)
  306. {
  307. my ($hash) = @_;
  308. CO20_dataread($hash,"stickdata",1);
  309. return $hash->{SERIALNUMBER};
  310. }
  311. sub
  312. CO20_poll($)
  313. {
  314. my ($hash) = @_;
  315. my $name = $hash->{NAME};
  316. if(!$hash->{LOCAL}) {
  317. RemoveInternalTimer($hash);
  318. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "CO20_poll", $hash, 0);
  319. }
  320. if($hash->{BLOCKED}) {
  321. return undef;
  322. }
  323. if( $hash->{manufacturer} && $hash->{product} ) {
  324. my $buf = "@".sprintf("%c",$hash->{helper}{seq2})."TRF?\n@@@@@@@@@";
  325. Log3 $name, 5, "$name: sent $buf / ".ord(substr($buf,0,1));
  326. my $ret = $hash->{DEV}->bulk_write(0x00000002, $buf, 16, $hash->{helper}{timeout});
  327. if( $ret != 16 ) {
  328. my $ret2 = 0;
  329. $ret2 = $hash->{DEV}->bulk_write(0x00000002, "@@@@@@@@@@@@@@@@", 16, $hash->{helper}{timeout}) if( $ret != -19 );
  330. $hash->{FAIL} = $hash->{FAIL}+1;
  331. Log3 $name, 3, "$name: write error $ret/$ret2 ($hash->{FAIL})";
  332. RemoveInternalTimer($hash);
  333. InternalTimer(gettimeofday()+30, "CO20_poll", $hash, 1);
  334. if($hash->{FAIL} >= $hash->{helper}{retries}) {
  335. readingsSingleUpdate($hash, 'state', 'reconnect', 1 );
  336. $hash->{FAIL} = 0;
  337. CO20_Disconnect($hash);
  338. $hash->{RECONNECT} = $hash->{RECONNECT}+1;
  339. CO20_Connect($hash);
  340. } else {
  341. readingsSingleUpdate($hash, 'state', 'retry', 1 );
  342. }
  343. return undef;
  344. }
  345. if ($hash->{helper}{seq2} < 0xFF){ $hash->{helper}{seq2}++} else {$hash->{helper}{seq2} = 0x67};
  346. my $data="";
  347. for( $a = 1; $a <= 3; $a = $a + 1 ) {
  348. $ret=$hash->{DEV}->bulk_read(0x00000081, $buf, 16, $hash->{helper}{timeout});
  349. if( $ret != 16 and $ret != 0 ) {
  350. Log3 $name, 4, "$name: read error $ret";
  351. }
  352. $data.=$buf;
  353. }
  354. Log3 $name, 5, "$name got ".unpack('H*', $data)." / ".length($data)." / ".ord(substr($data,0,1));
  355. if( $ret != 16 and $ret != 0 and length($data) < 16 ) {
  356. $hash->{FAIL} = $hash->{FAIL}+1;
  357. RemoveInternalTimer($hash);
  358. InternalTimer(gettimeofday()+30, "CO20_poll", $hash, 1);
  359. Log3 $name, 4, "$name: readloop error $ret ($hash->{FAIL})";
  360. if($hash->{FAIL} >= $hash->{helper}{retries}) {
  361. readingsSingleUpdate($hash, 'state', 'reconnect', 1 );
  362. $hash->{FAIL} = 0;
  363. CO20_Disconnect($hash);
  364. $hash->{RECONNECT} = $hash->{RECONNECT}+1;
  365. CO20_Connect($hash);
  366. } else {
  367. readingsSingleUpdate($hash, 'state', 'retry', 1 );
  368. }
  369. return undef;
  370. }
  371. if( length($data) >= 16 ) {
  372. $data = "@".$data if(ord(substr($data,0,1)) > 64);
  373. $hash->{FAIL} = 0;
  374. my $voc = ord(substr($data,3,1))*256 + ord(substr($data,2,1));
  375. my $dbg = ord(substr($data,5,1))*256 + ord(substr($data,4,1));
  376. my $pwm = ord(substr($data,7,1))*256 + ord(substr($data,6,1));
  377. my $rh = ord(substr($data,9,1))*256 + ord(substr($data,8,1));
  378. my $rs = ord(substr($data,14,1))*65536 + ord(substr($data,13,1))*256 + ord(substr($data,12,1));
  379. if (ord(substr($data,3,1)) < 128) {
  380. readingsBeginUpdate($hash);
  381. readingsBulkUpdate( $hash, "voc", $voc, 1 );
  382. readingsBulkUpdate($hash, 'state', 'open', 1 );
  383. if( AttrVal($name, "advanced", 0 ) == 1 ){
  384. readingsBulkUpdate( $hash, "debug", $dbg, 1 );
  385. readingsBulkUpdate( $hash, "pwm", $pwm, 1 );
  386. readingsBulkUpdate( $hash, "r_h", $rh/100, 1 );
  387. readingsBulkUpdate( $hash, "r_s", $rs, 1 );
  388. }
  389. readingsEndUpdate($hash,1);
  390. }
  391. #my $bufdec = ord(substr($buf,0,1))." ".ord(substr($buf,1,1))." ".ord(substr($buf,2,1))." ".ord(substr($buf,3,1))." ".ord(substr($buf,4,1))." ".ord(substr($buf,5,1))." ".ord(substr($buf,6,1))." ".ord(substr($buf,7,1))." ".ord(substr($buf,8,1))." ".ord(substr($buf,9,1))." ".ord(substr($buf,10,1))." ".ord(substr($buf,11,1))." ".ord(substr($buf,12,1))." ".ord(substr($buf,13,1))." ".ord(substr($buf,14,1))." ".ord(substr($buf,15,1))." ".ord(substr($buf,16,1));
  392. # Log3 $name, 5, "$name: read 1 success\n$bufdec";
  393. } else {
  394. $hash->{FAIL} = $hash->{FAIL}+1;
  395. Log3 $name, 2, "$name: read failed $ret ($hash->{FAIL})";
  396. if($hash->{FAIL} >= $hash->{helper}{retries}) {
  397. readingsSingleUpdate($hash, 'state', 'reconnect', 1 );
  398. $hash->{FAIL} = 0;
  399. CO20_Disconnect($hash);
  400. $hash->{RECONNECT} = $hash->{RECONNECT}+1;
  401. CO20_Connect($hash);
  402. } else {
  403. readingsSingleUpdate($hash, 'state', 'retry', 1 );
  404. }
  405. }
  406. $hash->{LAST_POLL} = FmtDateTime( gettimeofday() );
  407. } else {
  408. Log3 $name, 2, "$name: no device";
  409. $hash->{FAIL} = 0;
  410. CO20_Disconnect($hash);
  411. $hash->{RECONNECT} = $hash->{RECONNECT}+1;
  412. CO20_Connect($hash);
  413. }
  414. }
  415. sub
  416. CO20_dataread($$;$)
  417. {
  418. my ($hash, $readingstype, $identify) = @_;
  419. my $name = $hash->{NAME};
  420. if(!defined($hash->{DEV}))
  421. {
  422. Log3 $name, 1, "$name: no device";
  423. return undef;
  424. }
  425. my $reqstr = "";
  426. my $retcount = 16;
  427. if($readingstype eq "knobdata") {
  428. $reqstr = "KNOBPRE?";
  429. $retcount = 16;
  430. } elsif ($readingstype eq "flagdata") {
  431. $reqstr = "FLAGGET?";
  432. $retcount = 3;
  433. } elsif ($readingstype eq "stickdata") {
  434. $reqstr = "*IDN?";
  435. $retcount = 8;
  436. } else {
  437. return undef;
  438. }
  439. RemoveInternalTimer($hash);
  440. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "CO20_poll", $hash, 1);
  441. if( $hash->{manufacturer} && $hash->{product} ) {
  442. my $seq = sprintf("%04X",$hash->{helper}{seq4});
  443. my $seqstr = sprintf("%c",hex substr($seq,2,2)).sprintf("%c",hex substr($seq,0,2));
  444. $hash->{helper}{seq4} = ($hash->{helper}{seq4} +1) & 0xFFFF;
  445. my $buf = substr("@".$seq.$reqstr."\n@@@@@@@@@@@@@@@@",0,16);
  446. my $ret = $hash->{DEV}->bulk_write(0x00000002, $buf, 16, $hash->{helper}{timeout}) if(defined($hash->{DEV}));
  447. Log3 $name, 4, "getdata write $ret" if($ret != 16);
  448. my $data = "";
  449. my $intdata = "";
  450. if($ret == 16) {
  451. for( $a = 1; $a <= $retcount; $a = $a + 1 ){
  452. $hash->{DEV}->bulk_read(0x00000081, $buf, 16, $hash->{helper}{timeout});
  453. $data.=$buf;
  454. Log3 $name, 5, "getdata read $ret" if($ret != 16);
  455. $intdata = ord(substr($buf,0,1))." ".ord(substr($buf,1,1))." ".ord(substr($buf,2,1))." ".ord(substr($buf,3,1))." ".ord(substr($buf,4,1))." ".ord(substr($buf,5,1))." ".ord(substr($buf,6,1))." ".ord(substr($buf,7,1))." ".ord(substr($buf,8,1))." ".ord(substr($buf,9,1))." ".ord(substr($buf,10,1))." ".ord(substr($buf,11,1))." ".ord(substr($buf,12,1))." ".ord(substr($buf,13,1))." ".ord(substr($buf,14,1))." ".ord(substr($buf,15,1)) if(length($buf) > 15);
  456. Log3 $name, 5, "$intdata\n$buf";
  457. }
  458. Log3 $name, 5, length($data);
  459. }
  460. if($readingstype eq "knobdata") {
  461. CO20_SetStickData($hash,$data);
  462. } elsif ($readingstype eq "flagdata") {
  463. CO20_SetStickData($hash,$data);
  464. } elsif ($readingstype eq "stickdata") {
  465. if ($data =~ /\b;\b(.*?)\b \$;;MCU\b/) {
  466. my $fwstr = $1;
  467. $fwstr =~ s/;C;/, Firmware: /g;
  468. $fwstr =~ s/\$//g;
  469. $hash->{FIRMWARE} = $fwstr;
  470. }
  471. if ($data =~ /\bS\/N:\b(.*?)\b;bI\b/) {
  472. return $1 if($identify);
  473. $hash->{SERIALNUMBER} = $1;
  474. }
  475. }
  476. }
  477. }
  478. sub
  479. CO20_flashread($)
  480. {
  481. my ($hash) = @_;
  482. my $name = $hash->{NAME};
  483. return undef;
  484. # 40 30 30 31 31 52 45 43 4F 52 44 53 3F 0A 40 40 @0011RECORDS?.@@
  485. #
  486. # 40 30 30 31 32 4C 42 53 49 5A 45 3F 0A 40 40 40 @0012LBSIZE?.@@@
  487. #
  488. # 40 30 30 31 33 46 4C 53 54 4F 50 0A 40 40 40 40 @0013FLSTOP.@@@@
  489. #
  490. # 40 30 30 31 34 4C 42 53 49 5A 45 3F 0A 40 40 40 @0014LBSIZE?.@@@
  491. #
  492. # 40 30 30 31 35 2A 49 44 4E 3F 0A 40 40 40 40 40 @0015*IDN?.@@@@@
  493. #
  494. # 40 30 30 31 36 4C 42 41 56 47 3B 31 30 30 0A 40 @0016LBAVG;100.@
  495. #
  496. # 40 6A 4C 42 52 0A 40 40 40 40 40 40 40 40 40 40 @jLBR.@@@@@@@@@@ n times ?
  497. #
  498. # 40 30 30 31 37 46 4C 53 54 41 52 54 0A 40 40 40 @0017FLSTART.@@@ n times ?
  499. #
  500. # 2 reads each
  501. }
  502. sub
  503. CO20_dataset($$$)
  504. {
  505. my ($hash, $cmd, $val) = @_;
  506. my $name = $hash->{NAME};
  507. my $reqstr = "";
  508. if($cmd eq "flag_WARMUP") {
  509. $reqstr = "FLAGSET;WARMUP="; # 0000
  510. } elsif($cmd eq "flag_BURN-IN") {
  511. $reqstr = "FLAGSET;BURN-IN="; # 0000
  512. } elsif($cmd eq "flag_RESET_BASELINE") {
  513. $reqstr = "FLAGSET;RESET BASELINE="; # 0000
  514. } elsif($cmd eq "flag_CALIBRATE_HEATER") {
  515. $reqstr = "FLAGSET;CALIBRATE HEATER="; # 0000
  516. } elsif($cmd eq "flag_LOGGING") {
  517. $reqstr = "FLAGSET;LOGGING="; # 0000
  518. } elsif($cmd eq "knob_CO2/VOC_level_warn1") {
  519. $reqstr = "KNOBSET;CO2/VOC level_warn1=";
  520. } elsif($cmd eq "knob_CO2/VOC_level_warn2") {
  521. $reqstr = "KNOBSET;CO2/VOC level_warn2=";
  522. } elsif($cmd eq "knob_Reg_Set") {
  523. $reqstr = "KNOBSET;Reg_Set="; # 9100
  524. } elsif($cmd eq "knob_Reg_P") {
  525. $reqstr = "KNOBSET;Reg_P="; # 0300
  526. } elsif($cmd eq "knob_Reg_I") {
  527. $reqstr = "KNOBSET;Reg_I="; # 0A00
  528. } elsif($cmd eq "knob_Reg_D") {
  529. $reqstr = "KNOBSET;Reg_D="; # 0000
  530. } elsif($cmd eq "knob_LogInterval") {
  531. $reqstr = "KNOBSET;LogInterval="; # 0000
  532. } elsif($cmd eq "knob_ui16StartupBits") {
  533. $reqstr = "KNOBSET;ui16StartupBits="; # 0000
  534. } elsif($cmd eq "recalibrate_heater") {
  535. $reqstr = "FLAGSET;CALIBRATE HEATER="; # 0180
  536. } elsif($cmd eq "reset_baseline") {
  537. $reqstr = "FLAGSET;RESET BASELINE="; # 0180
  538. } elsif($cmd eq "reset_device") {
  539. $reqstr = "*RST";
  540. } elsif($cmd eq "reconnect") {
  541. CO20_Disconnect($hash);
  542. CO20_Connect($hash);
  543. return undef;
  544. }
  545. RemoveInternalTimer($hash);
  546. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "CO20_poll", $hash, 1);
  547. if( $hash->{manufacturer} && $hash->{product} ) {
  548. my $seq = sprintf("%04X",$hash->{helper}{seq4});
  549. $hash->{helper}{seq4} = ($hash->{helper}{seq4} +1) & 0xFFFF;
  550. my $buf = "@".$seq.$reqstr;
  551. if($cmd ne "reset_device") {
  552. $buf .= "\x02";
  553. if($cmd eq "recalibrate_heater" or $cmd eq "reset_baseline") {
  554. $buf .= "\x01\x80";
  555. } else {
  556. my $h = sprintf("%04X",$val & 0xFFFF);
  557. $buf .= sprintf("%c",hex substr($h,2,2)).sprintf("%c",hex substr($h,0,2));
  558. Log3 $name, 5, "$val $h \n";
  559. }
  560. }
  561. if (index($reqstr, "KNOBSET") != -1) {
  562. $buf .= ";";
  563. }
  564. $buf .= "\n";
  565. my $buflen = length($buf);
  566. $buf .= "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@";
  567. my $ret = $hash->{DEV}->bulk_write(0x00000002, substr($buf,0,16), 16, $hash->{helper}{timeout});
  568. Log3 $name, 5, "setdata write $ret" if($ret != 16);
  569. if($ret == 16 and ($buflen > 16 or $cmd eq "reset_device")) {
  570. $ret = $hash->{DEV}->bulk_write(0x00000002, substr($buf,16,16), 16, $hash->{helper}{timeout});
  571. Log3 $name, 5, "setdata write $ret" if($ret != 16);
  572. }
  573. if($ret == 16 and $buflen > 32) {
  574. $ret = $hash->{DEV}->bulk_write(0x00000002, substr($buf,32,16), 16, $hash->{helper}{timeout});
  575. Log3 $name, 5, "setdata write $ret" if($ret != 16);
  576. }
  577. if($ret == 16 and $buflen > 15) {
  578. $hash->{DEV}->bulk_read(0x00000081, $buf, 16, $hash->{helper}{timeout});
  579. $buflen = length($buf);
  580. Log3 $name, 5, "getdata read $ret";
  581. if($buflen > 15)
  582. {
  583. my $intdata .= ord(substr($buf,0,1))." ".ord(substr($buf,1,1))." ".ord(substr($buf,2,1))." ".ord(substr($buf,3,1))." ".ord(substr($buf,4,1))." ".ord(substr($buf,5,1))." ".ord(substr($buf,6,1))." ".ord(substr($buf,7,1))." ".ord(substr($buf,8,1))." ".ord(substr($buf,9,1))." ".ord(substr($buf,10,1))." ".ord(substr($buf,11,1))." ".ord(substr($buf,12,1))." ".ord(substr($buf,13,1))." ".ord(substr($buf,14,1))." ".ord(substr($buf,15,1));
  584. Log3 $name, 5, "$buf";
  585. }
  586. } else {
  587. Log3 $name, 4, "set data failed: $buf";
  588. return undef;
  589. }
  590. }
  591. return undef;
  592. }
  593. sub
  594. CO20_Get($$@)
  595. {
  596. my ($hash, $name, $cmd) = @_;
  597. my $list = "update:noArg";
  598. $list = "update:noArg air_data:noArg knob_data:noArg flag_data:noArg stick_data:noArg" if( AttrVal($name, "advanced", 0 ) == 1 );
  599. if( $cmd eq "air_data" or $cmd eq "update" ) {
  600. $hash->{LOCAL} = 1;
  601. CO20_poll($hash);
  602. delete $hash->{LOCAL};
  603. return undef;
  604. } elsif( $cmd eq "knob_data" ) {
  605. $hash->{BLOCKED} = 1;
  606. CO20_dataread($hash,"knobdata");
  607. delete $hash->{BLOCKED};
  608. return undef;
  609. } elsif( $cmd eq "flag_data" ) {
  610. $hash->{BLOCKED} = 1;
  611. CO20_dataread($hash,"flagdata");
  612. delete $hash->{BLOCKED};
  613. return undef;
  614. } elsif( $cmd eq "stick_data" ) {
  615. $hash->{BLOCKED} = 1;
  616. CO20_dataread($hash,"stickdata");
  617. delete $hash->{BLOCKED};
  618. return undef;
  619. }
  620. return "Unknown argument $cmd, choose one of $list";
  621. }
  622. sub
  623. CO20_Set($$$$)
  624. {
  625. my ($hash, $name, $cmd, $val) = @_;
  626. my $list = "";
  627. $list = "knob_CO2/VOC_level_warn1 knob_CO2/VOC_level_warn2 knob_Reg_Set knob_Reg_P knob_Reg_I knob_Reg_D knob_LogInterval knob_ui16StartupBits recalibrate_heater:noArg reset_baseline:noArg reset_device:noArg reconnect:noArg" if( AttrVal($name, "advanced", 0 ) == 1 );
  628. if (index($list, $cmd) != -1) {
  629. $hash->{BLOCKED} = 1;
  630. CO20_dataset($hash,$cmd,$val);
  631. delete $hash->{BLOCKED};
  632. return undef;
  633. }
  634. return "Unknown argument $cmd, choose one of $list";
  635. }
  636. sub
  637. CO20_Attr($$$)
  638. {
  639. my ($cmd, $name, $attrName, $attrVal) = @_;
  640. return undef if(!defined($defs{$name}));
  641. my $orig = $attrVal;
  642. $attrVal = int($attrVal) if($attrName eq "interval" || $attrName eq "retries" || $attrName eq "timeout");
  643. $attrVal = 10 if($attrName eq "interval" && $attrVal < 10 && $attrVal != 0);
  644. $attrVal = 20 if($attrName eq "retries" && ($attrVal < 0));
  645. $attrVal = 20 if($attrName eq "retries" && ($attrVal > 20));
  646. $attrVal = 250 if($attrName eq "timeout" && ($attrVal != 0 && $attrVal < 250));
  647. $attrVal = 10000 if($attrName eq "timeout" && ($attrVal != 0 && $attrVal > 10000));
  648. if( $attrName eq "disable" ) {
  649. my $hash = $defs{$name};
  650. if( $cmd eq "set" && $attrVal ne "0" ) {
  651. CO20_Disconnect($hash);
  652. } else {
  653. $attr{$name}{$attrName} = 0;
  654. CO20_Disconnect($hash);
  655. CO20_Connect($hash);
  656. }
  657. } elsif( $attrName eq "interval" ) {
  658. my $hash = $defs{$name};
  659. $hash->{INTERVAL} = $attrVal;
  660. CO20_poll($hash) if( $init_done );
  661. } elsif( $attrName eq "retries" ) {
  662. my $hash = $defs{$name};
  663. $hash->{helper}{retries} = $attrVal;
  664. } elsif( $attrName eq "timeout" ) {
  665. my $hash = $defs{$name};
  666. $hash->{helper}{timeout} = $attrVal;
  667. }
  668. if( $cmd eq "set" ) {
  669. if( $orig ne $attrVal ) {
  670. $attr{$name}{$attrName} = $attrVal;
  671. return $attrName ." set to ". $attrVal;
  672. }
  673. }
  674. return;
  675. }
  676. 1;
  677. =pod
  678. =item device
  679. =item summary USB iAQ Stick
  680. =begin html
  681. <a name="CO20"></a>
  682. <h3>CO20</h3>
  683. <ul>
  684. Module for measuring air quality with usb sticks based on the AppliedSensor iAQ-Engine sensor.
  685. Products currently know to work are the VOLTCRAFT CO-20, the Sentinel Haus Institut RaumluftW&auml;chter
  686. and the VELUX Raumluftf&uuml;hler.<br>
  687. Probably works with all devices recognized as iAQ Stick (0x03eb:0x2013).<br><br>
  688. Notes:
  689. <ul>
  690. <li>Device::USB hast to be installed on the FHEM host.<br>
  691. It can be installed with '<code>cpan install Device::USB</code>'<br>
  692. or on debian with '<code>sudo apt-get install libdevice-usb-perl'</code>'</li>
  693. <li>FHEM has to have permissions to open the device. To configure this with udev
  694. rules see here: <a href="https://code.google.com/p/usb-sensors-linux/wiki/Install_AirSensor_Linux">Install_AirSensor_Linux
  695. usb-sensors-linux</a></li>
  696. <li>Advanced features are only available after setting the attribute <i>advanced</i>.<br>
  697. Almost all the hidden settings from the Windows application are implemented in this mode.<br>
  698. Readout of values gathered in standalone mode is not possible yet.</li>
  699. </ul><br>
  700. <a name="CO20_Define"></a>
  701. <b>Define</b>
  702. <ul>
  703. <code>define &lt;name&gt; CO20 [bus:device]</code><br>
  704. <br>
  705. Defines a CO20 device. bus:device hast to be used if more than one sensor is connected to the same host.<br><br>
  706. Examples:
  707. <ul>
  708. <code>define CO20 CO20</code><br>
  709. </ul>
  710. </ul><br>
  711. <a name="CO20_Readings"></a>
  712. <b>Readings</b>
  713. <ul>
  714. <li>voc<br>
  715. CO2 equivalents in ppm</li>
  716. <li>debug<br>
  717. debug value</li>
  718. <li>pwm<br>
  719. pwm value</li>
  720. <li>r_h<br>
  721. resistance of heating element in Ohm (?)</li>
  722. <li>r_s<br>
  723. resistance of sensor element in Ohm (?)</li>
  724. </ul><br>
  725. <a name="CO20_Get"></a>
  726. <b>Get</b>
  727. <ul>
  728. <li>update / air_data<br>
  729. trigger an update</li>
  730. <li>flag_data<br>
  731. get internal flag values</li>
  732. <li>knob_data<br>
  733. get internal knob values</li>
  734. <li>stick_data<br>
  735. get stick information</li>
  736. </ul><br>
  737. <a name="CO20_Set"></a>
  738. <b>Set</b>
  739. <ul>
  740. <li>KNOB_CO2_VOC_level_warn1<br>
  741. sets threshold for yellow led</li>
  742. <li>KNOB_CO2_VOC_level_warn2<br>
  743. sets threshold for red led</li>
  744. <li>KNOB_Reg_Set<br>
  745. internal value, affects voc reading</li>
  746. <li>KNOB_Reg_P<br>
  747. internal pid value</li>
  748. <li>KNOB_Reg_I<br>
  749. internal pid value</li>
  750. <li>KNOB_Reg_D<br>
  751. internal pid value</li>
  752. <li>KNOB_LogInterval<br>
  753. log interval for standalone mode</li>
  754. <li>KNOB_ui16StartupBits<br>
  755. set to 0 for no automatic calibration on startup</li>
  756. <li>FLAG_WARMUP<br>
  757. warmup time left in minutes</li>
  758. <li>FLAG_BURN_IN<br>
  759. burn in time left in minutes</li>
  760. <li>FLAG_RESET_BASELINE<br>
  761. reset voc baseline value</li>
  762. <li>FLAG_CALIBRATE_HEATER<br>
  763. trigger calibration / burn in</li>
  764. <li>FLAG_LOGGING<br>
  765. value count from external logging</li>
  766. </ul><br>
  767. <a name="CO20_Attr"></a>
  768. <b>Attributes</b>
  769. <ul>
  770. <li>interval<br>
  771. the interval in seconds used to read updates [10..]. the default ist 300.</li>
  772. <li>retries<br>
  773. number of retries on USB read/write failures [0..20]. the default is 3.</li>
  774. <li>timeout<br>
  775. the USB connection timeout in milliseconds [250..10000]. the default is 1000.</li>
  776. <li>advanced<br>
  777. 1 -> enables most of the advanced settings and readings described here</li>
  778. <li>disable<br>
  779. 1 -> disconnect and stop polling</li>
  780. </ul>
  781. </ul>
  782. =end html
  783. =cut