From 8bfc3f0c1c25e5d0d398030192835e4dd62fdeea Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Mon, 8 May 2017 09:53:51 -0600 Subject: [PATCH] DVCSMP-2613OpenT2T: Update to 5/1 submission --- .../opent2t-smartapp-test.groovy | 246 +++++++++++------- 1 file changed, 152 insertions(+), 94 deletions(-) diff --git a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy index 845e8db..24d829c 100644 --- a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy +++ b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy @@ -1,3 +1,7 @@ +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; + /** * OpenT2T SmartApp Test * @@ -39,7 +43,7 @@ definition( * garageDoors | door | open, close | unknown, closed, open, closing, opening * cameras | image | take | * thermostats | thermostat | setHeatingSetpoint, | temperature, heatingSetpoint, coolingSetpoint, - * | | setCoolingSetpoint, | thermostatSetpoint, thermostatMode, + * | | setCoolingSetpoint, | thermostatSetpoint, thermostatMode, * | | off, heat, cool, auto,| thermostatFanMode, thermostatOperatingState * | | emergencyHeat, | * | | setThermostatMode, | @@ -55,7 +59,7 @@ preferences { input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false, hideWhenEmpty: true input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false, hideWhenEmpty: true input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false, hideWhenEmpty: true - input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false, hideWhenEmpty: true + input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false, hideWhenEmpty: true input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false, hideWhenEmpty: true input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors", multiple: true, required: false, hideWhenEmpty: true input "switches", "capability.switch", title: "Which Switches and Lights?", multiple: true, required: false, hideWhenEmpty: true @@ -66,44 +70,49 @@ preferences { def getInputs() { def inputList = [] - inputList += contactSensors?: [] - inputList += garageDoors?: [] - inputList += locks?: [] - inputList += cameras?: [] - inputList += motionSensors?: [] - inputList += presenceSensors?: [] - inputList += switches?: [] - inputList += thermostats?: [] - inputList += waterSensors?: [] + inputList += contactSensors ?: [] + inputList += garageDoors ?: [] + inputList += locks ?: [] + inputList += cameras ?: [] + inputList += motionSensors ?: [] + inputList += presenceSensors ?: [] + inputList += switches ?: [] + inputList += thermostats ?: [] + inputList += waterSensors ?: [] return inputList } //API external Endpoints mappings { path("/devices") { - action: [ + action: + [ GET: "getDevices" ] } path("/devices/:id") { - action: [ + action: + [ GET: "getDevice" ] } path("/update/:id") { - action: [ + action: + [ PUT: "updateDevice" ] } path("/deviceSubscription") { - action: [ - POST: "registerDeviceChange", + action: + [ + POST : "registerDeviceChange", DELETE: "unregisterDeviceChange" ] } path("/locationSubscription") { - action: [ - POST: "registerDeviceGraph", + action: + [ + POST : "registerDeviceGraph", DELETE: "unregisterDeviceGraph" ] } @@ -116,14 +125,21 @@ def installed() { def updated() { log.debug "Updating with settings: ${settings}" - if(state.deviceSubscriptionMap == null){ + + //Initialize state variables if didn't exist. + if (state.deviceSubscriptionMap == null) { state.deviceSubscriptionMap = [:] log.debug "deviceSubscriptionMap created." } - if( state.locationSubscriptionMap == null){ + if (state.locationSubscriptionMap == null) { state.locationSubscriptionMap = [:] log.debug "locationSubscriptionMap created." } + if (state.verificationKeyMap == null) { + state.verificationKeyMap = [:] + log.debug "verificationKeyMap created." + } + unsubscribe() registerAllDeviceSubscriptions() } @@ -132,9 +148,11 @@ def initialize() { log.debug "Initializing with settings: ${settings}" state.deviceSubscriptionMap = [:] log.debug "deviceSubscriptionMap created." - registerAllDeviceSubscriptions() state.locationSubscriptionMap = [:] log.debug "locationSubscriptionMap created." + state.verificationKeyMap = [:] + log.debug "verificationKeyMap created." + registerAllDeviceSubscriptions() } /*** Subscription Functions ***/ @@ -144,47 +162,43 @@ def registerAllDeviceSubscriptions() { registerChangeHandler(inputs) } -//Subscribe to events from a list of devices -def registerChangeHandler(myList) { - myList.each { myDevice -> - def theAtts = myDevice.supportedAttributes - theAtts.each {att -> - subscribe(myDevice, att.name, deviceEventHandler) - log.info "Registering for ${myDevice.displayName}.${att.name}" - } - } -} - //Endpoints function: Subscribe to events from a specific device def registerDeviceChange() { def subscriptionEndpt = params.subscriptionURL def deviceId = params.deviceId def myDevice = findDevice(deviceId) - if( myDevice == null ){ + + if (myDevice == null) { httpError(404, "Cannot find device with device ID ${deviceId}.") } def theAtts = myDevice.supportedAttributes try { - theAtts.each {att -> + theAtts.each { att -> subscribe(myDevice, att.name, deviceEventHandler) } log.info "Subscribing for ${myDevice.displayName}" - if(subscriptionEndpt != null){ - if(state.deviceSubscriptionMap[deviceId] == null){ + if (subscriptionEndpt != null) { + if (state.deviceSubscriptionMap[deviceId] == null) { state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt]) log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" - } else if (!state.deviceSubscriptionMap[deviceId].contains(subscriptionEndpt)){ + } else if (!state.deviceSubscriptionMap[deviceId].contains(subscriptionEndpt)) { state.deviceSubscriptionMap[deviceId] << subscriptionEndpt log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" } + + if (params.key != null) { + state.verificationKeyMap[subscriptionEndpt] = params.key + log.info "Added verification key: ${params.key} for ${subscriptionEndpt}" + } } } catch (e) { httpError(500, "something went wrong: $e") } log.info "Current subscription map is ${state.deviceSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" return ["succeed"] } @@ -194,18 +208,19 @@ def unregisterDeviceChange() { def deviceId = params.deviceId def myDevice = findDevice(deviceId) - if( myDevice == null ){ + if (myDevice == null) { httpError(404, "Cannot find device with device ID ${deviceId}.") } try { - if(subscriptionEndpt != null && subscriptionEndpt != "undefined"){ - if (state.deviceSubscriptionMap[deviceId]?.contains(subscriptionEndpt)){ - if(state.deviceSubscriptionMap[deviceId].size() == 1){ + if (subscriptionEndpt != null && subscriptionEndpt != "undefined") { + if (state.deviceSubscriptionMap[deviceId]?.contains(subscriptionEndpt)) { + if (state.deviceSubscriptionMap[deviceId].size() == 1) { state.deviceSubscriptionMap.remove(deviceId) } else { state.deviceSubscriptionMap[deviceId].remove(subscriptionEndpt) } + state.verificationKeyMap.remove(subscriptionEndpt) log.info "Removed subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" } } else { @@ -217,25 +232,33 @@ def unregisterDeviceChange() { } log.info "Current subscription map is ${state.deviceSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" } //Endpoints function: Subscribe to device additiona/removal updated in a location def registerDeviceGraph() { def subscriptionEndpt = params.subscriptionURL - if (subscriptionEndpt != null && subscriptionEndpt != "undefined"){ + if (subscriptionEndpt != null && subscriptionEndpt != "undefined") { subscribe(location, "DeviceCreated", locationEventHandler, [filterEvents: false]) subscribe(location, "DeviceUpdated", locationEventHandler, [filterEvents: false]) subscribe(location, "DeviceDeleted", locationEventHandler, [filterEvents: false]) - if(state.locationSubscriptionMap[location.id] == null){ + if (state.locationSubscriptionMap[location.id] == null) { state.locationSubscriptionMap.put(location.id, [subscriptionEndpt]) log.info "Added subscription URL: ${subscriptionEndpt} for Location ${location.name}" - } else if (!state.locationSubscriptionMap[location.id].contains(subscriptionEndpt)){ + } else if (!state.locationSubscriptionMap[location.id].contains(subscriptionEndpt)) { state.locationSubscriptionMap[location.id] << subscriptionEndpt log.info "Added subscription URL: ${subscriptionEndpt} for Location ${location.name}" } + + if (params.key != null) { + state.verificationKeyMap[subscriptionEndpt] = params.key + log.info "Added verification key: ${params.key} for ${subscriptionEndpt}" + } + log.info "Current location subscription map is ${state.locationSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" return ["succeed"] } else { httpError(400, "missing input parameter: subscriptionURL") @@ -247,16 +270,17 @@ def unregisterDeviceGraph() { def subscriptionEndpt = params.subscriptionURL try { - if(subscriptionEndpt != null && subscriptionEndpt != "undefined"){ - if (state.locationSubscriptionMap[location.id]?.contains(subscriptionEndpt)){ - if(state.locationSubscriptionMap[location.id].size() == 1){ + if (subscriptionEndpt != null && subscriptionEndpt != "undefined") { + if (state.locationSubscriptionMap[location.id]?.contains(subscriptionEndpt)) { + if (state.locationSubscriptionMap[location.id].size() == 1) { state.locationSubscriptionMap.remove(location.id) } else { state.locationSubscriptionMap[location.id].remove(subscriptionEndpt) } + state.verificationKeyMap.remove(subscriptionEndpt) log.info "Removed subscription URL: ${subscriptionEndpt} for Location ${location.name}" } - }else{ + } else { httpError(400, "missing input parameter: subscriptionURL") } } catch (e) { @@ -264,28 +288,40 @@ def unregisterDeviceGraph() { } log.info "Current location subscription map is ${state.locationSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" } //When events are triggered, send HTTP post to web socket servers def deviceEventHandler(evt) { - def evt_device = evt.device - def evt_deviceType = getDeviceType(evt_device) - def deviceInfo + def evtDevice = evt.device + def evtDeviceType = getDeviceType(evtDevice) + def deviceData = []; - def params = [ body: [deviceName: evt_device.displayName, deviceId: evt_device.id, locationId: location.id] ] - - if(evt.data != null){ + if (evt.data != null) { def evtData = parseJson(evt.data) - log.info "Received event for ${evt_device.displayName}, data: ${evtData}, description: ${evt.descriptionText}" + log.info "Received event for ${evtDevice.displayName}, data: ${evtData}, description: ${evt.descriptionText}" } + if (evtDeviceType == "thermostat") { + deviceData = [name: evtDevice.displayName, id: evtDevice.id, status: evtDevice.status, deviceType: evtDeviceType, manufacturer: evtDevice.manufacturerName, model: evtDevice.modelName, attributes: deviceAttributeList(evtDevice, evtDeviceType), locationMode: getLocationModeInfo(), locationId: location.id] + } else { + deviceData = [name: evtDevice.displayName, id: evtDevice.id, status: evtDevice.status, deviceType: evtDeviceType, manufacturer: evtDevice.manufacturerName, model: evtDevice.modelName, attributes: deviceAttributeList(evtDevice, evtDeviceType), locationId: location.id] + } + + def params = [body: deviceData] + //send event to all subscriptions urls - log.debug "Current subscription urls for ${evt_device.displayName} is ${state.deviceSubscriptionMap[evt_device.id]}" - state.deviceSubscriptionMap[evt_device.id].each { + log.debug "Current subscription urls for ${evtDevice.displayName} is ${state.deviceSubscriptionMap[evtDevice.id]}" + state.deviceSubscriptionMap[evtDevice.id].each { params.uri = "${it}" + if (state.verificationKeyMap[it] != null) { + def key = state.verificationKeyMap[it] + params.header = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] + } log.trace "POST URI: ${params.uri}" + log.trace "Header: ${params.header}" log.trace "Payload: ${params.body}" - try{ + try { httpPostJson(params) { resp -> log.trace "response status code: ${resp.status}" log.trace "response data: ${resp.data}" @@ -298,20 +334,27 @@ def deviceEventHandler(evt) { def locationEventHandler(evt) { log.info "Received event for location ${location.name}/${location.id}, Event: ${evt.name}, description: ${evt.descriptionText}, apiServerUrl: ${apiServerUrl("")}" - switch(evt.name){ + switch (evt.name) { case "DeviceCreated": case "DeviceDeleted": - def evt_device = evt.device - def evt_deviceType = getDeviceType(evt_device) - log.info "DeviceName: ${evt_device.displayName}, DeviceID: ${evt_device.id}, deviceType: ${evt_deviceType}" + def evtDevice = evt.device + def evtDeviceType = getDeviceType(evtDevice) + def params = [body: [eventType: evt.name, deviceId: evtDevice.id, locationId: location.id]] - def params = [ body: [ eventType:evt.name, deviceId: evt_device.id, locationId: location.id ] ] + if (evt.name == "DeviceDeleted" && state.deviceSubscriptionMap[deviceId] != null) { + state.deviceSubscriptionMap.remove(evtDevice.id) + } state.locationSubscriptionMap[location.id].each { params.uri = "${it}" + if (state.verificationKeyMap[it] != null) { + def key = state.verificationKeyMap[it] + params.header = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] + } log.trace "POST URI: ${params.uri}" + log.trace "Header: ${params.header}" log.trace "Payload: ${params.body}" - try{ + try { httpPostJson(params) { resp -> log.trace "response status code: ${resp.status}" log.trace "response data: ${resp.data}" @@ -326,6 +369,23 @@ def locationEventHandler(evt) { } } +private ComputHMACValue(key, data) { + try { + SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA1") + Mac mac = Mac.getInstance("HmacSHA1") + mac.init(secretKeySpec) + byte[] digest = mac.doFinal(data.getBytes("UTF-8")) + return byteArrayToString(digest) + } catch (InvalidKeyException e) { + log.error "Invalid key exception while converting to HMac SHA1" + } +} + +private def byteArrayToString(byte[] data) { + BigInteger bigInteger = new BigInteger(1, data) + String hash = bigInteger.toString(16) + return hash +} /*** Device Query/Update Functions ***/ @@ -334,10 +394,10 @@ def getDevices() { def deviceData = [] inputs?.each { def deviceType = getDeviceType(it) - if(deviceType == "thermostat") { - deviceData << [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it, deviceType), locationMode: getLocationModeInfo()] + if (deviceType == "thermostat") { + deviceData << [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType), locationMode: getLocationModeInfo()] } else { - deviceData << [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it, deviceType)] + deviceData << [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType)] } } @@ -350,10 +410,10 @@ def getDevice() { def it = findDevice(params.id) def deviceType = getDeviceType(it) def device - if(deviceType == "thermostat") { - device = [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it,deviceType), locationMode: getLocationModeInfo()] + if (deviceType == "thermostat") { + device = [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType), locationMode: getLocationModeInfo()] } else { - device = [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it, deviceType)] + device = [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType)] } log.debug "getDevice, return: ${device}" @@ -366,18 +426,18 @@ void updateDevice() { request.JSON.each { def command = it.key def value = it.value - if (command){ + if (command) { def commandList = mapDeviceCommands(command, value) command = commandList[0] value = commandList[1] if (command == "setAwayMode") { log.info "Setting away mode to ${value}" - if (location.modes?.find {it.name == value}) { + if (location.modes?.find { it.name == value }) { location.setMode(value) } - }else if (command == "thermostatSetpoint"){ - switch(device.currentThermostatMode){ + } else if (command == "thermostatSetpoint") { + switch (device.currentThermostatMode) { case "cool": log.info "Update: ${device.displayName}, [${command}, ${value}]" device.setCoolingSetpoint(value) @@ -391,7 +451,7 @@ void updateDevice() { httpError(501, "this mode: ${device.currentThermostatMode} does not allow changing thermostat setpoint.") break } - }else if (!device) { + } else if (!device) { log.error "updateDevice, Device not found" httpError(404, "Device not found") } else if (!device.hasCommand(command)) { @@ -401,11 +461,11 @@ void updateDevice() { if (command == "setColor") { log.info "Update: ${device.displayName}, [${command}, ${value}]" device."$command"(hex: value) - } else if(value.isNumber()) { + } else if (value.isNumber()) { def intValue = value as Integer log.info "Update: ${device.displayName}, [${command}, ${intValue}(int)]" device."$command"(intValue) - } else if (value){ + } else if (value) { log.info "Update: ${device.displayName}, [${command}, ${value}]" device."$command"(value) } else { @@ -432,17 +492,16 @@ private getDeviceType(device) { log.debug "supported commands: [${device}, ${device.supportedCommands}]" //Loop through the device capability list to determine the device type. - capabilities.each {capability -> - switch(capability.name.toLowerCase()) - { + capabilities.each { capability -> + switch (capability.name.toLowerCase()) { case "switch": deviceType = "switch" //If the device also contains "Switch Level" capability, identify it as a "light" device. - if (capabilities.any{it.name.toLowerCase() == "switch level"}){ + if (capabilities.any { it.name.toLowerCase() == "switch level" }) { //If the device also contains "Power Meter" capability, identify it as a "dimmerSwitch" device. - if (capabilities.any{it.name.toLowerCase() == "power meter"}){ + if (capabilities.any { it.name.toLowerCase() == "power meter" }) { deviceType = "dimmerSwitch" return deviceType } else { @@ -489,24 +548,24 @@ private deviceAttributeList(device, deviceType) { allAttributes.each { attribute -> try { def currentState = device.currentState(attribute.name) - if(currentState != null ){ - switch(attribute.name){ + if (currentState != null) { + switch (attribute.name) { case 'temperature': - attributeList.putAll([ (attribute.name): currentState.value, 'temperatureScale':location.temperatureScale ]) + attributeList.putAll([(attribute.name): currentState.value, 'temperatureScale': location.temperatureScale]) break; default: - attributeList.putAll([(attribute.name): currentState.value ]) + attributeList.putAll([(attribute.name): currentState.value]) break; } - if( deviceType == "genericSensor" ){ + if (deviceType == "genericSensor") { def key = attribute.name + "_lastUpdated" - attributeList.putAll([ (key): currentState.isoDate ]) + attributeList.putAll([(key): currentState.isoDate]) } } else { - attributeList.putAll([ (attribute.name): null ]); + attributeList.putAll([(attribute.name): null]); } - } catch(e) { - attributeList.putAll([ (attribute.name): null ]); + } catch (e) { + attributeList.putAll([(attribute.name): null]); } } return attributeList @@ -579,8 +638,7 @@ private mapDeviceCommands(command, value) { if (value == 1 || value == "1" || value == "lock") { resultCommand = "lock" resultValue = "" - } - else if (value == 0 || value == "0" || value == "unlock") { + } else if (value == 0 || value == "0" || value == "unlock") { resultCommand = "unlock" resultValue = "" } @@ -589,5 +647,5 @@ private mapDeviceCommands(command, value) { break } - return [resultCommand,resultValue] + return [resultCommand, resultValue] }