/** * Copyright 2016 Eric Maycock * * 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. * * Sonoff (Connect) * * Author: Eric Maycock (erocm123) * Date: 2016-06-02 */ definition( name: "Sonoff (Connect)", namespace: "erocm123", author: "Eric Maycock (erocm123)", description: "Service Manager for Sonoff switches", category: "Convenience", iconUrl: "https://raw.githubusercontent.com/erocm123/SmartThingsPublic/master/smartapps/erocm123/sonoff-connect.src/sonoff-connect-icon.png", iconX2Url: "https://raw.githubusercontent.com/erocm123/SmartThingsPublic/master/smartapps/erocm123/sonoff-connect.src/sonoff-connect-icon-2x.png", iconX3Url: "https://raw.githubusercontent.com/erocm123/SmartThingsPublic/master/smartapps/erocm123/sonoff-connect.src/sonoff-connect-icon-3x.png" ) preferences { page(name: "mainPage") page(name: "configurePDevice") page(name: "deletePDevice") page(name: "changeName") page(name: "discoveryPage", title: "Device Discovery", content: "discoveryPage", refreshTimeout:5) page(name: "addDevices", title: "Add Sonoff Switches", content: "addDevices") page(name: "deviceDiscovery") } def mainPage() { dynamicPage(name: "mainPage", title: "Manage your Sonoff switches", nextPage: null, uninstall: true, install: true) { section("Configure"){ href "deviceDiscovery", title:"Discover Sonoff Devices", description:"" } section("Installed Devices"){ getChildDevices().sort({ a, b -> a["deviceNetworkId"] <=> b["deviceNetworkId"] }).each { href "configurePDevice", title:"$it.label", description:"", params: [did: it.deviceNetworkId] } } } } def configurePDevice(params){ def currentDevice getChildDevices().each { if(it.deviceNetworkId == params.did){ state.currentDeviceId = it.deviceNetworkId state.currentDisplayName = it.displayName } } dynamicPage(name: "configurePDevice", title: "Configure Sonoff Switches created with this app", nextPage: null) { section { app.updateSetting("${state.currentDeviceId}_label", getChildDevice(state.currentDeviceId).label) input "${state.currentDeviceId}_label", "text", title:"Device Name", description: "", required: false href "changeName", title:"Change Device Name", description: "Edit the name above and click here to change it", params: [did: state.currentDeviceId] } section { href "deletePDevice", title:"Delete $state.currentDisplayName", description: "", params: [did: state.currentDeviceId] } } } def deletePDevice(params){ try { unsubscribe() getChildDevices().each { if(it.deviceNetworkId.startsWith("${params.did}/")) deleteChildDevice(it.deviceNetworkId) } deleteChildDevice(params.did) dynamicPage(name: "deletePDevice", title: "Deletion Summary", nextPage: "mainPage") { section { paragraph "The device has been deleted. Press next to continue" } } } catch (e) { dynamicPage(name: "deletePDevice", title: "Deletion Summary", nextPage: "mainPage") { section { paragraph "Error: ${(e as String).split(":")[1]}." } } } } def changeName(params){ def thisDevice = getChildDevice(params.did) thisDevice.label = settings["${params.did}_label"] dynamicPage(name: "changeName", title: "Change Name Summary", nextPage: "mainPage") { section { paragraph "The device has been renamed. Press \"Next\" to continue" } } } def discoveryPage(){ return deviceDiscovery() } def deviceDiscovery(params=[:]) { def devices = devicesDiscovered() int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int state.deviceRefreshCount = deviceRefreshCount + 1 def refreshInterval = 3 def options = devices ?: [] def numFound = options.size() ?: 0 if ((numFound == 0 && state.deviceRefreshCount > 25) || params.reset == "true") { log.trace "Cleaning old device memory" state.devices = [:] state.deviceRefreshCount = 0 app.updateSetting("selectedDevice", "") } ssdpSubscribe() //sonoff discovery request every 15 //25 seconds if((deviceRefreshCount % 5) == 0) { discoverDevices() } //setup.xml request every 3 seconds except on discoveries if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 5) != 0)) { verifyDevices() } return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"addDevices", refreshInterval:refreshInterval, uninstall: true) { section("Please wait while we discover your Sonoff devices. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { input "selectedDevices", "enum", required:false, title:"Select Sonoff Switch (${numFound} found)", multiple:true, options:options } section("Options") { href "deviceDiscovery", title:"Reset list of discovered devices", description:"", params: ["reset": "true"] } } } Map devicesDiscovered() { def vdevices = getVerifiedDevices() def map = [:] vdevices.each { def value = "${it.value.name}" def key = "${it.value.mac}" map["${key}"] = value } map } def getVerifiedDevices() { getDevices().findAll{ it?.value?.verified == true } } private discoverDevices() { sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:Basic:1", physicalgraph.device.Protocol.LAN)) } def configured() { } def buttonConfigured(idx) { return settings["lights_$idx"] } def isConfigured(){ if(getChildDevices().size() > 0) return true else return false } def isVirtualConfigured(did){ def foundDevice = false getChildDevices().each { if(it.deviceNetworkId != null){ if(it.deviceNetworkId.startsWith("${did}/")) foundDevice = true } } return foundDevice } private virtualCreated(number) { if (getChildDevice(getDeviceID(number))) { return true } else { return false } } private getDeviceID(number) { return "${state.currentDeviceId}/${app.id}/${number}" } def installed() { initialize() } def updated() { unsubscribe() unschedule() initialize() } def initialize() { ssdpSubscribe() runEvery5Minutes("ssdpDiscover") } void ssdpSubscribe() { subscribe(location, "ssdpTerm.urn:schemas-upnp-org:device:Basic:1", ssdpHandler) } void ssdpDiscover() { sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:Basic:1", physicalgraph.device.Protocol.LAN)) } def ssdpHandler(evt) { def description = evt.description def hub = evt?.hubId def parsedEvent = parseLanMessage(description) parsedEvent << ["hub":hub] def devices = getDevices() String ssdpUSN = parsedEvent.ssdpUSN.toString() if (devices."${ssdpUSN}") { def d = devices."${ssdpUSN}" def child = getChildDevice(parsedEvent.mac) def childIP def childPort if (child) { childIP = child.getDeviceDataByName("ip") childPort = child.getDeviceDataByName("port").toString() log.debug "Device data: ($childIP:$childPort) - reporting data: (${convertHexToIP(parsedEvent.networkAddress)}:${convertHexToInt(parsedEvent.deviceAddress)})." if(childIP != convertHexToIP(parsedEvent.networkAddress) || childPort != convertHexToInt(parsedEvent.deviceAddress).toString()){ log.debug "Device data (${child.getDeviceDataByName("ip")}) does not match what it is reporting(${convertHexToIP(parsedEvent.networkAddress)}). Attempting to update." child.sync(convertHexToIP(parsedEvent.networkAddress), convertHexToInt(parsedEvent.deviceAddress).toString()) } } if (d.networkAddress != parsedEvent.networkAddress || d.deviceAddress != parsedEvent.deviceAddress) { d.networkAddress = parsedEvent.networkAddress d.deviceAddress = parsedEvent.deviceAddress } } else { devices << ["${ssdpUSN}": parsedEvent] } } void verifyDevices() { def devices = getDevices().findAll { it?.value?.verified != true } devices.each { def ip = convertHexToIP(it.value.networkAddress) def port = convertHexToInt(it.value.deviceAddress) String host = "${ip}:${port}" sendHubCommand(new physicalgraph.device.HubAction("""GET ${it.value.ssdpPath} HTTP/1.1\r\nHOST: $host\r\n\r\n""", physicalgraph.device.Protocol.LAN, host, [callback: deviceDescriptionHandler])) } } def getDevices() { state.devices = state.devices ?: [:] } void deviceDescriptionHandler(physicalgraph.device.HubResponse hubResponse) { log.trace "description.xml response (application/xml)" def body = hubResponse.xml if (body?.device?.modelName?.text().startsWith("Sonoff Wifi Switch")) { def devices = getDevices() def device = devices.find {it?.key?.contains(body?.device?.UDN?.text())} if (device) { device.value << [name:body?.device?.friendlyName?.text() + " (" + convertHexToIP(hubResponse.ip) + ")", serialNumber:body?.device?.serialNumber?.text(), verified: true] } else { log.error "/description.xml returned a device that didn't exist" } } } def addDevices() { def devices = getDevices() def sectionText = "" selectedDevices.each { dni ->bridgeLinking def selectedDevice = devices.find { it.value.mac == dni } def d if (selectedDevice) { d = getChildDevices()?.find { it.deviceNetworkId == selectedDevice.value.mac } } if (!d) { log.debug "Creating Sonoff Switch with dni: ${selectedDevice.value.mac}" log.debug Integer.parseInt(selectedDevice.value.deviceAddress,16) addChildDevice("erocm123", "Sonoff Wifi Switch", selectedDevice.value.mac, selectedDevice?.value.hub, [ "label": selectedDevice?.value?.name ?: "Sonoff Wifi Switch", "data": [ "mac": selectedDevice.value.mac, "ip": convertHexToIP(selectedDevice.value.networkAddress), "port": "" + Integer.parseInt(selectedDevice.value.deviceAddress,16) ] ]) sectionText = sectionText + "Succesfully added Sonoff Wifi Switch with ip address ${convertHexToIP(selectedDevice.value.networkAddress)} \r\n" } } log.debug sectionText return dynamicPage(name:"addDevices", title:"Devices Added", nextPage:"mainPage", uninstall: true) { if(sectionText != ""){ section("Add Sonoff Results:") { paragraph sectionText } }else{ section("No devices added") { paragraph "All selected devices have previously been added" } } } } def uninstalled() { unsubscribe() getChildDevices().each { deleteChildDevice(it.deviceNetworkId) } } 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) } def log(message){ }