diff --git a/devicetypes/fuzzysb/garadget.src/garadget.groovy b/devicetypes/fuzzysb/garadget.src/garadget.groovy new file mode 100644 index 0000000..fed9d0d --- /dev/null +++ b/devicetypes/fuzzysb/garadget.src/garadget.groovy @@ -0,0 +1,406 @@ +/** + * Garadget Device Handler + * + * Copyright 2016 Stuart Buchanan based loosely based on original code by Krishnaraj Varma with thanks + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * 12/02/2016 V1.3 updated with to remove token and DeviceId parameters from inputs to retrieving from dni + */ + + +import groovy.json.JsonOutput + +preferences { + input("prdt", "text", title: "sensor scan interval in mS (default: 1000)") + input("pmtt", "text", title: "door moving time in mS(default: 10000)") + input("prlt", "text", title: "button press time mS (default: 300)") + input("prlp", "text", title: "delay between consecutive button presses in mS (default: 1000)") + input("psrr", "text", title: "number of sensor reads used in averaging (default: 3)") + input("psrt", "text", title: "reflection threshold below which the door is considered open (default: 25)") + input("paot", "text", title: "alert for open timeout in seconds (default: 320)") + input("pans", "text", title: " alert for night time start in minutes from midnight (default: 1320)") + input("pane", "text", title: " alert for night time end in minutes from midnight (default: 360)") +} + +metadata { + definition (name: "Garadget", namespace: "fuzzysb", author: "Stuart Buchanan") { + + capability "Switch" + capability "Contact Sensor" + capability "Signal Strength" + capability "Actuator" + capability "Sensor" + capability "Refresh" + capability "Polling" + capability "Configuration" + + attribute "reflection", "string" + attribute "status", "string" + attribute "time", "string" + attribute "lastAction", "string" + attribute "reflection", "string" + attribute "ver", "string" + + command "stop" + command "statusCommand" + command "setConfigCommand" + command "doorConfigCommand" + command "netConfigCommand" + } + + simulator { + + } + +tiles(scale: 2) { + multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ + tileAttribute ("device.status", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', action:"switch.off", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" + attributeState "opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffa81e" + attributeState "closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#6699ff" + attributeState "closed", label:'${name}', action:"switch.on", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" + } + tileAttribute ("device.lastAction", key: "SECONDARY_CONTROL") { + attributeState "default", label: 'Time In State: ${currentValue}' + } + } + standardTile("contact", "device.contact", width: 1, height: 1) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + valueTile("reflection", "reflection", decoration: "flat", width: 2, height: 1) { + state "reflection", label:'Reflection\r\n${currentValue}%' + } + valueTile("rssi", "device.rssi", decoration: "flat", width: 1, height: 1) { + state "rssi", label:'Wifi\r\n${currentValue} dBm', unit: "",backgroundColors:[ + [value: 16, color: "#5600A3"], + [value: -31, color: "#153591"], + [value: -44, color: "#1e9cbb"], + [value: -59, color: "#90d2a7"], + [value: -74, color: "#44b621"], + [value: -84, color: "#f1d801"], + [value: -95, color: "#d04e00"], + [value: -96, color: "#bc2323"] + ] + } + standardTile("refresh", "refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"polling.poll", icon:"st.secondary.refresh" + } + standardTile("stop", "stop") { + state "default", label:"", action: "stop", icon:"http://cdn.device-icons.smartthings.com/sonos/stop-btn@2x.png" + } + valueTile("ip", "ip", decoration: "flat", width: 2, height: 1) { + state "ip", label:'IP Address\r\n${currentValue}' + } + valueTile("ssid", "ssid", decoration: "flat", width: 2, height: 1) { + state "ssid", label:'Wifi SSID\r\n${currentValue}' + } + valueTile("ver", "ver", decoration: "flat", width: 1, height: 1) { + state "ver", label:'Version\r\n${currentValue}' + } + standardTile("configure", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "", backgroundColor: "#ffffff", action: "configure", icon:"st.secondary.configure" + } + + main "status" + details(["status", "contact", "reflection", "ver", "configure", "lastAction", "rssi", "stop", "ip", "ssid", "refresh"]) + } +} + +// handle commands +def poll() { + log.debug "Executing 'poll'" + refresh() +} + +def refresh() { + log.debug "Executing 'refresh'" + statusCommand() + netConfigCommand() + doorConfigCommand() + +} + +def configure() { + log.debug "Resetting Sensor Parameters to SmartThings Compatible Defaults" + SetConfigCommand() +} + +// Parse incoming device messages to generate events +private parseDoorStatusResponse(resp) { + log.debug("Executing parseDoorStatusResponse: "+resp.data) + log.debug("Output status: "+resp.status) + if(resp.status == 200) { + log.debug("returnedresult: "+resp.data.result) + def results = (resp.data.result).tokenize('|') + def statusvalues = (results[0]).tokenize('=') + def timevalues = (results[1]).tokenize('=') + def sensorvalues = (results[2]).tokenize('=') + def signalvalues = (results[3]).tokenize('=') + def status = statusvalues[1] + sendEvent(name: 'status', value: status) + if(status == "open" || status == "closed"){ + sendEvent(name: 'contact', value: status) + } + def time = timevalues[1] + sendEvent(name: 'lastAction', value: time) + def sensor = sensorvalues[1] + sendEvent(name: 'reflection', value: sensor) + def signal = signalvalues[1] + sendEvent(name: 'rssi', value: signal) + + }else if(resp.status == 201){ + log.debug("Something was created/updated") + } +} + +private parseDoorConfigResponse(resp) { + log.debug("Executing parseResponse: "+resp.data) + log.debug("Output status: "+resp.status) + if(resp.status == 200) { + log.debug("returnedresult: "+resp.data.result) + def results = (resp.data.result).tokenize('|') + def vervalues = (results[0]).tokenize('=') + def rdtvalues = (results[1]).tokenize('=') + def mttvalues = (results[2]).tokenize('=') + def rltvalues = (results[3]).tokenize('=') + def rlpvalues = (results[4]).tokenize('=') + def srrvalues = (results[5]).tokenize('=') + def srtvalues = (results[6]).tokenize('=') + def aotvalues = (results[7]).tokenize('=') + def ansvalues = (results[8]).tokenize('=') + def anevalues = (results[9]).tokenize('=') + def ver = vervalues[1] + sendEvent(name: 'ver', value: ver) + log.debug("Firmware Version: "+ver) + def rdt = rdtvalues[1] + log.debug("Sensor Scan Interval (ms): "+rdt ) + def mtt = mttvalues[1] + state.mtt = mtt + sendEvent(name: 'mtt', value: mtt) + log.debug("Door Moving Time (ms): "+mtt ) + def rlt = rltvalues[1] + log.debug("Button Press Time (ms): "+rlt ) + def rlp = rlpvalues[1] + log.debug("Delay Between Consecutive Button Presses (ms): "+rlp ) + def srr = srrvalues[1] + log.debug("number of sensor reads used in averaging: "+srr ) + def srt = srtvalues[1] + log.debug("reflection threshold below which the door is considered open: "+srt ) + def aot = aotvalues[1] + log.debug("alert for open timeout in seconds: "+aot ) + def ans = ansvalues[1] + log.debug("alert for night time start in minutes from midnight: "+ans ) + def ane = anevalues[1] + log.debug("alert for night time end in minutes from midnight: "+ane ) + + }else if(resp.status == 201){ + log.debug("Something was created/updated") + } +} + +private parseNetConfigResponse(resp) { + log.debug("Executing parseResponse: "+resp.data) + log.debug("Output status: "+resp.status) + if(resp.status == 200) { + log.debug("returnedresult: "+resp.data.result) + def results = (resp.data.result).tokenize('|') + def ipvalues = (results[0]).tokenize('=') + def snetvalues = (results[1]).tokenize('=') + def dgwvalues = (results[2]).tokenize('=') + def macvalues = (results[3]).tokenize('=') + def ssidvalues = (results[4]).tokenize('=') + def ip = ipvalues[1] + sendEvent(name: 'ip', value: ip) + log.debug("IP Address: "+ip) + def snet = snetvalues[1] + log.debug("Subnet Mask: "+snet) + def dgw = dgwvalues[1] + log.debug("Default Gateway: "+dgw) + def mac = macvalues[1] + log.debug("Mac Address: "+mac) + def ssid = ssidvalues[1] + sendEvent(name: 'ssid', value: ssid) + log.debug("Wifi SSID : "+ssid) + }else if(resp.status == 201){ + log.debug("Something was created/updated") + } +} + +private parseResponse(resp) { + log.debug("Executing parseResponse: "+resp.data) + log.debug("Output status: "+resp.status) + if(resp.status == 200) { + log.debug("Executing parseResponse.successTrue") + def id = resp.data.id + def name = resp.data.name + def connected = resp.data.connected + def returnValue = resp.data.return_value + }else if(resp.status == 201){ + log.debug("Something was created/updated") + } +} + +private getDeviceDetails() { +def fullDni = device.deviceNetworkId +return fullDni +} + +private sendCommand(method, args = []) { + def DefaultUri = "https://api.particle.io" + def cdni = getDeviceDetails().tokenize(':') + def deviceId = cdni[0] + def token = cdni[1] + def methods = [ + 'doorStatus': [ + uri: "${DefaultUri}", + path: "/v1/devices/${deviceId}/doorStatus", + requestContentType: "application/json", + query: [access_token: token] + ], + 'doorConfig': [ + uri: "${DefaultUri}", + path: "/v1/devices/${deviceId}/doorConfig", + requestContentType: "application/json", + query: [access_token: token] + ], + 'netConfig': [ + uri: "${DefaultUri}", + path: "/v1/devices/${deviceId}/netConfig", + requestContentType: "application/json", + query: [access_token: token] + ], + 'setState': [ + uri: "${DefaultUri}", + path: "/v1/devices/${deviceId}/setState", + requestContentType: "application/json", + query: [access_token: token], + body: args[0] + ], + 'setConfig': [ + uri: "${DefaultUri}", + path: "/v1/devices/${deviceId}/setConfig", + requestContentType: "application/json", + query: [access_token: token], + body: args[0] + ] + ] + + def request = methods.getAt(method) + + log.debug "Http Params ("+request+")" + + try{ + log.debug "Executing 'sendCommand'" + + if (method == "doorStatus"){ + httpGet(request) { resp -> + parseDoorStatusResponse(resp) + } + }else if (method == "doorConfig"){ + log.debug "calling doorConfig Method" + httpGet(request) { resp -> + parseDoorConfigResponse(resp) + } + }else if (method == "netConfig"){ + log.debug "calling netConfig Method" + httpGet(request) { resp -> + parseNetConfigResponse(resp) + } + }else if (method == "setState"){ + log.debug "calling setState Method" + httpPost(request) { resp -> + parseResponse(resp) + } + }else if (method == "setConfig"){ + log.debug "calling setState Method" + httpPost(request) { resp -> + parseResponse(resp) + } + }else{ + httpGet(request) + } + } catch(Exception e){ + log.debug("___exception: " + e) + } +} + + +def on() { + log.debug "Executing 'on'" + openCommand() + statusCommand() + log.info("waiting for ${state.mtt} ms") + "delay ${state.mtt}" + log.info("Initiating Refresh after Transition time") + statusCommand() +} + +def off() { + log.debug "Executing 'off'" + closeCommand() + statusCommand() + log.info("waiting for ${state.mtt} ms") + "delay ${state.mtt}" + log.info("Initiating Refresh after Transition time") + statusCommand() +} + +def stop(){ + log.debug "Executing 'sendCommand.setState'" + def jsonbody = new groovy.json.JsonOutput().toJson(arg:"stop") + sendCommand("setState",[jsonbody]) + statusCommand() +} + +def statusCommand(){ + log.debug "Executing 'sendCommand.statusCommand'" + sendCommand("doorStatus",[]) +} + +def openCommand(){ + log.debug "Executing 'sendCommand.setState'" + def jsonbody = new groovy.json.JsonOutput().toJson(arg:"open") + sendCommand("setState",[jsonbody]) +} + +def closeCommand(){ + log.debug "Executing 'sendCommand.setState'" + def jsonbody = new groovy.json.JsonOutput().toJson(arg:"close") + sendCommand("setState",[jsonbody]) +} + +def doorConfigCommand(){ + log.debug "Executing 'sendCommand.doorConfig'" + sendCommand("doorConfig",[]) +} + +def SetConfigCommand(){ + def crdt = prdt ?: 1000 + def cmtt = pmtt ?: 10000 + def crlt = prlt ?: 300 + def crlp = prlp ?: 1000 + def csrr = psrr ?: 3 + def csrt = psrt ?: 25 + def caot = paot ?: 320 + def cans = pans ?: 1320 + def cane = pane ?: 360 + log.debug "Executing 'sendCommand.setConfig'" + def jsonbody = new groovy.json.JsonOutput().toJson(arg:"rdt=" + crdt +"|mtt=" + cmtt + "|rlt=" + crlt + "|rlp=" + crlp +"|srr=" + csrr + "|srt=" + csrt) + sendCommand("setConfig",[jsonbody]) + jsonbody = new groovy.json.JsonOutput().toJson(arg:"aot=" + caot + "|ans=" + cans + "|ane=" + cane) + sendCommand("setConfig",[jsonbody]) +} + +def netConfigCommand(){ + log.debug "Executing 'sendCommand.netConfig'" + sendCommand("netConfig",[]) +} \ No newline at end of file diff --git a/smartapps/fuzzysb/garadget-connect.src/garadget-connect.groovy b/smartapps/fuzzysb/garadget-connect.src/garadget-connect.groovy new file mode 100644 index 0000000..4f8f2f8 --- /dev/null +++ b/smartapps/fuzzysb/garadget-connect.src/garadget-connect.groovy @@ -0,0 +1,404 @@ +/** + * Garadget Connect + * + * Copyright 2016 Stuart Buchanan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + + import java.text.DecimalFormat + import groovy.json.JsonSlurper + import groovy.json.JsonOutput + +private apiUrl() { "https://api.particle.io" } +private getVendorName() { "Garadget" } +private getVendorTokenPath(){ "https://api.particle.io/oauth/token" } +private getVendorIcon() { "https://dl.dropboxusercontent.com/s/lkrub180btbltm8/garadget_128.png" } +private getClientId() { appSettings.clientId } +private getClientSecret() { appSettings.clientSecret } +private getServerUrl() { if(!appSettings.serverUrl){return getApiServerUrl()} } + + + // Automatically generated. Make future change here. +definition( + name: "Garadget (Connect)", + namespace: "fuzzysb", + author: "Stuart Buchanan", + description: "Garadget Integration", + category: "SmartThings Labs", + iconUrl: "https://dl.dropboxusercontent.com/s/lkrub180btbltm8/garadget_128.png", + iconX2Url: "https://dl.dropboxusercontent.com/s/w8tvaedewwq56kr/garadget_256.png", + iconX3Url: "https://dl.dropboxusercontent.com/s/5hiec37e0y5py06/garadget_512.png", + oauth: true, + singleInstance: true +) { + appSetting "serverUrl" +} + +preferences { + page(name: "startPage", title: "Garadget Integration", content: "startPage", install: false) + page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false) + page(name: "mainPage", title: "Garadget Integration", content: "mainPage") + page(name: "completePage", title: "${getVendorName()} is now connected to SmartThings!", content: "completePage") + page(name: "listDevices", title: "Garadget Devices", content: "listDevices", install: false) + page(name: "badCredentials", title: "Invalid Credentials", content: "badAuthPage", install: false) +} + +mappings { + path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]} +} + +def startPage() { + if (state.garadgetAccessToken) { return mainPage() } + else { return authPage() } +} + +def mainPage(){ + + def result = [success:false] + + + if (!state.garadgetAccessToken) { + createAccessToken() + log.debug "About to create Smarthings Garadget access token." + getToken(garadgetUsername, garadgetPassword) + } + if (state.garadgetAccessToken){ + result.success = true + } + + + if(result.success == true) { + return completePage() + } else { + return badAuthPage() + } +} + + + +def completePage(){ + def description = "Tap 'Next' to proceed" + return dynamicPage(name: "completePage", title: "Credentials Accepted!", nextPage: listDevices , uninstall: true, install:false) { + section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } + } +} + +def badAuthPage(){ + log.debug "In badAuthPage" + log.error "login result false" + return dynamicPage(name: "badCredentials", title: "Garadget", install:false, uninstall:true, nextPage: Credentials) { + section("") { + paragraph "Please check your username and password" + } + } +} + +def authPage() { + log.debug "In authPage" + if(canInstallLabs()) { + def description = null + + + log.debug "Prompting for Auth Details." + + description = "Tap to enter Credentials." + + return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage: mainPage, uninstall: false , install:false) { + section("Generate Username and Password") { + input "garadgetUsername", "text", title: "Your Garadget Username", required: true + input "garadgetPassword", "password", title: "Your Garadget Password", required: true + } + } + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + + return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section { + paragraph "$upgradeNeeded" + } + } + + } +} + +def createChildDevice(deviceFile, dni, name, label) { + log.debug "In createChildDevice" + try{ + def childDevice = addChildDevice("fuzzysb", deviceFile, dni, null, [name: name, label: label, completedSetup: true]) + } catch (e) { + log.error "Error creating device: ${e}" + } +} + +def listDevices() { + log.debug "In listDevices" + + def options = getDeviceList() + + dynamicPage(name: "listDevices", title: "Choose devices", install: true) { + section("Devices") { + input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: options, submitOnChange: true + } + } +} + +def buildRedirectUrl(endPoint) { + log.debug "In buildRedirectUrl" + log.debug("returning: " + getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}") + return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}" +} + +def receivedToken() { + log.debug "In receivedToken" + + def html = """ + + + + + ${getVendorName()} Connection + + + +
+ Vendor icon + connected device icon + SmartThings logo +

Tap 'Done' to continue to Devices.

+
+ + + """ + render contentType: 'text/html', data: html +} + +def getDeviceList() { + def garadgetDevices = [] + + httpGet( apiUrl() + "/v1/devices?access_token=${state.garadgetAccessToken}"){ resp -> + def restDevices = resp.data + restDevices.each { garadget -> + if (garadget.connected == true) + garadgetDevices << ["${garadget.id}|${garadget.name}":"${garadget.name}"] + } + + } + return garadgetDevices.sort() + +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + initialize() +} + +def uninstalled() { + log.debug "Uninstalling Garadget (Connect)" + deleteToken() + removeChildDevices(getChildDevices()) + log.debug "Garadget (Connect) Uninstalled" + +} + +def initialize() { + log.debug "Initialized with settings: ${settings}" + // Pull the latest device info into state + getDeviceList(); + def children = getChildDevices() + if(settings.devices) { + settings.devices.each { device -> + def item = device.tokenize('|') + def deviceId = item[0] + def deviceName = item[1] + def existingDevices = children.find{ d -> d.deviceNetworkId.contains(deviceId) } + if(!existingDevices) { + try { + createChildDevice("Garadget", deviceId + ":" + state.garadgetAccessToken, "${deviceName}", deviceName) + } catch (Exception e) { + log.error "Error creating device: ${e}" + } + } + } + } + + + // Do the initial poll + poll() + // Schedule it to run every 5 minutes + runEvery5Minutes("poll") +} + +def getToken(garadgetUsername, garadgetPassword){ + log.debug "Executing 'sendCommand.setState'" + def body = ("grant_type=password&username=${garadgetUsername}&password=${garadgetPassword}&expires_in=0") + sendCommand("createToken","particle","particle", body) +} + +private sendCommand(method, user, pass, command) { + def userpassascii = "${user}:${pass}" + def userpass = "Basic " + userpassascii.encodeAsBase64().toString() + def headers = [:] + headers.put("Authorization", userpass) + def methods = [ + 'createToken': [ + uri: getVendorTokenPath(), + requestContentType: "application/x-www-form-urlencoded", + headers: headers, + body: command + ], + 'deleteToken': [ + uri: apiUrl() + "/v1/access_tokens/${state.garadgetAccessToken}", + requestContentType: "application/x-www-form-urlencoded", + headers: headers, + ] + ] + def request = methods.getAt(method) + log.debug "Http Params ("+request+")" + + try{ + if (method == "createToken"){ + log.debug "Executing createToken 'sendCommand'" + httpPost(request) { resp -> + parseResponse(resp) + } + }else if (method == "deleteToken"){ + log.debug "Executing deleteToken 'sendCommand'" + httpDelete(request) { resp -> + parseResponse(resp) + } + }else{ + log.debug "Executing default HttpGet 'sendCommand'" + httpGet(request) { resp -> + parseResponse(resp) + } + } + } catch(Exception e){ + log.debug("___exception: " + e) + } +} + + +private parseResponse(resp) { + log.debug("Executing parseResponse: "+resp.data) + log.debug("Output status: "+resp.status) + if(resp.status == 200) { + log.debug("Executing parseResponse.successTrue") + state.garadgetAccessToken = resp.data.access_token + log.debug("Access Token: "+ state.garadgetAccessToken) + state.garadgetRefreshToken = resp.data.refresh_token + log.debug("Refresh Token: "+ state.garadgetRefreshToken) + state.garadgetTokenExpires = resp.data.expires_in + log.debug("Token Expires: "+ state.garadgetTokenExpires) + log.debug "Created new Garadget token" + }else if(resp.status == 201){ + log.debug("Something was created/updated") + } +} + +def poll() { + log.debug "In Poll" + getDeviceList(); + getAllChildDevices().each { + it.statusCommand() + } +} + +private Boolean canInstallLabs() { + return hasAllHubsOver("000.011.00603") +} + +private List getRealHubFirmwareVersions() { + return location.hubs*.firmwareVersionString.findAll { it } +} + +private Boolean hasAllHubsOver(String desiredFirmware) { + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +void deleteToken() { +try{ + sendCommand("deleteToken","${garadgetUsername}","${garadgetPassword}",[]) + log.debug "Deleted the existing Garadget Access Token" + } catch (e) {log.debug "Couldn't delete Garadget Token, There was an error (${e}), moving on"} +} + +private removeChildDevices(delete) { + try { + delete.each { + deleteChildDevice(it.deviceNetworkId) + log.info "Successfully Removed Child Device: ${it.displayName} (${it.deviceNetworkId})" + } + } + catch (e) { log.error "There was an error (${e}) when trying to delete the child device" } +} \ No newline at end of file