10_EQ3BT.pm 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. #############################################################
  2. #
  3. # EQ3BT.pm (c) by Dominik Karall, 2016-2018
  4. # dominik karall at gmail dot com
  5. # $Id: 10_EQ3BT.pm 17484 2018-10-07 18:54:14Z dominik $
  6. #
  7. # FHEM module to communicate with EQ-3 Bluetooth thermostats
  8. #
  9. #############################################################
  10. #
  11. # v2.0.5 - 20181007
  12. # - BUGFIX: ssh bugfixes by CoolTux
  13. #
  14. # v2.0.4 - 20180224
  15. # - FEATURE: support childlock
  16. #
  17. # v2.0.3 - 20171218
  18. # - FEATURE: support maxRetries and timeout attribute
  19. # maxRetries...number of tries before error is counted
  20. # timeout...timeout for the command
  21. #
  22. # v2.0.2 - 20171118
  23. # - FEATURE: support remote bluetooth interfaces via SSH (thx@Cooltux!)
  24. #
  25. # v2.0.1 - 20170204
  26. # - BUGFIX: fix lastChangeBy
  27. # - BUGFIX: fix retry of updateStatus, updateSystemInformation
  28. # if it BlockingCall timeouts
  29. #
  30. # v2.0.0 - 20170129
  31. # - FEATURE: use all available bluetooth interfaces to communicate
  32. # with the bluetooth thermostat
  33. # - FEATURE: new reading bluetoothDevice (shows used hci device)
  34. # - CHANGE: change maximum retries to 20
  35. # - FEATURE: new set function resetErrorCounters
  36. # - FEATURE: new set function resetConsumption (not today/yesterday)
  37. # - FEATURE: new reading lastChangeBy FHEM or thermostat
  38. # indicates who was responsible for the last change
  39. # - FEATURE: support $readingFnAttributes
  40. # - FEATURE: add VERSION internal and log output
  41. # - CHANGE: updateStatus is now 3min intervall starting from
  42. # last working updateStatus
  43. # - BUGFIX: do not run parallel gatttool commands for the same device
  44. #
  45. # v1.1.3 - 20161211
  46. # - BUGFIX: better error handling if no notification was received
  47. # - BUGFIX: update system information fixed
  48. # - CHANGE: allow multiple gatttools to be executed in parallel
  49. # - CHANGE: remove error reading
  50. # - CHANGE: add errorCounters based on function (update/...)
  51. # which will be increased if reading from the thermostat
  52. # fails 30 times for one command
  53. # - BUGFIX: retry mechanism for commands with notifications (updateStatus)
  54. # - BUGFIX: remain consumption values after restart
  55. #
  56. # v1.1.2 - 20161108
  57. # - FEATURE: support set <name> eco (eco temperature)
  58. # - FEATURE: support set <name> comfort (comfort temperature)
  59. # - CHANGE: updated commandref
  60. #
  61. # v1.1.1 - 20161106
  62. # - FEATURE: new reading consumption today/yesterday
  63. # - FEATURE: new reading firmware which shows the current version
  64. # - FEATURE: support set <name> mode automatic/manual
  65. #
  66. # v1.1.0 - 20161105
  67. # - CHANGE: code cleanup to make support of new functions easier
  68. # - FEATURE: support boost on/off command
  69. # - BUGFIX: redirect stderr to stdout to avoid "Device or ressource busy"
  70. # and other error messages in the log output, only
  71. # if an action fails 20 times an error will be shown in the log
  72. #
  73. # v1.0.7 - 20161101
  74. # - FEATURE: new reading consumption
  75. # calculation based on valvePosition and time (unit = %h)
  76. # - FEATURE: new reading battery
  77. # - FEATURE: new reading boost
  78. # - FEATURE: new reading windowOpen
  79. # - CHANGE: change mode reading to Automatic/Manual only
  80. # - FEATURE: new reading ecoMode (=holiday)
  81. #
  82. # v1.0.6 - 20161028
  83. # - BUGFIX: support temperature down to 4.5 (=OFF) degrees
  84. #
  85. # v1.0.5 - 20161027
  86. # - BUGFIX: fix wrong date/time after updateStatus again
  87. #
  88. # v1.0.4 - 20161025
  89. # - BUGFIX: remove unnecessary scan command on define
  90. #
  91. # v1.0.3 - 20161024
  92. # - BUGFIX: another fix for retry mechanism
  93. # - BUGFIX: wait before gatttool execution when
  94. # another gatttool/hcitool process is running
  95. # - BUGFIX: fix wrong date/time after updateStatus
  96. #
  97. # v1.0.2 - 20161020
  98. # - FEATURE: automatically pair/trust device on define
  99. # - FEATURE: add updateStatus method to update all values
  100. # - BUGFIX: fix retry mechanism for setDesiredTemperature
  101. # - BUGFIX: fix valvePosition value
  102. # - BUGFIX: fix uninitialized value error
  103. # - BUGFIX: RemoveTimer if set desired temp works again
  104. # - BUGFIX: set error reading to "" after it works again
  105. # - BUGFIX: disconnect device on define (startup)
  106. #
  107. # v1.0.1 - 20161016
  108. # - FEATURE: read mode/desiredTemp/valvePos every 2 hours
  109. # might have impact on battery life!
  110. # - CHANGED: temperature renamed to desiredTemperature
  111. # - FEATURE: retry setTemperature 20 times if it fails
  112. #
  113. # v1.0.0 - 20161015
  114. # - FEATURE: first public release
  115. #
  116. # NOTES
  117. # command dec
  118. # DONE: boost mode command 69 00/01
  119. # temperature offset 19 (x*2)+7
  120. # request profile 32 01-07
  121. # vacation mode 64 ...
  122. # system info 00 => frameType=1,version=value[1],typeCode=value[2]
  123. # window 20 t*2 time*5
  124. # factory reset -16
  125. # DONE: comfort temp 67
  126. # lock -128 00/01
  127. # DONE: mode 64 mode<<6
  128. # DONE: temp 65 temp*2
  129. # timer 3...
  130. # start FW update -96
  131. # DONE: eco mode 68
  132. # FW data -95 ...
  133. # profile set 16 ...
  134. # set tempconf 17 comfort*2 eco*2
  135. #
  136. # TODOs
  137. # - create virtual device (wohnzimmer)
  138. # - read/set eco/comfort temperature
  139. # - read/set tempOffset
  140. # - read/set windowOpen time settings
  141. # - read/set profiles per day
  142. #
  143. #############################################################
  144. package main;
  145. use strict;
  146. use warnings;
  147. use Blocking;
  148. use Encode;
  149. use SetExtensions;
  150. sub EQ3BT_Initialize($) {
  151. my ($hash) = @_;
  152. $hash->{DefFn} = 'EQ3BT_Define';
  153. $hash->{UndefFn} = 'EQ3BT_Undef';
  154. $hash->{GetFn} = 'EQ3BT_Get';
  155. $hash->{SetFn} = 'EQ3BT_Set';
  156. $hash->{AttrFn} = 'EQ3BT_Attribute';
  157. $hash->{AttrList} = 'sshHost maxRetries timeout blockingCallLoglevel '.
  158. $readingFnAttributes;
  159. return undef;
  160. }
  161. sub EQ3BT_Define($$) {
  162. #save BTMAC address
  163. my ($hash, $def) = @_;
  164. my @a = split("[ \t]+", $def);
  165. my $name = $a[0];
  166. my $mac;
  167. my $sshHost;
  168. $hash->{STATE} = "initialized";
  169. $hash->{VERSION} = "2.0.5";
  170. $hash->{loglevel} = 4;
  171. Log3 $hash, 3, "EQ3BT: EQ-3 Bluetooth Thermostat ".$hash->{VERSION};
  172. if (int(@a) > 4) {
  173. return 'EQ3BT: Wrong syntax, must be define <name> EQ3BT <mac address> "<sshHost-IP>"';
  174. } elsif(int(@a) == 3) {
  175. $mac = $a[2];
  176. $hash->{MAC} = $a[2];
  177. } elsif(int(@a) == 4) {
  178. $mac = $a[2];
  179. $hash->{MAC} = $a[2];
  180. $attr{$name}{sshHost} = $a[3];
  181. }
  182. EQ3BT_updateHciDevicelist($hash);
  183. BlockingCall("EQ3BT_pairDevice", $name."|".$hash->{MAC});
  184. RemoveInternalTimer($hash);
  185. InternalTimer(gettimeofday()+60, "EQ3BT_updateStatus", $hash, 0);
  186. InternalTimer(gettimeofday()+20, "EQ3BT_updateSystemInformation", $hash, 0);
  187. return undef;
  188. }
  189. sub EQ3BT_updateHciDevicelist {
  190. my ($hash) = @_;
  191. my $name = $hash->{NAME};
  192. #check for hciX devices
  193. $hash->{helper}{hcidevices} = ();
  194. my @btDevices;
  195. my $sshHost = AttrVal($name,"sshHost","none");
  196. if( $sshHost ne 'none' ) {
  197. @btDevices = split("\n", qx(ssh $sshHost 'hcitool dev'));
  198. } else {
  199. @btDevices = split("\n", qx(hcitool dev));
  200. }
  201. foreach my $btDevLine (@btDevices) {
  202. if($btDevLine =~ /hci(.)/) {
  203. push(@{$hash->{helper}{hcidevices}}, $1);
  204. }
  205. }
  206. $hash->{helper}{currenthcidevice} = 0;
  207. readingsSingleUpdate($hash, "bluetoothDevice", "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}], 1);
  208. return undef;
  209. }
  210. sub EQ3BT_pairDevice {
  211. my ($string) = @_;
  212. my ($name, $mac) = split("\\|", $string);
  213. my $sshHost = AttrVal($name,"sshHost","none");
  214. if( $sshHost ne 'none' ) {
  215. qx(ssh $sshHost 'echo "pair $mac\\n";sleep 7;echo "trust $mac\\ndisconnect $mac\\n";sleep 2; echo "quit\\n" | bluetoothctl');
  216. } else {
  217. qx(echo "pair $mac\\n";sleep 7;echo "trust $mac\\ndisconnect $mac\\n";sleep 2; echo "quit\\n" | bluetoothctl);
  218. }
  219. return $name;
  220. }
  221. sub EQ3BT_Attribute($$$$) {
  222. my ( $cmd, $name, $attrName, $attrVal ) = @_;
  223. my $hash = $defs{$name};
  224. if($cmd eq "set") {
  225. if( $attrName eq "blockingCallLoglevel" ) {
  226. $hash->{loglevel} = $attrVal;
  227. Log3 $name, 3, "EQ3BT ($name) - set blockingCallLoglevel to $attrVal";
  228. }
  229. } elsif($cmd eq "del") {
  230. if( $attrName eq "blockingCallLoglevel" ) {
  231. $hash->{loglevel} = 4;
  232. Log3 $name, 3, "EQ3BT ($name) - set blockingCallLoglevel to $attrVal";
  233. }
  234. }
  235. return undef;
  236. }
  237. sub EQ3BT_Set($@) {
  238. #set temperature/mode/...
  239. #BlockingCall for gatttool
  240. #handle result from BlockingCall in separate function and
  241. # write result into readings
  242. #
  243. my ($hash, $name, @params) = @_;
  244. my $workType = shift(@params);
  245. my $list = "desiredTemperature:slider,4.5,0.5,29.5,1 updateStatus:noArg boost:on,off mode:manual,automatic eco:noArg comfort:noArg ".
  246. "resetErrorCounters:noArg resetConsumption:noArg childlock:on,off";
  247. # check parameters for set function
  248. if($workType eq "?") {
  249. return SetExtensions($hash, $list, $name, $workType, @params);
  250. }
  251. if($workType eq "desiredTemperature") {
  252. return "EQ3BT: desiredTemperature requires <temperature> in celsius degrees as additional parameter" if(int(@params) < 1);
  253. return "EQ3BT: desiredTemperature supports temperatures from 4.5 - 29.5 degrees" if($params[0]<4.5 || $params[0]>29.5);
  254. EQ3BT_setDesiredTemperature($hash, $params[0]);
  255. } elsif($workType eq "updateStatus") {
  256. $hash->{helper}{retryUpdateStatusCounter} = 0;
  257. EQ3BT_updateStatus($hash, 1);
  258. } elsif($workType eq "boost") {
  259. return "EQ3BT: boost requires on/off as additional parameter" if(int(@params) < 1);
  260. EQ3BT_setBoost($hash, $params[0]);
  261. } elsif($workType eq "mode") {
  262. return "EQ3BT: mode requires automatic/manual as additional parameter" if(int(@params) < 1);
  263. EQ3BT_setMode($hash, $params[0]);
  264. } elsif($workType eq "eco") {
  265. EQ3BT_setEco($hash);
  266. } elsif($workType eq "comfort") {
  267. EQ3BT_setComfort($hash);
  268. } elsif($workType eq "resetErrorCounters") {
  269. EQ3BT_setResetErrorCounters($hash);
  270. } elsif($workType eq "resetConsumption") {
  271. EQ3BT_setResetConsumption($hash);
  272. } elsif($workType eq "childlock") {
  273. return "EQ3BT: childlock requires on/off as additional parameter" if(int(@params) < 1);
  274. EQ3BT_setChildlock($hash, $params[0]);
  275. } elsif($workType eq "holidaymode") {
  276. return "EQ3BT: holidaymode requires YYMMDDHHMM as additional parameter" if(int(@params) < 1);
  277. EQ3BT_setHolidaymode($hash, $params[0]);
  278. } elsif($workType eq "datetime") {
  279. return "EQ3BT: datetime requires YYMMDDHHMM as additional parameter" if(int(@params) < 1);
  280. EQ3BT_setDatetime($hash, $params[0]);
  281. } elsif($workType eq "window") {
  282. return "EQ3BT: windows requires open/closed as additional parameter" if(int(@params) < 1);
  283. EQ3BT_setWindow($hash, $params[0]);
  284. } elsif($workType eq "program") {
  285. return "EQ3BT: programming the device is not supported yet";
  286. } else {
  287. return SetExtensions($hash, $list, $name, $workType, @params);
  288. }
  289. return undef;
  290. }
  291. ### resetErrorCounters ###
  292. sub EQ3BT_setResetErrorCounters {
  293. my ($hash) = @_;
  294. foreach my $reading (keys %{ $hash->{READINGS} }) {
  295. if($reading =~ /errorCount-.*/) {
  296. readingsSingleUpdate($hash, $reading, 0, 1);
  297. }
  298. }
  299. return undef;
  300. }
  301. ### resetConsumption ###
  302. sub EQ3BT_setResetConsumption {
  303. my ($hash) = @_;
  304. readingsSingleUpdate($hash, "consumption", 0, 1);
  305. return undef;
  306. }
  307. ### updateSystemInformation ###
  308. sub EQ3BT_updateSystemInformation {
  309. my ($hash) = @_;
  310. my $name = $hash->{NAME};
  311. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|updateSystemInformation|0x0411|00|listen", "EQ3BT_processGatttoolResult", 300, "EQ3BT_updateSystemInformationFailed", $hash);
  312. }
  313. sub EQ3BT_updateSystemInformationSuccessful {
  314. my ($hash, $handle, $value) = @_;
  315. InternalTimer(gettimeofday()+7200+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
  316. return undef;
  317. }
  318. sub EQ3BT_updateSystemInformationRetry {
  319. my ($hash) = @_;
  320. EQ3BT_updateSystemInformation($hash);
  321. return undef;
  322. }
  323. sub EQ3BT_updateSystemInformationFailed {
  324. my ($hash) = @_;
  325. InternalTimer(gettimeofday()+7000+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
  326. return undef;
  327. }
  328. ### updateStatus ###
  329. sub EQ3BT_updateStatus {
  330. my ($hash) = @_;
  331. my $name = $hash->{NAME};
  332. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|updateStatus|0x0411|03|listen", "EQ3BT_processGatttoolResult", 300, "EQ3BT_updateStatusFailed", $hash);
  333. }
  334. sub EQ3BT_updateStatusSuccessful {
  335. my ($hash, $handle, $value) = @_;
  336. InternalTimer(gettimeofday()+140+int(rand(60)), "EQ3BT_updateStatus", $hash, 0);
  337. return undef;
  338. }
  339. sub EQ3BT_updateStatusRetry {
  340. my ($hash) = @_;
  341. EQ3BT_updateStatus($hash);
  342. return undef;
  343. }
  344. sub EQ3BT_updateStatusFailed {
  345. my ($hash, $handle, $value) = @_;
  346. InternalTimer(gettimeofday()+170+int(rand(60)), "EQ3BT_updateStatus", $hash, 0);
  347. return undef;
  348. }
  349. ### setDesiredTemperature ###
  350. sub EQ3BT_setDesiredTemperature($$) {
  351. my ($hash, $desiredTemp) = @_;
  352. my $name = $hash->{NAME};
  353. my $eq3Temp = sprintf("%02X", $desiredTemp * 2);
  354. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|setDesiredTemperature|0x0411|41".$eq3Temp, "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  355. return undef;
  356. }
  357. sub EQ3BT_setDesiredTemperatureSuccessful {
  358. my ($hash, $handle, $tempVal) = @_;
  359. my $temp = (hex($tempVal) - 0x4100) / 2;
  360. readingsSingleUpdate($hash, "desiredTemperature", sprintf("%.1f", $temp), 1);
  361. return undef;
  362. }
  363. sub EQ3BT_setDesiredTemperatureRetry {
  364. my ($hash) = @_;
  365. EQ3BT_retryGatttool($hash, "setDesiredTemperature");
  366. return undef;
  367. }
  368. ### setBoost ###
  369. sub EQ3BT_setBoost {
  370. my ($hash, $onoff) = @_;
  371. my $name = $hash->{NAME};
  372. my $data = "01";
  373. $data = "00" if($onoff eq "off");
  374. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|setBoost|0x0411|45".$data, "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  375. return undef;
  376. }
  377. sub EQ3BT_setBoostSuccessful {
  378. my ($hash, $handle, $value) = @_;
  379. my $val = (hex($value) - 0x4500);
  380. readingsSingleUpdate($hash, "boost", $val, 1);
  381. return undef;
  382. }
  383. sub EQ3BT_setBoostRetry {
  384. my ($hash) = @_;
  385. EQ3BT_retryGatttool($hash, "setBoost");
  386. return undef;
  387. }
  388. ### setMode ###
  389. sub EQ3BT_setMode {
  390. my ($hash, $mode) = @_;
  391. my $name = $hash->{NAME};
  392. my $data = "40";
  393. $data = "00" if($mode eq "automatic");
  394. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|setMode|0x0411|40".$data."|listen", "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  395. return undef;
  396. }
  397. sub EQ3BT_setModeSuccessful {
  398. my ($hash, $handle, $value) = @_;
  399. return undef;
  400. }
  401. sub EQ3BT_setModeRetry {
  402. my ($hash) = @_;
  403. EQ3BT_retryGatttool($hash, "setMode");
  404. return undef;
  405. }
  406. ### setEco ###
  407. sub EQ3BT_setEco {
  408. my ($hash) = @_;
  409. my $name = $hash->{NAME};
  410. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|setEco|0x0411|44|listen", "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  411. return undef;
  412. }
  413. sub EQ3BT_setEcoSuccessful {
  414. my ($hash, $handle, $value) = @_;
  415. return undef;
  416. }
  417. sub EQ3BT_setEcoRetry {
  418. my ($hash) = @_;
  419. EQ3BT_retryGatttool($hash, "setEco");
  420. return undef;
  421. }
  422. ### setComfort ###
  423. sub EQ3BT_setComfort {
  424. my ($hash) = @_;
  425. my $name = $hash->{NAME};
  426. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|setComfort|0x0411|43|listen", "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  427. return undef;
  428. }
  429. sub EQ3BT_setComfortSuccessful {
  430. my ($hash, $handle, $value) = @_;
  431. return undef;
  432. }
  433. sub EQ3BT_setComfortRetry {
  434. my ($hash) = @_;
  435. EQ3BT_retryGatttool($hash, "setEco");
  436. return undef;
  437. }
  438. ### Gatttool functions ###
  439. sub EQ3BT_retryGatttool {
  440. my ($hash, $workType) = @_;
  441. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $hash->{NAME}."|".$hash->{MAC}."|$workType|".$hash->{helper}{"handle$workType"}."|".$hash->{helper}{"value$workType"}."|".$hash->{helper}{"listen$workType"}, "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  442. return undef;
  443. }
  444. sub EQ3BT_execGatttool($) {
  445. my ($string) = @_;
  446. my ($name, $mac, $workType, $handle, $value, $listen) = split("\\|", $string);
  447. my $wait = 1;
  448. my $hash = $main::defs{$name};
  449. my $sshHost = AttrVal($name,"sshHost","none");
  450. my $gatttool; # = qx(which gatttool);
  451. $gatttool = qx(which gatttool) if($sshHost eq 'none');
  452. $gatttool = qx(ssh $sshHost 'which gatttool') if($sshHost ne 'none');
  453. chomp $gatttool;
  454. #if(-x $gatttool) {
  455. if(defined($gatttool) and ($gatttool)) {
  456. my $gtResult;
  457. my $cmd;
  458. my $hciDevice = "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}];
  459. while($wait) {
  460. my $grepGatttool = qx(ps ax| grep -E \'gatttool -b $mac\' | grep -v grep);
  461. if(not $grepGatttool =~ /^\s*$/) {
  462. #another gattool is running
  463. Log3 $name, 5, "EQ3BT ($name): another gatttool process is running. waiting...";
  464. sleep(1);
  465. } else {
  466. $wait = 0;
  467. }
  468. }
  469. $cmd .= "ssh $sshHost '" if($sshHost ne 'none');
  470. $cmd .= "timeout " . AttrVal($name, "timeout", 15) . " " if($listen);
  471. $cmd .= "gatttool -i $hciDevice -b $mac ";
  472. $cmd .= "--char-write-req -a $handle -n $value";
  473. $cmd .= " --listen" if($listen);
  474. $cmd .= " 2>&1 /dev/null";
  475. $cmd .= "'" if($sshHost ne 'none');
  476. if($value eq "03") {
  477. my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
  478. my $currentDate = sprintf("%02X%02X%02X%02X%02X", $year+1900-2000, $mon+1, $mday, $hour, $min);
  479. $value .= $currentDate;
  480. }
  481. #my $cmd = "gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value";
  482. # if( $sshHost ne 'none' ) {
  483. # $cmd = "ssh $sshHost 'gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value";
  484. # } else {
  485. # $cmd = "gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value";
  486. # }
  487. #
  488. # if(defined($listen) && $listen eq "listen") {
  489. # $cmd = "timeout ".AttrVal($name, "timeout", 15)." ".$cmd." --listen";
  490. # }
  491. #
  492. # #redirect stderr to stdout
  493. # if( $sshHost ne 'none' ) {
  494. # $cmd .= " 2>&1'";
  495. # } else {
  496. # $cmd .= " 2>&1";
  497. # }
  498. Log3 $name, 5, "EQ3BT ($name): $cmd";
  499. $gtResult = qx($cmd);
  500. chomp $gtResult;
  501. my @gtResultArr = split("\n", $gtResult);
  502. Log3 $name, 4, "EQ3BT ($name): gatttool result: ".join(",", @gtResultArr);
  503. if(defined($gtResultArr[0]) && $gtResultArr[0] eq "Characteristic value was written successfully") {
  504. #read notification
  505. if(defined($gtResultArr[1]) && $gtResultArr[1] =~ /Notification handle = 0x0421 value: (.*)/) {
  506. return "$name|$mac|ok|$workType|$handle|$value|$1";
  507. } else {
  508. if(defined($listen) && $listen eq "listen") {
  509. return "$name|$mac|error|$workType|$handle|$value|notification missing";
  510. } else {
  511. return "$name|$mac|ok|$workType|$handle|$value";
  512. }
  513. }
  514. } else {
  515. return "$name|$mac|error|$workType|$handle|$value|$workType failed";
  516. }
  517. } else {
  518. return "$name|$mac|error|$workType|$handle|$value|no gatttool binary found. Please check if bluez-package is properly installed";
  519. }
  520. }
  521. sub EQ3BT_processGatttoolResult($) {
  522. my ($string) = @_;
  523. return unless(defined($string));
  524. my @a = split("\\|", $string);
  525. my $name = $a[0];
  526. my $hash = $defs{$name};
  527. my $mac = $a[1];
  528. my $ret = $a[2];
  529. my $workType = $a[3];
  530. my $handle = $a[4];
  531. my $value = $a[5];
  532. my $notification = $a[6];
  533. delete($hash->{helper}{RUNNING_PID});
  534. Log3 $hash, 5, "EQ3BT ($name): gatttool return string: $string";
  535. $hash->{helper}{"handle$workType"} = $handle;
  536. $hash->{helper}{"value$workType"} = $value;
  537. $hash->{helper}{"listen$workType"} = $notification;
  538. if($ret eq "ok") {
  539. #process notification
  540. if(defined($notification)) {
  541. EQ3BT_processNotification($hash, $notification);
  542. }
  543. if($workType =~ /set.*/) {
  544. readingsSingleUpdate($hash, "lastChangeBy", "FHEM", 1);
  545. }
  546. #call WorkTypeSuccessful function
  547. my $call = "EQ3BT_".$workType."Successful";
  548. no strict "refs";
  549. eval {
  550. &{$call}($hash, $handle, $value);
  551. };
  552. use strict "refs";
  553. RemoveInternalTimer($hash, "EQ3BT_".$workType."Retry");
  554. $hash->{helper}{"retryCounter$workType"} = 0;
  555. } else {
  556. $hash->{helper}{"retryCounter$workType"} = 0 if(!defined($hash->{helper}{"retryCounter$workType"}));
  557. $hash->{helper}{"retryCounter$workType"}++;
  558. Log3 $hash, 4, "EQ3BT ($name): $workType failed ($handle, $value, $notification)";
  559. if ($hash->{helper}{"retryCounter$workType"} > AttrVal($name, "maxRetries", 20)) {
  560. my $errorCount = ReadingsVal($hash->{NAME}, "errorCount-$workType", 0);
  561. readingsSingleUpdate($hash, "errorCount-$workType", $errorCount+1, 1);
  562. Log3 $hash, 3, "EQ3BT ($name): $workType, $handle, $value failed 20 times.";
  563. $hash->{helper}{"retryCounter$workType"} = 0;
  564. $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0;
  565. #call WorkTypeFailed function
  566. my $call = "EQ3BT_".$workType."Failed";
  567. no strict "refs";
  568. eval {
  569. &{$call}($hash, $handle, $value);
  570. };
  571. use strict "refs";
  572. #update hci devicelist
  573. EQ3BT_updateHciDevicelist($hash);
  574. } else {
  575. $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0 if(!defined($hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}}));
  576. $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}}++;
  577. if ($hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} > 7) {
  578. #reset error counter
  579. $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0;
  580. #use next hci device next time
  581. $hash->{helper}{currenthcidevice} += 1;
  582. my $maxHciDevices = @{ $hash->{helper}{hcidevices} } - 1;
  583. if($hash->{helper}{currenthcidevice} > $maxHciDevices) {
  584. $hash->{helper}{currenthcidevice} = 0;
  585. }
  586. #update reading
  587. readingsSingleUpdate($hash, "bluetoothDevice", "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}], 1);
  588. }
  589. InternalTimer(gettimeofday()+3+int(rand(5)), "EQ3BT_".$workType."Retry", $hash, 0);
  590. }
  591. }
  592. return undef;
  593. }
  594. sub EQ3BT_processNotification {
  595. my ($hash, $notification) = @_;
  596. my @vals = split(" ", $notification);
  597. my $frameType = $vals[0];
  598. if($frameType eq "01") {
  599. my $version = hex($vals[1]);
  600. my $typeCode = hex($vals[2]);
  601. readingsSingleUpdate($hash, "firmware", $version, 1);
  602. #readingsSingleUpdate($hash, "typeCode", $typeCode, 1);
  603. } elsif($frameType eq "02") {
  604. return undef if(!defined($vals[2]));
  605. #vals[2]
  606. my $mode = hex($vals[2]) & 1;
  607. my $modeStr = "Manual";
  608. if($mode == 0) {
  609. $modeStr = "Automatic";
  610. }
  611. my $eco = (hex($vals[2]) & 2) >> 1;
  612. my $isBoost = (hex($vals[2]) & 4) >> 2;
  613. my $dst = (hex($vals[2]) & 8) >> 3;
  614. my $wndOpen = (hex($vals[2]) & 16) >> 4;
  615. my $locked = (hex($vals[2]) & 32) >> 5;
  616. my $unknown = (hex($vals[2]) & 64) >> 6;
  617. my $isLowBattery = (hex($vals[2]) & 128) >> 7;
  618. my $batteryStr = "ok";
  619. if($isLowBattery > 0) {
  620. $batteryStr = "low";
  621. }
  622. #vals[3]
  623. my $pct = hex($vals[3]);
  624. #vals[5]
  625. my $temp = hex($vals[5]) / 2;
  626. my $timeSinceLastChange = ReadingsAge($hash->{NAME}, "valvePosition", 0);
  627. my $consumption = ReadingsVal($hash->{NAME}, "consumption", 0);
  628. my $consumptionToday = ReadingsVal($hash->{NAME}, "consumptionToday", 0);
  629. my $consumptionTodaySecSinceLastChange = ReadingsAge($hash->{NAME}, "consumptionToday", 0);
  630. my $oldVal = ReadingsVal($hash->{NAME}, "valvePosition", 0);
  631. my $consumptionDiff = 0;
  632. if($timeSinceLastChange < 600) {
  633. $consumptionDiff += ($oldVal + $pct) / 2 * $timeSinceLastChange / 3600;
  634. }
  635. EQ3BT_readingsSingleUpdateIfChanged($hash, "consumption", sprintf("%.3f", $consumption+$consumptionDiff));
  636. my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
  637. if($consumptionTodaySecSinceLastChange > ($hour*3600+$min*60+$sec)) {
  638. readingsSingleUpdate($hash, "consumptionYesterday", $consumptionToday + $consumptionDiff/2, 1);
  639. readingsSingleUpdate($hash, "consumptionToday", 0 + $consumptionDiff/2, 1);
  640. } else {
  641. EQ3BT_readingsSingleUpdateIfChanged($hash, "consumptionToday", sprintf("%.3f", $consumptionToday+$consumptionDiff));
  642. }
  643. readingsSingleUpdate($hash, "valvePosition", $pct, 1);
  644. #changes below this line will set lastchangeby
  645. EQ3BT_readingsSingleUpdateIfChanged($hash, "windowOpen", $wndOpen, 1);
  646. EQ3BT_readingsSingleUpdateIfChanged($hash, "childlock", $locked, 1);
  647. EQ3BT_readingsSingleUpdateIfChanged($hash, "ecoMode", $eco, 1);
  648. EQ3BT_readingsSingleUpdateIfChanged($hash, "battery", $batteryStr, 1);
  649. EQ3BT_readingsSingleUpdateIfChanged($hash, "boost", $isBoost, 1);
  650. EQ3BT_readingsSingleUpdateIfChanged($hash, "mode", $modeStr, 1);
  651. EQ3BT_readingsSingleUpdateIfChanged($hash, "desiredTemperature", sprintf("%.1f", $temp), 1);
  652. }
  653. return undef;
  654. }
  655. sub EQ3BT_readingsSingleUpdateIfChanged {
  656. my ($hash, $reading, $value, $setLastChange) = @_;
  657. my $curVal = ReadingsVal($hash->{NAME}, $reading, "");
  658. if($curVal ne $value) {
  659. readingsSingleUpdate($hash, $reading, $value, 1);
  660. if(defined($setLastChange)) {
  661. readingsSingleUpdate($hash, "lastChangeBy", "Thermostat", 1);
  662. }
  663. }
  664. }
  665. sub EQ3BT_killGatttool($) {
  666. }
  667. sub EQ3BT_setDaymode($) {
  668. my ($hash) = @_;
  669. }
  670. sub EQ3BT_setNightmode($) {
  671. my ($hash) = @_;
  672. }
  673. sub EQ3BT_setChildlock($$) {
  674. my ($hash, $onoff) = @_;
  675. my $name = $hash->{NAME};
  676. my $data = "01";
  677. $data = "00" if($onoff eq "off");
  678. $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|setChildlock|0x0411|80".$data, "EQ3BT_processGatttoolResult", 60, "EQ3BT_killGatttool", $hash);
  679. return undef;
  680. }
  681. sub EQ3BT_setChildlockSuccessful {
  682. my ($hash, $handle, $value) = @_;
  683. my $val = (hex($value) - 0x8000);
  684. readingsSingleUpdate($hash, "childlock", $val, 1);
  685. return undef;
  686. }
  687. sub EQ3BT_setChildlockRetry {
  688. my ($hash) = @_;
  689. EQ3BT_retryGatttool($hash, "setChildlock");
  690. return undef;
  691. }
  692. sub EQ3BT_setHolidaymode($$) {
  693. my ($hash, $holidayEndTime) = @_;
  694. }
  695. sub EQ3BT_setDatetime($$) {
  696. my ($hash, $currentDatetime) = @_;
  697. }
  698. sub EQ3BT_setWindow($$) {
  699. my ($hash, $desiredState) = @_;
  700. }
  701. sub EQ3BT_setProgram($$) {
  702. my ($hash, $program) = @_;
  703. }
  704. sub EQ3BT_Undef($) {
  705. my ($hash) = @_;
  706. #remove internal timer
  707. RemoveInternalTimer($hash);
  708. return undef;
  709. }
  710. sub EQ3BT_Get($$) {
  711. return undef;
  712. }
  713. 1;
  714. =pod
  715. =item device
  716. =item summary Control EQ3 Bluetooth Smart Radiator Thermostat
  717. =item summary_DE Steuerung des EQ3 Bluetooth Thermostats
  718. =begin html
  719. <a name="EQ3BT"></a>
  720. <h3>EQ3BT</h3>
  721. <ul>
  722. EQ3BT is used to control a EQ3 Bluetooth Smart Radiator Thermostat<br><br>
  723. <b>Note:</b> The bluez package is required to run this module. Please check if gatttool executable is available on your system.
  724. <br>
  725. <br>
  726. <a name="EQ3BTdefine" id="EQ3BTdefine"></a>
  727. <b>Define</b>
  728. <ul>
  729. <code>define &lt;name&gt; EQ3BT &lt;mac address&gt;</code><br>
  730. <br>
  731. Example:
  732. <ul>
  733. <code>define livingroom.thermostat EQ3BT 00:33:44:33:22:11</code><br>
  734. </ul>
  735. </ul>
  736. <br>
  737. <a name="EQ3BTset" id="EQ3BTset"></a>
  738. <b>Set</b>
  739. <ul>
  740. <code>set &lt;name&gt; &lt;command&gt; [&lt;parameter&gt;]</code><br>
  741. The following commands are defined:<br><br>
  742. <ul>
  743. <li><code><b>desiredTemperature</b> [4.5...29.5]</code> &nbsp;&nbsp;-&nbsp;&nbsp; set the temperature</li>
  744. <li><code><b>boost</b> on/off</code> &nbsp;&nbsp;-&nbsp;&nbsp; activate boost command</li>
  745. <li><code><b>mode</b> manual/automatic</code> &nbsp;&nbsp;-&nbsp;&nbsp; set manual/automatic mode</li>
  746. <li><code><b>updateStatus</b></code> &nbsp;&nbsp;-&nbsp;&nbsp; read current thermostat state and update readings</li>
  747. <li><code><b>eco</b> </code> &nbsp;&nbsp;-&nbsp;&nbsp; set eco temperature</li>
  748. <li><code><b>comfort</b> </code> &nbsp;&nbsp;-&nbsp;&nbsp; set comfort temperature</li>
  749. </ul>
  750. <br>
  751. </ul>
  752. <a name="EQ3BTget" id="EQ3BTget"></a>
  753. <b>Get</b>
  754. <ul>
  755. <code>n/a</code>
  756. </ul>
  757. <br>
  758. <a name="EQ3BTattr" id="EQ3BTattr"></a>
  759. <b>attr</b>
  760. <ul>
  761. <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>
  762. </ul>
  763. <br>
  764. </ul>
  765. =end html
  766. =cut