mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 13:21:53 +00:00
Compare commits
1 Commits
MSA-2122-1
...
MSA-2033-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30eafb604d |
18
build.gradle
18
build.gradle
@@ -9,7 +9,7 @@ apply plugin: 'smartthings-slack'
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.12"
|
||||
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.11"
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@@ -19,7 +19,7 @@ buildscript {
|
||||
username smartThingsArtifactoryUserName
|
||||
password smartThingsArtifactoryPassword
|
||||
}
|
||||
url "https://smartthings.jfrog.io/smartthings/libs-release-local"
|
||||
url "https://artifactory.smartthings.com/libs-release-local"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ repositories {
|
||||
username smartThingsArtifactoryUserName
|
||||
password smartThingsArtifactoryPassword
|
||||
}
|
||||
url "https://smartthings.jfrog.io/smartthings/libs-release-local"
|
||||
url "https://artifactory.smartthings.com/libs-release-local"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ sourceSets {
|
||||
|
||||
dependencies {
|
||||
devicetypesCompile 'org.codehaus.groovy:groovy-all:2.4.7'
|
||||
devicetypesCompile 'smartthings:appengine-z-wave:0.1.3'
|
||||
devicetypesCompile 'smartthings:appengine-zigbee:0.1.12'
|
||||
devicetypesCompile 'smartthings:appengine-z-wave:0.1.2'
|
||||
devicetypesCompile 'smartthings:appengine-zigbee:0.1.11'
|
||||
smartappsCompile 'org.codehaus.groovy:groovy-all:2.4.7'
|
||||
smartappsCompile 'smartthings:appengine-common:0.1.9'
|
||||
smartappsCompile 'smartthings:appengine-common:0.1.8'
|
||||
smartappsCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1'
|
||||
smartappsCompile 'org.grails:grails-web:2.3.11'
|
||||
smartappsCompile 'org.json:json:20140107'
|
||||
@@ -74,19 +74,19 @@ slackSendMessage {
|
||||
String username
|
||||
switch (branch) {
|
||||
case 'master':
|
||||
username = 'DEV'
|
||||
username = 'Hickory'
|
||||
iconUrl = wolverine
|
||||
color = '#35D0F2'
|
||||
messageText = 'Began deployment of _SmartThingsPublic[master]_ branch to the _Dev_ environments.'
|
||||
break
|
||||
case 'staging':
|
||||
username = 'STG'
|
||||
username = 'Dickory'
|
||||
iconUrl = beach
|
||||
color = '#FFDE20'
|
||||
messageText = 'Began deployment of _SmartThingsPublic[staging]_ branch to the _Staging_ environments.'
|
||||
break
|
||||
case 'production':
|
||||
username = 'PRD'
|
||||
username = 'Dock'
|
||||
iconUrl = drinks
|
||||
color = '#FF1D23'
|
||||
messageText = 'Began deployment of _SmartThingsPublic[production]_ branch to the _Prod_ environments.'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Spruce Sensor -updated with SLP model number 5/2017
|
||||
* Spruce Sensor -Pre-release V2 10/8/2015
|
||||
*
|
||||
* Copyright 2014 Plaid Systems
|
||||
*
|
||||
@@ -14,33 +14,25 @@
|
||||
*
|
||||
-------10/20/2015 Updates--------
|
||||
-Fix/add battery reporting interval to update
|
||||
-remove polling and/or refresh
|
||||
|
||||
-------5/2017 Updates--------
|
||||
-Add fingerprints for SLP
|
||||
-add device health, check every 60mins + 2mins
|
||||
-remove polling and/or refresh(?)
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "Plaid Systems") {
|
||||
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "NCauffman") {
|
||||
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
//capability "Polling"
|
||||
|
||||
attribute "maxHum", "string"
|
||||
attribute "minHum", "string"
|
||||
|
||||
|
||||
command "resetHumidity"
|
||||
command "refresh"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01", deviceJoinName: "Spruce Sensor"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP1", deviceJoinName: "Spruce Sensor"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01"
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -301,11 +293,6 @@ def setConfig(){
|
||||
sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration initialized")
|
||||
}
|
||||
|
||||
def installed(){
|
||||
//check every 1 hour + 2mins
|
||||
sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
//when device preferences are changed
|
||||
def updated(){
|
||||
log.debug "device updated"
|
||||
@@ -316,8 +303,6 @@ def updated(){
|
||||
sendEvent(name: 'configuration',value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${interval} mins")
|
||||
}
|
||||
}
|
||||
//check every 1 hour + 2mins
|
||||
sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
//poll
|
||||
@@ -410,4 +395,4 @@ private byte[] reverseArray(byte[] array) {
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -1,34 +0,0 @@
|
||||
# Aeon Labs Key Fob
|
||||
|
||||
Cloud Execution
|
||||
|
||||
Works with:
|
||||
|
||||
* [Aeon Labs Key Fob](http://aeotec.com/z-wave-key-fob-remote-control)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents device has commands
|
||||
* **Button** - represents a device with one or more buttons
|
||||
* **Holdable Button** - represents a device with one or more holdable buttons
|
||||
* **Configuration** - allows for configuration of devices
|
||||
* **Sensor** - detects sensor events
|
||||
* **Battery** - defines device uses a battery
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
Aeon Key Fob is a ZWave totally sleepy device and is marked offline only in the case when Hub is offline.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range.
|
||||
Pairing needs to be tried again by placing the sensor closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the Aeon Labs Key Fob from SmartThings can be found in the following link:
|
||||
* [Aeotec Key Fob Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202294120-Aeon-Labs-Key-Fob)
|
||||
@@ -1,4 +1,3 @@
|
||||
import groovy.json.JsonOutput
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
@@ -20,7 +19,6 @@ metadata {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x80,0x84,0x85"
|
||||
fingerprint mfr: "0086", prod: "0001", model: "0026", deviceJoinName: "Aeon Panic Button"
|
||||
@@ -133,9 +131,6 @@ def updated() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// Arrival sensors only goes OFFLINE when Hub is off
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
|
||||
|
||||
def zwMap = getZwaveInfo()
|
||||
def buttons = 4 // Default for Key Fob
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -1,33 +0,0 @@
|
||||
# Aeon Minimote
|
||||
|
||||
Cloud Execution
|
||||
|
||||
Works with:
|
||||
|
||||
* [Aeotec Minimote](http://aeotec.com/small-z-wave-remote-control)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents device has commands
|
||||
* **Button** - represents a device with one or more buttons
|
||||
* **Holdable Button** - represents a device with one or more holdable buttons
|
||||
* **Configuration** - allows for configuration of devices
|
||||
* **Sensor** - detects sensor events
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
Aeon Minimote is a ZWave totally sleepy device and is marked offline only in the case when Hub is offline.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range.
|
||||
Pairing needs to be tried again by placing the sensor closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the Aeotec Minimote from SmartThings can be found in the following link:
|
||||
* [Aeotec Minimote Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202087904-Aeotec-Minimote)
|
||||
@@ -1,4 +1,3 @@
|
||||
import groovy.json.JsonOutput
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
@@ -19,7 +18,6 @@ metadata {
|
||||
capability "Holdable Button"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B", outClusters: "0x26,0x2B"
|
||||
fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B,0x85,0x84", outClusters: "0x26" // old style with numbered buttons
|
||||
@@ -121,7 +119,5 @@ def updated() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// Arrival sensors only goes OFFLINE when Hub is off
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
|
||||
sendEvent(name: "numberOfButtons", value: 4)
|
||||
}
|
||||
|
||||
@@ -27,9 +27,13 @@ Works with:
|
||||
## Device Health
|
||||
|
||||
Aeon Labs MultiSensor 6 is polled by the hub.
|
||||
Aeon MultiSensor reports in once every hour.
|
||||
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
|
||||
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
|
||||
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
|
||||
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
|
||||
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
|
||||
|
||||
* __122min__ checkInterval
|
||||
* __32min__ checkInterval
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -130,13 +130,13 @@ metadata {
|
||||
}
|
||||
|
||||
def installed(){
|
||||
// Device-Watch simply pings if no device events received for 122min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device-Watch simply pings if no device events received for 122min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ metadata {
|
||||
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
|
||||
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
|
||||
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
|
||||
state "auxheatonly", action:"switchMode", icon: "st.thermostat.emergency-heat"
|
||||
state "emergency heat", action:"switchMode", icon: "st.thermostat.emergency-heat" // emergency heat = auxHeatOnly
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
}
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
@@ -156,49 +156,47 @@ void poll() {
|
||||
def generateEvent(Map results) {
|
||||
log.debug "parsing data $results"
|
||||
if(results) {
|
||||
results.each { name, value ->
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
def supportedThermostatModes = ["off"]
|
||||
def thermostatMode = null
|
||||
|
||||
results.each { name, value ->
|
||||
def isChange = false
|
||||
def isDisplayed = true
|
||||
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name]
|
||||
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
|
||||
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
|
||||
event << [value: sendValue, unit: temperatureScale]
|
||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed]
|
||||
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
|
||||
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
|
||||
event << [value: sendValue, unit: temperatureScale, displayed: false]
|
||||
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||
if (value == true) {
|
||||
supportedThermostatModes << ((name == "auxHeatMode") ? "auxheatonly" : name - "Mode")
|
||||
}
|
||||
return // as we don't want to send this event here, proceed to next name/value pair
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||
} else if (name=="thermostatFanMode"){
|
||||
sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
|
||||
event << [value: value.toString(), data:[supportedThermostatFanModes: fanModes()]]
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||
} else if (name=="humidity") {
|
||||
event << [value: value.toString(), displayed: false, unit: "%"]
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"]
|
||||
} else if (name == "deviceAlive") {
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event['isStateChange'] = isChange
|
||||
event['displayed'] = false
|
||||
} else if (name == "thermostatMode") {
|
||||
thermostatMode = value.toLowerCase()
|
||||
return // as we don't want to send this event here, proceed to next name/value pair
|
||||
def mode = value.toString()
|
||||
mode = (mode == "auxHeatOnly") ? "emergency heat" : mode
|
||||
isChange = isStateChange(device, name, mode)
|
||||
event << [value: mode, isStateChange: isChange, displayed: isDisplayed]
|
||||
} else {
|
||||
event << [value: value.toString()]
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed]
|
||||
}
|
||||
sendEvent(event)
|
||||
}
|
||||
if (state.supportedThermostatModes != supportedThermostatModes) {
|
||||
state.supportedThermostatModes = supportedThermostatModes
|
||||
sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
|
||||
}
|
||||
if (thermostatMode) {
|
||||
sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText,
|
||||
descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
|
||||
}
|
||||
generateSetpointEvent ()
|
||||
generateStatusEvent ()
|
||||
}
|
||||
@@ -324,7 +322,15 @@ void resumeProgram() {
|
||||
}
|
||||
|
||||
def modes() {
|
||||
return state.supportedThermostatModes
|
||||
if (state.modes) {
|
||||
log.debug "Modes = ${state.modes}"
|
||||
return state.modes
|
||||
}
|
||||
else {
|
||||
state.modes = parent.availableModes(this)
|
||||
log.debug "Modes = ${state.modes}"
|
||||
return state.modes
|
||||
}
|
||||
}
|
||||
|
||||
def fanModes() {
|
||||
@@ -407,13 +413,11 @@ def setThermostatFanMode(String mode) {
|
||||
}
|
||||
|
||||
def generateModeEvent(mode) {
|
||||
sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: state.supportedThermostatModes],
|
||||
descriptionText: "$device.displayName is in ${mode} mode")
|
||||
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
|
||||
}
|
||||
|
||||
def generateFanModeEvent(fanMode) {
|
||||
sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: fanModes()],
|
||||
descriptionText: "$device.displayName fan is in ${fanMode} mode")
|
||||
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${fanMode} mode", displayed: true)
|
||||
}
|
||||
|
||||
def generateOperatingStateEvent(operatingState) {
|
||||
@@ -449,14 +453,14 @@ def heat() {
|
||||
}
|
||||
|
||||
def emergencyHeat() {
|
||||
auxheatonly()
|
||||
auxHeatOnly()
|
||||
}
|
||||
|
||||
def auxheatonly() {
|
||||
log.debug "auxheatonly()"
|
||||
def auxHeatOnly() {
|
||||
log.debug "auxHeatOnly = emergency heat"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode ("auxHeatOnly", deviceId))
|
||||
generateModeEvent("auxheatonly")
|
||||
generateModeEvent("emergency heat") // emergency heat = auxHeatOnly
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
@@ -589,7 +593,7 @@ def generateSetpointEvent() {
|
||||
} else if (mode == "off") {
|
||||
sendEvent("name":"thermostatSetpoint", "value":averageSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"displayThermostatSetpoint", "value":"Off", displayed: false)
|
||||
} else if (mode == "auxheatonly") {
|
||||
} else if (mode == "emergency heat") { // emergency heat = auxHeatOnly
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"displayThermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale, displayed: false)
|
||||
}
|
||||
@@ -628,7 +632,7 @@ void raiseSetpoint() {
|
||||
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5
|
||||
|
||||
if ((mode == "heat" || mode == "auxheatonly") && targetvalue > maxHeatingSetpoint) {
|
||||
if ((mode == "heat" || mode == "emergency heat") && targetvalue > maxHeatingSetpoint) { // emergency heat = auxHeatOnly
|
||||
targetvalue = maxHeatingSetpoint
|
||||
} else if (mode == "cool" && targetvalue > maxCoolingSetpoint) {
|
||||
targetvalue = maxCoolingSetpoint
|
||||
@@ -674,7 +678,7 @@ void lowerSetpoint() {
|
||||
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5
|
||||
|
||||
if ((mode == "heat" || mode == "auxheatonly") && targetvalue < minHeatingSetpoint) {
|
||||
if ((mode == "heat" || mode == "emergency heat") && targetvalue < minHeatingSetpoint) { // emergency heat = auxHeatOnly
|
||||
targetvalue = minHeatingSetpoint
|
||||
} else if (mode == "cool" && targetvalue < minCoolingSetpoint) {
|
||||
targetvalue = minCoolingSetpoint
|
||||
@@ -715,7 +719,7 @@ void alterSetpoint(temp) {
|
||||
}
|
||||
|
||||
//step1: check thermostatMode, enforce limits before sending request to cloud
|
||||
if (mode == "heat" || mode == "auxheatonly"){
|
||||
if (mode == "heat" || mode == "emergency heat"){ // emergency heat = auxHeatOnly
|
||||
if (temp.value > coolingSetpoint){
|
||||
targetHeatingSetpoint = temp.value
|
||||
targetCoolingSetpoint = temp.value
|
||||
@@ -750,7 +754,7 @@ void alterSetpoint(temp) {
|
||||
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||
} else {
|
||||
log.error "Error alterSetpoint()"
|
||||
if (mode == "heat" || mode == "auxheatonly"){
|
||||
if (mode == "heat" || mode == "emergency heat"){ // emergency heat = auxHeatOnly
|
||||
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
||||
sendEvent("name": "displayThermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
||||
} else if (mode == "cool") {
|
||||
@@ -779,7 +783,7 @@ def generateStatusEvent() {
|
||||
log.debug "Cooling set point = ${coolingSetpoint}"
|
||||
log.debug "HVAC Mode = ${mode}"
|
||||
|
||||
if (mode == "heat" || mode == "auxheatonly") {
|
||||
if (mode == "heat") {
|
||||
if (temperature >= heatingSetpoint) {
|
||||
statusText = "Right Now: Idle"
|
||||
} else {
|
||||
@@ -802,6 +806,8 @@ def generateStatusEvent() {
|
||||
}
|
||||
} else if (mode == "off") {
|
||||
statusText = "Right Now: Off"
|
||||
} else if (mode == "emergency heat") { // emergency heat = auxHeatOnly
|
||||
statusText = "Emergency Heat"
|
||||
} else {
|
||||
statusText = "?"
|
||||
}
|
||||
|
||||
@@ -300,21 +300,15 @@ def setColor(value) {
|
||||
value.hex = "#${hex(value.red)}${hex(value.green)}${hex(value.blue)}"
|
||||
}
|
||||
|
||||
if(value.hue) {
|
||||
sendEvent(name: "hue", value: value.hue, displayed: false)
|
||||
}
|
||||
if(value.saturation) {
|
||||
sendEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||
}
|
||||
if(value.hex?.trim()) {
|
||||
sendEvent(name: "color", value: value.hex, displayed: false)
|
||||
}
|
||||
if (value.level) {
|
||||
sendEvent(name: "level", value: value.level)
|
||||
}
|
||||
if (value.switch?.trim()) {
|
||||
sendEvent(name: "switch", value: value.switch)
|
||||
}
|
||||
sendEvent(name: "hue", value: value.hue, displayed: false)
|
||||
sendEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||
sendEvent(name: "color", value: value.hex, displayed: false)
|
||||
if (value.level) {
|
||||
sendEvent(name: "level", value: value.level)
|
||||
}
|
||||
if (value.switch) {
|
||||
sendEvent(name: "switch", value: value.switch)
|
||||
}
|
||||
|
||||
sendRGB(value.rh, value.gh, value.bh)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ metadata {
|
||||
|
||||
// tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.valve", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing"
|
||||
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"opening"
|
||||
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC"
|
||||
@@ -48,16 +48,14 @@ metadata {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "contact"
|
||||
details(["contact","refresh"])
|
||||
main "valve"
|
||||
details(["valve","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
def installed(){
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def updated(){
|
||||
@@ -87,17 +85,11 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
def open() {
|
||||
delayBetween([
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
], 500)
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format()
|
||||
}
|
||||
|
||||
def close() {
|
||||
delayBetween([
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
], 500)
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,6 +105,6 @@ def refresh() {
|
||||
|
||||
def createEventWithDebug(eventMap) {
|
||||
def event = createEvent(eventMap)
|
||||
log.debug "Event created with ${event?.name}:${event?.value} - ${event?.descriptionText}"
|
||||
log.debug "Event created with ${event?.descriptionText}"
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
capability "Light"
|
||||
capability "Outlet"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"
|
||||
|
||||
@@ -105,8 +105,6 @@ def parse(String description) {
|
||||
} else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}"
|
||||
}
|
||||
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
|
||||
map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
|
||||
}
|
||||
}
|
||||
} else if (map.name == "temperature") {
|
||||
@@ -131,10 +129,6 @@ def parse(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
translateZoneStatus(zs)
|
||||
}
|
||||
|
||||
private Map translateZoneStatus(ZoneStatus zs) {
|
||||
return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry')
|
||||
}
|
||||
|
||||
@@ -203,8 +197,7 @@ def ping() {
|
||||
def refresh() {
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
|
||||
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
|
||||
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS)
|
||||
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020)
|
||||
|
||||
return refreshCmds + zigbee.enrollResponse()
|
||||
}
|
||||
|
||||
@@ -111,8 +111,6 @@ def parse(String description) {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
log.debug "Doing a read attr motion event"
|
||||
map = getMotionResult(value)
|
||||
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
|
||||
map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
|
||||
}
|
||||
}
|
||||
} else if (map.name == "temperature") {
|
||||
@@ -137,10 +135,6 @@ def parse(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
translateZoneStatus(zs)
|
||||
}
|
||||
|
||||
private Map translateZoneStatus(ZoneStatus zs) {
|
||||
// Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion
|
||||
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
|
||||
}
|
||||
@@ -171,8 +165,8 @@ private Map getBatteryResult(rawValue) {
|
||||
def pct = batteryMap[volts]
|
||||
result.value = pct
|
||||
} else {
|
||||
def minVolts = 2.4
|
||||
def maxVolts = 2.7
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
@@ -206,8 +200,7 @@ def refresh() {
|
||||
log.debug "refresh called"
|
||||
|
||||
def refreshCmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
|
||||
zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
|
||||
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS)
|
||||
zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000)
|
||||
|
||||
return refreshCmds + zigbee.enrollResponse()
|
||||
}
|
||||
|
||||
@@ -134,9 +134,8 @@ def parse(String description) {
|
||||
} else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}"
|
||||
}
|
||||
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
|
||||
maps += translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
|
||||
} else {
|
||||
|
||||
maps += handleAcceleration(descMap)
|
||||
}
|
||||
}
|
||||
@@ -230,11 +229,6 @@ private List<Map> parseAxis(List<Map> attrData) {
|
||||
|
||||
private List<Map> parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
translateZoneStatus(zs)
|
||||
}
|
||||
|
||||
private List<Map> translateZoneStatus(ZoneStatus zs) {
|
||||
List<Map> results = []
|
||||
|
||||
if (garageSensor != "Yes") {
|
||||
@@ -274,7 +268,7 @@ private Map getBatteryResult(rawValue) {
|
||||
result.value = pct
|
||||
} else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 2.7
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
@@ -319,7 +313,7 @@ def refresh() {
|
||||
def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
|
||||
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
|
||||
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) +
|
||||
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.enrollResponse()
|
||||
zigbee.enrollResponse()
|
||||
|
||||
return refreshCmds
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -1,42 +0,0 @@
|
||||
# ZigBee Button
|
||||
|
||||
Cloud Execution
|
||||
|
||||
Works with:
|
||||
|
||||
* [OSRAM LIGHTIFY Dimming Switch](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch)
|
||||
* [Iris Smart Button](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button)
|
||||
* [Iris KeyFob](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - It represents that a device has commands.
|
||||
* **Battery** - It defines that the device has a battery
|
||||
* **Button** - It defines that a device has one or more buttons
|
||||
* **Holdable Button** - It defines that a device has one or more holdable buttons
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Sensor** - it represents that a Device has attributes.
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
|
||||
## Device Health
|
||||
|
||||
ZigBee Button is marked offline only in the case when Hub is offline.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
It may also happen that you need to reset the device.
|
||||
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||
* [OSRAM LIGHTIFY Dimming Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch)
|
||||
* [Iris Smart Button Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button)
|
||||
* [Iris KeyFob Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob)
|
||||
@@ -13,8 +13,6 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import physicalgraph.zigbee.zcl.DataType
|
||||
|
||||
metadata {
|
||||
@@ -26,7 +24,6 @@ metadata {
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -252,8 +249,6 @@ def updated() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// Arrival sensors only goes OFFLINE when Hub is off
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
|
||||
if ((device.getDataValue("manufacturer") == "OSRAM") && (device.getDataValue("model") == "LIGHTIFY Dimming Switch")) {
|
||||
sendEvent(name: "numberOfButtons", value: 2)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ metadata {
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.valve", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing"
|
||||
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"opening"
|
||||
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing"
|
||||
@@ -58,8 +58,8 @@ metadata {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["contact"])
|
||||
details(["contact", "battery", "refresh"])
|
||||
main(["valve"])
|
||||
details(["valve", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ metadata {
|
||||
definition (name: "Z-Wave Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Thermostat"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Polling"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
@@ -116,7 +117,7 @@ metadata {
|
||||
state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
state "default", action:"polling.poll", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "temperature"
|
||||
details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh"])
|
||||
@@ -124,20 +125,13 @@ metadata {
|
||||
}
|
||||
|
||||
def installed(){
|
||||
sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()))
|
||||
initialize()
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated(){
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
unschedule()
|
||||
runEvery5Minutes("refresh")
|
||||
refresh()
|
||||
}
|
||||
|
||||
def parse(String description)
|
||||
@@ -155,7 +149,6 @@ def parse(String description)
|
||||
]
|
||||
if (map.name == "thermostatMode") {
|
||||
state.lastTriedMode = map.value
|
||||
map.data = [supportedThermostatModes:state.supportedThermostatModes]
|
||||
if (map.value == "cool") {
|
||||
map2.value = device.latestValue("coolingSetpoint")
|
||||
log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
|
||||
@@ -179,7 +172,6 @@ def parse(String description)
|
||||
}
|
||||
} else if (map.name == "thermostatFanMode" && map.isStateChange) {
|
||||
state.lastTriedFanMode = map.value
|
||||
map.data = [supportedThermostatFanModes: state.supportedThermostatFanModes]
|
||||
}
|
||||
log.debug "Parse returned $result"
|
||||
result
|
||||
@@ -313,26 +305,26 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
|
||||
def supportedModes = []
|
||||
if(cmd.off) { supportedModes << "off" }
|
||||
if(cmd.heat) { supportedModes << "heat" }
|
||||
if(cmd.cool) { supportedModes << "cool" }
|
||||
if(cmd.auto) { supportedModes << "auto" }
|
||||
if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" }
|
||||
def supportedModes = ""
|
||||
if(cmd.off) { supportedModes += "off " }
|
||||
if(cmd.heat) { supportedModes += "heat " }
|
||||
if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " }
|
||||
if(cmd.cool) { supportedModes += "cool " }
|
||||
if(cmd.auto) { supportedModes += "auto " }
|
||||
|
||||
state.supportedThermostatModes = supportedModes
|
||||
sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false)
|
||||
state.supportedModes = supportedModes
|
||||
// No events to be generated, return empty map
|
||||
return [:]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
|
||||
def supportedFanModes = []
|
||||
if(cmd.auto) { supportedFanModes << "auto" } // "fanAuto "
|
||||
if(cmd.circulation) { supportedFanModes << "circulate" } // "fanCirculate"
|
||||
if(cmd.low) { supportedFanModes << "on" } // "fanOn"
|
||||
def supportedFanModes = ""
|
||||
if(cmd.auto) { supportedFanModes += "auto " } // "fanAuto "
|
||||
if(cmd.low) { supportedFanModes += "on " } // "fanOn"
|
||||
if(cmd.circulation) { supportedFanModes += "circulate " } // "fanCirculate"
|
||||
|
||||
state.supportedThermostatFanModes = supportedFanModes
|
||||
sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false)
|
||||
state.supportedFanModes = supportedFanModes
|
||||
// No events to be generated, return empty map
|
||||
return [:]
|
||||
}
|
||||
|
||||
@@ -345,17 +337,15 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
// Command Implementations
|
||||
def refresh() {
|
||||
def cmds = []
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.sensorMultilevelV2.sensorMultilevelGet().format()) // current temperature
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format())
|
||||
sendHubCommand(cmds)
|
||||
def poll() {
|
||||
delayBetween([
|
||||
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
|
||||
zwave.thermostatModeV2.thermostatModeGet().format(),
|
||||
zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
|
||||
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
|
||||
], 2300)
|
||||
}
|
||||
|
||||
def quickSetHeat(degrees) {
|
||||
@@ -426,14 +416,28 @@ def ping() {
|
||||
poll()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
delayBetween([
|
||||
zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
|
||||
zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
|
||||
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
|
||||
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
|
||||
zwave.thermostatModeV2.thermostatModeGet().format(),
|
||||
zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
|
||||
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
|
||||
], 2300)
|
||||
}
|
||||
|
||||
def modes() {
|
||||
return state.supportedThermostatModes
|
||||
["off", "heat", "cool", "auto", "emergency heat"]
|
||||
}
|
||||
|
||||
def switchMode() {
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
def lastTriedMode = state.lastTriedMode ?: currentMode ?: ["off"]
|
||||
def supportedModes = getDataByName("supportedThermostatModes")
|
||||
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off"
|
||||
def supportedModes = getDataByName("supportedModes")
|
||||
def modeOrder = modes()
|
||||
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||
def nextMode = next(lastTriedMode)
|
||||
@@ -450,7 +454,7 @@ def switchMode() {
|
||||
}
|
||||
|
||||
def switchToMode(nextMode) {
|
||||
def supportedModes = getDataByName("supportedThermostatModes")
|
||||
def supportedModes = getDataByName("supportedModes")
|
||||
if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
|
||||
if (nextMode in modes()) {
|
||||
state.lastTriedMode = nextMode
|
||||
@@ -462,9 +466,9 @@ def switchToMode(nextMode) {
|
||||
|
||||
def switchFanMode() {
|
||||
def currentMode = device.currentState("thermostatFanMode")?.value
|
||||
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: ["off"]
|
||||
def supportedModes = getDataByName("supportedThermostatFanModes") ?: ["auto", "on"]
|
||||
def modeOrder = state.supportedThermostatFanModes
|
||||
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off"
|
||||
def supportedModes = getDataByName("supportedFanModes") ?: "auto on" // "fanAuto fanOn"
|
||||
def modeOrder = ["auto", "circulate", "on"] // "fanAuto", "fanCirculate", "fanOn"
|
||||
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||
def nextMode = next(lastTriedMode)
|
||||
while (!supportedModes?.contains(nextMode) && nextMode != "auto") { // "fanAuto"
|
||||
@@ -474,7 +478,7 @@ def switchFanMode() {
|
||||
}
|
||||
|
||||
def switchToFanMode(nextMode) {
|
||||
def supportedFanModes = getDataByName("supportedThermostatFanModes")
|
||||
def supportedFanModes = getDataByName("supportedFanModes")
|
||||
if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
|
||||
|
||||
def returnCommand
|
||||
|
||||
@@ -58,7 +58,6 @@ metadata {
|
||||
def installed() {
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def updated() {
|
||||
|
||||
@@ -1,797 +0,0 @@
|
||||
/*****************************************************************************************************************
|
||||
* Copyright David Lomas (codersaur)
|
||||
*
|
||||
* Name: InfluxDB Logger
|
||||
*
|
||||
* Date: 2017-04-03
|
||||
*
|
||||
* Version: 1.11
|
||||
*
|
||||
* Source: https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
|
||||
*
|
||||
* Author: David Lomas (codersaur)
|
||||
*
|
||||
* Description: A SmartApp to log SmartThings device states to an InfluxDB database.
|
||||
*
|
||||
* For full information, including installation instructions, exmples, and version history, see:
|
||||
* https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
|
||||
*
|
||||
* IMPORTANT - To enable the resolution of groupNames (i.e. room names), you must manually insert the group IDs
|
||||
* into the getGroupName() command code at the end of this file.
|
||||
*
|
||||
* License:
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*****************************************************************************************************************/
|
||||
definition(
|
||||
name: "InfluxDB Logger",
|
||||
namespace: "codersaur",
|
||||
author: "David Lomas (codersaur)",
|
||||
description: "Log SmartThings device states to InfluxDB",
|
||||
category: "My Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
|
||||
|
||||
preferences {
|
||||
|
||||
section("General:") {
|
||||
//input "prefDebugMode", "bool", title: "Enable debug logging?", defaultValue: true, displayDuringSetup: true
|
||||
input (
|
||||
name: "configLoggingLevelIDE",
|
||||
title: "IDE Live Logging Level:\nMessages with this level and higher will be logged to the IDE.",
|
||||
type: "enum",
|
||||
options: [
|
||||
"0" : "None",
|
||||
"1" : "Error",
|
||||
"2" : "Warning",
|
||||
"3" : "Info",
|
||||
"4" : "Debug",
|
||||
"5" : "Trace"
|
||||
],
|
||||
defaultValue: "3",
|
||||
displayDuringSetup: true,
|
||||
required: false
|
||||
)
|
||||
}
|
||||
|
||||
section ("InfluxDB Database:") {
|
||||
input "prefDatabaseHost", "text", title: "Host", defaultValue: "10.10.10.10", required: true
|
||||
input "prefDatabasePort", "text", title: "Port", defaultValue: "8086", required: true
|
||||
input "prefDatabaseName", "text", title: "Database Name", defaultValue: "", required: true
|
||||
input "prefDatabaseUser", "text", title: "Username", required: false
|
||||
input "prefDatabasePass", "text", title: "Password", required: false
|
||||
}
|
||||
|
||||
section("Polling:") {
|
||||
input "prefSoftPollingInterval", "number", title:"Soft-Polling interval (minutes)", defaultValue: 10, required: true
|
||||
}
|
||||
|
||||
section("System Monitoring:") {
|
||||
input "prefLogModeEvents", "bool", title:"Log Mode Events?", defaultValue: true, required: true
|
||||
input "prefLogHubProperties", "bool", title:"Log Hub Properties?", defaultValue: true, required: true
|
||||
input "prefLogLocationProperties", "bool", title:"Log Location Properties?", defaultValue: true, required: true
|
||||
}
|
||||
|
||||
section("Devices To Monitor:") {
|
||||
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
|
||||
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
|
||||
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
input "co2s", "capability.carbonDioxideMeasurement", title: "Carbon Dioxide Detectors", multiple: true, required: false
|
||||
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "consumables", "capability.consumable", title: "Consumables", multiple: true, required: false
|
||||
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
|
||||
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
|
||||
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
|
||||
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "phMeters", "capability.pHMeasurement", title: "pH Meters", multiple: true, required: false
|
||||
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
|
||||
input "pressures", "capability.sensor", title: "Pressure Sensors", multiple: true, required: false
|
||||
input "shockSensors", "capability.shockSensor", title: "Shock Sensors", multiple: true, required: false
|
||||
input "signalStrengthMeters", "capability.signalStrength", title: "Signal Strength Meters", multiple: true, required: false
|
||||
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
input "soundSensors", "capability.soundSensor", title: "Sound Sensors", multiple: true, required: false
|
||||
input "spls", "capability.soundPressureLevel", title: "Sound Pressure Level Sensors", multiple: true, required: false
|
||||
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
|
||||
input "tamperAlerts", "capability.tamperAlert", title: "Tamper Alerts", multiple: true, required: false
|
||||
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
|
||||
input "threeAxis", "capability.threeAxis", title: "Three-axis (Orientation) Sensors", multiple: true, required: false
|
||||
input "touchs", "capability.touchSensor", title: "Touch Sensors", multiple: true, required: false
|
||||
input "uvs", "capability.ultravioletIndex", title: "UV Sensors", multiple: true, required: false
|
||||
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "volts", "capability.voltageMeasurement", title: "Voltage Meters", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
|
||||
input "windowShades", "capability.windowShade", title: "Window Shades", multiple: true, required: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************************************
|
||||
* SmartThings System Commands:
|
||||
*****************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* installed()
|
||||
*
|
||||
* Runs when the app is first installed.
|
||||
**/
|
||||
def installed() {
|
||||
state.installedAt = now()
|
||||
state.loggingLevelIDE = 5
|
||||
log.debug "${app.label}: Installed with settings: ${settings}"
|
||||
}
|
||||
|
||||
/**
|
||||
* uninstalled()
|
||||
*
|
||||
* Runs when the app is uninstalled.
|
||||
**/
|
||||
def uninstalled() {
|
||||
logger("uninstalled()","trace")
|
||||
}
|
||||
|
||||
/**
|
||||
* updated()
|
||||
*
|
||||
* Runs when app settings are changed.
|
||||
*
|
||||
* Updates device.state with input values and other hard-coded values.
|
||||
* Builds state.deviceAttributes which describes the attributes that will be monitored for each device collection
|
||||
* (used by manageSubscriptions() and softPoll()).
|
||||
* Refreshes scheduling and subscriptions.
|
||||
**/
|
||||
def updated() {
|
||||
logger("updated()","trace")
|
||||
|
||||
// Update internal state:
|
||||
state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3
|
||||
|
||||
// Database config:
|
||||
state.databaseHost = settings.prefDatabaseHost
|
||||
state.databasePort = settings.prefDatabasePort
|
||||
state.databaseName = settings.prefDatabaseName
|
||||
state.databaseUser = settings.prefDatabaseUser
|
||||
state.databasePass = settings.prefDatabasePass
|
||||
|
||||
state.path = "/write?db=${state.databaseName}"
|
||||
state.headers = [:]
|
||||
state.headers.put("HOST", "${state.databaseHost}:${state.databasePort}")
|
||||
state.headers.put("Content-Type", "application/x-www-form-urlencoded")
|
||||
if (state.databaseUser && state.databasePass) {
|
||||
state.headers.put("Authorization", encodeCredentialsBasic(state.databaseUser, state.databasePass))
|
||||
}
|
||||
|
||||
// Build array of device collections and the attributes we want to report on for that collection:
|
||||
// Note, the collection names are stored as strings. Adding references to the actual collection
|
||||
// objects causes major issues (possibly memory issues?).
|
||||
state.deviceAttributes = []
|
||||
state.deviceAttributes << [ devices: 'accelerometers', attributes: ['acceleration']]
|
||||
state.deviceAttributes << [ devices: 'alarms', attributes: ['alarm']]
|
||||
state.deviceAttributes << [ devices: 'batteries', attributes: ['battery']]
|
||||
state.deviceAttributes << [ devices: 'beacons', attributes: ['presence']]
|
||||
state.deviceAttributes << [ devices: 'buttons', attributes: ['button']]
|
||||
state.deviceAttributes << [ devices: 'cos', attributes: ['carbonMonoxide']]
|
||||
state.deviceAttributes << [ devices: 'co2s', attributes: ['carbonDioxide']]
|
||||
state.deviceAttributes << [ devices: 'colors', attributes: ['hue','saturation','color']]
|
||||
state.deviceAttributes << [ devices: 'consumables', attributes: ['consumableStatus']]
|
||||
state.deviceAttributes << [ devices: 'contacts', attributes: ['contact']]
|
||||
state.deviceAttributes << [ devices: 'doorsControllers', attributes: ['door']]
|
||||
state.deviceAttributes << [ devices: 'energyMeters', attributes: ['energy']]
|
||||
state.deviceAttributes << [ devices: 'humidities', attributes: ['humidity']]
|
||||
state.deviceAttributes << [ devices: 'illuminances', attributes: ['illuminance']]
|
||||
state.deviceAttributes << [ devices: 'locks', attributes: ['lock']]
|
||||
state.deviceAttributes << [ devices: 'motions', attributes: ['motion']]
|
||||
state.deviceAttributes << [ devices: 'musicPlayers', attributes: ['status','level','trackDescription','trackData','mute']]
|
||||
state.deviceAttributes << [ devices: 'peds', attributes: ['steps','goal']]
|
||||
state.deviceAttributes << [ devices: 'phMeters', attributes: ['pH']]
|
||||
state.deviceAttributes << [ devices: 'powerMeters', attributes: ['power','voltage','current','powerFactor']]
|
||||
state.deviceAttributes << [ devices: 'presences', attributes: ['presence']]
|
||||
state.deviceAttributes << [ devices: 'pressures', attributes: ['pressure']]
|
||||
state.deviceAttributes << [ devices: 'shockSensors', attributes: ['shock']]
|
||||
state.deviceAttributes << [ devices: 'signalStrengthMeters', attributes: ['lqi','rssi']]
|
||||
state.deviceAttributes << [ devices: 'sleepSensors', attributes: ['sleeping']]
|
||||
state.deviceAttributes << [ devices: 'smokeDetectors', attributes: ['smoke']]
|
||||
state.deviceAttributes << [ devices: 'soundSensors', attributes: ['sound']]
|
||||
state.deviceAttributes << [ devices: 'spls', attributes: ['soundPressureLevel']]
|
||||
state.deviceAttributes << [ devices: 'switches', attributes: ['switch']]
|
||||
state.deviceAttributes << [ devices: 'switchLevels', attributes: ['level']]
|
||||
state.deviceAttributes << [ devices: 'tamperAlerts', attributes: ['tamper']]
|
||||
state.deviceAttributes << [ devices: 'temperatures', attributes: ['temperature']]
|
||||
state.deviceAttributes << [ devices: 'thermostats', attributes: ['temperature','heatingSetpoint','coolingSetpoint','thermostatSetpoint','thermostatMode','thermostatFanMode','thermostatOperatingState','thermostatSetpointMode','scheduledSetpoint','optimisation','windowFunction']]
|
||||
state.deviceAttributes << [ devices: 'threeAxis', attributes: ['threeAxis']]
|
||||
state.deviceAttributes << [ devices: 'touchs', attributes: ['touch']]
|
||||
state.deviceAttributes << [ devices: 'uvs', attributes: ['ultravioletIndex']]
|
||||
state.deviceAttributes << [ devices: 'valves', attributes: ['contact']]
|
||||
state.deviceAttributes << [ devices: 'volts', attributes: ['voltage']]
|
||||
state.deviceAttributes << [ devices: 'waterSensors', attributes: ['water']]
|
||||
state.deviceAttributes << [ devices: 'windowShades', attributes: ['windowShade']]
|
||||
|
||||
// Configure Scheduling:
|
||||
state.softPollingInterval = settings.prefSoftPollingInterval.toInteger()
|
||||
manageSchedules()
|
||||
|
||||
// Configure Subscriptions:
|
||||
manageSubscriptions()
|
||||
}
|
||||
|
||||
/*****************************************************************************************************************
|
||||
* Event Handlers:
|
||||
*****************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* handleAppTouch(evt)
|
||||
*
|
||||
* Used for testing.
|
||||
**/
|
||||
def handleAppTouch(evt) {
|
||||
logger("handleAppTouch()","trace")
|
||||
|
||||
softPoll()
|
||||
}
|
||||
|
||||
/**
|
||||
* handleModeEvent(evt)
|
||||
*
|
||||
* Log Mode changes.
|
||||
**/
|
||||
def handleModeEvent(evt) {
|
||||
logger("handleModeEvent(): Mode changed to: ${evt.value}","info")
|
||||
|
||||
def locationId = escapeStringForInfluxDB(location.id)
|
||||
def locationName = escapeStringForInfluxDB(location.name)
|
||||
def mode = '"' + escapeStringForInfluxDB(evt.value) + '"'
|
||||
def data = "_stMode,locationId=${locationId},locationName=${locationName} mode=${mode}"
|
||||
postToInfluxDB(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* handleEvent(evt)
|
||||
*
|
||||
* Builds data to send to InfluxDB.
|
||||
* - Escapes and quotes string values.
|
||||
* - Calculates logical binary values where string values can be
|
||||
* represented as binary values (e.g. contact: closed = 1, open = 0)
|
||||
*
|
||||
* Useful references:
|
||||
* - http://docs.smartthings.com/en/latest/capabilities-reference.html
|
||||
* - https://docs.influxdata.com/influxdb/v0.10/guides/writing_data/
|
||||
**/
|
||||
def handleEvent(evt) {
|
||||
logger("handleEvent(): $evt.displayName($evt.name:$evt.unit) $evt.value","info")
|
||||
|
||||
// Build data string to send to InfluxDB:
|
||||
// Format: <measurement>[,<tag_name>=<tag_value>] field=<field_value>
|
||||
// If value is an integer, it must have a trailing "i"
|
||||
// If value is a string, it must be enclosed in double quotes.
|
||||
def measurement = evt.name
|
||||
// tags:
|
||||
def deviceId = escapeStringForInfluxDB(evt.deviceId)
|
||||
def deviceName = escapeStringForInfluxDB(evt.displayName)
|
||||
def groupId = escapeStringForInfluxDB(evt?.device.device.groupId)
|
||||
def groupName = escapeStringForInfluxDB(getGroupName(evt?.device.device.groupId))
|
||||
def hubId = escapeStringForInfluxDB(evt?.device.device.hubId)
|
||||
def hubName = escapeStringForInfluxDB(evt?.device.device.hub.toString())
|
||||
// Don't pull these from the evt.device as the app itself will be associated with one location.
|
||||
def locationId = escapeStringForInfluxDB(location.id)
|
||||
def locationName = escapeStringForInfluxDB(location.name)
|
||||
|
||||
def unit = escapeStringForInfluxDB(evt.unit)
|
||||
def value = escapeStringForInfluxDB(evt.value)
|
||||
def valueBinary = ''
|
||||
|
||||
def data = "${measurement},deviceId=${deviceId},deviceName=${deviceName},groupId=${groupId},groupName=${groupName},hubId=${hubId},hubName=${hubName},locationId=${locationId},locationName=${locationName}"
|
||||
|
||||
// Unit tag and fields depend on the event type:
|
||||
// Most string-valued attributes can be translated to a binary value too.
|
||||
if ('acceleration' == evt.name) { // acceleration: Calculate a binary value (active = 1, inactive = 0)
|
||||
unit = 'acceleration'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('active' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('alarm' == evt.name) { // alarm: Calculate a binary value (strobe/siren/both = 1, off = 0)
|
||||
unit = 'alarm'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('off' == evt.value) ? '0i' : '1i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('button' == evt.name) { // button: Calculate a binary value (held = 1, pushed = 0)
|
||||
unit = 'button'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('pushed' == evt.value) ? '0i' : '1i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('carbonMonoxide' == evt.name) { // carbonMonoxide: Calculate a binary value (detected = 1, clear/tested = 0)
|
||||
unit = 'carbonMonoxide'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('consumableStatus' == evt.name) { // consumableStatus: Calculate a binary value ("good" = 1, "missing"/"replace"/"maintenance_required"/"order" = 0)
|
||||
unit = 'consumableStatus'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('good' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('contact' == evt.name) { // contact: Calculate a binary value (closed = 1, open = 0)
|
||||
unit = 'contact'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('closed' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('door' == evt.name) { // door: Calculate a binary value (closed = 1, open/opening/closing/unknown = 0)
|
||||
unit = 'door'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('closed' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('lock' == evt.name) { // door: Calculate a binary value (locked = 1, unlocked = 0)
|
||||
unit = 'lock'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('locked' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('motion' == evt.name) { // Motion: Calculate a binary value (active = 1, inactive = 0)
|
||||
unit = 'motion'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('active' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('mute' == evt.name) { // mute: Calculate a binary value (muted = 1, unmuted = 0)
|
||||
unit = 'mute'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('muted' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('presence' == evt.name) { // presence: Calculate a binary value (present = 1, not present = 0)
|
||||
unit = 'presence'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('present' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('shock' == evt.name) { // shock: Calculate a binary value (detected = 1, clear = 0)
|
||||
unit = 'shock'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('sleeping' == evt.name) { // sleeping: Calculate a binary value (sleeping = 1, not sleeping = 0)
|
||||
unit = 'sleeping'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('sleeping' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('smoke' == evt.name) { // smoke: Calculate a binary value (detected = 1, clear/tested = 0)
|
||||
unit = 'smoke'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('sound' == evt.name) { // sound: Calculate a binary value (detected = 1, not detected = 0)
|
||||
unit = 'sound'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('switch' == evt.name) { // switch: Calculate a binary value (on = 1, off = 0)
|
||||
unit = 'switch'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('on' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('tamper' == evt.name) { // tamper: Calculate a binary value (detected = 1, clear = 0)
|
||||
unit = 'tamper'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('thermostatMode' == evt.name) { // thermostatMode: Calculate a binary value (<any other value> = 1, off = 0)
|
||||
unit = 'thermostatMode'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('off' == evt.value) ? '0i' : '1i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('thermostatFanMode' == evt.name) { // thermostatFanMode: Calculate a binary value (<any other value> = 1, off = 0)
|
||||
unit = 'thermostatFanMode'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('off' == evt.value) ? '0i' : '1i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('thermostatOperatingState' == evt.name) { // thermostatOperatingState: Calculate a binary value (heating = 1, <any other value> = 0)
|
||||
unit = 'thermostatOperatingState'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('heating' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('thermostatSetpointMode' == evt.name) { // thermostatSetpointMode: Calculate a binary value (followSchedule = 0, <any other value> = 1)
|
||||
unit = 'thermostatSetpointMode'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('followSchedule' == evt.value) ? '0i' : '1i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('threeAxis' == evt.name) { // threeAxis: Format to x,y,z values.
|
||||
unit = 'threeAxis'
|
||||
def valueXYZ = evt.value.split(",")
|
||||
def valueX = valueXYZ[0]
|
||||
def valueY = valueXYZ[1]
|
||||
def valueZ = valueXYZ[2]
|
||||
data += ",unit=${unit} valueX=${valueX}i,valueY=${valueY}i,valueZ=${valueZ}i" // values are integers.
|
||||
}
|
||||
else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, "" = 0)
|
||||
unit = 'touch'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('touched' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('optimisation' == evt.name) { // optimisation: Calculate a binary value (active = 1, inactive = 0)
|
||||
unit = 'optimisation'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('active' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('windowFunction' == evt.name) { // windowFunction: Calculate a binary value (active = 1, inactive = 0)
|
||||
unit = 'windowFunction'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('active' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, <any other value> = 0)
|
||||
unit = 'touch'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('touched' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('water' == evt.name) { // water: Calculate a binary value (wet = 1, dry = 0)
|
||||
unit = 'water'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('wet' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
else if ('windowShade' == evt.name) { // windowShade: Calculate a binary value (closed = 1, <any other value> = 0)
|
||||
unit = 'windowShade'
|
||||
value = '"' + value + '"'
|
||||
valueBinary = ('closed' == evt.value) ? '1i' : '0i'
|
||||
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
|
||||
}
|
||||
// Catch any other event with a string value that hasn't been handled:
|
||||
else if (evt.value ==~ /.*[^0-9\.,-].*/) { // match if any characters are not digits, period, comma, or hyphen.
|
||||
logger("handleEvent(): Found a string value that's not explicitly handled: Device Name: ${deviceName}, Event Name: ${evt.name}, Value: ${evt.value}","warn")
|
||||
value = '"' + value + '"'
|
||||
data += ",unit=${unit} value=${value}"
|
||||
}
|
||||
// Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).
|
||||
else {
|
||||
data += ",unit=${unit} value=${value}"
|
||||
}
|
||||
|
||||
// Post data to InfluxDB:
|
||||
postToInfluxDB(data)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************************************
|
||||
* Main Commands:
|
||||
*****************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* softPoll()
|
||||
*
|
||||
* Executed by schedule.
|
||||
*
|
||||
* Forces data to be posted to InfluxDB (even if an event has not been triggered).
|
||||
* Doesn't poll devices, just builds a fake event to pass to handleEvent().
|
||||
*
|
||||
* Also calls LogSystemProperties().
|
||||
**/
|
||||
def softPoll() {
|
||||
logger("softPoll()","trace")
|
||||
|
||||
logSystemProperties()
|
||||
|
||||
// Iterate over each attribute for each device, in each device collection in deviceAttributes:
|
||||
def devs // temp variable to hold device collection.
|
||||
state.deviceAttributes.each { da ->
|
||||
devs = settings."${da.devices}"
|
||||
if (devs && (da.attributes)) {
|
||||
devs.each { d ->
|
||||
da.attributes.each { attr ->
|
||||
if (d.hasAttribute(attr) && d.latestState(attr)?.value != null) {
|
||||
logger("softPoll(): Softpolling device ${d} for attribute: ${attr}","info")
|
||||
// Send fake event to handleEvent():
|
||||
handleEvent([
|
||||
name: attr,
|
||||
value: d.latestState(attr)?.value,
|
||||
unit: d.latestState(attr)?.unit,
|
||||
device: d,
|
||||
deviceId: d.id,
|
||||
displayName: d.displayName
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* logSystemProperties()
|
||||
*
|
||||
* Generates measurements for SmartThings system (hubs and locations) properties.
|
||||
**/
|
||||
def logSystemProperties() {
|
||||
logger("logSystemProperties()","trace")
|
||||
|
||||
def locationId = '"' + escapeStringForInfluxDB(location.id) + '"'
|
||||
def locationName = '"' + escapeStringForInfluxDB(location.name) + '"'
|
||||
|
||||
// Location Properties:
|
||||
if (prefLogLocationProperties) {
|
||||
try {
|
||||
def tz = '"' + escapeStringForInfluxDB(location.timeZone.ID) + '"'
|
||||
def mode = '"' + escapeStringForInfluxDB(location.mode) + '"'
|
||||
def hubCount = location.hubs.size()
|
||||
def times = getSunriseAndSunset()
|
||||
def srt = '"' + times.sunrise.format("HH:mm", location.timeZone) + '"'
|
||||
def sst = '"' + times.sunset.format("HH:mm", location.timeZone) + '"'
|
||||
|
||||
def data = "_stLocation,locationId=${locationId},locationName=${locationName},latitude=${location.latitude},longitude=${location.longitude},timeZone=${tz} mode=${mode},hubCount=${hubCount}i,sunriseTime=${srt},sunsetTime=${sst}"
|
||||
postToInfluxDB(data)
|
||||
} catch (e) {
|
||||
logger("logSystemProperties(): Unable to log Location properties: ${e}","error")
|
||||
}
|
||||
}
|
||||
|
||||
// Hub Properties:
|
||||
if (prefLogHubProperties) {
|
||||
location.hubs.each { h ->
|
||||
try {
|
||||
def hubId = '"' + escapeStringForInfluxDB(h.id) + '"'
|
||||
def hubName = '"' + escapeStringForInfluxDB(h.name) + '"'
|
||||
def hubIP = '"' + escapeStringForInfluxDB(h.localIP) + '"'
|
||||
def hubStatus = '"' + escapeStringForInfluxDB(h.status) + '"'
|
||||
def batteryInUse = ("false" == h.hub.getDataValue("batteryInUse")) ? "0i" : "1i"
|
||||
def hubUptime = h.hub.getDataValue("uptime") + 'i'
|
||||
def zigbeePowerLevel = h.hub.getDataValue("zigbeePowerLevel") + 'i'
|
||||
def zwavePowerLevel = '"' + escapeStringForInfluxDB(h.hub.getDataValue("zwavePowerLevel")) + '"'
|
||||
def firmwareVersion = '"' + escapeStringForInfluxDB(h.firmwareVersionString) + '"'
|
||||
|
||||
def data = "_stHub,locationId=${locationId},locationName=${locationName},hubId=${hubId},hubName=${hubName},hubIP=${hubIP} "
|
||||
data += "status=${hubStatus},batteryInUse=${batteryInUse},uptime=${hubUptime},zigbeePowerLevel=${zigbeePowerLevel},zwavePowerLevel=${zwavePowerLevel},firmwareVersion=${firmwareVersion}"
|
||||
postToInfluxDB(data)
|
||||
} catch (e) {
|
||||
logger("logSystemProperties(): Unable to log Hub properties: ${e}","error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* postToInfluxDB()
|
||||
*
|
||||
* Posts data to InfluxDB.
|
||||
*
|
||||
* Uses hubAction instead of httpPost() in case InfluxDB server is on the same LAN as the Smartthings Hub.
|
||||
**/
|
||||
def postToInfluxDB(data) {
|
||||
logger("postToInfluxDB(): Posting data to InfluxDB: Host: ${state.databaseHost}, Port: ${state.databasePort}, Database: ${state.databaseName}, Data: [${data}]","debug")
|
||||
|
||||
try {
|
||||
def hubAction = new physicalgraph.device.HubAction(
|
||||
[
|
||||
method: "POST",
|
||||
path: state.path,
|
||||
body: data,
|
||||
headers: state.headers
|
||||
],
|
||||
null,
|
||||
[ callback: handleInfluxResponse ]
|
||||
)
|
||||
|
||||
sendHubCommand(hubAction)
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger("postToInfluxDB(): Exception ${e} on ${hubAction}","error")
|
||||
}
|
||||
|
||||
// For reference, code that could be used for WAN hosts:
|
||||
// def url = "http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}"
|
||||
// try {
|
||||
// httpPost(url, data) { response ->
|
||||
// if (response.status != 999 ) {
|
||||
// log.debug "Response Status: ${response.status}"
|
||||
// log.debug "Response data: ${response.data}"
|
||||
// log.debug "Response contentType: ${response.contentType}"
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// logger("postToInfluxDB(): Something went wrong when posting: ${e}","error")
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* handleInfluxResponse()
|
||||
*
|
||||
* Handles response from post made in postToInfluxDB().
|
||||
**/
|
||||
def handleInfluxResponse(physicalgraph.device.HubResponse hubResponse) {
|
||||
if(hubResponse.status >= 400) {
|
||||
logger("postToInfluxDB(): Something went wrong! Response from InfluxDB: Headers: ${hubResponse.headers}, Body: ${hubResponse.body}","error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************************************
|
||||
* Private Helper Functions:
|
||||
*****************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* manageSchedules()
|
||||
*
|
||||
* Configures/restarts scheduled tasks:
|
||||
* softPoll() - Run every {state.softPollingInterval} minutes.
|
||||
**/
|
||||
private manageSchedules() {
|
||||
logger("manageSchedules()","trace")
|
||||
|
||||
// Generate a random offset (1-60):
|
||||
Random rand = new Random(now())
|
||||
def randomOffset = 0
|
||||
|
||||
// softPoll:
|
||||
try {
|
||||
unschedule(softPoll)
|
||||
}
|
||||
catch(e) {
|
||||
// logger("manageSchedules(): Unschedule failed!","error")
|
||||
}
|
||||
|
||||
if (state.softPollingInterval > 0) {
|
||||
randomOffset = rand.nextInt(60)
|
||||
logger("manageSchedules(): Scheduling softpoll to run every ${state.softPollingInterval} minutes (offset of ${randomOffset} seconds).","trace")
|
||||
schedule("${randomOffset} 0/${state.softPollingInterval} * * * ?", "softPoll")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* manageSubscriptions()
|
||||
*
|
||||
* Configures subscriptions.
|
||||
**/
|
||||
private manageSubscriptions() {
|
||||
logger("manageSubscriptions()","trace")
|
||||
|
||||
// Unsubscribe:
|
||||
unsubscribe()
|
||||
|
||||
// Subscribe to App Touch events:
|
||||
subscribe(app,handleAppTouch)
|
||||
|
||||
// Subscribe to mode events:
|
||||
if (prefLogModeEvents) subscribe(location, "mode", handleModeEvent)
|
||||
|
||||
// Subscribe to device attributes (iterate over each attribute for each device collection in state.deviceAttributes):
|
||||
def devs // dynamic variable holding device collection.
|
||||
state.deviceAttributes.each { da ->
|
||||
devs = settings."${da.devices}"
|
||||
if (devs && (da.attributes)) {
|
||||
da.attributes.each { attr ->
|
||||
logger("manageSubscriptions(): Subscribing to attribute: ${attr}, for devices: ${da.devices}","info")
|
||||
// There is no need to check if all devices in the collection have the attribute.
|
||||
subscribe(devs, attr, handleEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* logger()
|
||||
*
|
||||
* Wrapper function for all logging.
|
||||
**/
|
||||
private logger(msg, level = "debug") {
|
||||
|
||||
switch(level) {
|
||||
case "error":
|
||||
if (state.loggingLevelIDE >= 1) log.error msg
|
||||
break
|
||||
|
||||
case "warn":
|
||||
if (state.loggingLevelIDE >= 2) log.warn msg
|
||||
break
|
||||
|
||||
case "info":
|
||||
if (state.loggingLevelIDE >= 3) log.info msg
|
||||
break
|
||||
|
||||
case "debug":
|
||||
if (state.loggingLevelIDE >= 4) log.debug msg
|
||||
break
|
||||
|
||||
case "trace":
|
||||
if (state.loggingLevelIDE >= 5) log.trace msg
|
||||
break
|
||||
|
||||
default:
|
||||
log.debug msg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* encodeCredentialsBasic()
|
||||
*
|
||||
* Encode credentials for HTTP Basic authentication.
|
||||
**/
|
||||
private encodeCredentialsBasic(username, password) {
|
||||
return "Basic " + "${username}:${password}".encodeAsBase64().toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* escapeStringForInfluxDB()
|
||||
*
|
||||
* Escape values to InfluxDB.
|
||||
*
|
||||
* If a tag key, tag value, or field key contains a space, comma, or an equals sign = it must
|
||||
* be escaped using the backslash character \. Backslash characters do not need to be escaped.
|
||||
* Commas and spaces will also need to be escaped for measurements, though equals signs = do not.
|
||||
*
|
||||
* Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
|
||||
**/
|
||||
private escapeStringForInfluxDB(str) {
|
||||
if (str) {
|
||||
str = str.replaceAll(" ", "\\\\ ") // Escape spaces.
|
||||
str = str.replaceAll(",", "\\\\,") // Escape commas.
|
||||
str = str.replaceAll("=", "\\\\=") // Escape equal signs.
|
||||
str = str.replaceAll("\"", "\\\\\"") // Escape double quotes.
|
||||
//str = str.replaceAll("'", "_") // Replace apostrophes with underscores.
|
||||
}
|
||||
else {
|
||||
str = 'null'
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* getGroupName()
|
||||
*
|
||||
* Get the name of a 'Group' (i.e. Room) from its ID.
|
||||
*
|
||||
* This is done manually as there does not appear to be a way to enumerate
|
||||
* groups from a SmartApp currently.
|
||||
*
|
||||
* GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
|
||||
*
|
||||
* See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
|
||||
**/
|
||||
private getGroupName(id) {
|
||||
|
||||
if (id == null) {return 'Home'}
|
||||
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Kitchen'}
|
||||
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Lounge'}
|
||||
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Office'}
|
||||
else {return 'Unknown'}
|
||||
}
|
||||
412
smartapps/mujica/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy
Normal file
412
smartapps/mujica/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy
Normal file
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Tcp Bulbs (Connect)",
|
||||
namespace: "mujica",
|
||||
author: "SmartThings-Ule",
|
||||
description: "Connect your TCP bulbs to SmartThings using local integration. You must have a geteway with firmware ver 2",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "iniSettings", title: "Connect Your TCP Lights to SmartThings", content: "iniSettings")
|
||||
page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "bulbDiscovery")
|
||||
}
|
||||
|
||||
def iniSettings(){
|
||||
state.loadStatus = "Inactive"
|
||||
log.trace "state.loadStatus ${state.loadStatus}"
|
||||
return dynamicPage(name:"iniSettings", title:"Connect Your TCP Lights to SmartThings", nextPage:"chooseBulbs", install:false, uninstall: true) {
|
||||
section("TCP Connected Remote Credentials") {
|
||||
input "ipGateway", "text", title: "Enter TCP Gateway IP", required: true
|
||||
paragraph "Tap 'Next' after you have entered the ip of your TCP Connected Gateway.\r\n\r\nOnce your ip are accepted, SmartThings will scan your TCP installation for Bulbs."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bulbDiscovery() {
|
||||
debugOut "bulbDiscovery()"
|
||||
//getToken()
|
||||
state.token = "1234567890"
|
||||
|
||||
if (state.loadStatus == "Inactive"){
|
||||
state.count = 0
|
||||
state.loadStatus = "Loading"
|
||||
log.trace "state.loadStatus ${state.loadStatus}"
|
||||
deviceDiscovery()
|
||||
}
|
||||
log.trace "state.count ${state.count}"
|
||||
state.count = state.count + 1
|
||||
log.trace "state.count ${state.count}"
|
||||
if(state.loadStatus == "Loaded" ){
|
||||
def options = devicesDiscovered() ?: []
|
||||
log.trace "state.loadStatus ${state.loadStatus}"
|
||||
return dynamicPage(name:"chooseBulbs", title:"", nextPage:"", install:true, uninstall: true) {
|
||||
section("Tap Below to View Device List") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Bulb", multiple:true, options:options
|
||||
paragraph """Tap 'Done' after you have selected the desired devices."""
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if (state.count)
|
||||
|
||||
log.trace "state.loadStatus ${state.loadStatus}"
|
||||
def msg = state.count >= 3 ? "The TCP Gateway is not responding, please verify the ip address" : "Please wait while we discover your devices. Discovery can take some minutes or more, so sit back and relax! Select your device below once discovered."
|
||||
return dynamicPage(name:"chooseBulbs", title:"", nextPage:"", refreshInterval:5) {
|
||||
section(msg) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed() {
|
||||
debugOut "Installed with settings: ${settings}"
|
||||
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
|
||||
setupBulbs()
|
||||
|
||||
def cron = "0 0/1 * * * ?"
|
||||
log.debug "schedule('$cron', syncronizeDevices)"
|
||||
schedule(cron, syncronizeDevices)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
debugOut "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
setupBulbs()
|
||||
def cron = "0 0/1 * * * ?"
|
||||
log.debug "schedule('$cron', syncronizeDevices)"
|
||||
schedule(cron, syncronizeDevices)
|
||||
}
|
||||
|
||||
def uninstalled()
|
||||
{
|
||||
unschedule() //in case we have hanging runIn()'s
|
||||
}
|
||||
|
||||
private removeChildDevices(delete)
|
||||
{
|
||||
debugOut "deleting ${delete.size()} bulbs"
|
||||
debugOut "deleting ${delete}"
|
||||
delete.each {
|
||||
deleteChildDevice(it.device.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
|
||||
def uninstallFromChildDevice(childDevice)
|
||||
{
|
||||
def errorMsg = "uninstallFromChildDevice was called and "
|
||||
if (!settings.selectedBulbs) {
|
||||
debugOut errorMsg += "had empty list passed in"
|
||||
return
|
||||
}
|
||||
|
||||
def dni = childDevice.device.deviceNetworkId
|
||||
|
||||
if ( !dni ) {
|
||||
debugOut errorMsg += "could not find dni of device"
|
||||
return
|
||||
}
|
||||
|
||||
def newDeviceList = settings.selectedBulbs - dni
|
||||
app.updateSetting("selectedBulbs", newDeviceList)
|
||||
debugOut errorMsg += "completed succesfully"
|
||||
}
|
||||
|
||||
|
||||
def setupBulbs() {
|
||||
debugOut "setupBulbs()"
|
||||
def bulbs = state.devices
|
||||
def deviceFile = "TCP Bulb"
|
||||
|
||||
selectedBulbs.each { did ->
|
||||
//see if this is a selected bulb and install it if not already
|
||||
def d = getChildDevice(did)
|
||||
|
||||
if(!d) {
|
||||
def newBulb = bulbs.find { (it.did) == did }
|
||||
d = addChildDevice("mujica", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true,"data":["model":newBulb?.model,"nodetype":newBulb?.nodetype,"node":newBulb?.node,"dni":did]])
|
||||
} else {
|
||||
infoOut "Avoid add existent device ${did}"
|
||||
}
|
||||
}
|
||||
def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) }
|
||||
removeChildDevices(delete)
|
||||
}
|
||||
|
||||
|
||||
def deviceDiscovery() {
|
||||
log.trace "deviceDiscovery()"
|
||||
def data = "<gip><version>1</version><token>${state.token}</token></gip>"
|
||||
|
||||
def Params = [
|
||||
cmd: "RoomGetCarousel",
|
||||
data: "${data}",
|
||||
fmt: "json"
|
||||
]
|
||||
|
||||
def cmd = toQueryString(Params)
|
||||
|
||||
debugOut "deviceDiscovery()"
|
||||
|
||||
apiGet(cmd,"RoomGetCarouselHandler")
|
||||
}
|
||||
|
||||
def apiGet(String data, String calledBackHandler) {
|
||||
debugOut "apiGet($data, $calledBackHandler) $ipGateway"
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/gwr/gop.php?$data",
|
||||
headers: [
|
||||
HOST: "$ipGateway:80"
|
||||
]], getNetworkId("$ipGateway","80"), [callback: calledBackHandler]))
|
||||
}
|
||||
|
||||
|
||||
void SendCommandHandler(physicalgraph.device.HubResponse hubResponse){
|
||||
debugOut "SendCommandHandler($hubResponse)"
|
||||
debugOut "hubResponse.body ${hubResponse.body}"
|
||||
}
|
||||
void RoomGetCarouselHandler(physicalgraph.device.HubResponse hubResponse){
|
||||
debugOut "RoomGetCarouselHandler($hubResponse)"
|
||||
def bodyXml
|
||||
|
||||
if (hubResponse?.body?.contains("<gip>"))
|
||||
{ // description.xml response (application/xml)
|
||||
debugOut "body contains xml"
|
||||
bodyXml = new XmlSlurper().parseText(hubResponse.body)
|
||||
debugOut "bodyXml $bodyXml"
|
||||
}
|
||||
|
||||
|
||||
def rooms = ""
|
||||
def devices = []
|
||||
def deviceList = []
|
||||
|
||||
|
||||
rooms = bodyXml.room
|
||||
|
||||
|
||||
debugOut "rooms ${rooms[1]}"
|
||||
|
||||
rooms.each({
|
||||
devices = it.device
|
||||
debugOut "it.device ${it.device}"
|
||||
def roomName = it.name
|
||||
debugOut "roomName = ${it.name}"
|
||||
debugOut "devices[1] ${devices[1]}"
|
||||
debugOut "devices[1] != null"
|
||||
def roomId = it?.rid
|
||||
debugOut "Room Device Data: did:${roomId} roomName:${roomName}"
|
||||
devices.each({
|
||||
debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}"
|
||||
deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "${devices?.type}", "node" : "${devices?.node}", "nodetype" : "${devices?.nodetype}", "model" : "${devices?.prodmodel}"]
|
||||
})
|
||||
})
|
||||
|
||||
devices = ["devices" : deviceList]
|
||||
debugOut "devices $devices"
|
||||
state.devices = devices.devices
|
||||
state.loadStatus = "Loaded"
|
||||
}
|
||||
|
||||
def getDevices()
|
||||
{
|
||||
state.devices = state.devices ?: [:]
|
||||
}
|
||||
|
||||
void RoomGetCarouselUpdateHandler(physicalgraph.device.HubResponse hubResponse){
|
||||
debugOut "RoomGetCarouselUpdateHandler($hubResponse)"
|
||||
|
||||
|
||||
debugOut "msg ${parseLanMessage(hubResponse.body)}"
|
||||
|
||||
def bodyXml
|
||||
|
||||
if (hubResponse?.body?.contains("<gip>"))
|
||||
{
|
||||
debugOut "body contains xml"
|
||||
bodyXml = new XmlSlurper().parseText(hubResponse.body)
|
||||
}
|
||||
|
||||
|
||||
def rooms = ""
|
||||
def devices = []
|
||||
def deviceList = []
|
||||
|
||||
rooms = bodyXml.room
|
||||
|
||||
rooms.each({
|
||||
devices = it.device
|
||||
devices.each({
|
||||
def dni = it.did.text()
|
||||
def bulb = getChildDevice(dni)
|
||||
if ( bulb ){
|
||||
def power = it.power ? it.power.text() as float :0
|
||||
sendEvent( dni, [name: "power", value: power*1000] )
|
||||
if (( it.state.text() == "1" ) && ( bulb?.currentValue("switch") != "on" ))
|
||||
sendEvent( dni, [name: "switch",value:"on"] )
|
||||
|
||||
if (( it.state.text() == "0" ) && ( bulb?.currentValue("switch") != "off" ))
|
||||
sendEvent( dni, [name: "switch",value:"off"] )
|
||||
|
||||
if ( it.level.text() != bulb?.currentValue("level")) {
|
||||
sendEvent( dni, [name: "level",value: "${it.level.text()}"] )
|
||||
sendEvent( dni, [name: "setLevel",value: "${it.level.text()}"] )
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Map devicesDiscovered() {
|
||||
def devices = state.devices
|
||||
def map = [:]
|
||||
if (devices instanceof java.util.Map) {
|
||||
devices.each {
|
||||
def value = "${it?.name}"
|
||||
def key = it?.did
|
||||
map["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
devices.each {
|
||||
def value = "${it?.name}"
|
||||
def key = it?.did
|
||||
map["${key}"] = value
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def getToken() {
|
||||
|
||||
state.token = "1234567890"
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
def syncronizeDevices() {
|
||||
poll(null)
|
||||
}
|
||||
|
||||
def getNetworkId(ipaddr, port) {
|
||||
"${ipaddr.tokenize('.').collect {String.format('%02X', it.toInteger())}.join()}:${String.format('%04X', port.toInteger())}"
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
Child Device Call In Methods
|
||||
**************************************************************************/
|
||||
def on(childDevice) {
|
||||
|
||||
def dni = childDevice.device.deviceNetworkId
|
||||
def data = ""
|
||||
def cmd = ""
|
||||
|
||||
data = "<gip><version>1</version><token>$state.token</token><did>${dni}</did><type>power</type><value>1</value></gip>"
|
||||
cmd = "DeviceSendCommand"
|
||||
|
||||
def qParams = [
|
||||
cmd: cmd,
|
||||
data: "${data}",
|
||||
fmt: "json"
|
||||
]
|
||||
|
||||
cmd = toQueryString(qParams)
|
||||
apiGet(cmd,"SendCommandHandler" )
|
||||
}
|
||||
|
||||
def off(childDevice) {
|
||||
|
||||
def dni = childDevice.device.deviceNetworkId
|
||||
def data = ""
|
||||
def cmd = ""
|
||||
|
||||
data = "<gip><version>1</version><token>$state.token</token><did>${dni}</did><type>power</type><value>0</value></gip>"
|
||||
cmd = "DeviceSendCommand"
|
||||
|
||||
def qParams = [
|
||||
cmd: cmd,
|
||||
data: "${data}",
|
||||
fmt: "json"
|
||||
]
|
||||
|
||||
cmd = toQueryString(qParams)
|
||||
apiGet(cmd,"SendCommandHandler")
|
||||
|
||||
}
|
||||
|
||||
def setLevel(childDevice, value) {
|
||||
debugOut "setLevel request from child device"
|
||||
|
||||
def dni = childDevice.device.deviceNetworkId
|
||||
def data = ""
|
||||
def cmd = ""
|
||||
|
||||
data = "<gip><version>1</version><token>${state.token}</token><did>${dni}</did><type>level</type><value>${value}</value></gip>"
|
||||
cmd = "DeviceSendCommand"
|
||||
|
||||
|
||||
def qParams = [
|
||||
cmd: cmd,
|
||||
data: "${data}",
|
||||
fmt: "json"
|
||||
]
|
||||
|
||||
cmd = toQueryString(qParams)
|
||||
|
||||
apiGet(cmd,"SendCommandHandler")
|
||||
}
|
||||
|
||||
def poll(childDevice) {
|
||||
infoOut "poll()"
|
||||
def eventTime = new Date().time
|
||||
if ((state.lastPollTime ?:0) + 10000 <= eventTime ){
|
||||
state.lastPollTime = new Date().time
|
||||
def Params = [
|
||||
cmd: "RoomGetCarousel",
|
||||
data: "<gip><version>1</version><token>${state.token}</token></gip>",
|
||||
fmt: "json"
|
||||
]
|
||||
def cmd = toQueryString(Params)
|
||||
apiGet(cmd,"RoomGetCarouselUpdateHandler")
|
||||
}else{
|
||||
infoOut "Multiple poll requests avoided"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************
|
||||
Msg Methods
|
||||
**************************************************************************/
|
||||
|
||||
def debugOut(msg) {
|
||||
//log.debug msg
|
||||
}
|
||||
|
||||
def traceOut(msg) {
|
||||
log.trace msg
|
||||
}
|
||||
|
||||
def infoOut(msg) {
|
||||
log.info msg
|
||||
}
|
||||
@@ -195,10 +195,7 @@ def registerDeviceChange() {
|
||||
state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt])
|
||||
log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}"
|
||||
} else if (!state.deviceSubscriptionMap[deviceId].contains(subscriptionEndpt)) {
|
||||
// state.deviceSubscriptionMap[deviceId] << subscriptionEndpt
|
||||
// For now, we will only have one subscription endpoint per device
|
||||
state.deviceSubscriptionMap.remove(deviceId)
|
||||
state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt])
|
||||
state.deviceSubscriptionMap[deviceId] << subscriptionEndpt
|
||||
log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}"
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ definition(
|
||||
|
||||
preferences {
|
||||
page(name: "selectButton")
|
||||
for (def i=1; i<=8; i++) {
|
||||
page(name: "configureButton$i")
|
||||
}
|
||||
page(name: "configureButton1")
|
||||
page(name: "configureButton2")
|
||||
page(name: "configureButton3")
|
||||
page(name: "configureButton4")
|
||||
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||
section {
|
||||
@@ -59,45 +60,22 @@ def selectButton() {
|
||||
}
|
||||
}
|
||||
|
||||
def createPage(pageNum) {
|
||||
if ((state.numButton == pageNum) || (pageNum == 8))
|
||||
state.installCondition = true
|
||||
dynamicPage(name: "configureButton$pageNum", title: "Set up button $pageNum here",
|
||||
nextPage: "configureButton${pageNum+1}", install: state.installCondition, uninstall: configured(), getButtonSections(pageNum))
|
||||
}
|
||||
|
||||
def configureButton1() {
|
||||
state.numButton = buttonDevice.currentState("numberOfButtons")?.longValue ?: 4
|
||||
log.debug "state variable numButton: ${state.numButton}"
|
||||
state.installCondition = false
|
||||
createPage(1)
|
||||
dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button",
|
||||
nextPage: "configureButton2", uninstall: configured(), getButtonSections(1))
|
||||
}
|
||||
def configureButton2() {
|
||||
createPage(2)
|
||||
dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here",
|
||||
nextPage: "configureButton3", uninstall: configured(), getButtonSections(2))
|
||||
}
|
||||
|
||||
def configureButton3() {
|
||||
createPage(3)
|
||||
dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here",
|
||||
nextPage: "configureButton4", uninstall: configured(), getButtonSections(3))
|
||||
}
|
||||
|
||||
def configureButton4() {
|
||||
createPage(4)
|
||||
}
|
||||
|
||||
def configureButton5() {
|
||||
createPage(5)
|
||||
}
|
||||
|
||||
def configureButton6() {
|
||||
createPage(6)
|
||||
}
|
||||
|
||||
def configureButton7() {
|
||||
createPage(7)
|
||||
}
|
||||
|
||||
def configureButton8() {
|
||||
createPage(8)
|
||||
dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here",
|
||||
install: true, uninstall: true, getButtonSections(4))
|
||||
}
|
||||
|
||||
def getButtonSections(buttonNumber) {
|
||||
|
||||
@@ -202,8 +202,7 @@ def inputSelectionPage() {
|
||||
|
||||
section("options variations") {
|
||||
paragraph "tap these elements and look at the differences when selecting an option"
|
||||
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", options: ["Thing 1", "Thing 2", "(Complicated) Thing 3"])
|
||||
input(type: "enum", name: "selectionSimpleGrouped", title: "Simple (Grouped) options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
|
||||
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
|
||||
input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
|
||||
}
|
||||
|
||||
@@ -215,15 +214,15 @@ def inputSelectionPage() {
|
||||
|
||||
section("segmented") {
|
||||
paragraph "segmented should only work if there are either 2 or 3 options to choose from"
|
||||
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", options: ["One"])
|
||||
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", options: ["One", "Two", "Three", "Four"])
|
||||
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
|
||||
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
|
||||
|
||||
paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
|
||||
input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
|
||||
input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
|
||||
|
||||
paragraph "specifying defaultValue still works with segmented selection elements"
|
||||
input(type: "enum", name: "selectionSegmentedWithDefault", style: "segmented", title: "defaulted to 'two'", options: ["One", "Two", "Three"], defaultValue: "Two")
|
||||
input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
|
||||
}
|
||||
|
||||
section("required: true") {
|
||||
@@ -232,8 +231,6 @@ def inputSelectionPage() {
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
|
||||
input(type: "enum", name: "selectionMultipleDefault1", title: "This allows multiple selections with a single default", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: "an option")
|
||||
input(type: "enum", name: "selectionMultipleDefault2", title: "This allows multiple selections with multiple defaults", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: ["an option", "another option"])
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
|
||||
@@ -72,7 +72,7 @@ def authPage() {
|
||||
log.debug "have LIFX access token"
|
||||
|
||||
def options = locationOptions() ?: []
|
||||
def count = options.size().toString()
|
||||
def count = options.size()
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
|
||||
section("Select your location") {
|
||||
|
||||
Reference in New Issue
Block a user