diff --git a/devicetypes/kriskit-wemo/wemo-maker-connect.src/wemo-maker-connect.groovy b/devicetypes/kriskit-wemo/wemo-maker-connect.src/wemo-maker-connect.groovy new file mode 100644 index 0000000..7f6d3e9 --- /dev/null +++ b/devicetypes/kriskit-wemo/wemo-maker-connect.src/wemo-maker-connect.groovy @@ -0,0 +1,320 @@ +/** + * Copyright 2015 Chris Kitch + * + * 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. + * + * Wemo Maker Service Manager + * + * Author: Chris Kitch + * Date: 2016-04-06 + */ +definition( + name: "Wemo Maker (Connect)", + namespace: "kriskit.wemo", + author: "Chris Kitch", + description: "Allows you to integrate your WeMo Maker with SmartThings.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png", + singleInstance: true +) + +preferences { + page(name:"firstPage", title:"Wemo Device Setup", content:"firstPage") +} + +def firstPage() +{ + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = 5 + + log.debug "REFRESH COUNT :: ${refreshCount}" + + ssdpSubscribe() + + //ssdp request every 25 seconds + if((refreshCount % 5) == 0) { + discoverWemoMakers() + } + + //setup.xml request every 5 seconds except on discoveries + if(((refreshCount % 1) == 0) && ((refreshCount % 5) != 0)) { + verifyDevices() + } + + def makersDiscovered = makersDiscovered() + + return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section("Select a device...") { + input "selectedMakers", "enum", required:false, title:"Select Wemo Makers \n(${makersDiscovered.size() ?: 0} found)", multiple:true, options:makersDiscovered + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + initialize() +} + +def initialize() { + unsubscribe() + unschedule() + + ssdpSubscribe() + + if (selectedMakers) + addMakers() + + runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds + runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds + runEvery5Minutes("refresh") +} + +private discoverWemoMakers() +{ + log.debug "Sending discover request..." + sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:Belkin:device:Maker:1", physicalgraph.device.Protocol.LAN)) +} + +private getFriendlyName(ip, deviceNetworkId) { + log.debug "Getting friendly name from http://${ip}/setup.xml" + sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}", [callback: "setupHandler"])) +} + +private verifyDevices() { + def makers = getWemoMakers().findAll { it?.value?.verified != true } + def devices = makers + devices.each { + def host = convertHexToIP(it.value.ip) + ":" + convertHexToInt(it.value.port) + def networkId = (it.value.ip + ":" + it.value.port) + getFriendlyName(host, networkId) + } +} + +void ssdpSubscribe() { + if (state.ssdpSubscribed) + return + + log.debug "Subscribing to SSDP..." + subscribe(location, "ssdpTerm.urn:Belkin:device:Maker:1", ssdpMakerHandler) + state.ssdpSubscribed = true +} + +def devicesDiscovered() { + def makers = getWemoMakers() + def devices = makers + devices?.collect{ [app.id, it.ssdpUSN].join('.') } +} + +def makersDiscovered() { + def makers = getWemoMakers().findAll { it?.value?.verified == true } + def map = [:] + makers.each { + def value = it.value.name ?: "WeMo Maker ${it.value.ssdpUSN.split(':')[1][-3..-1]}" + def key = it.value.mac + map["${key}"] = value + } + map +} + +def getWemoMakers() +{ + if (!state.makers) { state.makers = [:] } + state.makers +} + + +def resubscribe() { + log.debug "Resubscribe called, delegating to refresh()" + refresh() +} + +def refresh() { + log.debug "refresh() called" + doDeviceSync() + refreshDevices() +} + +def refreshDevices() { + log.debug "refreshDevices() called" + def devices = getAllChildDevices() + devices.each { d -> + log.debug "Calling refresh() on device: ${d.id}" + d.refresh() + } +} + +def subscribeToDevices() { + log.debug "subscribeToDevices() called" + def devices = getAllChildDevices() + devices.each { d -> + d.subscribe() + } +} + +def addMakers() { + def makers = getWemoMakers() + + selectedMakers.each { dni -> + def selectedMaker = makers.find { it.value.mac == dni } ?: makers.find { "${it.value.ip}:${it.value.port}" == dni } + def d + if (selectedMaker) { + d = getChildDevices()?.find { + it.deviceNetworkId == selectedMaker.value.mac || it.device.getDataValue("mac") == selectedMaker.value.mac + } + } + + if (!d) { + log.debug "Creating WeMo Maker with dni: ${selectedMaker.value.mac}" + d = addChildDevice("kriskit.wemo", "Wemo Maker", selectedMaker.value.mac, selectedMaker?.value.hub, [ + "label": selectedMaker?.value?.name ?: "Wemo Maker", + "data": [ + "mac": selectedMaker.value.mac, + "ip": selectedMaker.value.ip, + "port": selectedMaker.value.port + ] + ]) + def ipvalue = convertHexToIP(selectedMaker.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + } +} + +def ssdpMakerHandler(evt) { + def description = evt.description + def hub = evt?.hubId + def parsedEvent = parseDiscoveryMessage(description) + parsedEvent << ["hub":hub] + + def makers = getWemoMakers() + if (!(makers."${parsedEvent.ssdpUSN.toString()}")) { + //if it doesn't already exist + makers << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } else { + log.debug "Device was already found in state..." + def d = makers."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + log.debug "$d.ip <==> $parsedEvent.ip" + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + log.debug "Device's port or ip changed..." + def child = getChildDevice(parsedEvent.mac) + if (child) { + child.subscribe(parsedEvent.ip, parsedEvent.port) + child.poll() + } else { + log.debug "Device with mac $parsedEvent.mac not found" + } + } + } +} + +void setupHandler(hubResponse) { + String contentType = hubResponse?.headers['Content-Type'] + log.debug "Response received from ${convertHexToIP(hubResponse.ip)}:${convertHexToInt(hubResponse.port)}" + if (contentType != null && contentType == 'text/xml') { + def body = hubResponse.xml + def wemoDevices = [] + String deviceType = body?.device?.deviceType?.text() ?: "" + if (deviceType.startsWith("urn:Belkin:device:Maker:1")) { + wemoDevices = getWemoMakers() + } + + def wemoDevice = wemoDevices.find {it?.key?.contains(body?.device?.UDN?.text())} + if (wemoDevice) { + def friendlyName = body?.device?.friendlyName?.text() + log.debug "Verified '${friendlyName}'" + wemoDevice.value << [name: friendlyName, verified: true] + } else { + log.error "/setup.xml returned a wemo device that didn't exist" + } + } +} + +private def parseDiscoveryMessage(String description) { + def event = [:] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('devicetype:')) { + part -= "devicetype:" + event.devicetype = part.trim() + } + else if (part.startsWith('mac:')) { + part -= "mac:" + event.mac = part.trim() + } + else if (part.startsWith('networkAddress:')) { + part -= "networkAddress:" + event.ip = part.trim() + } + else if (part.startsWith('deviceAddress:')) { + part -= "deviceAddress:" + event.port = part.trim() + } + else if (part.startsWith('ssdpPath:')) { + part -= "ssdpPath:" + event.ssdpPath = part.trim() + } + else if (part.startsWith('ssdpUSN:')) { + part -= "ssdpUSN:" + event.ssdpUSN = part.trim() + } + else if (part.startsWith('ssdpTerm:')) { + part -= "ssdpTerm:" + event.ssdpTerm = part.trim() + } + else if (part.startsWith('headers')) { + part -= "headers:" + event.headers = part.trim() + } + else if (part.startsWith('body')) { + part -= "body:" + event.body = part.trim() + } + } + event +} + +def doDeviceSync(){ + log.debug "Doing Device Sync!" + discoverWemoMakers() +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private Boolean canInstallLabs() { + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) { + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() { + return location.hubs*.firmwareVersionString.findAll { it } +} \ No newline at end of file diff --git a/smartapps/kriskit-wemo/wemo-maker-connect.src/wemo-maker-connect.groovy b/smartapps/kriskit-wemo/wemo-maker-connect.src/wemo-maker-connect.groovy new file mode 100644 index 0000000..7f6d3e9 --- /dev/null +++ b/smartapps/kriskit-wemo/wemo-maker-connect.src/wemo-maker-connect.groovy @@ -0,0 +1,320 @@ +/** + * Copyright 2015 Chris Kitch + * + * 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. + * + * Wemo Maker Service Manager + * + * Author: Chris Kitch + * Date: 2016-04-06 + */ +definition( + name: "Wemo Maker (Connect)", + namespace: "kriskit.wemo", + author: "Chris Kitch", + description: "Allows you to integrate your WeMo Maker with SmartThings.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png", + singleInstance: true +) + +preferences { + page(name:"firstPage", title:"Wemo Device Setup", content:"firstPage") +} + +def firstPage() +{ + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = 5 + + log.debug "REFRESH COUNT :: ${refreshCount}" + + ssdpSubscribe() + + //ssdp request every 25 seconds + if((refreshCount % 5) == 0) { + discoverWemoMakers() + } + + //setup.xml request every 5 seconds except on discoveries + if(((refreshCount % 1) == 0) && ((refreshCount % 5) != 0)) { + verifyDevices() + } + + def makersDiscovered = makersDiscovered() + + return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section("Select a device...") { + input "selectedMakers", "enum", required:false, title:"Select Wemo Makers \n(${makersDiscovered.size() ?: 0} found)", multiple:true, options:makersDiscovered + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + initialize() +} + +def initialize() { + unsubscribe() + unschedule() + + ssdpSubscribe() + + if (selectedMakers) + addMakers() + + runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds + runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds + runEvery5Minutes("refresh") +} + +private discoverWemoMakers() +{ + log.debug "Sending discover request..." + sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:Belkin:device:Maker:1", physicalgraph.device.Protocol.LAN)) +} + +private getFriendlyName(ip, deviceNetworkId) { + log.debug "Getting friendly name from http://${ip}/setup.xml" + sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}", [callback: "setupHandler"])) +} + +private verifyDevices() { + def makers = getWemoMakers().findAll { it?.value?.verified != true } + def devices = makers + devices.each { + def host = convertHexToIP(it.value.ip) + ":" + convertHexToInt(it.value.port) + def networkId = (it.value.ip + ":" + it.value.port) + getFriendlyName(host, networkId) + } +} + +void ssdpSubscribe() { + if (state.ssdpSubscribed) + return + + log.debug "Subscribing to SSDP..." + subscribe(location, "ssdpTerm.urn:Belkin:device:Maker:1", ssdpMakerHandler) + state.ssdpSubscribed = true +} + +def devicesDiscovered() { + def makers = getWemoMakers() + def devices = makers + devices?.collect{ [app.id, it.ssdpUSN].join('.') } +} + +def makersDiscovered() { + def makers = getWemoMakers().findAll { it?.value?.verified == true } + def map = [:] + makers.each { + def value = it.value.name ?: "WeMo Maker ${it.value.ssdpUSN.split(':')[1][-3..-1]}" + def key = it.value.mac + map["${key}"] = value + } + map +} + +def getWemoMakers() +{ + if (!state.makers) { state.makers = [:] } + state.makers +} + + +def resubscribe() { + log.debug "Resubscribe called, delegating to refresh()" + refresh() +} + +def refresh() { + log.debug "refresh() called" + doDeviceSync() + refreshDevices() +} + +def refreshDevices() { + log.debug "refreshDevices() called" + def devices = getAllChildDevices() + devices.each { d -> + log.debug "Calling refresh() on device: ${d.id}" + d.refresh() + } +} + +def subscribeToDevices() { + log.debug "subscribeToDevices() called" + def devices = getAllChildDevices() + devices.each { d -> + d.subscribe() + } +} + +def addMakers() { + def makers = getWemoMakers() + + selectedMakers.each { dni -> + def selectedMaker = makers.find { it.value.mac == dni } ?: makers.find { "${it.value.ip}:${it.value.port}" == dni } + def d + if (selectedMaker) { + d = getChildDevices()?.find { + it.deviceNetworkId == selectedMaker.value.mac || it.device.getDataValue("mac") == selectedMaker.value.mac + } + } + + if (!d) { + log.debug "Creating WeMo Maker with dni: ${selectedMaker.value.mac}" + d = addChildDevice("kriskit.wemo", "Wemo Maker", selectedMaker.value.mac, selectedMaker?.value.hub, [ + "label": selectedMaker?.value?.name ?: "Wemo Maker", + "data": [ + "mac": selectedMaker.value.mac, + "ip": selectedMaker.value.ip, + "port": selectedMaker.value.port + ] + ]) + def ipvalue = convertHexToIP(selectedMaker.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + } +} + +def ssdpMakerHandler(evt) { + def description = evt.description + def hub = evt?.hubId + def parsedEvent = parseDiscoveryMessage(description) + parsedEvent << ["hub":hub] + + def makers = getWemoMakers() + if (!(makers."${parsedEvent.ssdpUSN.toString()}")) { + //if it doesn't already exist + makers << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } else { + log.debug "Device was already found in state..." + def d = makers."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + log.debug "$d.ip <==> $parsedEvent.ip" + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + log.debug "Device's port or ip changed..." + def child = getChildDevice(parsedEvent.mac) + if (child) { + child.subscribe(parsedEvent.ip, parsedEvent.port) + child.poll() + } else { + log.debug "Device with mac $parsedEvent.mac not found" + } + } + } +} + +void setupHandler(hubResponse) { + String contentType = hubResponse?.headers['Content-Type'] + log.debug "Response received from ${convertHexToIP(hubResponse.ip)}:${convertHexToInt(hubResponse.port)}" + if (contentType != null && contentType == 'text/xml') { + def body = hubResponse.xml + def wemoDevices = [] + String deviceType = body?.device?.deviceType?.text() ?: "" + if (deviceType.startsWith("urn:Belkin:device:Maker:1")) { + wemoDevices = getWemoMakers() + } + + def wemoDevice = wemoDevices.find {it?.key?.contains(body?.device?.UDN?.text())} + if (wemoDevice) { + def friendlyName = body?.device?.friendlyName?.text() + log.debug "Verified '${friendlyName}'" + wemoDevice.value << [name: friendlyName, verified: true] + } else { + log.error "/setup.xml returned a wemo device that didn't exist" + } + } +} + +private def parseDiscoveryMessage(String description) { + def event = [:] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('devicetype:')) { + part -= "devicetype:" + event.devicetype = part.trim() + } + else if (part.startsWith('mac:')) { + part -= "mac:" + event.mac = part.trim() + } + else if (part.startsWith('networkAddress:')) { + part -= "networkAddress:" + event.ip = part.trim() + } + else if (part.startsWith('deviceAddress:')) { + part -= "deviceAddress:" + event.port = part.trim() + } + else if (part.startsWith('ssdpPath:')) { + part -= "ssdpPath:" + event.ssdpPath = part.trim() + } + else if (part.startsWith('ssdpUSN:')) { + part -= "ssdpUSN:" + event.ssdpUSN = part.trim() + } + else if (part.startsWith('ssdpTerm:')) { + part -= "ssdpTerm:" + event.ssdpTerm = part.trim() + } + else if (part.startsWith('headers')) { + part -= "headers:" + event.headers = part.trim() + } + else if (part.startsWith('body')) { + part -= "body:" + event.body = part.trim() + } + } + event +} + +def doDeviceSync(){ + log.debug "Doing Device Sync!" + discoverWemoMakers() +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private Boolean canInstallLabs() { + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) { + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() { + return location.hubs*.firmwareVersionString.findAll { it } +} \ No newline at end of file