Compare commits

..

2 Commits

10 changed files with 969 additions and 100 deletions

View File

@@ -28,8 +28,8 @@ metadata {
}
}
valueTile("doNotRemove", "v", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
state "default", label:'If removed, Hue lights will not work properly'
}
state "default", label:'Do not remove'
}
valueTile("idNumber", "device.idNumber", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
state "default", label:'ID: ${currentValue}'
}
@@ -38,7 +38,7 @@ metadata {
}
main (["rich-control"])
details(["rich-control", "doNotRemove", "idNumber", "networkAddress"])
details(["rich-control", "idNumber", "networkAddress", "doNotRemove"])
}
}

View File

@@ -147,8 +147,8 @@ private Map parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
Map resultMap = [:]
resultMap.name = 'motion'
resultMap.value = zs.isAlarm2Set() ? 'active' : 'inactive'
result.name = 'motion'
result.value = zs.isAlarm2Set() ? 'active' : 'inactive'
log.debug(zs.isAlarm2Set() ? 'motion' : 'no motion')
return resultMap

View File

@@ -24,7 +24,7 @@ metadata {
capability "Temperature Measurement"
capability "Water Sensor"
capability "Health Check"
capability "Sensor"
capability "Sensor"
command "enrollResponse"
@@ -304,7 +304,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."

View File

@@ -24,7 +24,7 @@ metadata {
capability "Temperature Measurement"
capability "Refresh"
capability "Health Check"
capability "Sensor"
capability "Sensor"
command "enrollResponse"
@@ -315,7 +315,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."

View File

@@ -413,7 +413,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
log.debug "Configuring Reporting"

View File

@@ -267,7 +267,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."

View File

@@ -275,7 +275,7 @@ def refresh()
}
def configure() {
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
log.debug "Configuring Reporting and Bindings."
def configCmds = [

View File

@@ -22,7 +22,6 @@ metadata {
capability "Actuator"
capability "Color Temperature"
capability "Configuration"
capability "Health Check"
capability "Refresh"
capability "Switch"
capability "Switch Level"
@@ -73,12 +72,6 @@ def parse(String description) {
log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) {
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastActivity == null){
state.lastActivity = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
state.lastActivity = now()
if (event.name=="level" && event.value==0) {}
else {
if (event.name=="colorTemperature") {
@@ -105,29 +98,12 @@ def setLevel(value) {
zigbee.setLevel(value)
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.onOffRefresh()
} else {
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
}
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
}

View File

@@ -0,0 +1,927 @@
/**
The MIT License (MIT)
Copyright (c) 2016 Octoblu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import org.apache.commons.codec.binary.Base64
import java.text.DecimalFormat
import groovy.transform.Field
@Field final USE_DEBUG = true
@Field final selectedCapabilities = [ "actuator", "sensor" ]
private getVendorName() { "Octoblu" }
private getVendorIcon() { "http://i.imgur.com/BjTfDYk.png" }
private apiUrl() { appSettings.apiUrl ?: "https://meshblu.octoblu.com/" }
private getVendorAuthPath() { appSettings.vendorAuthPath ?: "https://oauth.octoblu.com/authorize" }
private getVendorTokenPath() { appSettings.vendorTokenPath ?: "https://oauth.octoblu.com/access_token" }
definition(
name: "Octoblu",
namespace: "citrix",
author: "Octoblu",
description: "Connect SmartThings devices to Octoblu",
category: "SmartThings Labs",
iconUrl: "http://i.imgur.com/BjTfDYk.png",
iconX2Url: "http://i.imgur.com/BjTfDYk.png"
) {
appSetting "apiUrl"
appSetting "vendorAuthPath"
appSetting "vendorTokenPath"
}
preferences {
page(name: "welcomePage")
page(name: "authPage")
page(name: "subscribePage")
page(name: "devicesPage")
}
mappings {
path("/oauthCode") {
action: [ GET: "getOauthCode" ]
}
path("/message") {
action: [ POST: "postMessage" ]
}
path("/app") {
action: [ POST: "postApp" ]
}
}
// --------------------------------------
def getDevInfo() {
return state.vendorDevices.collect { k, v -> "${v.uuid} " }.sort().join(" \n")
}
// --------------------------------------
def welcomePage() {
cleanUpTokens()
return dynamicPage(name: "welcomePage", nextPage: "authPage", uninstall: showUninstall) {
section {
paragraph title: "Welcome to the Octoblu SmartThings App!", "press 'Next' to continue"
}
if (state.vendorDevices && state.vendorDevices.size()>0) {
section {
paragraph title: "My SmartThings in Octobu (${state.vendorDevices.size()}):", getDevInfo()
}
}
if (state.installed) {
section {
input name: "showUninstall", type: "bool", title: "Uninstall", submitOnChange: true
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"
}
}
}
}
}
// --------------------------------------
def authPage() {
if (!state.accessToken) {
createAccessToken()
}
debug "using app access token ${state.accessToken}"
if (!state.vendorOAuthToken) {
createOAuthDevice()
}
def oauthParams = [
response_type: "code",
client_id: state.vendorOAuthUuid,
redirect_uri: getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/oauthCode"
]
def redirectUrl = getVendorAuthPath() + '?' + toQueryString(oauthParams)
debug "tokened redirect_uri = ${oauthParams.redirect_uri}"
def isRequired = !state.vendorBearerToken
return dynamicPage(name: "authPage", title: "Octoblu Authentication", nextPage:(isRequired ? null : "subscribePage"), install: isRequired) {
section {
debug "url: ${redirectUrl}"
if (isRequired) {
href url:redirectUrl, style:"embedded", title: "Authorize with Octoblu", required: isRequired, description:"please login with Octoblu to complete setup"
} else {
paragraph title: "Please press 'Next' to continue", "Octoblu token has been created"
}
}
}
}
def createOAuthDevice() {
def oAuthDevice = [
"name": "SmartThings",
"owner": "68c39f40-cc13-4560-a68c-e8acd021cff9",
"type": "device:oauth",
"online": true,
"options": [
"name": "SmartThings",
"imageUrl": "https://i.imgur.com/TsXefbK.png",
"callbackUrl": getApiServerUrl() + "/api"
],
"configureWhitelist": [ "68c39f40-cc13-4560-a68c-e8acd021cff9" ],
"discoverWhitelist": [ "*" ],
"receiveWhitelist": [],
"sendWhitelist": []
]
def postParams = [ uri: apiUrl()+"devices",
body: groovy.json.JsonOutput.toJson(oAuthDevice)]
try {
httpPostJson(postParams) { response ->
debug "got new token for oAuth device ${response.data}"
state.vendorOAuthUuid = response.data.uuid
state.vendorOAuthToken = response.data.token
}
} catch (e) {
log.error "unable to create oAuth device: ${e}"
}
}
// --------------------------------------
def subscribePage() {
return dynamicPage(name: "subscribePage", title: "Subscribe to SmartThing devices", nextPage: "devicesPage") {
section {
// input name: "selectedCapabilities", type: "enum", title: "capability filter",
// submitOnChange: true, multiple: true, required: false, options: [ "actuator", "sensor" ]
for (capability in selectedCapabilities) {
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!"
}
}
}
// --------------------------------------
def devicesPage() {
def postParams = [
uri: apiUrl() + "devices?owner=${state.vendorUuid}&category=smart-things",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]
]
state.vendorDevices = [:]
def hasDevice = [:]
hasDevice[app.id] = true
selectedCapabilities.each { capability ->
def smartDevices = settings["${capability}Capability"]
smartDevices.each { smartDevice ->
hasDevice[smartDevice.id] = true
}
}
debug "getting url ${postParams.uri}"
try {
httpGet(postParams) { response ->
debug "devices json ${response.data.devices}"
response.data.devices.each { device ->
if (device.smartDeviceId && hasDevice[device.smartDeviceId]) {
debug "found device ${device.uuid} with smartDeviceId ${device.smartDeviceId}"
state.vendorDevices[device.smartDeviceId] = getDeviceInfo(device)
}
debug "has device: ${device.uuid} ${device.name} ${device.type}"
}
}
} catch (e) {
log.error "devices error ${e}"
}
selectedCapabilities.each { capability ->
debug "checking devices for capability ${capability}"
createDevices(settings["${capability}Capability"])
}
if (pleaseCreateAppDevice)
createAppDevice()
return dynamicPage(name: "devicesPage", title: "Octoblu Things", install: true) {
section {
paragraph title: "Please press 'Done' to finish setup", "and subscribe to SmartThing events"
paragraph title: "My Octoblu UUID:", "${state.vendorUuid}"
paragraph title: "My SmartThings in Octobu (${state.vendorDevices.size()}):", getDevInfo()
}
}
}
def createDevices(smartDevices) {
smartDevices.each { smartDevice ->
def commands = [
[ "name": "app-get-value" ],
[ "name": "app-get-state" ],
[ "name": "app-get-device" ],
[ "name": "app-get-events" ]
]
smartDevice.supportedCommands.each { command ->
if (command.arguments.size()>0) {
commands.push([ "name": command.name, "args": command.arguments ])
} else {
commands.push([ "name": command.name ])
}
}
debug "creating device for ${smartDevice.id}"
def schemas = [
"version": "2.0.0",
"message": [:]
]
commands.each { command ->
schemas."message"."$command.name" = [
"type": "object",
"properties": [
"smartDeviceId": [
"type": "string",
"readOnly": true,
"default": "$smartDevice.id",
"x-schema-form": [
"condition": "false"
]
],
"command": [
"type": "string",
"readOnly": true,
"default": "$command.name",
"enum": ["$command.name"],
"x-schema-form": [
"condition": "false"
]
]
]
]
if (command.args) {
schemas."message"."$command.name"."properties"."args" = [
"type": "object",
"title": "Arguments",
"properties": [:]
]
command.args.each { arg ->
def argLower = "$arg"
argLower = argLower.toLowerCase()
if (argLower == "color_map") {
schemas."message"."$command.name"."properties"."args"."properties"."$argLower" = [
"type": "object",
"properties": [
"hex": [
"type": "string"
],
"level": [
"type": "number"
]
]
]
} else {
schemas."message"."$command.name"."properties"."args"."properties"."$argLower" = [
"type": "$argLower"
]
}
}
}
}
debug "UPDATED message schema: ${schemas}"
def deviceProperties = [
"schemas": schemas,
"needsSetup": false,
"online": true,
"name": "${smartDevice.displayName}",
"smartDeviceId": "${smartDevice.id}",
"logo": "https://i.imgur.com/TsXefbK.png",
"owner": "${state.vendorUuid}",
"configureWhitelist": [],
"discoverWhitelist": ["${state.vendorUuid}"],
"receiveWhitelist": [],
"sendWhitelist": [],
"type": "device:${smartDevice.name.replaceAll('\\s','-').toLowerCase()}",
"category": "smart-things",
"meshblu": [
"forwarders": [
"received": [[
"url": getApiServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/message",
"method": "POST",
"type": "webhook"
]]
]
]
]
updatePermissions(deviceProperties, smartDevice.id)
def params = [
uri: apiUrl() + "devices",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"],
body: groovy.json.JsonOutput.toJson(deviceProperties)
]
try {
if (!state.vendorDevices[smartDevice.id]) {
debug "creating new device for ${smartDevice.id}"
httpPostJson(params) { response ->
state.vendorDevices[smartDevice.id] = getDeviceInfo(response.data)
}
return
}
params.uri = params.uri + "/${state.vendorDevices[smartDevice.id].uuid}"
debug "the device ${smartDevice.id} has already been created, updating ${params.uri}"
httpPutJson(params) { response ->
resetVendorDeviceToken(smartDevice.id);
}
} catch (e) {
log.error "unable to create new device ${e}"
}
}
}
def createAppDevice() {
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 deviceUUID = state.vendorDevices[smartDeviceId].uuid
if (!deviceUUID) {
debug "no device uuid in resetVendorDeviceToken?"
return
}
debug "getting new token for ${smartDeviceId}/${deviceUUID}"
def postParams = [
uri: apiUrl() + "devices/${deviceUUID}/token",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]]
try {
httpPost(postParams) { response ->
state.vendorDevices[smartDeviceId] = getDeviceInfo(response.data)
debug "got new token for ${smartDeviceId}/${deviceUUID}"
}
} catch (e) {
log.error "unable to get new token ${e}"
}
}
// --------------------------------------
def updated() {
unsubscribe()
debug "Updated with settings: ${settings}"
def subscribed = [:]
selectedCapabilities.each{ capability ->
settings."${capability}Capability".each { thing ->
if (subscribed[thing.id]) {
return
}
subscribed[thing.id] = true
thing.supportedAttributes.each { attribute ->
debug "subscribe to attribute ${attribute.name}"
subscribe thing, attribute.name, eventForward
}
thing.supportedCommands.each { command ->
debug "subscribe to command ${command.name}"
subscribeToCommand thing, command.name, eventForward
}
debug "subscribed to thing ${thing.id}"
}
}
cleanUpTokens()
}
// --------------------------------------
def cleanUpTokens() {
if (state.vendorToken) {
def params = [
uri: apiUrl() + "devices/${state.vendorUuid}/tokens/${state.vendorToken}",
headers: ["Authorization": "Bearer ${state.vendorBearerToken}"]
]
debug "deleting url ${params.uri}"
try {
httpDelete(params) { response ->
debug "revoked token for ${state.vendorUuid}...?"
}
} catch (e) {
log.error "token delete error ${e}"
}
}
state.vendorBearerToken = null
state.vendorUuid = null
state.vendorToken = null
if (state.vendorOAuthToken) {
def params = [
uri: apiUrl() + "devices/${state.vendorOAuthUuid}",
headers: [
"meshblu_auth_uuid": state.vendorOAuthUuid,
"meshblu_auth_token": state.vendorOAuthToken
]
]
debug "deleting url ${params.uri}"
try {
httpDelete(params) { response ->
debug "deleted oauth device for ${state.vendorOAuthUuid}...?"
}
} catch (e) {
log.error "oauth token delete error ${e}"
}
}
state.vendorOAuthUuid = null
state.vendorOAuthToken = null
}
// --------------------------------------
def getOauthCode() {
// revokeAccessToken()
// state.accessToken = createAccessToken()
debug "generated app access token ${state.accessToken}"
def postParams = [
uri: getVendorTokenPath(),
body: [
client_id: state.vendorOAuthUuid,
client_secret: state.vendorOAuthToken,
grant_type: "authorization_code",
code: params.code
]
]
def style = "<style type='text/css'>body{font-size:2em;padding:1em}</style>"
def startBody = "<html>${style}<body>"
def endBody = "</body></html>"
def goodResponse = "${startBody}<h1>Received Octoblu Token!</h1><h2>Press 'Done' to finish setup.</h2>${endBody}"
def badResponse = "${startBody}<h1>Something went wrong...</h1><h2>PANIC!</h2>${endBody}"
debug "authorizeToken with postParams ${postParams}"
try {
httpPost(postParams) { response ->
debug "response: ${response.data}"
state.vendorBearerToken = response.data.access_token
def bearer = new String((new Base64()).decode(state.vendorBearerToken)).split(":")
state.vendorUuid = bearer[0]
state.vendorToken = bearer[1]
debug "have octoblu tokens ${state.vendorBearerToken}"
render contentType: 'text/html', data: (state.vendorBearerToken ? goodResponse : badResponse)
}
} catch(e) {
log.error "second leg oauth error ${e}"
render contentType: 'text/html', data: badResponse
}
}
def getEventData(evt) {
return [
"date" : evt.date,
"id" : evt.id,
"data" : evt.data,
"description" : evt.description,
"descriptionText" : evt.descriptionText,
"displayName" : evt.displayName,
"deviceId" : evt.deviceId,
"hubId" : evt.hubId,
"installedSmartAppId" : evt.installedSmartAppId,
"isoDate" : evt.isoDate,
"isDigital" : evt.isDigital(),
"isPhysical" : evt.isPhysical(),
"isStateChange" : evt.isStateChange(),
"locationId" : evt.locationId,
"name" : evt.name,
"source" : evt.source,
"unit" : evt.unit,
"value" : evt.value,
"category" : "event",
"type" : "device:smart-thing"
]
}
def eventForward(evt) {
def eventData = [ "devices" : [ "*" ], "payload" : getEventData(evt) ]
debug "sending event: ${groovy.json.JsonOutput.toJson(eventData)}"
def vendorDevice = state.vendorDevices[evt.deviceId]
if (!vendorDevice) {
log.error "aborting, vendor device for ${evt.deviceId} doesn't exist?"
return
}
debug "using device ${vendorDevice}"
def postParams = [
uri: apiUrl() + "messages",
headers: [
"meshblu_auth_uuid": vendorDevice.uuid,
"meshblu_auth_token": vendorDevice.token
],
body: groovy.json.JsonOutput.toJson(eventData)
]
try {
httpPostJson(postParams) { response ->
debug "sent off device event"
}
} catch (e) {
log.error "unable to send device event ${e}"
}
}
// --------------------------------------
def postMessage() {
debug("received message data ${request.JSON}")
def foundDevice = false
selectedCapabilities.each{ capability ->
settings."${capability}Capability".each { thing ->
if (!foundDevice && thing.id == request.JSON.smartDeviceId) {
def vendorDevice = state.vendorDevices[thing.id]
foundDevice = true
if (vendorDevice.uuid == request.JSON.fromUuid) {
log.error "aborting message from self"
return
}
if (!request.JSON.command.startsWith("app-")) {
def args = []
if (request.JSON.args) {
request.JSON.args.each { k, v ->
args.push(v)
}
}
debug "command being sent: ${request.JSON.command}\targs to be sent: ${args}"
thing."${request.JSON.command}"(*args)
} else {
debug "calling internal command ${request.JSON.command}"
def commandData = [:]
switch (request.JSON.command) {
case "app-get-value":
debug "got command value"
thing.supportedAttributes.each { attribute ->
commandData[attribute.name] = thing.latestValue(attribute.name)
}
break
case "app-get-state":
debug "got command state"
thing.supportedAttributes.each { attribute ->
commandData[attribute.name] = thing.latestState(attribute.name)?.value
}
break
case "app-get-device":
debug "got command device"
commandData = [
"id" : thing.id,
"displayName" : thing.displayName,
"name" : thing.name,
"label" : thing.label,
"capabilities" : thing.capabilities.collect{ thingCapability -> return thingCapability.name },
"supportedAttributes" : thing.supportedAttributes.collect{ attribute -> return attribute.name },
"supportedCommands" : thing.supportedCommands.collect{ command -> return ["name" : command.name, "arguments" : command.arguments ] }
]
break
case "app-get-events":
debug "got command events"
commandData.events = []
thing.events().each { event ->
commandData.events.push(getEventData(event))
}
break
default:
commandData.error = "unknown command"
debug "unknown command ${request.JSON.command}"
}
commandData.command = request.JSON.command
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}"
}
}
}
}
}
}
// --------------------------------------
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) {
if (USE_DEBUG)
log.debug logStr
}
String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
def initialize()
{
debug "Initialized with settings: ${settings}"
}
def 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() {
debug "Installed with settings: ${settings}"
state.installed = true
}
private Boolean canInstallLabs()
{
return hasAllHubsOver("000.011.00603")
}
private Boolean hasAllHubsOver(String desiredFirmware)
{
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}
private List getRealHubFirmwareVersions()
{
return location.hubs*.firmwareVersionString.findAll { it }
}

View File

@@ -133,23 +133,14 @@ def bulbDiscovery() {
state.inBulbDiscovery = true
def bridge = null
if (selectedHue) {
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListData)
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListData)
}
state.bridgeRefreshCount = 0
def allLightsFound = bulbsDiscovered() ?: [:]
// List lights currently not added to the user (editable)
def newLights = allLightsFound.findAll {getChildDevice(it.key) == null} ?: [:]
newLights = newLights.sort {it.value.toLowerCase()}
// List lights already added to the user (not editable)
def existingLights = allLightsFound.findAll {getChildDevice(it.key) != null} ?: [:]
existingLights = existingLights.sort {it.value.toLowerCase()}
def numFound = newLights.size() ?: 0
if (numFound == 0)
app.updateSetting("selectedBulbs", "")
state.bridgeRefreshCount = 0
def bulboptions = bulbsDiscovered() ?: [:]
def numFound = bulboptions.size() ?: 0
if (numFound == 0)
app.updateSetting("selectedBulbs", "")
if((bulbRefreshCount % 5) == 0) {
discoverHueBulbs()
@@ -157,25 +148,14 @@ def bulbDiscovery() {
def selectedBridge = state.bridges.find { key, value -> value?.serialNumber?.equalsIgnoreCase(selectedHue) }
def title = selectedBridge?.value?.name ?: "Find bridges"
// List of all lights previously added shown to user
def existingLightsDescription = ""
if (existingLights) {
existingLights.each {
if (existingLightsDescription.isEmpty()) {
existingLightsDescription += it.value
} else {
existingLightsDescription += ", ${it.value}"
}
}
}
return dynamicPage(name:"bulbDiscovery", title:"Light Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your Hue Lights. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, options:newLights
paragraph title: "Previously added Hue Lights (${existingLights.size()} added)", existingLightsDescription
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights (${numFound} found)", multiple:true, options:bulboptions
}
section {
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
}
}
}
@@ -310,11 +290,8 @@ def manualRefresh() {
}
def uninstalled(){
// Remove bridgedevice connection to allow uninstall of smartapp even though bridge is listed
// as user of smartapp
app.updateSetting("bridgeDevice", null)
state.bridges = [:]
state.username = null
state.username = null
}
// Handles events to add new bulbs
@@ -438,32 +415,23 @@ def addBridge() {
// Hue uses last 6 digits of MAC address as ID number, this number is shown on the bottom of the bridge
def idNumber = getBridgeIdNumber(selectedHue)
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub, ["label": "Hue Bridge ($idNumber)"])
if (d) {
// Associate smartapp to bridge so user will be warned if trying to delete bridge
app.updateSetting("bridgeDevice", [type: "device.hueBridge", value: d.id])
d.completedSetup = true
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
def childDevice = getChildDevice(d.deviceNetworkId)
updateBridgeStatus(childDevice)
childDevice.sendEvent(name: "idNumber", value: idNumber)
if (vbridge.value.ip && vbridge.value.port) {
if (vbridge.value.ip.contains(".")) {
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
} else {
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
}
} else {
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
}
d?.completedSetup = true
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
def childDevice = getChildDevice(d.deviceNetworkId)
updateBridgeStatus(childDevice)
childDevice.sendEvent(name: "idNumber", value: idNumber)
if (vbridge.value.ip && vbridge.value.port) {
if (vbridge.value.ip.contains(".")) {
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
} else {
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
}
} else {
log.error "Failed to create Hue Bridge device"
}
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
}
}
} else {
log.debug "found ${d.displayName} with id $selectedHue already exists"
@@ -1049,12 +1017,12 @@ def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'"
updateInProgress()
def value = [:]
def hue = null
def sat = null
def xy = null
// For now ignore model to get a consistent color if same color is set across multiple devices
// def model = state.bulbs[getId(childDevice)]?.modelid
if (huesettings.hex != null) {
@@ -1070,12 +1038,11 @@ def setColor(childDevice, huesettings) {
else
value.hue = Math.min(Math.round(childDevice.device?.currentValue("hue") * 65535 / 100), 65535)
if (huesettings.saturation != null)
if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
else
value.sat = Math.min(Math.round(childDevice.device?.currentValue("saturation") * 254 / 100), 254)
/* Disabled for now due to bad behavior via Lightning Wizard
if (!value.xy) {
// Below will translate values to hex->XY to take into account the color support of the different hue types
def hex = colorUtil.hslToHex((int) huesettings.hue, (int) huesettings.saturation)
@@ -1083,7 +1050,6 @@ def setColor(childDevice, huesettings) {
// Once groups, or scenes are introduced it might be a good idea to use unique models again
value.xy = calculateXY(hex)
}
*/
// Default behavior is to turn light on
value.on = true
@@ -1093,7 +1059,7 @@ def setColor(childDevice, huesettings) {
value.on = false
else if (huesettings.level == 1)
value.bri = 1
else
else
value.bri = Math.min(Math.round(huesettings.level * 254 / 100), 254)
}
value.alert = huesettings.alert ? huesettings.alert : "none"