Modifying 'Octoblu'

This commit is contained in:
Square Root of Saturn
2016-08-30 15:27:35 -05:00
parent 156adc3b86
commit 73e73dc5a6

View File

@@ -22,7 +22,7 @@ import org.apache.commons.codec.binary.Base64
import java.text.DecimalFormat import java.text.DecimalFormat
import groovy.transform.Field import groovy.transform.Field
@Field final USE_DEBUG = false @Field final USE_DEBUG = true
@Field final selectedCapabilities = [ "actuator", "sensor" ] @Field final selectedCapabilities = [ "actuator", "sensor" ]
private getVendorName() { "Octoblu" } private getVendorName() { "Octoblu" }
@@ -53,11 +53,14 @@ preferences {
} }
mappings { mappings {
path("/receiveCode") { path("/oauthCode") {
action: [ GET: "receiveCode" ] action: [ GET: "getOauthCode" ]
} }
path("/message") { path("/message") {
action: [ POST: "receiveMessage" ] action: [ POST: "postMessage" ]
}
path("/app") {
action: [ POST: "postApp" ]
} }
} }
@@ -83,8 +86,10 @@ def welcomePage() {
} }
if (state.installed) { if (state.installed) {
section { section {
input name: "showUninstall", type: "bool", title: "Uninstall", description: "false", submitOnChange: true input name: "showUninstall", type: "bool", title: "Uninstall", submitOnChange: true
if (showUninstall) { if (showUninstall) {
state.removeDevices = removeDevices
input name: "removeDevices", type: "bool", title: "Remove Octoblu devices", submitOnChange: true
paragraph title: "Sorry to see you go!", "please email <support@octoblu.com> with any feedback or issues" paragraph title: "Sorry to see you go!", "please email <support@octoblu.com> with any feedback or issues"
} }
} }
@@ -109,7 +114,7 @@ def authPage() {
def oauthParams = [ def oauthParams = [
response_type: "code", response_type: "code",
client_id: state.vendorOAuthUuid, client_id: state.vendorOAuthUuid,
redirect_uri: getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/receiveCode" redirect_uri: getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/oauthCode"
] ]
def redirectUrl = getVendorAuthPath() + '?' + toQueryString(oauthParams) def redirectUrl = getVendorAuthPath() + '?' + toQueryString(oauthParams)
@@ -140,9 +145,9 @@ def createOAuthDevice() {
"callbackUrl": getApiServerUrl() + "/api" "callbackUrl": getApiServerUrl() + "/api"
], ],
"configureWhitelist": [ "68c39f40-cc13-4560-a68c-e8acd021cff9" ], "configureWhitelist": [ "68c39f40-cc13-4560-a68c-e8acd021cff9" ],
"discoverWhitelist": [ "*", "68c39f40-cc13-4560-a68c-e8acd021cff9" ], "discoverWhitelist": [ "*" ],
"receiveWhitelist": [ "*" ], "receiveWhitelist": [],
"sendWhitelist": [ "*" ] "sendWhitelist": []
] ]
def postParams = [ uri: apiUrl()+"devices", def postParams = [ uri: apiUrl()+"devices",
@@ -163,7 +168,7 @@ def createOAuthDevice() {
// -------------------------------------- // --------------------------------------
def subscribePage() { def subscribePage() {
return dynamicPage(name: "subscribePage", title: "Subscribe to SmartThings", nextPage: "devicesPage") { return dynamicPage(name: "subscribePage", title: "Subscribe to SmartThing devices", nextPage: "devicesPage") {
section { section {
// input name: "selectedCapabilities", type: "enum", title: "capability filter", // input name: "selectedCapabilities", type: "enum", title: "capability filter",
// submitOnChange: true, multiple: true, required: false, options: [ "actuator", "sensor" ] // submitOnChange: true, multiple: true, required: false, options: [ "actuator", "sensor" ]
@@ -171,6 +176,13 @@ def subscribePage() {
input name: "${capability}Capability".toString(), type: "capability.$capability", title: "${capability.capitalize()} Things", multiple: true, required: false input name: "${capability}Capability".toString(), type: "capability.$capability", title: "${capability.capitalize()} Things", multiple: true, required: false
} }
} }
section(" ") {
input name: "pleaseCreateAppDevice", type: "bool", title: "Create a SmartApp device", defaultValue: true
paragraph "A SmartApp device allows access to location and hub information for this installation"
}
section(" ") {
paragraph title: "", "Existing Octoblu devices may be modified!"
}
} }
} }
@@ -178,11 +190,13 @@ def subscribePage() {
def devicesPage() { def devicesPage() {
def postParams = [ def postParams = [
uri: apiUrl() + "devices?owner=${state.vendorUuid}&category=smart-things", uri: apiUrl() + "devices?owner=${state.vendorUuid}&category=smart-things",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]] headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]
]
state.vendorDevices = [:] state.vendorDevices = [:]
def hasDevice = [:] def hasDevice = [:]
hasDevice[app.id] = true
selectedCapabilities.each { capability -> selectedCapabilities.each { capability ->
def smartDevices = settings["${capability}Capability"] def smartDevices = settings["${capability}Capability"]
smartDevices.each { smartDevice -> smartDevices.each { smartDevice ->
@@ -197,7 +211,7 @@ def devicesPage() {
response.data.devices.each { device -> response.data.devices.each { device ->
if (device.smartDeviceId && hasDevice[device.smartDeviceId]) { if (device.smartDeviceId && hasDevice[device.smartDeviceId]) {
debug "found device ${device.uuid} with smartDeviceId ${device.smartDeviceId}" debug "found device ${device.uuid} with smartDeviceId ${device.smartDeviceId}"
state.vendorDevices[device.smartDeviceId] = getVendorDeviceStateInfo(device) state.vendorDevices[device.smartDeviceId] = getDeviceInfo(device)
} }
debug "has device: ${device.uuid} ${device.name} ${device.type}" debug "has device: ${device.uuid} ${device.name} ${device.type}"
} }
@@ -210,6 +224,8 @@ def devicesPage() {
debug "checking devices for capability ${capability}" debug "checking devices for capability ${capability}"
createDevices(settings["${capability}Capability"]) createDevices(settings["${capability}Capability"])
} }
if (pleaseCreateAppDevice)
createAppDevice()
return dynamicPage(name: "devicesPage", title: "Octoblu Things", install: true) { return dynamicPage(name: "devicesPage", title: "Octoblu Things", install: true) {
section { section {
@@ -223,7 +239,12 @@ def devicesPage() {
def createDevices(smartDevices) { def createDevices(smartDevices) {
smartDevices.each { smartDevice -> smartDevices.each { smartDevice ->
def commands = [[ "name": "* get value" ],[ "name": "* get state" ],[ "name": "* get device" ],[ "name": "* get events" ]] def commands = [
[ "name": "app-get-value" ],
[ "name": "app-get-state" ],
[ "name": "app-get-device" ],
[ "name": "app-get-events" ]
]
smartDevice.supportedCommands.each { command -> smartDevice.supportedCommands.each { command ->
if (command.arguments.size()>0) { if (command.arguments.size()>0) {
@@ -307,32 +328,34 @@ def createDevices(smartDevices) {
"owner": "${state.vendorUuid}", "owner": "${state.vendorUuid}",
"configureWhitelist": [], "configureWhitelist": [],
"discoverWhitelist": ["${state.vendorUuid}"], "discoverWhitelist": ["${state.vendorUuid}"],
"receiveWhitelist": ["*"], "receiveWhitelist": [],
"sendWhitelist": ["*"], "sendWhitelist": [],
"type": "device:${smartDevice.name.replaceAll('\\s','-').toLowerCase()}", "type": "device:${smartDevice.name.replaceAll('\\s','-').toLowerCase()}",
"category": "smart-things", "category": "smart-things",
"meshblu": [ "meshblu": [
"messageHooks": [ "forwarders": [
[ "received": [[
"url": getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/message", "url": getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/message",
"method": "POST", "method": "POST",
"generateAndForwardMeshbluCredentials": false "type": "webhook"
] ]]
] ]
] ]
] ]
updatePermissions(deviceProperties, smartDevice.id)
def params = [ def params = [
uri: apiUrl() + "devices", uri: apiUrl() + "devices",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"], headers: ["Authorization": "Bearer ${state.vendorBearerToken}"],
body: groovy.json.JsonOutput.toJson(deviceProperties) ] body: groovy.json.JsonOutput.toJson(deviceProperties)
]
try { try {
if (!state.vendorDevices[smartDevice.id]) { if (!state.vendorDevices[smartDevice.id]) {
debug "creating new device for ${smartDevice.id}" debug "creating new device for ${smartDevice.id}"
httpPostJson(params) { response -> httpPostJson(params) { response ->
state.vendorDevices[smartDevice.id] = getVendorDeviceStateInfo(response.data) state.vendorDevices[smartDevice.id] = getDeviceInfo(response.data)
} }
return return
} }
@@ -349,8 +372,120 @@ def createDevices(smartDevices) {
} }
} }
def getVendorDeviceStateInfo(device) { def createAppDevice() {
return [ "uuid": device.uuid, "token": device.token ] def commands = [
[ "name": "app-get-location" ],
[ "name": "app-get-devices" ],
[ "name": "app-set-mode" ],
]
def schemas = [
"version": "2.0.0",
"message": [:]
]
commands.each { command ->
schemas."message"."$command.name" = [
"type": "object",
"properties": [
"command": [
"type": "string",
"readOnly": true,
"default": "$command.name",
"enum": ["$command.name"],
"x-schema-form": [
"condition": "false"
]
]
]
]
}
schemas."message"."app-set-mode"."properties"."args" = [
"type": "object",
"title": "Arguments",
"properties": [
"mode": [
"type": "string"
]
]
]
def deviceProperties = [
"schemas": schemas,
"needsSetup": false,
"online": true,
"name": "${location.name} SmartApp",
"smartDeviceId": "${app.id}",
"logo": "https://i.imgur.com/TsXefbK.png",
"owner": "${state.vendorUuid}",
"configureWhitelist": [],
"discoverWhitelist": ["${state.vendorUuid}"],
"receiveWhitelist": [],
"sendWhitelist": [],
"type": "device:smart-things-app",
"category": "smart-things",
"meshblu": [
"forwarders": [
"received": [[
"url": getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/app",
"method": "POST",
"type": "webhook"
]]
]
]
]
updatePermissions(deviceProperties, app.id)
def params = [
uri: apiUrl() + "devices",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"],
body: groovy.json.JsonOutput.toJson(deviceProperties)
]
debug "creating app device!"
debug params.body
try {
// debug params
if (!state.vendorDevices[app.id]) {
debug "creating new app device for ${app.id}"
httpPostJson(params) { response ->
state.vendorDevices[app.id] = getDeviceInfo(response.data)
}
return
}
params.uri = params.uri + "/${state.vendorDevices[app.id].uuid}"
debug "the app device ${app.id} has already been created, updating ${params.uri}"
httpPutJson(params) { response ->
resetVendorDeviceToken(app.id);
}
} catch (e) {
log.error "unable to create new device ${e}"
}
}
def updatePermissions(newDevice, id) {
def device = state.vendorDevices[id]
if (!device) return
newDevice.configureWhitelist = device.configureWhitelist
newDevice.discoverWhitelist = device.discoverWhitelist
newDevice.receiveWhitelist = device.receiveWhitelist
newDevice.sendWhitelist = device.sendWhitelist
}
def getDeviceInfo(device) {
return [
"uuid": device.uuid,
"token": device.token,
"configureWhitelist": device.configureWhitelist,
"discoverWhitelist": device.discoverWhitelist,
"receiveWhitelist": device.receiveWhitelist,
"sendWhitelist": device.sendWhitelist,
]
} }
def resetVendorDeviceToken(smartDeviceId) { def resetVendorDeviceToken(smartDeviceId) {
@@ -365,7 +500,7 @@ def resetVendorDeviceToken(smartDeviceId) {
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]] headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]]
try { try {
httpPost(postParams) { response -> httpPost(postParams) { response ->
state.vendorDevices[smartDeviceId] = getVendorDeviceStateInfo(response.data) state.vendorDevices[smartDeviceId] = getDeviceInfo(response.data)
debug "got new token for ${smartDeviceId}/${deviceUUID}" debug "got new token for ${smartDeviceId}/${deviceUUID}"
} }
} catch (e) { } catch (e) {
@@ -424,10 +559,12 @@ def cleanUpTokens() {
state.vendorToken = null state.vendorToken = null
if (state.vendorOAuthToken) { if (state.vendorOAuthToken) {
params.uri = apiUrl() + "devices/${state.vendorOAuthUuid}" def params = [
params.headers = [ uri: apiUrl() + "devices/${state.vendorOAuthUuid}",
"meshblu_auth_uuid": state.vendorOAuthUuid, headers: [
"meshblu_auth_token": state.vendorOAuthToken "meshblu_auth_uuid": state.vendorOAuthUuid,
"meshblu_auth_token": state.vendorOAuthToken
]
] ]
debug "deleting url ${params.uri}" debug "deleting url ${params.uri}"
@@ -447,7 +584,7 @@ def cleanUpTokens() {
// -------------------------------------- // --------------------------------------
def receiveCode() { def getOauthCode() {
// revokeAccessToken() // revokeAccessToken()
// state.accessToken = createAccessToken() // state.accessToken = createAccessToken()
debug "generated app access token ${state.accessToken}" debug "generated app access token ${state.accessToken}"
@@ -488,31 +625,31 @@ def receiveCode() {
def getEventData(evt) { def getEventData(evt) {
return [ return [
"date" : evt.date, "date" : evt.date,
"id" : evt.id, "id" : evt.id,
"data" : evt.data, "data" : evt.data,
"description" : evt.description, "description" : evt.description,
"descriptionText" : evt.descriptionText, "descriptionText" : evt.descriptionText,
"displayName" : evt.displayName, "displayName" : evt.displayName,
"deviceId" : evt.deviceId, "deviceId" : evt.deviceId,
"hubId" : evt.hubId, "hubId" : evt.hubId,
"installedSmartAppId" : evt.installedSmartAppId, "installedSmartAppId" : evt.installedSmartAppId,
"isoDate" : evt.isoDate, "isoDate" : evt.isoDate,
"isDigital" : evt.isDigital(), "isDigital" : evt.isDigital(),
"isPhysical" : evt.isPhysical(), "isPhysical" : evt.isPhysical(),
"isStateChange" : evt.isStateChange(), "isStateChange" : evt.isStateChange(),
"locationId" : evt.locationId, "locationId" : evt.locationId,
"name" : evt.name, "name" : evt.name,
"source" : evt.source, "source" : evt.source,
"unit" : evt.unit, "unit" : evt.unit,
"value" : evt.value, "value" : evt.value,
"category" : "event", "category" : "event",
"type" : "device:smart-thing" "type" : "device:smart-thing"
] ]
} }
def eventForward(evt) { def eventForward(evt) {
def eventData = [ "devices" : "*", "payload" : getEventData(evt) ] def eventData = [ "devices" : [ "*" ], "payload" : getEventData(evt) ]
debug "sending event: ${groovy.json.JsonOutput.toJson(eventData)}" debug "sending event: ${groovy.json.JsonOutput.toJson(eventData)}"
@@ -544,14 +681,20 @@ def eventForward(evt) {
// -------------------------------------- // --------------------------------------
def receiveMessage() { def postMessage() {
debug("received data ${request.JSON}") debug("received message data ${request.JSON}")
def foundDevice = false def foundDevice = false
selectedCapabilities.each{ capability -> selectedCapabilities.each{ capability ->
settings."${capability}Capability".each { thing -> settings."${capability}Capability".each { thing ->
if (!foundDevice && thing.id == request.JSON.smartDeviceId) { if (!foundDevice && thing.id == request.JSON.smartDeviceId) {
def vendorDevice = state.vendorDevices[thing.id]
foundDevice = true foundDevice = true
if (!request.JSON.command.startsWith("* get ")) { if (vendorDevice.uuid == request.JSON.fromUuid) {
log.error "aborting message from self"
return
}
if (!request.JSON.command.startsWith("app-")) {
def args = [] def args = []
if (request.JSON.args) { if (request.JSON.args) {
request.JSON.args.each { k, v -> request.JSON.args.each { k, v ->
@@ -565,19 +708,19 @@ def receiveMessage() {
debug "calling internal command ${request.JSON.command}" debug "calling internal command ${request.JSON.command}"
def commandData = [:] def commandData = [:]
switch (request.JSON.command) { switch (request.JSON.command) {
case "* get value": case "app-get-value":
debug "got command value" debug "got command value"
thing.supportedAttributes.each { attribute -> thing.supportedAttributes.each { attribute ->
commandData[attribute.name] = thing.latestValue(attribute.name) commandData[attribute.name] = thing.latestValue(attribute.name)
} }
break break
case "* get state": case "app-get-state":
debug "got command state" debug "got command state"
thing.supportedAttributes.each { attribute -> thing.supportedAttributes.each { attribute ->
commandData[attribute.name] = thing.latestState(attribute.name)?.value commandData[attribute.name] = thing.latestState(attribute.name)?.value
} }
break break
case "* get device": case "app-get-device":
debug "got command device" debug "got command device"
commandData = [ commandData = [
"id" : thing.id, "id" : thing.id,
@@ -589,7 +732,7 @@ def receiveMessage() {
"supportedCommands" : thing.supportedCommands.collect{ command -> return ["name" : command.name, "arguments" : command.arguments ] } "supportedCommands" : thing.supportedCommands.collect{ command -> return ["name" : command.name, "arguments" : command.arguments ] }
] ]
break break
case "* get events": case "app-get-events":
debug "got command events" debug "got command events"
commandData.events = [] commandData.events = []
thing.events().each { event -> thing.events().each { event ->
@@ -601,14 +744,12 @@ def receiveMessage() {
debug "unknown command ${request.JSON.command}" debug "unknown command ${request.JSON.command}"
} }
commandData.command = request.JSON.command commandData.command = request.JSON.command
def vendorDevice = state.vendorDevices[thing.id]
debug "with vendorDevice ${vendorDevice} for ${groovy.json.JsonOutput.toJson(commandData)}" debug "with vendorDevice ${vendorDevice} for ${groovy.json.JsonOutput.toJson(commandData)}"
def postParams = [ def postParams = [
uri: apiUrl() + "messages", uri: apiUrl() + "messages",
headers: ["meshblu_auth_uuid": vendorDevice.uuid, "meshblu_auth_token": vendorDevice.token], headers: ["meshblu_auth_uuid": vendorDevice.uuid, "meshblu_auth_token": vendorDevice.token],
body: groovy.json.JsonOutput.toJson([ "devices" : "*", "payload" : commandData ]) body: groovy.json.JsonOutput.toJson([ "devices" : [ "*" ], "payload" : commandData ])
] ]
debug "posting params ${postParams}" debug "posting params ${postParams}"
@@ -630,6 +771,105 @@ def receiveMessage() {
// -------------------------------------- // --------------------------------------
def postApp() {
debug("received app data ${request.JSON}")
if (state.vendorDevices[app.id].uuid == request.JSON.fromUuid) {
log.error "aborting message from self"
return
}
def args = []
if (request.JSON.args) {
request.JSON.args.each { k, v ->
args.push(v)
}
}
def commandData = [:]
switch (request.JSON.command) {
case "app-get-location":
debug "got command location"
def modes = []
location.modes.each { mode ->
modes.push([
"id" : mode.id,
"name" : mode.name
])
}
def hubs = []
location.hubs.each { hub ->
debug "hub : ${hub}"
hubs.push([
"firmwareVersionString" : hub.firmwareVersionString,
"id" : hub.id,
"localIP" : hub.localIP,
"localSrvPortTCP" : hub.localSrvPortTCP,
"name" : hub.name,
"type" : hub.type,
"zigbeeEui" : hub.zigbeeEui,
"zigbeeId" : hub.zigbeeId
])
}
commandData = [
"contactBookEnabled" : location.contactBookEnabled,
"id" : location.id,
"latitude" : location.latitude,
"longitude" : location.longitude,
"temperatureScale" : location.temperatureScale,
"timeZone" : location.timeZone.getID(),
"zipCode" : location.zipCode,
"mode" : location.mode,
"modes" : modes,
"hubs" : hubs
]
debug "copied location!"
debug commandData
break
case "app-get-devices":
debug "got command devices"
commandData.devices = state.vendorDevices.collect { k, v -> [ "smartDeviceId" : k, "uuid" : v.uuid ] }
break
case "app-set-mode":
location.setMode(*args)
commandData.mode = args[0]
break
default:
commandData.error = "unknown command"
debug "unknown command ${request.JSON.command}"
}
commandData.command = request.JSON.command
debug "sending ${commandData}"
def vendorDevice = state.vendorDevices[app.id]
debug "with vendorDevice ${vendorDevice} for ${groovy.json.JsonOutput.toJson(commandData)}"
def postParams = [
uri: apiUrl() + "messages",
headers: [ "meshblu_auth_uuid" : vendorDevice.uuid, "meshblu_auth_token" : vendorDevice.token ],
body: groovy.json.JsonOutput.toJson([ "devices" : [ "*" ], "payload" : commandData ])
]
debug "posting params ${postParams}"
try {
debug "calling httpPostJson!"
httpPostJson(postParams) { response ->
debug "sent off command result"
}
} catch (e) {
log.error "unable to send command result ${e}"
}
}
// --------------------------------------
private debug(logStr) { private debug(logStr) {
if (USE_DEBUG) if (USE_DEBUG)
log.debug logStr log.debug logStr
@@ -646,7 +886,24 @@ def initialize()
def uninstalled() def uninstalled()
{ {
debug "In uninstalled" debug "In uninstalled ${state.removeDevices}"
if (state.removeDevices) {
state.vendorDevices.each { k, device ->
def params = [
uri: apiUrl() + "devices/${device.uuid}",
headers: [ "meshblu_auth_uuid" : device.uuid, "meshblu_auth_token" : device.token ],
]
debug "deleting url ${params.uri}"
try {
httpDelete(params) { response ->
debug "delete device ${device.uuid}"
}
} catch (e) {
log.error "token delete error ${e}"
}
}
}
} }
def installed() { def installed() {