Compare commits

..

28 Commits

Author SHA1 Message Date
Square Root of Saturn
73e73dc5a6 Modifying 'Octoblu' 2016-08-30 15:27:35 -05:00
Square Root of Saturn
156adc3b86 MSA-1451: This SmartApp allows for device creation and bridging of messages to and from Octoblu. Octoblu's IoT services are built on the open source Meshblu platform, a communications and management platform that supports a variety of protocols for physical devices to communicate seamlessly with each other, people, and web services. Through public, private, or hybrid clouds users can connect, design, process, and analyze the flow of information. All services have been designed through a robust security and right management architecture. 2016-08-23 18:24:32 -05:00
Vinay Rao
17014dd248 Merge branch 'staging'
# Conflicts:
#	devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy
#	devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy
#	devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy
2016-08-23 14:50:39 -07:00
Vinay Rao
555edf623a Merge pull request #1144 from SmartThingsCommunity/revert-1141-revert-dprot-2
DPROT-2 Reverting again to put the change back " Revert "Update DTHs to use ZigBee library ZoneStatus""
2016-08-23 14:05:48 -07:00
Vinay Rao
feb6ba0e24 Revert " Revert DPROT-2: "Update DTHs to use ZigBee library ZoneStatus"" 2016-08-23 14:04:47 -07:00
Vinay Rao
99d6e9f668 Merge pull request #1142 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-08-23 12:23:38 -07:00
Vinay Rao
a35f271a8e Merge pull request #1138 from workingmonk/bug/SSVD-2532
SSVD-2532 setting up the generic name at install
2016-08-23 11:34:46 -07:00
Vinay Rao
fd4969981f Merge pull request #1141 from varzac/revert-dprot-2
Revert DPROT-2: "Update DTHs to use ZigBee library ZoneStatus"
2016-08-23 11:27:23 -07:00
Jack Chi
1ec110155d Merge pull request #1137 from jackchi/healthcheck-osram
[CHF-239] Health Check Osram RGB / Dimmer Devices
2016-08-23 10:03:04 -07:00
Zach Varberg
79b90d741f Revert "Update DTHs to use ZigBee library ZoneStatus"
This reverts commit 7bfa0304af.
2016-08-23 08:45:47 -05:00
Vinay Rao
6009bc52ab SSVD-2532 setting up the generic name at install 2016-08-23 02:59:06 -07:00
Lars Finander
33a8fe108e Merge pull request #1133 from larsfinander/DVCSMP-1727_Philips_Hue_Update_status_response_staging
DVCSMP-1727 Philips Hue: Update status on response
2016-08-22 16:09:37 -06:00
jackchi
fadc496e1f [CHF-239] Health Check Osram RGB / Dimmer Devices 2016-08-22 15:06:57 -07:00
Vinay Rao
910694f1d1 Merge pull request #1136 from jackchi/revert-pr1424
Revert PR-1424
2016-08-22 14:08:58 -07:00
jackchi
fa9ebed998 Revert PR-1424 2016-08-22 13:58:11 -07:00
Jack Chi
0a2f2bffc2 Merge pull request #1131 from jackchi/healthcheck-core
[CHF-201] [CHF-206] Health Check Core Devices
2016-08-22 11:14:12 -07:00
jackchi
8dc36eb8f6 [CHF-201] [CHF-206] Health Check Core Devices 2016-08-22 11:05:24 -07:00
Lars Finander
df6646103a DVCSMP-1727 Philips Hue: Update status on response
-Only send events when feedback is recevied from bridge
-Cleaned up color conversions
-DVCSMP-1728 Philips Hue: Handle new LAN polling
-DVCSMP-1763 Philips Hue: Bad rounding can cause level to be 0, light on
-DVCSMP-1678 Philips Hue: Bridge detail page should display the status.
-DVCSMP-1669 Phillips Hue: The on/off button flickering between states
-DEVC-450 Hue Connect app is throwing errors in the IDE
-Changed manual refresh to check bridge and run poll()
-Updated some strings
2016-08-19 14:58:44 -06:00
Vinay Rao
014affe1ea Merge pull request #1130 from workingmonk/cccTile
SSVD-2465 changing the kelvin tile to name tiles for OSRAM
2016-08-18 16:17:48 -07:00
Vinay Rao
53fc948b00 SSVD-2465 changing the kelvin tile to name tiles 2016-08-18 15:04:48 -07:00
Vinay Rao
f389e925d2 Merge pull request #1129 from workingmonk/vdfixosram
SSVD-2463 hotfix for level jumping issues
2016-08-17 21:59:52 -07:00
Vinay Rao
ef47ec9393 hotfix for level jumping issues 2016-08-17 18:21:54 -07:00
Vinay Rao
62d800e99a Merge pull request #1128 from larsfinander/SSVD-2465_production
SSVD-2465 Philips Hue No description for slider
2016-08-17 15:08:18 -07:00
Lars Finander
a79c9c1ade SVD-2465 Philips Hue No description for slider
-Changed Kelvin tile to Whites (same as Philips Hue app)
2016-08-17 15:56:31 -06:00
Vinay Rao
4505ca85b2 Merge pull request #1126 from SmartThingsCommunity/master
Rolling up master to staging
2016-08-16 16:11:09 -07:00
Vinay Rao
3c0c050b3a Merge pull request #1125 from SmartThingsCommunity/staging
Rolling up staging to production for deployment
2016-08-16 14:45:48 -07:00
Vinay Rao
a1b375c929 Merge pull request #1111 from SmartThingsCommunity/master
Rolling up master to staging
2016-08-09 14:55:52 -07:00
Vinay Rao
4f97d1a3ef Merge pull request #1109 from SmartThingsCommunity/staging
Rolling up staging to production for deploy
2016-08-09 14:30:15 -07:00
21 changed files with 1940 additions and 529 deletions

View File

@@ -22,7 +22,6 @@ metadata {
capability "Configuration"
capability "Sensor"
capability "Battery"
capability "Health Check"
attribute "tamper", "enum", ["detected", "clear"]
attribute "batteryStatus", "string"
@@ -328,9 +327,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
}
def configure() {
// allow device user configured or default 16 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2* (timeOptionValueMap[reportInterval] ?: (2*8*60)), displayed: false)
// This sensor joins as a secure device if you double-click the button to include it
log.debug "${device.displayName} is configuring its settings"
def request = []

View File

@@ -20,7 +20,6 @@ metadata {
capability "Configuration"
capability "Sensor"
capability "Battery"
capability "Health Check"
command "configureAfterSecure"
@@ -248,8 +247,6 @@ def configureAfterSecure() {
def configure() {
// log.debug "configure()"
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
// allow device 16 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2*8*60, displayed: false)
}
private setConfigured() {

View File

@@ -20,7 +20,6 @@ metadata {
capability "Illuminance Measurement"
capability "Sensor"
capability "Battery"
capability "Health Check"
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
}
@@ -181,9 +180,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
}
def configure() {
// allow device 10 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2*5*60, displayed: false)
delayBetween([
// send binary sensor report instead of basic set for motion
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),

View File

@@ -23,6 +23,7 @@ metadata {
capability "Refresh"
capability "Switch"
capability "Switch Level"
capability "Health Check"
fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019"
}
@@ -66,6 +67,12 @@ def parse(String description) {
def resultMap = zigbee.getEvent(description)
if (resultMap) {
sendEvent(resultMap)
// 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()
}
else {
log.debug "DID NOT PARSE MESSAGE for description : $description"
@@ -85,6 +92,21 @@ def setLevel(value) {
zigbee.setLevel(value) + ["delay 500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
}
/**
* 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.levelRefresh()
} 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.onOffConfig() + zigbee.levelConfig()
}
@@ -95,5 +117,6 @@ def poll() {
def configure() {
log.debug "Configuring Reporting and Bindings."
sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
}

View File

@@ -43,7 +43,7 @@ metadata {
}
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
state "default", label:"Reset To White", action:"reset", icon:"st.lights.philips.hue-single"
}
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
@@ -51,7 +51,7 @@ metadata {
}
main(["rich-control"])
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
details(["rich-control", "reset", "refresh"])
}
}
@@ -75,118 +75,78 @@ def parse(description) {
// handle commands
void on() {
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
void off() {
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
void nextLevel() {
def level = device.latestValue("level") as Integer ?: 0
if (level <= 100) {
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
}
else {
level = 25
}
setLevel(level)
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
if (verifyPercent(percent)) {
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
sendEvent(name: "switch", value: "on")
log.trace parent.setLevel(this, percent)
}
}
void setSaturation(percent) {
log.debug "Executing 'setSaturation'"
if (verifyPercent(percent)) {
parent.setSaturation(this, percent)
sendEvent(name: "saturation", value: percent, displayed: false)
log.trace parent.setSaturation(this, percent)
}
}
void setHue(percent) {
log.debug "Executing 'setHue'"
if (verifyPercent(percent)) {
parent.setHue(this, percent)
sendEvent(name: "hue", value: percent, displayed: false)
log.trace parent.setHue(this, percent)
}
}
void setColor(value) {
log.debug "setColor: ${value}, $this"
def events = []
def validValues = [:]
if (verifyPercent(value.hue)) {
events << createEvent(name: "hue", value: value.hue, displayed: false)
validValues.hue = value.hue
}
if (verifyPercent(value.saturation)) {
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
validValues.saturation = value.saturation
}
if (value.hex != null) {
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
events << createEvent(name: "color", value: value.hex)
validValues.hex = value.hex
} else {
log.warn "$value.hex is not a valid color"
}
}
if (verifyPercent(value.level)) {
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
validValues.level = value.level
}
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
events << createEvent(name: "switch", value: "off")
validValues.switch = "off"
} else {
events << createEvent(name: "switch", value: "on")
validValues.switch = "on"
}
if (!events.isEmpty()) {
parent.setColor(this, validValues)
}
events.each {
sendEvent(it)
if (!validValues.isEmpty()) {
log.trace parent.setColor(this, validValues)
}
}
void reset() {
log.debug "Executing 'reset'"
def value = [level:100, saturation:56, hue:23]
setAdjustedColor(value)
parent.poll()
def value = [hue:20, saturation:2]
setAdjustedColor(value)
}
void setAdjustedColor(value) {
if (value) {
log.trace "setAdjustedColor: ${value}"
def adjusted = value + [:]
adjusted.hue = adjustOutgoingHue(value.hue)
// Needed because color picker always sends 100
adjusted.level = null
setColor(adjusted)
setColor(adjusted)
} else {
log.warn "Invalid color input"
}
}
void setColorTemperature(value) {
if (value) {
log.trace "setColorTemperature: ${value}k"
parent.setColorTemperature(this, value)
sendEvent(name: "colorTemperature", value: value)
sendEvent(name: "switch", value: "on")
} else {
log.warn "Invalid color temperature"
log.warn "Invalid color input $value"
}
}
@@ -195,22 +155,6 @@ void refresh() {
parent.manualRefresh()
}
def adjustOutgoingHue(percent) {
def adjusted = percent
if (percent > 31) {
if (percent < 63.0) {
adjusted = percent + (7 * (percent -30 ) / 32)
}
else if (percent < 73.0) {
adjusted = 69 + (5 * (percent - 62) / 10)
}
else {
adjusted = percent + (2 * (100 - percent) / 28)
}
}
log.info "percent: $percent, adjusted: $adjusted"
adjusted
}
def verifyPercent(percent) {
if (percent == null)

View File

@@ -7,8 +7,13 @@
metadata {
// Automatically generated. Make future change here.
definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") {
attribute "serialNumber", "string"
attribute "networkAddress", "string"
// Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network
// Possible values "Online" or "Offline"
attribute "status", "string"
// Id is the number on the back of the hub, Hue uses last six digits of Mac address
// This is also used in the Hue application as ID
attribute "idNumber", "string"
}
simulator {
@@ -17,22 +22,23 @@ metadata {
tiles(scale: 2) {
multiAttributeTile(name:"rich-control"){
tileAttribute ("", key: "PRIMARY_CONTROL") {
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
attributeState "Offline", label: '${currentValue}', action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#ffffff"
attributeState "Online", label: '${currentValue}', action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#79b821"
}
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
attributeState "default", label:'SN: ${currentValue}'
}
valueTile("doNotRemove", "v", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
state "default", label:'Do not remove'
}
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
state "default", label:'SN: ${currentValue}'
valueTile("idNumber", "device.idNumber", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
state "default", label:'ID: ${currentValue}'
}
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
state "default", label:'IP: ${currentValue}'
}
main (["rich-control"])
details(["rich-control", "networkAddress"])
details(["rich-control", "idNumber", "networkAddress", "doNotRemove"])
}
}

View File

@@ -43,16 +43,16 @@ metadata {
}
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: 'WHITES'
}
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
state "default", label:"Reset To White", action:"reset", icon:"st.lights.philips.hue-single"
}
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
@@ -84,118 +84,86 @@ def parse(description) {
// handle commands
void on() {
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
void off() {
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
void nextLevel() {
def level = device.latestValue("level") as Integer ?: 0
if (level <= 100) {
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
}
else {
level = 25
}
setLevel(level)
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
if (verifyPercent(percent)) {
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
sendEvent(name: "switch", value: "on")
log.trace parent.setLevel(this, percent)
}
}
void setSaturation(percent) {
log.debug "Executing 'setSaturation'"
if (verifyPercent(percent)) {
parent.setSaturation(this, percent)
sendEvent(name: "saturation", value: percent, displayed: false)
log.trace parent.setSaturation(this, percent)
}
}
void setHue(percent) {
log.debug "Executing 'setHue'"
if (verifyPercent(percent)) {
parent.setHue(this, percent)
sendEvent(name: "hue", value: percent, displayed: false)
log.trace parent.setHue(this, percent)
}
}
void setColor(value) {
log.debug "setColor: ${value}, $this"
def events = []
def validValues = [:]
if (verifyPercent(value.hue)) {
events << createEvent(name: "hue", value: value.hue, displayed: false)
validValues.hue = value.hue
}
if (verifyPercent(value.saturation)) {
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
validValues.saturation = value.saturation
}
if (value.hex != null) {
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
events << createEvent(name: "color", value: value.hex)
validValues.hex = value.hex
} else {
log.warn "$value.hex is not a valid color"
}
}
if (verifyPercent(value.level)) {
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
validValues.level = value.level
}
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
events << createEvent(name: "switch", value: "off")
validValues.switch = "off"
} else {
events << createEvent(name: "switch", value: "on")
validValues.switch = "on"
}
if (!events.isEmpty()) {
parent.setColor(this, validValues)
}
events.each {
sendEvent(it)
if (!validValues.isEmpty()) {
log.trace parent.setColor(this, validValues)
}
}
void reset() {
log.debug "Executing 'reset'"
def value = [level:100, saturation:56, hue:23]
setAdjustedColor(value)
parent.poll()
setColorTemperature(4000)
}
void setAdjustedColor(value) {
if (value) {
log.trace "setAdjustedColor: ${value}"
def adjusted = value + [:]
adjusted.hue = adjustOutgoingHue(value.hue)
// Needed because color picker always sends 100
adjusted.level = null
setColor(adjusted)
setColor(adjusted)
} else {
log.warn "Invalid color input"
log.warn "Invalid color input $value"
}
}
void setColorTemperature(value) {
if (value) {
log.trace "setColorTemperature: ${value}k"
parent.setColorTemperature(this, value)
sendEvent(name: "colorTemperature", value: value)
sendEvent(name: "switch", value: "on")
log.trace parent.setColorTemperature(this, value)
} else {
log.warn "Invalid color temperature"
log.warn "Invalid color temperature $value"
}
}
@@ -204,23 +172,6 @@ void refresh() {
parent.manualRefresh()
}
def adjustOutgoingHue(percent) {
def adjusted = percent
if (percent > 31) {
if (percent < 63.0) {
adjusted = percent + (7 * (percent -30 ) / 32)
}
else if (percent < 73.0) {
adjusted = 69 + (5 * (percent - 62) / 10)
}
else {
adjusted = percent + (2 * (100 - percent) / 28)
}
}
log.info "percent: $percent, adjusted: $adjusted"
adjusted
}
def verifyPercent(percent) {
if (percent == null)
return false

View File

@@ -68,20 +68,16 @@ def parse(description) {
// handle commands
void on() {
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
void off() {
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
if (percent != null && percent >= 0 && percent <= 100) {
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent)
sendEvent(name: "switch", value: "on")
} else {
log.warn "$percent is not 0-100"
}

View File

@@ -36,12 +36,12 @@ metadata {
}
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2200..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
state "colorTemperature", label: 'WHITES'
}
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
@@ -73,20 +73,16 @@ def parse(description) {
// handle commands
void on() {
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
void off() {
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
if (percent != null && percent >= 0 && percent <= 100) {
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent)
sendEvent(name: "switch", value: "on")
log.trace parent.setLevel(this, percent)
} else {
log.warn "$percent is not 0-100"
}
@@ -95,9 +91,7 @@ void setLevel(percent) {
void setColorTemperature(value) {
if (value) {
log.trace "setColorTemperature: ${value}k"
parent.setColorTemperature(this, value)
sendEvent(name: "colorTemperature", value: value)
sendEvent(name: "switch", value: "on")
log.trace parent.setColorTemperature(this, value)
} else {
log.warn "Invalid color temperature"
}
@@ -107,4 +101,3 @@ void refresh() {
log.debug "Executing 'refresh'"
parent.manualRefresh()
}

View File

@@ -102,11 +102,11 @@ def parse(String description) {
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off'
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true)
// Temporary fix for the case when Device is OFFLINE and is connected again
if (state.lastOnOff == null){
state.lastOnOff = now()
sendEvent(name: "deviceWatch-lastActivity", value: state.lastOnOff, description: "Last Activity is on ${new Date(state.lastOnOff)}", displayed: false, isStateChange: true)
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.lastOnOff = now()
state.lastActivity = now()
}
}
else {
@@ -123,18 +123,17 @@ def on() {
zigbee.on()
}
/**
* PING is used by Device-Watch in attempt to reach the Outlet
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
// send read attribute onOFF if the last time we heard from the outlet is outside of the checkInterval
if (state.lastOnOff < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastOnOff=${new Date(state.lastOnOff)}"
state.lastOnOff = null
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
state.lastActivity = null
return zigbee.onOffRefresh()
} else { // if the last onOff activity is within the checkInterval we artificially create a Device-Watch event
log.info "ping, alive=yes, lastOnOff=${new Date(state.lastOnOff)}"
sendEvent(name: "deviceWatch-lastActivity", value: state.lastOnOff, description: "Last Activity is on ${new Date(state.lastOnOff)}", displayed: false, isStateChange: true)
} 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)
}
}
@@ -143,7 +142,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 1200, displayed: false)
sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
zigbee.onOffConfig() + powerConfig() + refresh()
}

View File

@@ -101,6 +101,13 @@ def parse(String description) {
map = parseIasMessage(description)
}
// 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()
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
@@ -271,6 +278,21 @@ private Map getMoistureResult(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.readAttribute(0x001, 0x0020) // Read the Battery Level
} 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() {
log.debug "Refreshing Temperature and Battery"
def refreshCmds = [
@@ -282,7 +304,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
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

@@ -105,6 +105,13 @@ def parse(String description) {
map = parseIasMessage(description)
}
// 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()
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
@@ -282,6 +289,21 @@ private Map getMotionResult(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.readAttribute(0x001, 0x0020) // Read the Battery Level
} 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() {
log.debug "refresh called"
def refreshCmds = [
@@ -293,7 +315,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
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

@@ -127,6 +127,13 @@ def parse(String description) {
map = parseIasMessage(description)
}
// 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()
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
@@ -228,9 +235,9 @@ private Map parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
Map resultMap = [:]
if(garageSensor != "Yes") {
if (garageSensor != "Yes"){
resultMap = zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
}
}
return resultMap
}
@@ -364,6 +371,21 @@ private getAccelerationResult(numValue) {
]
}
/**
* 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.readAttribute(0x001, 0x0020) // Read the Battery Level
} 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() {
log.debug "Refreshing Values "
@@ -391,7 +413,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
log.debug "Configuring Reporting"

View File

@@ -92,6 +92,13 @@ def parse(String description) {
map = parseIasMessage(description)
}
// 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()
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
@@ -234,6 +241,21 @@ private Map getContactResult(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.readAttribute(0x001, 0x0020) // Read the Battery Level
} 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() {
log.debug "Refreshing Temperature and Battery"
def refreshCmds = [
@@ -245,7 +267,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
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

@@ -83,6 +83,13 @@ def parse(String description) {
map = parseCustomMessage(description)
}
// 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()
log.debug "Parse returned $map"
return map ? createEvent(map) : null
}
@@ -239,6 +246,20 @@ private Map getHumidityResult(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.readAttribute(0x001, 0x0020) // Read the Battery Level
} 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()
{
log.debug "refresh temperature, humidity, and battery"
@@ -254,7 +275,7 @@ def refresh()
}
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
sendEvent(name: "checkInterval", value: 7200, displayed: false, data: [protocol: "zigbee"])
log.debug "Configuring Reporting and Bindings."
def configCmds = [

View File

@@ -19,6 +19,7 @@ metadata {
capability "Refresh"
capability "Switch"
capability "Switch Level"
capability "Health Check"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
@@ -53,7 +54,16 @@ def parse(String description) {
def event = zigbee.getEvent(description)
if (event) {
sendEvent(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 {
sendEvent(event)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
@@ -72,6 +82,20 @@ def on() {
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.onOffConfig() + zigbee.levelConfig()
@@ -79,5 +103,7 @@ def refresh() {
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.onOffRefresh() + zigbee.levelRefresh()
}

View File

@@ -27,6 +27,10 @@ metadata {
capability "Refresh"
capability "Switch"
capability "Switch Level"
capability "Health Check"
attribute "colorName", "string"
command "setGenericName"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
@@ -54,15 +58,15 @@ metadata {
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorName", label: '${currentValue}'
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
details(["switch", "colorTempSliderControl", "colorName", "refresh"])
}
}
@@ -78,10 +82,22 @@ private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getEvent(description)
if (finalResult) {
log.debug finalResult
sendEvent(finalResult)
def event = zigbee.getEvent(description)
if (event) {
log.debug 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") {
setGenericName(event.value)
}
sendEvent(event)
}
}
else {
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
@@ -110,20 +126,54 @@ def on() {
def off() {
zigbee.off()
}
/**
* 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.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
zigbee.onOffRefresh() + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
}
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.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
}
def setColorTemperature(value) {
setGenericName(value)
zigbee.setColorTemperature(value)
}
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
def setGenericName(value){
if (value != null) {
def genericName = "White"
if (value < 3300) {
genericName = "Soft White"
} else if (value < 4150) {
genericName = "Moonlight"
} else if (value <= 5000) {
genericName = "Cool White"
} else if (value >= 5000) {
genericName = "Daylight"
}
sendEvent(name: "colorName", value: genericName)
}
}
def setLevel(value) {
zigbee.setLevel(value)
}

View File

@@ -49,9 +49,6 @@ metadata {
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
attributeState "colorName", label:'${currentValue}'
}
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
@@ -61,12 +58,12 @@ metadata {
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorName", label: '${currentValue}'
}
main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
details(["switch", "colorTempSliderControl", "colorName", "refresh"])
}
}
@@ -75,7 +72,13 @@ def parse(String description) {
log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) {
sendEvent(event)
if (event.name=="level" && event.value==0) {}
else {
if (event.name=="colorTemperature") {
setGenericName(event.value)
}
sendEvent(event)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -1,146 +0,0 @@
definition(
name: "Door & Lock Manager",
namespace: "airoscar",
author: "Oscar Chen",
description: "Manages door and lock behaviors. Send push notification or text message if the door is left open or if the lock is left unlocked for a preset amount of time; as well as automatically locking the lock with a preset delay after the door has been closed.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/bon-voyage.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/bon-voyage%402x.png"
)
preferences {
section("Monitor this door") {
input "contact", "capability.contactSensor"
}
section ("Monitor this lock") {
input "lock", "capability.lock"
}
section("And notify me if it's open for more than this many minutes (default: 3)") {
input "openThreshold", "number", description: "Number of minutes", required: false
}
section("Delay between notifications (default 10 minutes") {
input "frequency", "number", title: "Number of minutes", description: "", required: false
}
section("Automatically lock this door after this many minutes (default: 0.5)") {
input "lockDelay", "number", title: "Number of minutes", required: false
}
section("Via text message at this number (or via push notification if not specified") {
input("recipients", "contact", title: "Send notifications to") {
input "phone", "phone", title: "Phone number (optional)", required: false
}
}
}
def installed() {
log.trace "installed()"
subscribe()
}
def updated() {
log.trace "updated()"
unsubscribe()
subscribe()
}
def subscribe() {
subscribe(contact, "contact", doorHandler)
subscribe(lock, "lock", lockHandler)
}
def lockHandler(evt)
{
log.trace "LockHandler($evt.name: $evt.value)"
def t0 = now()
def delay = (openThreshold != null && openThreshold != "") ? openThreshold * 60 : 180 //unlocked notification delay
runIn(delay, doorOpenTooLong, [overwrite: false])
log.debug "scheduled doorOpenTooLong in ${now() - t0} msec"
}
def doorHandler(evt)
{
log.trace "doorHandler($evt.name: $evt.value)"
def t0 = now()
def delay = (openThreshold != null && openThreshold != "") ? openThreshold * 60 : 180 //door open notification delay
def delay2 = (lockDelay != null && lockDelay != "") ? lockDelay * 60 : 30 // auto lock delay
runIn(delay2, autoLock, [overwrite: false])
runIn(delay, doorOpenTooLong, [overwrite: false])
log.debug "scheduled doorOpenTooLong in ${now() - t0} msec"
}
def autoLock() {
def contactState = contact.currentState("contact")
def lockState = lock.currentState("lock")
def elapsed = now() - contactState.rawDateCreated.time
def autoLockDelay = ((delay2 != null && delay2 != "") ? delay2 * 60000 : 60000) - 1000
if (elapsed >= threshold) {
if (contactState.value == "closed" && lockState.value == "unlocked") {
lock.lock()
}
}
}
def doorOpenTooLong() {
def contactState = contact.currentState("contact")
def lockState = lock.currentState("lock")
def freq = (frequency != null && frequency != "") ? frequency * 60 : 600 //notification frequency
if (contactState.value == "open") {
def elapsed = now() - contactState.rawDateCreated.time
def threshold = ((openThreshold != null && openThreshold != "") ? openThreshold * 60000 : 60000) - 1000
if (elapsed >= threshold) {
log.debug "contact has stayed open long enough since last check ($elapsed ms): calling sendMessage()"
sendMessage()
runIn(freq, doorOpenTooLong, [overwrite: false])
} else {
log.debug "contact has not stayed open long enough since last check ($elapsed ms): doing nothing"
}
} else if (lockState.value =="unlocked") {
def elapsed = now() - contactState.rawDateCreated.time
def threshold = ((openThreshold != null && openThreshold != "") ? openThreshold * 60000 : 60000) - 1000
if (elapsed >= threshold) {
log.debug "lock has stayed unlocked long enough since last check ($elapsed ms): calling sendMessage()"
sendMessage()
runIn(freq, doorOpenTooLong, [overwrite: false])
} else {
log.debug "lock has not stayed unlocked long enough since last check ($elapsed ms): doing nothing"
}
} else {
log.warn "doing nothing"
}
}
void sendMessage()
{
def minutes = (openThreshold != null && openThreshold != "") ? openThreshold : 3
def msg = ""
if (contact.currentState("contact").value == "open") {
msg = "${contact.displayName} has been left open for ${minutes} minutes."
} else if (lock.currentState("lock").value == "unlocked") {
msg = "${contact.displayName} is closed but ${lock.displayName} has been left unlocked for ${minutes} minutes."
} else {
msg = "No message"
}
log.info msg
if (location.contactBookEnabled) {
sendNotificationToContacts(msg, recipients)
}
else {
if (phone) {
sendSms phone, msg
} else {
sendPush msg
}
}
}

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

File diff suppressed because it is too large Load Diff