74_XiaomiFlowerSens.pm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. ###############################################################################
  2. #
  3. # Developed with Kate
  4. #
  5. # (c) 2016-2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com)
  6. # All rights reserved
  7. #
  8. # This script is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # any later version.
  12. #
  13. # The GNU General Public License can be found at
  14. # http://www.gnu.org/copyleft/gpl.html.
  15. # A copy is found in the textfile GPL.txt and important notices to the license
  16. # from the author is found in LICENSE.txt distributed with these scripts.
  17. #
  18. # This script is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. # GNU General Public License for more details.
  22. #
  23. #
  24. # $Id: 74_XiaomiFlowerSens.pm 13407 2017-02-13 20:40:31Z CoolTux $
  25. #
  26. ###############################################################################
  27. package main;
  28. use strict;
  29. use warnings;
  30. use POSIX;
  31. use JSON;
  32. use Blocking;
  33. my $version = "0.6.8";
  34. # Declare functions
  35. sub XiaomiFlowerSens_Initialize($);
  36. sub XiaomiFlowerSens_Define($$);
  37. sub XiaomiFlowerSens_Undef($$);
  38. sub XiaomiFlowerSens_Attr(@);
  39. sub XiaomiFlowerSens_stateRequest($);
  40. sub XiaomiFlowerSens_stateRequestTimer($);
  41. sub XiaomiFlowerSens_Set($$@);
  42. sub XiaomiFlowerSens_Run($);
  43. sub XiaomiFlowerSens_BlockingRun($);
  44. sub XiaomiFlowerSens_callGatttool($@);
  45. sub XiaomiFlowerSens_forRun_encodeJSON($$);
  46. sub XiaomiFlowerSens_forDone_encodeJSON($$$$$$);
  47. sub XiaomiFlowerSens_BlockingDone($);
  48. sub XiaomiFlowerSens_BlockingAborted($);
  49. sub XiaomiFlowerSens_Initialize($) {
  50. my ($hash) = @_;
  51. $hash->{SetFn} = "XiaomiFlowerSens_Set";
  52. $hash->{DefFn} = "XiaomiFlowerSens_Define";
  53. $hash->{UndefFn} = "XiaomiFlowerSens_Undef";
  54. $hash->{AttrFn} = "XiaomiFlowerSens_Attr";
  55. $hash->{AttrList} = "interval ".
  56. "disable:1 ".
  57. "hciDevice:hci0,hci1,hci2 ".
  58. "disabledForIntervals ".
  59. $readingFnAttributes;
  60. foreach my $d(sort keys %{$modules{XiaomiFlowerSens}{defptr}}) {
  61. my $hash = $modules{XiaomiFlowerSens}{defptr}{$d};
  62. $hash->{VERSION} = $version;
  63. }
  64. }
  65. sub XiaomiFlowerSens_Define($$) {
  66. my ( $hash, $def ) = @_;
  67. my @a = split( "[ \t][ \t]*", $def );
  68. return "too few parameters: define <name> XiaomiFlowerSens <BTMAC>" if( @a != 3 );
  69. my $name = $a[0];
  70. my $mac = $a[2];
  71. $hash->{BTMAC} = $mac;
  72. $hash->{VERSION} = $version;
  73. $hash->{INTERVAL} = 300;
  74. $modules{XiaomiFlowerSens}{defptr}{$hash->{BTMAC}} = $hash;
  75. readingsSingleUpdate ($hash,"state","initialized", 0);
  76. $attr{$name}{room} = "FlowerSens" if( !defined($attr{$name}{room}) );
  77. RemoveInternalTimer($hash);
  78. if( $init_done ) {
  79. XiaomiFlowerSens_stateRequestTimer($hash);
  80. } else {
  81. InternalTimer( gettimeofday()+int(rand(30))+15, "XiaomiFlowerSens_stateRequestTimer", $hash, 0 );
  82. }
  83. Log3 $name, 3, "XiaomiFlowerSens ($name) - defined with BTMAC $hash->{BTMAC}";
  84. $modules{XiaomiFlowerSens}{defptr}{$hash->{BTMAC}} = $hash;
  85. return undef;
  86. }
  87. sub XiaomiFlowerSens_Undef($$) {
  88. my ( $hash, $arg ) = @_;
  89. my $mac = $hash->{BTMAC};
  90. my $name = $hash->{NAME};
  91. RemoveInternalTimer($hash);
  92. BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID}));
  93. delete($modules{XiaomiFlowerSens}{defptr}{$mac});
  94. Log3 $name, 3, "Sub XiaomiFlowerSens_Undef ($name) - delete device $name";
  95. return undef;
  96. }
  97. sub XiaomiFlowerSens_Attr(@) {
  98. my ( $cmd, $name, $attrName, $attrVal ) = @_;
  99. my $hash = $defs{$name};
  100. my $orig = $attrVal;
  101. if( $attrName eq "disable" ) {
  102. if( $cmd eq "set" and $attrVal eq "1" ) {
  103. readingsSingleUpdate ( $hash, "state", "disabled", 1 );
  104. Log3 $name, 3, "XiaomiFlowerSens ($name) - disabled";
  105. }
  106. elsif( $cmd eq "del" ) {
  107. readingsSingleUpdate ( $hash, "state", "active", 1 );
  108. Log3 $name, 3, "XiaomiFlowerSens ($name) - enabled";
  109. }
  110. }
  111. if( $attrName eq "disabledForIntervals" ) {
  112. if( $cmd eq "set" ) {
  113. Log3 $name, 3, "XiaomiFlowerSens ($name) - disabledForIntervals";
  114. readingsSingleUpdate ( $hash, "state", "Unknown", 1 );
  115. }
  116. elsif( $cmd eq "del" ) {
  117. readingsSingleUpdate ( $hash, "state", "active", 1 );
  118. Log3 $name, 3, "XiaomiFlowerSens ($name) - enabled";
  119. }
  120. }
  121. if( $attrName eq "interval" ) {
  122. if( $cmd eq "set" ) {
  123. if( $attrVal < 300 ) {
  124. Log3 $name, 3, "XiaomiFlowerSens ($name) - interval too small, please use something >= 300 (sec), default is 3600 (sec)";
  125. return "interval too small, please use something >= 300 (sec), default is 3600 (sec)";
  126. } else {
  127. $hash->{INTERVAL} = $attrVal;
  128. Log3 $name, 3, "XiaomiFlowerSens ($name) - set interval to $attrVal";
  129. }
  130. }
  131. elsif( $cmd eq "del" ) {
  132. $hash->{INTERVAL} = 300;
  133. Log3 $name, 3, "XiaomiFlowerSens ($name) - set interval to default";
  134. }
  135. }
  136. return undef;
  137. }
  138. sub XiaomiFlowerSens_stateRequest($) {
  139. my ($hash) = @_;
  140. my $name = $hash->{NAME};
  141. if( !IsDisabled($name) ) {
  142. readingsSingleUpdate ( $hash, "state", "active", 1 ) if( (ReadingsVal($name, "state", 0) eq "initialized" or ReadingsVal($name, "state", 0) eq "unreachable" or ReadingsVal($name, "state", 0) eq "corrupted data" or ReadingsVal($name, "state", 0) eq "disabled" or ReadingsVal($name, "state", 0) eq "Unknown" or ReadingsVal($name, "state", 0) eq "charWrite faild") );
  143. XiaomiFlowerSens_Run($hash);
  144. } else {
  145. readingsSingleUpdate ( $hash, "state", "disabled", 1 );
  146. }
  147. }
  148. sub XiaomiFlowerSens_stateRequestTimer($) {
  149. my ($hash) = @_;
  150. my $name = $hash->{NAME};
  151. RemoveInternalTimer($hash);
  152. if( !IsDisabled($name) ) {
  153. readingsSingleUpdate ( $hash, "state", "active", 1 ) if( (ReadingsVal($name, "state", 0) eq "initialized" or ReadingsVal($name, "state", 0) eq "unreachable" or ReadingsVal($name, "state", 0) eq "corrupted data" or ReadingsVal($name, "state", 0) eq "disabled" or ReadingsVal($name, "state", 0) eq "Unknown" or ReadingsVal($name, "state", 0) eq "charWrite faild") );
  154. XiaomiFlowerSens_Run($hash);
  155. } else {
  156. readingsSingleUpdate ( $hash, "state", "disabled", 1 );
  157. }
  158. InternalTimer( gettimeofday()+$hash->{INTERVAL}+int(rand(300)), "XiaomiFlowerSens_stateRequestTimer", $hash, 1 );
  159. Log3 $name, 5, "Sub XiaomiFlowerSens_stateRequestTimer ($name) - Request Timer wird aufgerufen";
  160. }
  161. sub XiaomiFlowerSens_Set($$@) {
  162. my ($hash, $name, @aa) = @_;
  163. my ($cmd, @args) = @aa;
  164. if( $cmd eq 'statusRequest' ) {
  165. return "usage: statusRequest" if( @args != 0 );
  166. XiaomiFlowerSens_stateRequest($hash);
  167. } elsif( $cmd eq 'clearFirmwareReading' ) {
  168. return "usage: clearFirmwareReading" if( @args != 0 );
  169. readingsSingleUpdate($hash,'firmware','',0);
  170. } else {
  171. my $list = "statusRequest:noArg clearFirmwareReading:noArg";
  172. return "Unknown argument $cmd, choose one of $list";
  173. }
  174. return undef;
  175. }
  176. sub XiaomiFlowerSens_Run($) {
  177. my ( $hash, $cmd ) = @_;
  178. my $name = $hash->{NAME};
  179. my $mac = $hash->{BTMAC};
  180. my $wfr;
  181. if( ReadingsVal($name, 'firmware', '') eq "2.6.2" ) {
  182. $wfr = 0;
  183. } else {
  184. $wfr = 1;
  185. }
  186. my $response_encode = XiaomiFlowerSens_forRun_encodeJSON($mac,$wfr);
  187. $hash->{helper}{RUNNING_PID} = BlockingCall("XiaomiFlowerSens_BlockingRun", $name."|".$response_encode, "XiaomiFlowerSens_BlockingDone", 30, "XiaomiFlowerSens_BlockingAborted", $hash) unless(exists($hash->{helper}{RUNNING_PID}));
  188. Log3 $name, 4, "Sub XiaomiFlowerSens_Run ($name) - start blocking call";
  189. readingsSingleUpdate ( $hash, "state", "call data", 1 ) if( ReadingsVal($name, "state", 0) eq "active" );
  190. }
  191. sub XiaomiFlowerSens_BlockingRun($) {
  192. my ($string) = @_;
  193. my ($name,$data) = split("\\|", $string);
  194. my $data_json = decode_json($data);
  195. my $mac = $data_json->{mac};
  196. my $wfr = $data_json->{wfr};
  197. Log3 $name, 4, "Sub XiaomiFlowerSens_BlockingRun ($name) - Running nonBlocking";
  198. ##### call sensor data
  199. my ($sensData,$batFwData) = XiaomiFlowerSens_callGatttool($name,$mac,$wfr);
  200. Log3 $name, 4, "Sub XiaomiFlowerSens_BlockingRun ($name) - Processing response data: $sensData";
  201. return "$name|Unknown Error, look at verbose 5 output" # if error in stdout the error will given to $sensData variable
  202. unless( defined($batFwData) );
  203. #### processing sensor respons
  204. my @dataSensor = split(" ",$sensData);
  205. return "$name|charWrite faild"
  206. unless( $dataSensor[0] ne "aa" and $dataSensor[1] ne "bb" and $dataSensor[2] ne "cc" and $dataSensor[3] ne "dd" and $dataSensor[4] ne "ee" and $dataSensor[5] ne "ff");
  207. my $temp;
  208. if( $dataSensor[1] eq "ff" ) {
  209. $temp = hex("0x".$dataSensor[1].$dataSensor[0]) - hex("0xffff");
  210. } else {
  211. $temp = hex("0x".$dataSensor[1].$dataSensor[0]);
  212. }
  213. my $lux = hex("0x".$dataSensor[4].$dataSensor[3]);
  214. my $moisture = hex("0x".$dataSensor[7]);
  215. my $fertility = hex("0x".$dataSensor[9].$dataSensor[8]);
  216. ### processing firmware and battery response
  217. my @dataBatFw = split(" ",$batFwData);
  218. my $blevel = hex("0x".$dataBatFw[0]);
  219. my $fw = ($dataBatFw[2]-30).".".($dataBatFw[4]-30).".".($dataBatFw[6]-30);
  220. ###### return processing data
  221. return "$name|corrupted data"
  222. if( $temp == 0 and $lux == 0 and $moisture == 0 and $fertility == 0 );
  223. my $response_encode = XiaomiFlowerSens_forDone_encodeJSON($temp,$lux,$moisture,$fertility,$blevel,$fw);
  224. Log3 $name, 4, "Sub XiaomiFlowerSens_BlockingRun ($name) - no dataerror, create encode json: $response_encode";
  225. return "$name|$response_encode";
  226. }
  227. sub XiaomiFlowerSens_callGatttool($@) {
  228. my ($name,$mac,$wfr) = @_;
  229. my $hci = AttrVal($name,"hciDevice","hci0");
  230. my $loop;
  231. my $wresp;
  232. my @readSensData;
  233. my @readBatFwData;
  234. $loop = 0;
  235. while ( (qx(ps ax | grep -v grep | grep "gatttool -b $mac") and $loop = 0) or (qx(ps ax | grep -v grep | grep "gatttool -b $mac") and $loop < 5) ) {
  236. Log3 $name, 4, "Sub XiaomiFlowerSens ($name) - check gattool is running. loop: $loop";
  237. sleep 0.5;
  238. $loop++;
  239. }
  240. #### Read Sensor Data
  241. ## support for Firmware 2.6.6, man muß erst einen Characterwert schreiben
  242. Log3 $name, 5, "Sub XiaomiFlowerSens_callGatttool ($name) - WFR: $wfr";
  243. if($wfr == 1) {
  244. $loop = 0;
  245. do {
  246. $wresp = qx(gatttool -i $hci -b $mac --char-write-req -a 0x33 -n A01F 2>&1 /dev/null);
  247. $loop++;
  248. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - call gatttool charWrite loop $loop";
  249. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - charWrite wresp: $wresp" if(defined($wresp) and ($wresp) );
  250. } while( ($loop < 10) and (not $wresp =~ /^Characteristic value was written successfully$/) );
  251. }
  252. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - run gatttool";
  253. $loop = 0;
  254. do {
  255. @readSensData = split(": ",qx(gatttool -i $hci -b $mac --char-read -a 0x35 2>&1 /dev/null));
  256. $loop++;
  257. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - call gatttool charRead loop $loop";
  258. } while( $loop < 10 and not $readSensData[0] =~ /^Characteristic value\/descriptor$/ );
  259. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - processing gatttool response. sensData[0]: $readSensData[0]";
  260. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - processing gatttool response. sensData: $readSensData[1]";
  261. return ($readSensData[1],undef)
  262. unless( $readSensData[0] =~ /^Characteristic value\/descriptor$/ );
  263. ### Read Firmware and Battery Data
  264. $loop = 0;
  265. do {
  266. @readBatFwData = split(": ",qx(gatttool -i $hci -b $mac --char-read -a 0x38 2>&1 /dev/null));
  267. $loop++;
  268. Log3 $name, 4, "Sub XiaomiFlowerSens ($name) - call gatttool readBatFw loop $loop";
  269. } while( $loop < 10 and not $readBatFwData[0] =~ /^Characteristic value\/descriptor$/ );
  270. Log3 $name, 4, "Sub XiaomiFlowerSens_callGatttool ($name) - processing gatttool response. batFwData: $readBatFwData[1]";
  271. return ($readBatFwData[1],undef)
  272. unless( $readBatFwData[0] =~ /^Characteristic value\/descriptor$/ );
  273. ### no Error in data string
  274. return ($readSensData[1],$readBatFwData[1])
  275. }
  276. sub XiaomiFlowerSens_forRun_encodeJSON($$) {
  277. my ($mac,$wfr) = @_;
  278. my %data = (
  279. 'mac' => $mac,
  280. 'wfr' => $wfr
  281. );
  282. return encode_json \%data;
  283. }
  284. sub XiaomiFlowerSens_forDone_encodeJSON($$$$$$) {
  285. my ($temp,$lux,$moisture,$fertility,$blevel,$fw) = @_;
  286. my %response = (
  287. 'temp' => $temp,
  288. 'lux' => $lux,
  289. 'moisture' => $moisture,
  290. 'fertility' => $fertility,
  291. 'blevel' => $blevel,
  292. 'firmware' => $fw
  293. );
  294. return encode_json \%response;
  295. }
  296. sub XiaomiFlowerSens_BlockingDone($) {
  297. my ($string) = @_;
  298. my ($name,$response) = split("\\|",$string);
  299. my $hash = $defs{$name};
  300. delete($hash->{helper}{RUNNING_PID});
  301. Log3 $name, 4, "Sub XiaomiFlowerSens_BlockingDone ($name) - Der Helper ist diabled. Daher wird hier abgebrochen" if($hash->{helper}{DISABLED});
  302. return if($hash->{helper}{DISABLED});
  303. readingsBeginUpdate($hash);
  304. if( $response eq "corrupted data" ) {
  305. readingsBulkUpdate($hash,"state","corrupted data");
  306. readingsEndUpdate($hash,1);
  307. return undef;
  308. } elsif( $response eq "charWrite faild") {
  309. readingsBulkUpdate($hash,"state","charWrite faild");
  310. readingsEndUpdate($hash,1);
  311. return undef;
  312. } elsif( $response eq "Unknown Error, look at verbose 5 output" ) {
  313. readingsBulkUpdate($hash,"lastGattError","$response");
  314. readingsBulkUpdate($hash,"state","unreachable");
  315. readingsEndUpdate($hash,1);
  316. return undef;
  317. } elsif( ref($response) eq "HASH" ) {
  318. readingsBulkUpdate($hash,"lastGattError","$response");
  319. readingsBulkUpdate($hash,"state","unreachable");
  320. readingsEndUpdate($hash,1);
  321. return undef;
  322. }
  323. my $response_json = decode_json($response);
  324. readingsBulkUpdate($hash, "batteryLevel", $response_json->{blevel});
  325. readingsBulkUpdate($hash, "battery", ($response_json->{blevel}>20?"ok":"low") );
  326. readingsBulkUpdate($hash, "temperature", $response_json->{temp}/10);
  327. readingsBulkUpdate($hash, "lux", $response_json->{lux});
  328. readingsBulkUpdate($hash, "moisture", $response_json->{moisture});
  329. readingsBulkUpdate($hash, "fertility", $response_json->{fertility});
  330. readingsBulkUpdate($hash, "firmware", $response_json->{firmware});
  331. readingsBulkUpdate($hash, "state", "active") if( ReadingsVal($name,"state", 0) eq "call data" or ReadingsVal($name,"state", 0) eq "unreachable" or ReadingsVal($name,"state", 0) eq "corrupted data" );
  332. readingsEndUpdate($hash,1);
  333. Log3 $name, 4, "Sub XiaomiFlowerSens_BlockingDone ($name) - Abschluss!";
  334. }
  335. sub XiaomiFlowerSens_BlockingAborted($) {
  336. my ($hash) = @_;
  337. my $name = $hash->{NAME};
  338. delete($hash->{helper}{RUNNING_PID});
  339. readingsSingleUpdate($hash,"state","unreachable", 1);
  340. Log3 $name, 3, "($name) Sub XiaomiFlowerSens_BlockingAborted - The BlockingCall Process terminated unexpectedly. Timedout";
  341. }
  342. 1;
  343. =pod
  344. =item device
  345. =item summary Modul to retrieves data from a Xiaomi Flower Monitor
  346. =item summary_DE Modul um Daten vom Xiaomi Flower Monitor aus zu lesen
  347. =begin html
  348. <a name="XiaomiFlowerSens"></a>
  349. <h3>Xiaomi Flower Monitor</h3>
  350. <ul>
  351. <u><b>XiaomiFlowerSens - Retrieves data from a Xiaomi Flower Monitor</b></u>
  352. <br>
  353. With this module it is possible to read the data from a sensor and to set it as reading.</br>
  354. Gatttool and hcitool is required to use this modul. (apt-get install bluez)
  355. <br><br>
  356. <a name="XiaomiFlowerSensdefine"></a>
  357. <b>Define</b>
  358. <ul><br>
  359. <code>define &lt;name&gt; XiaomiFlowerSens &lt;BT-MAC&gt;</code>
  360. <br><br>
  361. Example:
  362. <ul><br>
  363. <code>define Weihnachtskaktus XiaomiFlowerSens C4:7C:8D:62:42:6F</code><br>
  364. </ul>
  365. <br>
  366. This statement creates a XiaomiFlowerSens with the name Weihnachtskaktus and the Bluetooth Mac C4:7C:8D:62:42:6F.<br>
  367. After the device has been created, the current data of the Xiaomi Flower Monitor is automatically read from the device.
  368. </ul>
  369. <br><br>
  370. <a name="XiaomiFlowerSensreadings"></a>
  371. <b>Readings</b>
  372. <ul>
  373. <li>state - Status of the flower sensor or error message if any errors.</li>
  374. <li>battery - current battery state dependent on batteryLevel.</li>
  375. <li>batteryLevel - current battery level in percent.</li>
  376. <li>fertility - Values for the fertilizer content</li>
  377. <li>firmware - current device firmware</li>
  378. <li>lux - current light intensity</li>
  379. <li>moisture - current moisture content</li>
  380. <li>temperature - current temperature</li>
  381. </ul>
  382. <br><br>
  383. <a name="XiaomiFlowerSensset"></a>
  384. <b>Set</b>
  385. <ul>
  386. <li>statusRequest - retrieves the current state of the Xiaomi Flower Monitor.</li>
  387. <li>clearFirmwareReading - clear firmware reading for new begin.</li>
  388. <br>
  389. </ul>
  390. <br><br>
  391. <a name="NUKIDeviceattribut"></a>
  392. <b>Attributes</b>
  393. <ul>
  394. <li>disable - disables the Nuki device</li>
  395. <li>interval - interval in seconds for statusRequest</li>
  396. <br>
  397. </ul>
  398. </ul>
  399. =end html
  400. =begin html_DE
  401. =end html_DE
  402. =cut