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 1533e47..47240e1 100644 --- a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy +++ b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy @@ -14,56 +14,103 @@ * */ definition( - name: "OpenT2T SmartApp Test", - namespace: "opent2t", - author: "OpenT2T", - description: "Test app to test end to end SmartThings scenarios via OpenT2T", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", - iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + name: "OpenT2T SmartApp Test", + namespace: "opent2t", + author: "OpenT2T", + description: "Test app to test end to end SmartThings scenarios via OpenT2T", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + +/** --------------------+---------------+-----------------------+------------------------------------ + * Device Type | Attribute Name| Commands | Attribute Values + * --------------------+---------------+-----------------------+------------------------------------ + * switches | switch | on, off | on, off + * motionSensors | motion | | active, inactive + * contactSensors | contact | | open, closed + * presenceSensors | presence | | present, 'not present' + * temperatureSensors | temperature | | + * accelerationSensors | acceleration | | active, inactive + * waterSensors | water | | wet, dry + * lightSensors | illuminance | | + * humiditySensors | humidity | | + * locks | lock | lock, unlock | locked, unlocked + * garageDoors | door | open, close | unknown, closed, open, closing, opening + * cameras | image | take | + * thermostats | thermostat | setHeatingSetpoint, | temperature, heatingSetpoint, coolingSetpoint, + * | | setCoolingSetpoint, | thermostatSetpoint, thermostatMode, + * | | off, heat, cool, auto,| thermostatFanMode, thermostatOperatingState + * | | emergencyHeat, | + * | | setThermostatMode, | + * | | fanOn, fanAuto, | + * | | fanCirculate, | + * | | setThermostatFanMode | + * --------------------+---------------+-----------------------+------------------------------------ + */ //Device Inputs preferences { -section("Allow OpenT2T Test SmartApp to control these things...") { - input "switches", "capability.switch", title: "Which Switches and Lights?", multiple: true, required: false + section("Allow to control these things...") { + input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false + input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors", multiple: true, required: false - input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false - input "waterSensors", "capability.waterSensor", title: "Which Water Leak Sensors?", multiple: true, required: false - input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false - input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false - input "cameras", "capability.videoCamera", title: "Which Cameras?", multiple: true, required: false + input "switches", "capability.switch", title: "Which Switches and Lights?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostat?", multiple: true, required: false + input "waterSensors", "capability.waterSensor", title: "Which Water Leak Sensors?", multiple: true, required: false } } +def getInputs() { + def inputList = [] + 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("/subscriptionURL/:url") { - action: [ - PUT: "updateEndpointURL" - ] - } - path("/connectionId/:connId") { - action: [ - PUT: "updateConnectionId" - ] - } + action: [ + PUT: "updateEndpointURL" + ] + } + path("/connectionId/:connId") { + action: [ + PUT: "updateConnectionId" + ] + } path("/devices") { action: [ GET: "getDevices" ] } - path("/devices/:id") { + path("/devices/:id") { action: [ GET: "getDevice" ] } - path("/update/:id") { - action: [ - PUT: "updateDevice" - ] - } + path("/update/:id") { + action: [ + PUT: "updateDevice" + ] + } + path("/subscription/:id") { + action: [ + POST: "registerDeviceChange", + DELETE: "unregisterDeviceChange" + ] + } } def installed() { @@ -74,282 +121,343 @@ def installed() { def updated() { log.debug "Updated with settings: ${settings}" unsubscribe() - initialize() + registerSubscriptions() } def initialize() { state.connectionId = "" - state.endpointURL = "https://ifs.windows-int.com/v1/cb/81C7E77B-EABC-488A-B2BF-FEC42F0DABD2/notify" + state.endpointURL = "https://ifs.windows-int.com/v1/cb/81C7E77B-EABC-488A-B2BF-FEC42F0DABD2/notify" registerSubscriptions() } -//Register events for each device +//Subscribe events for all devices def registerSubscriptions() { - registerChangeHandler(switches) - registerChangeHandler(motionSensors) - registerChangeHandler(presenceSensors) - registerChangeHandler(contactSensors) - registerChangeHandler(waterSensors) - registerChangeHandler(locks) - registerChangeHandler(garageDoors) - registerChangeHandler(cameras) + 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, eventHandler) - log.debug "Registering ${myDevice.displayName}.${att.name}" + subscribe(myDevice, att.name, eventHandler) + log.info "Registering ${myDevice.displayName}.${att.name}" } } } +//Endpoints function: Subscribe to events from a specific device +def registerDeviceChange() { + def myDevice = findDevice(params.id) + def theAtts = myDevice.supportedAttributes + try { + theAtts.each {att -> + subscribe(myDevice, att.name, eventHandler) + log.info "Registering ${myDevice.displayName}.${att.name}" + } + return ["succeed"] + } catch (e) { + httpError(500, "something went wrong: $e") + } +} + +//Endpoints function: Unsubscribe to events from a specific device +def unregisterDeviceChange() { + def myDevice = findDevice(params.id) + try { + unsubscribe(myDevice) + log.info "Unregistering ${myDevice.displayName}" + return ["succeed"] + } catch (e) { + httpError(500, "something went wrong: $e") + } +} + //When events are triggered, send HTTP post to web socket servers def eventHandler(evt) { - def evt_device_id = evt.deviceId - def evt_device_value = evt.value - def evt_name = evt.name - def evt_device = evt.device - def evt_deviceType = getDeviceType(evt_device); - def params = [ - uri: "${state.endpointURL}/${state.connectionId}", - body: [ - name: evt_device.displayName, - id: evt_device.id, - deviceType:evt_deviceType, - attributes: deviceAttributeList(evt_device) - ] - ] - try { - log.debug "POST URI: ${params.uri}" - log.debug "Payload: ${params.body}" - httpPostJson(params) { resp -> - resp.headers.each { - log.debug "${it.name} : ${it.value}" - } - log.debug "response status code: ${resp.status}" - log.debug "response data: ${resp.data}" - } - } catch (e) { - log.debug "something went wrong: $e" - } -} - -//Endpoints function; update subcription endpoint url [state.endpoint] -void updateEndpointURL() { - state.endpointURL = params.url - log.debug "Updated EndpointURL to ${state.endpointURL}" -} - -//Endpoints function; update global variable [state.connectionId] -void updateConnectionId() { - def connId = params.connId - state.connectionId = connId - log.debug "Updated ConnectionID to ${state.connectionId}" -} - -//Endpoints functions; return data from all devices supported in JSON format -def getDevices() { - def deviceData = [] - switches.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - motionSensors.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - contactSensors.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - presenceSensors.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - waterSensors.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - locks.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - garageDoors.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - cameras.each { - def deviceType = getDeviceType(it) - deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - } - log.debug "getDevices, return: ${deviceData}" - return deviceData -} - -//Endpoints functions; return data from a specific device in JSON format -def getDevice() { - def it = findDevice(params.id) - def deviceType = getDeviceType(it) - def device = [name: it.displayName, id: it.id, deviceType:deviceType, attributes: deviceAttributeList(it)] - log.debug "getDevice, return: ${device}" - return device -} - -//Endpoints functions; update device data -void updateDevice() { - def device = findDevice(params.id) - request.JSON.each { - def command = it.key - def value = it.value - if (command){ - def commandList = mapDeviceCommands(command, value) - command = commandList[0] - value = commandList[1] - if (!device) { - log.debug "updateDevice, Device not found" - httpError(404, "Device not found") - } else if (!device.hasCommand(command)) { - log.debug "updateDevice, Device does not have the command" - httpError(404, "Device does not have the command") - } else { - if (command == "setColor") { - log.debug "Update: [${command}, ${value}], device: ${device}" - device."$command"(hex: value) - } else if(value.isNumber()) { - def intValue = value as Integer - log.debug "Update: [${command}, ${intValue}(int)]" - device."$command"(intValue) - } else if (value){ - log.debug "Update: [${command}, ${value}]" - device."$command"(value) - } else { - device."$command"() - } - } - } + def evt_device_value = evt.value + def evt_name = evt.name + def evt_device = evt.device + def evt_deviceType = getDeviceType(evt_device); + def params = [ + uri: "${state.endpointURL}/${state.connectionId}", + body: [ + name: evt_device.displayName, + id: evt_device.id, + deviceType:evt_deviceType, + manufacturer:evt_device.getManufacturerName(), + model:evt_device.getModelName(), + attributes: deviceAttributeList(evt_device) + ] + ] + try { + log.trace "POST URI: ${params.uri}" + log.trace "Payload: ${params.body}" + httpPostJson(params) { resp -> + resp.headers.each { + log.debug "${it.name} : ${it.value}" + } + log.trace "response status code: ${resp.status}" + log.trace "response data: ${resp.data}" + } + } catch (e) { + log.debug "something went wrong: $e" } } -//Private Functions; manually map each device to a type given it's capabilities +//Endpoints function: update subcription endpoint url [state.endpoint] +void updateEndpointURL() { + state.endpointURL = params.url + log.info "Updated EndpointURL to ${state.endpointURL}" +} + +//Endpoints function: update global variable [state.connectionId] +void updateConnectionId() { + def connId = params.connId + state.connectionId = connId + log.info "Updated ConnectionID to ${state.connectionId}" +} + +//Endpoints function: return all device data in json format +def getDevices() { + def deviceData = [] + inputs?.each { + def deviceType = getDeviceType(it) + if(deviceType == "thermostat") + { + deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it), locationMode: getLocationModeInfo()] + } + else + { + deviceData << [name: it.displayName, id: it.id, deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it)] + } + } + + log.debug "getDevices, return: ${deviceData}" + return deviceData +} + +//Endpoints function: get device data +def getDevice() { + def it = findDevice(params.id) + def deviceType = getDeviceType(it) + def device + if(deviceType == "thermostat") + { + device = [name: it.displayName, id: it.id, deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it), locationMode: getLocationModeInfo()] + } + else + { + device = [name: it.displayName, id: it.id, deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it)] + } + log.debug "getDevice, return: ${device}" + return device +} + +//Endpoints function: update device data +void updateDevice() { + def device = findDevice(params.id) + request.JSON.each { + def command = it.key + def value = it.value + 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}) { + location.setMode(value) + } + }else if (command == "thermostatSetpoint"){ + switch(device.currentThermostatMode){ + case "cool": + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device.setCoolingSetpoint(value) + break + case "heat": + case "emergency heat": + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device.setHeatingSetpoint(value) + break + default: + httpError(501, "this mode: ${device.currentThermostatMode} does not allow changing thermostat setpoint.") + break + } + }else if (!device) { + log.error "updateDevice, Device not found" + httpError(404, "Device not found") + } else if (!device.hasCommand(command)) { + log.error "updateDevice, Device does not have the command" + httpError(404, "Device does not have such command") + } else { + if (command == "setColor") { + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device."$command"(hex: value) + } else if(value.isNumber()) { + def intValue = value as Integer + log.info "Update: ${device.displayName}, [${command}, ${intValue}(int)]" + device."$command"(intValue) + } else if (value){ + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device."$command"(value) + } else { + log.info "Update: ${device.displayName}, [${command}]" + device."$command"() + } + } + } + } +} + +/*** Private Functions ***/ + +//Return current location mode info +private getLocationModeInfo() { + return [mode: location.mode, supported: location.modes.name] +} + +//Map each device to a type given it's capabilities private getDeviceType(device) { def deviceType - def caps = device.capabilities + def caps = device.capabilities + log.debug "capabilities: [${device}, ${caps}]" + log.debug "supported commands: [${device}, ${device.supportedCommands}]" caps.each { - log.debug "capabilities: [${device}, ${caps}]" - if(it.name.toLowerCase().contains("switch")) { - deviceType = "switch" - } - if(it.name.toLowerCase().contains("water")) { - deviceType = "waterSensor" - } - if(it.name.toLowerCase().contains("motion")) { - deviceType = "motionSensor" - } - if(it.name.toLowerCase().contains("presence")) { - deviceType = "presenceSensor" - } - if(it.name.toLowerCase().contains("contact")) { - deviceType = "contactSensor" - } - if(it.name.toLowerCase().contains("lock")) { - deviceType = "lock" - } - if(it.name.toLowerCase().contains("level")) { - deviceType = "light" - } - if(it.name.toLowerCase().contains("garageDoorControl")) { - deviceType = "garageDoor" - } - if(it.name.toLowerCase().contains("video")) { - deviceType = "camera" - } - } - return deviceType + switch(it.name.toLowerCase()) + { + case "switch": + deviceType = "switch" + break + case "switch level": + deviceType = "light" + break + case "contact sensor": + deviceType = "contactSensor" + break + case "garageDoorControl": + deviceType = "garageDoor" + break + case "lock": + deviceType = "lock" + break + case "video camera": + deviceType = "camera" + break + case "motion sensor": + deviceType = "motionSensor" + break + case "presence sensor": + deviceType = "presenceSensor" + break + case "thermostat": + deviceType = "thermostat" + break + case "water sensor": + deviceType = "waterSensor" + break + default: + break + } + } + return deviceType } -//Private Functions; return device +//Return a specific device give the device ID. private findDevice(deviceId) { - def device = switches.find { it.id == deviceId } - if (device) return device - device = motionSensors.find { it.id == deviceId } - if (device) return device - device = waterSensors.find { it.id == deviceId } - if (device) return device - device = contactSensors.find { it.id == deviceId } - if (device) return device - device = presenceSensors.find { it.id == deviceId } - if (device) return device - device = locks.find { it.id == deviceId } - if (device) return device - device = garageDoors.find { it.id == deviceId } - if (device) return device - device = cameras.find { it.id == deviceId } - return device - } + return inputs?.find { it.id == deviceId } +} -//Private Functions; return a list of device attributes +//Return a list of device attributes private deviceAttributeList(device) { - device.supportedAttributes.collectEntries { attribute-> - try { - [ - (attribute.name): device.currentValue(attribute.name) - ] - } catch(e) { - [ - (attribute.name): null - ] - } - } + device.supportedAttributes.collectEntries { attribute-> + try { + [ (attribute.name): device.currentValue(attribute.name) ] + } catch(e) { + [ (attribute.name): null ] + } + } } -//Private Functions; map device command and value. +//Map device command and value. //input command and value are from UWP, //returns resultCommand and resultValue that corresponds with function and value in SmartApps private mapDeviceCommands(command, value) { log.debug "mapDeviceCommands: [${command}, ${value}]" def resultCommand = command - def resultValue = value - switch (command) { - case "switch": - if (value == 1 || value == "1" || value == "on") { - resultCommand = "on" - resultValue = "" - } else if (value == 0 || value == "0" || value == "off") { - resultCommand = "off" - resultValue = "" - } - break - case "level": - resultCommand = "setLevel" - resultValue = value - break - case "hue": - resultCommand = "setHue" - resultValue = value - Log - break - case "saturation": - resultCommand = "setSaturation" - resultValue = value - break - case "color": - resultCommand = "setColor" - resultValue = value - case "locked": - if (value == 1 || value == "1" || value == "lock") { - resultCommand = "lock" - resultValue = "" - } - else if (value == 0 || value == "0" || value == "unlock") { - resultCommand = "unlock" - resultValue = "" - } - break - } - - return [resultCommand,resultValue] - + def resultValue = value + switch (command) { + case "switch": + if (value == 1 || value == "1" || value == "on") { + resultCommand = "on" + resultValue = "" + } else if (value == 0 || value == "0" || value == "off") { + resultCommand = "off" + resultValue = "" + } + break + // light attributes + case "level": + resultCommand = "setLevel" + resultValue = value + break + case "hue": + resultCommand = "setHue" + resultValue = value + Log + break + case "saturation": + resultCommand = "setSaturation" + resultValue = value + break + case "ct": + resultCommand = "setColorTemperature" + resultValue = value + break + case "color": + resultCommand = "setColor" + resultValue = value + // thermostat attributes + case "hvacMode": + resultCommand = "setThermostatMode" + resultValue = value + break + case "fanMode": + resultCommand = "setThermostatFanMode" + resultValue = value + break + case "awayMode": + resultCommand = "setAwayMode" + resultValue = value + break + case "coolingSetpoint": + resultCommand = "setCoolingSetpoint" + resultValue = value + break + case "heatingSetpoint": + resultCommand = "setHeatingSetpoint" + resultValue = value + break + case "thermostatSetpoint": + resultCommand = "thermostatSetpoint" + resultValue = value + break + // lock attributes + case "locked": + if (value == 1 || value == "1" || value == "lock") { + resultCommand = "lock" + resultValue = "" + } + else if (value == 0 || value == "0" || value == "unlock") { + resultCommand = "unlock" + resultValue = "" + } + break + default: + break + } + + return [resultCommand,resultValue] } \ No newline at end of file