Compare commits
10 Commits
user186_9
...
MSA-2550-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
899346fb3a | ||
|
|
6e6a1d7083 | ||
|
|
cc2cd987ad | ||
|
|
1716bcba2b | ||
|
|
de8740d5e0 | ||
|
|
be89b7dbe9 | ||
|
|
263b0deeaa | ||
|
|
22b141416d | ||
|
|
9365b166d0 | ||
|
|
0fc84dafda |
@@ -181,7 +181,8 @@ def setColorTemperature(kelvin) {
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
|
||||
def value = parent.apiPUT("/lights/${selector()}/state", [power: "on"])
|
||||
if (value.status == 207 && value.data.results.status[0] == "ok") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
@@ -191,7 +192,8 @@ def on() {
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
|
||||
def value = parent.apiPUT("/lights/${selector()}/state", [power: "off"])
|
||||
if (value.status == 207 && value.data.results.status[0] == "ok") {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,8 @@ def setColorTemperature(kelvin) {
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
|
||||
def value = parent.apiPUT("/lights/${selector()}/state", [power: "on"])
|
||||
if (value.status == 207 && value.data.results.status[0] == "ok") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
@@ -120,7 +121,8 @@ def on() {
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
|
||||
def value = parent.apiPUT("/lights/${selector()}/state", [power: "off"])
|
||||
if (value.status == 207 && value.data.results.status[0] == "ok") {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,11 @@ def getClientId() { return appSettings.clientId }
|
||||
private getVendorName() { "LIFX" }
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage test1"
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
def validateToken = locationOptions() ?: []
|
||||
}
|
||||
|
||||
if (!state.lifxAccessToken) {
|
||||
log.debug "no LIFX access token"
|
||||
// This is the SmartThings access token
|
||||
@@ -61,9 +65,6 @@ def authPage() {
|
||||
}
|
||||
def description = "Tap to enter LIFX credentials"
|
||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
|
||||
// def redirectUrl = "${apiServerUrl}"
|
||||
// log.debug "app id: ${app.id}"
|
||||
// log.debug "redirect url: ${redirectUrl}"s
|
||||
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
|
||||
section {
|
||||
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
|
||||
@@ -322,7 +323,7 @@ def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}")
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("lifxAccessToken")
|
||||
state.lifxAccessToken = null
|
||||
options.logObject.warn "Access token is not valid"
|
||||
}
|
||||
return options.errorReturn
|
||||
@@ -335,6 +336,10 @@ def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
|
||||
def apiGET(path) {
|
||||
try {
|
||||
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
|
||||
if (response.status == 401) { // token is expired
|
||||
log.warn "Access token is not valid"
|
||||
state.lifxAccessToken = null
|
||||
}
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
@@ -348,6 +353,10 @@ def apiPUT(path, body = [:]) {
|
||||
try {
|
||||
log.debug("Beginning API PUT: ${path}, ${body}")
|
||||
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
|
||||
if (response.status == 401) { // token is expired
|
||||
log.warn "Access token is not valid"
|
||||
state.lifxAccessToken = null
|
||||
}
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
@@ -361,6 +370,9 @@ def devicesList(selector = '') {
|
||||
def resp = apiGET("/lights/${selector}")
|
||||
if (resp.status == 200) {
|
||||
return resp.data
|
||||
} else if (resp.status == 401) {
|
||||
log.warn "Access token is not valid"
|
||||
state.lifxAccessToken = null
|
||||
} else {
|
||||
log.debug("No response from device list call. ${resp.status} ${resp.data}")
|
||||
return []
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* SmartThingsToStart REST Api
|
||||
*
|
||||
* Copyright 2017 Dr1rrb
|
||||
*
|
||||
* 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: "SmartThingsToStart",
|
||||
namespace: "torick.net",
|
||||
author: "Dr1rrb",
|
||||
description: "SmartThingsToStart REST Api",
|
||||
category: "My Apps",
|
||||
iconUrl: "http://smartthingstostartproxy.azurewebsites.net/Assets/AppLogo.png",
|
||||
iconX2Url: "http://smartthingstostartproxy.azurewebsites.net/Assets/AppLogo@2X.png",
|
||||
iconX3Url: "http://smartthingstostartproxy.azurewebsites.net/Assets/AppLogo@3X.png",
|
||||
oauth: true)
|
||||
|
||||
|
||||
preferences {
|
||||
section("Control these devices") {
|
||||
input "switches", "capability.switch", title: "Select switches", multiple: true, required: false
|
||||
input "bubls", "capability.bulb", title: "Select bubls", hideWhenEmpty: true, multiple: true, required: false
|
||||
input "lights", "capability.light", title: "Select lights", hideWhenEmpty: true, multiple: true, required: false
|
||||
input "outlets", "capability.outlet", title: "Select outlets", hideWhenEmpty: true, multiple: true, required: false
|
||||
input "relaySwitches", "capability.relaySwitch", title: "Select relay switches", hideWhenEmpty: true, multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/infos") {
|
||||
action: [GET: "retreiveServerInfos"]
|
||||
}
|
||||
path("/items") {
|
||||
action: [GET: "retreiveDevicesAndRoutines"]
|
||||
}
|
||||
path("/device/:id") {
|
||||
action: [GET: "retreiveDevice"]
|
||||
}
|
||||
path("/device/:id/subscription/:subscriptionId") {
|
||||
action: [
|
||||
PUT: "updateOrCreateSubscription",
|
||||
POST: "updateOrCreateSubscription",
|
||||
]
|
||||
}
|
||||
// TODO
|
||||
//path("/device/:id/unsubscribe") {
|
||||
// action: [POST: "unsubscribeFromDevice"]
|
||||
//}
|
||||
path("/device/:id/:command") {
|
||||
action: [ PUT: "updateDevice" ]
|
||||
}
|
||||
path("/routine/:id/execute") {
|
||||
action: [PUT: "executeRoutine"]
|
||||
}
|
||||
}
|
||||
|
||||
// Region: App lifecycle
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
//state.pushChannels = [:]
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
def channels = state.pushChannels = state.pushChannels ?: [:];
|
||||
channels.each
|
||||
{
|
||||
def device = findDevice(it.key);
|
||||
if (device != null)
|
||||
{
|
||||
subscribeToDevice(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Region: Http request handlers
|
||||
def retreiveServerInfos()
|
||||
{
|
||||
return [version: 1]
|
||||
}
|
||||
|
||||
def retreiveDevicesAndRoutines() {
|
||||
def details = params.details == "true" ? true : false;
|
||||
|
||||
return [
|
||||
devices: getDevices().collect { getDeviceInfos(it, details) },
|
||||
routines: location.helloHome?.getPhrases().collect { getRoutineInfos(it, details) }
|
||||
];
|
||||
}
|
||||
|
||||
def retreiveDevice() {
|
||||
def device = getDevice(params.id);
|
||||
def details = params.details == "true" ? true : false;
|
||||
|
||||
return getDeviceInfos(device, details);
|
||||
}
|
||||
|
||||
def updateOrCreateSubscription() {
|
||||
def device = getDevice(params.id);
|
||||
def channelUri = notNull("channelUri", request.JSON?.channelUri);
|
||||
def token = notNull("token", request.JSON?.token);
|
||||
|
||||
log.debug "Subscribing to device '${device.id}' (target: '${channelUri}' / token: '${token}')"
|
||||
|
||||
// Get or create the push notification channel from / into the local state
|
||||
def subscriptionId = params.subscriptionId ?: UUID.randomUUID().toString() ;
|
||||
def allSubscriptions = state.pushChannels ?: (state.pushChannels = [:]);
|
||||
def deviceSubscriptions = allSubscriptions[device.id] ?: (allSubscriptions[device.id] = []);
|
||||
def subscription = deviceSubscriptions.find { it.id == subscriptionId };
|
||||
if (subscription == null)
|
||||
{
|
||||
deviceSubscriptions << [
|
||||
id: subscriptionId,
|
||||
deviceId: device.id,
|
||||
channelUri: channelUri,
|
||||
token: token
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
subscription["channelUri"] = channelUri;
|
||||
subscription["token"] = token;
|
||||
}
|
||||
|
||||
log.debug "Active subscriptions: \n" + state.pushChannels.collect { "** Device ${it.key} **\n" + it.value.collect{c -> "- - - > ${c.channelUri} : ${c.token.substring(0, 10)}..."}.join('\n') + "\n***************************" }.join('\n\n')
|
||||
|
||||
// (Re)create the subscription(s)
|
||||
subscribeToDevice(device)
|
||||
|
||||
return [subscriptionId: subscriptionId];
|
||||
}
|
||||
|
||||
def subscribeToDevice(device)
|
||||
{
|
||||
log.debug "Subscribing to device '${device.id}'"
|
||||
|
||||
unsubscribe(device);
|
||||
subscribe(device, "switch", switchStateChanged)
|
||||
|
||||
if (device.hasCapability("Color Control"))
|
||||
{
|
||||
log.debug "Device '${device.id}' has also the color capability. Subscribe to it."
|
||||
subscribe(device, "color", colorStateChanged)
|
||||
}
|
||||
}
|
||||
|
||||
def switchStateChanged(eventArgs) { sendPushNotification("switch", eventArgs) }
|
||||
def colorStateChanged(eventArgs) { sendPushNotification("color", eventArgs) }
|
||||
|
||||
def updateDevice() {
|
||||
def device = getDevice(params.id)
|
||||
def command = notNull("command", params.command)
|
||||
|
||||
log.debug "Executing '${command}' on device '${device.id}'."
|
||||
|
||||
switch(command) {
|
||||
case "on":
|
||||
case "On":
|
||||
device.on()
|
||||
break
|
||||
|
||||
case "off":
|
||||
case "Off":
|
||||
device.off()
|
||||
break
|
||||
|
||||
case "toggle":
|
||||
case "Toggle":
|
||||
if (device.currentSwitch == "on")
|
||||
device.off();
|
||||
else
|
||||
device.on();
|
||||
break;
|
||||
|
||||
default:
|
||||
httpError(501, "'${command}' is not a valid command for '${device.id}'")
|
||||
}
|
||||
|
||||
return getDeviceInfos(device);
|
||||
}
|
||||
|
||||
def executeRoutine() {
|
||||
def routine = getRoutine(params.id);
|
||||
log.debug "Executing routine '${routine.id}' (${routine.label})"
|
||||
|
||||
location.helloHome?.execute(routine.id)
|
||||
}
|
||||
|
||||
// Region: Get device
|
||||
def getDevices()
|
||||
{
|
||||
return switches
|
||||
+ bubls
|
||||
+ lights
|
||||
+ outlets
|
||||
+ relaySwitches;
|
||||
}
|
||||
|
||||
def findDevice(deviceId)
|
||||
{
|
||||
notNull("deviceId", deviceId);
|
||||
|
||||
return getDevices().find { it.id == deviceId };
|
||||
}
|
||||
|
||||
def getDevice(deviceId)
|
||||
{
|
||||
def device = findDevice(deviceId);
|
||||
if (device == null)
|
||||
{
|
||||
httpError(404, "Device '${deviceId}' not found.")
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
// Region: Get routine
|
||||
def findRoutine(routineId)
|
||||
{
|
||||
return location.helloHome?.getPhrases().find{ it.id == routineId};
|
||||
}
|
||||
|
||||
def getRoutine(routineId)
|
||||
{
|
||||
def routine = findRoutine(routineId);
|
||||
if (routine == null)
|
||||
{
|
||||
httpError(404, "Routine '${routineId}' not found.")
|
||||
}
|
||||
return routine;
|
||||
}
|
||||
|
||||
// Region: Parameters assertion helpers
|
||||
def notNull(parameterName, value)
|
||||
{
|
||||
if(value == null || value == "")
|
||||
{
|
||||
httpError(404, "Missing parameter '${parameterName}'.")
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Region: Get infos
|
||||
def getDeviceInfos(device, details = false)
|
||||
{
|
||||
def infos = [
|
||||
id: device.id,
|
||||
name: device.displayName,
|
||||
state: device.currentValue("switch"),
|
||||
color: device.currentValue("color"),
|
||||
hue: device.currentValue("hue"),
|
||||
saturation: device.currentValue("saturation"),
|
||||
capabilities: device.capabilities.collect { getCapabilityInfos(it, details) }
|
||||
]
|
||||
|
||||
if (details)
|
||||
{
|
||||
infos["attributes"] = device.supportedAttributes.collect { getAttributeInfos(it, details) }
|
||||
infos["commands"] = device.supportedCommands.collect { getCommandInfos(it, details) }
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
def getCapabilityInfos(capablity, details = false)
|
||||
{
|
||||
def infos = [name: capablity.name]
|
||||
|
||||
if(details)
|
||||
{
|
||||
infos["attributes"] = capablity.attributes.collect { getAttributeInfos(it, details) }
|
||||
infos["commands"] = capablity.commands.collect { getCommandInfos(it, details) }
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
def getCommandInfos(command, details = false)
|
||||
{
|
||||
return [
|
||||
name: command.name,
|
||||
arguments: command.arguments
|
||||
]
|
||||
}
|
||||
|
||||
def getAttributeInfos(attribute, details = false)
|
||||
{
|
||||
return [
|
||||
name: attribute.name,
|
||||
arguments: attribute.dataType,
|
||||
values: attribute.values
|
||||
]
|
||||
}
|
||||
|
||||
def getRoutineInfos(routine, details = false)
|
||||
{
|
||||
def infos = [
|
||||
id: routine.id,
|
||||
name: routine.label
|
||||
];
|
||||
|
||||
if (details)
|
||||
{
|
||||
infos["hasSecureActions"] = routine.hasSecureActions;
|
||||
infos["action"] = routine.action;
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
// Region: Push notification
|
||||
def sendPushNotification(capability, eventArgs)
|
||||
{
|
||||
def deviceId = eventArgs.deviceId;
|
||||
log.debug "Received notification for '${capability}' for device '${deviceId}'.";
|
||||
|
||||
def subscriptions = state.pushChannels.get(deviceId);
|
||||
if (subscriptions == null || subscriptions.empty)
|
||||
{
|
||||
log.error "No subscription found for device ${deviceId}, unsubscribing!";
|
||||
unsubscribe(eventArgs.device);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
subscriptions.groupBy { it.channelUri }.each { sendPushNotification(capability, eventArgs, it.key, it.value) }
|
||||
}
|
||||
|
||||
def sendPushNotification(capability, eventArgs, channelUri, subscriptions)
|
||||
{
|
||||
try {
|
||||
def request = [
|
||||
uri: channelUri,
|
||||
//headers: [name: "Authorization", value: "Bearer ${subscription.token}"],
|
||||
body: [
|
||||
location: [
|
||||
id: eventArgs.locationId,
|
||||
],
|
||||
device: getDeviceInfos(eventArgs.device),
|
||||
event: [
|
||||
source: capability,
|
||||
date: eventArgs.isoDate,
|
||||
value: eventArgs.value,
|
||||
name: eventArgs.name,
|
||||
],
|
||||
subscriptions: subscriptions.collect { [id: it.id, token: it.token] }
|
||||
]
|
||||
]
|
||||
|
||||
// Async post is still in beta stage ...
|
||||
httpPostJson(request) { resp -> log.debug "response: ${resp.status}." }
|
||||
|
||||
} catch (e) {
|
||||
log.error "Failed to push notification: ${e}"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user