10_EQ3BT.pm 26 KB

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