2 * Copyright 2015 SmartThings
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at:
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 * for the specific language governing permissions and limitations under the License.
13 * Wemo Service Manager
19 name: "Wemo (Connect)",
20 namespace: "smartthings",
21 author: "SmartThings",
22 description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
23 category: "SmartThings Labs",
24 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
25 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
30 page(name:"firstPage", title:"Wemo Device Setup", content:"firstPage")
33 private discoverAllWemoTypes()
35 sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:Belkin:device:insight:1/urn:Belkin:device:controllee:1/urn:Belkin:device:sensor:1/urn:Belkin:device:lightswitch:1", physicalgraph.device.Protocol.LAN))
38 private getFriendlyName(String deviceNetworkId) {
39 sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1
40 HOST: ${deviceNetworkId}
42 """, physicalgraph.device.Protocol.LAN, "${deviceNetworkId}", [callback: "setupHandler"]))
45 private verifyDevices() {
46 def switches = getWemoSwitches().findAll { it?.value?.verified != true }
47 def motions = getWemoMotions().findAll { it?.value?.verified != true }
48 def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified != true }
49 def devices = switches + motions + lightSwitches
51 getFriendlyName((it.value.ip + ":" + it.value.port))
55 void ssdpSubscribe() {
56 subscribe(location, "ssdpTerm.urn:Belkin:device:insight:1", ssdpSwitchHandler)
57 subscribe(location, "ssdpTerm.urn:Belkin:device:controllee:1", ssdpSwitchHandler)
58 subscribe(location, "ssdpTerm.urn:Belkin:device:sensor:1", ssdpMotionHandler)
59 subscribe(location, "ssdpTerm.urn:Belkin:device:lightswitch:1", ssdpLightSwitchHandler)
66 int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
67 state.refreshCount = refreshCount + 1
68 def refreshInterval = 5
70 log.debug "REFRESH COUNT :: ${refreshCount}"
74 //ssdp request every 25 seconds
75 if((refreshCount % 5) == 0) {
76 discoverAllWemoTypes()
79 //setup.xml request every 5 seconds except on discoveries
80 if(((refreshCount % 1) == 0) && ((refreshCount % 5) != 0)) {
84 def switchesDiscovered = switchesDiscovered()
85 def motionsDiscovered = motionsDiscovered()
86 def lightSwitchesDiscovered = lightSwitchesDiscovered()
88 return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
89 section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
90 section("Select a device...") {
91 input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
92 input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
93 input "selectedLightSwitches", "enum", required:false, title:"Select Wemo Light Switches \n(${lightSwitchesDiscovered.size() ?: 0} found)", multiple:true, options:lightSwitchesDiscovered
99 def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
101 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"."""
103 return dynamicPage(name:"firstPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
105 paragraph "$upgradeNeeded"
111 def devicesDiscovered() {
112 def switches = getWemoSwitches()
113 def motions = getWemoMotions()
114 def lightSwitches = getWemoLightSwitches()
115 def devices = switches + motions + lightSwitches
116 devices?.collect{ [app.id, it.ssdpUSN].join('.') }
119 def switchesDiscovered() {
120 def switches = getWemoSwitches().findAll { it?.value?.verified == true }
123 def value = it.value.name ?: "WeMo Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
124 def key = it.value.mac
125 map["${key}"] = value
130 def motionsDiscovered() {
131 def motions = getWemoMotions().findAll { it?.value?.verified == true }
134 def value = it.value.name ?: "WeMo Motion ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
135 def key = it.value.mac
136 map["${key}"] = value
141 def lightSwitchesDiscovered() {
142 //def vmotions = switches.findAll { it?.verified == true }
143 //log.trace "MOTIONS HERE: ${vmotions}"
144 def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified == true }
147 def value = it.value.name ?: "WeMo Light Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
148 def key = it.value.mac
149 map["${key}"] = value
154 def getWemoSwitches()
156 if (!state.switches) { state.switches = [:] }
162 if (!state.motions) { state.motions = [:] }
166 def getWemoLightSwitches()
168 if (!state.lightSwitches) { state.lightSwitches = [:] }
173 log.debug "Installed with settings: ${settings}"
178 log.debug "Updated with settings: ${settings}"
188 if (selectedSwitches)
194 if (selectedLightSwitches)
197 runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
198 runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
199 runEvery5Minutes("refresh")
203 log.debug "Resubscribe called, delegating to refresh()"
208 log.debug "refresh() called"
213 def refreshDevices() {
214 log.debug "refreshDevices() called"
215 def devices = getAllChildDevices()
217 log.debug "Calling refresh() on device: ${d.id}"
222 def subscribeToDevices() {
223 log.debug "subscribeToDevices() called"
224 def devices = getAllChildDevices()
231 def switches = getWemoSwitches()
233 selectedSwitches.each { dni ->
234 def selectedSwitch = switches.find { it.value.mac == dni } ?: switches.find { "${it.value.ip}:${it.value.port}" == dni }
236 if (selectedSwitch) {
237 d = getChildDevices()?.find {
238 it.deviceNetworkId == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac
241 log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}"
242 d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [
243 "label": selectedSwitch?.value?.name ?: "Wemo Switch",
245 "mac": selectedSwitch.value.mac,
246 "ip": selectedSwitch.value.ip,
247 "port": selectedSwitch.value.port
250 def ipvalue = convertHexToIP(selectedSwitch.value.ip)
251 d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
252 log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
254 log.debug "found ${d.displayName} with id $dni already exists"
261 def motions = getWemoMotions()
263 selectedMotions.each { dni ->
264 def selectedMotion = motions.find { it.value.mac == dni } ?: motions.find { "${it.value.ip}:${it.value.port}" == dni }
266 if (selectedMotion) {
267 d = getChildDevices()?.find {
268 it.deviceNetworkId == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac
271 log.debug "Creating WeMo Motion with dni: ${selectedMotion.value.mac}"
272 d = addChildDevice("smartthings", "Wemo Motion", selectedMotion.value.mac, selectedMotion?.value.hub, [
273 "label": selectedMotion?.value?.name ?: "Wemo Motion",
275 "mac": selectedMotion.value.mac,
276 "ip": selectedMotion.value.ip,
277 "port": selectedMotion.value.port
280 def ipvalue = convertHexToIP(selectedMotion.value.ip)
281 d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
282 log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
284 log.debug "found ${d.displayName} with id $dni already exists"
290 def addLightSwitches() {
291 def lightSwitches = getWemoLightSwitches()
293 selectedLightSwitches.each { dni ->
294 def selectedLightSwitch = lightSwitches.find { it.value.mac == dni } ?: lightSwitches.find { "${it.value.ip}:${it.value.port}" == dni }
296 if (selectedLightSwitch) {
297 d = getChildDevices()?.find {
298 it.deviceNetworkId == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac
301 log.debug "Creating WeMo Light Switch with dni: ${selectedLightSwitch.value.mac}"
302 d = addChildDevice("smartthings", "Wemo Light Switch", selectedLightSwitch.value.mac, selectedLightSwitch?.value.hub, [
303 "label": selectedLightSwitch?.value?.name ?: "Wemo Light Switch",
305 "mac": selectedLightSwitch.value.mac,
306 "ip": selectedLightSwitch.value.ip,
307 "port": selectedLightSwitch.value.port
310 def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
311 d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
312 log.debug "created ${d.displayName} with id $dni"
314 log.debug "found ${d.displayName} with id $dni already exists"
320 def ssdpSwitchHandler(evt) {
321 def description = evt.description
323 def parsedEvent = parseDiscoveryMessage(description)
324 parsedEvent << ["hub":hub]
325 log.debug parsedEvent
327 def switches = getWemoSwitches()
328 if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
329 //if it doesn't already exist
330 switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
332 log.debug "Device was already found in state..."
333 def d = switches."${parsedEvent.ssdpUSN.toString()}"
334 boolean deviceChangedValues = false
335 log.debug "$d.ip <==> $parsedEvent.ip"
336 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
337 d.ip = parsedEvent.ip
338 d.port = parsedEvent.port
339 deviceChangedValues = true
340 log.debug "Device's port or ip changed..."
341 def child = getChildDevice(parsedEvent.mac)
343 child.subscribe(parsedEvent.ip, parsedEvent.port)
346 log.debug "Device with mac $parsedEvent.mac not found"
352 def ssdpMotionHandler(evt) {
353 log.info("ssdpMotionHandler")
354 def description = evt.description
356 def parsedEvent = parseDiscoveryMessage(description)
357 parsedEvent << ["hub":hub]
358 log.debug parsedEvent
360 def motions = getWemoMotions()
361 if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
362 //if it doesn't already exist
363 motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
364 } else { // just update the values
365 log.debug "Device was already found in state..."
367 def d = motions."${parsedEvent.ssdpUSN.toString()}"
368 boolean deviceChangedValues = false
370 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
371 d.ip = parsedEvent.ip
372 d.port = parsedEvent.port
373 deviceChangedValues = true
374 log.debug "Device's port or ip changed..."
377 if (deviceChangedValues) {
378 def children = getChildDevices()
379 log.debug "Found children ${children}"
381 if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
382 log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
383 it.subscribe(parsedEvent.ip, parsedEvent.port)
390 def ssdpLightSwitchHandler(evt) {
391 log.info("ssdpLightSwitchHandler")
392 def description = evt.description
394 def parsedEvent = parseDiscoveryMessage(description)
395 parsedEvent << ["hub":hub]
396 log.debug parsedEvent
398 def lightSwitches = getWemoLightSwitches()
400 if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) {
401 //if it doesn't already exist
402 lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
404 log.debug "Device was already found in state..."
406 def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
407 boolean deviceChangedValues = false
409 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
410 d.ip = parsedEvent.ip
411 d.port = parsedEvent.port
412 deviceChangedValues = true
413 log.debug "Device's port or ip changed..."
414 def child = getChildDevice(parsedEvent.mac)
416 log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}"
417 child.subscribe(parsedEvent.ip, parsedEvent.port)
419 log.debug "Device with mac $parsedEvent.mac not found"
425 void setupHandler(hubResponse) {
426 String contentType = hubResponse?.headers['Content-Type']
427 if (contentType != null && contentType == 'text/xml') {
428 def body = hubResponse.xml
430 String deviceType = body?.device?.deviceType?.text() ?: ""
431 if (deviceType.startsWith("urn:Belkin:device:controllee:1") || deviceType.startsWith("urn:Belkin:device:insight:1")) {
432 wemoDevices = getWemoSwitches()
433 } else if (deviceType.startsWith("urn:Belkin:device:sensor")) {
434 wemoDevices = getWemoMotions()
435 } else if (deviceType.startsWith("urn:Belkin:device:lightswitch")) {
436 wemoDevices = getWemoLightSwitches()
439 def wemoDevice = wemoDevices.find {it?.key?.contains(body?.device?.UDN?.text())}
441 wemoDevice.value << [name:body?.device?.friendlyName?.text(), verified: true]
443 log.error "/setup.xml returned a wemo device that didn't exist"
449 def locationHandler(evt) {
450 def description = evt.description
452 def parsedEvent = parseDiscoveryMessage(description)
453 parsedEvent << ["hub":hub]
454 log.debug parsedEvent
456 if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
457 def switches = getWemoSwitches()
458 if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
459 //if it doesn't already exist
460 switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
462 log.debug "Device was already found in state..."
463 def d = switches."${parsedEvent.ssdpUSN.toString()}"
464 boolean deviceChangedValues = false
465 log.debug "$d.ip <==> $parsedEvent.ip"
466 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
467 d.ip = parsedEvent.ip
468 d.port = parsedEvent.port
469 deviceChangedValues = true
470 log.debug "Device's port or ip changed..."
471 def child = getChildDevice(parsedEvent.mac)
473 child.subscribe(parsedEvent.ip, parsedEvent.port)
476 log.debug "Device with mac $parsedEvent.mac not found"
481 else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
482 def motions = getWemoMotions()
483 if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
484 //if it doesn't already exist
485 motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
486 } else { // just update the values
487 log.debug "Device was already found in state..."
489 def d = motions."${parsedEvent.ssdpUSN.toString()}"
490 boolean deviceChangedValues = false
492 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
493 d.ip = parsedEvent.ip
494 d.port = parsedEvent.port
495 deviceChangedValues = true
496 log.debug "Device's port or ip changed..."
499 if (deviceChangedValues) {
500 def children = getChildDevices()
501 log.debug "Found children ${children}"
503 if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
504 log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
505 it.subscribe(parsedEvent.ip, parsedEvent.port)
512 else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:lightswitch")) {
514 def lightSwitches = getWemoLightSwitches()
516 if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
517 { //if it doesn't already exist
518 lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
520 log.debug "Device was already found in state..."
522 def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
523 boolean deviceChangedValues = false
525 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
526 d.ip = parsedEvent.ip
527 d.port = parsedEvent.port
528 deviceChangedValues = true
529 log.debug "Device's port or ip changed..."
530 def child = getChildDevice(parsedEvent.mac)
532 log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}"
533 child.subscribe(parsedEvent.ip, parsedEvent.port)
535 log.debug "Device with mac $parsedEvent.mac not found"
540 else if (parsedEvent.headers && parsedEvent.body) {
541 String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
542 if (headerString != null && (headerString.contains('text/xml') || headerString.contains('application/xml'))) {
543 def body = parseXmlBody(parsedEvent.body)
544 if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:controllee:1"))
546 def switches = getWemoSwitches()
547 def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())}
550 wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true]
554 log.error "/setup.xml returned a wemo device that didn't exist"
558 if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:insight:1"))
560 def switches = getWemoSwitches()
561 def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())}
564 wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true]
568 log.error "/setup.xml returned a wemo device that didn't exist"
572 if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:sensor")) //?:1
574 def motions = getWemoMotions()
575 def wemoMotion = motions.find {it?.key?.contains(body?.device?.UDN?.text())}
578 wemoMotion.value << [name:body?.device?.friendlyName?.text(), verified: true]
582 log.error "/setup.xml returned a wemo device that didn't exist"
586 if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:lightswitch")) //?:1
588 def lightSwitches = getWemoLightSwitches()
589 def wemoLightSwitch = lightSwitches.find {it?.key?.contains(body?.device?.UDN?.text())}
592 wemoLightSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true]
596 log.error "/setup.xml returned a wemo device that didn't exist"
604 private def parseXmlBody(def body) {
605 def decodedBytes = body.decodeBase64()
608 bodyString = new String(decodedBytes)
609 } catch (Exception e) {
610 // Keep this log for debugging StringIndexOutOfBoundsException issue
611 log.error("Exception decoding bytes in sonos connect: ${decodedBytes}")
614 return new XmlSlurper().parseText(bodyString)
617 private def parseDiscoveryMessage(String description) {
619 def parts = description.split(',')
622 if (part.startsWith('devicetype:')) {
623 part -= "devicetype:"
624 event.devicetype = part.trim()
626 else if (part.startsWith('mac:')) {
628 event.mac = part.trim()
630 else if (part.startsWith('networkAddress:')) {
631 part -= "networkAddress:"
632 event.ip = part.trim()
634 else if (part.startsWith('deviceAddress:')) {
635 part -= "deviceAddress:"
636 event.port = part.trim()
638 else if (part.startsWith('ssdpPath:')) {
640 event.ssdpPath = part.trim()
642 else if (part.startsWith('ssdpUSN:')) {
644 event.ssdpUSN = part.trim()
646 else if (part.startsWith('ssdpTerm:')) {
648 event.ssdpTerm = part.trim()
650 else if (part.startsWith('headers')) {
652 event.headers = part.trim()
654 else if (part.startsWith('body')) {
656 event.body = part.trim()
663 log.debug "Doing Device Sync!"
664 discoverAllWemoTypes()
667 private String convertHexToIP(hex) {
668 [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
671 private Integer convertHexToInt(hex) {
672 Integer.parseInt(hex,16)
675 private Boolean canInstallLabs() {
676 return hasAllHubsOver("000.011.00603")
679 private Boolean hasAllHubsOver(String desiredFirmware) {
680 return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
683 private List getRealHubFirmwareVersions() {
684 return location.hubs*.firmwareVersionString.findAll { it }