4 * Copyright 2016 Alex Lee Yuk Cheung
6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 * in compliance with the License. You may obtain a copy of the License at:
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 * for the specific language governing permissions and limitations under the License.
16 * 28-06-2018: 1.2.4b - Bug fix. Stop nullPointerException on reschedule method.
17 * 18-04-2018: 1.2.4 - Show restriction summary text in app when contact sensor restrictions are configured.
18 * 19-01-2018: 1.2.3 - Allow contact sensors to trigger clean if conditions are met.
19 * 17-01-2018: 1.2.2 - Allow contact sensors to restrict Botvac start.
20 * 06-01-2018: 1.2.1e - Fix null pointer exception on new installations.
21 * 05-01-2018: 1.2.1d - Another attempt to remove null reference when Botvac is removed.
22 * 05-01-2018: 1.2.1c - Attempt to remove null reference when Botvac is removed.
23 * 14-10-2017: 1.2.1b - Fix to setting Smart Home Monitor.
24 * 20-09-2017: 1.2.1 BETA - Allow option for a SmartSchedule 'day' be measured from midnight rather than last cleaning time.
25 * 06-07-2017: 1.2h - Bug fix. Fix to smart schedule event handler typo preventing SHM mode changing. Fix to allow delayed start for multiple botvacs.
26 * 30-05-2017: 1.2g - Bug fix. Null botvac ID generated when no trigger smart schedule is set.
27 * 23-03-2017: 1.2f - Bug fix. Neato Botvac null pointer when start delay is set.
28 * 16-03-2017: 1.2e - Bug fix. Enforce single instance of app.
29 * 16-03-2017: 1.2d - Bug fix. Schedule not reset automatically when clean starts in some scenarios.
30 * - Bug fix. Switch triggers not working.
31 * 06-03-2017: 1.2c - Bug fix. Schedule ignored when SS notifications are turned off for mode and switch triggers.
32 * 02-03-2017: 1.2b - Critical error fix that stopped cleaning completely.
33 * 23-02-2017: 1.2 - Add delay option for clean when using Mode as trigger. Add option to disable notification before scheduled clean.
34 * 27-01-2017: 1.2 BETA Release 2 - Fix to scheduler.
35 * 25-01-2017: 1.2 BETA Release 1b - Minor fix to SmartSchedule menus.
36 * 24-01-2017: 1.2 BETA Release 1 - Individual SmartSchedule for each Botvac. (Loses SmartSchedule from earlier versions).
38 * 17-01-2017: 1.1.7b - Clean up display and formatting for multiple Botvacs.
39 * 12-01-2017: 1.1.7 - Add authentication scope for Maps. Added reauthentication option.
41 * 26-11-2016: 1.1.6 - Enforce SHM mode if SHM is changed during a clean.
43 * 01-11-2016: 1.1.5 - Improved handling of lost credentials to Neato. Better time zone handling.
45 * 24-10-2016: 1.1.4b - Bug fix. Override switch handler fix to prevent false negatives.
46 * 23-10-2016: 1.1.4 - Improve error notification from device status.
48 * 21-10-2016: 1.1.3b - Force poll on settings update.
49 * 20-10-2016: 1.1.3 - Allow device handler to display smart scheduling information.
51 * 20-10-2016: 1.1.2b - Bug fix. SmartSchedule does not operate if force clean option is disabled.
52 * 19-10-2016: 1.1.2 - Option to specify "no trigger" in SmartSchedule. Notification when Force clean is due in 24 hours.
53 Separate Smart schedule time markers from force clean time markers.
55 * 19-10-2016: 1.1.1b - Unschedule auto dock if cleaning is resumed.
56 * 18-10-2016: 1.1.1 - Allow smart schedule to also be triggered on presence and switch events. Add option to specify how override switches work (all or any).
58 * 18-10-2016: 1.1d - Bug fix. Custom state validation errors and error saving page message when upgrading from 1.0 to 1.1.
59 * 18-10-2016: 1.1c - Bug fix. Smart schedule was not updating last clean time properly when Botvac was activated.
60 * 17-10-2016: 1.1b - Set last clean value to new devices for smart schedule.
61 * 17-10-2016: 1.1 - SmartSchedule functionality and minor fixes
63 * 15-10-2016: 1.0c - Fix to auto SHM mode not triggering
64 * 14-10-2016: 1.0b - Minor fix to preference list
65 * 14-10-2016: 1.0 - Initial Version
68 name: "Neato (Connect)",
70 author: "Alex Lee Yuk Cheung",
71 description: "Integration to Neato Robotics Connected Series robot vacuums",
73 iconUrl: "https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/smartapps/alyc100/neato_icon.png",
74 iconX2Url: "https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/smartapps/alyc100/neato_icon.png",
75 iconX3Url: "https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/smartapps/alyc100/neato_icon.png",
81 appSetting "clientSecret"
86 page(name: "auth", title: "Neato", nextPage:"", content:"authPage", uninstall: true, install:true)
87 page(name: "selectDevicePAGE")
88 page(name: "preferencesPAGE")
89 page(name: "notificationsPAGE")
90 page(name: "smartSchedulePAGE")
91 page(name: "timeIntervalPAGE")
95 path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
96 path("/oauth/callback") {action: [GET: "callback"]}
100 log.debug "authPage()"
102 if(!atomicState.accessToken) { //this is to access token for 3rd party to make a call to connect app
103 atomicState.accessToken = createAccessToken()
107 def uninstallAllowed = false
108 def oauthTokenProvided = false
110 if(atomicState.authToken) {
111 description = "You are connected."
112 uninstallAllowed = true
113 oauthTokenProvided = true
115 description = "Click to enter Neato Credentials"
118 def redirectUrl = buildRedirectUrl
119 log.debug "RedirectUrl = ${redirectUrl}"
120 // get rid of next button until the user is actually auth'd
121 if (!oauthTokenProvided) {
122 return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) {
123 section { headerSECTION() }
125 paragraph "Tap below to log in to the Neato service and authorize SmartThings access."
126 href url:redirectUrl, style:"embedded", required:true, title:"Neato", description:description
131 //Disable push option if contact book is enabled
132 if (location.contactBookEnabled) {
133 settings.sendPush = false
136 dynamicPage(name: "auth", uninstall: false, install: false) {
137 section { headerSECTION() }
139 section ("Choose your Neato Botvacs:") {
140 href("selectDevicePAGE", title: null, description: devicesSelected() ? "Devices:" + getDevicesSelectedString() : "Tap to select your Neato Botvacs", state: devicesSelected())
142 if (devicesSelected() == "complete") {
143 section ("SmartSchedule Configuration:") {
144 if (selectedBotvacs.size() > 0) {
145 selectedBotvacs.each() {
146 //Migrate settings from v1.1 and earlier to v1.1.1
147 if (settings["smartScheduleEnabled#$it"] && settings["ssScheduleTrigger#$it"] == null) {
148 settings["ssScheduleTrigger#$it"] = "mode"
150 def ssEnabled = smartScheduleSelected(it)
151 href("smartSchedulePAGE", params: ["botvacId": it], title: "SmartSchedule for ${state.botvacDevices[it]}", description: settings["smartScheduleEnabled#$it"] ? "${getSmartScheduleString(it)}" : "Tap to configure SmartSchedule for ${state.botvacDevices[it]}", state: ssEnabled, required: false, submitOnChange: false)
155 section ("Preferences:") {
156 href("preferencesPAGE", title: null, description: preferencesSelected() ? getPreferencesString() : "Tap to configure preferences", state: preferencesSelected())
158 section ("Notifications:") {
159 href("notificationsPAGE", title: null, description: notificationsSelected() ? getNotificationsString() : "Tap to configure notifications", state: notificationsSelected())
163 section("Botvac Status:") {
164 getChildDevices().each { childDevice ->
166 paragraph image: "https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/devicetypes/alyc100/neato_botvac_image.png", "${childDevice.displayName} is ${childDevice.currentStatus}. Battery is ${childDevice.currentBattery}%"
169 log.trace "Error checking status."
176 paragraph "Tap below to reauthenticate to the Neato service and reauthorize SmartThings access."
177 href url:redirectUrl, style:"embedded", required:false, title:"Neato", description:description
183 def selectDevicePAGE() {
185 dynamicPage(name: "selectDevicePAGE", title: "Devices", uninstall: false, install: false) {
186 section { headerSECTION() }
188 paragraph "Tap below to see the list of Neato Botvacs available in your Neato account and select the ones you want to connect to SmartThings."
189 input "selectedBotvacs", "enum", image: "https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/devicetypes/alyc100/neato_botvac_image.png", required:false, title:"Select Neato Devices \n(${state.botvacDevices.size() ?: 0} found)", multiple:true, options:state.botvacDevices
194 def smartSchedulePAGE(params) {
195 log.debug "PARAMS: $params"
196 if (params.containsKey("botvacId")) state.configBotvacId = params?.botvacId
197 def botvacId = state.configBotvacId
198 return dynamicPage(name: "smartSchedulePAGE", title: "SmartSchedule for ${state.botvacDevices[botvacId]}", install: false, uninstall: false) {
200 paragraph "Configure a dymanic schedule for your Botvac so that it can clean on a regular interval but based on mode, presence sensor or switch triggers."
201 input "smartScheduleEnabled#$botvacId", "bool", title: "Enable SmartSchedule?", required: false, defaultValue: false, submitOnChange: true
203 if (settings["smartScheduleEnabled#$botvacId"]) {
205 input ("ssEnableWarning#$botvacId", "bool", title: "Enable schedule notification before cleaning", required: false, defaultValue: true)
207 section("Configure your cleaning interval and schedule triggers:") {
208 //SmartSchedule configuration options.
209 //Configure regular cleaning interval in days
210 input ("ssCleaningInterval#$botvacId", "number", title: "Set your ideal cleaning interval in days", required: true, defaultValue: 3)
212 //Define when day should be mesaured
213 paragraph "[BETA] If enabled then a day is calculated from midnight before the last clean."
214 input ("ssIntervalFromMidnight#$botvacId", "bool", title: "Measure day interval from midnight before last clean?", required: false, defaultValue: false)
216 //Define smart schedule trigger
217 input("ssScheduleTrigger#$botvacId", "enum", title: "How do you want to trigger the schedule?", multiple: false, required: true, submitOnChange: true, options: ["mode": "Away Modes", "switch": "Switches", "presence": "Presence", "none": "No Triggers"])
219 //Define your away modes
220 if (settings["ssScheduleTrigger#$botvacId"] == "mode") {
221 input ("ssAwayModes#$botvacId", "mode", title:"Specify your away modes:", multiple: true, required: true)
223 if (settings["ssScheduleTrigger#$botvacId"] == "switch") {
224 input ("ssSwitchTrigger#$botvacId", "capability.switch", title:"Which switches?", multiple: true, required: true)
225 input ("ssSwitchTriggerCondition#$botvacId", "enum", title:"Trigger schedule when:", multiple: false, required: true, options: ["any": "Any switch turns on", "all": "All switches are on"], defaultValue: "any")
227 if (settings["ssScheduleTrigger#$botvacId"] == "presence") {
228 input ("ssPeopleAway#$botvacId", "capability.presenceSensor", title:"Which presence sensors?", multiple: true, required: true)
229 input ("ssPeopleAwayCondition#$botvacId", "enum", title:"Trigger schedule when:", multiple: false, required: true, options: ["any": "Someone leaves", "all": "Everyone is away"], defaultValue: "all")
232 if (settings["ssScheduleTrigger#$botvacId"] != "none") {
233 input ("ssStartDelay#$botvacId", "number", title:"Set start delay time (minutes):", required: true, defaultValue: 0)
236 section("SmartSchedule restrictions:") {
238 paragraph "Set SmartSchedule restrictions so that your Botvacs don't start unless below conditions are met."
239 def greyedOutTime = greyedOutTime(settings["starting#$botvacId"], settings["ending#$botvacId"])
240 def timeLabel = getTimeLabel(settings["starting#$botvacId"], settings["ending#$botvacId"])
241 href ("timeIntervalPAGE", params: ["botvacId": botvacId], title: "Operate Botvac only during a certain time", description: timeLabel, state: greyedOutTime, refreshAfterSelection:true)
242 //Define allowed days of operation
243 input ("days#$botvacId", "enum", title: "Operate Botvac only on certain days of the week", multiple: true, required: false,
244 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
245 //Define contact sensors
246 input ("ssRestrictContactSensors#$botvacId", "capability.contactSensor", title:"Set SmartSchedule restriction contact sensors", multiple: true, required: false, submitOnChange: true)
247 if (settings["ssRestrictContactSensors#$botvacId"]) {
248 input ("ssRestrictContactSensorsCondition#$botvacId", "enum", title:"Start Botvac only when:", multiple: false, required: true, options: ["allclosed": "All selected contacts are closed", "anyclosed": "Any selected contacts are closed", "allopen": "All selected contacts are open", "anyopen": "Any selected contacts are open"], defaultValue: "allclosed")
252 section("SmartSchedule overrides:") {
253 //Define override switches to restart SmartSchedule countdown
254 paragraph "Routine override switches/buttons will cancel the next scheduled clean and reset the interval countdown when switched on."
255 input ("ssOverrideSwitch#$botvacId", "capability.switch", title:"Set SmartSchedule override switches", multiple: true, required: false, submitOnChange: true)
256 if (settings["ssOverrideSwitch#$botvacId"]) {
257 input ("ssOverrideSwitchCondition#$botvacId", "enum", title:"Override schedule when:", multiple: false, required: true, options: ["any": "Any selected switch turns on", "all": "All selected switches are on"], defaultValue: "any")
260 section("Notifications:") {
261 paragraph "Turn on SmartSchedule notifications. You can configure specific recipients via Notification settings section."
262 input "ssNotification", "bool", title: "Enable SmartSchedule notifications?", required: false, defaultValue: true
269 def timeIntervalPAGE(params) {
270 def botvacId = params.botvacId
271 return dynamicPage(name: "timeIntervalPAGE", title: "Only during a certain time", refreshAfterSelection:true) {
273 input "starting#$botvacId", "time", title: "Starting", required: false
274 input "ending#$botvacId", "time", title: "Ending", required: false
279 def notificationsPAGE() {
280 return dynamicPage(name: "notificationsPAGE", title: "Notifications", install: false, uninstall: false) {
282 input("recipients", "contact", title: "Send notifications to", required: false, submitOnChange: true) {
283 input "sendPush", "bool", title: "Send notifications via Push?", required: false, defaultValue: false, submitOnChange: true
285 input "sendSMS", "phone", title: "Send notifications via SMS?", required: false, defaultValue: null, submitOnChange: true
286 if ((location.contactBookEnabled && settings.recipients) || settings.sendPush || settings.sendSMS != null) {
287 input "sendBotvacOn", "bool", title: "Notify when Botvacs are on?", required: false, defaultValue: false
288 input "sendBotvacOff", "bool", title: "Notify when Botvacs are off?", required: false, defaultValue: false
289 input "sendBotvacError", "bool", title: "Notify on Botvacs have an error?", required: false, defaultValue: true
290 input "sendBotvacBin", "bool", title: "Notify when Botvacs have a full bin?", required: false, defaultValue: true
291 def smartScheduleEnabled = false
292 if (selectedBotvacs.size() > 0) {
293 selectedBotvacs.each() {
294 if (settings["smartScheduleEnabled#$it"]) smartScheduleEnabled = true
297 if (smartScheduleEnabled) {
298 input "ssNotification", "bool", title: "Enable SmartSchedule notifications?", required: false, defaultValue: true
305 def preferencesPAGE() {
306 return dynamicPage(name: "preferencesPAGE", title: "Preferences", install: false, uninstall: false) {
308 section("Force Clean"){
309 paragraph "If Botvac has been inactive for a number of days specified, then force a clean."
310 input "forceClean", "bool", title: "Force clean after elapsed time?", required: false, defaultValue: false, submitOnChange: true
311 if (forceClean != false) {
312 input ("forceCleanDelay", "number", title: "Number of days before force clean (in days)", required: false, defaultValue: 7)
315 section("Auto Dock") {
316 paragraph "When Botvac is paused, automatically send to base after a specified number of seconds."
317 input "autoDock", "bool", title: "Auto dock Botvac after pause?", required: false, defaultValue: true, submitOnChange: true
318 if (autoDock != false) {
319 input ("autoDockDelay", "number", title: "Auto dock delay after pause (in seconds)", required: false, defaultValue: 60)
322 section("Auto Smart Home Monitor..."){
323 paragraph "If Smart Home Monitor is set to Arm(Away), auto Set Smart Home Monitor to Arm(Stay) when cleaning and reset when done. If Smart Home Monitor is Disarmed during cleaning, then this will not reactivate SHM."
324 input "autoSHM", "bool", title: "Auto set Smart Home Monitor?", required: false, defaultValue: false, submitOnChange: true
330 def headerSECTION() {
331 return paragraph (image: "https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/smartapps/alyc100/neato_icon.png",
336 log.debug "oauthInitUrl with callback: ${callbackUrl}"
338 atomicState.oauthInitState = UUID.randomUUID().toString()
341 response_type: "code",
342 scope: "public_profile control_robots maps",
343 client_id: clientId(),
344 state: atomicState.oauthInitState,
345 redirect_uri: callbackUrl
348 redirect(location: "${apiEndpoint}/oauth2/authorize?${toQueryString(oauthParams)}")
351 // The toQueryString implementation simply gathers everything in the passed in map and converts them to a string joined with the "&" character.
352 String toQueryString(Map m) {
353 return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
357 log.debug "callback()>> params: $params, params.code ${params.code}"
359 def code = params.code
360 def oauthState = params.state
362 if (oauthState == atomicState.oauthInitState) {
364 grant_type: "authorization_code",
366 client_id : clientId(),
367 client_secret: clientSecret(),
368 redirect_uri: callbackUrl
371 def tokenUrl = "https://beehive.neatocloud.com/oauth2/token?${toQueryString(tokenParams)}"
373 httpPost(uri: tokenUrl) { resp ->
374 atomicState.refreshToken = resp.data.refresh_token
375 atomicState.authToken = resp.data.access_token
378 if (atomicState.authToken) {
385 log.error "callback() failed oauthState != atomicState.oauthInitState"
390 // Example success method
393 <p>Your Neato Account is now connected to SmartThings!</p>
394 <p>Click 'Done' to finish setup.</p>
396 displayMessageAsHtml(message)
401 <p>The connection could not be established!</p>
402 <p>Click 'Done' to return to the menu.</p>
404 displayMessageAsHtml(message)
407 def displayMessageAsHtml(message) {
408 def redirectHtml = ""
409 if (redirectUrl) { redirectHtml = """<meta http-equiv="refresh" content="3; url=${redirectUrl}" />""" }
415 <meta name="viewport" content="width=640">
416 <title>SmartThings & Neato connection</title>
417 <style type="text/css">
419 font-family: 'Swiss 721 W01 Thin';
420 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
421 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
422 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
423 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
424 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
429 font-family: 'Swiss 721 W01 Light';
430 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
431 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
432 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
433 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
434 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
441 /*background: #eee;*/
445 vertical-align: middle;
449 font-family: 'Swiss 721 W01 Thin';
456 font-family: 'Swiss 721 W01 Light';
461 <div class="container">
462 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
463 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
464 <img src="https://raw.githubusercontent.com/alyc100/SmartThingsPublic/master/smartapps/alyc100/neato_icon.png" alt="neato icon" width="205" />
470 render contentType: 'text/html', data: html
473 private refreshAuthToken() {
474 log.debug "refreshing auth token"
476 if(!atomicState.refreshToken) {
477 log.warn "Can not refresh OAuth token since there is no refreshToken stored"
479 def refreshParams = [
481 uri : "https://beehive.neatocloud.com",
482 path : "/oauth2/token",
483 query : [grant_type: 'refresh_token', refresh_token: "${atomicState.refreshToken}"],
486 def notificationMessage = "Neato is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Neato (Connect) SmartApp and re-enter your account login credentials."
487 //changed to httpPost
490 httpPost(refreshParams) { resp ->
491 if(resp.status == 200) {
492 log.debug "Token refreshed...calling saved RestAction now!"
493 saveTokenAndResumeAction(resp.data)
496 } catch (groovyx.net.http.HttpResponseException e) {
497 log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
498 def reAttemptPeriod = 300 // in sec
499 if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
500 runIn(reAttemptPeriod, "refreshAuthToken")
501 } else if (e.statusCode == 401) { // unauthorized
502 if (!atomicState.reAttempt) atomicState.reAttempt = 0
503 atomicState.reAttempt = atomicState.reAttempt + 1
504 log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
505 if (atomicState.reAttempt <= 3) {
506 runIn(reAttemptPeriod, "refreshAuthToken")
508 messageHandler(notificationMessage, true)
509 atomicState.authToken = null
510 atomicState.reAttempt = 0
518 private void saveTokenAndResumeAction(json) {
519 log.debug "saveTokenAndResumeAction: token response json: $json"
521 atomicState.refreshToken = json?.refresh_token
522 atomicState.authToken = json?.access_token
523 if (atomicState.action) {
524 log.debug "got refresh token, executing next action: ${atomicState.action}"
525 "${atomicState.action}"()
528 log.warn "did not get response body from refresh token response"
530 atomicState.action = ""
534 log.debug "Installed with settings: ${settings}"
539 log.debug "Updated with settings: ${settings}"
546 //Initialise variables
547 if (state.lastClean == null) {
548 state.lastClean = [:]
550 if (state.smartSchedule == null) {
551 state.smartSchedule = [:]
553 if (state.forceCleanNotificationSent == null) {
554 state.forceCleanNotificationSent = [:]
556 if (state.botvacOnTimeMarker == null) {
557 state.botvacOnTimeMarker = [:]
559 state.remove("taskStartTimes")
560 if (selectedBotvacs) addBotvacs()
562 getChildDevices().each { childDevice ->
563 def botvacId = childDevice.deviceNetworkId
564 //subscribe to events for smartSchedule
565 if (settings["smartScheduleEnabled#$botvacId"]) {
566 //store last mode selected
567 if ((!state.lastTriggerMode) || (state.lastTriggerMode instanceof String)) state.lastTriggerMode = [:]
569 if (settings["ssScheduleTrigger#$botvacId"] == "mode") { subscribe(location, "mode", smartScheduleHandler, [filterEvents: false]) }
570 else if (settings["ssScheduleTrigger#$botvacId"] == "switch") { subscribe(settings["ssSwitchTrigger#$botvacId"], "switch.on", smartScheduleHandler, [filterEvents: false]) }
571 else if (settings["ssScheduleTrigger#$botvacId"] == "presence") { subscribe(settings["ssPeopleAway#$botvacId"], "presence", smartScheduleHandler, [filterEvents: false]) }
573 subscribe(settings["ssOverrideSwitch#$botvacId"], "switch.on", smartScheduleHandler, [filterEvents: false])
574 subscribe(settings["ssRestrictContactSensors#$botvacId"], "contact", smartScheduleHandler, [filterEvents: false])
577 if (state.botvacOnTimeMarker[botvacId] == null) state.botvacOnTimeMarker[botvacId] = now()
578 //subscribe to events for notifications if activated
579 if (settings["smartScheduleEnabled#$botvacId"] || preferencesSelected() == "complete" || notificationsSelected() == "complete") {
580 subscribe(childDevice, "status.cleaning", eventHandler, [filterEvents: false])
582 if (preferencesSelected() == "complete" || notificationsSelected() == "complete") {
583 subscribe(childDevice, "status.ready", eventHandler, [filterEvents: false])
584 subscribe(childDevice, "status.error", eventHandler, [filterEvents: false])
585 subscribe(childDevice, "status.paused", eventHandler, [filterEvents: false])
586 subscribe(childDevice, "bin.full", eventHandler, [filterEvents: false])
588 //initialise force clean flags
589 if (settings.forceClean) {
590 if (state.forceCleanNotificationSent[botvacId] == null) state.forceCleanNotificationSent[botvacId] = false
592 //subscribe to events for smartSchedule
593 if (settings["smartScheduleEnabled#$botvacId"]) {
594 //Initialize flags for Smart Schedule
595 if (state.smartSchedule[botvacId] == null) state.smartSchedule[botvacId] = false
596 if (state.lastClean[botvacId] == null) {
597 if (settings["ssIntervalFromMidnight#$botvacId"]) {
598 state.lastClean[botvacId] = (new Date()).clearTime().getTime()
600 state.lastClean[botvacId] = now()
603 //Trigger has changed so reset all smart schedule flags
604 if ((state.lastTriggerMode.containsKey(botvacId)) && (state.lastTriggerMode[botvacId] != settings["ssScheduleTrigger#$botvacId"])) {
605 log.debug "Smart schedule trigger mode has changed. Resetting smart schedule flag."
606 state.smartSchedule[botvacId] = false
607 state.lastTriggerMode[botvacId] = settings["ssScheduleTrigger#$botvacId"]
612 def nextTimeInSeconds = getNextTimeInSeconds()
613 if (nextTimeInSeconds >= 0) {
614 runIn(nextTimeInSeconds, timeHandler)
617 log.warn "No time has been scheduled. Check that you have Botvacs added under the Neato (Connect) app."
619 runEvery5Minutes('pollOn') // Asynchronously refresh devices so we don't block
624 log.info("Uninstalling, removing child devices...")
626 removeChildDevices(getChildDevices())
629 def updateDevices() {
630 log.debug "Executing 'updateDevices'"
631 if (!state.devices) {
634 def devices = devicesList()
635 state.botvacDevices = [:]
637 devices.each { device ->
638 if (device.serial != null) {
639 selectors.add("${device.serial}|${device.secret_key}")
641 value = "Neato Botvac - " + device.name
642 def key = device.serial + "|" + device.secret_key
643 state.botvacDevices["${key}"] = value
646 log.debug "selectors: $selectors"
647 //Remove devices if does not exist on the Neato platform
648 getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
649 log.info("Deleting ${it.deviceNetworkId}")
651 deleteChildDevice(it.deviceNetworkId)
652 } catch (physicalgraph.exception.NotFoundException e) {
653 log.info("Could not find ${it.deviceNetworkId}. Assuming manually deleted.")
654 } catch (physicalgraph.exception.ConflictException ce) {
655 log.info("Device ${it.deviceNetworkId} in use. Please manually delete.")
658 if (selectedBotvacs) {
659 selectedBotvacs.retainAll(selectors as Object[])
664 log.debug "Executing 'addBotvacs'"
667 selectedBotvacs.each { device ->
669 def childDevice = getChildDevice("${device}")
672 log.info("Adding Neato Botvac device ${device}: ${state.botvacDevices[device]}")
675 name: state.botvacDevices[device],
676 label: state.botvacDevices[device],
678 childDevice = addChildDevice(app.namespace, "Neato Botvac Connected Series", "$device", null, data)
679 childDevice.refresh()
681 log.debug "Created ${state.botvacDevices[device]} with id: ${device}"
683 log.debug "found ${state.botvacDevices[device]} with id ${device} already exists"
688 private removeChildDevices(devices) {
690 deleteChildDevice(it.deviceNetworkId) // 'it' is default
696 def resp = beehiveGET("/users/me/robots")
697 def notificationMessage = "Neato is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Neato (Connect) SmartApp and re-enter your account login credentials."
698 if (resp.status == 200) {
700 } else if (resp.status == 401) {
701 atomicState.action = "updateDevices"
702 if (!atomicState.reAttempt) atomicState.reAttempt = 0
703 atomicState.reAttempt = atomicState.reAttempt + 1
704 log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
705 if (atomicState.reAttempt <= 3) {
706 runIn(reAttemptPeriod, "refreshAuthToken")
708 messageHandler(notificationMessage, true)
709 atomicState.authToken = null
710 atomicState.reAttempt = 0
714 log.error("Non-200 from device list call. ${resp.status} ${resp.data}")
715 runIn(reAttemptPeriod, "refreshAuthToken")
721 def devicesSelected() {
722 return (selectedBotvacs) ? "complete" : null
725 def getDevicesSelectedString() {
728 selectedBotvacs.each { childDevice ->
729 if (null != state.botvacDevices) {
730 listString += "\n• " + state.botvacDevices[childDevice]
736 def smartScheduleSelected(botvacId) {
737 return settings["smartScheduleEnabled#$botvacId"] ? "complete" : null
740 def getSmartScheduleString(botvacId) {
742 if (settings["smartScheduleEnabled#$botvacId"]) {
743 listString += "SmartSchedule set for every ${settings["ssCleaningInterval#$botvacId"]} days "
744 if (settings["ssScheduleTrigger#$botvacId"] == "mode") {listString += "when mode is ${settings["ssAwayModes#$botvacId"]}."}
745 else if (settings["ssScheduleTrigger#$botvacId"] == "switch") {
746 if (settings["ssSwitchTriggerCondition#$botvacId"] == "any") {
747 listString += "when any of ${settings["ssSwitchTrigger#$botvacId"]} turns on."
749 listString += "when ${settings["ssSwitchTrigger#$botvacId"]} are all on."
753 else if (settings["ssScheduleTrigger#$botvacId"] == "presence") {
754 if (settings["ssPeopleAwayCondition#$botvacId"] == "any") {
755 listString += "when one of ${settings["ssPeopleAway#$botvacId"]} leaves."
757 listString += "when ${settings["ssPeopleAway#$botvacId"]} are all away."
761 listString += "\n\nThe following restrictions apply:\n"
762 if (settings["starting#$botvacId"]) listString += "• ${getTimeLabel(settings["starting#$botvacId"], settings["ending#$botvacId"])}\n"
763 if (settings["days#$botvacId"]) listString += "• Only on ${settings["days#$botvacId"]}.\n"
764 if (settings["ssRestrictContactSensors#$botvacId"]) {
765 if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "allclosed") {
766 listString += "• When ${settings["ssRestrictContactSensors#$botvacId"]} are all closed.\n"
767 } else if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "anyclosed") {
768 listString += "• When any one of ${settings["ssRestrictContactSensors#$botvacId"]} are closed.\n"
769 } else if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "allopen") {
770 listString += "• When ${settings["ssRestrictContactSensors#$botvacId"]} are all open.\n"
772 listString += "• When any one of ${settings["ssRestrictContactSensors#$botvacId"]} are closed.\n"
775 if (settings["ssOverrideSwitch#$botvacId"]) {
776 if (settings["ssOverrideSwitchCondition#$botvacId"] == "any") {
777 listString += "• Override schedule if any of ${settings["ssOverrideSwitch#$botvacId"]} turns on.\n"
779 listString += "• Override schedule if ${settings["ssOverrideSwitch#$botvacId"]} are all on.\n"
786 def preferencesSelected() {
787 return (settings.forceClean || settings.autoDock || settings.autoSHM) ? "complete" : null
790 def getPreferencesString() {
792 if (settings.forceClean) listString += "• Force clean after ${settings.forceCleanDelay} days\n"
793 if (settings.autoDock) listString += "• Auto Dock after ${settings.autoDockDelay} seconds\n"
794 if (settings.autoSHM) listString += "• Automatically set Smart Home Monitor\n"
796 if (listString != "") listString = listString.substring(0, listString.length() - 1)
800 def notificationsSelected() {
801 return ((location.contactBookEnabled && settings.recipients) || settings.sendPush || settings.sendSMS != null) && (settings.sendBotvacOn || settings.sendBotvacOff || settings.sendBotvacError || settings.sendBotvacBin || settings.ssNotification) ? "complete" : null
804 def getNotificationsString() {
806 if (location.contactBookEnabled && settings.recipients) {
807 listString += "Send the following notifications to " + settings.recipients
809 else if (settings.sendPush) {
810 listString += "Send the following notifications"
813 if (!settings.recipients && !settings.sendPush && settings.sendSMS != null) {
814 listString += "Send the following SMS to ${settings.sendSMS}"
816 else if (settings.sendSMS != null) {
817 listString += " and SMS to ${settings.sendSMS}"
820 if ((location.contactBookEnabled && settings.recipients) || settings.sendPush || settings.sendSMS != null) {
822 if (settings.sendBotvacOn) listString += "• Botvac On\n"
823 if (settings.sendBotvacOff) listString += "• Botvac Off\n"
824 if (settings.sendBotvacError) listString += "• Botvac Error\n"
825 if (settings.sendBotvacBin) listString += "• Bin Full\n"
826 if (settings.ssNotification) listString += "• SmartSchedule\n"
828 if (listString != "") listString = listString.substring(0, listString.length() - 1)
833 def beehiveGET(path, body = [:]) {
835 log.debug("Beginning API GET: ${beehiveURL(path)}, ${beehiveRequestHeaders()}")
837 httpGet(uri: beehiveURL(path), contentType: 'application/json', headers: beehiveRequestHeaders()) {response ->
838 logResponse(response)
841 } catch (groovyx.net.http.HttpResponseException e) {
842 logResponse(e.response)
847 Map beehiveRequestHeaders() {
849 'Accept': 'application/vnd.neato.nucleo.v1',
850 'Content-Type': 'application/*+json',
851 'X-Agent': '0.11.3-142',
852 'Authorization': "Bearer ${atomicState.authToken}"
856 def logResponse(response) {
857 log.info("Status: ${response.status}")
858 log.info("Body: ${response.data}")
861 def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
864 } catch (groovyx.net.http.HttpResponseException e) {
865 log.error("got error: ${e}, body: ${e.getResponse().getData()}")
866 return options.errorReturn
867 } catch (java.net.SocketTimeoutException e) {
868 log.warn "Connection timed out, not much we can do here"
869 return options.errorReturn
873 // Implement event handlers
874 def eventHandler(evt) {
875 log.debug "Executing 'eventHandler' for ${evt.displayName}"
877 if (evt.value == "paused") {
878 log.trace "Setting auto dock for ${evt.displayName}"
879 //If configured, set to dock automatically after one minute.
880 if (settings.autoDock) {
881 runIn(settings.autoDockDelay, scheduleAutoDock)
884 else if (evt.value == "error") {
886 unschedule(scheduleAutoDock)
887 runEvery5Minutes('pollOn')
888 sendEvent(linkText:app.label, name:"${evt.displayName}", value:"error",descriptionText:"${evt.displayName} has an error", eventType:"SOLUTION_EVENT", displayed: true)
889 log.trace "${evt.displayName} has an error"
890 msg = "${evt.displayName} has an error: " + evt.device.latestState('statusMsg').stringValue.minus('HAS A PROBLEM - ')
891 if (settings.sendBotvacError) {
892 messageHandler(msg, false)
895 else if (evt.value == "cleaning") {
897 unschedule(scheduleAutoDock)
898 //Increase poll interval during cleaning
899 schedule("0 0/1 * * * ?", pollOn)
900 //Record last cleaning time for device
901 log.debug "$evt.device.deviceNetworkId has started cleaning"
902 if (settings["ssIntervalFromMidnight#$evt.device.deviceNetworkId"]) {
903 state.lastClean[evt.device.deviceNetworkId] = (new Date()).clearTime().getTime()
905 state.lastClean[evt.device.deviceNetworkId] = now()
907 state.botvacOnTimeMarker[evt.device.deviceNetworkId] = now()
908 log.debug "$evt.device.deviceNetworkId has started cleaning"
909 if (settings.forceClean) { state.forceCleanNotificationSent[evt.device.deviceNetworkId] = false }
910 //Remove SmartSchedule flag
911 state.smartSchedule[evt.device.deviceNetworkId] = false
912 sendEvent(linkText:app.label, name:"${evt.displayName}", value:"on",descriptionText:"${evt.displayName} is on", eventType:"SOLUTION_EVENT", displayed: true)
913 msg = "${evt.displayName} is on"
914 if (settings.sendBotvacOn) {
915 messageHandler(msg, false)
919 else if (evt.value == "full") {
921 runEvery5Minutes('pollOn')
922 sendEvent(linkText:app.label, name:"${evt.displayName}", value:"bin full",descriptionText:"${evt.displayName} bin is full", eventType:"SOLUTION_EVENT", displayed: true)
923 log.trace "${evt.displayName} bin is full"
924 msg = "${evt.displayName} bin is full"
925 if (settings.sendBotvacBin) {
926 messageHandler(msg, false)
929 else if (evt.value == "ready") {
931 unschedule(scheduleAutoDock)
932 runEvery5Minutes('pollOn')
933 sendEvent(linkText:app.label, name:"${evt.displayName}", value:"off",descriptionText:"${evt.displayName} is off", eventType:"SOLUTION_EVENT", displayed: true)
934 log.trace "${evt.displayName} is off"
935 msg = "${evt.displayName} is off"
936 if (settings.sendBotvacOff) {
937 messageHandler(msg, false)
942 def timeHandler(evt) {
943 smartScheduleHandler(evt)
946 def smartScheduleHandler(evt) {
948 log.debug "Executing 'smartScheduleHandler' for ${evt.displayName}"
950 log.debug "Executing 'smartScheduleHandler' for scheduled event"
953 def nextTimeInSeconds = getNextTimeInSeconds()
954 if (nextTimeInSeconds >= 0) {
955 runIn(nextTimeInSeconds, timeHandler)
958 log.warn "No time has been scheduled. Check that you have Botvacs added under the Neato (Connect) app."
960 getChildDevices().each { childDevice ->
961 def botvacId = childDevice.deviceNetworkId
962 //If switch on for override event
963 if (evt != null && evt.name == "switch") {
964 def switchInList = false
965 for (switchName in settings["ssOverrideSwitch#$botvacId"].name) {
966 if (switchName == evt.device.name) {
971 log.debug "Swtich found in override switch list: $switchInList"
973 def executeOverride = true
974 //If override switch condition is ALL...
975 if (settings["ssOverrideSwitchCondition#$botvacId"] == "all") {
976 //Check all switches in override switch settings are on
977 for (switchVal in settings["ssOverrideSwitch#$botvacId"].currentSwitch) {
978 if (switchVal == "off") {
979 executeOverride = false
985 if (executeOverride) {
986 //Reset last clean date to current time
987 resetSmartScheduleForDevice(botvacId)
990 if (settings.ssNotification) {
991 messageHandler("Neato SmartSchedule has reset schedule for ${childDevice.name} as override switch ${evt.displayName} is on.", false)
995 //If mode change event, schedule trigger, contact sensor or presence trigger
996 //Check conditions, time and day have been met and execute clean. If no trigger is specified rely on pollOn method to start clean.
997 if (settings["ssScheduleTrigger#$botvacId"] != "none") {
999 if (settings["ssStartDelay#$botvacId"]) delay = settings["ssStartDelay#$botvacId"] * 60
1001 runIn(delay, startConditionalClean, [data: [botvacId: botvacId], overwrite: false])
1003 startConditionalClean([botvacId: botvacId])
1010 def scheduleAutoDock() {
1011 log.debug "Executing 'scheduleAutoDock'"
1012 getChildDevices().each { childDevice ->
1013 if (childDevice.latestState('status').stringValue == 'paused') {
1020 log.debug "Executing 'pollOn'"
1022 def activeCleaners = false
1023 log.debug "Last clean states: ${state.lastClean}"
1024 log.debug "Smart schedule states: ${state.smartSchedule}"
1025 log.debug "Botvac ON time markers: ${state.botvacOnTimeMarker}"
1026 getChildDevices().each { childDevice ->
1027 def botvacId = childDevice.deviceNetworkId
1028 state.pollState = now()
1030 if (childDevice.currentSwitch == "off") {
1031 //Update smart schedule state. Create notification when clean is due.
1032 if (settings["smartScheduleEnabled#$botvacId"] && state.lastClean != null && state.lastClean[botvacId] != null) {
1033 def t = now() - state.lastClean[botvacId]
1034 log.debug "$childDevice.displayName schedule marker at " + state.lastClean[botvacId] + ". ${t/86400000} days has elapsed since. ${settings["ssCleaningInterval#$botvacId"] - (t/86400000)} days to scheduled clean."
1036 //Set SmartSchedule flag if SmartSchedule has not been set already, interval has elapsed and trigger conditions are not met
1037 if ((settings["ssScheduleTrigger#$botvacId"] == "none") && ((settings["ssCleaningInterval#$botvacId"] - (t/86400000)) < 1) && (!state.smartSchedule[botvacId]) && (settings["ssEnableWarning#$botvacId"])) {
1038 //hour calculation for notification of next clean
1039 state.smartSchedule[botvacId] = true
1040 if (settings.ssNotification) {
1041 messageHandler("Neato SmartSchedule has scheduled ${childDevice.displayName} for a clean in 24 hours (date and time restrictions permitting). Please clear obstacles and leave internal doors open ready for the clean.", false)
1043 } else if ((!getTriggerConditionsOk(botvacId)) && (t > (settings["ssCleaningInterval#$botvacId"] * 86400000)) && (!state.smartSchedule[botvacId]) && (settings["ssEnableWarning#$botvacId"])) {
1044 state.smartSchedule[botvacId] = true
1045 if (settings.ssNotification) {
1046 def reason = "you're next away"
1047 if (settings["ssScheduleTrigger#$botvacId"] == "switch") { reason = "your selected switches turn on" }
1048 else if (settings["ssScheduleTrigger#$botvacId"] == "presence") { reason = "your selected presence sensors leave"}
1049 messageHandler("Neato SmartSchedule has scheduled ${childDevice.displayName} for a clean when " + reason + " (date and time restrictions permitting). Please clear obstacles and leave internal doors open ready for the clean.", false)
1052 //If no trigger has been set for smart schedule, execute clean when interval time has elapsed
1053 if ((settings["ssScheduleTrigger#$botvacId"] == "none") && (state.smartSchedule[botvacId] || (!settings["ssEnableWarning#$botvacId"])) && (t > (settings["ssCleaningInterval#$botvacId"] * 86400000))) {
1054 startConditionalClean([botvacId: botvacId])
1057 //Update force clean state and create notification when clean is due.
1058 if (settings.forceClean && state.botvacOnTimeMarker != null && state.botvacOnTimeMarker[botvacId] != null) {
1059 def t = now() - state.botvacOnTimeMarker[botvacId]
1060 log.debug "$childDevice.displayName ON time marker at " + state.botvacOnTimeMarker[botvacId] + ". ${t/86400000} days has elapsed since. ${settings.forceCleanDelay - (t/86400000)} days to force clean."
1062 //Create 24 hour warning for force clean.
1063 if ((state.forceCleanNotificationSent != null) && (!state.forceCleanNotificationSent[botvacId]) && ((settings.forceCleanDelay - (t/86400000)) < 1)) {
1064 //Send notification when force clean is due
1065 log.debug "Force clean due within 24 hours"
1066 messageHandler(childDevice.displayName + " has not cleaned for " + (settings.forceCleanDelay - 1) + " days. Forcing a clean in 24 hours. Please clear obstacles and leave internal doors open ready for the clean.", true)
1067 state.forceCleanNotificationSent[botvacId] = true
1070 //Execute force clean (no conditions need checking)
1071 if (t > (settings.forceCleanDelay * 86400000)) {
1072 log.debug "Force clean activated as ${t/86400000} days has elapsed"
1073 messageHandler(childDevice.displayName + " has not cleaned for " + settings.forceCleanDelay + " days. Forcing a clean.", true)
1074 resetSmartScheduleForDevice(botvacId)
1079 if (childDevice.currentStatus == "cleaning") {
1080 //Search for active cleaners
1081 activeCleaners = true
1085 //Set SHM mode depending on whether there are active cleaners.
1086 if (activeCleaners) {
1092 //If SHM is disarmed because of external event, then disable auto SHM mode
1093 if (location.currentState("alarmSystemStatus")?.value == "off") {
1094 state.autoSHMchange = "n"
1098 //Access methods for device type
1099 def isSmartScheduleEnabled(botvacId) {
1100 return settings["smartScheduleEnabled#$botvacId"]
1103 def timeToSmartScheduleClean(botvacId) {
1104 log.debug "Executing 'timeToSmartScheduleClean' with device $botvacId"
1106 if (settings["smartScheduleEnabled#$botvacId"] && state.lastClean != null && state.lastClean[botvacId] != null) {
1107 result = (state.lastClean[botvacId] + (settings["ssCleaningInterval#$botvacId"] * 86400000)) - now()
1109 log.debug "Time to smart schedule clean: $result milliseconds"
1113 def timeToForceClean(botvacId) {
1114 log.debug "Executing 'timeToForceClean' with device $botvacId"
1116 if (settings.forceClean && state.botvacOnTimeMarker != null && state.botvacOnTimeMarker[botvacId] != null) {
1117 result = (state.botvacOnTimeMarker[botvacId] + (settings.forceCleanDelay * 86400000)) - now()
1119 log.debug "Time to force clean: $result milliseconds"
1123 def autoDockDelayValue() {
1124 log.debug "Executing 'autoDockDelayValue'"
1126 if (settings.autoDock) {
1127 result = settings.autoDockDelay
1129 log.debug "Auto dock delay: $result seconds"
1133 def resetSmartScheduleForDevice(botvacId) {
1134 log.debug "Executing 'resetSmartScheduleForDevice' with device $botvacId"
1135 if (settings["smartScheduleEnabled#$botvacId"] && state.lastClean != null && state.smartSchedule != null) {
1136 //Reset last clean date to current time
1137 state.lastClean[botvacId] = now()
1138 if (settings["ssIntervalFromMidnight#$botvacId"]) {
1139 state.lastClean[botvacId] = (new Date()).clearTime().getTime()
1141 state.lastClean[botvacId] = now()
1143 //Remove existing SmartSchedule flag
1144 state.smartSchedule[botvacId] = false
1147 //DEBUG PURPOSES ONLY. FAKE TIME ON OVERRIDE SWITCH AND INCREASE POLL
1148 //state.lastClean[deviceNetworkId] = Date.parseToStringDate("Thu Oct 13 01:23:45 UTC 2016").getTime()
1149 state.lastClean[botvacId] = 1476868627993
1150 state.botvacOnTimeMarker[botvacId] = 1476889942741
1152 schedule("0 0/1 * * * ?", pollOn)
1153 log.debug "Fake data loaded.... " + (now() - state.lastClean[botvacId])/86400000
1159 def setSHMToStay() {
1160 if (settings.autoSHM) {
1161 if (location.currentState("alarmSystemStatus")?.value == "away") {
1162 sendEvent(linkText:app.label, name:"Smart Home Monitor", value:"stay",descriptionText:"Smart Home Monitor was set to stay", eventType:"SOLUTION_EVENT", displayed: true)
1163 log.trace "Smart Home Monitor is set to stay"
1164 sendLocationEvent(name: "alarmSystemStatus", value: "stay")
1165 state.autoSHMchange = "y"
1166 messageHandler("Smart Home Monitor is set to stay as a Neato Botvac is cleaning", true)
1171 def setSHMToAway() {
1172 if (settings.autoSHM) {
1173 if (location.currentState("alarmSystemStatus")?.value == "stay" && state.autoSHMchange == "y") {
1174 sendEvent(linkText:app.label, name:"Smart Home Monitor", value:"away",descriptionText:"Smart Home Monitor was set back to away", eventType:"SOLUTION_EVENT", displayed: true)
1175 log.trace "Smart Home Monitor is set back to away"
1176 sendLocationEvent(name: "alarmSystemStatus", value: "away")
1177 state.autoSHMchange = "n"
1178 messageHandler("Smart Home Monitor is set to away as all Neato Botvacs are off", true)
1183 def startConditionalClean(data) {
1184 def botvacId = data.botvacId
1185 log.debug "Executing 'startConditionalClean for $botvacId'"
1186 if (getAllOk(botvacId)) {
1187 def botvacDevice = getChildDevice(botvacId)
1188 //If smartSchedule flag has been set, start clean.
1189 if ((state.smartSchedule[botvacId]) || (!settings["ssEnableWarning#$botvacId"])) {
1190 if (settings.ssNotification) {
1191 messageHandler("Neato SmartSchedule has started ${botvacDevice.displayName} cleaning.", false)
1193 resetSmartScheduleForDevice(botvacId)
1199 def adjustTimeforTimeZone(originalTime) {
1200 if (getTimeZone()) {
1201 def adjustedTime = timeToday(originalTime, location.timeZone)
1202 def timeNow = now() + (2*1000)
1203 if (adjustedTime.time < timeNow) {
1204 adjustedTime = adjustedTime + 1
1211 def getNextTimeInSeconds() {
1213 getChildDevices().each { childDevice ->
1215 def botvacId = childDevice.deviceNetworkId
1216 if (settings["starting#$botvacId"]) {
1217 time = adjustTimeforTimeZone(settings["starting#$botvacId"])
1219 time = timeToday("00:01", location.timeZone)
1221 def t = timeTodayAfter(new Date(), time.format("HH:mm", getTimeZone()), getTimeZone())
1223 nextTime = (nextTime > t.getTime()) ? t.getTime() : nextTime
1225 nextTime = t.getTime()
1229 def seconds = Math.ceil((nextTime - now()) / 1000)
1230 log.debug "Scheduling ST job to run in ${seconds}s, at ${nextTime}"
1231 return seconds as Integer
1236 def messageHandler(msg, forceFlag) {
1237 log.debug "Executing 'messageHandler for $msg. Forcing is $forceFlag'"
1238 if (settings.sendSMS != null && !forceFlag) {
1239 sendSms(settings.sendSMS, msg)
1241 if (location.contactBookEnabled && settings.recipients) {
1242 sendNotificationToContacts(msg, settings.recipients)
1243 } else if (settings.sendPush || forceFlag) {
1248 private getAllOk(botvacId) {
1249 getTriggerConditionsOk(botvacId) && getDaysOk(botvacId) && getTimeOk(botvacId) && getScheduleOk(botvacId) && getContactSensorsOk(botvacId)
1252 private getScheduleOk(botvacId) {
1253 def t = (now() - state.lastClean[botvacId]) + 2
1254 def result = t > (settings["ssCleaningInterval#$botvacId"] * 86400000)
1255 log.trace "scheduleOk for $botvacId = $result"
1259 private getTriggerConditionsOk(botvacId) {
1260 //Calculate, depending on smart schedule trigger mode, whether conditions currently match
1263 if (settings["ssScheduleTrigger#$botvacId"] == "mode") {
1264 result = location.mode in settings["ssAwayModes#$botvacId"]
1265 } else if (settings["ssScheduleTrigger#$botvacId"] == "switch") {
1266 if (settings["ssSwitchTriggerCondition#$botvacId"] == "any") {
1267 result = "on" in settings["ssSwitchTrigger#$botvacId"].currentSwitch
1269 for (switchVal in settings["ssSwitchTrigger#$botvacId"].currentSwitch) {
1270 if (switchVal == "off") {
1276 } else if (settings["ssScheduleTrigger#$botvacId"] == "presence") {
1277 if (settings["ssPeopleAwayCondition#$botvacId"] == "any") {
1278 result = "not present" in settings["ssPeopleAway#$botvacId"].currentPresence
1280 for (person in settings["ssPeopleAway#$botvacId"]) {
1281 if (person.currentPresence == "present") {
1289 log.trace "triggerConditionsOk for $botvacId = $result"
1293 private getDaysOk(botvacId) {
1295 if (settings["days#$botvacId"]) {
1296 def df = new java.text.SimpleDateFormat("EEEE")
1297 if (getTimeZone()) { df.setTimeZone(location.timeZone) }
1298 def day = df.format(new Date())
1299 result = settings["days#$botvacId"].contains(day)
1301 log.trace "daysOk for $botvacId = $result"
1305 private getTimeOk(botvacId) {
1307 if (settings["starting#$botvacId"] && settings["ending#$botvacId"]) {
1308 def currTime = now()
1309 def start = timeToday(settings["starting#$botvacId"], location.timeZone).time
1310 def stop = timeToday(settings["ending#$botvacId"], location.timeZone).time
1311 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
1313 log.trace "timeOk for $botvacId = $result"
1317 private getContactSensorsOk(botvacId) {
1319 def currContacts = settings["ssRestrictContactSensors#$botvacId"]?.currentContact
1321 if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "allclosed") {
1322 if (currContacts.contains("open")) { result = false }
1324 else if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "anyclosed") {
1325 result = currContacts.findAll {contactVal -> contactVal == "closed" ? true : false}
1327 else if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "allopen") {
1328 if (currContacts.contains("closed")) { result = false }
1330 else if (settings["ssRestrictContactSensorsCondition#$botvacId"] == "anyopen") {
1331 result = currContacts.findAll {contactVal -> contactVal == "open" ? true : false}
1334 log.trace "contactSesnorsOk for $botvacId = $result"
1338 private hhmm(time, fmt = "h:mm a z") {
1339 def t = timeToday(time, location.timeZone)
1340 def f = new java.text.SimpleDateFormat(fmt)
1341 if (getTimeZone()) { f.setTimeZone(location.timeZone ?: timeZone(time)) }
1345 def getTimeLabel(starting, ending){
1346 def timeLabel = "Tap to set"
1348 if(starting && ending){
1349 timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending)
1351 else if (starting) {
1352 timeLabel = "Start at" + " " + hhmm(starting)
1355 timeLabel = "End at" + hhmm(ending)
1360 def greyedOutTime(starting, ending){
1362 if (starting || ending) {
1370 if(location?.timeZone) { tz = location?.timeZone }
1371 if(!tz) { log.warn "No time zone has been retrieved from SmartThings. Please try to open your ST location and press Save." }
1375 def getChildName() { return "Neato BotVac" }
1376 def getServerUrl() { return "https://graph.api.smartthings.com" }
1377 def getShardUrl() { return getApiServerUrl() }
1378 def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" }
1379 def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
1380 def getApiEndpoint() { return "https://apps.neatorobotics.com" }
1381 def getSmartThingsClientId() { return appSettings.clientId }
1382 def beehiveURL(path = '/') { return "https://beehive.neatocloud.com${path}" }
1383 private def textVersion() {
1384 def text = "Neato (Connect)\nVersion: 1.2.4b\nDate: 28062018(1000)"
1387 private def textCopyright() {
1388 def text = "Copyright © 2018 Alex Lee Yuk Cheung"
1392 if(!appSettings.clientId) {
1393 return "3ba64237d07f43e2e6ecff97de60916b73c4b06df71e9ad35ec02d7b3b513881"
1395 return appSettings.clientId
1399 def clientSecret() {
1400 if(!appSettings.clientSecret) {
1401 return "e7fd560dab04efdd38488f918a2a8b0c097157d765e19003360fc458f5119bde"
1403 return appSettings.clientSecret