Compare commits

...

30 Commits

Author SHA1 Message Date
Lars Finander
1736caebfe Merge pull request #716 from larsfinander/DVCSMP-1597-Hue-remove-temp-control
DVCSMP-1597 Bloom and Strip lights need temperature control removed
2016-03-29 14:08:45 -07:00
Lars Finander
0fa363fa1a Merge pull request #715 from larsfinander/DVCSMP-1672-hue-lux-icon-color
DVCSMP-1672 Phillips HUE: Mismatched on button  color for hue lux
2016-03-29 13:42:22 -07:00
Lars Finander
0c5840087b DVCSMP-1597 Philips HUE: Bloom and Strip lights need temperature control removed
-Added new device type for Hue lights that have color control but no temperature control (Bloom/Strip)
-Add missing event to setLevel
2016-03-29 11:26:25 -07:00
Lars Finander
a6ee53641f DVCSMP-1672 Phillips HUE: Mismatched in on button light color for hue lux
-Changed color from green to blue to match other Hue lights
2016-03-28 20:51:32 -07:00
Duncan McKee
c6818c8c2b Add new Ecolink Motion MSR to door/window retyping code 2016-03-28 13:29:51 -04:00
Vinay Rao
6ac174c2f3 Merge pull request #707 from SmartThingsCommunity/staging
Rolling down build changes to master from staging
2016-03-25 16:51:58 -07:00
Vinay Rao
eb8d5ed4c9 Merge pull request #706 from SmartThingsCommunity/production
Rolling down build changes to staging from production
2016-03-25 16:47:12 -07:00
Vinay Rao
7b5d618de8 Merge pull request #705 from jeff-blaisdell/fix-production
Update public repo to new build setup.
2016-03-25 16:45:47 -07:00
Jeff Blaisdell
c024e09fb8 Update public repo to new build setup. 2016-03-25 18:33:15 -05:00
Amol Mundayoor
e5841fb3cb Merge pull request #702 from larsfinander/DVCSMP-1667-Update-Hue-user-creation
DVCSMP-1667 Authentication needs to be updated for Philips Hue
2016-03-25 11:28:19 -07:00
Lars Finander
805b870447 DVCSMP-1667 Authentication needs to be updated for Philips Hue 2016-03-25 10:12:48 -07:00
Vinay Rao
fe92f7ad19 Merge pull request #698 from SmartThingsCommunity/master
Rolling up master to staging
2016-03-24 13:25:36 -07:00
Vinay Rao
10245315ee Merge pull request #697 from SmartThingsCommunity/staging
Rolling down staging commits to master
2016-03-24 13:22:33 -07:00
Vinay Rao
0b239d4686 Merge pull request #696 from SmartThingsCommunity/revert-668-staging_remove_hue
Revert "Revert "DVCSMP-1615 & DEVC-372""
2016-03-24 13:19:48 -07:00
Vinay Rao
9374290d64 Revert "Revert "DVCSMP-1615 & DEVC-372"" 2016-03-24 13:12:23 -07:00
Lars Finander
53406ada8e Merge pull request #690 from larsfinander/DVCSMP-1615-Improve-Hue-error-handling
DVCSMP-1615 Hue setColor throws exceptions when missing parameters
2016-03-23 18:57:32 -07:00
Lars Finander
ffd0dd1545 DVCSMP-1615 Hue setColor throws exceptions when missing parameters
-DVCSMP-1637 Color picker set the light on but status not updated on the detail page
-DVCSMP-1638 Temperature control selection set the light on but status not updated on the detail page
-Added a lot of error checking for input parameters
-Fixed some data parsing exceptions
2016-03-23 16:10:11 -07:00
Vinay Rao
5c1236a21a Merge pull request #693 from SmartThingsCommunity/staging
[DVCSMP-1657] Syncing down changes from staging to master
2016-03-23 15:24:13 -07:00
Vinay Rao
1e27dc1824 Merge pull request #674 from workingmonk/feature/zigbee_library_refactor
[DPROT-20] ZigBee Refactor
2016-03-23 11:44:42 -07:00
Tom Manley
4bf3679942 Merge pull request #376 from tpmanley/feature/zigbee_refactor
[DPROT-20] Feature/zigbee refactor
2016-03-23 09:51:37 -05:00
Tom Manley
c714720578 Update zigbee-lock, zigbee-dimmer and zigbee-dimmer-power to use ZB lib API
https://smartthings.atlassian.net/browse/DPROT-20
2016-03-23 09:51:02 -05:00
Vinay Rao
281fc939ac refactoring code to get it inline with the new zigbee apis 2016-03-23 00:27:23 -07:00
tslagle13
03c2dec425 Merge pull request #667 from tslagle13/fixes-to-vacation-lighting-director
Update vacation-lighting-director.groovy
2016-03-22 17:38:42 -07:00
tslagle13
38d0ca6170 Update vacation-lighting-director.groovy
* 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
2016-03-22 13:00:49 -07:00
Duncan McKee
836dd608c6 Merge pull request #539 from SmartThingsCommunity/DEVFRWK-78
Fix First Alert smoke alarm check-in handling
2016-03-21 22:33:07 -04:00
Juan Pablo Risso
43e4db28eb Merge pull request #660 from juano2310/hue_discover
Added hubVerification()
2016-03-21 16:21:33 -04:00
juano2310
df421a51ac Added hubVerification()
If the hub exist as a thing parsing the response from description.xml
was lost. Now it is sent back to the parent were the device can be
marked as verified if the modelName starts with "Philips hue bridge"
2016-03-21 14:38:24 -04:00
Duncan McKee
3affdd21fc Merge pull request #540 from SmartThingsCommunity/DVCSMP-1438
Z-Wave Motion Sensor: fix MissingMethodException on NotificationReport
2016-03-21 12:21:30 -04:00
Duncan McKee
e61be4ff9c DVCSMP-1438: Fix Z-Wave Motion handling of Notification Report 2016-02-22 14:55:58 -05:00
Duncan McKee
6123fbeea5 DEVFRWK-78 Fix First Alert smoke alarm check-in handling 2016-02-22 13:52:56 -05:00
17 changed files with 779 additions and 366 deletions

View File

@@ -7,9 +7,10 @@ apply plugin: 'smartthings-hipchat'
buildscript {
dependencies {
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.3"
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.6"
}
repositories {
mavenLocal()
jcenter()
maven {
credentials {

View File

@@ -15,13 +15,13 @@ deployment:
develop:
branch: master
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 hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
stage:
branch: staging
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 hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD

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

View File

@@ -17,16 +17,13 @@ metadata {
tiles(scale: 2) {
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"
}
}
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
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) {
state "default", label:'SN: ${currentValue}'
}
@@ -34,7 +31,7 @@ metadata {
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
}
main (["icon"])
main (["rich-control"])
details(["rich-control", "networkAddress"])
}
}
@@ -75,6 +72,7 @@ def parse(description) {
}
else if (contentType?.contains("xml")) {
log.debug "HUE BRIDGE ALREADY PRESENT"
parent.hubVerification(device.hub.id, msg.body)
}
}
}

View File

@@ -1,8 +1,11 @@
/**
* Hue Bulb
*
* Philips Hue Type "Extended Color Light"
*
* Author: SmartThings
*/
// for the UI
metadata {
// Automatically generated. Make future change here.
@@ -27,10 +30,10 @@ metadata {
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:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
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)"
@@ -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)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
@@ -60,29 +57,12 @@ metadata {
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
}
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"
}
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"])
}
}
@@ -91,11 +71,13 @@ metadata {
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}")
}
@@ -125,62 +107,104 @@ void nextLevel() {
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
parent.setLevel(this, percent)
sendEvent(name: "level", value: 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'"
parent.setSaturation(this, percent)
sendEvent(name: "saturation", value: 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'"
parent.setHue(this, percent)
sendEvent(name: "hue", value: 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"
parent.setColor(this, value)
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
if (value.hex) { sendEvent(name: "color", value: value.hex)}
if (value.level) { sendEvent(name: "level", value: value.level)}
if (value.switch) { sendEvent(name: "switch", value: value.switch)}
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, hex:"#90C638", saturation:56, hue:23]
log.debug "Executing 'reset'"
def value = [level:100, saturation:56, hue:23]
setAdjustedColor(value)
parent.poll()
parent.poll()
}
void setAdjustedColor(value) {
if (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) {
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()
log.debug "Executing 'refresh'"
parent.manualRefresh()
}
def adjustOutgoingHue(percent) {
@@ -199,3 +223,14 @@ def adjustOutgoingHue(percent) {
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
}
}

View File

@@ -1,6 +1,8 @@
/**
* Hue Lux Bulb
*
* Philips Hue Type "Dimmable Light"
*
* Author: SmartThings
*/
// for the UI
@@ -23,10 +25,10 @@ metadata {
tiles(scale: 2) {
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
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)"
@@ -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)") {
state "level", action:"switch level.setLevel"
}
@@ -51,7 +46,7 @@ metadata {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
main(["rich-control"])
details(["rich-control", "refresh"])
}
}
@@ -75,19 +70,24 @@ def parse(description) {
// handle commands
void on() {
parent.on(this)
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
void off() {
parent.off(this)
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent)
if (percent != null && percent >= 0 && percent <= 100) {
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent)
sendEvent(name: "switch", value: "on")
} else {
log.warn "$percent is not 0-100"
}
}
void refresh() {

View File

@@ -56,21 +56,17 @@ metadata {
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
def event = zigbee.getEvent(description)
if (event) {
log.info event
if (event.name == "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
sendEvent(name: "power", value: powerValue)
event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(event)
}
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
sendEvent(event)
}
}
else {

View File

@@ -51,15 +51,9 @@ metadata {
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
def event = zigbee.getEvent(description)
if (event) {
sendEvent(event)
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -83,32 +83,19 @@ def uninstalled() {
}
def configure() {
/*
def cmds =
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
"${TYPE_ENUM8}", 0, 3600, "{01}") +
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
"${TYPE_U8}", 600, 21600, "{01}")
*/
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",
]
zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,
TYPE_ENUM8, 0, 3600, null) +
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
TYPE_U8, 600, 21600, 0x01)
log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config
}
def refresh() {
def cmds =
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) +
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
log.info "refresh() --- cmds: $cmds"
return cmds
}
@@ -121,34 +108,27 @@ def parse(String description) {
map = parseReportAttributeMessage(description)
}
log.debug "parse() --- Parse returned $map"
def result = map ? createEvent(map) : null
log.debug "parse() --- returned: $result"
return result
}
// Lock capability commands
def lock() {
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
//log.info "lock() -- cmds: $cmds"
//return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR)
log.info "lock() -- cmds: $cmds"
return cmds
}
def unlock() {
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
//log.info "unlock() -- cmds: $cmds"
//return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR)
log.info "unlock() -- cmds: $cmds"
return cmds
}
// Private methods
private Map parseReportAttributeMessage(String description) {
log.trace "parseReportAttributeMessage() --- description: $description"
Map descMap = zigbee.parseDescriptionAsMap(description)
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
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
resultMap.value = Integer.parseInt(descMap.value, 16)
}
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
}
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
def value = Integer.parseInt(descMap.value, 16)
def linkText = getLinkText(device)
resultMap.name = "lock"
resultMap.putAll([0:["value":"unknown",
"descriptionText":"Not fully locked"],
1:["value":"locked"],
2:["value":"unlocked"]].get(value,
["value":"unknown",
"descriptionText":"Unknown lock state"]))
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
if (value == 0) {
resultMap.value = "unknown"
resultMap.descriptionText = "${linkText} is not fully locked"
} else if (value == 1) {
resultMap.value = "locked"
resultMap.descriptionText = "${linkText} is locked"
} 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 {
log.debug "parseReportAttributeMessage() --- ignoring attribute"

View File

@@ -51,22 +51,15 @@ metadata {
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def event = zigbee.getEvent(description)
if (event) {
if (event.name == "power") {
def powerValue
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
sendEvent(name: "power", value: powerValue)
}
powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue)
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
sendEvent(event)
}
}
else {

View File

@@ -53,16 +53,9 @@ metadata {
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
def event = zigbee.getEvent(description)
if (event) {
sendEvent(event)
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -73,16 +73,9 @@ metadata {
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getKnownDescription(description)
if (finalResult) {
log.info finalResult
if (finalResult.type == "update") {
log.info "$device updates: ${finalResult.value}"
}
else {
sendEvent(name: finalResult.type, value: finalResult.value)
}
def event = zigbee.getEvent(description)
if (event) {
sendEvent(event)
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -245,6 +245,7 @@ def retypeBasedOnMSR() {
break
case "011F-0001-0001": // Schlage motion
case "014A-0001-0001": // Ecolink motion
case "014A-0004-0001": // Ecolink motion +
case "0060-0001-0002": // Everspring SP814
case "0060-0001-0003": // Everspring HSP02
case "011A-0601-0901": // Enerwave ZWN-BPC

View File

@@ -57,7 +57,7 @@ def parse(String description) {
return result
}
def sensorValueEvent(Short value) {
def sensorValueEvent(value) {
if (value) {
createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
} else {
@@ -94,24 +94,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
{
def result = []
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)
} else if (cmd.event == 0x00) {
result << sensorValueEvent(0)
} else if (cmd.event == 0x03) {
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true)
result << response(zwave.batteryV1.batteryGet())
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
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) {
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 {
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
}

View File

@@ -61,37 +61,44 @@ def parse(String description) {
zwaveEvent(cmd, results)
}
}
// log.debug "\"$description\" parsed to ${results.inspect()}"
log.debug "'$description' parsed to ${results.inspect()}"
return results
}
def createSmokeOrCOEvents(name, results) {
def text = null
if (name == "smoke") {
text = "$device.displayName smoke was detected!"
// these are displayed:false because the composite event is the one we want to see in the app
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
} else if (name == "carbonMonoxide") {
text = "$device.displayName carbon monoxide was detected!"
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
} else if (name == "tested") {
text = "$device.displayName was tested"
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
} else if (name == "smokeClear") {
text = "$device.displayName smoke is clear"
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
name = "clear"
} else if (name == "carbonMonoxideClear") {
text = "$device.displayName carbon monoxide is clear"
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
name = "clear"
} else if (name == "testClear") {
text = "$device.displayName smoke is clear"
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
name = "clear"
switch (name) {
case "smoke":
text = "$device.displayName smoke was detected!"
// these are displayed:false because the composite event is the one we want to see in the app
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
break
case "carbonMonoxide":
text = "$device.displayName carbon monoxide was detected!"
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
break
case "tested":
text = "$device.displayName was tested"
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
break
case "smokeClear":
text = "$device.displayName smoke is clear"
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
name = "clear"
break
case "carbonMonoxideClear":
text = "$device.displayName carbon monoxide is clear"
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
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
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)
break
case 13: // sent every hour -- not sure what this means, just a wake up notification?
if (cmd.alarmLevel != 255) {
results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true)
if (cmd.alarmLevel == 255) {
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
@@ -127,9 +136,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
}
// Check battery if we don't have a recent battery event
def prevBattery = device.currentState("battery")
if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) {
results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())
if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) {
results << response(zwave.batteryV1.batteryGet())
}
break
default:
@@ -158,12 +166,17 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd,
}
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)
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 map = [ name: "battery", unit: "%" ]
def map = [ name: "battery", unit: "%", isStateChange: true ]
state.lastbatt = now()
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "$device.displayName battery is low!"

View File

@@ -24,7 +24,7 @@ definition(
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
//singleInstance: true
singleInstance: true
)
preferences {
@@ -68,7 +68,7 @@ def bridgeDiscovery(params=[:])
}
//setup.xml request every 3 seconds except on discoveries
if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) {
if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) {
verifyHueBridges()
}
@@ -161,7 +161,7 @@ private sendDeveloperReq() {
headers: [
HOST: host
],
body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}"))
body: [devicetype: "$token-0"]], "${selectedHue}"))
}
private discoverHueBulbs() {
@@ -175,6 +175,7 @@ private discoverHueBulbs() {
}
private verifyHueBridge(String deviceNetworkId, String host) {
log.trace "Verify Hue Bridge $deviceNetworkId"
sendHubCommand(new physicalgraph.device.HubAction([
method: "GET",
path: "/description.xml",
@@ -288,7 +289,7 @@ def bulbListHandler(hub, data = "") {
def object = new groovy.json.JsonSlurper().parseText(data)
object.each { k,v ->
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
@@ -299,6 +300,40 @@ def bulbListHandler(hub, data = "") {
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 bulbs = getHueBulbs()
selectedBulbs?.each { dni ->
@@ -308,11 +343,7 @@ def addBulbs() {
if (bulbs instanceof java.util.Map) {
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
if (newHueBulb != null) {
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) {
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])
}
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
log.debug "created ${d.displayName} with id $dni"
d.refresh()
} else {
@@ -321,16 +352,15 @@ def addBulbs() {
} else {
//backwards compatable
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()
}
} else {
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
if (bulbs instanceof java.util.Map) {
// Update device type if incorrect
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") {
d.setDeviceType("Hue Lux Bulb")
}
upgradeDeviceType(d, newHueBulb?.value?.type)
}
}
}
@@ -472,7 +502,7 @@ def locationHandler(evt) {
def bulbs = getHueBulbs()
log.debug "Adding bulbs to state!"
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) {
log.debug "Executing 'on'"
put("lights/${getId(childDevice)}/state", [on: true])
@@ -642,35 +686,53 @@ def setColorTemperature(childDevice, huesettings) {
}
def setColor(childDevice, 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
log.debug "Executing 'setColor($huesettings)'"
def value = [:]
def hue = null
def sat = null
def xy = null
if (huesettings.hex != null) {
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)
}
// Default behavior is to turn light on
value.on = true
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
if (huesettings.level != null) {
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
value.on = value.bri > 0
}
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
if (huesettings.switch) {
value.on = huesettings.switch == "on"
}
// 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)
log.debug "sending command $value"
put("lights/${getId(childDevice)}/state", value)
return "Color set to $value"
}
def nextLevel(childDevice) {
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(childDevice,level)
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(childDevice,level)
}
private getId(childDevice) {
@@ -743,6 +805,57 @@ private getBridgeIP() {
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) {
Integer.parseInt(hex,16)
}
@@ -752,7 +865,7 @@ def convertBulbListToMap() {
if (state.bulbs instanceof java.util.List) {
def map = [:]
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
}

View File

@@ -1,11 +1,17 @@
/**
* 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
*
* 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
* in compliance with the License. You may obtain a copy of the License at:
@@ -51,8 +57,7 @@ def pageSetup() {
return dynamicPage(pageProperties) {
section(""){
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. " +
"I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. "
"Please use each of the the sections below to setup the different preferences to your liking. "
}
section("Setup Menu") {
href "Setup", title: "Setup", description: "", state:greyedOut()
@@ -70,7 +75,7 @@ def Setup() {
def newMode = [
name: "newMode",
type: "mode",
title: "Which?",
title: "Modes",
multiple: true,
required: true
]
@@ -96,14 +101,6 @@ def Setup() {
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 pageProperties = [
@@ -116,10 +113,11 @@ def Setup() {
section(""){
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))") {
input newMode
section("Simulator Triggers") {
input newMode
href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
}
section("Light switches to turn on/off") {
input switches
@@ -130,9 +128,6 @@ def Setup() {
section("Number of active lights at any given time") {
input number_of_active_lights
}
section("People") {
input people
}
}
}
@@ -162,18 +157,29 @@ def Settings() {
title: "Settings",
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) {
section(""){
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") {
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") {
href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true
input days
}
}
@@ -181,9 +187,24 @@ def Settings() {
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
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() {
@@ -201,10 +222,13 @@ def initialize(){
if (newMode != null) {
subscribe(location, modeChangeHandler)
}
if (starting != null) {
schedule(starting, modeChangeHandler)
}
log.debug "Installed with settings: ${settings}"
}
def modeChangeHandler(evt) {
log.debug "Mode change to: ${evt.value}"
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60
runIn(delay, scheduleCheck)
}
@@ -212,48 +236,54 @@ 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
def scheduleCheck(evt) {
if(allOk){
log.debug("Running")
// turn off all the switches
switches.off()
// grab a random switch
def random = new Random()
def inactive_switches = switches
for (int i = 0 ; i < number_of_active_lights ; i++) {
// if there are no inactive switches to turn on then let's break
if (inactive_switches.size() == 0){
break
if(allOk){
log.debug("Running")
// turn off all the switches
switches.off()
// grab a random switch
def random = new Random()
def inactive_switches = switches
for (int i = 0 ; i < number_of_active_lights ; i++) {
// if there are no inactive switches to turn on then let's break
if (inactive_switches.size() == 0){
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)
}
// 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
runIn(frequency_minutes * 60, scheduleCheck)
}
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
else if (modeOk) {
log.debug("mode OK. Running again")
runIn(frequency_minutes * 60, scheduleCheck)
switches.off()
}
//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")
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
else if (modeOk) {
log.debug("mode OK. Running again")
switches.off()
}
//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")
unschedule()
}
else{
log.debug("Stopping Check for Light and turning off all lights")
switches.off()
unschedule()
}
}
else{
log.debug("Stopping Check for Light and turning off all lights")
switches.off()
else if (!modeOk) {
unschedule()
}
}
}
}
@@ -286,26 +316,6 @@ private getDaysOk() {
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() {
def result = true
@@ -330,25 +340,59 @@ private getSomeoneIsHome() {
return result
}
//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 getTimeLabel(starting, ending){
def timeLabel = "Tap to set"
if(starting && ending){
timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending)
}
else if (starting) {
timeLabel = "Start at" + " " + hhmm(starting)
}
else if(ending){
timeLabel = "End at" + hhmm(ending)
}
timeLabel
private getTimeOk() {
def result = true
def start = timeWindowStart()
def stop = timeWindowStop()
if (start && stop && location.timeZone) {
result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
}
log.trace "timeOk = $result"
result
}
private timeWindowStart() {
def result = null
if (startTimeType == "sunrise") {
result = location.currentState("sunriseTime")?.dateValue
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")
{
def t = timeToday(time, location.timeZone)
@@ -357,6 +401,41 @@ private hhmm(time, fmt = "h:mm a")
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
def greyedOut(){
def result = ""
@@ -369,16 +448,7 @@ def greyedOut(){
//sets complete/not complete for the settings section on the main dynamic page
def greyedOutSettings(){
def result = ""
if (starting || ending || days || falseAlarmThreshold) {
result = "complete"
}
result
}
//sets complete/not complete for time restriction section in settings
def greyedOutTime(starting, ending){
def result = ""
if (starting || ending) {
if (people || days || falseAlarmThreshold ) {
result = "complete"
}
result