74_THINKINGCLEANER.pm 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857
  1. ###############################################################################
  2. # $Id: 74_THINKINGCLEANER.pm 14167 2017-05-02 14:12:13Z loredo $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use vars qw(%data);
  7. use HttpUtils;
  8. use Encode;
  9. use Data::Dumper;
  10. # initialize ##################################################################
  11. sub THINKINGCLEANER_Initialize($) {
  12. my ($hash) = @_;
  13. Log3 $hash, 5, "THINKINGCLEANER_Initialize: Entering";
  14. my $webhookFWinstance =
  15. join( ",", devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') );
  16. $hash->{DefFn} = "THINKINGCLEANER_Define";
  17. $hash->{UndefFn} = "THINKINGCLEANER_Undefine";
  18. $hash->{SetFn} = "THINKINGCLEANER_Set";
  19. $hash->{AttrFn} = "THINKINGCLEANER_Attr";
  20. $hash->{parseParams} = 1;
  21. $hash->{AttrList} =
  22. "disable:0,1 disabledForIntervals timeout:1,2,3,4,5 pollInterval:30,45,60,75,90 pollMultiplierWebhook pollMultiplierCleaning model webhookHttpHostname webhookPort webhookFWinstance:$webhookFWinstance restart:noArg "
  23. . $readingFnAttributes;
  24. # 98_powerMap.pm support
  25. $hash->{powerMap} = {
  26. model => 'modelid', # fallback to attribute
  27. modelid => {
  28. 'Roomba_700_Series' => {
  29. rname_E => 'energy',
  30. rname_P => 'consumption',
  31. map => {
  32. presence => {
  33. absent => 0,
  34. },
  35. deviceStatus => {
  36. base => 0.1,
  37. plug => 0.1,
  38. base_recon => 33,
  39. plug_recon => 33,
  40. base_full => 33,
  41. plug_full => 33,
  42. base_trickle => 5,
  43. plug_trickle => 5,
  44. base_wait => 0.1,
  45. plug_wait => 0.1,
  46. '*' => 0,
  47. },
  48. },
  49. },
  50. },
  51. };
  52. }
  53. # regular Fn ##################################################################
  54. sub THINKINGCLEANER_Define($$$) {
  55. my ( $hash, $a, $h ) = @_;
  56. my $name = $hash->{NAME};
  57. my $infix = "THINKINGCLEANER";
  58. Log3 $name, 5,
  59. "THINKINGCLEANER $name: called function THINKINGCLEANER_Define()";
  60. eval {
  61. require JSON;
  62. import JSON qw( decode_json );
  63. };
  64. return "Please install Perl JSON to use module THINKINGCLEANER"
  65. if ($@);
  66. if ( int(@$a) < 2 ) {
  67. my $msg =
  68. "Wrong syntax: define <name> THINKINGCLEANER <ip-or-hostname>";
  69. Log3 $name, 4, $msg;
  70. return $msg;
  71. }
  72. $hash->{TYPE} = "THINKINGCLEANER";
  73. my $address = @$a[2];
  74. $hash->{DeviceName} = $address;
  75. # set reverse pointer
  76. $modules{THINKINGCLEANER}{defptr}{$name} = \$hash;
  77. # set default settings on first define
  78. if ( $init_done && !defined( $hash->{OLDDEF} ) ) {
  79. $attr{$name}{cmdIcon} =
  80. 'on-max:text_max on-spot:refresh on-delayed:time_timer dock:measure_battery_50 locate:rc_SEARCH';
  81. $attr{$name}{devStateIcon} =
  82. 'on-delayed:rc_STOP@green:off on-max:rc_BLUE@green:off on-spot:rc_GREEN@red:off on.*:rc_GREEN@green:off dock:rc_GREEN@orange:off off:rc_STOP:on standby|remote:rc_YELLOW:on locate:rc_YELLOW .*:rc_RED';
  83. $attr{$name}{icon} = 'scene_cleaning';
  84. $attr{$name}{webCmd} = 'on-max:on-spot:on-delayed:dock:locate';
  85. }
  86. if ( THINKINGCLEANER_addExtension( $name, "THINKINGCLEANER_CGI", $infix ) )
  87. {
  88. $hash->{fhem}{infix} = $infix;
  89. }
  90. $hash->{WEBHOOK_REGISTER} = "unregistered";
  91. # start the status update timer
  92. THINKINGCLEANER_GetStatus( $hash, 2 );
  93. return undef;
  94. }
  95. sub THINKINGCLEANER_Undefine($$$) {
  96. my ( $hash, $a, $h ) = @_;
  97. my $name = $hash->{NAME};
  98. if ( defined( $hash->{fhem}{infix} ) ) {
  99. THINKINGCLEANER_removeExtension( $hash->{fhem}{infix} );
  100. }
  101. Log3 $name, 5,
  102. "THINKINGCLEANER $name: called function THINKINGCLEANER_Undefine()";
  103. # Stop the internal GetStatus-Loop and exit
  104. RemoveInternalTimer($hash);
  105. # release reverse pointer
  106. delete $modules{THINKINGCLEANER}{defptr}{$name};
  107. return undef;
  108. }
  109. sub THINKINGCLEANER_Set($$$);
  110. sub THINKINGCLEANER_Set($$$) {
  111. my ( $hash, $a, $h ) = @_;
  112. my $name = $hash->{NAME};
  113. my $state = ReadingsVal( $name, "state", "absent" );
  114. my $deviceStatus = ReadingsVal( $name, "deviceStatus", "off" );
  115. my $presence = ReadingsVal( $name, "presence", "absent" );
  116. my $power = ReadingsVal( $name, "power", "off" );
  117. Log3 $name, 5,
  118. "THINKINGCLEANER $name: called function THINKINGCLEANER_Set()";
  119. return "Argument is missing" if ( int(@$a) < 1 );
  120. my $usage =
  121. "Unknown argument "
  122. . @$a[1]
  123. . ", choose one of statusRequest:noArg toggle:noArg on:noArg on-spot:noArg on-max:noArg off:noArg power:off,on dock:noArg undock:noArg locate:noArg on-delayed:noArg cleaningDelay remoteControl:forward,backward,left,left-spin,right,right-spin,stop,drive scheduleAdd name damageProtection:off,on reboot:noArg autoUpdate:on,off vacuumDrive:off,on restartAC:on,off alwaysMAX:on,off autoDock:on,off keepAwakeOnDock:on,off songSubmit songReset:noArg dockAt stopAt";
  124. my $cmd = '';
  125. my $result;
  126. # find existing schedules and offer set commands
  127. my $sd0 = ReadingsVal( $name, "schedule0", "" );
  128. my $sd1 = ReadingsVal( $name, "schedule1", "" );
  129. my $sd2 = ReadingsVal( $name, "schedule2", "" );
  130. my $sd3 = ReadingsVal( $name, "schedule3", "" );
  131. my $sd4 = ReadingsVal( $name, "schedule4", "" );
  132. my $sd5 = ReadingsVal( $name, "schedule5", "" );
  133. my $sd6 = ReadingsVal( $name, "schedule6", "" );
  134. my $schedules = "";
  135. my $si = "0";
  136. foreach ( $sd0, $sd1, $sd2, $sd3, $sd4, $sd5, $sd6 ) {
  137. if ( $_ ne "" ) {
  138. $schedules .= "," if ( $schedules ne "" );
  139. $_ =~ s/(\d+)_(\d{2}:\d{2}:\d{2})_(([A-Za-z]+),?)/$si\_$1_$2_$3/g;
  140. $schedules .= $_;
  141. }
  142. $si++;
  143. }
  144. $usage .= " scheduleDel:$schedules scheduleMod:$schedules"
  145. if ( $schedules ne "" );
  146. # statusRequest
  147. if ( lc( @$a[1] ) eq "statusrequest" ) {
  148. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  149. THINKINGCLEANER_GetStatus($hash);
  150. }
  151. # scheduleAdd
  152. elsif ( lc( @$a[1] ) eq "scheduleadd" ) {
  153. if ( $state ne "absent" ) {
  154. Log3 $name, 3,
  155. "THINKINGCLEANER set $name "
  156. . @$a[1] . " "
  157. . @$a[2] . " "
  158. . @$a[3] . " "
  159. . @$a[4];
  160. return
  161. "Missing arguments. Usage: scheduleAdd <day> <time> <command>"
  162. if ( !defined( @$a[2] )
  163. || !defined( @$a[3] )
  164. || !defined( @$a[4] ) );
  165. return
  166. "Invalid value for day, needs to be between 0(=sunday) and 6(=saturday)"
  167. if ( @$a[2] !~ /^[0-6]$/ );
  168. return "Invalid value for time, needs to be of format 00:00:00"
  169. if ( @$a[3] !~ /^([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/ );
  170. return
  171. "Invalid value for command, choose one of clean max dock stop"
  172. if ( @$a[4] !~ /^(clean|max|dock|stop)$/ );
  173. my $time = THINKINGCLEANER_time2sec( @$a[3] );
  174. my $command;
  175. $command = "0"
  176. if ( @$a[4] eq "clean" );
  177. $command = "1"
  178. if ( @$a[4] eq "max" );
  179. $command = "2"
  180. if ( @$a[4] eq "dock" );
  181. $command = "3"
  182. if ( @$a[4] eq "stop" );
  183. my $wday = @$a[2] - 1;
  184. $wday = "6" if ( $wday < 0 );
  185. $cmd = "$command&day=$wday&time=$time";
  186. $result =
  187. THINKINGCLEANER_SendCommand( $hash, "add_schedule.json", $cmd );
  188. }
  189. else {
  190. return "Device needs to be reachable to be controlled.";
  191. }
  192. }
  193. # scheduleMod
  194. elsif ( lc( @$a[1] ) eq "schedulemod" ) {
  195. if ( $state ne "absent" ) {
  196. Log3 $name, 3,
  197. "THINKINGCLEANER set $name "
  198. . @$a[1] . " "
  199. . @$a[2] . " "
  200. . @$a[3] . " "
  201. . @$a[4] . " "
  202. . @$a[5];
  203. return
  204. "Missing arguments. Usage: scheduleMod <day> <index> <time> <command>"
  205. if ( !defined( @$a[2] )
  206. || !defined( @$a[3] )
  207. || !defined( @$a[4] ) );
  208. return
  209. "Invalid value for day, needs to be between 0(=sunday) and 6(=saturday)"
  210. if ( @$a[2] !~ /^[0-6]/ );
  211. if ( @$a[2] =~ s/_(\d+)_\d{2}:\d{2}:\d{2}_.*//
  212. && !defined( @$a[5] ) )
  213. {
  214. @$a[4] = @$a[3];
  215. @$a[5] = @$a[4];
  216. @$a[3] = $1;
  217. }
  218. return "Invalid value for index, needs to be integer value"
  219. if ( @$a[3] !~ /^\d+$/ );
  220. return "Invalid value for time, needs to be of format 00:00:00"
  221. if ( @$a[4] !~ /^([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/ );
  222. return
  223. "Invalid value for command, choose one of clean max dock stop"
  224. if ( @$a[5] !~ /^(clean|max|dock|stop)$/ );
  225. my $time = THINKINGCLEANER_time2sec( @$a[4] );
  226. my $command;
  227. $command = "0"
  228. if ( @$a[5] eq "clean" );
  229. $command = "1"
  230. if ( @$a[5] eq "max" );
  231. $command = "2"
  232. if ( @$a[5] eq "dock" );
  233. $command = "3"
  234. if ( @$a[5] eq "stop" );
  235. my $wday = @$a[2] - 1;
  236. $wday = "6" if ( $wday < 0 );
  237. $cmd = "$command&day=$wday&index=" . @$a[3] . "&time=$time";
  238. $result =
  239. THINKINGCLEANER_SendCommand( $hash, "change_schedule.json",
  240. $cmd );
  241. }
  242. else {
  243. return "Device needs to be reachable to be controlled.";
  244. }
  245. }
  246. # scheduleDel
  247. elsif ( lc( @$a[1] ) eq "scheduledel" ) {
  248. if ( $state ne "absent" ) {
  249. Log3 $name, 3,
  250. "THINKINGCLEANER set $name "
  251. . @$a[1] . " "
  252. . @$a[2] . " "
  253. . @$a[3];
  254. return "Missing arguments. Usage: scheduleDel <day> <index>"
  255. if ( !defined( @$a[2] ) );
  256. return
  257. "Invalid value for day, needs to be between 0(=sunday) and 6(=saturday)"
  258. if ( @$a[2] !~ /^[0-6]/ );
  259. @$a[3] = $1
  260. if ( @$a[2] =~ s/_(\d+)_\d{2}:\d{2}:\d{2}_.*//
  261. && !defined( @$a[3] ) );
  262. return "Invalid value for index, needs to be integer value"
  263. if ( @$a[3] !~ /^\d+$/ );
  264. my $wday = @$a[2] - 1;
  265. $wday = "6" if ( $wday < 0 );
  266. $cmd = "&day=$wday&index=" . @$a[3];
  267. $result =
  268. THINKINGCLEANER_SendCommand( $hash, "remove_schedule.json",
  269. $cmd );
  270. }
  271. else {
  272. return "Device needs to be reachable to be controlled.";
  273. }
  274. }
  275. # remoteControl
  276. elsif ( lc( @$a[1] ) eq "remotecontrol" ) {
  277. if ( $state ne "absent" ) {
  278. Log3 $name, 3,
  279. "THINKINGCLEANER set $name "
  280. . @$a[1] . " "
  281. . @$a[2] . " "
  282. . @$a[3];
  283. return
  284. "No argument given, choose one of forward left right left-spin right-spin stop drive"
  285. if ( !defined( @$a[2] ) );
  286. if ( $power eq "off" ) {
  287. $result =
  288. THINKINGCLEANER_SendCommand( $hash, "command.json",
  289. "forward", "power" );
  290. return
  291. fhem "sleep 2;set $name "
  292. . @$a[1] . " "
  293. . @$a[2] . " "
  294. . @$a[3];
  295. }
  296. if ( @$a[2] eq "forward" ) {
  297. $cmd = @$a[2];
  298. }
  299. elsif ( @$a[2] eq "backward" ) {
  300. $cmd = "drive_only&speed=-200&degrees=180";
  301. }
  302. elsif ( @$a[2] =~ /^(left|right|stop)$/ ) {
  303. $cmd = "drive" . @$a[2];
  304. }
  305. elsif ( @$a[2] =~ /^(left|right|stop|left-spin|right-spin)$/ ) {
  306. $cmd = @$a[2];
  307. $cmd =~ s/(\w+)-spin/spin$1/;
  308. }
  309. elsif ( @$a[2] = "drive" ) {
  310. return
  311. "Missing arguments. Usage: remoteControl drive <speed> <degrees>"
  312. if ( !defined( @$a[3] ) || !defined( @$a[4] ) );
  313. return "Invalid value for speed"
  314. if ( @$a[3] !~ /^-?\d+/
  315. || @$a[3] < -500
  316. || @$a[3] > 500 );
  317. return "Invalid value for degree"
  318. if ( @$a[4] !~ /^-?\d+/ || @$a[4] < 0 || @$a[4] > 360 );
  319. $cmd = "drive_only&speed=" . @$a[3] . "&degrees=" . @$a[4];
  320. }
  321. else {
  322. return "Unknown driving command";
  323. }
  324. $result =
  325. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  326. "remoteControl" );
  327. }
  328. else {
  329. return "Device needs to be reachable to be controlled.";
  330. }
  331. }
  332. # cleaningDelay
  333. elsif ( lc( @$a[1] ) eq "cleaningdelay" ) {
  334. if ( $state ne "absent" ) {
  335. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  336. return "Missing value: minutes"
  337. if ( !defined( @$a[2] ) );
  338. return
  339. "Invalid value for minutes: needs to be between 30 and 240 minutes"
  340. if ( @$a[2] !~ /^\d+/ || @$a[2] < 30 || @$a[2] > 240 );
  341. $cmd = "CleanDelay&minutes=" . @$a[2];
  342. $result =
  343. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  344. "cleaningDelay" );
  345. }
  346. else {
  347. return "Device needs to be reachable to be controlled.";
  348. }
  349. }
  350. # dockAt
  351. elsif ( lc( @$a[1] ) eq "dockat" ) {
  352. if ( $state ne "absent" ) {
  353. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  354. return "Missing value: percent"
  355. if ( !defined( @$a[2] ) );
  356. return
  357. "Invalid value for minutes: needs to be between 10 and 50 percent"
  358. if ( @$a[2] !~ /^\d+/ || @$a[2] < 10 || @$a[2] > 50 );
  359. $cmd = "DockAt" . @$a[2];
  360. $result =
  361. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  362. "dockAt" );
  363. }
  364. else {
  365. return "Device needs to be reachable to be controlled.";
  366. }
  367. }
  368. # stopAt
  369. elsif ( lc( @$a[1] ) eq "stopat" ) {
  370. if ( $state ne "absent" ) {
  371. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  372. return "Missing value: percent"
  373. if ( !defined( @$a[2] ) );
  374. return
  375. "Invalid value for minutes: needs to be between 6 and 50 percent"
  376. if ( @$a[2] !~ /^\d+/ || @$a[2] < 6 || @$a[2] > 50 );
  377. $cmd = "StopAt" . @$a[2];
  378. $result =
  379. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  380. "stopAt" );
  381. }
  382. else {
  383. return "Device needs to be reachable to be controlled.";
  384. }
  385. }
  386. # autoUpdate
  387. elsif ( lc( @$a[1] ) eq "autoupdate" ) {
  388. if ( $state ne "absent" ) {
  389. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  390. return "Missing value"
  391. if ( !defined( @$a[2] ) );
  392. $cmd = "UpdateOFF";
  393. $cmd = "UpdateON" if ( @$a[2] eq "on" );
  394. $result =
  395. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  396. "autoUpdate" );
  397. }
  398. else {
  399. return "Device needs to be reachable to set " . @$a[1];
  400. }
  401. }
  402. # songSubmit
  403. elsif ( lc( @$a[1] ) eq "songsubmit" ) {
  404. if ( $state ne "absent" ) {
  405. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  406. return "Missing value"
  407. if ( !defined( @$a[2] ) );
  408. $cmd = @$a[2];
  409. $result =
  410. THINKINGCLEANER_SendCommand( $hash, "newsong.json", $cmd );
  411. }
  412. else {
  413. return "Device needs to be reachable to set " . @$a[1];
  414. }
  415. }
  416. # songReset
  417. elsif ( lc( @$a[1] ) eq "songreset" ) {
  418. if ( $state ne "absent" ) {
  419. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  420. $cmd = "resetSongCommand";
  421. $result =
  422. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd );
  423. }
  424. else {
  425. return "Device needs to be reachable to set " . @$a[1];
  426. }
  427. }
  428. # restartAC
  429. elsif ( lc( @$a[1] ) eq "restartac" ) {
  430. if ( $state ne "absent" ) {
  431. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  432. return "Missing value"
  433. if ( !defined( @$a[2] ) );
  434. $cmd = "MAXOFF";
  435. $cmd = "MAXON" if ( @$a[2] eq "on" );
  436. $result =
  437. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  438. "restartAC" );
  439. }
  440. else {
  441. return "Device needs to be reachable to set " . @$a[1];
  442. }
  443. }
  444. # alwaysMAX
  445. elsif ( lc( @$a[1] ) eq "alwaysmax" ) {
  446. if ( $state ne "absent" ) {
  447. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  448. return "Missing value"
  449. if ( !defined( @$a[2] ) );
  450. $cmd = "MAXOFF";
  451. $cmd = "MAXON" if ( @$a[2] eq "on" );
  452. $result =
  453. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  454. "alwaysMAX" );
  455. }
  456. else {
  457. return "Device needs to be reachable to set " . @$a[1];
  458. }
  459. }
  460. # autoDock
  461. elsif ( lc( @$a[1] ) eq "autodock" ) {
  462. if ( $state ne "absent" ) {
  463. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  464. return "Missing value"
  465. if ( !defined( @$a[2] ) );
  466. $cmd = "AutoDockOFF";
  467. $cmd = "AutoDockON" if ( @$a[2] eq "on" );
  468. $result =
  469. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  470. "autoDock" );
  471. }
  472. else {
  473. return "Device needs to be reachable to set " . @$a[1];
  474. }
  475. }
  476. # keepAwakeOnDock
  477. elsif ( lc( @$a[1] ) eq "keepawakeondock" ) {
  478. if ( $state ne "absent" ) {
  479. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  480. return "Missing value"
  481. if ( !defined( @$a[2] ) );
  482. $cmd = "keepAwakeOnDockOFF";
  483. $cmd = "keepAwakeOnDockON" if ( @$a[2] eq "on" );
  484. $result =
  485. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  486. "keepAwakeOnDock" );
  487. }
  488. else {
  489. return "Device needs to be reachable to set " . @$a[1];
  490. }
  491. }
  492. # name
  493. elsif ( lc( @$a[1] ) eq "name" ) {
  494. if ( $state ne "absent" ) {
  495. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  496. return "Missing value: name"
  497. if ( !defined( @$a[2] ) );
  498. return "Wrong format for name"
  499. if ( @$a[2] !~ /^\w+$/ );
  500. $cmd = "rename_device&name=" . @$a[2];
  501. $result =
  502. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  503. "name" );
  504. }
  505. else {
  506. return "Device needs to be reachable to set " . @$a[1];
  507. }
  508. }
  509. # reboot
  510. elsif ( lc( @$a[1] ) eq "reboot" ) {
  511. if ( $state ne "absent" ) {
  512. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  513. $cmd = "crash";
  514. $result =
  515. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd );
  516. }
  517. else {
  518. return "Device needs to be reachable to be rebooted.";
  519. }
  520. }
  521. # locate
  522. elsif ( lc( @$a[1] ) eq "locate" ) {
  523. if ( $state ne "absent" ) {
  524. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  525. $cmd = "find_me";
  526. $result =
  527. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  528. "locate" );
  529. }
  530. else {
  531. return "Device needs to be reachable to be located.";
  532. }
  533. }
  534. # dock
  535. elsif ( lc( @$a[1] ) eq "dock" ) {
  536. if ( $state ne "absent" ) {
  537. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  538. $cmd = "dock";
  539. $result = THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd )
  540. if ( $deviceStatus !~ /^(base.*|plug.*)$/ );
  541. }
  542. else {
  543. return "Device needs to be reachable to be docked.";
  544. }
  545. }
  546. # undock
  547. elsif ( lc( @$a[1] ) eq "undock" ) {
  548. if ( $state ne "absent" ) {
  549. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  550. $cmd = "leavehomebase";
  551. $result = THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd )
  552. if ( $deviceStatus =~ /^(base.*)$/ );
  553. }
  554. else {
  555. return "Device needs to be reachable to be undocked.";
  556. }
  557. }
  558. # damageProtection
  559. elsif ( lc( @$a[1] ) eq "damageprotection" ) {
  560. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  561. return "No argument given, choose one of on off"
  562. if ( !defined( @$a[2] ) );
  563. if ( $state ne "absent" ) {
  564. $cmd = "DriveNormal";
  565. $cmd = "DriveAlways" if ( lc( @$a[2] eq "off" ) );
  566. $result =
  567. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  568. "damageProtection" );
  569. }
  570. else {
  571. return "Device needs to be reachable to set " . @$a[1];
  572. }
  573. }
  574. # vacuumDrive
  575. elsif ( lc( @$a[1] ) eq "vacuumdrive" ) {
  576. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  577. return "No argument given, choose one of on off"
  578. if ( !defined( @$a[2] ) );
  579. if ( $state ne "absent" ) {
  580. $cmd = "VacuumDriveON";
  581. $cmd = "VacuumDriveOFF" if ( lc( @$a[2] eq "off" ) );
  582. $result =
  583. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  584. "vacuumDrive" );
  585. }
  586. else {
  587. return "Device needs to be reachable to set vacuumDrive.";
  588. }
  589. }
  590. # power
  591. elsif ( lc( @$a[1] ) eq "power" ) {
  592. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1] . " " . @$a[2];
  593. return "No argument given, choose one of on off"
  594. if ( !defined( @$a[2] ) );
  595. if ( $state ne "absent" ) {
  596. $cmd = "poweroff";
  597. $cmd = "forward" if ( lc( @$a[2] eq "on" ) );
  598. $result =
  599. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  600. "power" )
  601. if ( ( $cmd eq "forward" && $state ne "on" && $power ne "on" )
  602. || $cmd eq "poweroff" );
  603. }
  604. else {
  605. return "Device needs to be reachable to be controlled.";
  606. }
  607. }
  608. # on-delayed
  609. elsif ( lc( @$a[1] ) eq "on-delayed" ) {
  610. if ( $state ne "absent" ) {
  611. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  612. $cmd = "delayedclean";
  613. $result =
  614. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  615. "on-delayed" );
  616. }
  617. else {
  618. return "Device needs to be reachable to be controlled.";
  619. }
  620. }
  621. # on-spot
  622. elsif ( lc( @$a[1] ) eq "on-spot" ) {
  623. if ( $state ne "absent" ) {
  624. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  625. if ( $power eq "off" ) {
  626. $result =
  627. THINKINGCLEANER_SendCommand( $hash, "command.json",
  628. "forward", "power" );
  629. return fhem "sleep 2;set $name " . @$a[1];
  630. }
  631. $cmd = "spot";
  632. $result =
  633. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  634. "on-spot" );
  635. }
  636. else {
  637. return "Device needs to be reachable to be controlled.";
  638. }
  639. }
  640. # on-max
  641. elsif ( lc( @$a[1] ) eq "on-max" ) {
  642. if ( $state ne "absent" ) {
  643. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  644. if ( $power eq "off" ) {
  645. $result =
  646. THINKINGCLEANER_SendCommand( $hash, "command.json",
  647. "forward", "power" );
  648. return fhem "sleep 2;set $name " . @$a[1];
  649. }
  650. $cmd = "max";
  651. $result =
  652. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  653. "on-max" );
  654. }
  655. else {
  656. return "Device needs to be reachable to be turned on.";
  657. }
  658. }
  659. # on
  660. elsif ( lc( @$a[1] ) eq "on" ) {
  661. if ( $state ne "absent" ) {
  662. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  663. if ( $power eq "off" ) {
  664. $result =
  665. THINKINGCLEANER_SendCommand( $hash, "command.json",
  666. "forward", "power" );
  667. return fhem "sleep 2;set $name " . @$a[1];
  668. }
  669. $cmd = "clean";
  670. $result =
  671. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd, "on" )
  672. if ( $state ne "on" );
  673. }
  674. else {
  675. return "Device needs to be reachable to be turned on.";
  676. }
  677. }
  678. # off
  679. elsif ( lc( @$a[1] ) eq "off" ) {
  680. if ( $state ne "absent" ) {
  681. Log3 $name, 3, "THINKINGCLEANER set $name " . @$a[1];
  682. $cmd = "clean";
  683. if ( $state ne "on-delayed" && $state =~ /^(dock|on.*)$/ ) {
  684. $result =
  685. THINKINGCLEANER_SendCommand( $hash, "command.json", $cmd,
  686. "off" );
  687. }
  688. }
  689. else {
  690. return "Device needs to be reachable to be set to standby mode.";
  691. }
  692. }
  693. # toggle
  694. elsif ( lc( @$a[1] ) eq "toggle" ) {
  695. if ( $state ne "on" ) {
  696. return THINKINGCLEANER_Set( $hash, $name, "on" );
  697. }
  698. else {
  699. return THINKINGCLEANER_Set( $hash, $name, "off" );
  700. }
  701. }
  702. # return usage hint
  703. else {
  704. return $usage;
  705. }
  706. return undef;
  707. }
  708. sub THINKINGCLEANER_Attr(@) {
  709. my ( $cmd, $name, $attrName, $attrVal ) = @_;
  710. my $hash = $defs{$name};
  711. Log3 $name, 5,
  712. "THINKINGCLEANER $name: called function THINKINGCLEANER_Attr()";
  713. return
  714. "Invalid value for attribute $attrName: can only by FQDN or IPv4 or IPv6 address"
  715. if ( $attrVal
  716. && $attrName eq "webhookHttpHostname"
  717. && $attrVal !~ /^([A-Za-z_.0-9]+\.[A-Za-z_.0-9]+)|[0-9:]+$/ );
  718. return
  719. "Invalid value for attribute $attrName: needs to be different from the defined name/address of your Roomba, we need to know how Rooma can connect back to FHEM here!"
  720. if ( $attrVal
  721. && $attrName eq "webhookHttpHostname"
  722. && $attrVal eq $hash->{DeviceName} );
  723. return
  724. "Invalid value for attribute $attrName: FHEMWEB instance $attrVal not existing"
  725. if (
  726. $attrVal
  727. && $attrName eq "webhookFWinstance"
  728. && ( !defined( $defs{$attrVal} )
  729. || $defs{$attrVal}{TYPE} ne "FHEMWEB" )
  730. );
  731. return
  732. "Invalid value for attribute $attrName: needs to be an integer value"
  733. if ( $attrVal && $attrName eq "webhookPort" && $attrVal !~ /^\d+$/ );
  734. return
  735. "Invalid value for attribute $attrName: minimum value is 1 second, maximum 5 seconds"
  736. if ( $attrVal
  737. && $attrName eq "timeout"
  738. && ( $attrVal < 1 || $attrVal > 5 ) );
  739. return "Invalid value for attribute $attrName: minimum value is 16 seconds"
  740. if ( $attrVal && $attrName eq "pollInterval" && $attrVal < 16 );
  741. return
  742. "Invalid value for attribute $attrName: minimum factor is 1.25, maximum is 4"
  743. if ( $attrVal
  744. && $attrName eq "pollMultiplierWebhook"
  745. && ( $attrVal < 1.25 || $attrVal > 4 ) );
  746. return
  747. "Invalid value for attribute $attrName: minimum factor is 0.2, maximum is 30"
  748. if ( $attrVal
  749. && $attrName eq "pollMultiplierCleaning"
  750. && ( $attrVal < 0.2 || $attrVal > 30 ) );
  751. # webhook*
  752. if ( $attrName =~ /^webhook.*/ ) {
  753. my $webhookHttpHostname = (
  754. $attrName eq "webhookHttpHostname"
  755. ? $attrVal
  756. : AttrVal( $name, "webhookHttpHostname", "" )
  757. );
  758. my $webhookFWinstance = (
  759. $attrName eq "webhookFWinstance"
  760. ? $attrVal
  761. : AttrVal( $name, "webhookFWinstance", "" )
  762. );
  763. $hash->{WEBHOOK_URI} = "/"
  764. . AttrVal( $webhookFWinstance, "webname", "fhem" )
  765. . "/THINKINGCLEANER";
  766. $hash->{WEBHOOK_PORT} = (
  767. $attrName eq "webhookPort"
  768. ? $attrVal
  769. : AttrVal(
  770. $name, "webhookPort",
  771. InternalVal( $webhookFWinstance, "PORT", "" )
  772. )
  773. );
  774. $hash->{WEBHOOK_URL} = "";
  775. $hash->{WEBHOOK_COUNTER} = "0";
  776. if ( $webhookHttpHostname ne "" && $hash->{WEBHOOK_PORT} ne "" ) {
  777. $hash->{WEBHOOK_URL} =
  778. "http://"
  779. . $webhookHttpHostname . ":"
  780. . $hash->{WEBHOOK_PORT}
  781. . $hash->{WEBHOOK_URI};
  782. my $cmd =
  783. "&h_url=$webhookHttpHostname&h_path="
  784. . $hash->{WEBHOOK_URI}
  785. . "&h_port="
  786. . $hash->{WEBHOOK_PORT};
  787. THINKINGCLEANER_SendCommand( $hash, "register_webhook.json", $cmd );
  788. $hash->{WEBHOOK_REGISTER} = "sent";
  789. }
  790. else {
  791. $hash->{WEBHOOK_REGISTER} = "incomplete_attributes";
  792. }
  793. }
  794. return undef;
  795. }
  796. # module Fn ####################################################################
  797. sub THINKINGCLEANER_addExtension($$$) {
  798. my ( $name, $func, $link ) = @_;
  799. my $url = "/$link";
  800. return 0
  801. if ( defined( $data{FWEXT}{$url} )
  802. && $data{FWEXT}{$url}{deviceName} ne $name );
  803. Log3 $name, 2,
  804. "THINKINGCLEANER $name: Registering THINKINGCLEANER for webhook URI $url ...";
  805. $data{FWEXT}{$url}{deviceName} = $name;
  806. $data{FWEXT}{$url}{FUNC} = $func;
  807. $data{FWEXT}{$url}{LINK} = $link;
  808. return 1;
  809. }
  810. sub THINKINGCLEANER_removeExtension($) {
  811. my ($link) = @_;
  812. my $url = "?/$link";
  813. my $name = $data{FWEXT}{$url}{deviceName};
  814. Log3 $name, 2,
  815. "THINKINGCLEANER $name: Unregistering THINKINGCLEANER for webhook URI $url...";
  816. delete $data{FWEXT}{$url};
  817. }
  818. sub THINKINGCLEANER_CGI() {
  819. my ($request) = @_;
  820. # data received
  821. if ( defined( $FW_httpheader{UUID} ) ) {
  822. if ( defined( $modules{THINKINGCLEANER}{defptr} ) ) {
  823. while ( my ( $key, $value ) =
  824. each %{ $modules{THINKINGCLEANER}{defptr} } )
  825. {
  826. my $uuid = ReadingsVal( $key, "uuid", undef );
  827. next if ( !$uuid || $uuid ne $FW_httpheader{UUID} );
  828. $defs{$key}{WEBHOOK_COUNTER}++;
  829. $defs{$key}{WEBHOOK_LAST} = TimeNow();
  830. Log3 $key, 4,
  831. "THINKINGCLEANER $key: Received webhook for matching UUID at device $key";
  832. my $delay = undef;
  833. # we need some delay as to the Robo seems to send webhooks but it's status does
  834. # not really reflect the change we'd expect to get here already so give 'em some
  835. # more time to think about it...
  836. $delay = "2"
  837. if ( defined( $defs{$key}{LAST_COMMAND} )
  838. && time() - time_str2num( $defs{$key}{LAST_COMMAND} ) < 3 );
  839. THINKINGCLEANER_GetStatus( $defs{$key}, $delay );
  840. last;
  841. }
  842. }
  843. return ( undef, undef );
  844. }
  845. # no data received
  846. else {
  847. Log3 undef, 5, "THINKINGCLEANER: received malformed request\n$request";
  848. }
  849. return ( "text/plain; charset=utf-8", "Call failure: " . $request );
  850. }
  851. sub THINKINGCLEANER_GetStatus($;$) {
  852. my ( $hash, $delay ) = @_;
  853. my $name = $hash->{NAME};
  854. $hash->{INTERVAL_MULTIPLIER} = (
  855. ReadingsVal( $name, "state", "off" ) ne "off"
  856. && ReadingsVal( $name, "state", "absent" ) ne "absent"
  857. && ReadingsVal( $name, "state", "standby" ) ne "standby"
  858. ? AttrVal( $name, "pollMultiplierCleaning", "0.5" )
  859. : (
  860. $hash->{WEBHOOK_REGISTER} eq "success"
  861. ? AttrVal( $name, "pollMultiplierWebhook", "2" )
  862. : "1"
  863. )
  864. );
  865. $hash->{INTERVAL} =
  866. AttrVal( $name, "pollInterval", "45" ) * $hash->{INTERVAL_MULTIPLIER};
  867. my $interval = (
  868. $delay
  869. ? $delay
  870. : $hash->{INTERVAL}
  871. );
  872. Log3 $name, 5,
  873. "THINKINGCLEANER $name: called function THINKINGCLEANER_GetStatus()";
  874. RemoveInternalTimer($hash);
  875. InternalTimer( gettimeofday() + $interval,
  876. "THINKINGCLEANER_GetStatus", $hash, 0 );
  877. return
  878. if ( $delay || AttrVal( $name, "disable", 0 ) == 1 );
  879. THINKINGCLEANER_SendCommand( $hash, "full_status.json" );
  880. return;
  881. }
  882. sub THINKINGCLEANER_SendCommand($$;$$) {
  883. my ( $hash, $service, $cmd, $type ) = @_;
  884. my $name = $hash->{NAME};
  885. my $address = $hash->{DeviceName};
  886. my $http_method = "GET";
  887. my $http_noshutdown = AttrVal( $name, "http-noshutdown", "1" );
  888. my $timeout;
  889. $cmd = ( defined($cmd) && $cmd ne "" ) ? "command=$cmd" : "";
  890. Log3 $name, 5,
  891. "THINKINGCLEANER $name: called function THINKINGCLEANER_SendCommand()";
  892. my $http_proto = "http";
  893. my $http_user = "";
  894. my $http_passwd = "";
  895. my $URL;
  896. my $response;
  897. my $return;
  898. $http_method = "POST"
  899. if ( $service eq "register_webhook.json" || $service eq "newsong.json" );
  900. if ( !defined($cmd) || $cmd eq "" ) {
  901. Log3 $name, 4, "THINKINGCLEANER $name: REQ $service";
  902. }
  903. else {
  904. $cmd = "?" . $cmd . "&"
  905. if ( $http_method eq "GET" || $http_method eq "" );
  906. Log3 $name, 4, "THINKINGCLEANER $name: REQ $service/" . urlDecode($cmd);
  907. }
  908. $URL = $http_proto . "://" . $address . "/" . $service;
  909. $URL .= $cmd if ( $http_method eq "GET" || $http_method eq "" );
  910. if ( AttrVal( $name, "timeout", "3" ) =~ /^\d+$/ ) {
  911. $timeout = AttrVal( $name, "timeout", "3" );
  912. }
  913. else {
  914. Log3 $name, 3,
  915. "THINKINGCLEANER $name: wrong format in attribute 'timeout'";
  916. $timeout = 3;
  917. }
  918. # send request via HTTP-GET method
  919. if ( $http_method eq "GET" || $http_method eq "" || $cmd eq "" ) {
  920. Log3 $name, 5,
  921. "THINKINGCLEANER $name: GET "
  922. . urlDecode($URL)
  923. . " (noshutdown="
  924. . $http_noshutdown . ")";
  925. HttpUtils_NonblockingGet(
  926. {
  927. url => $URL,
  928. timeout => $timeout,
  929. noshutdown => $http_noshutdown,
  930. data => undef,
  931. hash => $hash,
  932. service => $service,
  933. cmd => $cmd,
  934. type => $type,
  935. callback => \&THINKINGCLEANER_ReceiveCommand,
  936. httpversion => "1.1",
  937. loglevel => AttrVal( $name, "httpLoglevel", 4 ),
  938. header => {
  939. Agent => 'FHEM-THINKINGCLEANER/1.0.0',
  940. 'User-Agent' => 'FHEM-THINKINGCLEANER/1.0.0',
  941. Accept => 'application/json;charset=UTF-8',
  942. 'Accept-Charset' => 'UTF-8',
  943. },
  944. }
  945. );
  946. }
  947. # send request via HTTP-POST method
  948. elsif ( $http_method eq "POST" ) {
  949. Log3 $name, 5,
  950. "THINKINGCLEANER $name: GET "
  951. . $URL
  952. . " (POST DATA: "
  953. . urlDecode($cmd)
  954. . ", noshutdown="
  955. . $http_noshutdown . ")";
  956. HttpUtils_NonblockingGet(
  957. {
  958. url => $URL,
  959. timeout => $timeout,
  960. noshutdown => $http_noshutdown,
  961. data => $cmd,
  962. hash => $hash,
  963. service => $service,
  964. cmd => $cmd,
  965. type => $type,
  966. callback => \&THINKINGCLEANER_ReceiveCommand,
  967. httpversion => "1.1",
  968. loglevel => AttrVal( $name, "httpLoglevel", 4 ),
  969. header => {
  970. Agent => 'FHEM-THINKINGCLEANER/1.0.0',
  971. 'User-Agent' => 'FHEM-THINKINGCLEANER/1.0.0',
  972. 'Content-Type' => 'application/json',
  973. Accept => 'application/json;charset=UTF-8',
  974. 'Accept-Charset' => 'UTF-8',
  975. },
  976. }
  977. );
  978. }
  979. # other HTTP methods are not supported
  980. else {
  981. Log3 $name, 1,
  982. "THINKINGCLEANER $name: ERROR: HTTP method "
  983. . $http_method
  984. . " is not supported.";
  985. }
  986. if ( $service eq "command.json" ) {
  987. $hash->{LAST_COMMAND} = TimeNow();
  988. THINKINGCLEANER_GetStatus( $hash, 6 );
  989. }
  990. return;
  991. }
  992. sub THINKINGCLEANER_ReceiveCommand($$$) {
  993. my ( $param, $err, $data ) = @_;
  994. my $hash = $param->{hash};
  995. my $name = $hash->{NAME};
  996. my $service = $param->{service};
  997. my $cmd = $param->{cmd};
  998. my $state = ReadingsVal( $name, "state", "off" );
  999. my $power = ReadingsVal( $name, "power", "off" );
  1000. my $presence = ReadingsVal( $name, "presence", "absent" );
  1001. my $type = ( $param->{type} ) ? $param->{type} : "";
  1002. my $return;
  1003. Log3 $name, 5,
  1004. "THINKINGCLEANER $name: called function THINKINGCLEANER_ReceiveCommand()";
  1005. readingsBeginUpdate($hash);
  1006. # device not reachable
  1007. if ($err) {
  1008. # powerstate
  1009. $state = "absent";
  1010. $power = "off";
  1011. if ( !defined($cmd) || $cmd eq "" ) {
  1012. Log3 $name, 4, "THINKINGCLEANER $name: RCV TIMEOUT $service";
  1013. }
  1014. else {
  1015. Log3 $name, 4,
  1016. "THINKINGCLEANER $name: RCV TIMEOUT $service/" . urlDecode($cmd);
  1017. }
  1018. $presence = "absent";
  1019. readingsBulkUpdate( $hash, "presence", $presence )
  1020. if ( ReadingsVal( $name, "presence", "" ) ne $presence );
  1021. }
  1022. # data received
  1023. elsif ($data) {
  1024. $presence = "present";
  1025. readingsBulkUpdate( $hash, "presence", $presence )
  1026. if ( ReadingsVal( $name, "presence", "" ) ne $presence );
  1027. if ( !defined($cmd) || $cmd eq "" ) {
  1028. Log3 $name, 4, "THINKINGCLEANER $name: RCV $service";
  1029. }
  1030. else {
  1031. Log3 $name, 4,
  1032. "THINKINGCLEANER $name: RCV $service/" . urlDecode($cmd);
  1033. }
  1034. if ( $data ne "" ) {
  1035. if ( $data =~ /^{/ || $data =~ /^\[/ ) {
  1036. if ( !defined($cmd) || ref($cmd) eq "HASH" || $cmd eq "" ) {
  1037. Log3 $name, 5,
  1038. "THINKINGCLEANER $name: RES $service\n" . $data;
  1039. }
  1040. else {
  1041. Log3 $name, 5,
  1042. "THINKINGCLEANER $name: RES $service/"
  1043. . urlDecode($cmd) . "\n"
  1044. . $data;
  1045. }
  1046. eval '$return = decode_json( Encode::encode_utf8($data) ); 1';
  1047. if ($@) {
  1048. if ( !defined($cmd) || $cmd eq "" ) {
  1049. Log3 $name, 5,
  1050. "THINKINGCLEANER $name: RES ERROR $service - unable to parse malformed JSON: $@\n"
  1051. . $data;
  1052. }
  1053. else {
  1054. Log3 $name, 5,
  1055. "THINKINGCLEANER $name: RES ERROR $service/"
  1056. . urlDecode($cmd)
  1057. . " - unable to parse malformed JSON: $@\n"
  1058. . $data;
  1059. }
  1060. return undef;
  1061. }
  1062. }
  1063. else {
  1064. if ( !defined($cmd) || $cmd eq "" ) {
  1065. Log3 $name, 5,
  1066. "THINKINGCLEANER $name: RES ERROR $service - not in JSON format\n"
  1067. . $data;
  1068. }
  1069. else {
  1070. Log3 $name, 5,
  1071. "THINKINGCLEANER $name: RES ERROR $service/"
  1072. . urlDecode($cmd)
  1073. . " - not in JSON format\n"
  1074. . $data;
  1075. }
  1076. return undef;
  1077. }
  1078. }
  1079. $return = Encode::encode_utf8($data)
  1080. if ( $return && ref($return) ne "HASH" );
  1081. #######################
  1082. # process return data
  1083. #
  1084. # full_status
  1085. if ( $service eq "full_status.json" ) {
  1086. if ( defined($return)
  1087. && ref($return) eq "HASH" )
  1088. {
  1089. $state = "off";
  1090. if ( $return->{result} ne "success" ) {
  1091. $state = "error";
  1092. }
  1093. else {
  1094. foreach my $r ( keys %{$return} ) {
  1095. next if ( ref( $return->{$r} ) ne "HASH" );
  1096. my $rPrefix = $r;
  1097. $rPrefix = "" if ( $r eq "firmware" );
  1098. $rPrefix = "battery" if ( $r eq "power_status" );
  1099. $rPrefix = "" if ( $r eq "tc_status" );
  1100. $rPrefix = "button" if ( $r eq "buttons" );
  1101. $rPrefix = "sensor" if ( $r eq "sensors" );
  1102. foreach my $r2 ( keys %{ $return->{$r} } ) {
  1103. next unless ( $r2 && $r2 ne "" );
  1104. # INTERNALS or dynamic values
  1105. if ( $r2 eq "cleaning" ) {
  1106. # let state be on if cleaning is clearly going on
  1107. $state = "on"
  1108. if ( $return->{$r}{$r2} eq "1"
  1109. && $state !~ /dock|on-.*/ );
  1110. next;
  1111. }
  1112. elsif ( $r2 eq "modelnr" ) {
  1113. $hash->{modelid} =
  1114. "Roomba_" . $return->{$r}{$r2} . "_Series";
  1115. $attr{$name}{model} =
  1116. "Roomba_" . $return->{$r}{$r2} . "_Series";
  1117. next;
  1118. }
  1119. elsif ( $r2 eq "time_h_m" ) {
  1120. $hash->{SYSTEMTIME} = $return->{$r}{$r2};
  1121. next;
  1122. }
  1123. elsif ( lc($r2) eq "selected_timezone" ) {
  1124. $hash->{TIMEZONE} = $return->{$r}{$r2};
  1125. next;
  1126. }
  1127. elsif ( $r2 eq "boot_version" ) {
  1128. $hash->{SWVERSION_BOOTLOADER} =
  1129. $return->{$r}{$r2};
  1130. next;
  1131. }
  1132. elsif ( $r2 eq "cleaning_distance_miles" ) {
  1133. next;
  1134. }
  1135. elsif ( $r2 eq "wifi_version" ) {
  1136. $hash->{SWVERSION_WIFI} =
  1137. $return->{$r}{$r2};
  1138. next;
  1139. }
  1140. elsif ( $r2 eq "version" ) {
  1141. $hash->{SWVERSION} = $return->{$r}{$r2};
  1142. next;
  1143. }
  1144. elsif ( $r2 eq "schedule_serial_number" ) {
  1145. my $serial = (
  1146. $hash->{SCHEDULE_SERIAL}
  1147. ? $hash->{SCHEDULE_SERIAL}
  1148. : "0"
  1149. );
  1150. $hash->{SCHEDULE_SERIAL} =
  1151. $return->{$r}{$r2};
  1152. THINKINGCLEANER_SendCommand( $hash,
  1153. "schedule.json" )
  1154. if ( $serial ne $return->{$r}{$r2} );
  1155. next;
  1156. }
  1157. # READINGS
  1158. my $v = $return->{$r}{$r2};
  1159. my $readingName;
  1160. if ( $r2 eq "cleaner_state" ) {
  1161. $readingName = "deviceStatus";
  1162. $v =~ s/^st_//;
  1163. # change state based on cleaner_state
  1164. if ( $v =~ /^clean_(.*)$/ ) {
  1165. $state = "on-$1";
  1166. }
  1167. elsif ($state ne "on"
  1168. && $v eq "delayed" )
  1169. {
  1170. $state = "on-delayed";
  1171. }
  1172. elsif ($state ne "on"
  1173. && $v =~
  1174. /^(off|clean|stopped|cleanstop|wait)$/ )
  1175. {
  1176. $state = "standby";
  1177. }
  1178. elsif (
  1179. $v =~ /^dock|locate$/
  1180. || ( $state ne "on"
  1181. && $v !~ /^(base.*|plug.*)$/ )
  1182. )
  1183. {
  1184. $state = $v;
  1185. }
  1186. my $cvals;
  1187. $cvals->{cleaningDistance} =
  1188. ReadingsVal( $name, "cleaningDistance", "0" );
  1189. $cvals->{cleaningDistanceLast} =
  1190. ReadingsVal( $name, "cleaningDistanceLast",
  1191. "0" );
  1192. $cvals->{cleaningDistanceStart} = ReadingsVal(
  1193. $name,
  1194. "cleaningDistanceStart",
  1195. $return->{"tc_status"}{"cleaning_distance"}
  1196. );
  1197. $cvals->{cleaningDistanceTotal} =
  1198. $return->{"tc_status"}{"cleaning_distance"};
  1199. # left at base station / begin stats
  1200. if ( $v !~ /^(base.*|plug.*)$/
  1201. && ReadingsVal( $name, $readingName, "" )
  1202. =~ /^(base.*|plug.*)$/ )
  1203. {
  1204. $cvals->{cleaningDistanceStart} =
  1205. $cvals->{cleaningDistanceTotal};
  1206. }
  1207. # arrived at base station / end stats
  1208. elsif ( $v =~ /^(base.*|plug.*)$/
  1209. && ReadingsVal( $name, $readingName, "" )
  1210. !~ /^(base.*|plug.*)$/ )
  1211. {
  1212. if ( $cvals->{cleaningDistanceStart} > 0 ) {
  1213. $cvals->{cleaningDistanceLast} =
  1214. $cvals->{cleaningDistanceTotal} -
  1215. $cvals->{cleaningDistanceStart};
  1216. $cvals->{cleaningDistanceStart} =
  1217. $cvals->{cleaningDistanceTotal};
  1218. }
  1219. $cvals->{cleaningDistance} = "0";
  1220. }
  1221. while ( my ( $key, $value ) = each %{$cvals} ) {
  1222. readingsBulkUpdate( $hash, $key, $value )
  1223. if ( ReadingsVal( $name, $key, "" ) ne
  1224. $value );
  1225. }
  1226. }
  1227. elsif ($r2 eq "cleaning_time"
  1228. && $v eq "0"
  1229. && ReadingsVal( $name, "cleaningTime", "0" ) ne
  1230. "0" )
  1231. {
  1232. readingsBulkUpdate( $hash, "cleaningTimeLast",
  1233. ReadingsVal( $name, "cleaningTime", "0" ) );
  1234. }
  1235. elsif ($r2 eq "auto_update"
  1236. || $r2 eq "vacuum_drive"
  1237. || $r2 eq "restart_AC"
  1238. || $r2 eq "always_MAX"
  1239. || $r2 eq "auto_dock"
  1240. || $r2 eq "keepAwakeOnDock"
  1241. || $r2 eq "webview_advanced" )
  1242. {
  1243. $readingName = $r2;
  1244. $v = "off";
  1245. $v = "on" if ( $return->{$r}{$r2} eq "1" );
  1246. }
  1247. elsif ( $r2 eq "bin_status" ) {
  1248. $readingName = $r2;
  1249. $v = "ok";
  1250. $v = "full" if ( $return->{$r}{$r2} eq "1" );
  1251. }
  1252. elsif ( $r2 eq "tc-roomba-conn" ) {
  1253. $readingName = "roombaConnection";
  1254. $power = "off";
  1255. $power = "on"
  1256. if ( $v ne "0" );
  1257. }
  1258. elsif ( $r2 eq "clean_delay" ) {
  1259. $readingName = "cleaningDelay";
  1260. }
  1261. elsif ( $r2 eq "DHCP" ) {
  1262. $readingName = "networkDHCP";
  1263. }
  1264. elsif ( $r2 eq "cleaning_distance" ) {
  1265. $readingName = "cleaningDistanceTotal";
  1266. my $cleaningDistance =
  1267. $v -
  1268. ReadingsVal( $name, "cleaningDistanceStart",
  1269. "0" );
  1270. readingsBulkUpdate( $hash, "cleaningDistance",
  1271. $cleaningDistance )
  1272. if (
  1273. ReadingsVal(
  1274. $name, "cleaningDistance", ""
  1275. ) ne $cleaningDistance
  1276. );
  1277. }
  1278. elsif ( $r2 eq "battery_charge" ) {
  1279. $readingName = "batteryLevel";
  1280. }
  1281. elsif ( $rPrefix ne "" && $r2 !~ /^battery/ ) {
  1282. $readingName = $rPrefix . ucfirst($r2);
  1283. }
  1284. else {
  1285. $readingName = $r2;
  1286. }
  1287. if ($readingName && $readingName ne "") {
  1288. $readingName =~ s/_(state|button|current)$//;
  1289. $readingName =~ s/[-_](\w)/\U$1/g;
  1290. readingsBulkUpdateIfChanged( $hash,
  1291. $readingName, $v );
  1292. }
  1293. else {
  1294. Log3 $name, 4,
  1295. "THINKINGCLEANER $name: "
  1296. . "ERROR: variable readingName is not initialized - r=$r r2=$r2 v=$v".Dumper($return);
  1297. }
  1298. }
  1299. }
  1300. }
  1301. }
  1302. elsif ( $state ne "undefined" ) {
  1303. Log3 $name, 2,
  1304. "THINKINGCLEANER $name: ERROR: Undefined state of device";
  1305. $state = "undefined";
  1306. }
  1307. }
  1308. # command
  1309. elsif ( $service eq "command.json" ) {
  1310. if ( $return->{result} eq "success" ) {
  1311. # power
  1312. if ( $type eq "power" ) {
  1313. # off
  1314. if ( $cmd =~ /=poweroff&/ ) {
  1315. $state = "standby"
  1316. if ( ReadingsVal( $name, "deviceState", "" ) !~
  1317. /^(dock.*|plug.*)$/ );
  1318. $power = "off";
  1319. readingsBulkUpdate( $hash, "deviceStatus", "off" )
  1320. if (
  1321. ReadingsVal( $name, "deviceStatus", "" ) ne "off" );
  1322. readingsBulkUpdate( $hash, "roombaConnection", "0" )
  1323. if (
  1324. ReadingsVal( $name, "roombaConnection", "" ) ne
  1325. "0" );
  1326. }
  1327. # on
  1328. else {
  1329. $power = "on";
  1330. $state = "standby";
  1331. readingsBulkUpdate( $hash, "deviceStatus", "wait" )
  1332. if ( ReadingsVal( $name, "deviceStatus", "" ) ne
  1333. "wait" );
  1334. }
  1335. THINKINGCLEANER_GetStatus( $hash, 6 );
  1336. }
  1337. # off
  1338. elsif ( $type eq "off" ) {
  1339. $state = "standby";
  1340. readingsBulkUpdate( $hash, "deviceStatus", "clean" )
  1341. if (
  1342. ReadingsVal( $name, "deviceStatus", "" ) ne "clean" );
  1343. THINKINGCLEANER_GetStatus( $hash, 10 );
  1344. }
  1345. # on
  1346. elsif ( $type eq "on" ) {
  1347. $power = "on";
  1348. $state = "on";
  1349. readingsBulkUpdate( $hash, "deviceStatus", "clean" )
  1350. if (
  1351. ReadingsVal( $name, "deviceStatus", "" ) ne "clean" );
  1352. THINKINGCLEANER_GetStatus( $hash, 6 );
  1353. }
  1354. # on-spot
  1355. elsif ( $type eq "on-spot" ) {
  1356. $power = "on";
  1357. $state = "on-spot";
  1358. readingsBulkUpdate( $hash, "deviceStatus", "clean_spot" )
  1359. if ( ReadingsVal( $name, "deviceStatus", "" ) ne
  1360. "clean_spot" );
  1361. THINKINGCLEANER_GetStatus( $hash, 6 );
  1362. }
  1363. # on-max
  1364. elsif ( $type eq "on-max" ) {
  1365. $power = "on";
  1366. $state = "on-max";
  1367. readingsBulkUpdate( $hash, "deviceStatus", "clean_max" )
  1368. if ( ReadingsVal( $name, "deviceStatus", "" ) ne
  1369. "clean_max" );
  1370. THINKINGCLEANER_GetStatus( $hash, 6 );
  1371. }
  1372. # dock
  1373. elsif ( $type eq "dock" ) {
  1374. $power = "on";
  1375. $state = "dock";
  1376. readingsBulkUpdate( $hash, "deviceStatus", "dock" )
  1377. if ( ReadingsVal( $name, "deviceStatus", "" ) ne "dock" );
  1378. THINKINGCLEANER_GetStatus( $hash, 6 );
  1379. }
  1380. # remoteControl
  1381. elsif ( $type eq "remoteControl" ) {
  1382. $power = "on";
  1383. $state = "remote";
  1384. readingsBulkUpdate( $hash, "deviceStatus", "remote" )
  1385. if (
  1386. ReadingsVal( $name, "deviceStatus", "" ) ne "remote" );
  1387. }
  1388. # vacuumDrive
  1389. elsif ( $type eq "vacuumDrive" ) {
  1390. my $v = "off";
  1391. $v = "on" if ( $cmd =~ /=VacuumDriveON&/ );
  1392. readingsBulkUpdate( $hash, "vacuumDrive", $v )
  1393. if ( ReadingsVal( $name, "vacuumDrive", "" ) ne $v );
  1394. }
  1395. # cleaningDelay
  1396. elsif ( $type eq "cleaningDelay" ) {
  1397. my $v = $cmd;
  1398. $v =~ s/.*minutes=(\d+).*/$1/;
  1399. readingsBulkUpdate( $hash, "cleaningDelay", $v )
  1400. if ( ReadingsVal( $name, "cleaningDelay", "" ) ne $v );
  1401. }
  1402. # locate
  1403. elsif ( $type eq "locate" ) {
  1404. $power = "on";
  1405. $state = "locate";
  1406. readingsBulkUpdate( $hash, "deviceStatus", "locate" )
  1407. if (
  1408. ReadingsVal( $name, "deviceStatus", "" ) ne "locate" );
  1409. THINKINGCLEANER_GetStatus( $hash, 10 );
  1410. }
  1411. }
  1412. }
  1413. # schedule
  1414. elsif ( $service eq "schedule.json" ) {
  1415. if ( $return->{result} eq "success" ) {
  1416. $hash->{SCHEDULE_SERIAL} = $return->{serial_number};
  1417. foreach my $r ( keys %{$return} ) {
  1418. next if ( ref( $return->{$r} ) ne "HASH" );
  1419. foreach my $wday ( keys %{ $return->{$r} } ) {
  1420. my $wdayStnd = $wday + 1;
  1421. $wdayStnd = "0" if ( $wdayStnd > 6 );
  1422. my $readingName = "schedule$wdayStnd";
  1423. my $v = "";
  1424. foreach my $ti ( @{ $return->{$r}{$wday} } ) {
  1425. my $command;
  1426. $command = "clean"
  1427. if ( $ti->{command} eq "0" );
  1428. $command = "max"
  1429. if ( $ti->{command} eq "1" );
  1430. $command = "doch"
  1431. if ( $ti->{command} eq "2" );
  1432. $command = "stop"
  1433. if ( $ti->{command} eq "3" );
  1434. $v .= "," if ( $v ne "" );
  1435. $v .=
  1436. $ti->{index} . "_"
  1437. . THINKINGCLEANER_sec2time( $ti->{time} )
  1438. . "_"
  1439. . $command;
  1440. }
  1441. readingsBulkUpdate( $hash, $readingName, $v )
  1442. if ( ReadingsVal( $name, $readingName, "-" ) ne $v );
  1443. }
  1444. }
  1445. }
  1446. }
  1447. # add_schedule, change_schedule, remove_schedule
  1448. elsif ( $service =~ /^(add|change|remove)_schedule.json$/ ) {
  1449. if ( $return->{result} eq "success" ) {
  1450. $hash->{SCHEDULE_SERIAL}++;
  1451. THINKINGCLEANER_SendCommand( $hash, "schedule.json" );
  1452. }
  1453. }
  1454. # register_webhook
  1455. elsif ( $service eq "register_webhook.json" ) {
  1456. $hash->{WEBHOOK_REGISTER} = $return->{result};
  1457. }
  1458. else {
  1459. Log3 $name, 2,
  1460. "THINKINGCLEANER $name: ERROR: Response could not be interpreted";
  1461. }
  1462. }
  1463. # Set reading for power
  1464. #
  1465. readingsBulkUpdate( $hash, "power", $power )
  1466. if ( ReadingsVal( $name, "power", "" ) ne $power );
  1467. # Set reading for state
  1468. #
  1469. readingsBulkUpdate( $hash, "state", $state )
  1470. if ( ReadingsVal( $name, "state", "" ) ne $state );
  1471. readingsEndUpdate( $hash, 1 );
  1472. undef $return;
  1473. return;
  1474. }
  1475. sub THINKINGCLEANER_time2sec($) {
  1476. my ($timeString) = @_;
  1477. my @time = split /:/, $timeString;
  1478. return $time[0] * 3600 + $time[1] * 60;
  1479. }
  1480. sub THINKINGCLEANER_sec2time($) {
  1481. my ($sec) = @_;
  1482. # return human readable format
  1483. my $hours = ( abs($sec) < 3600 ? 0 : int( abs($sec) / 3600 ) );
  1484. $sec -= ( $hours == 0 ? 0 : ( $hours * 3600 ) );
  1485. my $minutes = ( abs($sec) < 60 ? 0 : int( abs($sec) / 60 ) );
  1486. my $seconds = abs($sec) % 60;
  1487. $hours = "0" . $hours if ( $hours < 10 );
  1488. $minutes = "0" . $minutes if ( $minutes < 10 );
  1489. $seconds = "0" . $seconds if ( $seconds < 10 );
  1490. return "$hours:$minutes:$seconds";
  1491. }
  1492. 1;
  1493. =pod
  1494. =item device
  1495. =item summary control for Roomba cleaning robots using ThinkingCleaner add-on
  1496. =item summary_DE Steuerung von Roomba Staubsauger Robotern mit ThinkingCleaner add-on
  1497. =begin html
  1498. <a name="THINKINGCLEANER" id="THINKINGCLEANER"></a>
  1499. <h3>THINKINGCLEANER</h3>
  1500. <ul>
  1501. This module provides support for <a href="http://www.thinkingcleaner.com/">ThinkingCleaner</a> hardware add-on module for Roomba cleaning robots.
  1502. <br><br>
  1503. <a name="THINKINGCLEANERdefine"></a>
  1504. <b>Define</b>
  1505. <ul><br>
  1506. <code>define &lt;name&gt; THINKINGCLEANER &lt;IP-ADRESS or HOSTNAME&gt;</code>
  1507. <br><br>
  1508. Example:
  1509. <ul><br>
  1510. <code>define Robby THINKINGCLEANER 192.168.0.35</code><br>
  1511. </ul>
  1512. <br>
  1513. </ul>
  1514. <br><br>
  1515. <a name="THINKINGCLEANERset"></a>
  1516. <b>Set</b>
  1517. <ul>
  1518. <li>cleaningDelay - sets cleaning delay in minutes when using on-delayed cleaning</li>
  1519. <li>damageProtection - turns damage protection on or off while sending remotrControl commands (on/off)</li>
  1520. <li>dock - Send Roomba back to it's docking station</li>
  1521. <li>locate - Play sound to help finding Roomba</li>
  1522. <li>off - Stop/pause cleaning</li>
  1523. <li>on - Start cleaning</li>
  1524. <li>on-delayed - Delayed start for cleaning according to cleaningDelay</li>
  1525. <li>on-max - Start cleaning with max setting</li>
  1526. <li>on-spot - Start spot cleaning</li>
  1527. <li>power - Turn Roomba on or off (on/off)</li>
  1528. <li>remoteControl - Send remote control commands</li>
  1529. <li>scheduleAdd - Add new cleaning schedule</li>
  1530. <li>scheduleDel - Delete existing cleaning schedule</li>
  1531. <li>scheduleMod - Modify existing cleaning schedule</li>
  1532. <li>statusRequest - Update device readings</li>
  1533. <li>toggle - Toogle between on and off</li>
  1534. <li>undock - Let Roomba leave it's docking station</li>
  1535. <li>vacuumDrive - Enable or disable vaccuming during remoteControl commands (on/off)</li>
  1536. </ul>
  1537. <br><br>
  1538. <a name="THINKINGCLEANERattr"></a>
  1539. <b>Attributes</b>
  1540. <ul>
  1541. <li>pollInterval - Set regular polling interval in minutes (defaults to 45s)</li>
  1542. <li>pollMultiplierCleaning - Change interval multiplier used during cleaning (defaults to 0.5)</li>
  1543. <li>pollMultiplierWebhook - Change interval multiplier used during standby and webhook being enabled (defaults to 2)</li>
  1544. <li>webhookFWinstance - Set FHEMWEB instance for incoming webhook events used by Roomba (mandatory for webhook)</li>
  1545. <li>webhookHttpHostname - Set HTTP Hostname or IP address for incoming webhook events used by Roomba (mandatory for webhook)</li>
  1546. <li>webhookPort - Use different port instead of what defined FHEMWEB instance uses (optional)</li>
  1547. </ul>
  1548. <br><br>
  1549. </ul>
  1550. =end html
  1551. =begin html_DE
  1552. <a name="THINKINGCLEANER" id="THINKINGCLEANER"></a>
  1553. <h3>THINKINGCLEANER</h3>
  1554. <ul>
  1555. Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden:
  1556. </ul>
  1557. <ul>
  1558. <a href='http://fhem.de/commandref.html#THINKINGCLEANER'>THINKINGCLEANER</a>
  1559. </ul>
  1560. =end html_DE
  1561. =cut