From 91c01dc643afb2c35894ec67af810ef3dccdd7fc Mon Sep 17 00:00:00 2001 From: Luke Bredeson Date: Thu, 29 Oct 2015 12:35:28 -0500 Subject: [PATCH 01/21] DST fix for Button Controller --- .../button-controller.src/button-controller.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/button-controller.src/button-controller.groovy b/smartapps/smartthings/button-controller.src/button-controller.groovy index 1f99b08..d03551b 100644 --- a/smartapps/smartthings/button-controller.src/button-controller.groovy +++ b/smartapps/smartthings/button-controller.src/button-controller.groovy @@ -294,8 +294,8 @@ private getTimeOk() { def result = true if (starting && ending) { def currTime = now() - def start = timeToday(starting).time - def stop = timeToday(ending).time + def start = timeToday(starting, location.timeZone).time + def stop = timeToday(ending, location.timeZone).time result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start } log.trace "timeOk = $result" From d17cadc4c7cbcf365fe403452d42e6325cc522a8 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Fri, 22 Apr 2016 17:41:57 -0700 Subject: [PATCH 02/21] stage 2 of adding preferences to support garage sensor in mutli sensor --- .../smartsense-multi.groovy | 85 ++++++++++++++++--- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy index 17f5ddd..8ed1a3d 100644 --- a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy +++ b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy @@ -45,17 +45,28 @@ metadata { } preferences { - input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + section { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + section { + input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) + } } tiles(scale: 2) { - multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ - tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" + multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ + tileAttribute ("device.status", key: "PRIMARY_CONTROL") { attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" + attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" + attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" } } + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") @@ -78,8 +89,8 @@ metadata { state "battery", label:'${currentValue}% battery', unit:"" } - main(["contact", "acceleration", "temperature"]) - details(["contact", "acceleration", "temperature", "battery"]) + main(["status", "acceleration", "temperature"]) + details(["status", "acceleration", "temperature", "battery"]) } } @@ -87,7 +98,9 @@ def parse(String description) { def results if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { - results = parseSingleMessage(description) + if (garageSensor != "Yes") { + results = parseSingleMessage(description) + } } else if (description == 'updated') { //TODO is there a better way to handle this like the other device types? @@ -203,7 +216,9 @@ private List parseContactMessage(String description) { parts.each { part -> part = part.trim() if (part.startsWith('contactState:')) { - results.addAll(getContactResult(part, description)) + if (garageSensor != "Yes") { + results.addAll(getContactResult(part, description)) + } } else if (part.startsWith('accelerationState:')) { results << getAccelerationResult(part, description) @@ -225,6 +240,29 @@ private List parseContactMessage(String description) { results } +def updated() { + log.debug "updated called" + log.info "garage value : $garageSensor" + if (garageSensor == "Yes") { + def descriptionText = "Updating device to garage sensor" + if (device.latestValue("status") == "open") { + sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText) + } + else if (device.latestValue("status") == "closed") { + sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText) + } + } + else { + def descriptionText = "Updating device to open/close sensor" + if (device.latestValue("status") == "garage-open") { + sendEvent(name: 'status', value: 'open', descriptionText: descriptionText) + } + else if (device.latestValue("status") == "garage-closed") { + sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText) + } + } +} + private List parseOrientationMessage(String description) { def results = [] def xyzResults = [x: 0, y: 0, z: 0] @@ -254,7 +292,34 @@ private List parseOrientationMessage(String description) { } } - results << getXyzResult(xyzResults, description) + def xyz = getXyzResult(xyzResults, description) + results << xyz + + if (garageSensor == "Yes") { + // Looks for Z-axis orientation as virtual contact state + def a = xyz.value.split(',').collect{it.toInteger()} + def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1])) + def absValueZ = Math.abs(a[2]) + log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ" + + def contactValue = null + def garageValue = null + + if (absValueZ > 825 && absValueXY < 175) { + contactValue = 'open' + garageValue = 'garage-open' + log.debug "STATUS: open" + } + else if (absValueZ < 75 && absValueXY > 825) { + contactValue = 'closed' + garageValue = 'garage-closed' + log.debug "STATUS: closed" + } + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${contactValue == 'open' ? 'opened' : 'closed'}" + results << createEvent(name: "contact", value: contactValue, descriptionText: descriptionText, unit: "", displayed: false) + results << createEvent(name: "status", value: garageValue, descriptionText: descriptionText, unit: "") + } results } From 27c05f4e5b6f79a8a86611f5526e627d6c497683 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Fri, 6 May 2016 09:29:41 -0700 Subject: [PATCH 03/21] DVCSMP-1738 Philips HUE: Detail page showing the Brightness level twice -Removed secondary control from all Hue DTH --- devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy | 3 --- devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy | 3 --- devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy | 3 --- 3 files changed, 9 deletions(-) diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy index 952b183..0c10c51 100644 --- a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -37,9 +37,6 @@ metadata { tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel", range:"(0..100)" } - tileAttribute ("device.level", key: "SECONDARY_CONTROL") { - attributeState "level", label: 'Level ${currentValue}%' - } tileAttribute ("device.color", key: "COLOR_CONTROL") { attributeState "color", action:"setAdjustedColor" } diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 950ac51..d533006 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -38,9 +38,6 @@ metadata { tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel", range:"(0..100)" } - tileAttribute ("device.level", key: "SECONDARY_CONTROL") { - attributeState "level", label: 'Level ${currentValue}%' - } tileAttribute ("device.color", key: "COLOR_CONTROL") { attributeState "color", action:"setAdjustedColor" } diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index a1d7f33..2944ce3 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -33,9 +33,6 @@ metadata { tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel", range:"(0..100)" } - tileAttribute ("device.level", key: "SECONDARY_CONTROL") { - attributeState "level", label: 'Level ${currentValue}%' - } } controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { From ab2ba8104d0ce6555edcec77a1d9999cad4f16ff Mon Sep 17 00:00:00 2001 From: Matthew Page Date: Sat, 7 May 2016 16:51:30 -0500 Subject: [PATCH 04/21] MSA-1255: The following app submitted will be an endpoint for our IOS app Gidjit. Gidjit is like having a home screen based on your environment. It scans your surroundings looking for home devices, personal electronic devices, times and locations of interest, and more. It will present a list of corresponding launcher panels that allow you to quickly access actions like open an app, control/monitor a device, execute a SmartThings Routine, or link to the control/launch screen of a device. For example based on the users location they will quickly be able to access a preset routine. We are really looking forward to having SmartThings in integration with the app. Link: https://itunes.apple.com/us/app/gidjit-smart-launcher/id961388659?mt=8 The app currently will not have the SmartThing's support until the next version that will be released when this is hopefully approved, as we will also have to go through the Apple approval process allowing them to test the features. Thank you for your time! Matt --- .../gidjit-hub.src/gidjit-hub.groovy | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy diff --git a/smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy b/smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy new file mode 100644 index 0000000..a1f84b2 --- /dev/null +++ b/smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy @@ -0,0 +1,252 @@ +/** + * Gidjit Hub + * + * Copyright 2016 Matthew Page + * + * 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. + * + */ +definition( + name: "Gidjit Hub", + namespace: "com.gidjit.smartthings.hub", + author: "Matthew Page", + description: "Act as an endpoint so user's of Gidjit can quickly access and control their devices and execute routines. Users can do this quickly as Gidjit filters these actions based on their environment", + category: "Convenience", + iconUrl: "http://www.gidjit.com/appicon.png", + iconX2Url: "http://www.gidjit.com/appicon@2x.png", + iconX3Url: "http://www.gidjit.com/appicon@3x.png", + oauth: [displayName: "Gidjit", displayLink: "www.gidjit.com"]) + + + +preferences { + section ("Allow Gidjit to have access, there by allowing you to quickly control and monitor the following devices") { + input "switches", "capability.switch", title: "Control/Monitor your switches", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Control/Monitor your thermostats", multiple: true, required: false + input "windowShades", "capability.windowShade", title: "Control/Monitor your window shades", multiple: true, required: false //windowShade + //input "bulbs", "capability.colorControl", title: "Control your lights", multiple: true, required: false //windowShade + + } +} +mappings { + path("/structureinfo") { + action: [ + GET: "structureInfo" + ] + } + path("/helloactions") { + action: [ + GET: "helloActions" + ] + } + path("/helloactions/:label") { + action: [ + PUT: "executeAction" + ] + } + + path("/switch/:id/:command") { + action: [ + PUT: "updateSwitch" + ] + } + + path("/thermostat/:id/:command") { + action: [ + PUT: "updateThermostat" + ] + } + + path("/windowshade/:id/:command") { + action: [ + PUT: "updateWindowShade" + ] + } + path("/acquiredata/:id") { + action: [ + GET: "acquiredata" + ] + } +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + // subscribe to attributes, devices, locations, etc. +} +def helloActions() { + def actions = location.helloHome?.getPhrases()*.label + if(!actions) { + return [] + } + return actions +} +def executeAction() { + def actions = location.helloHome?.getPhrases()*.label + def a = actions?.find() { it == params.label } + if (!a) { + httpError(400, "invalid label $params.label") + return + } + location.helloHome?.execute(params.label) +} +/* this is the primary function called to query at the structure and its devices */ +def structureInfo() { //list all devices + def list = [:] + def currId = location.id + list[currId] = [:] + list[currId].name = location.name + list[currId].id = location.id + list[currId].temperatureScale = location.temperatureScale + list[currId].devices = [:] + + def setValues = { + if (params.brief) { + return [id: it.id, name: it.displayName] + } + def newList = [id: it.id, name: it.displayName, suppCapab: it.capabilities.collect { + "$it.name" + }, suppAttributes: it.supportedAttributes.collect { + "$it.name" + }, suppCommands: it.supportedCommands.collect { + "$it.name" + }] + + return newList + } + switches?.each { + list[currId].devices[it.id] = setValues(it) + } + thermostats?.each { + list[currId].devices[it.id] = setValues(it) + } + windowShades?.each { + list[currId].devices[it.id] = setValues(it) + } + + return list + +} +/* This function returns all of the current values of the specified Devices attributes */ +def acquiredata() { + def resp = [:] + if (!params.id) { + httpError(400, "invalid id $params.id") + return + } + def dev = switches.find() { it.id == params.id } ?: windowShades.find() { it.id == params.id } ?: + thermostats.find() { it.id == params.id } + + if (!dev) { + httpError(400, "invalid id $params.id") + return + } + def att = dev.supportedAttributes + att.each { + resp[it.name] = dev.currentValue("$it.name") + } + return resp +} + +void updateSwitch() { + // use the built-in request object to get the command parameter + def command = params.command + def sw = switches.find() { it.id == params.id } + if (!sw) { + httpError(400, "invalid id $params.id") + return + } + switch(command) { + case "on": + if ( sw.currentSwitch != "on" ) { + sw.on() + } + break + case "off": + if ( sw.currentSwitch != "off" ) { + sw.off() + } + break + default: + httpError(400, "$command is not a valid") + } +} + + +void updateThermostat() { + // use the built-in request object to get the command parameter + def command = params.command + def therm = thermostats.find() { it.id == params.id } + if (!therm || !command) { + httpError(400, "invalid id $params.id") + return + } + def passComm = [ + "off", + "heat", + "emergencyHeat", + "cool", + "fanOn", + "fanAuto", + "fanCirculate", + "auto" + + ] + def passNumParamComm = [ + "setHeatingSetpoint", + "setCoolingSetpoint", + ] + def passStringParamComm = [ + "setThermostatMode", + "setThermostatFanMode", + ] + if (command in passComm) { + therm."$command"() + } else if (command in passNumParamComm && params.p1 && params.p1.isFloat()) { + therm."$command"(Float.parseFloat(params.p1)) + } else if (command in passStringParamComm && params.p1) { + therm."$command"(params.p1) + } else { + httpError(400, "$command is not a valid command") + } +} + +void updateWindowShade() { + // use the built-in request object to get the command parameter + def command = params.command + def ws = windowShades.find() { it.id == params.id } + if (!ws || !command) { + httpError(400, "invalid id $params.id") + return + } + def passComm = [ + "open", + "close", + "presetPosition", + ] + if (command in passComm) { + ws."$command"() + } else { + httpError(400, "$command is not a valid command") + } +} +// TODO: implement event handlers \ No newline at end of file From bd62962ee1db268727934aa379c57613e6c5545a Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Wed, 11 May 2016 23:06:09 -0700 Subject: [PATCH 05/21] DVCSMP-1716 Remove heartbeat from device types -Remove heartbeat from OSRAM white bulb and Smartpower Outlet -Heartbeat was a hack previously used for Amazon Echo --- .../osram-lightify-led-tunable-white-60w.groovy | 9 --------- .../smartpower-outlet.src/smartpower-outlet.groovy | 7 ------- 2 files changed, 16 deletions(-) diff --git a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy index 8253582..80fa3ba 100644 --- a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy +++ b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy @@ -19,11 +19,6 @@ metadata { capability "Sensor" attribute "colorName", "string" - - // indicates that device keeps track of heartbeat (in state.heartbeat) - attribute "heartbeat", "string" - - } // simulator metadata @@ -75,9 +70,6 @@ metadata { def parse(String description) { //log.trace description - // save heartbeat (i.e. last time we got a message from device) - state.heartbeat = Calendar.getInstance().getTimeInMillis() - if (description?.startsWith("catchall:")) { if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) { @@ -132,7 +124,6 @@ def off() { } def refresh() { - sendEvent(name: "heartbeat", value: "alive", displayed:false) [ "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index 1b47c3e..d28d8cf 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -25,9 +25,6 @@ metadata { capability "Sensor" capability "Health Check" - // indicates that device keeps track of heartbeat (in state.heartbeat) - attribute "heartbeat", "string" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet" @@ -81,9 +78,6 @@ metadata { def parse(String description) { log.debug "description is $description" - // save heartbeat (i.e. last time we got a message from device) - state.heartbeat = Calendar.getInstance().getTimeInMillis() - def finalResult = zigbee.getKnownDescription(description) //TODO: Remove this after getKnownDescription can parse it automatically @@ -124,7 +118,6 @@ def on() { } def refresh() { - sendEvent(name: "heartbeat", value: "alive", displayed:false) zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B") } From a133406b6e3d495befdb868270e843c50325f0de Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Wed, 11 May 2016 23:41:19 -0700 Subject: [PATCH 06/21] DVCSMP-1770 Add MSR to zwave-switch and configure at pairing -Make ZWave switch and Dimmer switch behave the same and save MSR and manufacturer name in data --- .../dimmer-switch.src/dimmer-switch.groovy | 1 + .../smartthings/zwave-switch.src/zwave-switch.groovy | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy index 6b7eca1..11af042 100644 --- a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy +++ b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy @@ -138,6 +138,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS log.debug "productTypeId: ${cmd.productTypeId}" def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) updateDataValue("MSR", msr) + updateDataValue("manufacturer", cmd.manufacturerName) createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) } diff --git a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy index cda2e69..798aa63 100644 --- a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy +++ b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy @@ -95,11 +95,17 @@ def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - if (state.manufacturer != cmd.manufacturerName) { - updateDataValue("manufacturer", cmd.manufacturerName) - } + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + updateDataValue("manufacturer", cmd.manufacturerName) + createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) } + def zwaveEvent(physicalgraph.zwave.Command cmd) { // Handles all Z-Wave commands we aren't interested in [:] From 45b78eff8d24c04e2e6f107e37a2680c83b1c190 Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Mon, 9 May 2016 11:25:18 -0700 Subject: [PATCH 07/21] DVCSMP-1699 Na02>Missing/Failed Oauth Tokens - Netatmo - refactored the oauth component for the app to work in different shards, if supported - fixed some white spacing issues and indents - addressed comments - edited spacing --- .../netatmo-connect.groovy | 450 ++++++++---------- 1 file changed, 208 insertions(+), 242 deletions(-) diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index 905cdde..a127d5b 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -4,29 +4,33 @@ import java.text.DecimalFormat import groovy.json.JsonSlurper -private apiUrl() { "https://api.netatmo.com" } -private getVendorName() { "netatmo" } -private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" } -private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" } +private getApiUrl() { "https://api.netatmo.com" } +private getVendorName() { "netatmo" } +private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" } +private getVendorTokenPath(){ "${apiUrl}/oauth2/token" } private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" } -private getClientId() { appSettings.clientId } -private getClientSecret() { appSettings.clientSecret } -private getServerUrl() { "https://graph.api.smartthings.com" } +private getClientId() { appSettings.clientId } +private getClientSecret() { appSettings.clientSecret } +private getServerUrl() { appSettings.serverUrl } +private getShardUrl() { return getApiServerUrl() } +private getCallbackUrl() { "${serverUrl}/oauth/callback" } +private getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" } // Automatically generated. Make future change here. definition( - name: "Netatmo (Connect)", - namespace: "dianoga", - author: "Brian Steere", - description: "Netatmo Integration", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", - oauth: true, - singleInstance: true + name: "Netatmo (Connect)", + namespace: "dianoga", + author: "Brian Steere", + description: "Netatmo Integration", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", + oauth: true, + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" + appSetting "serverUrl" } preferences { @@ -35,35 +39,52 @@ preferences { } mappings { - path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]} - path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]} - path("/auth"){action: [GET: "auth"]} + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} + path("/oauth/callback") {action: [GET: "callback"]} } def authPage() { log.debug "In authPage" - if(canInstallLabs()) { - def description = null - if (state.vendorAccessToken == null) { - log.debug "About to create access token." + def description + def uninstallAllowed = false + def oauthTokenProvided = false - createAccessToken() - description = "Tap to enter Credentials." + if (!state.accessToken) { + log.debug "About to create access token." + state.accessToken = createAccessToken() + } - return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) { - section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description } + if (canInstallLabs()) { + + def redirectUrl = getBuildRedirectUrl() + log.debug "Redirect url = ${redirectUrl}" + + if (state.authToken) { + description = "Tap 'Next' to proceed" + uninstallAllowed = true + oauthTokenProvided = true + } else { + description = "Click to enter Credentials." + } + + if (!oauthTokenProvided) { + log.debug "Show the login page" + return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) { + section() { + paragraph "Tap below to log in to the netatmo and authorize SmartThings access." + href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description + } } } else { - description = "Tap 'Next' to proceed" - - return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) { - section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } + log.debug "Show the devices page" + return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) { + section() { + input(name:"Devices", style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description) + } } } - } - else - { + } else { def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. 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".""" @@ -78,229 +99,175 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next } } -def auth() { - redirect location: oauthInitUrl() -} - def oauthInitUrl() { log.debug "In oauthInitUrl" - /* OAuth Step 1: Request access code with our client ID */ - state.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [ response_type: "code", - client_id: getClientId(), - state: state.oauthInitState, - redirect_uri: buildRedirectUrl("receiveToken") , - scope: "read_station" - ] - - return getVendorAuthPath() + toQueryString(oauthParams) -} - -def buildRedirectUrl(endPoint) { - log.debug "In buildRedirectUrl" - - return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}" -} - -def receiveToken() { - log.debug "In receiveToken" - def oauthParams = [ - client_secret: getClientSecret(), + response_type: "code", client_id: getClientId(), - grant_type: "authorization_code", - redirect_uri: buildRedirectUrl('receiveToken'), - code: params.code, + client_secret: getClientSecret(), + state: state.oauthInitState, + redirect_uri: getCallbackUrl(), scope: "read_station" - ] - - def tokenUrl = getVendorTokenPath() - def params = [ - uri: tokenUrl, - contentType: 'application/x-www-form-urlencoded', - body: oauthParams, ] - log.debug params + log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}" - /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ - try { - httpPost(params) { response -> - log.debug response.data - def slurper = new JsonSlurper(); + redirect (location: getVendorAuthPath() + toQueryString(oauthParams)) +} - response.data.each {key, value -> - def data = slurper.parseText(key); - log.debug "Data: $data" +def callback() { + log.debug "callback()>> params: $params, params.code ${params.code}" - state.vendorRefreshToken = data.refresh_token - state.vendorAccessToken = data.access_token - state.vendorTokenExpires = now() + (data.expires_in * 1000) - return + def code = params.code + def oauthState = params.state + + if (oauthState == state.oauthInitState) { + + def tokenParams = [ + client_secret: getClientSecret(), + client_id : getClientId(), + grant_type: "authorization_code", + redirect_uri: getCallbackUrl(), + code: code, + scope: "read_station" + ] + + log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}" + + def tokenUrl = getVendorTokenPath() + def params = [ + uri: tokenUrl, + contentType: 'application/x-www-form-urlencoded', + body: tokenParams + ] + + log.debug "PARAMS: ${params}" + + httpPost(params) { resp -> + + def slurper = new JsonSlurper() + + resp.data.each { key, value -> + def data = slurper.parseText(key) + + state.refreshToken = data.refresh_token + state.authToken = data.access_token + state.tokenExpires = now() + (data.expires_in * 1000) + log.debug "swapped token: $resp.data" } - } - } catch (Exception e) { - log.debug "Error: $e" + + // Handle success and failure here, and render stuff accordingly + if (state.authToken) { + success() + } else { + fail() + } + + } else { + log.error "callback() failed oauthState != state.oauthInitState" } +} - log.debug "State: $state" +def success() { + log.debug "in success" + def message = """ +

We have located your """ + getVendorName() + """ account.

+

Tap 'Done' to continue to Devices.

+ """ + connectionStatus(message) +} - if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install - return +def fail() { + log.debug "in fail" + def message = """ +

The connection could not be established!

+

Click 'Done' to return to the menu.

+ """ + connectionStatus(message) +} + +def connectionStatus(message, redirectUrl = null) { + def redirectHtml = "" + if (redirectUrl) { + redirectHtml = """ + + """ } - /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */ - def html = """ - - - - - ${getVendorName()} Connection - - - -
- Vendor icon - connected device icon - SmartThings logo -

We have located your """ + getVendorName() + """ account.

-

Tap 'Done' to process your credentials.

+ + + + + ${getVendorName()} Connection + + + +
+ Vendor icon + connected device icon + SmartThings logo + ${message}
- """ + """ render contentType: 'text/html', data: html } -def receivedToken() { - log.debug "In receivedToken" - - def html = """ - - - - - Withings Connection - - - -
- Vendor icon - connected device icon - SmartThings logo -

Tap 'Done' to continue to Devices.

-
- - - """ - render contentType: 'text/html', data: html -} - -// " - def refreshToken() { log.debug "In refreshToken" @@ -308,8 +275,8 @@ def refreshToken() { client_secret: getClientSecret(), client_id: getClientId(), grant_type: "refresh_token", - refresh_token: state.vendorRefreshToken - ] + refresh_token: state.refreshToken + ] def tokenUrl = getVendorTokenPath() def params = [ @@ -318,7 +285,7 @@ def refreshToken() { body: oauthParams, ] - /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ + // OAuth Step 2: Request access token with our client Secret and OAuth "Code" try { httpPost(params) { response -> def slurper = new JsonSlurper(); @@ -327,9 +294,9 @@ def refreshToken() { def data = slurper.parseText(key); log.debug "Data: $data" - state.vendorRefreshToken = data.refresh_token - state.vendorAccessToken = data.access_token - state.vendorTokenExpires = now() + (data.expires_in * 1000) + state.refreshToken = data.refresh_token + state.accessToken = data.access_token + state.tokenExpires = now() + (data.expires_in * 1000) return true } @@ -338,9 +305,8 @@ def refreshToken() { log.debug "Error: $e" } - log.debug "State: $state" - - if ( !state.vendorAccessToken ) { //We didn't get an access token + // We didn't get an access token + if ( !state.accessToken ) { return false } } @@ -482,13 +448,13 @@ def listDevices() { } def apiGet(String path, Map query, Closure callback) { - if(now() >= state.vendorTokenExpires) { + if(now() >= state.tokenExpires) { refreshToken(); } - query['access_token'] = state.vendorAccessToken + query['access_token'] = state.accessToken def params = [ - uri: apiUrl(), + uri: getApiUrl(), path: path, 'query': query ] From b33d6216961bcce067b7404ab2629b97095195c5 Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Tue, 10 May 2016 11:35:01 -0700 Subject: [PATCH 08/21] DVCSMP-1699 Na02>Missing/Failed Oauth Tokens - now calling the method to get the right shard url for Life360 - removed serverUrl from the app settings --- .../smartthings/life360-connect.src/life360-connect.groovy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/smartapps/smartthings/life360-connect.src/life360-connect.groovy b/smartapps/smartthings/life360-connect.src/life360-connect.groovy index b5d6555..cf07750 100644 --- a/smartapps/smartthings/life360-connect.src/life360-connect.groovy +++ b/smartapps/smartthings/life360-connect.src/life360-connect.groovy @@ -27,7 +27,6 @@ definition( ) { appSetting "clientId" appSetting "clientSecret" - appSetting "serverUrl" } preferences { @@ -192,7 +191,7 @@ def getSmartThingsClientId() { return "pREqugabRetre4EstetherufrePumamExucrEHuc" } -def getServerUrl() { appSettings.serverUrl } +def getServerUrl() { getApiServerUrl() } def buildRedirectUrl() { From 56eef9cf22b50e607056cf8284160579df1ee42f Mon Sep 17 00:00:00 2001 From: Matthew Hartley Date: Thu, 12 May 2016 14:28:40 -0500 Subject: [PATCH 09/21] MSA-1278: The Prempoint SmartApp is a service bridge to allow Prempoint users to import, share, and manage devices that are controlled by their SmartThings hub. --- .../prempoint.src/prempoint.groovy | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 smartapps/prempoint-com/prempoint.src/prempoint.groovy diff --git a/smartapps/prempoint-com/prempoint.src/prempoint.groovy b/smartapps/prempoint-com/prempoint.src/prempoint.groovy new file mode 100644 index 0000000..5bf9584 --- /dev/null +++ b/smartapps/prempoint-com/prempoint.src/prempoint.groovy @@ -0,0 +1,345 @@ +/** + * SmartThings service for Prempoint + * + * Author: Prempoint Inc. (c) 2016 + * + */ +definition( + name: "Prempoint", + namespace: "prempoint.com", + author: "Prempoint Inc.", + description: "SmartThings service for Prempoint", + category: "Connections", + iconUrl: "http://www.prempoint.com/images/social_app_emblem_50x50.png", + iconX2Url: "http://www.prempoint.com/images/social_app_emblem_100x100.png", + iconX3Url: "http://www.prempoint.com/images/social_app_emblem_150x150.png", + oauth: [displayName: "Prempoint", displayLink: "http://www.prempoint.com/"]) + +preferences { + section("Allow Prempoint to Control & Access These Things...") { + input "switches", "capability.switch", title: "Which Switches?", 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 "doors", "capability.doorControl", title: "Which Doors?", multiple: true, required: false + input "cameras", "capability.imageCapture", title: "Which Cameras?", multiple: true, required: false + } +} + +mappings { + path("/list") { + action: [ + GET: "listDevices" + ] + } + path("/switches") { + action: [ + GET: "listSwitches" + ] + } + path("/switches/:id") { + action: [ + GET: "showSwitch" + ] + } + path("/switches/:id/:command") { + action: [ + GET: "updateSwitch" + ] + } + path("/switches/:id/:command/:level") { + action: [ + GET: "updateSwitch" + ] + } + path("/locks") { + action: [ + GET: "listLocks" + ] + } + path("/locks/:id") { + action: [ + GET: "showLock" + ] + } + path("/locks/:id/:command") { + action: [ + GET: "updateLock" + ] + } + path("/doors/:id") { + action: [ + GET: "showDoor" + ] + } + path("/doors/:id/:command") { + action: [ + GET: "updateDoor" + ] + } + path("/garagedoors/:id") { + action: [ + GET: "showGarageDoor" + ] + } + path("/garagedoors/:id/:command") { + action: [ + GET: "updateGarageDoor" + ] + } + path("/cameras/:id") { + action: [ + GET: "showCamera" + ] + } + path("/cameras/:id/:command") { + action: [ + GET: "updateCamera" + ] + } +} + +def installed() {} + +def updated() {} + +def listDevices() { + log.debug "entering listDevices" + //return listSwitches() + listLocks() + listGarageDoors() + listDoors() + listCameras() + return listSwitches() + listLocks() + listGarageDoors() + listCameras() +} + +//switches +def listSwitches() { + log.debug "entering listSwitches" + switches.collect{showDevice(it,"switch")} +} + +def showSwitch() { + log.debug "entering showSwitches" + show(switches, "switch") +} + +def updateSwitch() { + log.debug "entering updateSwitches" + update(switches, "switch") +} + +//locks +def listLocks() { + log.debug "entering listLocks" + locks.collect{showDevice(it,"lock")} +} + +def showLock() { + log.debug "entering showLock" + show(locks, "lock") +} + +def updateLock() { + log.debug "entering updateLock" + update(locks, "lock") +} + +//doors +def listDoors() { + log.debug "entering listDoors" + locks.collect{showDevice(it,"door")} +} + +def showDoor() { + log.debug "entering showDoors" + show(doors, "door") +} + +def updateDoor() { + log.debug "entering updateDoor" + update(doors, "door") +} + +//garagedoors +def listGarageDoors() { + log.debug "entering listGarageDoors" + locks.collect{showDevice(it,"garagedoor")} +} + +def showGarageDoor() { + log.debug "entering showGarageDoors" + show(garagedoors, "garagedoor") +} + +def updateGarageDoor() { + log.debug "entering updateGarageDoor" + update(gargedoors, "garagedoor") +} + +//cameras +def listCameras() { + log.debug "entering listCameras" + cameras.collect{showDevice(it,"image")} +} + +def showCamera() { + log.debug "entering showCameras" + show(cameras, "camera") +} + +def updateCamera() { + log.debug "entering updateCamera" + update(cameras, "camera") +} + +def deviceHandler(evt) {} + +private update(devices, type) { + def rc = null + + //def command = request.JSON?.command + def command = params.command + + log.debug "update, request: params: ${params}, devices: $devices.id type=$type command=$command" + + // Process the command. + if (command) + { + def dev = devices.find { it.id == params.id } + if (!dev) { + httpError(404, "Device not found: $params.id") + } else if (type == "switch") { + switch(command) { + case "on": + rc = dev.on() + break + case "off": + rc = dev.off() + break + default: + httpError(400, "Device command=$command is not a valid for device=$it.id $dev") + } + } else if (type == "lock") { + switch(command) { + case "lock": + rc = dev.lock() + break + case "unlock": + rc = dev.unlock() + break + default: + httpError(400, "Device command=$command is not a valid for device:=$it.id $dev") + } + } else if (type == "door") { + switch(command) { + case "open": + rc = dev.open() + break + case "close": + rc = dev.close() + break + default: + httpError(400, "Device command=$command is not a valid for device=$it.id $dev") + } + } else if (type == "garagedoor") { + switch(command) { + case "open": + rc = dev.open() + break + case "close": + rc = dev.close() + break + default: + httpError(400, "Device command=$command is not a valid for device=$it.id $dev") + } + } else if (type == "camera") { + switch(command) { + case "take": + rc = dev.take() + log.debug "Device command=$command device=$it.id $dev current image=$it.currentImage" + break + default: + httpError(400, "Device command=$command is not a valid for device=$it.id $dev") + } + } + + log.debug "executed device=$it.id $dev command=$command rc=$rc" + + // Check that the device is a switch that is currently on, supports 'setLevel" + // and that a level was specified. + int level = params.level ? params.level as int : -1; + if ((type == "switch") && (dev.currentValue('switch') == "on") && hasLevel(dev) && (level != -1)) { + log.debug "device about to setLevel=$level" + dev.setLevel(level); + } + + // Show the device info if necessary. + if (rc == null) { + rc = showDevice(dev, type) + } + } + + return rc +} + +private show(devices, type) { + def dev = devices.find { it.id == params.id } + if (!dev) { + httpError(404, "Device not found") + } else { + // Show the device info. + showDevice(dev, type) + } +} + +private showDevice(it, type) { + def props = null + + // Get the current state for the device type. + def state = [it.currentState(type)] + + // Check that whether the a switch device with level support is located and update the returned device type. + def devType = type + + if (type == "switch" && hasLevel(it)) { + // Assign "switchWithLevel" to device type. + devType = "switchWithLevel" + // Add the level state. + def levelState = it.currentState("level") + if (levelState) { + state.add(levelState) + } + } + + log.debug "device label=$it.label type=$devType" + + // Assign the device item properties if appropriate. + if (it) { + props = [id: it.id, label: it.label, type: devType, state: state] + // Add the hub information to the device properties + // if appropriate. + if (it.hub) { + props.put("location", it.hub.hub.location) + } + if (it.currentImage) { + props.put("currentImage", it.currentImage) + } + } + + return props +} + +private hasLevel(device) { + // Default return value. + def rc = false; + + // Get the device supported commands. + def supportedCommands = device.supportedCommands + + // Check to see if the "setLevel" was found and assign + // the appropriate return value. + if (supportedCommands) { + // Find the "setLevel" command. + rc = supportedCommands.toString().indexOf("setLevel") != -1 + } + + log.debug "hasLevel device label=$device.label supportedCommands=$supportedCommands rc=$rc" + + return rc +} \ No newline at end of file From f77c5810c6f5182a2dbd0cbfa2d8b709b82738a5 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 13 May 2016 10:26:45 -0500 Subject: [PATCH 10/21] DOCS-284 - update tiles for best practices, add setColor to colorWheel tile --- .../tile-basic-colorwheel.groovy | 9 +++++- .../tile-basic-slider.groovy | 2 +- .../tile-basic-standard.groovy | 14 ++++----- .../tile-basic-value.groovy | 28 ++++++++--------- .../tile-multiattribute-generic.groovy | 22 +++++++------- .../tile-multiattribute-lighting.groovy | 7 ++--- .../tile-multiattribute-mediaplayer.groovy | 6 ++-- .../tile-multiattribute-thermostat.groovy | 30 ++++++++++--------- 8 files changed, 62 insertions(+), 56 deletions(-) diff --git a/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy index 8086c2c..102f9e3 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy @@ -22,7 +22,7 @@ metadata { tiles(scale: 2) { valueTile("currentColor", "device.color") { - state "default", label: '${currentValue}' + state "color", label: '${currentValue}', defaultState: true } controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) { @@ -41,6 +41,13 @@ def parse(String description) { log.debug "Parsing '${description}'" } +def setColor(value) { + log.debug "setting color: $value" + if (value.hex) { sendEvent(name: "color", value: value.hex) } + if (value.hue) { sendEvent(name: "hue", value: value.hue) } + if (value.saturation) { sendEvent(name: "saturation", value: value.saturation) } +} + def setSaturation(percent) { log.debug "Executing 'setSaturation'" sendEvent(name: "saturation", value: percent) diff --git a/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy index 12e30be..923b4d4 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy @@ -39,7 +39,7 @@ metadata { } valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) { - state "default", label:'${currentValue}' + state "range", label:'${currentValue}', defaultState: true } controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") { diff --git a/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy index d2b7e35..1f4f029 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy @@ -41,17 +41,17 @@ metadata { // standard flat tile with only a label standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") { - state "default", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff" + state "label", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff", defaultState: true } // standard flat tile with icon and label standardTile("flatIconLabel", "device.switch", width: 2, height: 2, decoration: "flat") { - state "default", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff" + state "iconLabel", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff", defaultState: true } // standard flat tile with only icon (Refreh text is IN the icon file) standardTile("flatIcon", "device.switch", width: 2, height: 2, decoration: "flat") { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + state "icon", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true } // standard with defaultState = true @@ -74,19 +74,19 @@ metadata { // utility tiles to fill the spaces standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") { - state "default", label:'' + state "emptySmall", label:'', defaultState: true } standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") { - state "default", label:'' + state "emptyBigger", label:'', defaultState: true } // multi-line text (explicit newlines) standardTile("multiLine", "device.multiLine", width: 2, height: 2) { - state "default", label: '${currentValue}' + state "multiLine", label: '${currentValue}', defaultState: true } standardTile("multiLineWithIcon", "device.multiLine", width: 2, height: 2) { - state "default", label: '${currentValue}', icon: "st.switches.switch.off" + state "multiLineIcon", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true } main("actionRings") diff --git a/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy index 8ea7dee..e1efb8a 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy @@ -22,68 +22,68 @@ metadata { tiles(scale: 2) { valueTile("text", "device.text", width: 2, height: 2) { - state "default", label:'${currentValue}' + state "val", label:'${currentValue}', defaultState: true } valueTile("longText", "device.longText", width: 2, height: 2) { - state "default", label:'${currentValue}' + state "val", label:'${currentValue}', defaultState: true } valueTile("integer", "device.integer", width: 2, height: 2) { - state "default", label:'${currentValue}' + state "val", label:'${currentValue}', defaultState: true } valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) { - state "default", label:'${currentValue}' + state "val", label:'${currentValue}', defaultState: true } valueTile("pi", "device.pi", width: 2, height: 2) { - state "default", label:'${currentValue}' + state "val", label:'${currentValue}', defaultState: true } valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) { - state "default", label:'${currentValue}' + state "val", label:'${currentValue}', defaultState: true } valueTile("bgColor", "device.integer", width: 2, height: 2) { - state "default", label:'${currentValue}', backgroundColor: "#e86d13" + state "val", label:'${currentValue}', backgroundColor: "#e86d13", defaultState: true } valueTile("bgColorRange", "device.integer", width: 2, height: 2) { - state "default", label:'${currentValue}', backgroundColors: [ + state "val", label:'${currentValue}', defaultState: true, backgroundColors: [ [value: 10, color: "#ff0000"], [value: 90, color: "#0000ff"] ] } valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) { - state "default", label:'${currentValue}', backgroundColors: [ + state "val", label:'${currentValue}', defaultState: true, backgroundColors: [ [value: 10, color: "#333333"] ] } valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) { - state "default", label:'${currentValue}', backgroundColors: [ + state "valWithConflict", label:'${currentValue}', defaultState: true, backgroundColors: [ [value: 10, color: "#990000"], [value: 10, color: "#000099"] ] } valueTile("noValue", "device.nada", width: 4, height: 2) { - state "default", label:'${currentValue}' + state "noval", label:'${currentValue}', defaultState: true } valueTile("multiLine", "device.multiLine", width: 3, height: 2) { - state "default", label: '${currentValue}' + state "val", label: '${currentValue}', defaultState: true } valueTile("multiLineWithIcon", "device.multiLine", width: 3, height: 2) { - state "default", label: '${currentValue}', icon: "st.switches.switch.off" + state "val", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true } main("text") details([ - "text", "longText", "integer", + "text", "longText", "integer", "integerFloat", "pi", "floatAsText", "bgColor", "bgColorRange", "bgColorRangeSingleItem", "bgColorRangeConflict", "noValue", diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy index 61f09d9..d535647 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy @@ -39,15 +39,15 @@ metadata { attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn" } tileAttribute("device.level", key: "SECONDARY_CONTROL") { - attributeState "default", icon: 'st.Weather.weather1', action:"randomizeLevel" + attributeState "level", icon: 'st.Weather.weather1', action:"randomizeLevel", defaultState: true } tileAttribute("device.level", key: "SLIDER_CONTROL") { - attributeState "default", action:"switch level.setLevel" + attributeState "level", action:"switch level.setLevel", defaultState: true } } multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) { tileAttribute("device.level", key: "PRIMARY_CONTROL") { - attributeState "default", label:'${currentValue}', backgroundColors:[ + attributeState "level", label:'${currentValue}', defaultState: true, backgroundColors:[ [value: 0, color: "#ff0000"], [value: 20, color: "#ffff00"], [value: 40, color: "#00ff00"], @@ -69,34 +69,34 @@ metadata { } multiAttributeTile(name:"lengthyTile", type:"generic", width:6, height:4) { tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") { - attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821" + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true } tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") { - attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821" + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true } } multiAttributeTile(name:"multilineTile", type:"generic", width:6, height:4) { tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") { - attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821" + attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true } tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") { - attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821" + attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true } } multiAttributeTile(name:"lengthyTileWithIcon", type:"generic", width:6, height:4) { tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") { - attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on" + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true } tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") { - attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on" + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true } } multiAttributeTile(name:"multilineTileWithIcon", type:"generic", width:6, height:4) { tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") { - attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on" + attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true } tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") { - attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on" + attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true } } diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy index 6ebbb3e..7ffaabc 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy @@ -96,10 +96,10 @@ metadata { } standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + state "reset", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single", defaultState: true } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + state "refresh", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true } main(["switch"]) @@ -173,7 +173,6 @@ def setColor(value) { def reset() { log.debug "Executing 'reset'" setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23]) - //parent.poll() } def setAdjustedColor(value) { @@ -189,7 +188,6 @@ def setAdjustedColor(value) { def refresh() { log.debug "Executing 'refresh'" - //parent.manualRefresh() } def adjustOutgoingHue(percent) { @@ -208,4 +206,3 @@ def adjustOutgoingHue(percent) { log.info "percent: $percent, adjusted: $adjusted" adjusted } - diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy index b22c3f7..2ad41f4 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy @@ -37,10 +37,10 @@ metadata { attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing") } tileAttribute("device.status", key: "PREVIOUS_TRACK") { - attributeState("default", action:"music Player.previousTrack") + attributeState("status", action:"music Player.previousTrack", defaultState: true) } tileAttribute("device.status", key: "NEXT_TRACK") { - attributeState("default", action:"music Player.nextTrack") + attributeState("status", action:"music Player.nextTrack", defaultState: true) } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState("level", action:"music Player.setLevel") @@ -50,7 +50,7 @@ metadata { attributeState("muted", action:"music Player.unmute", nextState: "unmuted") } tileAttribute("device.trackDescription", key: "MARQUEE") { - attributeState("default", label:"${currentValue}") + attributeState("trackDescription", label:"${currentValue}", defaultState: true) } } diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy index 6f8d979..6fb57bd 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy @@ -32,14 +32,14 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) { tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { - attributeState("default", label:'${currentValue}', unit:"dF") + attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true) } tileAttribute("device.temperature", key: "VALUE_CONTROL") { attributeState("VALUE_UP", action: "tempUp") attributeState("VALUE_DOWN", action: "tempDown") } tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { - attributeState("default", label:'${currentValue}%', unit:"%") + attributeState("humidity", label:'${currentValue}%', unit:"%", defaultState: true) } tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { attributeState("idle", backgroundColor:"#44b621") @@ -53,15 +53,16 @@ metadata { attributeState("auto", label:'${name}') } tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { - attributeState("default", label:'${currentValue}', unit:"dF") + attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) } tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { - attributeState("default", label:'${currentValue}', unit:"dF") + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) } } multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) { tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { - attributeState("default", label:'${currentValue}', unit:"dF") + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + attributeState("temp", label:'${currentValue}', unit:"dF") } tileAttribute("device.temperature", key: "VALUE_CONTROL") { attributeState("VALUE_UP", action: "tempUp") @@ -79,15 +80,16 @@ metadata { attributeState("auto", label:'${name}') } tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { - attributeState("default", label:'${currentValue}', unit:"dF") + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF") } tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { - attributeState("default", label:'${currentValue}', unit:"dF") + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) } } multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) { tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { - attributeState("default", label:'${currentValue}', unit:"dF", + attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true, backgroundColors:[ [value: 31, color: "#153591"], [value: 44, color: "#1e9cbb"], @@ -118,30 +120,30 @@ metadata { ) } standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"tempDown" + state "tempDown", label:'down', action:"tempDown", defaultState: true } standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"tempUp" + state "tempUp", label:'up', action:"tempUp", defaultState: true } valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff" } standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"heatDown" + state "heatDown", label:'down', action:"heatDown", defaultState: true } standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"heatUp" + state "heatUp", label:'up', action:"heatUp", defaultState: true } valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff" } standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"coolDown" + state "coolDown", label:'down', action:"coolDown", defaultState: true } standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"coolUp" + state "coolUp", label:'up', action:"coolUp", defaultState: true } standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { From 8de3276ce6f91f1d00b8e129e036c996a4532066 Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Fri, 13 May 2016 11:13:13 -0700 Subject: [PATCH 11/21] DVCSMP-1709 Android>SmartApps>PlatLinkConnector>Unable To Add - now getting the right shard url --- .../plantlink-connector.src/plantlink-connector.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy b/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy index 12cad96..6a1423d 100644 --- a/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy +++ b/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy @@ -370,9 +370,7 @@ def parse_api_response(resp, message) { } } -def getServerUrl() { - return "https://graph.api.smartthings.com" -} +def getServerUrl() { return getApiServerUrl() } def debugEvent(message, displayEvent) { def results = [ From 26a0f6f9398c86f18d242647e2e3bc5a7157a8cf Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 13 May 2016 21:10:54 -0500 Subject: [PATCH 12/21] DOCS-284 adding README for example/test tiles --- devicetypes/smartthings/tile-ux/README.md | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 devicetypes/smartthings/tile-ux/README.md diff --git a/devicetypes/smartthings/tile-ux/README.md b/devicetypes/smartthings/tile-ux/README.md new file mode 100644 index 0000000..ac1b48b --- /dev/null +++ b/devicetypes/smartthings/tile-ux/README.md @@ -0,0 +1,42 @@ +# Device Tiles Examples and Reference + +This package contains examples of Device tiles, organized by tile type. + +## Purpose + +Each Device Handler shows example usages of a specific tile, and is meant to represent the variety of permutations that a tile can be configured. + +The various tiles can be used by QA to test tiles on all supported mobile devices, and by developers as a reference implementation. + +## Installation + +1. Self-publish the Device Handlers in this package. +2. Self-publish the Device Tile Controller SmartApp. The SmartApp can be found [here](https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy). +3. Install the SmartApp from the Marketplace, under "My Apps". +4. Select the simulated devices you want to install and press "Done". + +The simulated devices can then be found in the "Things" view of "My Home" in the mobile app. +You may wish to create a new room for these simulated devices for easy access. + +## Usage + +Each simulated device can be interacted with like other devices. +You can use the mobile app to interact with the tiles to see how they look and behave. + +## Troubleshooting + +If you get an error when installing the simulated devices using the controller SmartApp, ensure that you have published all the Device Handlers for yourself. +Also check live logging to see if there is a specific tile that is causing installation issues. + +## FAQ + +*Question: A tile isn't behaving as expected. What should I do?* + +QA should create a JIRA ticket for any issues or inconsistencies of tiles across devices. + +Developers may file a support ticket, and reference the specific tile and issue observed. + +*Question: I'd like to contribute an example tile usage that would be helpful for testing and reference purposes. Can I do that?* + +We recommend that you open an issue in the SmartThingsPublic repository describing the example tile and usage. +That way we can discuss with you the proposed change, and then if appropriate you can create a PR associated to the issue. From 293a73136e671863db4dce4a37b6649a61f47718 Mon Sep 17 00:00:00 2001 From: jackchi Date: Mon, 16 May 2016 11:43:21 -0700 Subject: [PATCH 13/21] [CHF-74] Add category to SmartSense DTH --- .../smartthings/smartpower-outlet.src/smartpower-outlet.groovy | 2 +- .../smartsense-moisture-sensor.groovy | 2 +- .../smartsense-motion-sensor.groovy | 2 +- .../smartsense-multi-sensor.src/smartsense-multi-sensor.groovy | 2 +- .../smartsense-open-closed-accelerometer-sensor.groovy | 2 +- .../smartsense-temp-humidity-sensor.groovy | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index d28d8cf..11febf1 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -16,7 +16,7 @@ metadata { // Automatically generated. Make future change here. - definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", category: "C1") { capability "Actuator" capability "Switch" capability "Power Meter" diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy index de0526b..8248bf0 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -15,7 +15,7 @@ */ metadata { - definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") { capability "Configuration" capability "Battery" capability "Refresh" diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy index 29d9dd0..660b0b7 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -15,7 +15,7 @@ */ metadata { - definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") { capability "Motion Sensor" capability "Configuration" capability "Battery" diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 892e5ad..1e30759 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -15,7 +15,7 @@ */ metadata { - definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") { capability "Three Axis" capability "Battery" diff --git a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy index eb2bfdd..9f03f17 100644 --- a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy @@ -16,7 +16,7 @@ //DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH metadata { - definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") { capability "Battery" capability "Configuration" capability "Contact Sensor" diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index a938cb9..0479b5d 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") { capability "Configuration" capability "Battery" capability "Refresh" From 85a335d365a91c514a0dc4dfcdc266f666828aa4 Mon Sep 17 00:00:00 2001 From: chelseaokey Date: Wed, 18 May 2016 16:46:17 -0500 Subject: [PATCH 14/21] INTL-527 Temperature Notifications Should Respect Location Temp Scale --- smartapps/smartthings/its-too-cold.src/its-too-cold.groovy | 3 ++- smartapps/smartthings/its-too-hot.src/its-too-hot.groovy | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy b/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy index 0e66cc3..8364c20 100644 --- a/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy +++ b/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy @@ -73,7 +73,8 @@ def temperatureHandler(evt) { // TODO: Send "Temperature back to normal" SMS, turn switch off } else { log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch" - send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:"F"}") + def tempScale = location.temperatureScale ?: "F" + send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:tempScale}") switch1?.on() } } diff --git a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy index e9c8c0f..5b8e10d 100644 --- a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy +++ b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy @@ -73,7 +73,8 @@ def temperatureHandler(evt) { // TODO: Send "Temperature back to normal" SMS, turn switch off } else { log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch" - send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:"F"}") + def tempScale = location.temperatureScale ?: "F" + send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}") switch1?.on() } } From b105d9d80ed9194d56c355b038ee72e4128cefe5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 19 May 2016 13:58:44 -0400 Subject: [PATCH 15/21] DVCSMP-1775 - Call poll() every 5 minutes to reduce duplicated calls (#902) * DVCSMP-1775 - Call poll() every 5 minutes to reduce duplicated calls Call poll() every 5 minutes instead of discovery() to reduce duplicated getActivityList() call. * Better error logging --- .../logitech-harmony-connect.groovy | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 5cdfdff..83b6cc6 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -339,7 +339,7 @@ def initialize() { state.aux = 0 if (selectedhubs || selectedactivities) { addDevice() - runEvery5Minutes("discovery") + runEvery5Minutes("poll") } } @@ -394,9 +394,9 @@ def discovery() { } } catch (java.net.SocketTimeoutException e) { log.warn "Connection to the hub timed out. Please restart the hub and try again." - state.resethub = true + state.resethub = true } catch (e) { - log.warn "Hostname in certificate didn't match. Please try again later." + log.info "Logitech Harmony - Error: $e" } return null } @@ -474,7 +474,7 @@ def activity(dni,mode) { def poll() { // GET THE LIST OF ACTIVITIES if (state.HarmonyAccessToken) { - getActivityList() + getActivityList() def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}" try { @@ -520,14 +520,17 @@ def poll() { return "Poll completed $map - $state.hubs" } } catch (groovyx.net.http.HttpResponseException e) { - if (e.statusCode == 401) { // token is expired - state.remove("HarmonyAccessToken") - return "Harmony Access token has expired" - } - } catch(Exception e) { - log.trace e - } - } + if (e.statusCode == 401) { // token is expired + state.remove("HarmonyAccessToken") + log.warn "Harmony Access token has expired" + } + } catch (java.net.SocketTimeoutException e) { + log.warn "Connection to the hub timed out. Please restart the hub and try again." + state.resethub = true + } catch (e) { + log.info "Logitech Harmony - Error: $e" + } + } } From a21f9f177c3722e4495597779e6ebc4e40d32c33 Mon Sep 17 00:00:00 2001 From: Will Price Date: Fri, 20 May 2016 14:22:19 -0500 Subject: [PATCH 16/21] MSA-1285: Updated submission, already reviewed, just cleaning up the pull request. --- .../simple-control.src/simple-control.groovy | 150 +++++++++++++++--- 1 file changed, 124 insertions(+), 26 deletions(-) diff --git a/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy index c2b3fdd..3b4bfaa 100644 --- a/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy +++ b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy @@ -21,6 +21,12 @@ preferences() section("Allow Simple Control to Monitor and Control These Things...") { input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false + input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false + input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false + input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false + input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false } page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5) @@ -31,12 +37,17 @@ preferences() mappings { path("/devices") { + action: [ + GET: "getDevices" + ] + } + path("/:deviceType/devices") { action: [ GET: "getDevices", POST: "handleDevicesWithIDs" ] - } - path("/device/:id") { + } + path("/device/:deviceType/:id") { action: [ GET: "getDevice", POST: "updateDevice" @@ -93,33 +104,40 @@ def handleDevicesWithIDs() //log.debug("ids: ${ids}") def command = data?.command def arguments = data?.arguments + def type = params?.deviceType + //log.debug("device type: ${type}") if (command) { - def success = false + def statusCode = 404 //log.debug("command ${command}, arguments ${arguments}") for (devId in ids) { def device = allDevices.find { it.id == devId } - if (device) { - if (arguments) { + //log.debug("device: ${device}") + // Check if we have a device that responds to the specified command + if (validateCommand(device, type, command)) { + if (arguments) { device."$command"(*arguments) - } else { - device."$command"() - } - success = true + } + else { + device."$command"() + } + statusCode = 200 } else { - //log.debug("device not found ${devId}") + statusCode = 403 } } - - if (success) + def responseData = "{}" + switch (statusCode) { - render status: 200, data: "{}" - } - else - { - render status: 404, data: '{"msg": "Device not found"}' + case 403: + responseData = '{"msg": "Access denied. This command is not supported by current capability."}' + break + case 404: + responseData = '{"msg": "Device not found"}' + break } + render status: statusCode, data: responseData } else { @@ -164,25 +182,101 @@ def updateDevice() def data = request.JSON def command = data?.command def arguments = data?.arguments + def type = params?.deviceType + //log.debug("device type: ${type}") //log.debug("updateDevice, params: ${params}, request: ${data}") if (!command) { render status: 400, data: '{"msg": "command is required"}' } else { + def statusCode = 404 def device = allDevices.find { it.id == params.id } if (device) { - if (arguments) { - device."$command"(*arguments) + // Check if we have a device that responds to the specified command + if (validateCommand(device, type, command)) { + if (arguments) { + device."$command"(*arguments) + } + else { + device."$command"() + } + statusCode = 200 } else { - device."$command"() + statusCode = 403 } - render status: 204, data: "{}" - } else { - render status: 404, data: '{"msg": "Device not found"}' } + + def responseData = "{}" + switch (statusCode) + { + case 403: + responseData = '{"msg": "Access denied. This command is not supported by current capability."}' + break + case 404: + responseData = '{"msg": "Device not found"}' + break + } + render status: statusCode, data: responseData } } +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, deviceType, command) { + //log.debug("validateCommand ${command}") + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + //log.debug("capabilityCommands: ${capabilityCommands}") + def currentDeviceCapability = getCapabilityName(deviceType) + //log.debug("currentDeviceCapability: ${currentDeviceCapability}") + if (capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") + } +} + +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(type) { + switch(type) { + case "switches": + return "Switch" + case "locks": + return "Lock" + case "thermostats": + return "Thermostat" + case "doorControls": + return "Door Control" + case "colorControls": + return "Color Control" + case "musicPlayers": + return "Music Player" + case "switchLevels": + return "Switch Level" + default: + return type + } +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + def listSubscriptions() { //log.debug "listSubscriptions()" @@ -361,7 +455,13 @@ def agentDiscovery(params=[:]) } section("Allow Simple Control to Monitor and Control These Things...") { - input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false + input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false + input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false + input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false + input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false } } } @@ -672,5 +772,3 @@ def List getRealHubFirmwareVersions() } - - From 0c1208928fc95103dfc2144741616869f2408600 Mon Sep 17 00:00:00 2001 From: jackchi Date: Tue, 24 May 2016 15:17:40 -0700 Subject: [PATCH 17/21] [CHF-74] Add Smartsense device-health related documentation for motion sensor --- .../smartsense-motion-sensor.src/.st-ignore | 2 ++ .../smartsense-motion-sensor.src/README.md | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore create mode 100644 devicetypes/smartthings/smartsense-motion-sensor.src/README.md diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/README.md b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md new file mode 100644 index 0000000..82bff3f --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md @@ -0,0 +1,30 @@ +# Smartsense Motion Sensor + + + +Works with: + +* [Samsung SmartThings Motion Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-motion-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health]($health) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Motion Sensor** - can detect motion +* **Battery** - defines device uses a battery +* **Refresh** - _refresh()_ command for status updates +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +A Category C2 motion sensor that has 120min check-in interval + + + + + + From ecfb99974b4d1d71d7501772d300dd889eb54077 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Thu, 26 May 2016 16:52:45 -0700 Subject: [PATCH 18/21] DVCSMP-1795 Philips Hue: Add support for Hue Ambience bulb -Add support for Hue White Ambiance Bulb (white,dimmable+color temp) --- .../hue-white-ambiance-bulb.groovy | 110 ++++++++++++++++++ .../hue-connect.src/hue-connect.groovy | 2 + 2 files changed, 112 insertions(+) create mode 100644 devicetypes/smartthings/hue-white-ambiance-bulb/hue-white-ambiance-bulb.groovy diff --git a/devicetypes/smartthings/hue-white-ambiance-bulb/hue-white-ambiance-bulb.groovy b/devicetypes/smartthings/hue-white-ambiance-bulb/hue-white-ambiance-bulb.groovy new file mode 100644 index 0000000..c9bedb7 --- /dev/null +++ b/devicetypes/smartthings/hue-white-ambiance-bulb/hue-white-ambiance-bulb.groovy @@ -0,0 +1,110 @@ +/** + * Hue White Ambiance Bulb + * + * Philips Hue Type "Color Temperature Light" + * + * Author: SmartThings + */ + +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue White Ambiance Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + + command "refresh" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles (scale: 2){ + multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel", range:"(0..100)" + } + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorTemperature", label: '${currentValue} K' + } + + standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["rich-control"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"]) + } +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + def map = description + if (description instanceof String) { + log.debug "Hue Ambience Bulb stringToMap - ${map}" + map = stringToMap(description) + } + + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +void on() { + log.trace parent.on(this) + sendEvent(name: "switch", value: "on") +} + +void off() { + log.trace parent.off(this) + sendEvent(name: "switch", value: "off") +} + +void setLevel(percent) { + log.debug "Executing 'setLevel'" + if (percent != null && percent >= 0 && percent <= 100) { + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent) + sendEvent(name: "switch", value: "on") + } else { + log.warn "$percent is not 0-100" + } +} + +void setColorTemperature(value) { + if (value) { + log.trace "setColorTemperature: ${value}k" + parent.setColorTemperature(this, value) + sendEvent(name: "colorTemperature", value: value) + sendEvent(name: "switch", value: "on") + } else { + log.warn "Invalid color temperature" + } +} + +void refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} + diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 95faa6f..0ebf042 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -323,6 +323,8 @@ private getDeviceType(hueType) { return "Hue Bulb" else if (hueType?.equalsIgnoreCase("Color Light")) return "Hue Bloom" + else if (hueType?.equalsIgnoreCase("Color Temperature Light")) + return "Hue White Ambiance Bulb" else return null } From 417c246d615a4c601c8376256c63f7d0f5dfe6ca Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Wed, 1 Jun 2016 12:09:38 -0700 Subject: [PATCH 19/21] Revert "DVCSMP-1107 Stage 2 of adding preferences to support garage sensor in v1 safari mutli sensor" --- .../smartsense-multi.groovy | 85 +++---------------- 1 file changed, 10 insertions(+), 75 deletions(-) diff --git a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy index 8ed1a3d..17f5ddd 100644 --- a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy +++ b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy @@ -45,28 +45,17 @@ metadata { } preferences { - section { - input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false - } - section { - input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) - } + input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false } tiles(scale: 2) { - multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ - tileAttribute ("device.status", key: "PRIMARY_CONTROL") { - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" - attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" - attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" } } - standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") - } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") @@ -89,8 +78,8 @@ metadata { state "battery", label:'${currentValue}% battery', unit:"" } - main(["status", "acceleration", "temperature"]) - details(["status", "acceleration", "temperature", "battery"]) + main(["contact", "acceleration", "temperature"]) + details(["contact", "acceleration", "temperature", "battery"]) } } @@ -98,9 +87,7 @@ def parse(String description) { def results if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { - if (garageSensor != "Yes") { - results = parseSingleMessage(description) - } + results = parseSingleMessage(description) } else if (description == 'updated') { //TODO is there a better way to handle this like the other device types? @@ -216,9 +203,7 @@ private List parseContactMessage(String description) { parts.each { part -> part = part.trim() if (part.startsWith('contactState:')) { - if (garageSensor != "Yes") { - results.addAll(getContactResult(part, description)) - } + results.addAll(getContactResult(part, description)) } else if (part.startsWith('accelerationState:')) { results << getAccelerationResult(part, description) @@ -240,29 +225,6 @@ private List parseContactMessage(String description) { results } -def updated() { - log.debug "updated called" - log.info "garage value : $garageSensor" - if (garageSensor == "Yes") { - def descriptionText = "Updating device to garage sensor" - if (device.latestValue("status") == "open") { - sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText) - } - else if (device.latestValue("status") == "closed") { - sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText) - } - } - else { - def descriptionText = "Updating device to open/close sensor" - if (device.latestValue("status") == "garage-open") { - sendEvent(name: 'status', value: 'open', descriptionText: descriptionText) - } - else if (device.latestValue("status") == "garage-closed") { - sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText) - } - } -} - private List parseOrientationMessage(String description) { def results = [] def xyzResults = [x: 0, y: 0, z: 0] @@ -292,34 +254,7 @@ private List parseOrientationMessage(String description) { } } - def xyz = getXyzResult(xyzResults, description) - results << xyz - - if (garageSensor == "Yes") { - // Looks for Z-axis orientation as virtual contact state - def a = xyz.value.split(',').collect{it.toInteger()} - def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1])) - def absValueZ = Math.abs(a[2]) - log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ" - - def contactValue = null - def garageValue = null - - if (absValueZ > 825 && absValueXY < 175) { - contactValue = 'open' - garageValue = 'garage-open' - log.debug "STATUS: open" - } - else if (absValueZ < 75 && absValueXY > 825) { - contactValue = 'closed' - garageValue = 'garage-closed' - log.debug "STATUS: closed" - } - def linkText = getLinkText(device) - def descriptionText = "${linkText} was ${contactValue == 'open' ? 'opened' : 'closed'}" - results << createEvent(name: "contact", value: contactValue, descriptionText: descriptionText, unit: "", displayed: false) - results << createEvent(name: "status", value: garageValue, descriptionText: descriptionText, unit: "") - } + results << getXyzResult(xyzResults, description) results } From 34107f935e0d3e94b137867c4d22e111402c9a2d Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Fri, 13 May 2016 14:49:37 -0700 Subject: [PATCH 20/21] PENG-158 UBI should not allow undefined commands - now validating commands per capability of the device in the smartapp removed commented out code --- smartapps/smartthings/ubi.src/ubi.groovy | 94 +++++++++++++++++++----- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/smartapps/smartthings/ubi.src/ubi.groovy b/smartapps/smartthings/ubi.src/ubi.groovy index a00f5ab..6e8d357 100644 --- a/smartapps/smartthings/ubi.src/ubi.groovy +++ b/smartapps/smartthings/ubi.src/ubi.groovy @@ -107,8 +107,8 @@ mappings { path("/locks") { action: [ GET: "listLocks", - PUT: "updateLock", - POST: "updateLock" + PUT: "updateLocks", + POST: "updateLocks" ] } path("/locks/:id") { @@ -442,31 +442,87 @@ def executePhrase() { } private void updateAll(devices) { + def type = params.param1 def command = request.JSON?.command - if (command) - { - command = command.toLowerCase() - devices."$command"() + if (!devices) { + httpError(404, "Devices not found") + } + if (command){ + devices.each { device -> + executeCommand(device, type, command) + } } } private void update(devices) { log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" - //def command = request.JSON?.command - def command = params.command - if (command) - { - command = command.toLowerCase() - def device = devices.find { it.id == params.id } - if (!device) - { + def type = params.param1 + def command = request.JSON?.command + def device = devices?.find { it.id == params.id } + + if (!device) { httpError(404, "Device not found") - } - else - { - device."$command"() - } } + + if (command) { + executeCommand(device, type, command) + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, deviceType, command) { + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + def currentDeviceCapability = getCapabilityName(deviceType) + if (capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") + } +} + +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(type) { + switch(type) { + case "switches": + return "Switch" + case "locks": + return "Lock" + default: + return type + } +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + +/** + * Validates and executes the command + * on the device or devices + */ +def executeCommand(device, type, command) { + if (validateCommand(device, type, command)) { + device."$command"() + } else { + httpError(403, "Access denied. This command is not supported by current capability.") + } } private show(devices, type) { From ac7f1a0c667deb05d3b581c7ff287607e371961f Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Thu, 2 Jun 2016 14:24:29 -0700 Subject: [PATCH 21/21] removed Prempoint from staging --- .../prempoint.src/prempoint.groovy | 345 ------------------ 1 file changed, 345 deletions(-) delete mode 100644 smartapps/prempoint-com/prempoint.src/prempoint.groovy diff --git a/smartapps/prempoint-com/prempoint.src/prempoint.groovy b/smartapps/prempoint-com/prempoint.src/prempoint.groovy deleted file mode 100644 index 5bf9584..0000000 --- a/smartapps/prempoint-com/prempoint.src/prempoint.groovy +++ /dev/null @@ -1,345 +0,0 @@ -/** - * SmartThings service for Prempoint - * - * Author: Prempoint Inc. (c) 2016 - * - */ -definition( - name: "Prempoint", - namespace: "prempoint.com", - author: "Prempoint Inc.", - description: "SmartThings service for Prempoint", - category: "Connections", - iconUrl: "http://www.prempoint.com/images/social_app_emblem_50x50.png", - iconX2Url: "http://www.prempoint.com/images/social_app_emblem_100x100.png", - iconX3Url: "http://www.prempoint.com/images/social_app_emblem_150x150.png", - oauth: [displayName: "Prempoint", displayLink: "http://www.prempoint.com/"]) - -preferences { - section("Allow Prempoint to Control & Access These Things...") { - input "switches", "capability.switch", title: "Which Switches?", 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 "doors", "capability.doorControl", title: "Which Doors?", multiple: true, required: false - input "cameras", "capability.imageCapture", title: "Which Cameras?", multiple: true, required: false - } -} - -mappings { - path("/list") { - action: [ - GET: "listDevices" - ] - } - path("/switches") { - action: [ - GET: "listSwitches" - ] - } - path("/switches/:id") { - action: [ - GET: "showSwitch" - ] - } - path("/switches/:id/:command") { - action: [ - GET: "updateSwitch" - ] - } - path("/switches/:id/:command/:level") { - action: [ - GET: "updateSwitch" - ] - } - path("/locks") { - action: [ - GET: "listLocks" - ] - } - path("/locks/:id") { - action: [ - GET: "showLock" - ] - } - path("/locks/:id/:command") { - action: [ - GET: "updateLock" - ] - } - path("/doors/:id") { - action: [ - GET: "showDoor" - ] - } - path("/doors/:id/:command") { - action: [ - GET: "updateDoor" - ] - } - path("/garagedoors/:id") { - action: [ - GET: "showGarageDoor" - ] - } - path("/garagedoors/:id/:command") { - action: [ - GET: "updateGarageDoor" - ] - } - path("/cameras/:id") { - action: [ - GET: "showCamera" - ] - } - path("/cameras/:id/:command") { - action: [ - GET: "updateCamera" - ] - } -} - -def installed() {} - -def updated() {} - -def listDevices() { - log.debug "entering listDevices" - //return listSwitches() + listLocks() + listGarageDoors() + listDoors() + listCameras() - return listSwitches() + listLocks() + listGarageDoors() + listCameras() -} - -//switches -def listSwitches() { - log.debug "entering listSwitches" - switches.collect{showDevice(it,"switch")} -} - -def showSwitch() { - log.debug "entering showSwitches" - show(switches, "switch") -} - -def updateSwitch() { - log.debug "entering updateSwitches" - update(switches, "switch") -} - -//locks -def listLocks() { - log.debug "entering listLocks" - locks.collect{showDevice(it,"lock")} -} - -def showLock() { - log.debug "entering showLock" - show(locks, "lock") -} - -def updateLock() { - log.debug "entering updateLock" - update(locks, "lock") -} - -//doors -def listDoors() { - log.debug "entering listDoors" - locks.collect{showDevice(it,"door")} -} - -def showDoor() { - log.debug "entering showDoors" - show(doors, "door") -} - -def updateDoor() { - log.debug "entering updateDoor" - update(doors, "door") -} - -//garagedoors -def listGarageDoors() { - log.debug "entering listGarageDoors" - locks.collect{showDevice(it,"garagedoor")} -} - -def showGarageDoor() { - log.debug "entering showGarageDoors" - show(garagedoors, "garagedoor") -} - -def updateGarageDoor() { - log.debug "entering updateGarageDoor" - update(gargedoors, "garagedoor") -} - -//cameras -def listCameras() { - log.debug "entering listCameras" - cameras.collect{showDevice(it,"image")} -} - -def showCamera() { - log.debug "entering showCameras" - show(cameras, "camera") -} - -def updateCamera() { - log.debug "entering updateCamera" - update(cameras, "camera") -} - -def deviceHandler(evt) {} - -private update(devices, type) { - def rc = null - - //def command = request.JSON?.command - def command = params.command - - log.debug "update, request: params: ${params}, devices: $devices.id type=$type command=$command" - - // Process the command. - if (command) - { - def dev = devices.find { it.id == params.id } - if (!dev) { - httpError(404, "Device not found: $params.id") - } else if (type == "switch") { - switch(command) { - case "on": - rc = dev.on() - break - case "off": - rc = dev.off() - break - default: - httpError(400, "Device command=$command is not a valid for device=$it.id $dev") - } - } else if (type == "lock") { - switch(command) { - case "lock": - rc = dev.lock() - break - case "unlock": - rc = dev.unlock() - break - default: - httpError(400, "Device command=$command is not a valid for device:=$it.id $dev") - } - } else if (type == "door") { - switch(command) { - case "open": - rc = dev.open() - break - case "close": - rc = dev.close() - break - default: - httpError(400, "Device command=$command is not a valid for device=$it.id $dev") - } - } else if (type == "garagedoor") { - switch(command) { - case "open": - rc = dev.open() - break - case "close": - rc = dev.close() - break - default: - httpError(400, "Device command=$command is not a valid for device=$it.id $dev") - } - } else if (type == "camera") { - switch(command) { - case "take": - rc = dev.take() - log.debug "Device command=$command device=$it.id $dev current image=$it.currentImage" - break - default: - httpError(400, "Device command=$command is not a valid for device=$it.id $dev") - } - } - - log.debug "executed device=$it.id $dev command=$command rc=$rc" - - // Check that the device is a switch that is currently on, supports 'setLevel" - // and that a level was specified. - int level = params.level ? params.level as int : -1; - if ((type == "switch") && (dev.currentValue('switch') == "on") && hasLevel(dev) && (level != -1)) { - log.debug "device about to setLevel=$level" - dev.setLevel(level); - } - - // Show the device info if necessary. - if (rc == null) { - rc = showDevice(dev, type) - } - } - - return rc -} - -private show(devices, type) { - def dev = devices.find { it.id == params.id } - if (!dev) { - httpError(404, "Device not found") - } else { - // Show the device info. - showDevice(dev, type) - } -} - -private showDevice(it, type) { - def props = null - - // Get the current state for the device type. - def state = [it.currentState(type)] - - // Check that whether the a switch device with level support is located and update the returned device type. - def devType = type - - if (type == "switch" && hasLevel(it)) { - // Assign "switchWithLevel" to device type. - devType = "switchWithLevel" - // Add the level state. - def levelState = it.currentState("level") - if (levelState) { - state.add(levelState) - } - } - - log.debug "device label=$it.label type=$devType" - - // Assign the device item properties if appropriate. - if (it) { - props = [id: it.id, label: it.label, type: devType, state: state] - // Add the hub information to the device properties - // if appropriate. - if (it.hub) { - props.put("location", it.hub.hub.location) - } - if (it.currentImage) { - props.put("currentImage", it.currentImage) - } - } - - return props -} - -private hasLevel(device) { - // Default return value. - def rc = false; - - // Get the device supported commands. - def supportedCommands = device.supportedCommands - - // Check to see if the "setLevel" was found and assign - // the appropriate return value. - if (supportedCommands) { - // Find the "setLevel" command. - rc = supportedCommands.toString().indexOf("setLevel") != -1 - } - - log.debug "hasLevel device label=$device.label supportedCommands=$supportedCommands rc=$rc" - - return rc -} \ No newline at end of file