mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-17 13:10:52 +00:00
Compare commits
33 Commits
PROD_2016.
...
MSA-1147-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e16b662c4e | ||
|
|
3a0c9c1298 | ||
|
|
4a096fc884 | ||
|
|
1736caebfe | ||
|
|
0fa363fa1a | ||
|
|
0c5840087b | ||
|
|
a6ee53641f | ||
|
|
c6818c8c2b | ||
|
|
6ac174c2f3 | ||
|
|
eb8d5ed4c9 | ||
|
|
7b5d618de8 | ||
|
|
c024e09fb8 | ||
|
|
e5841fb3cb | ||
|
|
805b870447 | ||
|
|
fe92f7ad19 | ||
|
|
10245315ee | ||
|
|
0b239d4686 | ||
|
|
9374290d64 | ||
|
|
53406ada8e | ||
|
|
ffd0dd1545 | ||
|
|
5c1236a21a | ||
|
|
1e27dc1824 | ||
|
|
4bf3679942 | ||
|
|
c714720578 | ||
|
|
281fc939ac | ||
|
|
03c2dec425 | ||
|
|
38d0ca6170 | ||
|
|
836dd608c6 | ||
|
|
43e4db28eb | ||
|
|
df421a51ac | ||
|
|
3affdd21fc | ||
|
|
e61be4ff9c | ||
|
|
6123fbeea5 |
@@ -7,9 +7,10 @@ apply plugin: 'smartthings-hipchat'
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.3"
|
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.6"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
maven {
|
||||||
credentials {
|
credentials {
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ deployment:
|
|||||||
develop:
|
develop:
|
||||||
branch: master
|
branch: master
|
||||||
commands:
|
commands:
|
||||||
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Ps3BucketName=$S3_BUCKET_NAME_PREPROD_DEV
|
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Ps3Buckets="$S3_BUCKETS_DEV"
|
||||||
- ./gradlew hipchatSendNotification -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Pbranch=$CIRCLE_BRANCH
|
- ./gradlew hipchatSendNotification -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Pbranch=$CIRCLE_BRANCH
|
||||||
- ./gradlew hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
|
- ./gradlew hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
|
||||||
|
|
||||||
stage:
|
stage:
|
||||||
branch: staging
|
branch: staging
|
||||||
commands:
|
commands:
|
||||||
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Ps3BucketName=$S3_BUCKET_NAME_PREPROD_STAGING
|
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Ps3Buckets="$S3_BUCKETS_STAGE"
|
||||||
- ./gradlew hipchatSendNotification -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Pbranch=$CIRCLE_BRANCH
|
- ./gradlew hipchatSendNotification -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Pbranch=$CIRCLE_BRANCH
|
||||||
- ./gradlew hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
|
- ./gradlew hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
|
||||||
|
|||||||
194
devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy
Normal file
194
devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Iris Smart Fob
|
||||||
|
*
|
||||||
|
* Copyright 2015 Mitch Pond
|
||||||
|
* Presence code adapted from SmartThings Arrival Sensor HA device type
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Iris Smart Fob", namespace: "mitchpond", author: "Mitch Pond") {
|
||||||
|
capability "Battery"
|
||||||
|
capability "Button"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Presence Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
//fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0007,0020,0B05", outClusters: "0003,0006,0019", model:"3450-L", manufacturer: "CentraLite"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences{
|
||||||
|
input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
|
||||||
|
defaultValue: 3, displayDuringSetup: false)
|
||||||
|
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
||||||
|
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
||||||
|
input "logging", "bool", title: "Enable debug logging",
|
||||||
|
defaultValue: false, displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles(scale: 2) {
|
||||||
|
standardTile("presence", "device.presence", width: 4, height: 4, canChangeBackground: true) {
|
||||||
|
state "present", label: "Present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||||
|
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["presence"])
|
||||||
|
details(["presence","button","battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||||
|
logIt descMap
|
||||||
|
state.lastCheckin = now()
|
||||||
|
logIt "lastCheckin = ${state.lastCheckin}"
|
||||||
|
handlePresenceEvent(true)
|
||||||
|
|
||||||
|
def results = []
|
||||||
|
if (description?.startsWith('catchall:'))
|
||||||
|
results = parseCatchAllMessage(descMap)
|
||||||
|
else if (description?.startsWith('read attr -'))
|
||||||
|
results = parseReportAttributeMessage(descMap)
|
||||||
|
else logIt(descMap, "trace")
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
startTimer()
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure(){
|
||||||
|
logIt "Configuring Smart Fob..."
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 2 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 3 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 4 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 200"
|
||||||
|
] +
|
||||||
|
zigbee.configureReporting(0x0001,0x0020,0x20,20,20,0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseCatchAllMessage(descMap) {
|
||||||
|
if (descMap?.clusterId == "0006" && descMap?.command == "01") //button pressed
|
||||||
|
handleButtonPress(descMap.sourceEndpoint as int)
|
||||||
|
else if (descMap?.clusterId == "0006" && descMap?.command == "00") //button released
|
||||||
|
handleButtonRelease(descMap.sourceEndpoint as int)
|
||||||
|
else logIt("Parse: Unhandled message: ${descMap}","trace")
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseReportAttributeMessage(descMap) {
|
||||||
|
if (descMap?.cluster == "0001" && descMap?.attrId == "0020") createBatteryEvent(getBatteryLevel(descMap.value))
|
||||||
|
else logIt descMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBatteryEvent(percent) {
|
||||||
|
logIt "Battery level at " + percent
|
||||||
|
return createEvent([name: "battery", value: percent])
|
||||||
|
}
|
||||||
|
|
||||||
|
//this method determines if a press should count as a push or a hold and returns the relevant event type
|
||||||
|
private handleButtonRelease(button) {
|
||||||
|
logIt "lastPress state variable: ${state.lastPress}"
|
||||||
|
def sequenceError = {logIt("Uh oh...missed a message? Dropping this event.", "error"); state.lastPress = null; return []}
|
||||||
|
|
||||||
|
if (!state.lastPress) return sequenceError()
|
||||||
|
else if (state.lastPress.button != button) return sequenceError()
|
||||||
|
|
||||||
|
def currentTime = now()
|
||||||
|
def startOfPress = state.lastPress?.time
|
||||||
|
def timeDif = currentTime - startOfPress
|
||||||
|
def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000
|
||||||
|
|
||||||
|
state.lastPress = null //we're done with this. clear it to make error conditions easier to catch
|
||||||
|
|
||||||
|
if (timeDif < 0)
|
||||||
|
//likely a message sequence issue or dropped packet. Drop this press and wait for another.
|
||||||
|
return sequenceError()
|
||||||
|
else if (timeDif < holdTimeMillisec)
|
||||||
|
return createButtonEvent(button,"pushed")
|
||||||
|
else
|
||||||
|
return createButtonEvent(button,"held")
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleButtonPress(button) {
|
||||||
|
state.lastPress = [button: button, time: now()]
|
||||||
|
}
|
||||||
|
|
||||||
|
private createButtonEvent(button,action) {
|
||||||
|
logIt "Button ${button} ${action}"
|
||||||
|
return createEvent([
|
||||||
|
name: "button",
|
||||||
|
value: action,
|
||||||
|
data:[buttonNumber: button],
|
||||||
|
descriptionText: "${device.displayName} button ${button} was ${action}",
|
||||||
|
isStateChange: true,
|
||||||
|
displayed: true])
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBatteryLevel(rawValue) {
|
||||||
|
def intValue = Integer.parseInt(rawValue,16)
|
||||||
|
def min = 2.1
|
||||||
|
def max = 3.0
|
||||||
|
def vBatt = intValue / 10
|
||||||
|
return ((vBatt - min) / (max - min) * 100) as int
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePresenceEvent(present) {
|
||||||
|
def wasPresent = device.currentState("presence")?.value == "present"
|
||||||
|
if (!wasPresent && present) {
|
||||||
|
logIt "Sensor is present"
|
||||||
|
startTimer()
|
||||||
|
} else if (!present) {
|
||||||
|
logIt "Sensor is not present"
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def eventMap = [
|
||||||
|
name: "presence",
|
||||||
|
value: present ? "present" : "not present",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}",
|
||||||
|
]
|
||||||
|
logIt "Creating presence event: ${eventMap}"
|
||||||
|
sendEvent(eventMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer() {
|
||||||
|
logIt "Scheduling periodic timer"
|
||||||
|
schedule("0 * * * * ?", checkPresenceCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTimer() {
|
||||||
|
logIt "Stopping periodic timer"
|
||||||
|
unschedule()
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkPresenceCallback() {
|
||||||
|
def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000
|
||||||
|
def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60
|
||||||
|
logIt "Sensor checked in ${timeSinceLastCheckin} seconds ago"
|
||||||
|
if (timeSinceLastCheckin >= theCheckInterval) {
|
||||||
|
handlePresenceEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****** Utility functions ******
|
||||||
|
|
||||||
|
private logIt(str, logLevel = 'debug') {if (settings.logging) log."$logLevel"(str) }
|
||||||
227
devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy
Normal file
227
devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/**
|
||||||
|
* Hue Bloom
|
||||||
|
*
|
||||||
|
* Philips Hue Type "Color Light"
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*/
|
||||||
|
|
||||||
|
// for the UI
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "Hue Bloom", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "setAdjustedColor"
|
||||||
|
command "reset"
|
||||||
|
command "refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles (scale: 2){
|
||||||
|
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff"
|
||||||
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn"
|
||||||
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
|
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||||
|
attributeState "color", action:"setAdjustedColor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["rich-control"])
|
||||||
|
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(description) {
|
||||||
|
log.debug "parse() - $description"
|
||||||
|
def results = []
|
||||||
|
|
||||||
|
def map = description
|
||||||
|
if (description instanceof String) {
|
||||||
|
log.debug "Hue Bulb stringToMap - ${map}"
|
||||||
|
map = stringToMap(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map?.name && map?.value) {
|
||||||
|
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
void on() {
|
||||||
|
log.trace parent.on(this)
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
void off() {
|
||||||
|
log.trace parent.off(this)
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextLevel() {
|
||||||
|
def level = device.latestValue("level") as Integer ?: 0
|
||||||
|
if (level <= 100) {
|
||||||
|
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
level = 25
|
||||||
|
}
|
||||||
|
setLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLevel(percent) {
|
||||||
|
log.debug "Executing 'setLevel'"
|
||||||
|
if (verifyPercent(percent)) {
|
||||||
|
parent.setLevel(this, percent)
|
||||||
|
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSaturation(percent) {
|
||||||
|
log.debug "Executing 'setSaturation'"
|
||||||
|
if (verifyPercent(percent)) {
|
||||||
|
parent.setSaturation(this, percent)
|
||||||
|
sendEvent(name: "saturation", value: percent, displayed: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHue(percent) {
|
||||||
|
log.debug "Executing 'setHue'"
|
||||||
|
if (verifyPercent(percent)) {
|
||||||
|
parent.setHue(this, percent)
|
||||||
|
sendEvent(name: "hue", value: percent, displayed: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setColor(value) {
|
||||||
|
log.debug "setColor: ${value}, $this"
|
||||||
|
def events = []
|
||||||
|
def validValues = [:]
|
||||||
|
|
||||||
|
if (verifyPercent(value.hue)) {
|
||||||
|
events << createEvent(name: "hue", value: value.hue, displayed: false)
|
||||||
|
validValues.hue = value.hue
|
||||||
|
}
|
||||||
|
if (verifyPercent(value.saturation)) {
|
||||||
|
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||||
|
validValues.saturation = value.saturation
|
||||||
|
}
|
||||||
|
if (value.hex != null) {
|
||||||
|
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
||||||
|
events << createEvent(name: "color", value: value.hex)
|
||||||
|
validValues.hex = value.hex
|
||||||
|
} else {
|
||||||
|
log.warn "$value.hex is not a valid color"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifyPercent(value.level)) {
|
||||||
|
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
|
||||||
|
validValues.level = value.level
|
||||||
|
}
|
||||||
|
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
||||||
|
events << createEvent(name: "switch", value: "off")
|
||||||
|
validValues.switch = "off"
|
||||||
|
} else {
|
||||||
|
events << createEvent(name: "switch", value: "on")
|
||||||
|
validValues.switch = "on"
|
||||||
|
}
|
||||||
|
if (!events.isEmpty()) {
|
||||||
|
parent.setColor(this, validValues)
|
||||||
|
}
|
||||||
|
events.each {
|
||||||
|
sendEvent(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
log.debug "Executing 'reset'"
|
||||||
|
def value = [level:100, saturation:56, hue:23]
|
||||||
|
setAdjustedColor(value)
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAdjustedColor(value) {
|
||||||
|
if (value) {
|
||||||
|
log.trace "setAdjustedColor: ${value}"
|
||||||
|
def adjusted = value + [:]
|
||||||
|
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||||
|
// Needed because color picker always sends 100
|
||||||
|
adjusted.level = null
|
||||||
|
setColor(adjusted)
|
||||||
|
} else {
|
||||||
|
log.warn "Invalid color input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setColorTemperature(value) {
|
||||||
|
if (value) {
|
||||||
|
log.trace "setColorTemperature: ${value}k"
|
||||||
|
parent.setColorTemperature(this, value)
|
||||||
|
sendEvent(name: "colorTemperature", value: value)
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
} else {
|
||||||
|
log.warn "Invalid color temperature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
log.debug "Executing 'refresh'"
|
||||||
|
parent.manualRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def adjustOutgoingHue(percent) {
|
||||||
|
def adjusted = percent
|
||||||
|
if (percent > 31) {
|
||||||
|
if (percent < 63.0) {
|
||||||
|
adjusted = percent + (7 * (percent -30 ) / 32)
|
||||||
|
}
|
||||||
|
else if (percent < 73.0) {
|
||||||
|
adjusted = 69 + (5 * (percent - 62) / 10)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
adjusted = percent + (2 * (100 - percent) / 28)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info "percent: $percent, adjusted: $adjusted"
|
||||||
|
adjusted
|
||||||
|
}
|
||||||
|
|
||||||
|
def verifyPercent(percent) {
|
||||||
|
if (percent == null)
|
||||||
|
return false
|
||||||
|
else if (percent >= 0 && percent <= 100) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
log.warn "$percent is not 0-100"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,16 +17,13 @@ metadata {
|
|||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"rich-control"){
|
multiAttributeTile(name:"rich-control"){
|
||||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
tileAttribute ("", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
||||||
}
|
}
|
||||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||||
attributeState "default", label:'SN: ${currentValue}'
|
attributeState "default", label:'SN: ${currentValue}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
|
||||||
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
|
|
||||||
}
|
|
||||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||||
state "default", label:'SN: ${currentValue}'
|
state "default", label:'SN: ${currentValue}'
|
||||||
}
|
}
|
||||||
@@ -34,7 +31,7 @@ metadata {
|
|||||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||||
}
|
}
|
||||||
|
|
||||||
main (["icon"])
|
main (["rich-control"])
|
||||||
details(["rich-control", "networkAddress"])
|
details(["rich-control", "networkAddress"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +72,7 @@ def parse(description) {
|
|||||||
}
|
}
|
||||||
else if (contentType?.contains("xml")) {
|
else if (contentType?.contains("xml")) {
|
||||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||||
|
parent.hubVerification(device.hub.id, msg.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Hue Bulb
|
* Hue Bulb
|
||||||
*
|
*
|
||||||
|
* Philips Hue Type "Extended Color Light"
|
||||||
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// for the UI
|
// for the UI
|
||||||
metadata {
|
metadata {
|
||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
@@ -27,10 +30,10 @@ metadata {
|
|||||||
tiles (scale: 2){
|
tiles (scale: 2){
|
||||||
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff"
|
||||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn"
|
||||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff"
|
||||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn"
|
||||||
}
|
}
|
||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||||
@@ -43,16 +46,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
|
||||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
|
||||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
|
||||||
}
|
|
||||||
|
|
||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
||||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "colorTemperature", label: '${currentValue} K'
|
state "colorTemperature", label: '${currentValue} K'
|
||||||
}
|
}
|
||||||
@@ -60,29 +57,12 @@ metadata {
|
|||||||
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
|
||||||
|
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
|
||||||
state "level", action:"switch level.setLevel"
|
|
||||||
}
|
|
||||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
|
||||||
state "level", label: 'Level ${currentValue}%'
|
|
||||||
}
|
|
||||||
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
|
|
||||||
state "saturation", action:"color control.setSaturation"
|
|
||||||
}
|
|
||||||
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
|
|
||||||
state "saturation", label: 'Sat ${currentValue} '
|
|
||||||
}
|
|
||||||
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
|
|
||||||
state "hue", action:"color control.setHue"
|
|
||||||
}
|
|
||||||
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
|
||||||
state "hue", label: 'Hue ${currentValue} '
|
|
||||||
}
|
|
||||||
|
|
||||||
main(["switch"])
|
main(["rich-control"])
|
||||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
|
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,11 +71,13 @@ metadata {
|
|||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
def results = []
|
def results = []
|
||||||
|
|
||||||
def map = description
|
def map = description
|
||||||
if (description instanceof String) {
|
if (description instanceof String) {
|
||||||
log.debug "Hue Bulb stringToMap - ${map}"
|
log.debug "Hue Bulb stringToMap - ${map}"
|
||||||
map = stringToMap(description)
|
map = stringToMap(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map?.name && map?.value) {
|
if (map?.name && map?.value) {
|
||||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||||
}
|
}
|
||||||
@@ -125,62 +107,104 @@ void nextLevel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setLevel(percent) {
|
void setLevel(percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
parent.setLevel(this, percent)
|
if (verifyPercent(percent)) {
|
||||||
sendEvent(name: "level", value: percent)
|
parent.setLevel(this, percent)
|
||||||
|
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSaturation(percent) {
|
void setSaturation(percent) {
|
||||||
log.debug "Executing 'setSaturation'"
|
log.debug "Executing 'setSaturation'"
|
||||||
parent.setSaturation(this, percent)
|
if (verifyPercent(percent)) {
|
||||||
sendEvent(name: "saturation", value: percent)
|
parent.setSaturation(this, percent)
|
||||||
|
sendEvent(name: "saturation", value: percent, displayed: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHue(percent) {
|
void setHue(percent) {
|
||||||
log.debug "Executing 'setHue'"
|
log.debug "Executing 'setHue'"
|
||||||
parent.setHue(this, percent)
|
if (verifyPercent(percent)) {
|
||||||
sendEvent(name: "hue", value: percent)
|
parent.setHue(this, percent)
|
||||||
|
sendEvent(name: "hue", value: percent, displayed: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setColor(value) {
|
void setColor(value) {
|
||||||
log.debug "setColor: ${value}, $this"
|
log.debug "setColor: ${value}, $this"
|
||||||
parent.setColor(this, value)
|
def events = []
|
||||||
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
def validValues = [:]
|
||||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
|
||||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
if (verifyPercent(value.hue)) {
|
||||||
if (value.level) { sendEvent(name: "level", value: value.level)}
|
events << createEvent(name: "hue", value: value.hue, displayed: false)
|
||||||
if (value.switch) { sendEvent(name: "switch", value: value.switch)}
|
validValues.hue = value.hue
|
||||||
|
}
|
||||||
|
if (verifyPercent(value.saturation)) {
|
||||||
|
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||||
|
validValues.saturation = value.saturation
|
||||||
|
}
|
||||||
|
if (value.hex != null) {
|
||||||
|
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
||||||
|
events << createEvent(name: "color", value: value.hex)
|
||||||
|
validValues.hex = value.hex
|
||||||
|
} else {
|
||||||
|
log.warn "$value.hex is not a valid color"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifyPercent(value.level)) {
|
||||||
|
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
|
||||||
|
validValues.level = value.level
|
||||||
|
}
|
||||||
|
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
||||||
|
events << createEvent(name: "switch", value: "off")
|
||||||
|
validValues.switch = "off"
|
||||||
|
} else {
|
||||||
|
events << createEvent(name: "switch", value: "on")
|
||||||
|
validValues.switch = "on"
|
||||||
|
}
|
||||||
|
if (!events.isEmpty()) {
|
||||||
|
parent.setColor(this, validValues)
|
||||||
|
}
|
||||||
|
events.each {
|
||||||
|
sendEvent(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
log.debug "Executing 'reset'"
|
log.debug "Executing 'reset'"
|
||||||
def value = [level:100, hex:"#90C638", saturation:56, hue:23]
|
def value = [level:100, saturation:56, hue:23]
|
||||||
setAdjustedColor(value)
|
setAdjustedColor(value)
|
||||||
parent.poll()
|
parent.poll()
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAdjustedColor(value) {
|
void setAdjustedColor(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
log.trace "setAdjustedColor: ${value}"
|
log.trace "setAdjustedColor: ${value}"
|
||||||
def adjusted = value + [:]
|
def adjusted = value + [:]
|
||||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||||
// Needed because color picker always sends 100
|
// Needed because color picker always sends 100
|
||||||
adjusted.level = null
|
adjusted.level = null
|
||||||
setColor(adjusted)
|
setColor(adjusted)
|
||||||
|
} else {
|
||||||
|
log.warn "Invalid color input"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setColorTemperature(value) {
|
void setColorTemperature(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
log.trace "setColorTemperature: ${value}k"
|
log.trace "setColorTemperature: ${value}k"
|
||||||
parent.setColorTemperature(this, value)
|
parent.setColorTemperature(this, value)
|
||||||
sendEvent(name: "colorTemperature", value: value)
|
sendEvent(name: "colorTemperature", value: value)
|
||||||
}
|
sendEvent(name: "switch", value: "on")
|
||||||
|
} else {
|
||||||
|
log.warn "Invalid color temperature"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
parent.manualRefresh()
|
parent.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def adjustOutgoingHue(percent) {
|
def adjustOutgoingHue(percent) {
|
||||||
@@ -199,3 +223,14 @@ def adjustOutgoingHue(percent) {
|
|||||||
log.info "percent: $percent, adjusted: $adjusted"
|
log.info "percent: $percent, adjusted: $adjusted"
|
||||||
adjusted
|
adjusted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def verifyPercent(percent) {
|
||||||
|
if (percent == null)
|
||||||
|
return false
|
||||||
|
else if (percent >= 0 && percent <= 100) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
log.warn "$percent is not 0-100"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Hue Lux Bulb
|
* Hue Lux Bulb
|
||||||
*
|
*
|
||||||
|
* Philips Hue Type "Dimmable Light"
|
||||||
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
*/
|
*/
|
||||||
// for the UI
|
// for the UI
|
||||||
@@ -23,10 +25,10 @@ metadata {
|
|||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
|
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
|
||||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff"
|
||||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn"
|
||||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff"
|
||||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn"
|
||||||
}
|
}
|
||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||||
@@ -36,13 +38,6 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
|
||||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
|
||||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
|
||||||
}
|
|
||||||
|
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
state "level", action:"switch level.setLevel"
|
state "level", action:"switch level.setLevel"
|
||||||
}
|
}
|
||||||
@@ -51,7 +46,7 @@ metadata {
|
|||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main(["rich-control"])
|
||||||
details(["rich-control", "refresh"])
|
details(["rich-control", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,19 +70,24 @@ def parse(description) {
|
|||||||
|
|
||||||
// handle commands
|
// handle commands
|
||||||
void on() {
|
void on() {
|
||||||
parent.on(this)
|
log.trace parent.on(this)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
}
|
}
|
||||||
|
|
||||||
void off() {
|
void off() {
|
||||||
parent.off(this)
|
log.trace parent.off(this)
|
||||||
sendEvent(name: "switch", value: "off")
|
sendEvent(name: "switch", value: "off")
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLevel(percent) {
|
void setLevel(percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
parent.setLevel(this, percent)
|
if (percent != null && percent >= 0 && percent <= 100) {
|
||||||
sendEvent(name: "level", value: percent)
|
parent.setLevel(this, percent)
|
||||||
|
sendEvent(name: "level", value: percent)
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
} else {
|
||||||
|
log.warn "$percent is not 0-100"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
|
|||||||
@@ -56,21 +56,17 @@ metadata {
|
|||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
|
||||||
def resultMap = zigbee.getKnownDescription(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (resultMap) {
|
if (event) {
|
||||||
log.info resultMap
|
log.info event
|
||||||
if (resultMap.type == "update") {
|
if (event.name == "power") {
|
||||||
log.info "$device updates: ${resultMap.value}"
|
|
||||||
}
|
|
||||||
else if (resultMap.type == "power") {
|
|
||||||
def powerValue
|
|
||||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration
|
||||||
sendEvent(name: "power", value: powerValue)
|
sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -51,15 +51,9 @@ metadata {
|
|||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
|
||||||
def resultMap = zigbee.getKnownDescription(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (resultMap) {
|
if (event) {
|
||||||
log.info resultMap
|
sendEvent(event)
|
||||||
if (resultMap.type == "update") {
|
|
||||||
log.info "$device updates: ${resultMap.value}"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
|||||||
@@ -83,32 +83,19 @@ def uninstalled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
/*
|
|
||||||
def cmds =
|
def cmds =
|
||||||
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
|
zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,
|
||||||
"${TYPE_ENUM8}", 0, 3600, "{01}") +
|
TYPE_ENUM8, 0, 3600, null) +
|
||||||
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
|
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
|
||||||
"${TYPE_U8}", 600, 21600, "{01}")
|
TYPE_U8, 600, 21600, 0x01)
|
||||||
*/
|
|
||||||
def zigbeeId = device.zigbeeId
|
|
||||||
def cmds =
|
|
||||||
[
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
|
|
||||||
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
|
|
||||||
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
|
||||||
]
|
|
||||||
log.info "configure() --- cmds: $cmds"
|
log.info "configure() --- cmds: $cmds"
|
||||||
return cmds + refresh() // send refresh cmds as part of config
|
return cmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
def cmds =
|
def cmds =
|
||||||
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
|
zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) +
|
||||||
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
|
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
|
||||||
log.info "refresh() --- cmds: $cmds"
|
log.info "refresh() --- cmds: $cmds"
|
||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
@@ -121,34 +108,27 @@ def parse(String description) {
|
|||||||
map = parseReportAttributeMessage(description)
|
map = parseReportAttributeMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug "parse() --- Parse returned $map"
|
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
log.debug "parse() --- returned: $result"
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock capability commands
|
// Lock capability commands
|
||||||
def lock() {
|
def lock() {
|
||||||
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR)
|
||||||
//log.info "lock() -- cmds: $cmds"
|
log.info "lock() -- cmds: $cmds"
|
||||||
//return cmds
|
return cmds
|
||||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def unlock() {
|
def unlock() {
|
||||||
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR)
|
||||||
//log.info "unlock() -- cmds: $cmds"
|
log.info "unlock() -- cmds: $cmds"
|
||||||
//return cmds
|
return cmds
|
||||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private methods
|
// Private methods
|
||||||
private Map parseReportAttributeMessage(String description) {
|
private Map parseReportAttributeMessage(String description) {
|
||||||
log.trace "parseReportAttributeMessage() --- description: $description"
|
|
||||||
|
|
||||||
Map descMap = zigbee.parseDescriptionAsMap(description)
|
Map descMap = zigbee.parseDescriptionAsMap(description)
|
||||||
|
|
||||||
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
|
|
||||||
|
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
||||||
resultMap.name = "battery"
|
resultMap.name = "battery"
|
||||||
@@ -156,18 +136,24 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
|
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
|
||||||
resultMap.value = Integer.parseInt(descMap.value, 16)
|
resultMap.value = Integer.parseInt(descMap.value, 16)
|
||||||
}
|
}
|
||||||
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
|
|
||||||
}
|
}
|
||||||
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
||||||
def value = Integer.parseInt(descMap.value, 16)
|
def value = Integer.parseInt(descMap.value, 16)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
resultMap.name = "lock"
|
resultMap.name = "lock"
|
||||||
resultMap.putAll([0:["value":"unknown",
|
if (value == 0) {
|
||||||
"descriptionText":"Not fully locked"],
|
resultMap.value = "unknown"
|
||||||
1:["value":"locked"],
|
resultMap.descriptionText = "${linkText} is not fully locked"
|
||||||
2:["value":"unlocked"]].get(value,
|
} else if (value == 1) {
|
||||||
["value":"unknown",
|
resultMap.value = "locked"
|
||||||
"descriptionText":"Unknown lock state"]))
|
resultMap.descriptionText = "${linkText} is locked"
|
||||||
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
|
} else if (value == 2) {
|
||||||
|
resultMap.value = "unlocked"
|
||||||
|
resultMap.descriptionText = "${linkText} is unlocked"
|
||||||
|
} else {
|
||||||
|
resultMap.value = "unknown"
|
||||||
|
resultMap.descriptionText = "${linkText} is in unknown lock state"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.debug "parseReportAttributeMessage() --- ignoring attribute"
|
log.debug "parseReportAttributeMessage() --- ignoring attribute"
|
||||||
|
|||||||
@@ -51,22 +51,15 @@ metadata {
|
|||||||
// Parse incoming device messages to generate events
|
// Parse incoming device messages to generate events
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
def event = zigbee.getEvent(description)
|
||||||
def resultMap = zigbee.getKnownDescription(description)
|
if (event) {
|
||||||
if (resultMap) {
|
if (event.name == "power") {
|
||||||
log.info resultMap
|
|
||||||
if (resultMap.type == "update") {
|
|
||||||
log.info "$device updates: ${resultMap.value}"
|
|
||||||
}
|
|
||||||
else if (resultMap.type == "power") {
|
|
||||||
def powerValue
|
def powerValue
|
||||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
sendEvent(name: "power", value: powerValue)
|
||||||
sendEvent(name: "power", value: powerValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -53,16 +53,9 @@ metadata {
|
|||||||
// Parse incoming device messages to generate events
|
// Parse incoming device messages to generate events
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
def event = zigbee.getEvent(description)
|
||||||
def resultMap = zigbee.getKnownDescription(description)
|
if (event) {
|
||||||
if (resultMap) {
|
sendEvent(event)
|
||||||
log.info resultMap
|
|
||||||
if (resultMap.type == "update") {
|
|
||||||
log.info "$device updates: ${resultMap.value}"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
|||||||
@@ -73,16 +73,9 @@ metadata {
|
|||||||
// Parse incoming device messages to generate events
|
// Parse incoming device messages to generate events
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
def event = zigbee.getEvent(description)
|
||||||
def finalResult = zigbee.getKnownDescription(description)
|
if (event) {
|
||||||
if (finalResult) {
|
sendEvent(event)
|
||||||
log.info finalResult
|
|
||||||
if (finalResult.type == "update") {
|
|
||||||
log.info "$device updates: ${finalResult.value}"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ def retypeBasedOnMSR() {
|
|||||||
break
|
break
|
||||||
case "011F-0001-0001": // Schlage motion
|
case "011F-0001-0001": // Schlage motion
|
||||||
case "014A-0001-0001": // Ecolink motion
|
case "014A-0001-0001": // Ecolink motion
|
||||||
|
case "014A-0004-0001": // Ecolink motion +
|
||||||
case "0060-0001-0002": // Everspring SP814
|
case "0060-0001-0002": // Everspring SP814
|
||||||
case "0060-0001-0003": // Everspring HSP02
|
case "0060-0001-0003": // Everspring HSP02
|
||||||
case "011A-0601-0901": // Enerwave ZWN-BPC
|
case "011A-0601-0901": // Enerwave ZWN-BPC
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def parse(String description) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
def sensorValueEvent(Short value) {
|
def sensorValueEvent(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
|
createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
|
||||||
} else {
|
} else {
|
||||||
@@ -94,24 +94,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
|||||||
{
|
{
|
||||||
def result = []
|
def result = []
|
||||||
if (cmd.notificationType == 0x07) {
|
if (cmd.notificationType == 0x07) {
|
||||||
if (cmd.event == 0x01 || cmd.event == 0x02) {
|
if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors
|
||||||
|
result << sensorValueEvent(cmd.v1AlarmLevel)
|
||||||
|
} else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) {
|
||||||
result << sensorValueEvent(1)
|
result << sensorValueEvent(1)
|
||||||
|
} else if (cmd.event == 0x00) {
|
||||||
|
result << sensorValueEvent(0)
|
||||||
} else if (cmd.event == 0x03) {
|
} else if (cmd.event == 0x03) {
|
||||||
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
||||||
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
|
result << response(zwave.batteryV1.batteryGet())
|
||||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
|
||||||
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
|
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
|
||||||
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
|
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
|
||||||
} else if (cmd.event == 0x07) {
|
|
||||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
|
||||||
result << sensorValueEvent(1)
|
|
||||||
}
|
}
|
||||||
} else if (cmd.notificationType) {
|
} else if (cmd.notificationType) {
|
||||||
def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}"
|
def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}"
|
||||||
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false)
|
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false)
|
||||||
} else {
|
} else {
|
||||||
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
|
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
|
||||||
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false)
|
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false)
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,37 +61,44 @@ def parse(String description) {
|
|||||||
zwaveEvent(cmd, results)
|
zwaveEvent(cmd, results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// log.debug "\"$description\" parsed to ${results.inspect()}"
|
log.debug "'$description' parsed to ${results.inspect()}"
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def createSmokeOrCOEvents(name, results) {
|
def createSmokeOrCOEvents(name, results) {
|
||||||
def text = null
|
def text = null
|
||||||
if (name == "smoke") {
|
switch (name) {
|
||||||
text = "$device.displayName smoke was detected!"
|
case "smoke":
|
||||||
// these are displayed:false because the composite event is the one we want to see in the app
|
text = "$device.displayName smoke was detected!"
|
||||||
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
|
// these are displayed:false because the composite event is the one we want to see in the app
|
||||||
} else if (name == "carbonMonoxide") {
|
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
|
||||||
text = "$device.displayName carbon monoxide was detected!"
|
break
|
||||||
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
|
case "carbonMonoxide":
|
||||||
} else if (name == "tested") {
|
text = "$device.displayName carbon monoxide was detected!"
|
||||||
text = "$device.displayName was tested"
|
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
|
||||||
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
|
break
|
||||||
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
|
case "tested":
|
||||||
} else if (name == "smokeClear") {
|
text = "$device.displayName was tested"
|
||||||
text = "$device.displayName smoke is clear"
|
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
|
||||||
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
|
||||||
name = "clear"
|
break
|
||||||
} else if (name == "carbonMonoxideClear") {
|
case "smokeClear":
|
||||||
text = "$device.displayName carbon monoxide is clear"
|
text = "$device.displayName smoke is clear"
|
||||||
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
|
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
||||||
name = "clear"
|
name = "clear"
|
||||||
} else if (name == "testClear") {
|
break
|
||||||
text = "$device.displayName smoke is clear"
|
case "carbonMonoxideClear":
|
||||||
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
text = "$device.displayName carbon monoxide is clear"
|
||||||
results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
|
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
|
||||||
name = "clear"
|
name = "clear"
|
||||||
|
break
|
||||||
|
case "testClear":
|
||||||
|
text = "$device.displayName test cleared"
|
||||||
|
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
||||||
|
results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
|
||||||
|
name = "clear"
|
||||||
|
break
|
||||||
}
|
}
|
||||||
// This composite event is used for updating the tile
|
// This composite event is used for updating the tile
|
||||||
results << createEvent(name: "alarmState", value: name, descriptionText: text)
|
results << createEvent(name: "alarmState", value: name, descriptionText: text)
|
||||||
@@ -117,8 +124,10 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
|
|||||||
createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results)
|
createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results)
|
||||||
break
|
break
|
||||||
case 13: // sent every hour -- not sure what this means, just a wake up notification?
|
case 13: // sent every hour -- not sure what this means, just a wake up notification?
|
||||||
if (cmd.alarmLevel != 255) {
|
if (cmd.alarmLevel == 255) {
|
||||||
results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true)
|
results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false)
|
||||||
|
} else {
|
||||||
|
results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear smoke in case they pulled batteries and we missed the clear msg
|
// Clear smoke in case they pulled batteries and we missed the clear msg
|
||||||
@@ -127,9 +136,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check battery if we don't have a recent battery event
|
// Check battery if we don't have a recent battery event
|
||||||
def prevBattery = device.currentState("battery")
|
if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) {
|
||||||
if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) {
|
results << response(zwave.batteryV1.batteryGet())
|
||||||
results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@@ -158,12 +166,17 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd,
|
|||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) {
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) {
|
||||||
results << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
|
||||||
results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)
|
results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)
|
||||||
|
if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) {
|
||||||
|
results << response(zwave.batteryV1.batteryGet(), "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
|
} else {
|
||||||
|
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) {
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) {
|
||||||
def map = [ name: "battery", unit: "%" ]
|
def map = [ name: "battery", unit: "%", isStateChange: true ]
|
||||||
|
state.lastbatt = now()
|
||||||
if (cmd.batteryLevel == 0xFF) {
|
if (cmd.batteryLevel == 0xFF) {
|
||||||
map.value = 1
|
map.value = 1
|
||||||
map.descriptionText = "$device.displayName battery is low!"
|
map.descriptionText = "$device.displayName battery is low!"
|
||||||
|
|||||||
71
devicetypes/swarmx/swarmx1.src/swarmx1.groovy
Normal file
71
devicetypes/swarmx/swarmx1.src/swarmx1.groovy
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* swarmx1
|
||||||
|
*
|
||||||
|
* Copyright 2016 Badrinarayanan Rangarajan
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "swarmx1", namespace: "swarmx", author: "Badrinarayanan Rangarajan", oauth: true) {
|
||||||
|
capability "Video Camera"
|
||||||
|
capability "Video Capture"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
// TODO: define your main and details tiles here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
// TODO: handle 'camera' attribute
|
||||||
|
// TODO: handle 'statusMessage' attribute
|
||||||
|
// TODO: handle 'mute' attribute
|
||||||
|
// TODO: handle 'settings' attribute
|
||||||
|
// TODO: handle 'clip' attribute
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def on() {
|
||||||
|
log.debug "Executing 'on'"
|
||||||
|
// TODO: handle 'on' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "Executing 'off'"
|
||||||
|
// TODO: handle 'off' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def mute() {
|
||||||
|
log.debug "Executing 'mute'"
|
||||||
|
// TODO: handle 'mute' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def unmute() {
|
||||||
|
log.debug "Executing 'unmute'"
|
||||||
|
// TODO: handle 'unmute' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def flip() {
|
||||||
|
log.debug "Executing 'flip'"
|
||||||
|
// TODO: handle 'flip' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def capture() {
|
||||||
|
log.debug "Executing 'capture'"
|
||||||
|
// TODO: handle 'capture' command
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ definition(
|
|||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
|
||||||
//singleInstance: true
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -68,7 +68,7 @@ def bridgeDiscovery(params=[:])
|
|||||||
}
|
}
|
||||||
|
|
||||||
//setup.xml request every 3 seconds except on discoveries
|
//setup.xml request every 3 seconds except on discoveries
|
||||||
if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) {
|
if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) {
|
||||||
verifyHueBridges()
|
verifyHueBridges()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ private sendDeveloperReq() {
|
|||||||
headers: [
|
headers: [
|
||||||
HOST: host
|
HOST: host
|
||||||
],
|
],
|
||||||
body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}"))
|
body: [devicetype: "$token-0"]], "${selectedHue}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private discoverHueBulbs() {
|
private discoverHueBulbs() {
|
||||||
@@ -175,6 +175,7 @@ private discoverHueBulbs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private verifyHueBridge(String deviceNetworkId, String host) {
|
private verifyHueBridge(String deviceNetworkId, String host) {
|
||||||
|
log.trace "Verify Hue Bridge $deviceNetworkId"
|
||||||
sendHubCommand(new physicalgraph.device.HubAction([
|
sendHubCommand(new physicalgraph.device.HubAction([
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/description.xml",
|
path: "/description.xml",
|
||||||
@@ -288,7 +289,7 @@ def bulbListHandler(hub, data = "") {
|
|||||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||||
object.each { k,v ->
|
object.each { k,v ->
|
||||||
if (v instanceof Map)
|
if (v instanceof Map)
|
||||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def bridge = null
|
def bridge = null
|
||||||
@@ -299,6 +300,40 @@ def bulbListHandler(hub, data = "") {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private upgradeDeviceType(device, newHueType) {
|
||||||
|
def deviceType = getDeviceType(newHueType)
|
||||||
|
|
||||||
|
// Automatically change users Hue bulbs to correct device types
|
||||||
|
if (deviceType && !(device?.typeName?.equalsIgnoreCase(deviceType))) {
|
||||||
|
log.debug "Update device type: \"$device.label\" ${device?.typeName}->$deviceType"
|
||||||
|
device.setDeviceType(deviceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDeviceType(hueType) {
|
||||||
|
// Determine ST device type based on Hue classification of light
|
||||||
|
if (hueType?.equalsIgnoreCase("Dimmable light"))
|
||||||
|
return "Hue Lux Bulb"
|
||||||
|
else if (hueType?.equalsIgnoreCase("Extended Color Light"))
|
||||||
|
return "Hue Bulb"
|
||||||
|
else if (hueType?.equalsIgnoreCase("Color Light"))
|
||||||
|
return "Hue Bloom"
|
||||||
|
else
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private addChildBulb(dni, hueType, name, hub, update=false, device = null) {
|
||||||
|
def deviceType = getDeviceType(hueType)
|
||||||
|
|
||||||
|
if (deviceType) {
|
||||||
|
return addChildDevice("smartthings", deviceType, dni, hub, ["label": name])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "Device type $hueType not supported"
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def addBulbs() {
|
def addBulbs() {
|
||||||
def bulbs = getHueBulbs()
|
def bulbs = getHueBulbs()
|
||||||
selectedBulbs?.each { dni ->
|
selectedBulbs?.each { dni ->
|
||||||
@@ -308,11 +343,7 @@ def addBulbs() {
|
|||||||
if (bulbs instanceof java.util.Map) {
|
if (bulbs instanceof java.util.Map) {
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||||
if (newHueBulb != null) {
|
if (newHueBulb != null) {
|
||||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) {
|
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
|
||||||
} else {
|
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
|
||||||
}
|
|
||||||
log.debug "created ${d.displayName} with id $dni"
|
log.debug "created ${d.displayName} with id $dni"
|
||||||
d.refresh()
|
d.refresh()
|
||||||
} else {
|
} else {
|
||||||
@@ -321,16 +352,15 @@ def addBulbs() {
|
|||||||
} else {
|
} else {
|
||||||
//backwards compatable
|
//backwards compatable
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||||
d.refresh()
|
d.refresh()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||||
if (bulbs instanceof java.util.Map) {
|
if (bulbs instanceof java.util.Map) {
|
||||||
|
// Update device type if incorrect
|
||||||
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") {
|
upgradeDeviceType(d, newHueBulb?.value?.type)
|
||||||
d.setDeviceType("Hue Lux Bulb")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,7 +502,7 @@ def locationHandler(evt) {
|
|||||||
def bulbs = getHueBulbs()
|
def bulbs = getHueBulbs()
|
||||||
log.debug "Adding bulbs to state!"
|
log.debug "Adding bulbs to state!"
|
||||||
body.each { k,v ->
|
body.each { k,v ->
|
||||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub]
|
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:parsedEvent.hub]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -602,6 +632,20 @@ def parse(childDevice, description) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def hubVerification(bodytext) {
|
||||||
|
log.trace "Bridge sent back description.xml for verification"
|
||||||
|
def body = new XmlSlurper().parseText(bodytext)
|
||||||
|
if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
|
||||||
|
def bridges = getHueBridges()
|
||||||
|
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||||
|
if (bridge) {
|
||||||
|
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||||
|
} else {
|
||||||
|
log.error "/description.xml returned a bridge that didn't exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def on(childDevice) {
|
def on(childDevice) {
|
||||||
log.debug "Executing 'on'"
|
log.debug "Executing 'on'"
|
||||||
put("lights/${getId(childDevice)}/state", [on: true])
|
put("lights/${getId(childDevice)}/state", [on: true])
|
||||||
@@ -642,35 +686,53 @@ def setColorTemperature(childDevice, huesettings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def setColor(childDevice, huesettings) {
|
def setColor(childDevice, huesettings) {
|
||||||
log.debug "Executing 'setColor($huesettings)'"
|
log.debug "Executing 'setColor($huesettings)'"
|
||||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
|
||||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
|
||||||
def alert = huesettings.alert ? huesettings.alert : "none"
|
|
||||||
def transition = huesettings.transition ? huesettings.transition : 4
|
|
||||||
|
|
||||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
def value = [:]
|
||||||
if (huesettings.level != null) {
|
def hue = null
|
||||||
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
def sat = null
|
||||||
value.on = value.bri > 0
|
def xy = null
|
||||||
}
|
|
||||||
|
|
||||||
if (huesettings.switch) {
|
if (huesettings.hex != null) {
|
||||||
value.on = huesettings.switch == "on"
|
value.xy = getHextoXY(huesettings.hex)
|
||||||
}
|
} else {
|
||||||
|
if (huesettings.hue != null)
|
||||||
|
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||||
|
if (huesettings.saturation != null)
|
||||||
|
value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||||
|
}
|
||||||
|
|
||||||
log.debug "sending command $value"
|
// Default behavior is to turn light on
|
||||||
put("lights/${getId(childDevice)}/state", value)
|
value.on = true
|
||||||
|
|
||||||
|
if (huesettings.level != null) {
|
||||||
|
if (huesettings.level <= 0)
|
||||||
|
value.on = false
|
||||||
|
else if (huesettings.level == 1)
|
||||||
|
value.bri = 1
|
||||||
|
else
|
||||||
|
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||||
|
}
|
||||||
|
value.alert = huesettings.alert ? huesettings.alert : "none"
|
||||||
|
value.transition = huesettings.transition ? huesettings.transition : 4
|
||||||
|
|
||||||
|
// Make sure to turn off light if requested
|
||||||
|
if (huesettings.switch == "off")
|
||||||
|
value.on = false
|
||||||
|
|
||||||
|
log.debug "sending command $value"
|
||||||
|
put("lights/${getId(childDevice)}/state", value)
|
||||||
|
return "Color set to $value"
|
||||||
}
|
}
|
||||||
|
|
||||||
def nextLevel(childDevice) {
|
def nextLevel(childDevice) {
|
||||||
def level = device.latestValue("level") as Integer ?: 0
|
def level = device.latestValue("level") as Integer ?: 0
|
||||||
if (level < 100) {
|
if (level < 100) {
|
||||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||||
}
|
} else {
|
||||||
else {
|
level = 25
|
||||||
level = 25
|
}
|
||||||
}
|
setLevel(childDevice,level)
|
||||||
setLevel(childDevice,level)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getId(childDevice) {
|
private getId(childDevice) {
|
||||||
@@ -743,6 +805,57 @@ private getBridgeIP() {
|
|||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHextoXY(String colorStr) {
|
||||||
|
// For the hue bulb the corners of the triangle are:
|
||||||
|
// -Red: 0.675, 0.322
|
||||||
|
// -Green: 0.4091, 0.518
|
||||||
|
// -Blue: 0.167, 0.04
|
||||||
|
|
||||||
|
def cred = Integer.valueOf( colorStr.substring( 1, 3 ), 16 )
|
||||||
|
def cgreen = Integer.valueOf( colorStr.substring( 3, 5 ), 16 )
|
||||||
|
def cblue = Integer.valueOf( colorStr.substring( 5, 7 ), 16 )
|
||||||
|
|
||||||
|
double[] normalizedToOne = new double[3];
|
||||||
|
normalizedToOne[0] = (cred / 255);
|
||||||
|
normalizedToOne[1] = (cgreen / 255);
|
||||||
|
normalizedToOne[2] = (cblue / 255);
|
||||||
|
float red, green, blue;
|
||||||
|
|
||||||
|
// Make red more vivid
|
||||||
|
if (normalizedToOne[0] > 0.04045) {
|
||||||
|
red = (float) Math.pow(
|
||||||
|
(normalizedToOne[0] + 0.055) / (1.0 + 0.055), 2.4);
|
||||||
|
} else {
|
||||||
|
red = (float) (normalizedToOne[0] / 12.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make green more vivid
|
||||||
|
if (normalizedToOne[1] > 0.04045) {
|
||||||
|
green = (float) Math.pow((normalizedToOne[1] + 0.055) / (1.0 + 0.055), 2.4);
|
||||||
|
} else {
|
||||||
|
green = (float) (normalizedToOne[1] / 12.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make blue more vivid
|
||||||
|
if (normalizedToOne[2] > 0.04045) {
|
||||||
|
blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4);
|
||||||
|
} else {
|
||||||
|
blue = (float) (normalizedToOne[2] / 12.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
float X = (float) (red * 0.649926 + green * 0.103455 + blue * 0.197109);
|
||||||
|
float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598);
|
||||||
|
float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763);
|
||||||
|
|
||||||
|
float x = (X != 0 ? X / (X + Y + Z) : 0);
|
||||||
|
float y = (Y != 0 ? Y / (X + Y + Z) : 0);
|
||||||
|
|
||||||
|
double[] xy = new double[2];
|
||||||
|
xy[0] = x;
|
||||||
|
xy[1] = y;
|
||||||
|
return xy;
|
||||||
|
}
|
||||||
|
|
||||||
private Integer convertHexToInt(hex) {
|
private Integer convertHexToInt(hex) {
|
||||||
Integer.parseInt(hex,16)
|
Integer.parseInt(hex,16)
|
||||||
}
|
}
|
||||||
@@ -752,7 +865,7 @@ def convertBulbListToMap() {
|
|||||||
if (state.bulbs instanceof java.util.List) {
|
if (state.bulbs instanceof java.util.List) {
|
||||||
def map = [:]
|
def map = [:]
|
||||||
state.bulbs.unique {it.id}.each { bulb ->
|
state.bulbs.unique {it.id}.each { bulb ->
|
||||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]]
|
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]]
|
||||||
}
|
}
|
||||||
state.bulbs = map
|
state.bulbs = map
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* Vacation Lighting Director
|
* Vacation Lighting Director
|
||||||
*
|
*
|
||||||
* Version 2.4 - Added information paragraphs
|
* Version 2.5 - Moved scheduling over to Cron and added time as a trigger.
|
||||||
|
* Cleaned up formatting and some typos.
|
||||||
|
* Updated license.
|
||||||
|
* Made people option optional
|
||||||
|
* Added sttement to unschedule on mode change if people option is not selected
|
||||||
|
*
|
||||||
|
* Version 2.4 - Added information paragraphs
|
||||||
*
|
*
|
||||||
* Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
|
* Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
|
||||||
*
|
*
|
||||||
* Copyright 2015 Tim Slagle
|
* Copyright 2016 Tim Slagle
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -51,8 +57,7 @@ def pageSetup() {
|
|||||||
return dynamicPage(pageProperties) {
|
return dynamicPage(pageProperties) {
|
||||||
section(""){
|
section(""){
|
||||||
paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
|
paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
|
||||||
"Please use each othe the sections below to setup the different preferences to your liking. " +
|
"Please use each of the the sections below to setup the different preferences to your liking. "
|
||||||
"I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. "
|
|
||||||
}
|
}
|
||||||
section("Setup Menu") {
|
section("Setup Menu") {
|
||||||
href "Setup", title: "Setup", description: "", state:greyedOut()
|
href "Setup", title: "Setup", description: "", state:greyedOut()
|
||||||
@@ -70,7 +75,7 @@ def Setup() {
|
|||||||
def newMode = [
|
def newMode = [
|
||||||
name: "newMode",
|
name: "newMode",
|
||||||
type: "mode",
|
type: "mode",
|
||||||
title: "Which?",
|
title: "Modes",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
required: true
|
required: true
|
||||||
]
|
]
|
||||||
@@ -96,14 +101,6 @@ def Setup() {
|
|||||||
required: true,
|
required: true,
|
||||||
]
|
]
|
||||||
|
|
||||||
def people = [
|
|
||||||
name: "people",
|
|
||||||
type: "capability.presenceSensor",
|
|
||||||
title: "If these people are home do not change light status",
|
|
||||||
required: true,
|
|
||||||
multiple: true
|
|
||||||
]
|
|
||||||
|
|
||||||
def pageName = "Setup"
|
def pageName = "Setup"
|
||||||
|
|
||||||
def pageProperties = [
|
def pageProperties = [
|
||||||
@@ -116,10 +113,11 @@ def Setup() {
|
|||||||
|
|
||||||
section(""){
|
section(""){
|
||||||
paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
|
paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
|
||||||
paragraph "you are away. All of these settings are required in order for the simulator to run correctly."
|
"you are away. All of these settings are required in order for the simulator to run correctly."
|
||||||
}
|
}
|
||||||
section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") {
|
section("Simulator Triggers") {
|
||||||
input newMode
|
input newMode
|
||||||
|
href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
|
||||||
}
|
}
|
||||||
section("Light switches to turn on/off") {
|
section("Light switches to turn on/off") {
|
||||||
input switches
|
input switches
|
||||||
@@ -130,9 +128,6 @@ def Setup() {
|
|||||||
section("Number of active lights at any given time") {
|
section("Number of active lights at any given time") {
|
||||||
input number_of_active_lights
|
input number_of_active_lights
|
||||||
}
|
}
|
||||||
section("People") {
|
|
||||||
input people
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -163,17 +158,28 @@ def Settings() {
|
|||||||
nextPage: "pageSetup"
|
nextPage: "pageSetup"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def people = [
|
||||||
|
name: "people",
|
||||||
|
type: "capability.presenceSensor",
|
||||||
|
title: "If these people are home do not change light status",
|
||||||
|
required: false,
|
||||||
|
multiple: true
|
||||||
|
]
|
||||||
|
|
||||||
return dynamicPage(pageProperties) {
|
return dynamicPage(pageProperties) {
|
||||||
|
|
||||||
section(""){
|
section(""){
|
||||||
paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " +
|
paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " +
|
||||||
paragraph "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change."
|
"as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change."
|
||||||
}
|
}
|
||||||
section("Delay to start simulator") {
|
section("Delay to start simulator") {
|
||||||
input falseAlarmThreshold
|
input falseAlarmThreshold
|
||||||
}
|
}
|
||||||
|
section("People") {
|
||||||
|
paragraph "Not using this setting may cause some lights to remain on when you arrive home"
|
||||||
|
input people
|
||||||
|
}
|
||||||
section("More options") {
|
section("More options") {
|
||||||
href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true
|
|
||||||
input days
|
input days
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,9 +187,24 @@ def Settings() {
|
|||||||
|
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
|
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
|
||||||
section {
|
section {
|
||||||
input "starting", "time", title: "Starting", required: false
|
input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
|
||||||
input "ending", "time", title: "Ending", required: false
|
if (startTimeType in ["sunrise","sunset"]) {
|
||||||
|
input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input "starting", "time", title: "Start time", required: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
section {
|
||||||
|
input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
|
||||||
|
if (endTimeType in ["sunrise","sunset"]) {
|
||||||
|
input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input "ending", "time", title: "End time", required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
@@ -201,10 +222,13 @@ def initialize(){
|
|||||||
if (newMode != null) {
|
if (newMode != null) {
|
||||||
subscribe(location, modeChangeHandler)
|
subscribe(location, modeChangeHandler)
|
||||||
}
|
}
|
||||||
|
if (starting != null) {
|
||||||
|
schedule(starting, modeChangeHandler)
|
||||||
|
}
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
}
|
}
|
||||||
|
|
||||||
def modeChangeHandler(evt) {
|
def modeChangeHandler(evt) {
|
||||||
log.debug "Mode change to: ${evt.value}"
|
|
||||||
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60
|
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60
|
||||||
runIn(delay, scheduleCheck)
|
runIn(delay, scheduleCheck)
|
||||||
}
|
}
|
||||||
@@ -212,49 +236,55 @@ def modeChangeHandler(evt) {
|
|||||||
|
|
||||||
//Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
|
//Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
|
||||||
def scheduleCheck(evt) {
|
def scheduleCheck(evt) {
|
||||||
if(allOk){
|
if(allOk){
|
||||||
log.debug("Running")
|
log.debug("Running")
|
||||||
// turn off all the switches
|
// turn off all the switches
|
||||||
switches.off()
|
switches.off()
|
||||||
|
|
||||||
// grab a random switch
|
// grab a random switch
|
||||||
def random = new Random()
|
def random = new Random()
|
||||||
def inactive_switches = switches
|
def inactive_switches = switches
|
||||||
for (int i = 0 ; i < number_of_active_lights ; i++) {
|
for (int i = 0 ; i < number_of_active_lights ; i++) {
|
||||||
// if there are no inactive switches to turn on then let's break
|
// if there are no inactive switches to turn on then let's break
|
||||||
if (inactive_switches.size() == 0){
|
if (inactive_switches.size() == 0){
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab a random switch and turn it on
|
||||||
|
def random_int = random.nextInt(inactive_switches.size())
|
||||||
|
inactive_switches[random_int].on()
|
||||||
|
|
||||||
|
// then remove that switch from the pool off switches that can be turned on
|
||||||
|
inactive_switches.remove(random_int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-run again when the frequency demands it
|
||||||
|
schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
|
||||||
}
|
}
|
||||||
|
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
|
||||||
// grab a random switch and turn it on
|
else if (modeOk) {
|
||||||
def random_int = random.nextInt(inactive_switches.size())
|
log.debug("mode OK. Running again")
|
||||||
inactive_switches[random_int].on()
|
switches.off()
|
||||||
|
}
|
||||||
// then remove that switch from the pool off switches that can be turned on
|
//if none is ok turn off frequency check and turn off lights.
|
||||||
inactive_switches.remove(random_int)
|
else {
|
||||||
}
|
if(people){
|
||||||
|
//don't turn off lights if anyone is home
|
||||||
// re-run again when the frequency demands it
|
if(someoneIsHome()){
|
||||||
runIn(frequency_minutes * 60, scheduleCheck)
|
log.debug("Stopping Check for Light")
|
||||||
}
|
unschedule()
|
||||||
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
|
}
|
||||||
else if (modeOk) {
|
else{
|
||||||
log.debug("mode OK. Running again")
|
log.debug("Stopping Check for Light and turning off all lights")
|
||||||
runIn(frequency_minutes * 60, scheduleCheck)
|
switches.off()
|
||||||
switches.off()
|
unschedule()
|
||||||
}
|
}
|
||||||
//if none is ok turn off frequency check and turn off lights.
|
|
||||||
else if(people){
|
|
||||||
//don't turn off lights if anyone is home
|
|
||||||
if(someoneIsHome()){
|
|
||||||
log.debug("Stopping Check for Light")
|
|
||||||
}
|
}
|
||||||
else{
|
else if (!modeOk) {
|
||||||
log.debug("Stopping Check for Light and turning off all lights")
|
unschedule()
|
||||||
switches.off()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//below is used to check restrictions
|
//below is used to check restrictions
|
||||||
@@ -286,26 +316,6 @@ private getDaysOk() {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTimeOk() {
|
|
||||||
def result = true
|
|
||||||
if (starting && ending) {
|
|
||||||
def currTime = now()
|
|
||||||
def start = timeToday(starting).time
|
|
||||||
def stop = timeToday(ending).time
|
|
||||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (starting){
|
|
||||||
result = currTime >= start
|
|
||||||
}
|
|
||||||
else if (ending){
|
|
||||||
result = currTime <= stop
|
|
||||||
}
|
|
||||||
|
|
||||||
log.trace "timeOk = $result"
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHomeIsEmpty() {
|
private getHomeIsEmpty() {
|
||||||
def result = true
|
def result = true
|
||||||
|
|
||||||
@@ -330,25 +340,59 @@ private getSomeoneIsHome() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTimeOk() {
|
||||||
//gets the label for time restriction. Label phrasing changes depending on if there is both start and stop times or just one start/stop time.
|
def result = true
|
||||||
def getTimeLabel(starting, ending){
|
def start = timeWindowStart()
|
||||||
|
def stop = timeWindowStop()
|
||||||
def timeLabel = "Tap to set"
|
if (start && stop && location.timeZone) {
|
||||||
|
result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
|
||||||
if(starting && ending){
|
}
|
||||||
timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending)
|
log.trace "timeOk = $result"
|
||||||
}
|
result
|
||||||
else if (starting) {
|
}
|
||||||
timeLabel = "Start at" + " " + hhmm(starting)
|
|
||||||
}
|
private timeWindowStart() {
|
||||||
else if(ending){
|
def result = null
|
||||||
timeLabel = "End at" + hhmm(ending)
|
if (startTimeType == "sunrise") {
|
||||||
}
|
result = location.currentState("sunriseTime")?.dateValue
|
||||||
timeLabel
|
if (result && startTimeOffset) {
|
||||||
|
result = new Date(result.time + Math.round(startTimeOffset * 60000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (startTimeType == "sunset") {
|
||||||
|
result = location.currentState("sunsetTime")?.dateValue
|
||||||
|
if (result && startTimeOffset) {
|
||||||
|
result = new Date(result.time + Math.round(startTimeOffset * 60000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (starting && location.timeZone) {
|
||||||
|
result = timeToday(starting, location.timeZone)
|
||||||
|
}
|
||||||
|
log.trace "timeWindowStart = ${result}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private timeWindowStop() {
|
||||||
|
def result = null
|
||||||
|
if (endTimeType == "sunrise") {
|
||||||
|
result = location.currentState("sunriseTime")?.dateValue
|
||||||
|
if (result && endTimeOffset) {
|
||||||
|
result = new Date(result.time + Math.round(endTimeOffset * 60000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (endTimeType == "sunset") {
|
||||||
|
result = location.currentState("sunsetTime")?.dateValue
|
||||||
|
if (result && endTimeOffset) {
|
||||||
|
result = new Date(result.time + Math.round(endTimeOffset * 60000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ending && location.timeZone) {
|
||||||
|
result = timeToday(ending, location.timeZone)
|
||||||
|
}
|
||||||
|
log.trace "timeWindowStop = ${result}"
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
//fomrats time to readable format for time label
|
|
||||||
private hhmm(time, fmt = "h:mm a")
|
private hhmm(time, fmt = "h:mm a")
|
||||||
{
|
{
|
||||||
def t = timeToday(time, location.timeZone)
|
def t = timeToday(time, location.timeZone)
|
||||||
@@ -357,6 +401,41 @@ private hhmm(time, fmt = "h:mm a")
|
|||||||
f.format(t)
|
f.format(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private timeIntervalLabel() {
|
||||||
|
def start = ""
|
||||||
|
switch (startTimeType) {
|
||||||
|
case "time":
|
||||||
|
if (ending) {
|
||||||
|
start += hhmm(starting)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "sunrise":
|
||||||
|
case "sunset":
|
||||||
|
start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
|
||||||
|
if (startTimeOffset) {
|
||||||
|
start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
def finish = ""
|
||||||
|
switch (endTimeType) {
|
||||||
|
case "time":
|
||||||
|
if (ending) {
|
||||||
|
finish += hhmm(ending)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "sunrise":
|
||||||
|
case "sunset":
|
||||||
|
finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
|
||||||
|
if (endTimeOffset) {
|
||||||
|
finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start && finish ? "${start} to ${finish}" : ""
|
||||||
|
}
|
||||||
|
|
||||||
//sets complete/not complete for the setup section on the main dynamic page
|
//sets complete/not complete for the setup section on the main dynamic page
|
||||||
def greyedOut(){
|
def greyedOut(){
|
||||||
def result = ""
|
def result = ""
|
||||||
@@ -369,16 +448,7 @@ def greyedOut(){
|
|||||||
//sets complete/not complete for the settings section on the main dynamic page
|
//sets complete/not complete for the settings section on the main dynamic page
|
||||||
def greyedOutSettings(){
|
def greyedOutSettings(){
|
||||||
def result = ""
|
def result = ""
|
||||||
if (starting || ending || days || falseAlarmThreshold) {
|
if (people || days || falseAlarmThreshold ) {
|
||||||
result = "complete"
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
//sets complete/not complete for time restriction section in settings
|
|
||||||
def greyedOutTime(starting, ending){
|
|
||||||
def result = ""
|
|
||||||
if (starting || ending) {
|
|
||||||
result = "complete"
|
result = "complete"
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
|
|||||||
Reference in New Issue
Block a user