74_XiaomiBTLESens.pm 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. ###############################################################################
  2. #
  3. # Developed with Kate
  4. #
  5. # (c) 2017-2018 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_XiaomiBTLESens.pm 16154 2018-02-11 18:09:28Z CoolTux $
  25. #
  26. ###############################################################################
  27. #$cmd = "qx(gatttool -i $hci -b $mac --char-write-req -a 0x33 -n A01F";
  28. #$cmd = "qx(gatttool -i $hci -b $mac --char-read -a 0x35"; # Sensor Daten
  29. #$cmd = "qx(gatttool -i $hci -b $mac --char-read -a 0x38"; # Firmware und Batterie
  30. # e8 00 00 58 0f 00 00 34 f1 02 02 3c 00 fb 34 9b
  31. package main;
  32. use strict;
  33. use warnings;
  34. use POSIX;
  35. use JSON;
  36. use Blocking;
  37. my $version = "2.0.9";
  38. my %XiaomiModels = (
  39. flowerSens => {'rdata' => '0x35' ,'wdata' => '0x33' ,'wdataValue' => 'A01F' ,'wdatalisten' => 0 ,'battery' => '0x38' ,'firmware' => '0x38'},
  40. thermoHygroSens => {'wdata' => '0x10' ,'wdataValue' => '0100' ,'wdatalisten' => 1 ,'battery' => '0x18' ,'firmware' => '0x24' ,'devicename' => '0x3'},
  41. );
  42. my %CallBatteryAge = ( '8h' => 28800,
  43. '16h' => 57600,
  44. '24h' => 86400,
  45. '32h' => 115200,
  46. '40h' => 144000,
  47. '48h' => 172800
  48. );
  49. # Declare functions
  50. sub XiaomiBTLESens_Initialize($);
  51. sub XiaomiBTLESens_Define($$);
  52. sub XiaomiBTLESens_Undef($$);
  53. sub XiaomiBTLESens_Attr(@);
  54. sub XiaomiBTLESens_stateRequest($);
  55. sub XiaomiBTLESens_stateRequestTimer($);
  56. sub XiaomiBTLESens_Set($$@);
  57. sub XiaomiBTLESens_Get($$@);
  58. sub XiaomiBTLESens_Notify($$);
  59. sub XiaomiBTLESens_CreateParamGatttool($@);
  60. sub XiaomiBTLESens_ExecGatttool_Run($);
  61. sub XiaomiBTLESens_ExecGatttool_Done($);
  62. sub XiaomiBTLESens_ExecGatttool_Aborted($);
  63. sub XiaomiBTLESens_ProcessingNotification($@);
  64. sub XiaomiBTLESens_WriteReadings($$);
  65. sub XiaomiBTLESens_ProcessingErrors($$);
  66. sub XiaomiBTLESens_encodeJSON($);
  67. sub XiaomiBTLESens_CallBattery_IsUpdateTimeAgeToOld($$);
  68. sub XiaomiBTLESens_CallBattery_Timestamp($);
  69. sub XiaomiBTLESens_CallBattery_UpdateTimeAge($);
  70. sub XiaomiBTLESens_CreateDevicenameHEX($);
  71. sub XiaomiBTLESens_FlowerSensHandle0x35($$);
  72. sub XiaomiBTLESens_FlowerSensHandle0x38($$);
  73. sub XiaomiBTLESens_ThermoHygroSensHandle0x18($$);
  74. sub XiaomiBTLESens_ThermoHygroSensHandle0x10($$);
  75. sub XiaomiBTLESens_ThermoHygroSensHandle0x24($$);
  76. sub XiaomiBTLESens_ThermoHygroSensHandle0x3($$);
  77. sub XiaomiBTLESens_Initialize($) {
  78. my ($hash) = @_;
  79. $hash->{SetFn} = "XiaomiBTLESens_Set";
  80. $hash->{GetFn} = "XiaomiBTLESens_Get";
  81. $hash->{DefFn} = "XiaomiBTLESens_Define";
  82. $hash->{NotifyFn} = "XiaomiBTLESens_Notify";
  83. $hash->{UndefFn} = "XiaomiBTLESens_Undef";
  84. $hash->{AttrFn} = "XiaomiBTLESens_Attr";
  85. $hash->{AttrList} = "interval ".
  86. "disable:1 ".
  87. "disabledForIntervals ".
  88. "hciDevice:hci0,hci1,hci2 ".
  89. "batteryFirmwareAge:8h,16h,24h,32h,40h,48h ".
  90. "minFertility ".
  91. "maxFertility ".
  92. "minTemp ".
  93. "maxTemp ".
  94. "minMoisture ".
  95. "maxMoisture ".
  96. "minLux ".
  97. "maxLux ".
  98. "sshHost ".
  99. "model:flowerSens,thermoHygroSens ".
  100. "blockingCallLoglevel:2,3,4,5 ".
  101. $readingFnAttributes;
  102. foreach my $d(sort keys %{$modules{XiaomiBTLESens}{defptr}}) {
  103. my $hash = $modules{XiaomiBTLESens}{defptr}{$d};
  104. $hash->{VERSION} = $version;
  105. }
  106. }
  107. sub XiaomiBTLESens_Define($$) {
  108. my ( $hash, $def ) = @_;
  109. my @a = split( "[ \t][ \t]*", $def );
  110. return "too few parameters: define <name> XiaomiBTLESens <BTMAC>" if( @a != 3 );
  111. my $name = $a[0];
  112. my $mac = $a[2];
  113. $hash->{BTMAC} = $mac;
  114. $hash->{VERSION} = $version;
  115. $hash->{INTERVAL} = 300;
  116. $hash->{helper}{CallSensDataCounter} = 0;
  117. $hash->{helper}{CallBattery} = 0;
  118. $hash->{NOTIFYDEV} = "global,$name";
  119. $hash->{loglevel} = 4;
  120. readingsSingleUpdate($hash,"state","initialized", 0);
  121. $attr{$name}{room} = "XiaomiBTLESens" if( AttrVal($name,'room','none') eq 'none' );
  122. Log3 $name, 3, "XiaomiBTLESens ($name) - defined with BTMAC $hash->{BTMAC}";
  123. $modules{XiaomiBTLESens}{defptr}{$hash->{BTMAC}} = $hash;
  124. return undef;
  125. }
  126. sub XiaomiBTLESens_Undef($$) {
  127. my ( $hash, $arg ) = @_;
  128. my $mac = $hash->{BTMAC};
  129. my $name = $hash->{NAME};
  130. RemoveInternalTimer($hash);
  131. BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID}));
  132. delete($modules{XiaomiBTLESens}{defptr}{$mac});
  133. Log3 $name, 3, "Sub XiaomiBTLESens_Undef ($name) - delete device $name";
  134. return undef;
  135. }
  136. sub XiaomiBTLESens_Attr(@) {
  137. my ( $cmd, $name, $attrName, $attrVal ) = @_;
  138. my $hash = $defs{$name};
  139. if( $attrName eq "disable" ) {
  140. if( $cmd eq "set" and $attrVal eq "1" ) {
  141. RemoveInternalTimer($hash);
  142. readingsSingleUpdate ( $hash, "state", "disabled", 1 );
  143. Log3 $name, 3, "XiaomiBTLESens ($name) - disabled";
  144. }
  145. elsif( $cmd eq "del" ) {
  146. Log3 $name, 3, "XiaomiBTLESens ($name) - enabled";
  147. }
  148. }
  149. elsif( $attrName eq "disabledForIntervals" ) {
  150. if( $cmd eq "set" ) {
  151. return "check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'"
  152. unless($attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/);
  153. Log3 $name, 3, "XiaomiBTLESens ($name) - disabledForIntervals";
  154. readingsSingleUpdate ( $hash, "state", "disabled", 1 );
  155. }
  156. elsif( $cmd eq "del" ) {
  157. Log3 $name, 3, "XiaomiBTLESens ($name) - enabled";
  158. readingsSingleUpdate ( $hash, "state", "active", 1 );
  159. }
  160. }
  161. elsif( $attrName eq "interval" ) {
  162. RemoveInternalTimer($hash);
  163. if( $cmd eq "set" ) {
  164. if( $attrVal < 120 ) {
  165. Log3 $name, 3, "XiaomiBTLESens ($name) - interval too small, please use something >= 120 (sec), default is 300 (sec)";
  166. return "interval too small, please use something >= 120 (sec), default is 300 (sec)";
  167. } else {
  168. $hash->{INTERVAL} = $attrVal;
  169. Log3 $name, 3, "XiaomiBTLESens ($name) - set interval to $attrVal";
  170. }
  171. }
  172. elsif( $cmd eq "del" ) {
  173. $hash->{INTERVAL} = 300;
  174. Log3 $name, 3, "XiaomiBTLESens ($name) - set interval to default";
  175. }
  176. }
  177. elsif( $attrName eq "blockingCallLoglevel" ) {
  178. if( $cmd eq "set" ) {
  179. $hash->{loglevel} = $attrVal;
  180. Log3 $name, 3, "XiaomiBTLESens ($name) - set blockingCallLoglevel to $attrVal";
  181. }
  182. elsif( $cmd eq "del" ) {
  183. $hash->{loglevel} = 4;
  184. Log3 $name, 3, "XiaomiBTLESens ($name) - set blockingCallLoglevel to default";
  185. }
  186. }
  187. return undef;
  188. }
  189. sub XiaomiBTLESens_Notify($$) {
  190. my ($hash,$dev) = @_;
  191. my $name = $hash->{NAME};
  192. return if (IsDisabled($name));
  193. my $devname = $dev->{NAME};
  194. my $devtype = $dev->{TYPE};
  195. my $events = deviceEvents($dev,1);
  196. return if (!$events);
  197. XiaomiBTLESens_stateRequestTimer($hash) if( (grep /^DEFINED.$name$/,@{$events}
  198. or grep /^INITIALIZED$/,@{$events}
  199. or grep /^MODIFIED.$name$/,@{$events}
  200. or grep /^DELETEATTR.$name.disable$/,@{$events}
  201. or grep /^ATTR.$name.disable.0$/,@{$events}
  202. or grep /^DELETEATTR.$name.interval$/,@{$events}
  203. or grep /^DELETEATTR.$name.model$/,@{$events}
  204. or grep /^ATTR.$name.model.+/,@{$events}
  205. or grep /^ATTR.$name.interval.[0-9]+/,@{$events} ) and $init_done and $devname eq 'global' );
  206. XiaomiBTLESens_CreateParamGatttool($hash,'read',$XiaomiModels{AttrVal($name,'model','')}{devicename}) if( AttrVal($name,'model','thermoHygroSens') eq 'thermoHygroSens'
  207. and $devname eq $name
  208. and grep /^$name.firmware.+/,@{$events} );
  209. return;
  210. }
  211. sub XiaomiBTLESens_stateRequest($) {
  212. my ($hash) = @_;
  213. my $name = $hash->{NAME};
  214. my %readings;
  215. if( AttrVal($name,'model','none') eq 'none') {
  216. readingsSingleUpdate($hash,"state","set attribute model first",1);
  217. } elsif( !IsDisabled($name) ) {
  218. if( ReadingsVal($name,'firmware','none') ne 'none' ) {
  219. return XiaomiBTLESens_CreateParamGatttool($hash,'read',$XiaomiModels{AttrVal($name,'model','')}{battery})
  220. if( XiaomiBTLESens_CallBattery_IsUpdateTimeAgeToOld($hash,$CallBatteryAge{AttrVal($name,'BatteryFirmwareAge','24h')}) );
  221. if( $hash->{helper}{CallSensDataCounter} < 1 ) {
  222. XiaomiBTLESens_CreateParamGatttool($hash,'write',$XiaomiModels{AttrVal($name,'model','')}{wdata},$XiaomiModels{AttrVal($name,'model','')}{wdataValue});
  223. $hash->{helper}{CallSensDataCounter} = $hash->{helper}{CallSensDataCounter} + 1;
  224. } else {
  225. $readings{'lastGattError'} = 'charWrite faild';
  226. XiaomiBTLESens_WriteReadings($hash,\%readings);
  227. $hash->{helper}{CallSensDataCounter} = 0;
  228. return;
  229. }
  230. } else {
  231. XiaomiBTLESens_CreateParamGatttool($hash,'read',$XiaomiModels{AttrVal($name,'model','')}{firmware});
  232. }
  233. } else {
  234. readingsSingleUpdate($hash,"state","disabled",1);
  235. }
  236. }
  237. sub XiaomiBTLESens_stateRequestTimer($) {
  238. my ($hash) = @_;
  239. my $name = $hash->{NAME};
  240. XiaomiBTLESens_stateRequest($hash) if( $init_done );
  241. InternalTimer( gettimeofday()+$hash->{INTERVAL}+int(rand(90)), "XiaomiBTLESens_stateRequestTimer", $hash );
  242. Log3 $name, 4, "XiaomiBTLESens ($name) - stateRequestTimer: Call Request Timer";
  243. }
  244. sub XiaomiBTLESens_Set($$@) {
  245. my ($hash, $name, @aa) = @_;
  246. my ($cmd, @args) = @aa;
  247. my $mod;
  248. my $handle;
  249. my $value = 'write';
  250. if( $cmd eq 'devicename' ) {
  251. return "usage: devicename <name>" if( @args < 1 );
  252. my $devicename = join( " ", @args );
  253. $mod = 'write'; $handle = $XiaomiModels{AttrVal($name,'model','')}{devicename}; $value = XiaomiBTLESens_CreateDevicenameHEX(makeDeviceName($devicename));
  254. } else {
  255. my $list = "";
  256. $list .= "devicename" if( AttrVal($name,'model','thermoHygroSens') eq 'thermoHygroSens' );
  257. return "Unknown argument $cmd, choose one of $list";
  258. }
  259. XiaomiBTLESens_CreateParamGatttool($hash,$mod,$handle,$value);
  260. return undef;
  261. }
  262. sub XiaomiBTLESens_Get($$@) {
  263. my ($hash, $name, @aa) = @_;
  264. my ($cmd, @args) = @aa;
  265. my $mod = 'read';
  266. my $handle;
  267. if( $cmd eq 'sensorData' ) {
  268. return "usage: sensorData" if( @args != 0 );
  269. XiaomiBTLESens_stateRequest($hash);
  270. } elsif( $cmd eq 'firmware' ) {
  271. return "usage: firmware" if( @args != 0 );
  272. $mod = 'read'; $handle = $XiaomiModels{AttrVal($name,'model','')}{firmware};
  273. } elsif( $cmd eq 'devicename' ) {
  274. return "usage: devicename" if( @args != 0 );
  275. $mod = 'read'; $handle = $XiaomiModels{AttrVal($name,'model','')}{devicename};
  276. } else {
  277. my $list = "sensorData:noArg firmware:noArg";
  278. $list .= " devicename:noArg" if( AttrVal($name,'model','thermoHygroSens') eq 'thermoHygroSens' );
  279. return "Unknown argument $cmd, choose one of $list";
  280. }
  281. XiaomiBTLESens_CreateParamGatttool($hash,$mod,$handle) if( $cmd ne 'sensorData' );
  282. return undef;
  283. }
  284. sub XiaomiBTLESens_CreateParamGatttool($@) {
  285. my ($hash,$mod,$handle,$value) = @_;
  286. my $name = $hash->{NAME};
  287. my $mac = $hash->{BTMAC};
  288. Log3 $name, 4, "XiaomiBTLESens ($name) - Run CreateParamGatttool with mod: $mod";
  289. if( $mod eq 'read' ) {
  290. $hash->{helper}{RUNNING_PID} = BlockingCall("XiaomiBTLESens_ExecGatttool_Run", $name."|".$mac."|".$mod."|".$handle, "XiaomiBTLESens_ExecGatttool_Done", 90, "XiaomiBTLESens_ExecGatttool_Aborted", $hash) unless( exists($hash->{helper}{RUNNING_PID}) );
  291. readingsSingleUpdate($hash,"state","read sensor data",1);
  292. Log3 $name, 5, "XiaomiBTLESens ($name) - Read XiaomiBTLESens_ExecGatttool_Run $name|$mac|$mod|$handle";
  293. } elsif( $mod eq 'write' ) {
  294. $hash->{helper}{RUNNING_PID} = BlockingCall("XiaomiBTLESens_ExecGatttool_Run", $name."|".$mac."|".$mod."|".$handle."|".$value."|".$XiaomiModels{AttrVal($name,'model','')}{wdatalisten}, "XiaomiBTLESens_ExecGatttool_Done", 90, "XiaomiBTLESens_ExecGatttool_Aborted", $hash) unless( exists($hash->{helper}{RUNNING_PID}) );
  295. readingsSingleUpdate($hash,"state","write sensor data",1);
  296. Log3 $name, 5, "XiaomiBTLESens ($name) - Write XiaomiBTLESens_ExecGatttool_Run $name|$mac|$mod|$handle|$value";
  297. }
  298. }
  299. sub XiaomiBTLESens_ExecGatttool_Run($) {
  300. my $string = shift;
  301. my ($name,$mac,$gattCmd,$handle,$value,$listen) = split("\\|", $string);
  302. my $sshHost = AttrVal($name,"sshHost","none");
  303. my $gatttool;
  304. my $json_notification;
  305. $gatttool = qx(which gatttool) if($sshHost eq 'none');
  306. $gatttool = qx(ssh $sshHost 'which gatttool') if($sshHost ne 'none');
  307. chomp $gatttool;
  308. if(defined($gatttool) and ($gatttool)) {
  309. my $cmd;
  310. my $loop;
  311. my @gtResult;
  312. my $wait = 1;
  313. my $sshHost = AttrVal($name,"sshHost","none");
  314. my $hci = AttrVal($name,"hciDevice","hci0");
  315. $cmd = "ssh $sshHost '" if($sshHost ne 'none');
  316. $cmd .= "timeout 10 " if($listen);
  317. $cmd .= "gatttool -i $hci -b $mac ";
  318. $cmd .= "--char-read -a $handle" if($gattCmd eq 'read');
  319. $cmd .= "--char-write-req -a $handle -n $value" if($gattCmd eq 'write');
  320. $cmd .= " --listen" if($listen);
  321. $cmd .= " 2>&1 /dev/null";
  322. $cmd .= "'" if($sshHost ne 'none');
  323. $cmd = "ssh $sshHost 'gatttool -i $hci -b $mac --char-write-req -a 0x33 -n A01F && gatttool -i $hci -b $mac --char-read -a 0x35 2>&1 /dev/null'" if($sshHost ne 'none' and $gattCmd eq 'write' and AttrVal($name,"model","none") eq 'flowerSens');
  324. while($wait) {
  325. my $grepGatttool;
  326. $grepGatttool = qx(ps ax| grep -E \'gatttool -i $hci -b $mac\' | grep -v grep) if($sshHost eq 'none');
  327. $grepGatttool = qx(ssh $sshHost 'ps ax| grep -E "gatttool -i $hci -b $mac" | grep -v grep') if($sshHost ne 'none');
  328. if(not $grepGatttool =~ /^\s*$/) {
  329. Log3 $name, 3, "XiaomiBTLESens ($name) - ExecGatttool_Run: another gatttool process is running. waiting...";
  330. sleep(1);
  331. } else {
  332. $wait = 0;
  333. }
  334. }
  335. $loop = 0;
  336. do {
  337. Log3 $name, 5, "XiaomiBTLESens ($name) - ExecGatttool_Run: call gatttool with command: $cmd and loop $loop";
  338. @gtResult = split(": ",qx($cmd));
  339. Log3 $name, 5, "XiaomiBTLESens ($name) - ExecGatttool_Run: gatttool loop result ".join(",", @gtResult);
  340. $loop++;
  341. $gtResult[0] = 'connect error'
  342. unless( defined($gtResult[0]) );
  343. } while( $loop < 5 and $gtResult[0] eq 'connect error' );
  344. Log3 $name, 4, "XiaomiBTLESens ($name) - ExecGatttool_Run: gatttool result ".join(",", @gtResult);
  345. $handle = '0x35' if($sshHost ne 'none' and $gattCmd eq 'write' and AttrVal($name,'model','none') eq 'flowerSens');
  346. $gattCmd = 'read' if($sshHost ne 'none' and $gattCmd eq 'write' and AttrVal($name,'model','none') eq 'flowerSens');
  347. $gtResult[1] = 'no data response'
  348. unless( defined($gtResult[1]) );
  349. if( $gtResult[1] ne 'no data response' and $listen ) {
  350. ($gtResult[1]) = split("\n",$gtResult[1]);
  351. $gtResult[1] =~ s/\\n//g;
  352. }
  353. $json_notification = XiaomiBTLESens_encodeJSON($gtResult[1]);
  354. if($gtResult[1] =~ /^([0-9a-f]{2}(\s?))*$/) {
  355. return "$name|$mac|ok|$gattCmd|$handle|$json_notification";
  356. } elsif($gtResult[0] ne 'connect error' and $gattCmd eq 'write') {
  357. if( $sshHost ne 'none' ) {
  358. XiaomiBTLESens_ExecGatttool_Run($name."|".$mac."|read|0x35");
  359. } else {
  360. return "$name|$mac|ok|$gattCmd|$handle|$json_notification";
  361. }
  362. } else {
  363. return "$name|$mac|error|$gattCmd|$handle|$json_notification";
  364. }
  365. } else {
  366. $json_notification = XiaomiBTLESens_encodeJSON('no gatttool binary found. Please check if bluez-package is properly installed');
  367. return "$name|$mac|error|$gattCmd|$handle|$json_notification";
  368. }
  369. }
  370. sub XiaomiBTLESens_ExecGatttool_Done($) {
  371. my $string = shift;
  372. my ($name,$mac,$respstate,$gattCmd,$handle,$json_notification) = split("\\|", $string);
  373. my $hash = $defs{$name};
  374. delete($hash->{helper}{RUNNING_PID});
  375. Log3 $name, 5, "XiaomiBTLESens ($name) - ExecGatttool_Done: Helper is disabled. Stop processing" if($hash->{helper}{DISABLED});
  376. return if($hash->{helper}{DISABLED});
  377. Log3 $name, 5, "XiaomiBTLESens ($name) - ExecGatttool_Done: gatttool return string: $string";
  378. my $decode_json = eval{decode_json($json_notification)};
  379. if($@){
  380. Log3 $name, 4, "XiaomiBTLESens ($name) - ExecGatttool_Done: JSON error while request: $@";
  381. }
  382. if( $respstate eq 'ok' and $gattCmd eq 'write' and AttrVal($name,'model','none') eq 'flowerSens' ) {
  383. XiaomiBTLESens_CreateParamGatttool($hash,'read',$XiaomiModels{AttrVal($name,'model','')}{rdata});
  384. } elsif( $respstate eq 'ok' ) {
  385. XiaomiBTLESens_ProcessingNotification($hash,$gattCmd,$handle,$decode_json->{gtResult});
  386. } else {
  387. XiaomiBTLESens_ProcessingErrors($hash,$decode_json->{gtResult});
  388. }
  389. }
  390. sub XiaomiBTLESens_ExecGatttool_Aborted($) {
  391. my ($hash) = @_;
  392. my $name = $hash->{NAME};
  393. my %readings;
  394. delete($hash->{helper}{RUNNING_PID});
  395. readingsSingleUpdate($hash,"state","unreachable", 1);
  396. $readings{'lastGattError'} = 'The BlockingCall Process terminated unexpectedly. Timedout';
  397. XiaomiBTLESens_WriteReadings($hash,\%readings);
  398. Log3 $name, 4, "XiaomiBTLESens ($name) - ExecGatttool_Aborted: The BlockingCall Process terminated unexpectedly. Timedout";
  399. }
  400. sub XiaomiBTLESens_ProcessingNotification($@) {
  401. my ($hash,$gattCmd,$handle,$notification) = @_;
  402. my $name = $hash->{NAME};
  403. my $readings;
  404. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification";
  405. if( AttrVal($name,'model','none') eq 'flowerSens' ) {
  406. if( $handle eq '0x38' ) {
  407. ### Flower Sens - Read Firmware and Battery Data
  408. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification: handle 0x38";
  409. $readings = XiaomiBTLESens_FlowerSensHandle0x38($hash,$notification);
  410. } elsif( $handle eq '0x35' ) {
  411. ### Flower Sens - Read Sensor Data
  412. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification: handle 0x35";
  413. $readings = XiaomiBTLESens_FlowerSensHandle0x35($hash,$notification);
  414. }
  415. } elsif( AttrVal($name,'model','none') eq 'thermoHygroSens') {
  416. if( $handle eq '0x18' ) {
  417. ### Thermo/Hygro Sens - Read Battery Data
  418. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification: handle 0x18";
  419. $readings = XiaomiBTLESens_ThermoHygroSensHandle0x18($hash,$notification);
  420. }
  421. elsif( $handle eq '0x10' ) {
  422. ### Thermo/Hygro Sens - Read Sensor Data
  423. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification: handle 0x10";
  424. $readings = XiaomiBTLESens_ThermoHygroSensHandle0x10($hash,$notification);
  425. }
  426. elsif( $handle eq '0x24' ) {
  427. ### Thermo/Hygro Sens - Read Firmware Data
  428. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification: handle 0x24";
  429. $readings = XiaomiBTLESens_ThermoHygroSensHandle0x24($hash,$notification)
  430. }
  431. elsif( $handle eq '0x3' ) {
  432. ### Thermo/Hygro Sens - Read and Write Devicename
  433. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingNotification: handle 0x3";
  434. return XiaomiBTLESens_CreateParamGatttool($hash,'read',$XiaomiModels{AttrVal($name,'model','')}{devicename}) unless($gattCmd eq 'read');
  435. $readings = XiaomiBTLESens_ThermoHygroSensHandle0x3($hash,$notification)
  436. }
  437. }
  438. XiaomiBTLESens_WriteReadings($hash,$readings);
  439. }
  440. sub XiaomiBTLESens_FlowerSensHandle0x38($$) {
  441. ### FlowerSens - Read Firmware and Battery Data
  442. my ($hash,$notification) = @_;
  443. my $name = $hash->{NAME};
  444. my %readings;
  445. Log3 $name, 4, "XiaomiBTLESens ($name) - FlowerSens Handle0x38";
  446. my @dataBatFw = split(" ",$notification);
  447. $readings{'batteryLevel'} = hex("0x".$dataBatFw[0]);
  448. $readings{'battery'} = (hex("0x".$dataBatFw[0]) > 20?"ok":"low");
  449. $readings{'firmware'} = ($dataBatFw[2]-30).".".($dataBatFw[4]-30).".".($dataBatFw[6]-30);
  450. $hash->{helper}{CallBattery} = 1;
  451. XiaomiBTLESens_CallBattery_Timestamp($hash);
  452. return \%readings;
  453. }
  454. sub XiaomiBTLESens_FlowerSensHandle0x35($$) {
  455. ### Flower Sens - Read Sensor Data
  456. my ($hash,$notification) = @_;
  457. my $name = $hash->{NAME};
  458. my %readings;
  459. Log3 $name, 4, "XiaomiBTLESens ($name) - FlowerSens Handle0x35";
  460. my @dataSensor = split(" ",$notification);
  461. return XiaomiBTLESens_stateRequest($hash)
  462. 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");
  463. if( $dataSensor[1] eq "ff" ) {
  464. $readings{'temperature'} = (hex("0x".$dataSensor[1].$dataSensor[0]) - hex("0xffff")) / 10;
  465. } else {
  466. $readings{'temperature'} = hex("0x".$dataSensor[1].$dataSensor[0]) / 10;
  467. }
  468. $readings{'lux'} = hex("0x".$dataSensor[4].$dataSensor[3]);
  469. $readings{'moisture'} = hex("0x".$dataSensor[7]);
  470. $readings{'fertility'} = hex("0x".$dataSensor[9].$dataSensor[8]);
  471. $hash->{helper}{CallBattery} = 0;
  472. return \%readings;
  473. }
  474. sub XiaomiBTLESens_ThermoHygroSensHandle0x18($$) {
  475. ### Thermo/Hygro Sens - Battery Data
  476. my ($hash,$notification) = @_;
  477. my $name = $hash->{NAME};
  478. my %readings;
  479. Log3 $name, 4, "XiaomiBTLESens ($name) - Thermo/Hygro Sens Handle0x18";
  480. chomp($notification);
  481. $readings{'batteryLevel'} = hex("0x".$notification);
  482. $readings{'battery'} = (hex("0x".$notification) > 20?"ok":"low");
  483. $hash->{helper}{CallBattery} = 1;
  484. XiaomiBTLESens_CallBattery_Timestamp($hash);
  485. return \%readings;
  486. }
  487. sub XiaomiBTLESens_ThermoHygroSensHandle0x10($$) {
  488. ### Thermo/Hygro Sens - Read Sensor Data
  489. my ($hash,$notification) = @_;
  490. my $name = $hash->{NAME};
  491. my %readings;
  492. Log3 $name, 4, "XiaomiBTLESens ($name) - Thermo/Hygro Sens Handle0x10";
  493. return XiaomiBTLESens_stateRequest($hash)
  494. unless($notification =~ /^([0-9a-f]{2}(\s?))*$/);
  495. my @numberOfHex = split(' ',$notification);
  496. $notification =~ s/\s+//g;
  497. $readings{'temperature'} = pack('H*',substr($notification,4,8));
  498. if( scalar(@numberOfHex) < 14 ) {
  499. $readings{'humidity'} = pack('H*',substr($notification,16,8));
  500. } else {
  501. $readings{'humidity'} = pack('H*',substr($notification,18,8));
  502. }
  503. $hash->{helper}{CallBattery} = 0;
  504. return \%readings;
  505. }
  506. sub XiaomiBTLESens_ThermoHygroSensHandle0x24($$) {
  507. ### Thermo/Hygro Sens - Read Firmware Data
  508. my ($hash,$notification) = @_;
  509. my $name = $hash->{NAME};
  510. my %readings;
  511. Log3 $name, 4, "XiaomiBTLESens ($name) - Thermo/Hygro Sens Handle0x24";
  512. $notification =~ s/\s+//g;
  513. $readings{'firmware'} = pack('H*',$notification);
  514. $hash->{helper}{CallBattery} = 0;
  515. return \%readings;
  516. }
  517. sub XiaomiBTLESens_ThermoHygroSensHandle0x3($$) {
  518. ### Thermo/Hygro Sens - Read and Write Devicename
  519. my ($hash,$notification) = @_;
  520. my $name = $hash->{NAME};
  521. my %readings;
  522. Log3 $name, 4, "XiaomiBTLESens ($name) - Thermo/Hygro Sens Handle0x3";
  523. $notification =~ s/\s+//g;
  524. $readings{'devicename'} = pack('H*',$notification);
  525. $hash->{helper}{CallBattery} = 0;
  526. return \%readings;
  527. }
  528. sub XiaomiBTLESens_WriteReadings($$) {
  529. my ($hash,$readings) = @_;
  530. my $name = $hash->{NAME};
  531. readingsBeginUpdate($hash);
  532. while( my ($r,$v) = each %{$readings} ) {
  533. readingsBulkUpdate($hash,$r,$v);
  534. }
  535. readingsBulkUpdateIfChanged($hash, "state", ($readings->{'lastGattError'}?'error':'active'));
  536. readingsEndUpdate($hash,1);
  537. if( AttrVal($name,'model','none') eq 'flowerSens') {
  538. if( defined($readings->{temperature}) ) {
  539. DoTrigger($name, 'minFertility ' . ($readings->{fertility}<AttrVal($name,'minFertility',0)?'low':'ok')) if( AttrVal($name,'minFertility','none') ne 'none' );
  540. DoTrigger($name, 'maxFertility ' . ($readings->{fertility}>AttrVal($name,'maxFertility',0)?'high':'ok')) if( AttrVal($name,'maxFertility','none') ne 'none' );
  541. DoTrigger($name, 'minMoisture ' . ($readings->{moisture}<AttrVal($name,'minMoisture',0)?'low':'ok')) if( AttrVal($name,'minMoisture','none') ne 'none' );
  542. DoTrigger($name, 'maxMoisture ' . ($readings->{moisture}>AttrVal($name,'maxMoisture',0)?'high':'ok')) if( AttrVal($name,'maxMoisture','none') ne 'none' );
  543. DoTrigger($name, 'minLux ' . ($readings->{lux}<AttrVal($name,'minLux',0)?'low':'ok')) if( AttrVal($name,'minLux','none') ne 'none' );
  544. DoTrigger($name, 'maxLux ' . ($readings->{lux}>AttrVal($name,'maxLux',0)?'high':'ok')) if( AttrVal($name,'maxLux','none') ne 'none' );
  545. }
  546. }
  547. if( defined($readings->{temperature}) ) {
  548. DoTrigger($name, 'minTemp ' . ($readings->{temperature}<AttrVal($name,'minTemp',0)?'low':'ok')) if( AttrVal($name,'minTemp','none') ne 'none' );
  549. DoTrigger($name, 'maxTemp ' . ($readings->{temperature}>AttrVal($name,'maxTemp',0)?'high':'ok')) if( AttrVal($name,'maxTemp','none') ne 'none' );
  550. }
  551. Log3 $name, 4, "XiaomiBTLESens ($name) - WriteReadings: Readings were written";
  552. $hash->{helper}{CallSensDataCounter} = 0;
  553. XiaomiBTLESens_stateRequest($hash) if( $hash->{helper}{CallBattery} == 1 );
  554. }
  555. sub XiaomiBTLESens_ProcessingErrors($$) {
  556. my ($hash,$notification) = @_;
  557. my $name = $hash->{NAME};
  558. my %readings;
  559. Log3 $name, 4, "XiaomiBTLESens ($name) - ProcessingErrors";
  560. $readings{'lastGattError'} = $notification;
  561. XiaomiBTLESens_WriteReadings($hash,\%readings);
  562. }
  563. #### my little Helper
  564. sub XiaomiBTLESens_encodeJSON($) {
  565. my $gtResult = shift;
  566. chomp($gtResult);
  567. my %response = (
  568. 'gtResult' => $gtResult
  569. );
  570. return encode_json \%response;
  571. }
  572. ## Routinen damit Firmware und Batterie nur alle X male statt immer aufgerufen wird
  573. sub XiaomiBTLESens_CallBattery_Timestamp($) {
  574. my $hash = shift;
  575. # get timestamp
  576. $hash->{helper}{updateTimeCallBattery} = gettimeofday(); # in seconds since the epoch
  577. $hash->{helper}{updateTimestampCallBattery} = FmtDateTime(gettimeofday());
  578. }
  579. sub XiaomiBTLESens_CallBattery_UpdateTimeAge($) {
  580. my $hash = shift;
  581. $hash->{helper}{updateTimeCallBattery} = 0 if( not defined($hash->{helper}{updateTimeCallBattery}) );
  582. my $UpdateTimeAge = gettimeofday() - $hash->{helper}{updateTimeCallBattery};
  583. return $UpdateTimeAge;
  584. }
  585. sub XiaomiBTLESens_CallBattery_IsUpdateTimeAgeToOld($$) {
  586. my ($hash,$maxAge) = @_;;
  587. return (XiaomiBTLESens_CallBattery_UpdateTimeAge($hash)>$maxAge ? 1:0);
  588. }
  589. sub XiaomiBTLESens_CreateDevicenameHEX($) {
  590. my $devicename = shift;
  591. my $devicenameHex = unpack("H*", $devicename);
  592. return $devicenameHex;
  593. }
  594. 1;
  595. =pod
  596. =item device
  597. =item summary Modul to retrieves data from a Xiaomi BTLE Sensor
  598. =item summary_DE Modul um Daten vom Xiaomi BTLE Sensor aus zu lesen
  599. =begin html
  600. <a name="XiaomiBTLESens"></a>
  601. <h3>Xiaomi BTLE Sensor</h3>
  602. <ul>
  603. <u><b>XiaomiBTLESens - Retrieves data from a Xiaomi BTLE Sensor</b></u>
  604. <br>
  605. With this module it is possible to read the data from a sensor and to set it as reading.</br>
  606. Gatttool and hcitool is required to use this modul. (apt-get install bluez)
  607. <br><br>
  608. <a name="XiaomiBTLESensdefine"></a>
  609. <b>Define</b>
  610. <ul><br>
  611. <code>define &lt;name&gt; XiaomiBTLESens &lt;BT-MAC&gt;</code>
  612. <br><br>
  613. Example:
  614. <ul><br>
  615. <code>define Weihnachtskaktus XiaomiBTLESens C4:7C:8D:62:42:6F</code><br>
  616. </ul>
  617. <br>
  618. This statement creates a XiaomiBTLESens with the name Weihnachtskaktus and the Bluetooth Mac C4:7C:8D:62:42:6F.<br>
  619. After the device has been created, the current data of the Xiaomi BTLE Sensor is automatically read from the device.
  620. </ul>
  621. <br><br>
  622. <a name="XiaomiBTLESensreadings"></a>
  623. <b>Readings</b>
  624. <ul>
  625. <li>state - Status of the flower sensor or error message if any errors.</li>
  626. <li>battery - current battery state dependent on batteryLevel.</li>
  627. <li>batteryLevel - current battery level in percent.</li>
  628. <li>fertility - Values for the fertilizer content</li>
  629. <li>firmware - current device firmware</li>
  630. <li>lux - current light intensity</li>
  631. <li>moisture - current moisture content</li>
  632. <li>temperature - current temperature</li>
  633. </ul>
  634. <br><br>
  635. <a name="XiaomiBTLESensset"></a>
  636. <b>Set</b>
  637. <ul>
  638. <li>devicename - set a devicename</li>
  639. <br>
  640. </ul>
  641. <br><br>
  642. <a name="XiaomiBTLESensget"></a>
  643. <b>Get</b>
  644. <ul>
  645. <li>sensorData - retrieves the current data of the Xiaomi sensor</li>
  646. <li>devicename - fetch devicename</li>
  647. <li>firmware - fetch firmware</li>
  648. <br>
  649. </ul>
  650. <br><br>
  651. <a name="XiaomiBTLESensattribut"></a>
  652. <b>Attributes</b>
  653. <ul>
  654. <li>disable - disables the device</li>
  655. <li>disabledForIntervals - disable device for interval time (13:00-18:30 or 13:00-18:30 22:00-23:00)</li>
  656. <li>interval - interval in seconds for statusRequest</li>
  657. <li>minFertility - min fertility value for low warn event</li>
  658. <li>maxFertility - max fertility value for High warn event</li>
  659. <li>minMoisture - min moisture value for low warn event</li>
  660. <li>maxMoisture - max moisture value for High warn event</li>
  661. <li>minTemp - min temperature value for low warn event</li>
  662. <li>maxTemp - max temperature value for high warn event</li>
  663. <li>minlux - min lux value for low warn event</li>
  664. <li>maxlux - max lux value for high warn event
  665. <br>
  666. Event Example for min/max Value's: 2017-03-16 11:08:05 XiaomiBTLESens Dracaena minMoisture low<br>
  667. Event Example for min/max Value's: 2017-03-16 11:08:06 XiaomiBTLESens Dracaena maxTemp high</li>
  668. <li>sshHost - FQD-Name or IP of ssh remote system / you must configure your ssh system for certificate authentication. For better handling you can config ssh Client with .ssh/config file</li>
  669. <li>batteryFirmwareAge - how old can the reading befor fetch new data</li>
  670. <li>blockingCallLoglevel - Blocking.pm Loglevel for BlockingCall Logoutput</li>
  671. </ul>
  672. </ul>
  673. =end html
  674. =begin html_DE
  675. <a name="XiaomiBTLESens"></a>
  676. <h3>Xiaomi BTLE Sensor</h3>
  677. <ul>
  678. <u><b>XiaomiBTLESens - liest Daten von einem Xiaomi BTLE Sensor</b></u>
  679. <br />
  680. Dieser Modul liest Daten von einem Sensor und legt sie in den Readings ab.<br />
  681. Auf dem (Linux) FHEM-Server werden gatttool und hcitool vorausgesetzt. (sudo apt install bluez)
  682. <br /><br />
  683. <a name="XiaomiBTLESensdefine"></a>
  684. <b>Define</b>
  685. <ul><br />
  686. <code>define &lt;name&gt; XiaomiBTLESens &lt;BT-MAC&gt;</code>
  687. <br /><br />
  688. Beispiel:
  689. <ul><br />
  690. <code>define Weihnachtskaktus XiaomiBTLESens C4:7C:8D:62:42:6F</code><br />
  691. </ul>
  692. <br />
  693. Der Befehl legt ein Device vom Typ XiaomiBTLESens an mit dem Namen Weihnachtskaktus und der Bluetooth MAC C4:7C:8D:62:42:6F.<br />
  694. Nach dem Anlegen des Device werden umgehend und automatisch die aktuellen Daten vom betroffenen Xiaomi BTLE Sensor gelesen.
  695. </ul>
  696. <br /><br />
  697. <a name="XiaomiBTLESensreadings"></a>
  698. <b>Readings</b>
  699. <ul>
  700. <li>state - Status des BTLE Sensor oder eine Fehlermeldung falls Fehler beim letzten Kontakt auftraten.</li>
  701. <li>battery - aktueller Batterie-Status in Abhängigkeit vom Wert batteryLevel.</li>
  702. <li>batteryLevel - aktueller Ladestand der Batterie in Prozent.</li>
  703. <li>fertility - Wert des Fruchtbarkeitssensors (Bodenleitf&auml;higkeit)</li>
  704. <li>firmware - aktuelle Firmware-Version des BTLE Sensor</li>
  705. <li>lux - aktuelle Lichtintensit&auml;t</li>
  706. <li>moisture - aktueller Feuchtigkeitswert</li>
  707. <li>temperature - aktuelle Temperatur</li>
  708. </ul>
  709. <br /><br />
  710. <a name="XiaomiBTLESensset"></a>
  711. <b>Set</b>
  712. <ul>
  713. <li></li>
  714. <br />
  715. </ul>
  716. <br /><br />
  717. <a name="XiaomiBTLESensset"></a>
  718. <b>Get</b>
  719. <ul>
  720. <li>devicename - setzt einen Devicenamen</li>
  721. <br />
  722. </ul>
  723. <br /><br />
  724. <a name="XiaomiBTLESensGet"></a>
  725. <b>Get</b>
  726. <ul>
  727. <li>sensorData - aktive Abfrage der Sensors Werte</li>
  728. <li>devicename - liest den Devicenamen aus</li>
  729. <li>firmware - liest die Firmeware aus</li>
  730. <br />
  731. </ul>
  732. <br /><br />
  733. <a name="XiaomiBTLESensattribut"></a>
  734. <b>Attribute</b>
  735. <ul>
  736. <li>disable - deaktiviert das Device</li>
  737. <li>interval - Interval in Sekunden zwischen zwei Abfragen</li>
  738. <li>disabledForIntervals - deaktiviert das Gerät für den angegebenen Zeitinterval (13:00-18:30 or 13:00-18:30 22:00-23:00)</li>
  739. <li>minFertility - min Fruchtbarkeits-Grenzwert f&uuml;r ein Ereignis minFertility low </li>
  740. <li>maxFertility - max Fruchtbarkeits-Grenzwert f&uuml;r ein Ereignis maxFertility high </li>
  741. <li>minMoisture - min Feuchtigkeits-Grenzwert f&uuml;r ein Ereignis minMoisture low </li>
  742. <li>maxMoisture - max Feuchtigkeits-Grenzwert f&uuml;r ein Ereignis maxMoisture high </li>
  743. <li>minTemp - min Temperatur-Grenzwert f&uuml;r ein Ereignis minTemp low </li>
  744. <li>maxTemp - max Temperatur-Grenzwert f&uuml;r ein Ereignis maxTemp high </li>
  745. <li>minlux - min Helligkeits-Grenzwert f&uuml;r ein Ereignis minlux low </li>
  746. <li>maxlux - max Helligkeits-Grenzwert f&uuml;r ein Ereignis maxlux high
  747. <br /><br />Beispiele f&uuml;r min/max-Ereignisse:<br />
  748. 2017-03-16 11:08:05 XiaomiBTLESens Dracaena minMoisture low<br />
  749. 2017-03-16 11:08:06 XiaomiBTLESens Dracaena maxTemp high<br /><br /></li>
  750. <li>sshHost - FQDN oder IP-Adresse eines entfernten SSH-Systems. Das SSH-System ist auf eine Zertifikat basierte Authentifizierung zu konfigurieren. Am elegantesten geschieht das mit einer .ssh/config Datei auf dem SSH-Client.</li>
  751. <li>batteryFirmwareAge - wie alt soll der Timestamp des Readings sein bevor eine Aktuallisierung statt findet</li>
  752. <li>blockingCallLoglevel - Blocking.pm Loglevel für BlockingCall Logausgaben</li>
  753. </ul>
  754. </ul>
  755. =end html_DE
  756. =cut