74_THINKINGCLEANER.pm 63 KB

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