Compare commits

...

10 Commits

Author SHA1 Message Date
Aneesha Pillai
ee3aa29a4d MSA-1287: for me 2016-05-22 10:22:52 -05:00
rohandesai
2894d52efa Merge pull request #909 from SmartThingsCommunity/MSA-1285-3
MSA-1285: May 2016 Submission
2016-05-20 13:39:33 -07:00
Will Price
a21f9f177c MSA-1285: Updated submission, already reviewed, just cleaning up the pull request. 2016-05-20 14:22:19 -05:00
rohandesai
02f968b8cb Merge pull request #889 from SmartThingsCommunity/MSA-1278-2
MSA-1278: Updates to dynamic object execution for security compliance
2016-05-20 10:30:48 -07:00
Juan Pablo Risso
b105d9d80e 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
2016-05-19 13:58:44 -04:00
Chelsea Okey
9bfad5d6a4 Merge pull request #908 from ch3lsea/INTL-527
INTL-527 Temperature Notifications Should Respect Location Temp Scale
2016-05-19 10:41:01 -05:00
chelseaokey
85a335d365 INTL-527 Temperature Notifications Should Respect Location Temp Scale 2016-05-19 09:59:45 -05:00
Jim Anderson
40e6778e31 Merge pull request #898 from jimmyjames/add-tiles-readme
DOCS-284 adding README for example/test tiles
2016-05-18 13:58:45 -05:00
Jim Anderson
26a0f6f939 DOCS-284 adding README for example/test tiles 2016-05-13 21:10:54 -05:00
Matthew Hartley
56eef9cf22 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. 2016-05-12 14:28:40 -05:00
7 changed files with 675 additions and 40 deletions

View File

@@ -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.

View File

@@ -0,0 +1,145 @@
/**
* JSON
*
* Copyright 2015 Jesse Newland
*
* 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: "JSON API",
namespace: "jnewland",
author: "Jesse Newland",
description: "A JSON API for SmartThings",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
oauth: true)
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
if (!state.accessToken) {
createAccessToken()
}
}
preferences {
page(name: "copyConfig")
}
def copyConfig() {
if (!state.accessToken) {
createAccessToken()
}
dynamicPage(name: "copyConfig", title: "Config", install:true) {
section("Select devices to include in the /devices API call") {
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
input "hues", "capability.colorControl", title: "Hues", multiple: true, required: false
}
section() {
paragraph "View this SmartApp's configuration to use it in other places."
href url:"https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
}
section() {
href url:"https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Debug", description:"View accessories JSON"
}
}
}
def renderConfig() {
def configJson = new groovy.json.JsonOutput().toJson([
description: "JSON API",
platforms: [
[
platform: "SmartThings",
name: "SmartThings",
app_id: app.id,
access_token: state.accessToken
]
],
])
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
render contentType: "text/plain", data: configString
}
def deviceCommandMap(device, type) {
device.supportedCommands.collectEntries { command->
def commandUrl = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/${type}/${device.id}/command/${command.name}?access_token=${state.accessToken}"
[
(command.name): commandUrl
]
}
}
def authorizedDevices() {
[
switches: switches,
hues: hues
]
}
def renderDevices() {
def deviceData = authorizedDevices().collectEntries { devices->
[
(devices.key): devices.value.collect { device->
[
name: device.displayName,
commands: deviceCommandMap(device, devices.key)
]
}
]
}
def deviceJson = new groovy.json.JsonOutput().toJson(deviceData)
def deviceString = new groovy.json.JsonOutput().prettyPrint(deviceJson)
render contentType: "application/json", data: deviceString
}
def deviceCommand() {
def device = authorizedDevices()[params.type].find { it.id == params.id }
def command = params.command
if (!device) {
httpError(404, "Device not found")
} else {
if (params.value) {
device."$command"(params.value)
} else {
device."$command"()
}
}
}
mappings {
if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
path("/devices") { action: [GET: "authError"] }
path("/config") { action: [GET: "authError"] }
path("/:type/:id/command/:command") { action: [PUT: "authError"] }
} else {
path("/devices") { action: [GET: "renderDevices"] }
path("/config") { action: [GET: "renderConfig"] }
path("/:type/:id/command/:command") { action: [PUT: "deviceCommand"] }
}
}
def authError() {
[error: "Permission denied"]
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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"
}
}
}